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.11-slim
|
||||
FROM python:3.13
|
||||
|
||||
# Create working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies needed for mysqlclient
|
||||
RUN apt-get update && apt-get install -y \
|
||||
pkg-config \
|
||||
default-libmysqlclient-dev \
|
||||
build-essential \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
# Install system dependencies
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends gcc libc-dev default-libmysqlclient-dev && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements first (for caching)
|
||||
# Copy requirements first to leverage Docker cache
|
||||
COPY requirements.txt .
|
||||
|
||||
# Install dependencies
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Explicitly install pymysql (in case it's missing from requirements.txt)
|
||||
RUN pip install --no-cache-dir pymysql cryptography
|
||||
|
||||
# Copy the rest of the code
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
# Set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# Run the FastAPI app with Uvicorn
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
# Command to run when container starts
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--reload"]
|
||||
|
||||
+55
-30
@@ -16,6 +16,7 @@ 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()
|
||||
@@ -53,15 +54,9 @@ 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):
|
||||
"""Get current user from session with improved error handling"""
|
||||
try:
|
||||
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_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"""
|
||||
@@ -74,10 +69,12 @@ def require_login(func):
|
||||
@wraps(func)
|
||||
async def wrapper(request: Request, *args, **kwargs):
|
||||
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
|
||||
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)
|
||||
|
||||
# Check if the wrapped function is a coroutine function
|
||||
@@ -96,15 +93,14 @@ def require_admin(func):
|
||||
@wraps(func)
|
||||
async def wrapper(request: Request, *args, **kwargs):
|
||||
try:
|
||||
if "session" not in request.scope:
|
||||
return RedirectResponse(url="/auth/login", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
user = request.session.get("user")
|
||||
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.get("is_admin", False):
|
||||
if not user.is_admin:
|
||||
return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
# Check if the wrapped function is a coroutine function
|
||||
@@ -121,10 +117,10 @@ def require_admin(func):
|
||||
# Routes for authentication
|
||||
|
||||
@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"""
|
||||
# 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 templates.TemplateResponse(
|
||||
@@ -153,17 +149,21 @@ async def login(request: Request, db: Session = Depends(get_db)):
|
||||
status_code=status.HTTP_302_FOUND
|
||||
)
|
||||
|
||||
# Create user session
|
||||
# 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, # Use the actual is_admin value from the database
|
||||
"is_admin": is_admin_value,
|
||||
"picture": get_gravatar_url(user.email),
|
||||
"_permanent": True,
|
||||
"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:
|
||||
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 = {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
"is_active": user.is_active,
|
||||
"is_admin": is_admin_value, # Set the admin status correctly
|
||||
"is_active": getattr(user, "is_active", True),
|
||||
"is_admin": is_admin_value,
|
||||
"_permanent": True,
|
||||
"created_at": str(user.created_at),
|
||||
"created_at": str(getattr(user, "created_at", "")),
|
||||
}
|
||||
|
||||
# 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):
|
||||
"""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
|
||||
@@ -272,9 +276,9 @@ async def logout(request: Request):
|
||||
|
||||
@router.get("/profile")
|
||||
@require_login
|
||||
async def profile_page(request: Request):
|
||||
async def profile_page(request: Request, db: Session = Depends(get_db)):
|
||||
"""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})
|
||||
|
||||
@router.get("/register")
|
||||
@@ -327,6 +331,19 @@ async def register(request: Request, db: Session = Depends(get_db)):
|
||||
)
|
||||
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(
|
||||
@@ -343,7 +360,15 @@ async def registration_success(request: Request):
|
||||
)
|
||||
|
||||
@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"""
|
||||
user = request.session.get("user")
|
||||
return user or {"error": "Not authenticated"}
|
||||
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
|
||||
}
|
||||
|
||||
+54
-18
@@ -3,24 +3,50 @@ from fastapi.security import OAuth2PasswordBearer
|
||||
from sqlalchemy.orm import Session
|
||||
from .db import get_db
|
||||
from . import models
|
||||
from .auth import get_current_user, require_login, require_admin
|
||||
|
||||
# Reuse functions from auth.py
|
||||
# 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 configuration for API-based authentication
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token")
|
||||
|
||||
def get_current_active_user(request: Request):
|
||||
user = get_current_user(request)
|
||||
# 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,
|
||||
@@ -29,11 +55,21 @@ def get_current_active_user(request: Request):
|
||||
)
|
||||
return user
|
||||
|
||||
def get_current_admin_user(request: Request):
|
||||
user = get_current_user(request)
|
||||
if not user or not user.get("is_admin"):
|
||||
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)
|
||||
|
||||
+35
-26
@@ -10,7 +10,8 @@ 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, get_current_user
|
||||
from .auth import router as auth_router
|
||||
from .dependencies import get_user_from_session
|
||||
|
||||
# Create tables on startup
|
||||
init_db()
|
||||
@@ -19,24 +20,26 @@ init_db()
|
||||
# In a production app, you would handle this differently
|
||||
seed_db()
|
||||
|
||||
# Initialize FastAPI app
|
||||
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")
|
||||
if len(secret_key) < 32:
|
||||
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(
|
||||
SessionMiddleware,
|
||||
secret_key=secret_key,
|
||||
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",
|
||||
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")
|
||||
async def debug_session_middleware(request, call_next):
|
||||
"""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")
|
||||
|
||||
print(f"Request path: {request.url.path}")
|
||||
# Instead of checking request.scope, check if we can access the session dict
|
||||
has_session = hasattr(request, "session") and isinstance(request.session, dict)
|
||||
# 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}")
|
||||
|
||||
# Check session data
|
||||
if hasattr(request, "session"):
|
||||
try:
|
||||
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")
|
||||
# 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)}")
|
||||
|
||||
@@ -63,22 +64,27 @@ async def debug_session_middleware(request, call_next):
|
||||
|
||||
try:
|
||||
if hasattr(request, "session"):
|
||||
try:
|
||||
print(f"Session data after: {dict(request.session)}")
|
||||
except (TypeError, AttributeError):
|
||||
print(f"Session exists but isn't a dictionary")
|
||||
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 to make user available in templates
|
||||
# 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 user from session if available
|
||||
user = get_current_user(request)
|
||||
# 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
|
||||
@@ -88,17 +94,20 @@ async def add_user_to_request(request: Request, call_next):
|
||||
|
||||
# Debug output to check user and admin status
|
||||
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:
|
||||
print(f"Error setting user context: {str(e)}")
|
||||
finally:
|
||||
# Always close the DB connection
|
||||
if 'db' in locals():
|
||||
db.close()
|
||||
|
||||
# Process the request
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
# Routers
|
||||
app.include_router(auth_router, prefix="/auth", tags=["Auth"]) # Auth router should be first
|
||||
# Routers - include after middleware setup is complete
|
||||
app.include_router(auth_router, prefix="/auth", tags=["Auth"])
|
||||
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"])
|
||||
|
||||
@@ -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 ..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,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.
|
||||
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()
|
||||
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
|
||||
# For now, we mock a user ID = 1:
|
||||
user = db.query(User).filter_by(id=1).first()
|
||||
# Get the authenticated user
|
||||
user = get_user_from_session(request, db)
|
||||
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
|
||||
user_teams = [m.team for m in user.memberships]
|
||||
# Get user teams
|
||||
user_teams = [m.team for m in user.memberships] if hasattr(user, 'memberships') else []
|
||||
|
||||
return templates.TemplateResponse("redeem.html", {
|
||||
"request": request,
|
||||
@@ -45,6 +58,7 @@ def redeem_code(code: str, request: Request, db: Session = Depends(get_db)):
|
||||
})
|
||||
|
||||
@router.post("/apply/{code}")
|
||||
@require_login
|
||||
async def apply_code(
|
||||
request: Request,
|
||||
code: str,
|
||||
@@ -54,6 +68,11 @@ async def apply_code(
|
||||
Apply the QR code to a selected team (if user is a member),
|
||||
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
|
||||
form_data = await request.form()
|
||||
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()
|
||||
|
||||
if not team:
|
||||
@@ -129,6 +146,7 @@ async def apply_code(
|
||||
)
|
||||
|
||||
@router.post("/manual")
|
||||
@require_login
|
||||
async def manual_code_entry(
|
||||
request: Request,
|
||||
code: str = Form(...),
|
||||
@@ -138,6 +156,11 @@ 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()
|
||||
|
||||
|
||||
+24
-23
@@ -13,6 +13,8 @@ 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()
|
||||
|
||||
@@ -23,27 +25,12 @@ def get_db():
|
||||
finally:
|
||||
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)
|
||||
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 = db.query(User).filter_by(id=1).first()
|
||||
user = get_user_from_session(request, db)
|
||||
user_team_ids = []
|
||||
|
||||
if user:
|
||||
@@ -67,8 +54,11 @@ def list_teams(request: Request, db: Session = Depends(get_db)):
|
||||
)
|
||||
|
||||
@router.post("/create")
|
||||
def create_team(name: str = Form(...), db: Session = Depends(get_db)):
|
||||
user = get_or_create_default_user(db)
|
||||
@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)
|
||||
@@ -84,8 +74,11 @@ def create_team(name: str = Form(...), db: Session = Depends(get_db)):
|
||||
return RedirectResponse("/teams/", status_code=303)
|
||||
|
||||
@router.post("/join/{team_id}")
|
||||
def join_team(team_id: int, db: Session = Depends(get_db)):
|
||||
user = get_or_create_default_user(db)
|
||||
@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:
|
||||
@@ -111,8 +104,11 @@ 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 - using a default user for now
|
||||
user = get_or_create_default_user(db)
|
||||
# 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()
|
||||
@@ -248,14 +244,19 @@ def team_detail(request: Request, team_id: int, db: Session = Depends(get_db)):
|
||||
)
|
||||
|
||||
@router.post("/{team_id}/update")
|
||||
@require_login
|
||||
def update_team(
|
||||
request: Request,
|
||||
team_id: int,
|
||||
team_name: str = Form(...),
|
||||
is_public: bool = Form(False),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""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()
|
||||
|
||||
if not team:
|
||||
|
||||
@@ -48,5 +48,21 @@ services:
|
||||
volumes:
|
||||
- ./:/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:
|
||||
db_data:
|
||||
|
||||
+25
-15
@@ -1,18 +1,28 @@
|
||||
fastapi>=0.95.0
|
||||
uvicorn[standard]>=0.21.1
|
||||
sqlalchemy>=2.0.9
|
||||
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
|
||||
# Web framework and server
|
||||
fastapi>=0.103.1
|
||||
uvicorn>=0.23.2
|
||||
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
|
||||
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
|
||||
qrcode
|
||||
pydantic[email]>=1.10.7
|
||||
httpx
|
||||
email-validator>=2.0.0
|
||||
pydantic>=2.3.0
|
||||
qrcode>=7.4.2
|
||||
Reference in New Issue
Block a user