Refactor user authentication and dashboard features
- Added `is_admin` field to User model for role management. - Updated user context middleware to include current user in templates. - Enhanced session management with improved debug middleware. - Refactored dashboard view to fetch user-specific data and recent events. - Improved login and registration templates for better user experience. - Added admin routes with access control for admin users. - Updated Docker configuration for better error logging and dependency management. - Updated requirements to include new dependencies and specify versions.
This commit is contained in:
@@ -18,6 +18,9 @@ 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 . .
|
||||
|
||||
|
||||
+333
-6
@@ -1,14 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Skeleton for authentication logic.
|
||||
Placeholder for OAuth or password-based login.
|
||||
Authentication system using Authlib and session-based auth
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
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 .db import SessionLocal
|
||||
from . import models
|
||||
from passlib.context import CryptContext
|
||||
|
||||
from .db import SessionLocal
|
||||
from .templates_config import templates
|
||||
from . import models
|
||||
|
||||
# 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")
|
||||
|
||||
@@ -19,4 +45,305 @@ def get_db():
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# Add routes for login, logout, register, etc.
|
||||
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):
|
||||
"""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_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:
|
||||
if "session" not in request.scope or not request.session.get("user"):
|
||||
# Store the current URL for redirecting after login
|
||||
if "session" in request.scope:
|
||||
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:
|
||||
if "session" not in request.scope:
|
||||
return RedirectResponse(url="/auth/login", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
user = request.session.get("user")
|
||||
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):
|
||||
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):
|
||||
"""Show login page with appropriate authentication options"""
|
||||
# If already logged in, redirect to home
|
||||
if request.session.get("user"):
|
||||
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
|
||||
)
|
||||
|
||||
# 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
|
||||
|
||||
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
|
||||
"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 info in session
|
||||
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
|
||||
"_permanent": True,
|
||||
"created_at": str(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)
|
||||
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):
|
||||
"""Show user profile page"""
|
||||
user = request.session.get("user")
|
||||
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()
|
||||
|
||||
# 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):
|
||||
"""API endpoint to get current user information"""
|
||||
user = request.session.get("user")
|
||||
return user or {"error": "Not authenticated"}
|
||||
|
||||
@@ -3,19 +3,27 @@ import os
|
||||
from sqlalchemy import create_engine, inspect, text
|
||||
from sqlalchemy.orm import sessionmaker, declarative_base
|
||||
|
||||
DB_HOST = os.getenv("DB_HOST", "localhost")
|
||||
DB_PORT = os.getenv("DB_PORT", "3306")
|
||||
DB_NAME = os.getenv("DB_NAME", "pubquiz_db")
|
||||
DB_USER = os.getenv("DB_USER", "pubquiz_user")
|
||||
DB_PASS = os.getenv("DB_PASS", "pubquiz_pass")
|
||||
# Get database connection details from environment variables with fallbacks
|
||||
DB_HOST = os.environ.get("DB_HOST", "localhost")
|
||||
DB_PORT = os.environ.get("DB_PORT", "3306")
|
||||
DB_NAME = os.environ.get("DB_NAME", "pubquiz_db")
|
||||
DB_USER = os.environ.get("DB_USER", "pubquiz_user")
|
||||
DB_PASS = os.environ.get("DB_PASS", "pubquiz_pass")
|
||||
|
||||
SQLALCHEMY_DATABASE_URL = (
|
||||
f"mysql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
|
||||
# Create database URL
|
||||
SQLALCHEMY_DATABASE_URL = f"mysql+pymysql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
|
||||
|
||||
# Create engine with appropriate parameters
|
||||
engine = create_engine(
|
||||
SQLALCHEMY_DATABASE_URL,
|
||||
pool_pre_ping=True,
|
||||
pool_recycle=3600,
|
||||
)
|
||||
|
||||
engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True)
|
||||
# Create session factory
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
# Create base class for models
|
||||
Base = declarative_base()
|
||||
|
||||
def init_db():
|
||||
@@ -146,3 +154,12 @@ def migrate_schema():
|
||||
print(f"Error during schema migration: {e}")
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
# Add the missing get_db function
|
||||
def get_db():
|
||||
"""Database dependency for FastAPI endpoints"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
+35
-8
@@ -9,6 +9,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from .models import User, Team, TeamMembership, QRTicket
|
||||
from .db import SessionLocal
|
||||
from .auth import get_password_hash
|
||||
|
||||
def table_has_column(engine, table_name, column_name):
|
||||
"""Check if a table has a specific column."""
|
||||
@@ -28,13 +29,39 @@ def seed_db():
|
||||
print("Database already has data. Skipping seeding.")
|
||||
return
|
||||
|
||||
# Create users
|
||||
# Create users with properly hashed passwords
|
||||
users = [
|
||||
User(username="john_quizmaster", email="john@example.com", hashed_password="password123"),
|
||||
User(username="sarah_johnson", email="sarah@example.com", hashed_password="password123"),
|
||||
User(username="mike_peters", email="mike@example.com", hashed_password="password123"),
|
||||
User(username="emma_wilson", email="emma@example.com", hashed_password="password123"),
|
||||
User(username="robert_brown", email="robert@example.com", hashed_password="password123"),
|
||||
User(
|
||||
username="admin",
|
||||
email="admin@example.com",
|
||||
hashed_password=get_password_hash("password"),
|
||||
is_admin=True # Set admin privileges
|
||||
),
|
||||
User(
|
||||
username="john_quizmaster",
|
||||
email="john@example.com",
|
||||
hashed_password=get_password_hash("password123")
|
||||
),
|
||||
User(
|
||||
username="sarah_johnson",
|
||||
email="sarah@example.com",
|
||||
hashed_password=get_password_hash("password123")
|
||||
),
|
||||
User(
|
||||
username="mike_peters",
|
||||
email="mike@example.com",
|
||||
hashed_password=get_password_hash("password123")
|
||||
),
|
||||
User(
|
||||
username="emma_wilson",
|
||||
email="emma@example.com",
|
||||
hashed_password=get_password_hash("password123")
|
||||
),
|
||||
User(
|
||||
username="robert_brown",
|
||||
email="robert@example.com",
|
||||
hashed_password=get_password_hash("password123")
|
||||
),
|
||||
]
|
||||
db.add_all(users)
|
||||
db.commit()
|
||||
@@ -62,8 +89,8 @@ def seed_db():
|
||||
memberships = []
|
||||
membership_data = [
|
||||
# Quiz Wizards
|
||||
(1, 1, True, 160),
|
||||
(2, 1, False, 155),
|
||||
(1, 1, True, 160), # Admin user is team admin of Quiz Wizards
|
||||
(2, 1, True, 155),
|
||||
(3, 1, False, 130),
|
||||
(4, 1, False, 90),
|
||||
(5, 1, False, 45),
|
||||
|
||||
+29
-100
@@ -1,110 +1,39 @@
|
||||
from fastapi import Depends, HTTPException, status, Request
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from jose import JWTError, jwt
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import inspect
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from .db import get_db
|
||||
from . import models
|
||||
from .auth import get_current_user, require_login, require_admin
|
||||
|
||||
from .db import SessionLocal, engine
|
||||
from .models import User
|
||||
from .security import SECRET_KEY, ALGORITHM
|
||||
from .templates_config import templates
|
||||
# Reuse functions from auth.py
|
||||
# This is just for backward compatibility with any code that imported these from dependencies
|
||||
|
||||
# OAuth2 scheme for token authentication
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False)
|
||||
# Function to get a db session
|
||||
def get_session_db():
|
||||
return next(get_db())
|
||||
|
||||
def get_db():
|
||||
"""Database dependency."""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
# Function to get current authenticated user
|
||||
def get_authenticated_user(request: Request):
|
||||
return get_current_user(request)
|
||||
|
||||
# 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 []
|
||||
|
||||
async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
|
||||
"""Get the current authenticated user based on the access token."""
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# If no token, return None (not authenticated)
|
||||
if not token:
|
||||
return None
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
username: str = payload.get("sub")
|
||||
if username is None:
|
||||
raise credentials_exception
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
|
||||
user = db.query(User).filter(User.username == username).first()
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
|
||||
# Update last login time if column exists
|
||||
columns = get_available_user_columns()
|
||||
if 'last_login' in columns and hasattr(user, 'last_login'):
|
||||
user.last_login = datetime.utcnow()
|
||||
db.commit()
|
||||
# These are kept for API backward compatibility
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token")
|
||||
|
||||
def get_current_active_user(request: Request):
|
||||
user = get_current_user(request)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Not authenticated",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
return user
|
||||
|
||||
async def get_current_active_user(current_user: User = Depends(get_current_user)):
|
||||
"""Check if the current user is active."""
|
||||
if not current_user:
|
||||
return None
|
||||
|
||||
columns = get_available_user_columns()
|
||||
if 'is_active' in columns and hasattr(current_user, 'is_active') and not current_user.is_active:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
|
||||
return current_user
|
||||
|
||||
# Improved session-based user lookup with better error handling and logging
|
||||
async def get_user_from_session(request: Request):
|
||||
"""Get current user from session with improved error handling"""
|
||||
try:
|
||||
if not hasattr(request, "session"):
|
||||
print("No session attribute in request")
|
||||
return None
|
||||
|
||||
user_id = request.session.get("user_id")
|
||||
if not user_id:
|
||||
print("No user_id in session")
|
||||
return None
|
||||
|
||||
print(f"Looking up user with ID: {user_id}")
|
||||
# Manually get a database session from get_db
|
||||
db = next(get_db())
|
||||
try:
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
print(f"User with ID {user_id} not found in database")
|
||||
# Clear invalid session data
|
||||
request.session.clear()
|
||||
return None
|
||||
return user
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting user from session: {str(e)}")
|
||||
return None
|
||||
|
||||
# Template context processor to add user to all templates
|
||||
async def add_user_to_templates(request: Request):
|
||||
"""Add current user to all template contexts."""
|
||||
user = await get_user_from_session(request)
|
||||
return {"current_user": user}
|
||||
def get_current_admin_user(request: Request):
|
||||
user = get_current_user(request)
|
||||
if not user or not user.get("is_admin"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not enough permissions",
|
||||
)
|
||||
return user
|
||||
|
||||
+59
-51
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
from fastapi import FastAPI, Request, status
|
||||
from fastapi import FastAPI, Request, status, Depends
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
from datetime import datetime
|
||||
@@ -8,9 +8,9 @@ import os
|
||||
from .db import init_db, engine
|
||||
from . import models
|
||||
from .templates_config import templates
|
||||
from .views import qr, redeem, teams, admin, leaderboard, dashboard, auth
|
||||
from .views import qr, redeem, teams, admin, leaderboard, dashboard
|
||||
from .db_init import seed_db
|
||||
from .dependencies import get_user_from_session
|
||||
from .auth import router as auth_router, get_current_user
|
||||
|
||||
# Create tables on startup
|
||||
init_db()
|
||||
@@ -21,71 +21,84 @@ seed_db()
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# IMPORTANT: Add SessionMiddleware FIRST before any other middleware
|
||||
# This ensures session data is available to all other middleware and route handlers
|
||||
# Configure session middleware with environment variables or defaults
|
||||
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
|
||||
app.add_middleware(
|
||||
SessionMiddleware,
|
||||
secret_key=os.environ.get("SECRET_KEY", "a-default-secret-key-for-sessions"),
|
||||
max_age=int(os.environ.get("SESSION_MAX_AGE", 86400)), # 24 hours
|
||||
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
|
||||
https_only=os.environ.get("COOKIE_SECURE", "False").lower() == "true",
|
||||
session_cookie="league_ledger_session", # Custom cookie name for clarity
|
||||
session_cookie="league_ledger_session", # Custom cookie name
|
||||
)
|
||||
|
||||
# Add debugging middleware to help track sessions
|
||||
# Debug middleware to track session state
|
||||
@app.middleware("http")
|
||||
async def debug_session_middleware(request, call_next):
|
||||
"""Debug middleware to track session state"""
|
||||
session_cookie = request.cookies.get("league_ledger_session")
|
||||
try:
|
||||
session_cookie = request.cookies.get("league_ledger_session")
|
||||
|
||||
print(f"Request path: {request.url.path}")
|
||||
print(f"Has session attribute: {'session' in request.scope}")
|
||||
print(f"Has session cookie: {session_cookie is not None}")
|
||||
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)
|
||||
print(f"Has session attribute: {has_session}")
|
||||
print(f"Has session cookie: {session_cookie is not None}")
|
||||
|
||||
if "session" in request.scope:
|
||||
print(f"Session data before: {dict(request.session)}")
|
||||
# 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")
|
||||
except Exception as e:
|
||||
print(f"Error in debug middleware (pre): {str(e)}")
|
||||
|
||||
response = await call_next(request)
|
||||
|
||||
if "session" in request.scope:
|
||||
print(f"Session data after: {dict(request.session)}")
|
||||
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")
|
||||
except Exception as e:
|
||||
print(f"Error in debug middleware (post): {str(e)}")
|
||||
|
||||
return response
|
||||
|
||||
# Update the template globals at app startup to access the request
|
||||
# User context middleware to make user available in templates
|
||||
@app.middleware("http")
|
||||
async def add_user_to_request(request: Request, call_next):
|
||||
# Print debugging information
|
||||
print(f"Processing request to: {request.url.path}")
|
||||
|
||||
# Add user to request state so templates can access it
|
||||
"""Add user to request state and update template globals"""
|
||||
try:
|
||||
if "session" in request.scope:
|
||||
print("Session found in request scope")
|
||||
if "user_id" in request.session:
|
||||
print(f"User ID in session: {request.session['user_id']}")
|
||||
# Get user from session
|
||||
user = await get_user_from_session(request)
|
||||
request.state.user = user
|
||||
else:
|
||||
print("No user_id in session")
|
||||
request.state.user = None
|
||||
else:
|
||||
print("No session in request scope")
|
||||
request.state.user = None
|
||||
except Exception as e:
|
||||
print(f"Error in middleware: {e}")
|
||||
request.state.user = None
|
||||
# Get user from session if available
|
||||
user = get_current_user(request)
|
||||
|
||||
# Update template context with current user before processing the request
|
||||
templates.env.globals["current_user"] = request.state.user
|
||||
# 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.get('username')}, Admin: {user.get('is_admin', False)}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error setting user context: {str(e)}")
|
||||
|
||||
# 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
|
||||
app.include_router(auth_router, prefix="/auth", tags=["Auth"]) # Auth router should be first
|
||||
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"])
|
||||
@@ -97,34 +110,29 @@ app.include_router(dashboard.router, prefix="/dashboard", tags=["Dashboard"])
|
||||
def index(request: Request):
|
||||
return templates.TemplateResponse("index.html", {
|
||||
"request": request,
|
||||
"now": datetime.now,
|
||||
"current_user": getattr(request.state, "user", None)
|
||||
"now": datetime.now
|
||||
})
|
||||
|
||||
@app.get("/about", response_class=HTMLResponse)
|
||||
def about(request: Request):
|
||||
return templates.TemplateResponse("about.html", {
|
||||
"request": request,
|
||||
"current_user": getattr(request.state, "user", None)
|
||||
"request": request
|
||||
})
|
||||
|
||||
@app.get("/contact", response_class=HTMLResponse)
|
||||
def contact(request: Request):
|
||||
return templates.TemplateResponse("contact.html", {
|
||||
"request": request,
|
||||
"current_user": getattr(request.state, "user", None)
|
||||
"request": request
|
||||
})
|
||||
|
||||
@app.get("/privacy", response_class=HTMLResponse)
|
||||
def privacy(request: Request):
|
||||
return templates.TemplateResponse("privacy.html", {
|
||||
"request": request,
|
||||
"current_user": getattr(request.state, "user", None)
|
||||
"request": request
|
||||
})
|
||||
|
||||
@app.get("/terms", response_class=HTMLResponse)
|
||||
def terms(request: Request):
|
||||
return templates.TemplateResponse("terms.html", {
|
||||
"request": request,
|
||||
"current_user": getattr(request.state, "user", None)
|
||||
"request": request
|
||||
})
|
||||
|
||||
+90
-9
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, Boolean, DateTime, Text, Table
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, Boolean, DateTime, Text, Float
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from datetime import datetime
|
||||
from .db import Base
|
||||
|
||||
class User(Base):
|
||||
@@ -11,15 +12,20 @@ class User(Base):
|
||||
email = Column(String(255), unique=True, index=True, nullable=False)
|
||||
hashed_password = Column(String(255), nullable=True)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_verified = Column(Boolean, default=False)
|
||||
is_admin = Column(Boolean, default=False)
|
||||
verification_token = Column(String(255), nullable=True)
|
||||
reset_token = Column(String(255), nullable=True)
|
||||
reset_token_expires_at = Column(DateTime, nullable=True)
|
||||
last_login = Column(DateTime, nullable=True)
|
||||
|
||||
# Relationship to teams
|
||||
# Relationships
|
||||
memberships = relationship("TeamMembership", back_populates="user")
|
||||
teams = relationship("TeamMember", back_populates="user")
|
||||
points = relationship("UserPoints", back_populates="user")
|
||||
events_attended = relationship("EventAttendee", back_populates="user")
|
||||
|
||||
|
||||
class OAuthAccount(Base):
|
||||
@@ -40,14 +46,14 @@ class Team(Base):
|
||||
__tablename__ = "teams"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(100), unique=True, nullable=False)
|
||||
|
||||
# Add fields for team detail view
|
||||
description = Column(Text, nullable=True)
|
||||
is_public = Column(Boolean, default=False) # For team privacy setting
|
||||
created_at = Column(DateTime, server_default=func.now()) # For team founded date
|
||||
description = Column(Text, nullable=True) # Optional team description
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
# Relationship to memberships
|
||||
# Relationships
|
||||
memberships = relationship("TeamMembership", back_populates="team")
|
||||
members = relationship("TeamMember", back_populates="team")
|
||||
|
||||
|
||||
class TeamMembership(Base):
|
||||
@@ -56,14 +62,25 @@ class TeamMembership(Base):
|
||||
user_id = Column(Integer, ForeignKey("users.id"))
|
||||
team_id = Column(Integer, ForeignKey("teams.id"))
|
||||
is_admin = Column(Boolean, default=False)
|
||||
|
||||
# Add joined_at to track when members joined
|
||||
joined_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
user = relationship("User", back_populates="memberships")
|
||||
team = relationship("Team", back_populates="memberships")
|
||||
|
||||
|
||||
class TeamMember(Base):
|
||||
__tablename__ = "team_members"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
team_id = Column(Integer, ForeignKey("teams.id"), nullable=False)
|
||||
is_captain = Column(Boolean, default=False)
|
||||
joined_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", back_populates="teams")
|
||||
team = relationship("Team", back_populates="members")
|
||||
|
||||
|
||||
class QRTicket(Base):
|
||||
__tablename__ = "qr_tickets"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
@@ -81,7 +98,6 @@ class QRTicket(Base):
|
||||
event_name = Column(String(255), nullable=True)
|
||||
|
||||
|
||||
# New model for team achievements
|
||||
class TeamAchievement(Base):
|
||||
__tablename__ = "team_achievements"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
@@ -92,3 +108,68 @@ class TeamAchievement(Base):
|
||||
achieved_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
team = relationship("Team")
|
||||
|
||||
|
||||
class Event(Base):
|
||||
__tablename__ = "events"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(100), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
location = Column(String(200), nullable=True)
|
||||
event_date = Column(DateTime, nullable=False)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
# Relationships
|
||||
attendees = relationship("EventAttendee", back_populates="event")
|
||||
|
||||
|
||||
class EventAttendee(Base):
|
||||
__tablename__ = "event_attendees"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
event_id = Column(Integer, ForeignKey("events.id"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
check_in_time = Column(DateTime, server_default=func.now())
|
||||
|
||||
# Relationships
|
||||
event = relationship("Event", back_populates="attendees")
|
||||
user = relationship("User", back_populates="events_attended")
|
||||
|
||||
|
||||
class UserPoints(Base):
|
||||
__tablename__ = "user_points"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
points = Column(Float, nullable=False, default=0)
|
||||
reason = Column(String(200), nullable=True)
|
||||
awarded_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", back_populates="points")
|
||||
|
||||
|
||||
class QRCode(Base):
|
||||
__tablename__ = "qr_codes"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
code = Column(String(100), unique=True, index=True, nullable=False)
|
||||
points = Column(Float, default=1.0, nullable=False)
|
||||
description = Column(String(200), nullable=True)
|
||||
is_active = Column(Boolean, default=True)
|
||||
max_uses = Column(Integer, nullable=True) # null = unlimited
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
expires_at = Column(DateTime, nullable=True) # null = never expires
|
||||
|
||||
# Relationships
|
||||
redemptions = relationship("QRCodeRedemption", back_populates="qr_code")
|
||||
|
||||
|
||||
class QRCodeRedemption(Base):
|
||||
__tablename__ = "qr_code_redemptions"
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
qr_code_id = Column(Integer, ForeignKey("qr_codes.id"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
redeemed_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
# Relationships
|
||||
qr_code = relationship("QRCode", back_populates="redemptions")
|
||||
user = relationship("User")
|
||||
|
||||
+6
-10
@@ -1,18 +1,14 @@
|
||||
# Add is_admin field to User model if it's missing
|
||||
|
||||
from sqlalchemy import Column, Integer, String, Boolean
|
||||
# ...existing imports...
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime
|
||||
from sqlalchemy.sql import func
|
||||
from ..db import Base
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
# ...existing fields...
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String(50), unique=True, index=True)
|
||||
email = Column(String(100), unique=True, index=True)
|
||||
password = Column(String(255))
|
||||
|
||||
# Add is_admin field if it doesn't exist
|
||||
password_hash = Column(String(255)) # Renamed from password to password_hash for clarity
|
||||
is_admin = Column(Boolean, default=False)
|
||||
|
||||
# ...existing methods...
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
@@ -5,24 +5,25 @@
|
||||
<h1 class="text-2xl font-bold text-irish-green mb-6 text-center">Log In</h1>
|
||||
|
||||
<!-- Messages/Alerts -->
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="mb-4 p-3 rounded {% if message.type == 'error' %}bg-red-100 text-red-700{% else %}bg-green-100 text-green-700{% endif %}">
|
||||
{{ message.text }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if error %}
|
||||
<div class="mb-4 p-3 rounded bg-red-100 text-red-700">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if message %}
|
||||
<div class="mb-4 p-3 rounded bg-green-100 text-green-700">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form action="/auth/login" method="post" class="space-y-4">
|
||||
<input type="hidden" name="next" value="{{ next }}">
|
||||
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700 mb-1">Username or Email</label>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700 mb-1">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
value="{{ username or '' }}"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-irish-green focus:border-irish-green"
|
||||
>
|
||||
@@ -62,10 +63,6 @@
|
||||
Log In
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="text-center text-sm mt-4">
|
||||
<a href="/auth/forgot-password" class="text-irish-green hover:text-opacity-80">Forgot password?</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 pt-6 border-t border-gray-200 text-center">
|
||||
@@ -75,7 +72,17 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- OAuth login options (to be implemented later) -->
|
||||
<!-- OAuth login options -->
|
||||
{% if show_oauth %}
|
||||
<div class="mt-6 pt-6 border-t border-gray-200">
|
||||
<p class="text-center text-gray-600 mb-4">Or sign in with</p>
|
||||
<div class="flex justify-center">
|
||||
<a href="/auth/oauth-login" class="bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-opacity-90 transition w-full flex items-center justify-center">
|
||||
<i class="fas fa-sign-in-alt mr-2"></i> {{ oauth_provider_name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mt-6 pt-6 border-t border-gray-200">
|
||||
<p class="text-center text-gray-600 mb-4">Or sign in with</p>
|
||||
<div class="flex justify-center space-x-4">
|
||||
@@ -86,8 +93,9 @@
|
||||
<i class="fab fa-github mr-2"></i> GitHub
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-center text-gray-500 text-xs mt-2">OAuth login coming soon</p>
|
||||
<p class="text-center text-gray-500 text-xs mt-2">OAuth login currently disabled</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
+39
-168
@@ -1,184 +1,55 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="bg-white rounded-lg shadow-md overflow-hidden mb-8">
|
||||
<!-- Profile Header -->
|
||||
<div class="bg-irish-green text-white p-6">
|
||||
<div class="flex flex-col sm:flex-row items-center">
|
||||
<div class="w-24 h-24 rounded-full bg-white border-4 border-white overflow-hidden mb-4 sm:mb-0 sm:mr-6">
|
||||
<img src="https://via.placeholder.com/150" alt="Profile" class="w-full h-full object-cover">
|
||||
</div>
|
||||
<div class="text-center sm:text-left">
|
||||
<h1 class="text-2xl font-bold">{{ user.username }}</h1>
|
||||
<p class="text-green-100">Member since {{ user.created_at.strftime('%B %Y') }}</p>
|
||||
<p class="mt-2">
|
||||
{% if user.is_verified %}
|
||||
<span class="bg-golden-ale text-black-stout text-xs px-2 py-1 rounded-full font-medium mr-1">Verified</span>
|
||||
{% endif %}
|
||||
<span class="bg-white text-irish-green text-xs px-2 py-1 rounded-full font-medium">{{ user.memberships|length }} Teams</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="max-w-3xl mx-auto my-8">
|
||||
<div class="bg-white p-8 rounded-lg shadow-md">
|
||||
<div class="flex flex-col md:flex-row items-center md:items-start md:space-x-8">
|
||||
<!-- Profile Image -->
|
||||
<div class="mb-6 md:mb-0">
|
||||
{% if user.picture %}
|
||||
<img src="{{ user.picture }}" alt="Profile Picture" class="w-32 h-32 rounded-full object-cover border-4 border-irish-green">
|
||||
{% else %}
|
||||
<div class="w-32 h-32 rounded-full bg-irish-green flex items-center justify-center text-white text-4xl">
|
||||
{{ user.username[0]|upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Messages/Alerts -->
|
||||
{% if messages %}
|
||||
<div class="p-4">
|
||||
{% for message in messages %}
|
||||
<div class="mb-4 p-3 rounded {% if message.type == 'error' %}bg-red-100 text-red-700{% else %}bg-green-100 text-green-700{% endif %}">
|
||||
{{ message.text }}
|
||||
<!-- User Info -->
|
||||
<div class="flex-grow">
|
||||
<h1 class="text-2xl font-bold text-irish-green">{{ user.username }}</h1>
|
||||
<p class="text-gray-600 mb-4">{{ user.email }}</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div class="bg-cream-white p-4 rounded-md">
|
||||
<h3 class="font-semibold text-irish-green mb-1">Account Type</h3>
|
||||
<p>{% if user.is_admin %}Administrator{% else %}User{% endif %}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Profile Content -->
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<!-- Left Column: Personal Info -->
|
||||
<div class="md:col-span-1">
|
||||
<h2 class="text-xl font-semibold text-irish-green mb-4">Personal Information</h2>
|
||||
|
||||
<form action="/auth/update-profile" method="post" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-600">Display Name</label>
|
||||
<div class="mt-1">
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
value="{{ user.username }}"
|
||||
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-irish-green"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-600">Email</label>
|
||||
<div class="mt-1">
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value="{{ user.email }}"
|
||||
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-irish-green"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-4">
|
||||
<button type="submit" class="w-full bg-irish-green hover:bg-opacity-90 text-white font-medium py-2 px-4 rounded-md transition">
|
||||
Update Profile
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 pt-6 border-t">
|
||||
<h3 class="text-lg font-medium text-irish-green mb-3">Change Password</h3>
|
||||
<form action="/auth/change-password" method="post" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-600">Current Password</label>
|
||||
<div class="mt-1">
|
||||
<input
|
||||
type="password"
|
||||
name="current_password"
|
||||
required
|
||||
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-irish-green"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-600">New Password</label>
|
||||
<div class="mt-1">
|
||||
<input
|
||||
type="password"
|
||||
name="new_password"
|
||||
required
|
||||
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-irish-green"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-600">Confirm New Password</label>
|
||||
<div class="mt-1">
|
||||
<input
|
||||
type="password"
|
||||
name="confirm_password"
|
||||
required
|
||||
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-irish-green"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full bg-golden-ale hover:bg-opacity-90 text-black-stout font-medium py-2 px-4 rounded-md transition">
|
||||
Change Password
|
||||
</button>
|
||||
</form>
|
||||
<div class="bg-cream-white p-4 rounded-md">
|
||||
<h3 class="font-semibold text-irish-green mb-1">Member Since</h3>
|
||||
<p>{{ user.created_at.split(' ')[0] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Stats & Teams -->
|
||||
<div class="md:col-span-2">
|
||||
<h2 class="text-xl font-semibold text-irish-green mb-4">Your Statistics</h2>
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-xl font-semibold text-irish-green">Account Settings</h2>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<div class="bg-cream-white p-4 rounded-lg text-center">
|
||||
<p class="text-sm text-gray-600">Total Points</p>
|
||||
<p class="text-2xl font-bold text-irish-green">378</p>
|
||||
</div>
|
||||
<div class="bg-cream-white p-4 rounded-lg text-center">
|
||||
<p class="text-sm text-gray-600">QR Codes Redeemed</p>
|
||||
<p class="text-2xl font-bold text-irish-green">24</p>
|
||||
</div>
|
||||
<div class="bg-cream-white p-4 rounded-lg text-center">
|
||||
<p class="text-sm text-gray-600">Teams Joined</p>
|
||||
<p class="text-2xl font-bold text-irish-green">{{ user.memberships|length }}</p>
|
||||
</div>
|
||||
<div class="bg-cream-white p-4 rounded-lg text-center">
|
||||
<p class="text-sm text-gray-600">Best Position</p>
|
||||
<p class="text-2xl font-bold text-irish-green">#2</p>
|
||||
</div>
|
||||
<div class="border-b border-gray-200 pb-4">
|
||||
<h3 class="text-gray-700 font-medium mb-2">Change Password</h3>
|
||||
<p class="text-sm text-gray-600 mb-3">Update your password to keep your account secure.</p>
|
||||
<button class="bg-irish-green text-white py-2 px-4 rounded-md hover:bg-opacity-90 transition" disabled>
|
||||
Change Password
|
||||
<span class="text-xs">(Coming Soon)</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h2 class="text-xl font-semibold text-irish-green mb-4">Your Teams</h2>
|
||||
<div class="space-y-4">
|
||||
{% for team_info in user_teams %}
|
||||
<div class="border rounded-lg overflow-hidden">
|
||||
<div class="bg-cream-white px-4 py-3 flex justify-between items-center">
|
||||
<div class="font-medium">{{ team_info.team.name }}</div>
|
||||
{% if team_info.is_admin %}
|
||||
<span class="bg-golden-ale text-black-stout text-xs px-2 py-1 rounded-full">Captain</span>
|
||||
{% else %}
|
||||
<span class="bg-gray-200 text-gray-700 text-xs px-2 py-1 rounded-full">Member</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="px-4 py-3 flex justify-between items-center">
|
||||
<div>
|
||||
<p class="text-sm">Current Points: <span class="font-bold">187</span></p>
|
||||
<p class="text-sm text-gray-600">Current Rank: #4</p>
|
||||
</div>
|
||||
<a href="/teams/{{ team_info.team.id }}" class="text-irish-green hover:underline text-sm">View Team</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<p class="text-gray-600">You haven't joined any teams yet.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<a href="/teams/" class="block text-center text-irish-green hover:underline text-sm">
|
||||
<i class="fas fa-plus mr-1"></i> Join or Create Another Team
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Account Management -->
|
||||
<div class="mt-8 pt-6 border-t">
|
||||
<h3 class="text-xl text-red-600 font-semibold mb-4">Danger Zone</h3>
|
||||
<p class="text-gray-700 mb-4">These actions cannot be undone. Please be certain.</p>
|
||||
|
||||
<a href="/auth/delete-account" class="inline-block bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-md">
|
||||
<div class="pt-4">
|
||||
<h3 class="text-gray-700 font-medium mb-2">Danger Zone</h3>
|
||||
<p class="text-sm text-gray-600 mb-3">Permanently delete your account and all of your data.</p>
|
||||
<button class="border border-red-600 text-red-600 py-2 px-4 rounded-md hover:bg-red-600 hover:text-white transition" disabled>
|
||||
Delete Account
|
||||
</a>
|
||||
<span class="text-xs">(Coming Soon)</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,15 +2,13 @@
|
||||
{% block content %}
|
||||
<div class="max-w-md mx-auto my-8">
|
||||
<div class="bg-white p-8 rounded-lg shadow-md">
|
||||
<h1 class="text-2xl font-bold text-irish-green mb-6 text-center">Create an Account</h1>
|
||||
<h1 class="text-2xl font-bold text-irish-green mb-6 text-center">Create Account</h1>
|
||||
|
||||
<!-- Messages/Alerts -->
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="mb-4 p-3 rounded {% if message.type == 'error' %}bg-red-100 text-red-700{% else %}bg-green-100 text-green-700{% endif %}">
|
||||
{{ message.text }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if error %}
|
||||
<div class="mb-4 p-3 rounded bg-red-100 text-red-700">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form action="/auth/register" method="post" class="space-y-4">
|
||||
@@ -20,20 +18,17 @@
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
value="{{ username or '' }}"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-irish-green focus:border-irish-green"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-1">Choose a unique username (3-30 characters, letters, numbers and underscores only)</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">Email</label>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value="{{ email or '' }}"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-irish-green focus:border-irish-green"
|
||||
>
|
||||
@@ -46,6 +41,7 @@
|
||||
id="password"
|
||||
name="password"
|
||||
required
|
||||
minlength="8"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-irish-green focus:border-irish-green"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-1">At least 8 characters</p>
|
||||
@@ -58,36 +54,26 @@
|
||||
id="confirm_password"
|
||||
name="confirm_password"
|
||||
required
|
||||
minlength="8"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-irish-green focus:border-irish-green"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="terms"
|
||||
name="terms"
|
||||
required
|
||||
class="h-4 w-4 mt-1 text-irish-green focus:ring-irish-green border-gray-300 rounded"
|
||||
>
|
||||
<label for="terms" class="ml-2 block text-sm text-gray-700">
|
||||
I agree to the <a href="/terms" class="text-irish-green hover:underline" target="_blank">Terms of Service</a> and <a href="/privacy" class="text-irish-green hover:underline" target="_blank">Privacy Policy</a>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full bg-irish-green text-white py-2 px-4 rounded-md hover:bg-opacity-90 transition focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-irish-green"
|
||||
>
|
||||
Create Account
|
||||
Register
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 pt-6 border-t border-gray-200 text-center">
|
||||
<p class="text-gray-600">Already have an account?</p>
|
||||
<a href="/auth/login" class="text-irish-green hover:underline">Log in</a>
|
||||
<a href="/auth/login" class="block mt-2 bg-cream-white text-irish-green border border-irish-green py-2 px-4 rounded-md hover:bg-irish-green hover:text-white transition">
|
||||
Log in
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,33 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-md mx-auto my-12">
|
||||
<div class="bg-white p-8 rounded-lg shadow-md text-center">
|
||||
<div class="mb-6">
|
||||
<div class="inline-block p-4 rounded-full bg-green-100">
|
||||
<i class="fas fa-check-circle text-irish-green text-5xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="max-w-md mx-auto my-8">
|
||||
<div class="bg-white p-8 rounded-lg shadow-md">
|
||||
<div class="text-center">
|
||||
<i class="fas fa-check-circle text-green-500 text-5xl mb-4"></i>
|
||||
<h1 class="text-2xl font-bold text-irish-green mb-3">Registration Successful!</h1>
|
||||
<p class="text-gray-600 mb-6">Your account has been created successfully.</p>
|
||||
|
||||
<h1 class="text-2xl font-bold text-irish-green mb-4">Registration Successful!</h1>
|
||||
<p class="text-gray-600 mb-6">Your account has been created and you're now logged in.</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<a href="/dashboard" class="block w-full bg-irish-green hover:bg-opacity-90 text-white font-medium py-2 px-4 rounded-md transition">
|
||||
Go to Dashboard
|
||||
<a href="/auth/login" class="inline-block bg-irish-green text-white py-2 px-6 rounded-md hover:bg-opacity-90 transition">
|
||||
Log In
|
||||
</a>
|
||||
<a href="/teams/" class="block w-full bg-cream-white border border-irish-green text-irish-green hover:bg-irish-green hover:text-white font-medium py-2 px-4 rounded-md transition">
|
||||
Join a Team
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 pt-6 border-t border-gray-200">
|
||||
<h2 class="font-semibold mb-2">What's Next?</h2>
|
||||
<ul class="text-left text-sm space-y-2">
|
||||
<li><i class="fas fa-check-circle text-irish-green mr-2"></i> Explore the pub quiz leaderboards</li>
|
||||
<li><i class="fas fa-check-circle text-irish-green mr-2"></i> Join an existing team or create your own</li>
|
||||
<li><i class="fas fa-check-circle text-irish-green mr-2"></i> Scan QR codes at quiz events to earn points</li>
|
||||
<li><i class="fas fa-check-circle text-irish-green mr-2"></i> Complete your profile information</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+11
-1
@@ -44,7 +44,7 @@
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="flex justify-between items-center py-4">
|
||||
<div class="flex items-center space-x-3">
|
||||
<img src="https://via.placeholder.com/40x40" alt="LeagueLedger Logo" class="h-10 w-10">
|
||||
<img src="https://picsum.photos/40" alt="LeagueLedger Logo" class="h-10 w-10">
|
||||
<h1 class="text-2xl font-bold font-garamond">LeagueLedger</h1>
|
||||
</div>
|
||||
|
||||
@@ -62,6 +62,11 @@
|
||||
<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>
|
||||
@@ -111,6 +116,11 @@
|
||||
<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>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="bg-irish-green text-white p-6">
|
||||
<div class="flex flex-col sm:flex-row items-center">
|
||||
<div class="w-24 h-24 rounded-full bg-white border-4 border-white overflow-hidden mb-4 sm:mb-0 sm:mr-6">
|
||||
<img src="https://via.placeholder.com/150" alt="Profile" class="w-full h-full object-cover">
|
||||
<img src="https://picsum.photos/150" alt="Profile" class="w-full h-full object-cover">
|
||||
</div>
|
||||
<div class="text-center sm:text-left">
|
||||
<h1 class="text-2xl font-bold">John Quizmaster</h1>
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="w-10 h-10 rounded-full bg-gray-200 mr-3 overflow-hidden">
|
||||
<img src="https://via.placeholder.com/40" alt="User" class="w-full h-full object-cover">
|
||||
<img src="https://picsum.photos/40" alt="User" class="w-full h-full object-cover">
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium">{{ member.user.username }}</p>
|
||||
|
||||
@@ -20,3 +20,4 @@ templates = MyJinjaTemplates(directory=str(BASE_DIR / "templates"))
|
||||
|
||||
# Register a context processor to add current_user to all templates
|
||||
templates.env.globals["get_current_user"] = lambda: None # Will be overridden at runtime
|
||||
templates.env.globals["current_user"] = None
|
||||
|
||||
@@ -13,6 +13,7 @@ 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()
|
||||
|
||||
@@ -64,6 +65,7 @@ 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()]
|
||||
@@ -73,6 +75,7 @@ async def admin_home(request: Request):
|
||||
)
|
||||
|
||||
@router.get("/{model_name}", response_class=HTMLResponse)
|
||||
@require_admin
|
||||
async def list_records(
|
||||
request: Request,
|
||||
model_name: str,
|
||||
@@ -124,6 +127,7 @@ async def list_records(
|
||||
)
|
||||
|
||||
@router.get("/{model_name}/new", response_class=HTMLResponse)
|
||||
@require_admin
|
||||
async def create_record_form(
|
||||
request: Request,
|
||||
model_name: str,
|
||||
@@ -164,6 +168,7 @@ async def create_record_form(
|
||||
)
|
||||
|
||||
@router.post("/{model_name}/new")
|
||||
@require_admin
|
||||
async def create_record(
|
||||
request: Request,
|
||||
model_name: str,
|
||||
@@ -212,6 +217,7 @@ 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,
|
||||
@@ -263,6 +269,7 @@ async def edit_record_form(
|
||||
)
|
||||
|
||||
@router.post("/{model_name}/{record_id}")
|
||||
@require_admin
|
||||
async def update_record(
|
||||
request: Request,
|
||||
model_name: str,
|
||||
@@ -311,7 +318,9 @@ 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,
|
||||
record_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
|
||||
+74
-127
@@ -1,137 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Dashboard views for user-specific information.
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, Request, Form
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi import APIRouter, Request, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.sql import func
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
|
||||
from ..db import SessionLocal
|
||||
from ..models import User, Team, TeamMembership, QRTicket
|
||||
from ..db import get_db
|
||||
from ..templates_config import templates
|
||||
from ..auth import require_login
|
||||
from .. import models
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
@router.get("/")
|
||||
@require_login
|
||||
def user_dashboard(request: Request, db: Session = Depends(get_db)):
|
||||
"""User dashboard showing teams, events and stats"""
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
user = request.session.get("user")
|
||||
user_id = user.get("id")
|
||||
|
||||
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"
|
||||
# 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)\
|
||||
.filter(models.EventAttendee.user_id == user_id)\
|
||||
.order_by(models.Event.event_date.desc())\
|
||||
.limit(5)\
|
||||
.all()
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"dashboard/index.html",
|
||||
{
|
||||
"request": request,
|
||||
"user": user,
|
||||
"team_count": team_count,
|
||||
"total_points": total_points,
|
||||
"event_count": event_count,
|
||||
"recent_events": recent_events,
|
||||
"user_teams": user_teams
|
||||
}
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return user
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
async def user_dashboard(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Show the user's dashboard with team info and recent activity."""
|
||||
|
||||
# Get current user - using a default user for now
|
||||
# In a real app, this would come from auth system
|
||||
user = get_or_create_default_user(db)
|
||||
|
||||
# Get user's teams
|
||||
user_teams = db.query(Team).join(
|
||||
TeamMembership,
|
||||
TeamMembership.team_id == Team.id
|
||||
).filter(
|
||||
TeamMembership.user_id == user.id
|
||||
).all()
|
||||
|
||||
# Get team memberships with admin status
|
||||
team_memberships = db.query(
|
||||
TeamMembership
|
||||
).filter(
|
||||
TeamMembership.user_id == user.id
|
||||
).all()
|
||||
|
||||
admin_team_ids = [tm.team_id for tm in team_memberships if tm.is_admin]
|
||||
|
||||
# Get points per team
|
||||
team_points = {}
|
||||
for team in user_teams:
|
||||
points = db.query(func.sum(QRTicket.points)).filter(
|
||||
QRTicket.redeemed_at_team == team.id
|
||||
).scalar() or 0
|
||||
|
||||
# Get team ranking - simplified approach
|
||||
higher_teams = db.query(func.count(Team.id)).join(
|
||||
QRTicket,
|
||||
QRTicket.redeemed_at_team == Team.id
|
||||
).group_by(
|
||||
Team.id
|
||||
).having(
|
||||
func.sum(QRTicket.points) > points
|
||||
).scalar() or 0
|
||||
|
||||
rank = higher_teams + 1
|
||||
|
||||
team_points[team.id] = {
|
||||
'points': points,
|
||||
'rank': rank
|
||||
}
|
||||
|
||||
# Get recent activity
|
||||
# For simplicity, we're just getting recent QR code redemptions
|
||||
recent_activity = []
|
||||
|
||||
recent_tickets = db.query(QRTicket).filter(
|
||||
QRTicket.redeemed_by == user.id
|
||||
).order_by(
|
||||
QRTicket.id.desc() # Assuming higher ID = newer
|
||||
).limit(5).all()
|
||||
|
||||
for ticket in recent_tickets:
|
||||
team = db.query(Team).filter(Team.id == ticket.redeemed_at_team).first()
|
||||
activity = {
|
||||
'type': 'qr_redeem',
|
||||
'points': ticket.points,
|
||||
'team_name': team.name if team else "Unknown team",
|
||||
'date': "Recently" # Placeholder - would use ticket.created_at
|
||||
}
|
||||
recent_activity.append(activity)
|
||||
|
||||
# Get total points for user across all teams
|
||||
total_points = sum(team_data['points'] for team_data in team_points.values())
|
||||
|
||||
# Get best ranking
|
||||
best_rank = min(team_data['rank'] for team_data in team_points.values()) if team_points else None
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"dashboard.html",
|
||||
{
|
||||
"request": request,
|
||||
"user": user,
|
||||
"teams": user_teams,
|
||||
"team_points": team_points,
|
||||
"admin_team_ids": admin_team_ids,
|
||||
"recent_activity": recent_activity,
|
||||
"total_points": total_points,
|
||||
"best_rank": best_rank,
|
||||
"team_count": len(user_teams)
|
||||
}
|
||||
)
|
||||
|
||||
@router.get("/scan", response_class=HTMLResponse)
|
||||
async def scan_qr(request: Request):
|
||||
"""Show QR scanning interface."""
|
||||
return templates.TemplateResponse(
|
||||
"scan_qr.html",
|
||||
{"request": request}
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Dashboard error: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Dashboard error: {str(e)}")
|
||||
|
||||
+6
-1
@@ -13,7 +13,7 @@ services:
|
||||
ports:
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
- db_data:/var/lib/mysql:delegated
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$$MYSQL_ROOT_PASSWORD"]
|
||||
interval: 5s
|
||||
@@ -38,6 +38,11 @@ services:
|
||||
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"
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
|
||||
+17
-13
@@ -1,14 +1,18 @@
|
||||
fastapi
|
||||
uvicorn[standard]
|
||||
SQLAlchemy
|
||||
mysqlclient
|
||||
Jinja2
|
||||
python-multipart
|
||||
passlib[bcrypt]
|
||||
qrcode
|
||||
email-validator
|
||||
pillow
|
||||
python-jose[cryptography]
|
||||
itsdangerous
|
||||
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
|
||||
flask-session==0.5.0
|
||||
starlette>=0.27.0
|
||||
jinja2>=3.1.2
|
||||
itsdangerous>=2.1.2
|
||||
python-jose[cryptography]>=3.3.0
|
||||
python-dotenv>=1.0.0
|
||||
qrcode
|
||||
pydantic[email]>=1.10.7
|
||||
httpx
|
||||
Reference in New Issue
Block a user