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 %} -