From 9eef726d9be034c5b1521546e4e09b4539238b3c Mon Sep 17 00:00:00 2001 From: Christian Krakau-Louis Date: Sun, 13 Apr 2025 21:43:24 +0200 Subject: [PATCH] Refactor authentication and session management - Removed session-based user retrieval from dependencies.py and main.py. - Eliminated SessionMiddleware and related session handling code. - Updated dashboard and team views to directly query user data from the database. - Simplified redeem and team management views to remove authentication checks. - Adjusted templates to reflect changes in user context handling. - Added camera selection feature in scan_qr.html for improved QR code scanning. - Cleaned up Docker configuration and requirements for better deployment. --- app/auth.py | 374 ------------------ app/db_init.py | 7 +- app/dependencies.py | 75 ---- app/main.py | 88 +---- app/templates/base.html | 41 -- .../{dashboard.html => dashboard/index.html} | 0 app/templates/scan_qr.html | 103 +++-- app/views/admin.py | 8 - app/views/dashboard.py | 33 +- app/views/redeem.py | 84 ++-- app/views/teams.py | 43 +- docker-compose.yml | 10 +- requirements.txt | 5 +- 13 files changed, 126 insertions(+), 745 deletions(-) delete mode 100644 app/auth.py delete mode 100644 app/dependencies.py rename app/templates/{dashboard.html => dashboard/index.html} (100%) diff --git a/app/auth.py b/app/auth.py deleted file mode 100644 index 733986f..0000000 --- a/app/auth.py +++ /dev/null @@ -1,374 +0,0 @@ -#!/usr/bin/env python3 -""" -Authentication system using Authlib and session-based auth -""" -import os -import inspect -import hashlib -from functools import wraps - -from authlib.integrations.starlette_client import OAuth -from fastapi import APIRouter, Request, status, Depends, HTTPException -from starlette.responses import RedirectResponse -from sqlalchemy.orm import Session -from passlib.context import CryptContext - -from .db import SessionLocal -from .templates_config import templates -from . import models -from .dependencies import get_user_from_session - -# Initialize OAuth -oauth = OAuth() - -# Check if OAuth is configured via environment variables -OAUTH_CONFIGURED = bool(os.environ.get("OAUTH_CLIENT_ID") and os.environ.get("OAUTH_CLIENT_SECRET")) -OAUTH_PROVIDER_NAME = os.environ.get("OAUTH_PROVIDER_NAME", "Single Sign-On") - -# Configure OAuth provider if credentials are provided -if OAUTH_CONFIGURED: - oauth.register( - name="oauth_provider", - client_id=os.environ.get("OAUTH_CLIENT_ID"), - client_secret=os.environ.get("OAUTH_CLIENT_SECRET"), - server_metadata_url=os.environ.get("OAUTH_CONFIG_URL"), - client_kwargs={"scope": "openid profile email"}, - ) - -# Create router and password context -router = APIRouter() -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") - -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() - -def get_password_hash(password): - """Generate password hash""" - return pwd_context.hash(password) - -def verify_password(plain_password, hashed_password): - """Verify password against hash""" - return pwd_context.verify(plain_password, hashed_password) - -def get_current_user(request: Request, db: Session = Depends(get_db)): - """Get current user from session""" - return get_user_from_session(request, db) - -def get_gravatar_url(email): - """Generate a Gravatar URL for the given email""" - email = email.lower().strip() - email_hash = hashlib.md5(email.encode('utf-8')).hexdigest() - return f"https://www.gravatar.com/avatar/{email_hash}?d=identicon" - -def require_login(func): - """Decorator to require login for routes""" - @wraps(func) - async def wrapper(request: Request, *args, **kwargs): - try: - db = next(get_db()) - user = get_user_from_session(request, db) - - if not user: - # Store the current URL for redirecting after login - request.session["redirect_after_login"] = str(request.url) - return RedirectResponse(url="/auth/login", status_code=status.HTTP_302_FOUND) - - # Check if the wrapped function is a coroutine function - if inspect.iscoroutinefunction(func): - return await func(request, *args, **kwargs) - else: - return func(request, *args, **kwargs) - except Exception as e: - print(f"Error in require_login decorator: {str(e)}") - return RedirectResponse(url="/auth/login", status_code=status.HTTP_302_FOUND) - - return wrapper - -def require_admin(func): - """Decorator to require admin access for routes""" - @wraps(func) - async def wrapper(request: Request, *args, **kwargs): - try: - db = next(get_db()) - user = get_user_from_session(request, db) - - if not user: - request.session["redirect_after_login"] = str(request.url) - return RedirectResponse(url="/auth/login", status_code=status.HTTP_302_FOUND) - - if not user.is_admin: - return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND) - - # Check if the wrapped function is a coroutine function - if inspect.iscoroutinefunction(func): - return await func(request, *args, **kwargs) - else: - return func(request, *args, **kwargs) - except Exception as e: - print(f"Error in require_admin decorator: {str(e)}") - return RedirectResponse(url="/auth/login", status_code=status.HTTP_302_FOUND) - - return wrapper - -# Routes for authentication - -@router.get("/login") -async def login_page(request: Request, db: Session = Depends(get_db)): - """Show login page with appropriate authentication options""" - # If already logged in, redirect to home - if get_user_from_session(request, db): - return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND) - - return templates.TemplateResponse( - "auth/login.html", - { - "request": request, - "error": request.query_params.get("error"), - "message": request.query_params.get("message"), - "show_oauth": OAUTH_CONFIGURED, - "oauth_provider_name": OAUTH_PROVIDER_NAME - } - ) - -@router.post("/login") -async def login(request: Request, db: Session = Depends(get_db)): - """Handle username/password authentication""" - form_data = await request.form() - username = form_data.get("username") - password = form_data.get("password") - - # Check if username exists - user = db.query(models.User).filter(models.User.username == username).first() - if not user or not verify_password(password, user.hashed_password): - return RedirectResponse( - url="/auth/login?error=Invalid+username+or+password", - status_code=status.HTTP_302_FOUND - ) - - # Make sure we use the actual is_admin field from the User model - is_admin_value = False - if hasattr(user, "is_admin") and user.is_admin is not None: - 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"] = { - "id": user.id, - "username": user.username, - "email": user.email, - "is_admin": is_admin_value, - "picture": get_gravatar_url(user.email), - "_permanent": True, - "created_at": str(user.created_at) - } - - # Log the successful authentication - print(f"User authenticated: {username}, is_admin: {is_admin_value}") - - # Redirect to original destination or default - redirect_url = request.session.pop("redirect_after_login", "/") - return RedirectResponse(url=redirect_url, status_code=status.HTTP_303_SEE_OTHER) - -@router.get("/oauth-login") -async def oauth_login(request: Request): - """Handle OAuth login flow""" - if not OAUTH_CONFIGURED: - return RedirectResponse( - url="/auth/login?error=OAuth+not+configured", - status_code=status.HTTP_302_FOUND - ) - - redirect_uri = request.url_for("oauth_callback") - return await oauth.oauth_provider.authorize_redirect(request, redirect_uri) - -@router.get("/oauth-callback") -async def oauth_callback(request: Request, db: Session = Depends(get_db)): - """Handle OAuth callback from provider""" - try: - token = await oauth.oauth_provider.authorize_access_token(request) - userinfo = token.get("userinfo") - if not userinfo: - return RedirectResponse( - url="/auth/login?error=Failed+to+retrieve+user+information", - status_code=status.HTTP_302_FOUND - ) - - # Get or create user in database - email = userinfo.get("email") - if not email: - return RedirectResponse( - url="/auth/login?error=Email+not+provided+by+OAuth+provider", - status_code=status.HTTP_302_FOUND - ) - - # Find user by email or create a new one - user = db.query(models.User).filter(models.User.email == email).first() - if not user: - # Create new user with OAuth data - username = userinfo.get("preferred_username") or email.split("@")[0] - user = models.User( - username=username, - email=email, - hashed_password=get_password_hash(os.urandom(24).hex()), # Random password - is_active=True, # Set user as active - is_admin=False # Default to non-admin - ) - db.add(user) - db.commit() - db.refresh(user) - - # Determine if user is admin - is_admin_value = False - if hasattr(user, "is_admin") and user.is_admin is not None: - is_admin_value = user.is_admin - - # Store user ID in session (new standardized approach) - request.session["user_id"] = user.id - - # Also store legacy user data format - user_data = { - "id": user.id, - "username": user.username, - "email": user.email, - "is_active": getattr(user, "is_active", True), - "is_admin": is_admin_value, - "_permanent": True, - "created_at": str(getattr(user, "created_at", "")), - } - - # Add picture from OAuth or Gravatar - if userinfo.get("picture"): - user_data["picture"] = userinfo.get("picture") - elif email: - user_data["picture"] = get_gravatar_url(email) - - request.session["user"] = user_data - - # Log the successful authentication - print(f"User authenticated via OAuth: {email}, is_admin: {is_admin_value}") - - # Redirect to original destination or default - redirect_url = request.session.pop("redirect_after_login", "/") - return RedirectResponse(url=redirect_url, status_code=status.HTTP_303_SEE_OTHER) - except Exception as e: - print(f"OAuth authentication error: {str(e)}") - return RedirectResponse( - url=f"/auth/login?error=Authentication+failed:+{str(e)}", - status_code=status.HTTP_302_FOUND - ) - -@router.get("/logout") -async def logout(request: Request): - """Handle user logout""" - request.session.pop("user", None) - request.session.pop("user_id", None) - return RedirectResponse( - url="/auth/login?message=You+have+been+logged+out+successfully", - status_code=status.HTTP_302_FOUND - ) - -@router.get("/profile") -@require_login -async def profile_page(request: Request, db: Session = Depends(get_db)): - """Show user profile page""" - user = get_user_from_session(request, db) - return templates.TemplateResponse("auth/profile.html", {"request": request, "user": user}) - -@router.get("/register") -async def register_page(request: Request): - """Show registration page""" - return templates.TemplateResponse("auth/register.html", {"request": request}) - -@router.post("/register") -async def register(request: Request, db: Session = Depends(get_db)): - """Handle user registration""" - form_data = await request.form() - username = form_data.get("username") - email = form_data.get("email") - password = form_data.get("password") - confirm_password = form_data.get("confirm_password") - - # Validate input - if not username or not email or not password: - return RedirectResponse( - url="/auth/register?error=All+fields+are+required", - status_code=status.HTTP_302_FOUND - ) - - if password != confirm_password: - return RedirectResponse( - url="/auth/register?error=Passwords+do+not+match", - status_code=status.HTTP_302_FOUND - ) - - # Check if username or email already exists - if db.query(models.User).filter(models.User.username == username).first(): - return RedirectResponse( - url="/auth/register?error=Username+already+taken", - status_code=status.HTTP_302_FOUND - ) - - if db.query(models.User).filter(models.User.email == email).first(): - return RedirectResponse( - url="/auth/register?error=Email+already+registered", - status_code=status.HTTP_302_FOUND - ) - - # Create new user with the appropriate fields - user = models.User( - username=username, - email=email, - hashed_password=get_password_hash(password), - is_active=True, - is_admin=False # Explicitly set is_admin to False for new registrations - ) - db.add(user) - 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 - return RedirectResponse( - url="/auth/registration-success", - status_code=status.HTTP_303_SEE_OTHER - ) - -@router.get("/registration-success") -async def registration_success(request: Request): - """Show registration success page""" - return templates.TemplateResponse( - "auth/registration_success.html", - {"request": request} - ) - -@router.get("/api/whoami") -async def whoami(request: Request, db: Session = Depends(get_db)): - """API endpoint to get current user information""" - user = get_user_from_session(request, db) - 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 - } diff --git a/app/db_init.py b/app/db_init.py index 79a1f2a..b49cc3d 100644 --- a/app/db_init.py +++ b/app/db_init.py @@ -6,10 +6,15 @@ import random from datetime import datetime, timedelta from sqlalchemy import inspect from sqlalchemy.orm import Session +from passlib.context import CryptContext from .models import User, Team, TeamMembership, QRTicket from .db import SessionLocal -from .auth import get_password_hash + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +def get_password_hash(password: str) -> str: + return pwd_context.hash(password) def table_has_column(engine, table_name, column_name): """Check if a table has a specific column.""" diff --git a/app/dependencies.py b/app/dependencies.py deleted file mode 100644 index 22ff7be..0000000 --- a/app/dependencies.py +++ /dev/null @@ -1,75 +0,0 @@ -from fastapi import Depends, HTTPException, status, Request -from fastapi.security import OAuth2PasswordBearer -from sqlalchemy.orm import Session -from .db import get_db -from . import models - -# OAuth2 configuration for API-based authentication -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token") - -# Function to get user from session -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: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Not authenticated", - headers={"WWW-Authenticate": "Bearer"}, - ) - return user - -def get_current_admin_user(request: Request, db: Session = Depends(get_db)): - """Get current user and ensure they're an admin""" - user = get_user_from_session(request, db) - if not user or not user.is_admin: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Not enough permissions", - ) - 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) diff --git a/app/main.py b/app/main.py index e9db5f1..171945d 100644 --- a/app/main.py +++ b/app/main.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 -from fastapi import FastAPI, Request, status, Depends +from fastapi import FastAPI, Request, status from fastapi.responses import HTMLResponse, RedirectResponse -from starlette.middleware.sessions import SessionMiddleware from datetime import datetime import os @@ -10,8 +9,6 @@ from . import models from .templates_config import templates from .views import qr, redeem, teams, admin, leaderboard, dashboard from .db_init import seed_db -from .auth import router as auth_router -from .dependencies import get_user_from_session # Create tables on startup init_db() @@ -20,94 +17,23 @@ init_db() # In a production app, you would handle this differently seed_db() -# Initialize FastAPI app app = FastAPI() -# Configure session middleware settings -secret_key = os.environ.get("SECRET_KEY", "a-default-secret-key-for-sessions-please-change-this") -if len(secret_key) < 32: - print(f"WARNING: Secret key is too short ({len(secret_key)} chars). Recommended: 32+ chars") - -# IMPORTANT: Add SessionMiddleware before anything else -# This must be the FIRST middleware in the stack -app.add_middleware( - SessionMiddleware, - secret_key=secret_key, - max_age=int(os.environ.get("SESSION_MAX_AGE", "86400")), # 24 hours by default - same_site="lax", - https_only=os.environ.get("COOKIE_SECURE", "False").lower() == "true", - session_cookie="league_ledger_session", -) - -# Debug middleware - ADDED AFTER SessionMiddleware +# User context middleware to make template globals available @app.middleware("http") -async def debug_session_middleware(request, call_next): - """Debug middleware to track session state""" +async def add_template_globals(request: Request, call_next): + """Add template globals""" try: - session_cookie = request.cookies.get("league_ledger_session") - - print(f"Request path: {request.url.path}") - # First check if the session attribute exists properly - has_session = hasattr(request, "session") - is_dict = has_session and isinstance(request.session, dict) - print(f"Has session attribute: {has_session}") - print(f"Session is dict: {is_dict}") - print(f"Has session cookie: {session_cookie is not None}") - - # Safely check session data - if has_session and is_dict: - print(f"Session data before: {dict(request.session)}") - except Exception as e: - print(f"Error in debug middleware (pre): {str(e)}") - - response = await call_next(request) - - try: - if hasattr(request, "session"): - print(f"Session data after: {dict(request.session)}") - except Exception as e: - print(f"Error in debug middleware (post): {str(e)}") - - return response - -# User context middleware -@app.middleware("http") -async def add_user_to_request(request: Request, call_next): - """Add user to request state and update template globals""" - try: - # Get database connection from context - 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 - request.state.user = user - # Update template globals for all templates - templates.env.globals["current_user"] = user - - # Debug output to check user and admin status - if user: - print(f"User in context: {user.username}, Admin: {getattr(user, 'is_admin', False)}") + templates.env.globals["current_user"] = None except Exception as e: - print(f"Error setting user context: {str(e)}") - finally: - # Always close the DB connection - if 'db' in locals(): - db.close() + print(f"Error setting template globals: {str(e)}") # Process the request response = await call_next(request) return response -# Routers - include after middleware setup is complete -app.include_router(auth_router, prefix="/auth", tags=["Auth"]) +# Routers - auth router removed app.include_router(qr.router, prefix="/qr", tags=["QR"]) app.include_router(redeem.router, prefix="/redeem", tags=["Redeem"]) app.include_router(teams.router, prefix="/teams", tags=["Teams"]) diff --git a/app/templates/base.html b/app/templates/base.html index 6357b8a..a24f7a4 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -62,37 +62,15 @@ Dashboard - {% if current_user and current_user.is_admin %} Admin - {% endif %} About Contact - - - {% if current_user %} -
- - -
- {% else %} - - Log In - - {% endif %} @@ -116,34 +94,15 @@ Dashboard - {% if current_user and current_user.is_admin %} Admin - {% endif %} About Contact - - - {% if current_user %} - - {{ current_user.username }} - - - Logout - - {% else %} - - Log In - - - Sign Up - - {% endif %} diff --git a/app/templates/dashboard.html b/app/templates/dashboard/index.html similarity index 100% rename from app/templates/dashboard.html rename to app/templates/dashboard/index.html diff --git a/app/templates/scan_qr.html b/app/templates/scan_qr.html index 90247d1..8fd4e1e 100644 --- a/app/templates/scan_qr.html +++ b/app/templates/scan_qr.html @@ -19,6 +19,14 @@
Make sure the QR code is well-lit and clearly visible
+ + +
+ + +
@@ -78,50 +86,65 @@ {% endblock %} diff --git a/app/views/admin.py b/app/views/admin.py index 7a21c11..836a617 100644 --- a/app/views/admin.py +++ b/app/views/admin.py @@ -13,7 +13,6 @@ import inspect as py_inspect from ..db import SessionLocal, Base from ..models import User, Team, TeamMembership, QRTicket from ..templates_config import templates -from ..auth import require_admin router = APIRouter() @@ -65,7 +64,6 @@ def get_relationships(model_class: Type[Base]) -> Dict[str, str]: return relationships @router.get("/", response_class=HTMLResponse) -@require_admin async def admin_home(request: Request): """Admin dashboard home.""" model_list = [(key, name) for key, (_, name) in MODELS.items()] @@ -75,7 +73,6 @@ async def admin_home(request: Request): ) @router.get("/{model_name}", response_class=HTMLResponse) -@require_admin async def list_records( request: Request, model_name: str, @@ -127,7 +124,6 @@ async def list_records( ) @router.get("/{model_name}/new", response_class=HTMLResponse) -@require_admin async def create_record_form( request: Request, model_name: str, @@ -168,7 +164,6 @@ async def create_record_form( ) @router.post("/{model_name}/new") -@require_admin async def create_record( request: Request, model_name: str, @@ -217,7 +212,6 @@ async def create_record( return RedirectResponse(f"/admin/{model_name}", status_code=303) @router.get("/{model_name}/{record_id}", response_class=HTMLResponse) -@require_admin async def edit_record_form( request: Request, model_name: str, @@ -269,7 +263,6 @@ async def edit_record_form( ) @router.post("/{model_name}/{record_id}") -@require_admin async def update_record( request: Request, model_name: str, @@ -318,7 +311,6 @@ async def update_record( return RedirectResponse(f"/admin/{model_name}", status_code=303) @router.get("/{model_name}/{record_id}/delete") -@require_admin async def delete_record( request: Request, model_name: str, diff --git a/app/views/dashboard.py b/app/views/dashboard.py index ae5899b..af2d17d 100644 --- a/app/views/dashboard.py +++ b/app/views/dashboard.py @@ -6,59 +6,61 @@ from fastapi.responses import HTMLResponse, RedirectResponse from ..db import get_db from ..templates_config import templates -from ..auth import require_login from .. import models router = APIRouter() @router.get("/") -@require_login def user_dashboard(request: Request, db: Session = Depends(get_db)): """User dashboard showing teams, events and stats""" try: - user = request.session.get("user") - user_id = user.get("id") - + # Fetch user data directly from the database + user = db.query(models.User).filter_by(email="admin@example.com").first() # Example user lookup + if not user: + raise HTTPException(status_code=404, detail="User not found") + + user_id = user.id + # Initialize default values in case of errors team_count = 0 total_points = 0 event_count = 0 recent_events = [] user_teams = [] - + # Check if TeamMember model exists before querying if hasattr(models, "TeamMember"): # Get the team count for this user team_count = db.query(func.count(models.TeamMember.team_id))\ .filter(models.TeamMember.user_id == user_id)\ .scalar() or 0 - + # Get user teams user_teams = db.query(models.Team)\ .join(models.TeamMember)\ .filter(models.TeamMember.user_id == user_id)\ .all() - + # Check if UserPoints model exists before querying if hasattr(models, "UserPoints"): # Get the total points safely total_points_result = db.query(func.sum(models.UserPoints.points))\ .filter(models.UserPoints.user_id == user_id)\ .first() - + if total_points_result and total_points_result[0]: total_points = total_points_result[0] - + # Check if EventAttendee model exists before querying if hasattr(models, "EventAttendee") and hasattr(models, "Event"): # Get event count safely event_count_result = db.query(func.count(models.EventAttendee.event_id))\ .filter(models.EventAttendee.user_id == user_id)\ .first() - + if event_count_result and event_count_result[0]: event_count = event_count_result[0] - + # Recent events - only if both models exist recent_events = db.query(models.Event)\ .join(models.EventAttendee)\ @@ -66,7 +68,7 @@ def user_dashboard(request: Request, db: Session = Depends(get_db)): .order_by(models.Event.event_date.desc())\ .limit(5)\ .all() - + return templates.TemplateResponse( "dashboard/index.html", { @@ -82,3 +84,8 @@ def user_dashboard(request: Request, db: Session = Depends(get_db)): except Exception as e: print(f"Dashboard error: {str(e)}") raise HTTPException(status_code=500, detail=f"Dashboard error: {str(e)}") + +@router.get("/scan", response_class=HTMLResponse) +def scan_qr_page(request: Request): + """Render the QR code scanning page.""" + return templates.TemplateResponse("scan_qr.html", {"request": request}) diff --git a/app/views/redeem.py b/app/views/redeem.py index b0090c3..39d8fac 100644 --- a/app/views/redeem.py +++ b/app/views/redeem.py @@ -8,8 +8,6 @@ from sqlalchemy.orm import Session from ..db import SessionLocal from ..models import QRTicket, User, Team, TeamMembership from ..templates_config import templates -from ..dependencies import get_user_from_session -from ..auth import require_login router = APIRouter() @@ -24,7 +22,7 @@ def get_db(): 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. - If not logged in, prompt them. + No login required. """ ticket = db.query(QRTicket).filter_by(code=code, used=False).first() if not ticket: @@ -37,42 +35,24 @@ def redeem_code(code: str, request: Request, db: Session = Depends(get_db)): } ) - # Get the authenticated user - user = get_user_from_session(request, db) - if not user: - # 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) - - # Get user teams - user_teams = [m.team for m in user.memberships] if hasattr(user, 'memberships') else [] + # Get all available teams + all_teams = db.query(Team).all() return templates.TemplateResponse("redeem.html", { "request": request, "ticket": ticket, - "user_teams": user_teams + "user_teams": all_teams # Now showing all teams }) @router.post("/apply/{code}") -@require_login async def apply_code( request: Request, code: str, db: Session = Depends(get_db) ): """ - Apply the QR code to a selected team (if user is a member), - or set to pending if user isn't a member yet. + Apply the QR code to a selected team (without authentication) """ - # 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 form_data = await request.form() team_id = int(form_data.get("team_id", 0)) @@ -110,43 +90,28 @@ async def apply_code( } ) - # Check membership - membership = db.query(TeamMembership).filter_by(user_id=user.id, team_id=team.id).first() - if membership: - # Redeem - ticket.redeemed_by = user.id - ticket.redeemed_at_team = team.id - ticket.used = True + # Redeem without checking membership + ticket.redeemed_at_team = team.id + ticket.used = True - # If we have redeemed_at column, update it - if hasattr(ticket, 'redeemed_at'): - from datetime import datetime - ticket.redeemed_at = datetime.now() + # If we have redeemed_at column, update it + if hasattr(ticket, 'redeemed_at'): + from datetime import datetime + ticket.redeemed_at = datetime.now() - db.commit() + db.commit() - # Redirect to success page or dashboard - return templates.TemplateResponse( - "redeem_success.html", - { - "request": request, - "points": ticket.points, - "team": team - } - ) - else: - # In real app, create a pending record or request flow - return templates.TemplateResponse( - "error.html", - { - "request": request, - "error_title": "Not a Team Member", - "error_message": "You are not a member of this team. Please join the team first or select another team." - } - ) + # Redirect to success page or dashboard + return templates.TemplateResponse( + "redeem_success.html", + { + "request": request, + "points": ticket.points, + "team": team + } + ) @router.post("/manual") -@require_login async def manual_code_entry( request: Request, code: str = Form(...), @@ -156,11 +121,6 @@ async def manual_code_entry( Handle manual code entry from the form. 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 ticket = db.query(QRTicket).filter_by(code=code, used=False).first() diff --git a/app/views/teams.py b/app/views/teams.py index 96c07b9..f4440e1 100644 --- a/app/views/teams.py +++ b/app/views/teams.py @@ -13,8 +13,6 @@ from ..db import SessionLocal from ..models import Team, TeamMembership, User, QRTicket, TeamAchievement from ..schemas import TeamCreate from ..templates_config import templates -from ..dependencies import get_user_from_session -from ..auth import require_login router = APIRouter() @@ -30,13 +28,8 @@ def list_teams(request: Request, db: Session = Depends(get_db)): teams = db.query(Team).all() # Get the user's teams to highlight teams they're already in - user = get_user_from_session(request, db) user_team_ids = [] - if user: - memberships = db.query(TeamMembership).filter_by(user_id=user.id).all() - user_team_ids = [m.team_id for m in memberships] - return templates.TemplateResponse( "teams.html", { @@ -54,43 +47,28 @@ def list_teams(request: Request, db: Session = Depends(get_db)): ) @router.post("/create") -@require_login 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 new_team = Team(name=name) db.add(new_team) db.commit() db.refresh(new_team) - # Make user admin of team - membership = TeamMembership(user_id=user.id, team_id=new_team.id, is_admin=True) - db.add(membership) - db.commit() - return RedirectResponse("/teams/", status_code=303) @router.post("/join/{team_id}") -@require_login 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() if not team: return RedirectResponse("/teams/", status_code=303) # Check if membership exists - existing = db.query(TeamMembership).filter_by(user_id=user.id, team_id=team.id).first() + existing = db.query(TeamMembership).filter_by(team_id=team.id).first() if existing: return RedirectResponse("/teams/", status_code=303) # Create membership - new_member = TeamMembership(user_id=user.id, team_id=team.id, is_admin=False) + new_member = TeamMembership(team_id=team.id, is_admin=False) db.add(new_member) db.commit() @@ -104,12 +82,6 @@ def team_detail(request: Request, team_id: int, db: Session = Depends(get_db)): if not team: raise HTTPException(status_code=404, detail="Team not found") - # Get current user from session - 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 memberships = db.query(TeamMembership).filter_by(team_id=team_id).all() team_members = [] @@ -118,10 +90,6 @@ def team_detail(request: Request, team_id: int, db: Session = Depends(get_db)): for membership in memberships: member = db.query(User).filter_by(id=membership.user_id).first() if member: - # Check if current user is admin - if membership.user_id == user.id and membership.is_admin: - is_user_admin = True - # Use joined_at if available, otherwise use placeholder joined_date = getattr(membership, 'joined_at', None) or datetime.now() - timedelta(days=random.randint(30, 180)) if isinstance(joined_date, datetime): @@ -237,14 +205,12 @@ def team_detail(request: Request, team_id: int, db: Session = Depends(get_db)): "activities": activities, "performance": performance, "is_user_admin": is_user_admin, - "user": user, "days_ago": days_ago, "founded_date": founded_date_str } ) @router.post("/{team_id}/update") -@require_login def update_team( request: Request, team_id: int, @@ -253,10 +219,6 @@ def update_team( db: Session = Depends(get_db) ): """Update team details.""" - 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() if not team: @@ -264,7 +226,6 @@ def update_team( # Check if user is admin membership = db.query(TeamMembership).filter_by( - user_id=user.id, team_id=team.id, is_admin=True ).first() diff --git a/docker-compose.yml b/docker-compose.yml index d396487..093e5a5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,16 +33,10 @@ services: DB_NAME: "pubquiz_db" DB_USER: "pubquiz_user" DB_PASS: "pubquiz_pass" - # Session configuration - SECRET_KEY: "a-stronger-secret-key-for-sessions-32chars" - DEBUG: "True" - SESSION_MAX_AGE: "86400" # 24 hours - COOKIE_SECURE: "False" # Set to True in production with HTTPS # Add better error logging PYTHONUNBUFFERED: "1" - # Add dependency installation command - command: > - bash -c "pip install pymysql && uvicorn app.main:app --host 0.0.0.0 --reload" + # Just run uvicorn directly, no need for pymysql install since it's in requirements.txt + command: uvicorn app.main:app --host 0.0.0.0 --reload ports: - "8000:8000" volumes: diff --git a/requirements.txt b/requirements.txt index f5cdf1f..8b928ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,4 +25,7 @@ aiofiles>=23.2.1 python-dotenv>=1.0.0 email-validator>=2.0.0 pydantic>=2.3.0 -qrcode>=7.4.2 \ No newline at end of file +qrcode>=7.4.2 + +# Image processing library for QR code generation +Pillow>=9.0.0 \ No newline at end of file