Refactor authentication and session management
- Updated Dockerfile to use Python 3.13 and streamline system dependencies installation. - Enhanced user session handling in auth.py, introducing a centralized method for retrieving the current user from the session. - Removed deprecated auth routes and consolidated logic for user authentication and profile management. - Improved error handling and user feedback in redeem.py and teams.py, ensuring users are redirected to login when not authenticated. - Updated requirements.txt to include necessary packages for enhanced security and functionality. - Added phpMyAdmin service to docker-compose for easier database management.
This commit is contained in:
+13
-21
@@ -1,31 +1,23 @@
|
|||||||
# Use Python 3.11 (or whichever version you prefer)
|
FROM python:3.13
|
||||||
FROM python:3.11-slim
|
|
||||||
|
|
||||||
# Create working directory
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install system dependencies needed for mysqlclient
|
# Install system dependencies
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && \
|
||||||
pkg-config \
|
apt-get install -y --no-install-recommends gcc libc-dev default-libmysqlclient-dev && \
|
||||||
default-libmysqlclient-dev \
|
apt-get clean && \
|
||||||
build-essential \
|
rm -rf /var/lib/apt/lists/*
|
||||||
&& apt-get clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Copy requirements first (for caching)
|
# Copy requirements first to leverage Docker cache
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# Explicitly install pymysql (in case it's missing from requirements.txt)
|
# Copy application code
|
||||||
RUN pip install --no-cache-dir pymysql cryptography
|
|
||||||
|
|
||||||
# Copy the rest of the code
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Expose port
|
# Set environment variables
|
||||||
EXPOSE 8000
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
# Run the FastAPI app with Uvicorn
|
# Command to run when container starts
|
||||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--reload"]
|
||||||
|
|||||||
+54
-29
@@ -16,6 +16,7 @@ from passlib.context import CryptContext
|
|||||||
from .db import SessionLocal
|
from .db import SessionLocal
|
||||||
from .templates_config import templates
|
from .templates_config import templates
|
||||||
from . import models
|
from . import models
|
||||||
|
from .dependencies import get_user_from_session
|
||||||
|
|
||||||
# Initialize OAuth
|
# Initialize OAuth
|
||||||
oauth = OAuth()
|
oauth = OAuth()
|
||||||
@@ -53,15 +54,9 @@ def verify_password(plain_password, hashed_password):
|
|||||||
"""Verify password against hash"""
|
"""Verify password against hash"""
|
||||||
return pwd_context.verify(plain_password, hashed_password)
|
return pwd_context.verify(plain_password, hashed_password)
|
||||||
|
|
||||||
def get_current_user(request: Request):
|
def get_current_user(request: Request, db: Session = Depends(get_db)):
|
||||||
"""Get current user from session with improved error handling"""
|
"""Get current user from session"""
|
||||||
try:
|
return get_user_from_session(request, db)
|
||||||
if "session" not in request.scope:
|
|
||||||
return None
|
|
||||||
return request.session.get("user")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error getting user from session: {str(e)}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_gravatar_url(email):
|
def get_gravatar_url(email):
|
||||||
"""Generate a Gravatar URL for the given email"""
|
"""Generate a Gravatar URL for the given email"""
|
||||||
@@ -74,10 +69,12 @@ def require_login(func):
|
|||||||
@wraps(func)
|
@wraps(func)
|
||||||
async def wrapper(request: Request, *args, **kwargs):
|
async def wrapper(request: Request, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
if "session" not in request.scope or not request.session.get("user"):
|
db = next(get_db())
|
||||||
|
user = get_user_from_session(request, db)
|
||||||
|
|
||||||
|
if not user:
|
||||||
# Store the current URL for redirecting after login
|
# Store the current URL for redirecting after login
|
||||||
if "session" in request.scope:
|
request.session["redirect_after_login"] = str(request.url)
|
||||||
request.session["redirect_after_login"] = str(request.url)
|
|
||||||
return RedirectResponse(url="/auth/login", status_code=status.HTTP_302_FOUND)
|
return RedirectResponse(url="/auth/login", status_code=status.HTTP_302_FOUND)
|
||||||
|
|
||||||
# Check if the wrapped function is a coroutine function
|
# Check if the wrapped function is a coroutine function
|
||||||
@@ -96,15 +93,14 @@ def require_admin(func):
|
|||||||
@wraps(func)
|
@wraps(func)
|
||||||
async def wrapper(request: Request, *args, **kwargs):
|
async def wrapper(request: Request, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
if "session" not in request.scope:
|
db = next(get_db())
|
||||||
return RedirectResponse(url="/auth/login", status_code=status.HTTP_302_FOUND)
|
user = get_user_from_session(request, db)
|
||||||
|
|
||||||
user = request.session.get("user")
|
|
||||||
if not user:
|
if not user:
|
||||||
request.session["redirect_after_login"] = str(request.url)
|
request.session["redirect_after_login"] = str(request.url)
|
||||||
return RedirectResponse(url="/auth/login", status_code=status.HTTP_302_FOUND)
|
return RedirectResponse(url="/auth/login", status_code=status.HTTP_302_FOUND)
|
||||||
|
|
||||||
if not user.get("is_admin", False):
|
if not user.is_admin:
|
||||||
return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND)
|
return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND)
|
||||||
|
|
||||||
# Check if the wrapped function is a coroutine function
|
# Check if the wrapped function is a coroutine function
|
||||||
@@ -121,10 +117,10 @@ def require_admin(func):
|
|||||||
# Routes for authentication
|
# Routes for authentication
|
||||||
|
|
||||||
@router.get("/login")
|
@router.get("/login")
|
||||||
async def login_page(request: Request):
|
async def login_page(request: Request, db: Session = Depends(get_db)):
|
||||||
"""Show login page with appropriate authentication options"""
|
"""Show login page with appropriate authentication options"""
|
||||||
# If already logged in, redirect to home
|
# If already logged in, redirect to home
|
||||||
if request.session.get("user"):
|
if get_user_from_session(request, db):
|
||||||
return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND)
|
return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND)
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
@@ -153,17 +149,21 @@ async def login(request: Request, db: Session = Depends(get_db)):
|
|||||||
status_code=status.HTTP_302_FOUND
|
status_code=status.HTTP_302_FOUND
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create user session
|
|
||||||
# Make sure we use the actual is_admin field from the User model
|
# Make sure we use the actual is_admin field from the User model
|
||||||
is_admin_value = False
|
is_admin_value = False
|
||||||
if hasattr(user, "is_admin") and user.is_admin is not None:
|
if hasattr(user, "is_admin") and user.is_admin is not None:
|
||||||
is_admin_value = user.is_admin
|
is_admin_value = user.is_admin
|
||||||
|
|
||||||
|
# Store user info in session - STANDARDIZED APPROACH
|
||||||
|
# Store both the user_id (for new code) and the full user dict (for backward compatibility)
|
||||||
|
request.session["user_id"] = user.id
|
||||||
|
|
||||||
|
# Also store the legacy format for backward compatibility
|
||||||
request.session["user"] = {
|
request.session["user"] = {
|
||||||
"id": user.id,
|
"id": user.id,
|
||||||
"username": user.username,
|
"username": user.username,
|
||||||
"email": user.email,
|
"email": user.email,
|
||||||
"is_admin": is_admin_value, # Use the actual is_admin value from the database
|
"is_admin": is_admin_value,
|
||||||
"picture": get_gravatar_url(user.email),
|
"picture": get_gravatar_url(user.email),
|
||||||
"_permanent": True,
|
"_permanent": True,
|
||||||
"created_at": str(user.created_at)
|
"created_at": str(user.created_at)
|
||||||
@@ -229,15 +229,18 @@ async def oauth_callback(request: Request, db: Session = Depends(get_db)):
|
|||||||
if hasattr(user, "is_admin") and user.is_admin is not None:
|
if hasattr(user, "is_admin") and user.is_admin is not None:
|
||||||
is_admin_value = user.is_admin
|
is_admin_value = user.is_admin
|
||||||
|
|
||||||
# Store user info in session
|
# Store user ID in session (new standardized approach)
|
||||||
|
request.session["user_id"] = user.id
|
||||||
|
|
||||||
|
# Also store legacy user data format
|
||||||
user_data = {
|
user_data = {
|
||||||
"id": user.id,
|
"id": user.id,
|
||||||
"username": user.username,
|
"username": user.username,
|
||||||
"email": user.email,
|
"email": user.email,
|
||||||
"is_active": user.is_active,
|
"is_active": getattr(user, "is_active", True),
|
||||||
"is_admin": is_admin_value, # Set the admin status correctly
|
"is_admin": is_admin_value,
|
||||||
"_permanent": True,
|
"_permanent": True,
|
||||||
"created_at": str(user.created_at),
|
"created_at": str(getattr(user, "created_at", "")),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add picture from OAuth or Gravatar
|
# Add picture from OAuth or Gravatar
|
||||||
@@ -265,6 +268,7 @@ async def oauth_callback(request: Request, db: Session = Depends(get_db)):
|
|||||||
async def logout(request: Request):
|
async def logout(request: Request):
|
||||||
"""Handle user logout"""
|
"""Handle user logout"""
|
||||||
request.session.pop("user", None)
|
request.session.pop("user", None)
|
||||||
|
request.session.pop("user_id", None)
|
||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
url="/auth/login?message=You+have+been+logged+out+successfully",
|
url="/auth/login?message=You+have+been+logged+out+successfully",
|
||||||
status_code=status.HTTP_302_FOUND
|
status_code=status.HTTP_302_FOUND
|
||||||
@@ -272,9 +276,9 @@ async def logout(request: Request):
|
|||||||
|
|
||||||
@router.get("/profile")
|
@router.get("/profile")
|
||||||
@require_login
|
@require_login
|
||||||
async def profile_page(request: Request):
|
async def profile_page(request: Request, db: Session = Depends(get_db)):
|
||||||
"""Show user profile page"""
|
"""Show user profile page"""
|
||||||
user = request.session.get("user")
|
user = get_user_from_session(request, db)
|
||||||
return templates.TemplateResponse("auth/profile.html", {"request": request, "user": user})
|
return templates.TemplateResponse("auth/profile.html", {"request": request, "user": user})
|
||||||
|
|
||||||
@router.get("/register")
|
@router.get("/register")
|
||||||
@@ -327,6 +331,19 @@ async def register(request: Request, db: Session = Depends(get_db)):
|
|||||||
)
|
)
|
||||||
db.add(user)
|
db.add(user)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
db.refresh(user)
|
||||||
|
|
||||||
|
# Log the user in immediately after registration
|
||||||
|
request.session["user_id"] = user.id
|
||||||
|
request.session["user"] = {
|
||||||
|
"id": user.id,
|
||||||
|
"username": user.username,
|
||||||
|
"email": user.email,
|
||||||
|
"is_admin": False,
|
||||||
|
"picture": get_gravatar_url(user.email),
|
||||||
|
"_permanent": True,
|
||||||
|
"created_at": str(getattr(user, "created_at", ""))
|
||||||
|
}
|
||||||
|
|
||||||
# Redirect to registration success page
|
# Redirect to registration success page
|
||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
@@ -343,7 +360,15 @@ async def registration_success(request: Request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@router.get("/api/whoami")
|
@router.get("/api/whoami")
|
||||||
async def whoami(request: Request):
|
async def whoami(request: Request, db: Session = Depends(get_db)):
|
||||||
"""API endpoint to get current user information"""
|
"""API endpoint to get current user information"""
|
||||||
user = request.session.get("user")
|
user = get_user_from_session(request, db)
|
||||||
return user or {"error": "Not authenticated"}
|
if not user:
|
||||||
|
return {"error": "Not authenticated"}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": user.id,
|
||||||
|
"username": user.username,
|
||||||
|
"email": user.email,
|
||||||
|
"is_admin": user.is_admin if hasattr(user, "is_admin") else False
|
||||||
|
}
|
||||||
|
|||||||
+54
-18
@@ -3,24 +3,50 @@ from fastapi.security import OAuth2PasswordBearer
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from .db import get_db
|
from .db import get_db
|
||||||
from . import models
|
from . import models
|
||||||
from .auth import get_current_user, require_login, require_admin
|
|
||||||
|
|
||||||
# Reuse functions from auth.py
|
# OAuth2 configuration for API-based authentication
|
||||||
# This is just for backward compatibility with any code that imported these from dependencies
|
|
||||||
|
|
||||||
# Function to get a db session
|
|
||||||
def get_session_db():
|
|
||||||
return next(get_db())
|
|
||||||
|
|
||||||
# Function to get current authenticated user
|
|
||||||
def get_authenticated_user(request: Request):
|
|
||||||
return get_current_user(request)
|
|
||||||
|
|
||||||
# These are kept for API backward compatibility
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token")
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token")
|
||||||
|
|
||||||
def get_current_active_user(request: Request):
|
# Function to get user from session
|
||||||
user = get_current_user(request)
|
def get_user_from_session(request: Request, db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Get the current user from session.
|
||||||
|
This is the SINGLE source of truth for session-based auth.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# First check if request has session attribute and it's a dictionary
|
||||||
|
if not hasattr(request, "session"):
|
||||||
|
print("No session object found in request")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Make sure session is a dictionary before trying to access it
|
||||||
|
if not isinstance(request.session, dict):
|
||||||
|
print(f"Session is not a dictionary: {type(request.session)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Standard method - get user_id from session
|
||||||
|
user_id = request.session.get("user_id")
|
||||||
|
if user_id:
|
||||||
|
return db.query(models.User).filter(models.User.id == user_id).first()
|
||||||
|
|
||||||
|
# Legacy method - get user from session.user
|
||||||
|
user_dict = request.session.get("user")
|
||||||
|
if user_dict and isinstance(user_dict, dict) and "id" in user_dict:
|
||||||
|
return db.query(models.User).filter(models.User.id == user_dict["id"]).first()
|
||||||
|
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting user from session: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Function to get current authenticated user
|
||||||
|
def get_current_user(request: Request, db: Session = Depends(get_db)):
|
||||||
|
"""Get current authenticated user from session"""
|
||||||
|
return get_user_from_session(request, db)
|
||||||
|
|
||||||
|
def get_current_active_user(request: Request, db: Session = Depends(get_db)):
|
||||||
|
"""Get current user and ensure they're authenticated"""
|
||||||
|
user = get_user_from_session(request, db)
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
@@ -29,11 +55,21 @@ def get_current_active_user(request: Request):
|
|||||||
)
|
)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def get_current_admin_user(request: Request):
|
def get_current_admin_user(request: Request, db: Session = Depends(get_db)):
|
||||||
user = get_current_user(request)
|
"""Get current user and ensure they're an admin"""
|
||||||
if not user or not user.get("is_admin"):
|
user = get_user_from_session(request, db)
|
||||||
|
if not user or not user.is_admin:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Not enough permissions",
|
detail="Not enough permissions",
|
||||||
)
|
)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
# Keep for backward compatibility
|
||||||
|
def get_session_db():
|
||||||
|
"""Legacy function to get DB session directly"""
|
||||||
|
return next(get_db())
|
||||||
|
|
||||||
|
def get_authenticated_user(request: Request, db: Session = Depends(get_db)):
|
||||||
|
"""Legacy function for getting the current user"""
|
||||||
|
return get_user_from_session(request, db)
|
||||||
|
|||||||
+35
-26
@@ -10,7 +10,8 @@ from . import models
|
|||||||
from .templates_config import templates
|
from .templates_config import templates
|
||||||
from .views import qr, redeem, teams, admin, leaderboard, dashboard
|
from .views import qr, redeem, teams, admin, leaderboard, dashboard
|
||||||
from .db_init import seed_db
|
from .db_init import seed_db
|
||||||
from .auth import router as auth_router, get_current_user
|
from .auth import router as auth_router
|
||||||
|
from .dependencies import get_user_from_session
|
||||||
|
|
||||||
# Create tables on startup
|
# Create tables on startup
|
||||||
init_db()
|
init_db()
|
||||||
@@ -19,24 +20,26 @@ init_db()
|
|||||||
# In a production app, you would handle this differently
|
# In a production app, you would handle this differently
|
||||||
seed_db()
|
seed_db()
|
||||||
|
|
||||||
|
# Initialize FastAPI app
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
# Configure session middleware with environment variables or defaults
|
# Configure session middleware settings
|
||||||
secret_key = os.environ.get("SECRET_KEY", "a-default-secret-key-for-sessions-please-change-this")
|
secret_key = os.environ.get("SECRET_KEY", "a-default-secret-key-for-sessions-please-change-this")
|
||||||
if len(secret_key) < 32:
|
if len(secret_key) < 32:
|
||||||
print(f"WARNING: Secret key is too short ({len(secret_key)} chars). Recommended: 32+ chars")
|
print(f"WARNING: Secret key is too short ({len(secret_key)} chars). Recommended: 32+ chars")
|
||||||
|
|
||||||
# Apply SessionMiddleware FIRST - it must be the first middleware in the stack
|
# IMPORTANT: Add SessionMiddleware before anything else
|
||||||
|
# This must be the FIRST middleware in the stack
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
SessionMiddleware,
|
SessionMiddleware,
|
||||||
secret_key=secret_key,
|
secret_key=secret_key,
|
||||||
max_age=int(os.environ.get("SESSION_MAX_AGE", "86400")), # 24 hours by default
|
max_age=int(os.environ.get("SESSION_MAX_AGE", "86400")), # 24 hours by default
|
||||||
same_site="lax", # Important for security while allowing redirects
|
same_site="lax",
|
||||||
https_only=os.environ.get("COOKIE_SECURE", "False").lower() == "true",
|
https_only=os.environ.get("COOKIE_SECURE", "False").lower() == "true",
|
||||||
session_cookie="league_ledger_session", # Custom cookie name
|
session_cookie="league_ledger_session",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Debug middleware to track session state
|
# Debug middleware - ADDED AFTER SessionMiddleware
|
||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
async def debug_session_middleware(request, call_next):
|
async def debug_session_middleware(request, call_next):
|
||||||
"""Debug middleware to track session state"""
|
"""Debug middleware to track session state"""
|
||||||
@@ -44,18 +47,16 @@ async def debug_session_middleware(request, call_next):
|
|||||||
session_cookie = request.cookies.get("league_ledger_session")
|
session_cookie = request.cookies.get("league_ledger_session")
|
||||||
|
|
||||||
print(f"Request path: {request.url.path}")
|
print(f"Request path: {request.url.path}")
|
||||||
# Instead of checking request.scope, check if we can access the session dict
|
# First check if the session attribute exists properly
|
||||||
has_session = hasattr(request, "session") and isinstance(request.session, dict)
|
has_session = hasattr(request, "session")
|
||||||
|
is_dict = has_session and isinstance(request.session, dict)
|
||||||
print(f"Has session attribute: {has_session}")
|
print(f"Has session attribute: {has_session}")
|
||||||
|
print(f"Session is dict: {is_dict}")
|
||||||
print(f"Has session cookie: {session_cookie is not None}")
|
print(f"Has session cookie: {session_cookie is not None}")
|
||||||
|
|
||||||
# Check session data
|
# Safely check session data
|
||||||
if hasattr(request, "session"):
|
if has_session and is_dict:
|
||||||
try:
|
print(f"Session data before: {dict(request.session)}")
|
||||||
print(f"Session data before: {dict(request.session)}")
|
|
||||||
except (TypeError, AttributeError):
|
|
||||||
# The session might not be dict-like
|
|
||||||
print(f"Session exists but isn't a dictionary")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error in debug middleware (pre): {str(e)}")
|
print(f"Error in debug middleware (pre): {str(e)}")
|
||||||
|
|
||||||
@@ -63,22 +64,27 @@ async def debug_session_middleware(request, call_next):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if hasattr(request, "session"):
|
if hasattr(request, "session"):
|
||||||
try:
|
print(f"Session data after: {dict(request.session)}")
|
||||||
print(f"Session data after: {dict(request.session)}")
|
|
||||||
except (TypeError, AttributeError):
|
|
||||||
print(f"Session exists but isn't a dictionary")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error in debug middleware (post): {str(e)}")
|
print(f"Error in debug middleware (post): {str(e)}")
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# User context middleware to make user available in templates
|
# User context middleware
|
||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
async def add_user_to_request(request: Request, call_next):
|
async def add_user_to_request(request: Request, call_next):
|
||||||
"""Add user to request state and update template globals"""
|
"""Add user to request state and update template globals"""
|
||||||
try:
|
try:
|
||||||
# Get user from session if available
|
# Get database connection from context
|
||||||
user = get_current_user(request)
|
from .db import SessionLocal
|
||||||
|
db = SessionLocal()
|
||||||
|
|
||||||
|
# Get user from session with safer approach
|
||||||
|
user = None
|
||||||
|
try:
|
||||||
|
user = get_user_from_session(request, db)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting user for request context: {str(e)}")
|
||||||
|
|
||||||
# Store user in request.state for route handlers
|
# Store user in request.state for route handlers
|
||||||
request.state.user = user
|
request.state.user = user
|
||||||
@@ -88,17 +94,20 @@ async def add_user_to_request(request: Request, call_next):
|
|||||||
|
|
||||||
# Debug output to check user and admin status
|
# Debug output to check user and admin status
|
||||||
if user:
|
if user:
|
||||||
print(f"User in context: {user.get('username')}, Admin: {user.get('is_admin', False)}")
|
print(f"User in context: {user.username}, Admin: {getattr(user, 'is_admin', False)}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error setting user context: {str(e)}")
|
print(f"Error setting user context: {str(e)}")
|
||||||
|
finally:
|
||||||
|
# Always close the DB connection
|
||||||
|
if 'db' in locals():
|
||||||
|
db.close()
|
||||||
|
|
||||||
# Process the request
|
# Process the request
|
||||||
response = await call_next(request)
|
response = await call_next(request)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# Routers
|
# Routers - include after middleware setup is complete
|
||||||
app.include_router(auth_router, prefix="/auth", tags=["Auth"]) # Auth router should be first
|
app.include_router(auth_router, prefix="/auth", tags=["Auth"])
|
||||||
app.include_router(qr.router, prefix="/qr", tags=["QR"])
|
app.include_router(qr.router, prefix="/qr", tags=["QR"])
|
||||||
app.include_router(redeem.router, prefix="/redeem", tags=["Redeem"])
|
app.include_router(redeem.router, prefix="/redeem", tags=["Redeem"])
|
||||||
app.include_router(teams.router, prefix="/teams", tags=["Teams"])
|
app.include_router(teams.router, prefix="/teams", tags=["Teams"])
|
||||||
|
|||||||
@@ -1,560 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Authentication routes for user login, registration, and management.
|
|
||||||
"""
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status, Request, Form, Response
|
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
||||||
from fastapi.security import OAuth2PasswordRequestForm
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from sqlalchemy import inspect
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import Optional, Dict, Any
|
|
||||||
import smtplib
|
|
||||||
from email.message import EmailMessage
|
|
||||||
import os
|
|
||||||
|
|
||||||
from ..db import SessionLocal, engine
|
|
||||||
from ..models import User
|
|
||||||
from ..security import (
|
|
||||||
verify_password, get_password_hash, create_access_token, generate_token,
|
|
||||||
SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES
|
|
||||||
)
|
|
||||||
from ..dependencies import get_db, get_current_user, get_user_from_session
|
|
||||||
from ..templates_config import templates
|
|
||||||
from ..schemas import UserCreate, UserLogin, UserUpdate, PasswordReset
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
# Check if all required user columns exist
|
|
||||||
def get_available_user_columns():
|
|
||||||
inspector = inspect(engine)
|
|
||||||
if 'users' in inspector.get_table_names():
|
|
||||||
return [col['name'] for col in inspector.get_columns('users')]
|
|
||||||
return []
|
|
||||||
|
|
||||||
# --- HTML ROUTES (Web UI) ---
|
|
||||||
|
|
||||||
@router.get("/login", response_class=HTMLResponse)
|
|
||||||
async def login_page(request: Request, next: str = "/"):
|
|
||||||
"""Display login page."""
|
|
||||||
# Debug print to check what's happening in the route
|
|
||||||
print("Login page accessed, checking session...")
|
|
||||||
|
|
||||||
# DO NOT redirect if user is logged in - there's likely an issue with session handling
|
|
||||||
# Just render the login page regardless of session state for now
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"auth/login.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"next": next,
|
|
||||||
"messages": []
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.post("/login", response_class=HTMLResponse)
|
|
||||||
async def login(
|
|
||||||
request: Request,
|
|
||||||
response: Response,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
username: str = Form(...),
|
|
||||||
password: str = Form(...),
|
|
||||||
remember: bool = Form(False),
|
|
||||||
next: str = Form("/")
|
|
||||||
):
|
|
||||||
"""Process login form."""
|
|
||||||
# Try to authenticate user
|
|
||||||
user = db.query(User).filter((User.username == username) | (User.email == username)).first()
|
|
||||||
|
|
||||||
if not user or not verify_password(password, user.hashed_password):
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"auth/login.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"next": next,
|
|
||||||
"messages": [{"type": "error", "text": "Invalid username or password"}],
|
|
||||||
"username": username
|
|
||||||
},
|
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if is_active column exists and if user is active
|
|
||||||
columns = get_available_user_columns()
|
|
||||||
if 'is_active' in columns and hasattr(user, 'is_active') and not user.is_active:
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"auth/login.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"next": next,
|
|
||||||
"messages": [{"type": "error", "text": "Account is deactivated"}],
|
|
||||||
"username": username
|
|
||||||
},
|
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update last login time if column exists
|
|
||||||
if 'last_login' in columns and hasattr(user, 'last_login'):
|
|
||||||
user.last_login = datetime.utcnow()
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
# Debug info
|
|
||||||
print(f"User authenticated: {user.username}")
|
|
||||||
|
|
||||||
# Set session data with better error handling
|
|
||||||
try:
|
|
||||||
# Use directly accessible dictionary
|
|
||||||
request.session["user_id"] = user.id
|
|
||||||
request.session["username"] = user.username
|
|
||||||
request.session["_permanent"] = True
|
|
||||||
|
|
||||||
# Check if is_admin attribute exists
|
|
||||||
if hasattr(user, "is_admin"):
|
|
||||||
request.session["is_admin"] = user.is_admin
|
|
||||||
else:
|
|
||||||
request.session["is_admin"] = False
|
|
||||||
|
|
||||||
# Add timestamp for session creation
|
|
||||||
request.session["created_at"] = str(datetime.now())
|
|
||||||
|
|
||||||
# Debug session data
|
|
||||||
print(f"Session data set: {dict(request.session)}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error setting session: {str(e)}")
|
|
||||||
|
|
||||||
# Redirect to next page or home
|
|
||||||
return RedirectResponse(url=next, status_code=status.HTTP_303_SEE_OTHER)
|
|
||||||
|
|
||||||
@router.get("/register", response_class=HTMLResponse)
|
|
||||||
async def register_page(request: Request):
|
|
||||||
"""Display registration page."""
|
|
||||||
# Check if user is already logged in
|
|
||||||
user = await get_user_from_session(request)
|
|
||||||
if user:
|
|
||||||
return RedirectResponse(url="/")
|
|
||||||
|
|
||||||
return templates.TemplateResponse("auth/register.html", {"request": request})
|
|
||||||
|
|
||||||
@router.post("/register", response_class=HTMLResponse)
|
|
||||||
async def register(
|
|
||||||
request: Request,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
username: str = Form(...),
|
|
||||||
email: str = Form(...),
|
|
||||||
password: str = Form(...),
|
|
||||||
confirm_password: str = Form(...),
|
|
||||||
):
|
|
||||||
"""Process registration form."""
|
|
||||||
# Validate the form data
|
|
||||||
if password != confirm_password:
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"auth/register.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"messages": [{"type": "error", "text": "Passwords do not match"}],
|
|
||||||
"username": username,
|
|
||||||
"email": email
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if username already exists
|
|
||||||
if db.query(User).filter(User.username == username).first():
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"auth/register.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"messages": [{"type": "error", "text": "Username already exists"}],
|
|
||||||
"username": username,
|
|
||||||
"email": email
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if email already exists
|
|
||||||
if db.query(User).filter(User.email == email).first():
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"auth/register.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"messages": [{"type": "error", "text": "Email already exists"}],
|
|
||||||
"username": username,
|
|
||||||
"email": email
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create new user
|
|
||||||
hashed_password = get_password_hash(password)
|
|
||||||
|
|
||||||
# Get available columns
|
|
||||||
columns = get_available_user_columns()
|
|
||||||
user_data = {
|
|
||||||
"username": username,
|
|
||||||
"email": email,
|
|
||||||
"hashed_password": hashed_password
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add optional fields only if they exist in the database
|
|
||||||
if 'is_active' in columns:
|
|
||||||
user_data["is_active"] = True
|
|
||||||
if 'is_verified' in columns:
|
|
||||||
user_data["is_verified"] = False
|
|
||||||
if 'verification_token' in columns:
|
|
||||||
user_data["verification_token"] = generate_token()
|
|
||||||
|
|
||||||
new_user = User(**user_data)
|
|
||||||
|
|
||||||
db.add(new_user)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(new_user)
|
|
||||||
|
|
||||||
# In a real app, send verification email here
|
|
||||||
# For now, just redirect to a success page
|
|
||||||
|
|
||||||
# Log the user in
|
|
||||||
request.session["user_id"] = new_user.id
|
|
||||||
|
|
||||||
return RedirectResponse(
|
|
||||||
url="/auth/registration-success",
|
|
||||||
status_code=status.HTTP_303_SEE_OTHER
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.get("/registration-success", response_class=HTMLResponse)
|
|
||||||
async def registration_success(request: Request):
|
|
||||||
"""Display registration success page."""
|
|
||||||
return templates.TemplateResponse("auth/registration_success.html", {"request": request})
|
|
||||||
|
|
||||||
@router.get("/logout")
|
|
||||||
async def logout(request: Request):
|
|
||||||
"""Log user out by clearing session."""
|
|
||||||
request.session.clear()
|
|
||||||
return RedirectResponse(url="/")
|
|
||||||
|
|
||||||
@router.get("/profile", response_class=HTMLResponse)
|
|
||||||
async def profile_page(
|
|
||||||
request: Request,
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Display user profile page."""
|
|
||||||
user = await get_user_from_session(request)
|
|
||||||
if not user:
|
|
||||||
return RedirectResponse(url="/auth/login?next=/auth/profile")
|
|
||||||
|
|
||||||
# Get user's teams (using fresh user object from database to ensure relationships are loaded)
|
|
||||||
user = db.query(User).filter(User.id == user.id).first()
|
|
||||||
|
|
||||||
user_teams = []
|
|
||||||
for membership in user.memberships:
|
|
||||||
user_teams.append({
|
|
||||||
"team": membership.team,
|
|
||||||
"is_admin": membership.is_admin
|
|
||||||
})
|
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"auth/profile.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"user": user,
|
|
||||||
"user_teams": user_teams
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.post("/update-profile", response_class=HTMLResponse)
|
|
||||||
async def update_profile(
|
|
||||||
request: Request,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
username: str = Form(None),
|
|
||||||
email: str = Form(None),
|
|
||||||
):
|
|
||||||
"""Update user profile information."""
|
|
||||||
user = await get_user_from_session(request)
|
|
||||||
if not user:
|
|
||||||
return RedirectResponse(url="/auth/login?next=/auth/profile")
|
|
||||||
|
|
||||||
# Check for username collision
|
|
||||||
if username and username != user.username:
|
|
||||||
existing_user = db.query(User).filter(User.username == username).first()
|
|
||||||
if existing_user:
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"auth/profile.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"user": user,
|
|
||||||
"messages": [{"type": "error", "text": "Username already exists"}]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
user.username = username
|
|
||||||
|
|
||||||
# Check for email collision
|
|
||||||
if email and email != user.email:
|
|
||||||
existing_user = db.query(User).filter(User.email == email).first()
|
|
||||||
if existing_user:
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"auth/profile.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"user": user,
|
|
||||||
"messages": [{"type": "error", "text": "Email already exists"}]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
user.email = email
|
|
||||||
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"auth/profile.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"user": user,
|
|
||||||
"messages": [{"type": "success", "text": "Profile updated successfully"}]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.post("/change-password", response_class=HTMLResponse)
|
|
||||||
async def change_password(
|
|
||||||
request: Request,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_password: str = Form(...),
|
|
||||||
new_password: str = Form(...),
|
|
||||||
confirm_password: str = Form(...),
|
|
||||||
):
|
|
||||||
"""Change user password."""
|
|
||||||
user = await get_user_from_session(request)
|
|
||||||
if not user:
|
|
||||||
return RedirectResponse(url="/auth/login?next=/auth/profile")
|
|
||||||
|
|
||||||
# Verify current password
|
|
||||||
if not verify_password(current_password, user.hashed_password):
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"auth/profile.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"user": user,
|
|
||||||
"messages": [{"type": "error", "text": "Current password is incorrect"}]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if new passwords match
|
|
||||||
if new_password != confirm_password:
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"auth/profile.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"user": user,
|
|
||||||
"messages": [{"type": "error", "text": "New passwords do not match"}]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update password
|
|
||||||
user.hashed_password = get_password_hash(new_password)
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"auth/profile.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"user": user,
|
|
||||||
"messages": [{"type": "success", "text": "Password changed successfully"}]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.get("/forgot-password", response_class=HTMLResponse)
|
|
||||||
async def forgot_password_page(request: Request):
|
|
||||||
"""Display forgot password page."""
|
|
||||||
return templates.TemplateResponse("auth/forgot_password.html", {"request": request})
|
|
||||||
|
|
||||||
@router.post("/forgot-password")
|
|
||||||
async def forgot_password(
|
|
||||||
request: Request,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
email: str = Form(...)
|
|
||||||
):
|
|
||||||
"""Process forgot password form."""
|
|
||||||
# Find user by email
|
|
||||||
user = db.query(User).filter(User.email == email).first()
|
|
||||||
|
|
||||||
# Always show success to prevent email enumeration
|
|
||||||
if not user:
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"auth/forgot_password_sent.html",
|
|
||||||
{"request": request}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Generate reset token
|
|
||||||
reset_token = generate_token()
|
|
||||||
user.reset_token = reset_token
|
|
||||||
user.reset_token_expires_at = datetime.utcnow() + timedelta(hours=1)
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
# In a real app, send email with reset link
|
|
||||||
# For demo, just show the reset link on the success page
|
|
||||||
reset_url = f"/auth/reset-password?token={reset_token}"
|
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"auth/forgot_password_sent.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"reset_url": reset_url # Remove in production, just for demo
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.get("/reset-password", response_class=HTMLResponse)
|
|
||||||
async def reset_password_page(
|
|
||||||
request: Request,
|
|
||||||
token: str,
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Display reset password page."""
|
|
||||||
# Check if token exists and is valid
|
|
||||||
user = db.query(User).filter(
|
|
||||||
User.reset_token == token,
|
|
||||||
User.reset_token_expires_at > datetime.utcnow()
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if not user:
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"auth/reset_password_error.html",
|
|
||||||
{"request": request}
|
|
||||||
)
|
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"auth/reset_password.html",
|
|
||||||
{"request": request, "token": token}
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.post("/reset-password")
|
|
||||||
async def reset_password(
|
|
||||||
request: Request,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
token: str = Form(...),
|
|
||||||
new_password: str = Form(...),
|
|
||||||
confirm_password: str = Form(...)
|
|
||||||
):
|
|
||||||
"""Process reset password form."""
|
|
||||||
# Check if token exists and is valid
|
|
||||||
user = db.query(User).filter(
|
|
||||||
User.reset_token == token,
|
|
||||||
User.reset_token_expires_at > datetime.utcnow()
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if not user:
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"auth/reset_password_error.html",
|
|
||||||
{"request": request}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if passwords match
|
|
||||||
if new_password != confirm_password:
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"auth/reset_password.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"token": token,
|
|
||||||
"messages": [{"type": "error", "text": "Passwords do not match"}]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update password
|
|
||||||
user.hashed_password = get_password_hash(new_password)
|
|
||||||
user.reset_token = None
|
|
||||||
user.reset_token_expires_at = None
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"auth/reset_password_success.html",
|
|
||||||
{"request": request}
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.get("/delete-account", response_class=HTMLResponse)
|
|
||||||
async def delete_account_page(request: Request):
|
|
||||||
"""Display delete account confirmation page."""
|
|
||||||
user = await get_user_from_session(request)
|
|
||||||
if not user:
|
|
||||||
return RedirectResponse(url="/auth/login?next=/auth/delete-account")
|
|
||||||
|
|
||||||
return templates.TemplateResponse("auth/delete_account.html", {"request": request})
|
|
||||||
|
|
||||||
@router.post("/delete-account")
|
|
||||||
async def delete_account(
|
|
||||||
request: Request,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
password: str = Form(...)
|
|
||||||
):
|
|
||||||
"""Process account deletion."""
|
|
||||||
user = await get_user_from_session(request)
|
|
||||||
if not user:
|
|
||||||
return RedirectResponse(url="/auth/login?next=/auth/delete-account")
|
|
||||||
|
|
||||||
# Verify password
|
|
||||||
if not verify_password(password, user.hashed_password):
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"auth/delete_account.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"messages": [{"type": "error", "text": "Incorrect password"}]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# In a real app, you might want to anonymize the user data instead
|
|
||||||
# of deleting it completely, but for this demo we'll delete
|
|
||||||
|
|
||||||
# Clear session
|
|
||||||
request.session.clear()
|
|
||||||
|
|
||||||
# Delete user
|
|
||||||
db.delete(user)
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
return RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER)
|
|
||||||
|
|
||||||
# --- API ROUTES (for potential SPA frontend) ---
|
|
||||||
|
|
||||||
@router.post("/token")
|
|
||||||
async def login_api(
|
|
||||||
form_data: OAuth2PasswordRequestForm = Depends(),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""API login endpoint returning JWT token."""
|
|
||||||
# Authenticate user
|
|
||||||
user = db.query(User).filter((User.username == form_data.username) | (User.email == form_data.username)).first()
|
|
||||||
|
|
||||||
if not user or not verify_password(form_data.password, user.hashed_password):
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
detail="Incorrect username or password",
|
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
|
||||||
)
|
|
||||||
|
|
||||||
columns = get_available_user_columns()
|
|
||||||
if 'is_active' in columns and hasattr(user, 'is_active') and not user.is_active:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
detail="Inactive user",
|
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
|
||||||
)
|
|
||||||
|
|
||||||
if 'last_login' in columns and hasattr(user, 'last_login'):
|
|
||||||
user.last_login = datetime.utcnow()
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
# Create access token
|
|
||||||
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
|
||||||
access_token = create_access_token(
|
|
||||||
data={"sub": user.username},
|
|
||||||
expires_delta=access_token_expires
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"access_token": access_token,
|
|
||||||
"token_type": "bearer",
|
|
||||||
"user_id": user.id,
|
|
||||||
"username": user.username
|
|
||||||
}
|
|
||||||
|
|
||||||
@router.get("/session-test")
|
|
||||||
async def test_session(request: Request):
|
|
||||||
"""Test endpoint to verify session data persistence"""
|
|
||||||
has_session = hasattr(request, "session")
|
|
||||||
session_data = dict(request.session) if has_session else {}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"has_session": has_session,
|
|
||||||
"session_data": session_data,
|
|
||||||
"authenticated": "user_id" in session_data
|
|
||||||
}
|
|
||||||
+33
-10
@@ -8,6 +8,8 @@ from sqlalchemy.orm import Session
|
|||||||
from ..db import SessionLocal
|
from ..db import SessionLocal
|
||||||
from ..models import QRTicket, User, Team, TeamMembership
|
from ..models import QRTicket, User, Team, TeamMembership
|
||||||
from ..templates_config import templates
|
from ..templates_config import templates
|
||||||
|
from ..dependencies import get_user_from_session
|
||||||
|
from ..auth import require_login
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -24,19 +26,30 @@ def redeem_code(code: str, request: Request, db: Session = Depends(get_db)):
|
|||||||
Display a page to let the user choose which team to apply points to.
|
Display a page to let the user choose which team to apply points to.
|
||||||
If not logged in, prompt them.
|
If not logged in, prompt them.
|
||||||
"""
|
"""
|
||||||
# In real app, you'd check user session or redirect to login
|
|
||||||
ticket = db.query(QRTicket).filter_by(code=code, used=False).first()
|
ticket = db.query(QRTicket).filter_by(code=code, used=False).first()
|
||||||
if not ticket:
|
if not ticket:
|
||||||
return "Invalid or already used code."
|
return templates.TemplateResponse(
|
||||||
|
"error.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"error_title": "Invalid Code",
|
||||||
|
"error_message": "This code is invalid or has already been used."
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Skeleton: you'd get the user's teams from session user
|
# Get the authenticated user
|
||||||
# For now, we mock a user ID = 1:
|
user = get_user_from_session(request, db)
|
||||||
user = db.query(User).filter_by(id=1).first()
|
|
||||||
if not user:
|
if not user:
|
||||||
return "User not found. Please log in."
|
# Store the redeem URL for after login - with try/except
|
||||||
|
try:
|
||||||
|
if hasattr(request, "session") and isinstance(request.session, dict):
|
||||||
|
request.session["redirect_after_login"] = f"/redeem/{code}"
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error setting redirect URL in session: {str(e)}")
|
||||||
|
return RedirectResponse(url="/auth/login", status_code=302)
|
||||||
|
|
||||||
# This is where you'd show the team selection or "create new team" UI
|
# Get user teams
|
||||||
user_teams = [m.team for m in user.memberships]
|
user_teams = [m.team for m in user.memberships] if hasattr(user, 'memberships') else []
|
||||||
|
|
||||||
return templates.TemplateResponse("redeem.html", {
|
return templates.TemplateResponse("redeem.html", {
|
||||||
"request": request,
|
"request": request,
|
||||||
@@ -45,6 +58,7 @@ def redeem_code(code: str, request: Request, db: Session = Depends(get_db)):
|
|||||||
})
|
})
|
||||||
|
|
||||||
@router.post("/apply/{code}")
|
@router.post("/apply/{code}")
|
||||||
|
@require_login
|
||||||
async def apply_code(
|
async def apply_code(
|
||||||
request: Request,
|
request: Request,
|
||||||
code: str,
|
code: str,
|
||||||
@@ -54,6 +68,11 @@ async def apply_code(
|
|||||||
Apply the QR code to a selected team (if user is a member),
|
Apply the QR code to a selected team (if user is a member),
|
||||||
or set to pending if user isn't a member yet.
|
or set to pending if user isn't a member yet.
|
||||||
"""
|
"""
|
||||||
|
# Get the authenticated user
|
||||||
|
user = get_user_from_session(request, db)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse(url=f"/auth/login?next=/redeem/{code}", status_code=302)
|
||||||
|
|
||||||
# Get form data
|
# Get form data
|
||||||
form_data = await request.form()
|
form_data = await request.form()
|
||||||
team_id = int(form_data.get("team_id", 0))
|
team_id = int(form_data.get("team_id", 0))
|
||||||
@@ -79,8 +98,6 @@ async def apply_code(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# For skeleton, assume user = 1
|
|
||||||
user = db.query(User).filter_by(id=1).first()
|
|
||||||
team = db.query(Team).filter_by(id=team_id).first()
|
team = db.query(Team).filter_by(id=team_id).first()
|
||||||
|
|
||||||
if not team:
|
if not team:
|
||||||
@@ -129,6 +146,7 @@ async def apply_code(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@router.post("/manual")
|
@router.post("/manual")
|
||||||
|
@require_login
|
||||||
async def manual_code_entry(
|
async def manual_code_entry(
|
||||||
request: Request,
|
request: Request,
|
||||||
code: str = Form(...),
|
code: str = Form(...),
|
||||||
@@ -138,6 +156,11 @@ async def manual_code_entry(
|
|||||||
Handle manual code entry from the form.
|
Handle manual code entry from the form.
|
||||||
This redirects to the normal redeem flow after validating the code.
|
This redirects to the normal redeem flow after validating the code.
|
||||||
"""
|
"""
|
||||||
|
# Make sure the user is authenticated
|
||||||
|
user = get_user_from_session(request, db)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse(url="/auth/login?next=/dashboard/", status_code=302)
|
||||||
|
|
||||||
# Check if the code exists
|
# Check if the code exists
|
||||||
ticket = db.query(QRTicket).filter_by(code=code, used=False).first()
|
ticket = db.query(QRTicket).filter_by(code=code, used=False).first()
|
||||||
|
|
||||||
|
|||||||
+24
-23
@@ -13,6 +13,8 @@ from ..db import SessionLocal
|
|||||||
from ..models import Team, TeamMembership, User, QRTicket, TeamAchievement
|
from ..models import Team, TeamMembership, User, QRTicket, TeamAchievement
|
||||||
from ..schemas import TeamCreate
|
from ..schemas import TeamCreate
|
||||||
from ..templates_config import templates
|
from ..templates_config import templates
|
||||||
|
from ..dependencies import get_user_from_session
|
||||||
|
from ..auth import require_login
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -23,27 +25,12 @@ def get_db():
|
|||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
def get_or_create_default_user(db: Session):
|
|
||||||
"""Get user ID 1 or create it if it doesn't exist."""
|
|
||||||
user = db.query(User).filter_by(id=1).first()
|
|
||||||
if not user:
|
|
||||||
# Create a default user
|
|
||||||
user = User(
|
|
||||||
username="default_user",
|
|
||||||
email="default@example.com",
|
|
||||||
hashed_password="placeholder"
|
|
||||||
)
|
|
||||||
db.add(user)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(user)
|
|
||||||
return user
|
|
||||||
|
|
||||||
@router.get("/", response_class=HTMLResponse)
|
@router.get("/", response_class=HTMLResponse)
|
||||||
def list_teams(request: Request, db: Session = Depends(get_db)):
|
def list_teams(request: Request, db: Session = Depends(get_db)):
|
||||||
teams = db.query(Team).all()
|
teams = db.query(Team).all()
|
||||||
|
|
||||||
# Get the user's teams to highlight teams they're already in
|
# Get the user's teams to highlight teams they're already in
|
||||||
user = db.query(User).filter_by(id=1).first()
|
user = get_user_from_session(request, db)
|
||||||
user_team_ids = []
|
user_team_ids = []
|
||||||
|
|
||||||
if user:
|
if user:
|
||||||
@@ -67,8 +54,11 @@ def list_teams(request: Request, db: Session = Depends(get_db)):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@router.post("/create")
|
@router.post("/create")
|
||||||
def create_team(name: str = Form(...), db: Session = Depends(get_db)):
|
@require_login
|
||||||
user = get_or_create_default_user(db)
|
def create_team(request: Request, name: str = Form(...), db: Session = Depends(get_db)):
|
||||||
|
user = get_user_from_session(request, db)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse("/auth/login?next=/teams/", status_code=302)
|
||||||
|
|
||||||
# Create team
|
# Create team
|
||||||
new_team = Team(name=name)
|
new_team = Team(name=name)
|
||||||
@@ -84,8 +74,11 @@ def create_team(name: str = Form(...), db: Session = Depends(get_db)):
|
|||||||
return RedirectResponse("/teams/", status_code=303)
|
return RedirectResponse("/teams/", status_code=303)
|
||||||
|
|
||||||
@router.post("/join/{team_id}")
|
@router.post("/join/{team_id}")
|
||||||
def join_team(team_id: int, db: Session = Depends(get_db)):
|
@require_login
|
||||||
user = get_or_create_default_user(db)
|
def join_team(request: Request, team_id: int, db: Session = Depends(get_db)):
|
||||||
|
user = get_user_from_session(request, db)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse("/auth/login?next=/teams/", status_code=302)
|
||||||
|
|
||||||
team = db.query(Team).filter_by(id=team_id).first()
|
team = db.query(Team).filter_by(id=team_id).first()
|
||||||
if not team:
|
if not team:
|
||||||
@@ -111,8 +104,11 @@ def team_detail(request: Request, team_id: int, db: Session = Depends(get_db)):
|
|||||||
if not team:
|
if not team:
|
||||||
raise HTTPException(status_code=404, detail="Team not found")
|
raise HTTPException(status_code=404, detail="Team not found")
|
||||||
|
|
||||||
# Get current user - using a default user for now
|
# Get current user from session
|
||||||
user = get_or_create_default_user(db)
|
user = get_user_from_session(request, db)
|
||||||
|
if not user:
|
||||||
|
# If no authenticated user, redirect to login
|
||||||
|
return RedirectResponse("/auth/login?next=/teams/"+str(team_id), status_code=302)
|
||||||
|
|
||||||
# Get team members with admin status
|
# Get team members with admin status
|
||||||
memberships = db.query(TeamMembership).filter_by(team_id=team_id).all()
|
memberships = db.query(TeamMembership).filter_by(team_id=team_id).all()
|
||||||
@@ -248,14 +244,19 @@ def team_detail(request: Request, team_id: int, db: Session = Depends(get_db)):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@router.post("/{team_id}/update")
|
@router.post("/{team_id}/update")
|
||||||
|
@require_login
|
||||||
def update_team(
|
def update_team(
|
||||||
|
request: Request,
|
||||||
team_id: int,
|
team_id: int,
|
||||||
team_name: str = Form(...),
|
team_name: str = Form(...),
|
||||||
is_public: bool = Form(False),
|
is_public: bool = Form(False),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Update team details."""
|
"""Update team details."""
|
||||||
user = get_or_create_default_user(db)
|
user = get_user_from_session(request, db)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse("/auth/login?next=/teams/"+str(team_id), status_code=302)
|
||||||
|
|
||||||
team = db.query(Team).filter_by(id=team_id).first()
|
team = db.query(Team).filter_by(id=team_id).first()
|
||||||
|
|
||||||
if not team:
|
if not team:
|
||||||
|
|||||||
@@ -48,5 +48,21 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./:/app:delegated
|
- ./:/app:delegated
|
||||||
|
|
||||||
|
|
||||||
|
phpmyadmin:
|
||||||
|
image: phpmyadmin/phpmyadmin
|
||||||
|
container_name: pubquiz_phpmyadmin
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
environment:
|
||||||
|
PMA_HOST: db
|
||||||
|
PMA_PORT: 3306
|
||||||
|
PMA_USER: pubquiz_user
|
||||||
|
PMA_PASSWORD: pubquiz_pass
|
||||||
|
ports:
|
||||||
|
- "8001:80"
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db_data:
|
db_data:
|
||||||
|
|||||||
+25
-15
@@ -1,18 +1,28 @@
|
|||||||
fastapi>=0.95.0
|
# Web framework and server
|
||||||
uvicorn[standard]>=0.21.1
|
fastapi>=0.103.1
|
||||||
sqlalchemy>=2.0.9
|
uvicorn>=0.23.2
|
||||||
pymysql>=1.0.3
|
|
||||||
cryptography>=40.0.2
|
|
||||||
python-multipart>=0.0.6
|
|
||||||
authlib>=1.2.0
|
|
||||||
# Specify specific versions for passlib and bcrypt to avoid compatibility issues
|
|
||||||
passlib==1.7.4
|
|
||||||
bcrypt==4.0.1
|
|
||||||
starlette>=0.27.0
|
starlette>=0.27.0
|
||||||
jinja2>=3.1.2
|
httpx>=0.25.0 # HTTP client for making requests
|
||||||
|
|
||||||
|
# Database
|
||||||
|
sqlalchemy>=2.0.20
|
||||||
|
pymysql>=1.1.0
|
||||||
|
mysqlclient>=2.2.0
|
||||||
|
|
||||||
|
# Authentication and Security
|
||||||
|
authlib>=1.2.1
|
||||||
|
python-jose>=3.3.0
|
||||||
|
python-multipart>=0.0.6
|
||||||
|
passlib>=1.7.4
|
||||||
itsdangerous>=2.1.2
|
itsdangerous>=2.1.2
|
||||||
python-jose[cryptography]>=3.3.0
|
bcrypt>=4.0.1
|
||||||
|
|
||||||
|
# Templates and UI
|
||||||
|
jinja2>=3.1.2
|
||||||
|
aiofiles>=23.2.1
|
||||||
|
|
||||||
|
# Common utilities
|
||||||
python-dotenv>=1.0.0
|
python-dotenv>=1.0.0
|
||||||
qrcode
|
email-validator>=2.0.0
|
||||||
pydantic[email]>=1.10.7
|
pydantic>=2.3.0
|
||||||
httpx
|
qrcode>=7.4.2
|
||||||
Reference in New Issue
Block a user