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.
This commit is contained in:
-374
@@ -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
|
||||
}
|
||||
+6
-1
@@ -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."""
|
||||
|
||||
@@ -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)
|
||||
+7
-81
@@ -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"])
|
||||
|
||||
@@ -62,37 +62,15 @@
|
||||
<a href="/dashboard" class="hover:text-golden-ale transition-colors duration-200">
|
||||
<i class="fas fa-tachometer-alt"></i> Dashboard
|
||||
</a>
|
||||
{% if current_user and current_user.is_admin %}
|
||||
<a href="/admin/" class="hover:text-golden-ale transition-colors duration-200">
|
||||
<i class="fas fa-lock"></i> Admin
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="/about" class="hover:text-golden-ale transition-colors duration-200">
|
||||
About
|
||||
</a>
|
||||
<a href="/contact" class="hover:text-golden-ale transition-colors duration-200">
|
||||
Contact
|
||||
</a>
|
||||
|
||||
<!-- User Authentication -->
|
||||
{% if current_user %}
|
||||
<div class="relative group">
|
||||
<button class="flex items-center focus:outline-none">
|
||||
<span class="mr-1">{{ current_user.username }}</span>
|
||||
<i class="fas fa-chevron-down text-xs"></i>
|
||||
</button>
|
||||
<div class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg overflow-hidden z-20 hidden group-hover:block">
|
||||
<a href="/auth/profile" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Profile</a>
|
||||
<a href="/dashboard/" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Dashboard</a>
|
||||
<div class="border-t border-gray-100"></div>
|
||||
<a href="/auth/logout" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<a href="/auth/login" class="bg-white text-irish-green px-4 py-1 rounded-md hover:bg-cream-white transition-colors duration-200">
|
||||
Log In
|
||||
</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
@@ -116,34 +94,15 @@
|
||||
<a href="/dashboard" class="hover:bg-green-800 py-2 px-3 rounded-md transition-colors duration-200">
|
||||
<i class="fas fa-tachometer-alt"></i> Dashboard
|
||||
</a>
|
||||
{% if current_user and current_user.is_admin %}
|
||||
<a href="/admin/" class="hover:bg-green-800 py-2 px-3 rounded-md transition-colors duration-200">
|
||||
<i class="fas fa-lock"></i> Admin
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="/about" class="hover:bg-green-800 py-2 px-3 rounded-md transition-colors duration-200">
|
||||
About
|
||||
</a>
|
||||
<a href="/contact" class="hover:bg-green-800 py-2 px-3 rounded-md transition-colors duration-200">
|
||||
Contact
|
||||
</a>
|
||||
|
||||
<!-- Mobile Auth Links -->
|
||||
{% if current_user %}
|
||||
<a href="/auth/profile" class="hover:bg-green-800 py-2 px-3 rounded-md transition-colors duration-200">
|
||||
<i class="fas fa-user"></i> {{ current_user.username }}
|
||||
</a>
|
||||
<a href="/auth/logout" class="hover:bg-green-800 py-2 px-3 rounded-md transition-colors duration-200">
|
||||
<i class="fas fa-sign-out-alt"></i> Logout
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="/auth/login" class="bg-white text-irish-green py-2 px-3 rounded-md">
|
||||
Log In
|
||||
</a>
|
||||
<a href="/auth/register" class="border border-white text-white py-2 px-3 rounded-md">
|
||||
Sign Up
|
||||
</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+63
-40
@@ -19,6 +19,14 @@
|
||||
<div class="mt-4 text-sm text-gray-500">
|
||||
Make sure the QR code is well-lit and clearly visible
|
||||
</div>
|
||||
|
||||
<!-- Camera Selection Dropdown -->
|
||||
<div class="mt-4">
|
||||
<label for="camera-select" class="block text-sm font-medium text-gray-700">Select Camera:</label>
|
||||
<select id="camera-select" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-irish-green focus:border-irish-green sm:text-sm rounded-md">
|
||||
<option value="">Loading cameras...</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Manual Entry Fallback -->
|
||||
<div class="mt-8 border-t pt-6">
|
||||
@@ -78,50 +86,65 @@
|
||||
<!-- HTML5 QR Code Scanner Library -->
|
||||
<script src="https://unpkg.com/html5-qrcode"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
const cameraSelect = document.getElementById('camera-select');
|
||||
const html5QrCode = new Html5Qrcode("qr-reader");
|
||||
const qrOverlay = document.getElementById('scanner-overlay');
|
||||
|
||||
// Config
|
||||
const config = { fps: 10, qrbox: 250 };
|
||||
|
||||
// Success function
|
||||
function onScanSuccess(decodedText, decodedResult) {
|
||||
// Stop scanning
|
||||
html5QrCode.stop();
|
||||
|
||||
// Show loading message
|
||||
qrOverlay.innerHTML = '<div class="text-white text-center"><i class="fas fa-circle-notch fa-spin text-3xl mb-3"></i><p>Code detected!</p><p>Redirecting...</p></div>';
|
||||
qrOverlay.style.backgroundColor = 'rgba(0, 104, 55, 0.8)'; // Irish green with opacity
|
||||
|
||||
// Redirect to redeem page
|
||||
window.location.href = '/redeem/' + decodedText;
|
||||
|
||||
// Fetch available cameras
|
||||
try {
|
||||
const devices = await Html5Qrcode.getCameras();
|
||||
cameraSelect.innerHTML = devices.map(device => `<option value="${device.id}">${device.label || 'Camera'}</option>`).join('');
|
||||
} catch (error) {
|
||||
console.error("Error fetching cameras: ", error);
|
||||
cameraSelect.innerHTML = '<option value="">No cameras found</option>';
|
||||
}
|
||||
|
||||
// Start scanner
|
||||
html5QrCode.start(
|
||||
{ facingMode: "environment" },
|
||||
config,
|
||||
onScanSuccess,
|
||||
(errorMessage) => {
|
||||
// Handle error if needed
|
||||
console.log(errorMessage);
|
||||
})
|
||||
.then(() => {
|
||||
// Scanner started successfully
|
||||
qrOverlay.style.display = 'none';
|
||||
})
|
||||
.catch((err) => {
|
||||
// Update overlay with error message
|
||||
qrOverlay.innerHTML = `
|
||||
<div class="text-white text-center">
|
||||
<i class="fas fa-exclamation-triangle text-4xl mb-3"></i>
|
||||
<p>Camera access denied or not available</p>
|
||||
<p class="text-sm mt-2">Please use manual entry below</p>
|
||||
</div>
|
||||
`;
|
||||
qrOverlay.style.backgroundColor = 'rgba(178, 34, 34, 0.8)'; // Guinness red with opacity
|
||||
});
|
||||
// Start scanner on camera selection
|
||||
cameraSelect.addEventListener('change', async function() {
|
||||
const selectedCameraId = cameraSelect.value;
|
||||
if (selectedCameraId) {
|
||||
try {
|
||||
// Stop the scanner if it is running
|
||||
if (html5QrCode && html5QrCode.isScanning) {
|
||||
await html5QrCode.stop();
|
||||
}
|
||||
|
||||
// Start the scanner with the newly selected camera
|
||||
await html5QrCode.start(
|
||||
{ deviceId: { exact: selectedCameraId } },
|
||||
{ fps: 10, qrbox: { width: 250, height: 250 } },
|
||||
onScanSuccess,
|
||||
(errorMessage) => {
|
||||
if (!errorMessage.includes("No MultiFormat Readers")) {
|
||||
console.log("QR code parse error: ", errorMessage);
|
||||
}
|
||||
}
|
||||
);
|
||||
qrOverlay.style.display = 'none';
|
||||
} catch (err) {
|
||||
console.error("Error switching camera: ", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Success function
|
||||
function onScanSuccess(decodedText, decodedResult) {
|
||||
if (html5QrCode && html5QrCode.isScanning) {
|
||||
html5QrCode.stop().then(() => {
|
||||
console.log("Scanner stopped successfully.");
|
||||
qrOverlay.innerHTML = '<div class="text-white text-center"><i class="fas fa-circle-notch fa-spin text-3xl mb-3"></i><p>Code detected!</p><p>Redirecting...</p></div>';
|
||||
qrOverlay.style.backgroundColor = 'rgba(0, 104, 55, 0.8)';
|
||||
qrOverlay.style.display = 'flex';
|
||||
window.location.href = '/redeem/' + decodedText;
|
||||
}).catch(err => {
|
||||
console.error("Error stopping the scanner: ", err);
|
||||
window.location.href = '/redeem/' + decodedText;
|
||||
});
|
||||
} else {
|
||||
window.location.href = '/redeem/' + decodedText;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -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,
|
||||
|
||||
+20
-13
@@ -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})
|
||||
|
||||
+22
-62
@@ -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()
|
||||
|
||||
|
||||
+2
-41
@@ -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()
|
||||
|
||||
+2
-8
@@ -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:
|
||||
|
||||
+4
-1
@@ -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
|
||||
qrcode>=7.4.2
|
||||
|
||||
# Image processing library for QR code generation
|
||||
Pillow>=9.0.0
|
||||
Reference in New Issue
Block a user