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
|
# Install dependencies
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
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 the rest of the code
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
|||||||
+333
-6
@@ -1,14 +1,40 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Skeleton for authentication logic.
|
Authentication system using Authlib and session-based auth
|
||||||
Placeholder for OAuth or password-based login.
|
|
||||||
"""
|
"""
|
||||||
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 sqlalchemy.orm import Session
|
||||||
from .db import SessionLocal
|
|
||||||
from . import models
|
|
||||||
from passlib.context import CryptContext
|
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()
|
router = APIRouter()
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
|
||||||
@@ -19,4 +45,305 @@ def get_db():
|
|||||||
finally:
|
finally:
|
||||||
db.close()
|
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 import create_engine, inspect, text
|
||||||
from sqlalchemy.orm import sessionmaker, declarative_base
|
from sqlalchemy.orm import sessionmaker, declarative_base
|
||||||
|
|
||||||
DB_HOST = os.getenv("DB_HOST", "localhost")
|
# Get database connection details from environment variables with fallbacks
|
||||||
DB_PORT = os.getenv("DB_PORT", "3306")
|
DB_HOST = os.environ.get("DB_HOST", "localhost")
|
||||||
DB_NAME = os.getenv("DB_NAME", "pubquiz_db")
|
DB_PORT = os.environ.get("DB_PORT", "3306")
|
||||||
DB_USER = os.getenv("DB_USER", "pubquiz_user")
|
DB_NAME = os.environ.get("DB_NAME", "pubquiz_db")
|
||||||
DB_PASS = os.getenv("DB_PASS", "pubquiz_pass")
|
DB_USER = os.environ.get("DB_USER", "pubquiz_user")
|
||||||
|
DB_PASS = os.environ.get("DB_PASS", "pubquiz_pass")
|
||||||
|
|
||||||
SQLALCHEMY_DATABASE_URL = (
|
# Create database URL
|
||||||
f"mysql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
|
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)
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
# Create base class for models
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
@@ -146,3 +154,12 @@ def migrate_schema():
|
|||||||
print(f"Error during schema migration: {e}")
|
print(f"Error during schema migration: {e}")
|
||||||
finally:
|
finally:
|
||||||
connection.close()
|
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 .models import User, Team, TeamMembership, QRTicket
|
||||||
from .db import SessionLocal
|
from .db import SessionLocal
|
||||||
|
from .auth import get_password_hash
|
||||||
|
|
||||||
def table_has_column(engine, table_name, column_name):
|
def table_has_column(engine, table_name, column_name):
|
||||||
"""Check if a table has a specific column."""
|
"""Check if a table has a specific column."""
|
||||||
@@ -28,13 +29,39 @@ def seed_db():
|
|||||||
print("Database already has data. Skipping seeding.")
|
print("Database already has data. Skipping seeding.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Create users
|
# Create users with properly hashed passwords
|
||||||
users = [
|
users = [
|
||||||
User(username="john_quizmaster", email="john@example.com", hashed_password="password123"),
|
User(
|
||||||
User(username="sarah_johnson", email="sarah@example.com", hashed_password="password123"),
|
username="admin",
|
||||||
User(username="mike_peters", email="mike@example.com", hashed_password="password123"),
|
email="admin@example.com",
|
||||||
User(username="emma_wilson", email="emma@example.com", hashed_password="password123"),
|
hashed_password=get_password_hash("password"),
|
||||||
User(username="robert_brown", email="robert@example.com", hashed_password="password123"),
|
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.add_all(users)
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -62,8 +89,8 @@ def seed_db():
|
|||||||
memberships = []
|
memberships = []
|
||||||
membership_data = [
|
membership_data = [
|
||||||
# Quiz Wizards
|
# Quiz Wizards
|
||||||
(1, 1, True, 160),
|
(1, 1, True, 160), # Admin user is team admin of Quiz Wizards
|
||||||
(2, 1, False, 155),
|
(2, 1, True, 155),
|
||||||
(3, 1, False, 130),
|
(3, 1, False, 130),
|
||||||
(4, 1, False, 90),
|
(4, 1, False, 90),
|
||||||
(5, 1, False, 45),
|
(5, 1, False, 45),
|
||||||
|
|||||||
+29
-100
@@ -1,110 +1,39 @@
|
|||||||
from fastapi import Depends, HTTPException, status, Request
|
from fastapi import Depends, HTTPException, status, Request
|
||||||
from fastapi.security import OAuth2PasswordBearer
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
from jose import JWTError, jwt
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy import inspect
|
from .db import get_db
|
||||||
from typing import Optional
|
from . import models
|
||||||
from datetime import datetime
|
from .auth import get_current_user, require_login, require_admin
|
||||||
|
|
||||||
from .db import SessionLocal, engine
|
# Reuse functions from auth.py
|
||||||
from .models import User
|
# This is just for backward compatibility with any code that imported these from dependencies
|
||||||
from .security import SECRET_KEY, ALGORITHM
|
|
||||||
from .templates_config import templates
|
|
||||||
|
|
||||||
# OAuth2 scheme for token authentication
|
# Function to get a db session
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False)
|
def get_session_db():
|
||||||
|
return next(get_db())
|
||||||
|
|
||||||
def get_db():
|
# Function to get current authenticated user
|
||||||
"""Database dependency."""
|
def get_authenticated_user(request: Request):
|
||||||
db = SessionLocal()
|
return get_current_user(request)
|
||||||
try:
|
|
||||||
yield db
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
# Check if all required user columns exist
|
# These are kept for API backward compatibility
|
||||||
def get_available_user_columns():
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token")
|
||||||
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)):
|
def get_current_active_user(request: Request):
|
||||||
"""Get the current authenticated user based on the access token."""
|
user = get_current_user(request)
|
||||||
credentials_exception = HTTPException(
|
if not user:
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
raise HTTPException(
|
||||||
detail="Could not validate credentials",
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
detail="Not authenticated",
|
||||||
)
|
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()
|
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
async def get_current_active_user(current_user: User = Depends(get_current_user)):
|
def get_current_admin_user(request: Request):
|
||||||
"""Check if the current user is active."""
|
user = get_current_user(request)
|
||||||
if not current_user:
|
if not user or not user.get("is_admin"):
|
||||||
return None
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
columns = get_available_user_columns()
|
detail="Not enough permissions",
|
||||||
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 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}
|
|
||||||
|
|||||||
+61
-53
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from fastapi import FastAPI, Request, status
|
from fastapi import FastAPI, Request, status, Depends
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
from starlette.middleware.sessions import SessionMiddleware
|
from starlette.middleware.sessions import SessionMiddleware
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -8,9 +8,9 @@ import os
|
|||||||
from .db import init_db, engine
|
from .db import init_db, engine
|
||||||
from . import models
|
from . import models
|
||||||
from .templates_config import templates
|
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 .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
|
# Create tables on startup
|
||||||
init_db()
|
init_db()
|
||||||
@@ -21,71 +21,84 @@ seed_db()
|
|||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
# IMPORTANT: Add SessionMiddleware FIRST before any other middleware
|
# Configure session middleware with environment variables or defaults
|
||||||
# This ensures session data is available to all other middleware and route handlers
|
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(
|
app.add_middleware(
|
||||||
SessionMiddleware,
|
SessionMiddleware,
|
||||||
secret_key=os.environ.get("SECRET_KEY", "a-default-secret-key-for-sessions"),
|
secret_key=secret_key,
|
||||||
max_age=int(os.environ.get("SESSION_MAX_AGE", 86400)), # 24 hours
|
max_age=int(os.environ.get("SESSION_MAX_AGE", "86400")), # 24 hours by default
|
||||||
same_site="lax", # Important for security while allowing redirects
|
same_site="lax", # Important for security while allowing redirects
|
||||||
https_only=os.environ.get("COOKIE_SECURE", "False").lower() == "true",
|
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")
|
@app.middleware("http")
|
||||||
async def debug_session_middleware(request, call_next):
|
async def debug_session_middleware(request, call_next):
|
||||||
"""Debug middleware to track session state"""
|
"""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"Request path: {request.url.path}")
|
||||||
print(f"Has session cookie: {session_cookie is not None}")
|
# Instead of checking request.scope, check if we can access the session dict
|
||||||
|
has_session = hasattr(request, "session") and isinstance(request.session, dict)
|
||||||
if "session" in request.scope:
|
print(f"Has session attribute: {has_session}")
|
||||||
print(f"Session data before: {dict(request.session)}")
|
print(f"Has session cookie: {session_cookie is not None}")
|
||||||
|
|
||||||
|
# Check session data
|
||||||
|
if hasattr(request, "session"):
|
||||||
|
try:
|
||||||
|
print(f"Session data before: {dict(request.session)}")
|
||||||
|
except (TypeError, AttributeError):
|
||||||
|
# The session might not be dict-like
|
||||||
|
print(f"Session exists but isn't a dictionary")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in debug middleware (pre): {str(e)}")
|
||||||
|
|
||||||
response = await call_next(request)
|
response = await call_next(request)
|
||||||
|
|
||||||
if "session" in request.scope:
|
try:
|
||||||
print(f"Session data after: {dict(request.session)}")
|
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
|
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")
|
@app.middleware("http")
|
||||||
async def add_user_to_request(request: Request, call_next):
|
async def add_user_to_request(request: Request, call_next):
|
||||||
# Print debugging information
|
"""Add user to request state and update template globals"""
|
||||||
print(f"Processing request to: {request.url.path}")
|
|
||||||
|
|
||||||
# Add user to request state so templates can access it
|
|
||||||
try:
|
try:
|
||||||
if "session" in request.scope:
|
# Get user from session if available
|
||||||
print("Session found in request scope")
|
user = get_current_user(request)
|
||||||
if "user_id" in request.session:
|
|
||||||
print(f"User ID in session: {request.session['user_id']}")
|
# Store user in request.state for route handlers
|
||||||
# Get user from session
|
request.state.user = user
|
||||||
user = await get_user_from_session(request)
|
|
||||||
request.state.user = user
|
# Update template globals for all templates
|
||||||
else:
|
templates.env.globals["current_user"] = user
|
||||||
print("No user_id in session")
|
|
||||||
request.state.user = None
|
# Debug output to check user and admin status
|
||||||
else:
|
if user:
|
||||||
print("No session in request scope")
|
print(f"User in context: {user.get('username')}, Admin: {user.get('is_admin', False)}")
|
||||||
request.state.user = None
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error in middleware: {e}")
|
print(f"Error setting user context: {str(e)}")
|
||||||
request.state.user = None
|
|
||||||
|
|
||||||
# Update template context with current user before processing the request
|
|
||||||
templates.env.globals["current_user"] = request.state.user
|
|
||||||
|
|
||||||
# Process the request
|
# Process the request
|
||||||
response = await call_next(request)
|
response = await call_next(request)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# Routers
|
# 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(qr.router, prefix="/qr", tags=["QR"])
|
||||||
app.include_router(redeem.router, prefix="/redeem", tags=["Redeem"])
|
app.include_router(redeem.router, prefix="/redeem", tags=["Redeem"])
|
||||||
app.include_router(teams.router, prefix="/teams", tags=["Teams"])
|
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):
|
def index(request: Request):
|
||||||
return templates.TemplateResponse("index.html", {
|
return templates.TemplateResponse("index.html", {
|
||||||
"request": request,
|
"request": request,
|
||||||
"now": datetime.now,
|
"now": datetime.now
|
||||||
"current_user": getattr(request.state, "user", None)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
@app.get("/about", response_class=HTMLResponse)
|
@app.get("/about", response_class=HTMLResponse)
|
||||||
def about(request: Request):
|
def about(request: Request):
|
||||||
return templates.TemplateResponse("about.html", {
|
return templates.TemplateResponse("about.html", {
|
||||||
"request": request,
|
"request": request
|
||||||
"current_user": getattr(request.state, "user", None)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
@app.get("/contact", response_class=HTMLResponse)
|
@app.get("/contact", response_class=HTMLResponse)
|
||||||
def contact(request: Request):
|
def contact(request: Request):
|
||||||
return templates.TemplateResponse("contact.html", {
|
return templates.TemplateResponse("contact.html", {
|
||||||
"request": request,
|
"request": request
|
||||||
"current_user": getattr(request.state, "user", None)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
@app.get("/privacy", response_class=HTMLResponse)
|
@app.get("/privacy", response_class=HTMLResponse)
|
||||||
def privacy(request: Request):
|
def privacy(request: Request):
|
||||||
return templates.TemplateResponse("privacy.html", {
|
return templates.TemplateResponse("privacy.html", {
|
||||||
"request": request,
|
"request": request
|
||||||
"current_user": getattr(request.state, "user", None)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
@app.get("/terms", response_class=HTMLResponse)
|
@app.get("/terms", response_class=HTMLResponse)
|
||||||
def terms(request: Request):
|
def terms(request: Request):
|
||||||
return templates.TemplateResponse("terms.html", {
|
return templates.TemplateResponse("terms.html", {
|
||||||
"request": request,
|
"request": request
|
||||||
"current_user": getattr(request.state, "user", None)
|
|
||||||
})
|
})
|
||||||
|
|||||||
+91
-10
@@ -1,7 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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.orm import relationship
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
|
from datetime import datetime
|
||||||
from .db import Base
|
from .db import Base
|
||||||
|
|
||||||
class User(Base):
|
class User(Base):
|
||||||
@@ -11,15 +12,20 @@ class User(Base):
|
|||||||
email = Column(String(255), unique=True, index=True, nullable=False)
|
email = Column(String(255), unique=True, index=True, nullable=False)
|
||||||
hashed_password = Column(String(255), nullable=True)
|
hashed_password = Column(String(255), nullable=True)
|
||||||
created_at = Column(DateTime, server_default=func.now())
|
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_active = Column(Boolean, default=True)
|
||||||
is_verified = Column(Boolean, default=False)
|
is_verified = Column(Boolean, default=False)
|
||||||
|
is_admin = Column(Boolean, default=False)
|
||||||
verification_token = Column(String(255), nullable=True)
|
verification_token = Column(String(255), nullable=True)
|
||||||
reset_token = Column(String(255), nullable=True)
|
reset_token = Column(String(255), nullable=True)
|
||||||
reset_token_expires_at = Column(DateTime, nullable=True)
|
reset_token_expires_at = Column(DateTime, nullable=True)
|
||||||
last_login = Column(DateTime, nullable=True)
|
last_login = Column(DateTime, nullable=True)
|
||||||
|
|
||||||
# Relationship to teams
|
# Relationships
|
||||||
memberships = relationship("TeamMembership", back_populates="user")
|
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):
|
class OAuthAccount(Base):
|
||||||
@@ -40,14 +46,14 @@ class Team(Base):
|
|||||||
__tablename__ = "teams"
|
__tablename__ = "teams"
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
name = Column(String(100), unique=True, nullable=False)
|
name = Column(String(100), unique=True, nullable=False)
|
||||||
|
description = Column(Text, nullable=True)
|
||||||
# Add fields for team detail view
|
|
||||||
is_public = Column(Boolean, default=False) # For team privacy setting
|
is_public = Column(Boolean, default=False) # For team privacy setting
|
||||||
created_at = Column(DateTime, server_default=func.now()) # For team founded date
|
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")
|
memberships = relationship("TeamMembership", back_populates="team")
|
||||||
|
members = relationship("TeamMember", back_populates="team")
|
||||||
|
|
||||||
|
|
||||||
class TeamMembership(Base):
|
class TeamMembership(Base):
|
||||||
@@ -56,14 +62,25 @@ class TeamMembership(Base):
|
|||||||
user_id = Column(Integer, ForeignKey("users.id"))
|
user_id = Column(Integer, ForeignKey("users.id"))
|
||||||
team_id = Column(Integer, ForeignKey("teams.id"))
|
team_id = Column(Integer, ForeignKey("teams.id"))
|
||||||
is_admin = Column(Boolean, default=False)
|
is_admin = Column(Boolean, default=False)
|
||||||
|
|
||||||
# Add joined_at to track when members joined
|
|
||||||
joined_at = Column(DateTime, server_default=func.now())
|
joined_at = Column(DateTime, server_default=func.now())
|
||||||
|
|
||||||
user = relationship("User", back_populates="memberships")
|
user = relationship("User", back_populates="memberships")
|
||||||
team = relationship("Team", 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):
|
class QRTicket(Base):
|
||||||
__tablename__ = "qr_tickets"
|
__tablename__ = "qr_tickets"
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
@@ -81,7 +98,6 @@ class QRTicket(Base):
|
|||||||
event_name = Column(String(255), nullable=True)
|
event_name = Column(String(255), nullable=True)
|
||||||
|
|
||||||
|
|
||||||
# New model for team achievements
|
|
||||||
class TeamAchievement(Base):
|
class TeamAchievement(Base):
|
||||||
__tablename__ = "team_achievements"
|
__tablename__ = "team_achievements"
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
@@ -92,3 +108,68 @@ class TeamAchievement(Base):
|
|||||||
achieved_at = Column(DateTime, server_default=func.now())
|
achieved_at = Column(DateTime, server_default=func.now())
|
||||||
|
|
||||||
team = relationship("Team")
|
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, DateTime
|
||||||
|
from sqlalchemy.sql import func
|
||||||
from sqlalchemy import Column, Integer, String, Boolean
|
from ..db import Base
|
||||||
# ...existing imports...
|
|
||||||
|
|
||||||
class User(Base):
|
class User(Base):
|
||||||
__tablename__ = "users"
|
__tablename__ = "users"
|
||||||
|
|
||||||
# ...existing fields...
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
username = Column(String(50), unique=True, index=True)
|
username = Column(String(50), unique=True, index=True)
|
||||||
email = Column(String(100), unique=True, index=True)
|
email = Column(String(100), unique=True, index=True)
|
||||||
password = Column(String(255))
|
password_hash = Column(String(255)) # Renamed from password to password_hash for clarity
|
||||||
|
|
||||||
# Add is_admin field if it doesn't exist
|
|
||||||
is_admin = Column(Boolean, default=False)
|
is_admin = Column(Boolean, default=False)
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
# ...existing methods...
|
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>
|
<h1 class="text-2xl font-bold text-irish-green mb-6 text-center">Log In</h1>
|
||||||
|
|
||||||
<!-- Messages/Alerts -->
|
<!-- Messages/Alerts -->
|
||||||
{% if messages %}
|
{% if error %}
|
||||||
{% for message in messages %}
|
<div class="mb-4 p-3 rounded bg-red-100 text-red-700">
|
||||||
<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 %}">
|
{{ error }}
|
||||||
{{ message.text }}
|
</div>
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form action="/auth/login" method="post" class="space-y-4">
|
{% if message %}
|
||||||
<input type="hidden" name="next" value="{{ next }}">
|
<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">
|
||||||
<div>
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="username"
|
id="username"
|
||||||
name="username"
|
name="username"
|
||||||
value="{{ username or '' }}"
|
|
||||||
required
|
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"
|
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
|
Log In
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</form>
|
||||||
|
|
||||||
<div class="mt-6 pt-6 border-t border-gray-200 text-center">
|
<div class="mt-6 pt-6 border-t border-gray-200 text-center">
|
||||||
@@ -75,7 +72,17 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</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">
|
<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>
|
<p class="text-center text-gray-600 mb-4">Or sign in with</p>
|
||||||
<div class="flex justify-center space-x-4">
|
<div class="flex justify-center space-x-4">
|
||||||
@@ -86,8 +93,9 @@
|
|||||||
<i class="fab fa-github mr-2"></i> GitHub
|
<i class="fab fa-github mr-2"></i> GitHub
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+40
-169
@@ -1,184 +1,55 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="max-w-4xl mx-auto">
|
<div class="max-w-3xl mx-auto my-8">
|
||||||
<div class="bg-white rounded-lg shadow-md overflow-hidden mb-8">
|
<div class="bg-white p-8 rounded-lg shadow-md">
|
||||||
<!-- Profile Header -->
|
<div class="flex flex-col md:flex-row items-center md:items-start md:space-x-8">
|
||||||
<div class="bg-irish-green text-white p-6">
|
<!-- Profile Image -->
|
||||||
<div class="flex flex-col sm:flex-row items-center">
|
<div class="mb-6 md:mb-0">
|
||||||
<div class="w-24 h-24 rounded-full bg-white border-4 border-white overflow-hidden mb-4 sm:mb-0 sm:mr-6">
|
{% if user.picture %}
|
||||||
<img src="https://via.placeholder.com/150" alt="Profile" class="w-full h-full object-cover">
|
<img src="{{ user.picture }}" alt="Profile Picture" class="w-32 h-32 rounded-full object-cover border-4 border-irish-green">
|
||||||
</div>
|
{% else %}
|
||||||
<div class="text-center sm:text-left">
|
<div class="w-32 h-32 rounded-full bg-irish-green flex items-center justify-center text-white text-4xl">
|
||||||
<h1 class="text-2xl font-bold">{{ user.username }}</h1>
|
{{ user.username[0]|upper }}
|
||||||
<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>
|
|
||||||
</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 }}
|
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
<!-- User Info -->
|
||||||
<!-- Profile Content -->
|
<div class="flex-grow">
|
||||||
<div class="p-6">
|
<h1 class="text-2xl font-bold text-irish-green">{{ user.username }}</h1>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<p class="text-gray-600 mb-4">{{ user.email }}</p>
|
||||||
<!-- Left Column: Personal Info -->
|
|
||||||
<div class="md:col-span-1">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||||
<h2 class="text-xl font-semibold text-irish-green mb-4">Personal Information</h2>
|
<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>
|
||||||
|
|
||||||
<form action="/auth/update-profile" method="post" class="space-y-4">
|
<div class="bg-cream-white p-4 rounded-md">
|
||||||
<div>
|
<h3 class="font-semibold text-irish-green mb-1">Member Since</h3>
|
||||||
<label class="block text-sm font-medium text-gray-600">Display Name</label>
|
<p>{{ user.created_at.split(' ')[0] }}</p>
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right Column: Stats & Teams -->
|
<div class="space-y-4">
|
||||||
<div class="md:col-span-2">
|
<h2 class="text-xl font-semibold text-irish-green">Account Settings</h2>
|
||||||
<h2 class="text-xl font-semibold text-irish-green mb-4">Your Statistics</h2>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
<div class="border-b border-gray-200 pb-4">
|
||||||
<div class="bg-cream-white p-4 rounded-lg text-center">
|
<h3 class="text-gray-700 font-medium mb-2">Change Password</h3>
|
||||||
<p class="text-sm text-gray-600">Total Points</p>
|
<p class="text-sm text-gray-600 mb-3">Update your password to keep your account secure.</p>
|
||||||
<p class="text-2xl font-bold text-irish-green">378</p>
|
<button class="bg-irish-green text-white py-2 px-4 rounded-md hover:bg-opacity-90 transition" disabled>
|
||||||
</div>
|
Change Password
|
||||||
<div class="bg-cream-white p-4 rounded-lg text-center">
|
<span class="text-xs">(Coming Soon)</span>
|
||||||
<p class="text-sm text-gray-600">QR Codes Redeemed</p>
|
</button>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold text-irish-green mb-4">Your Teams</h2>
|
<div class="pt-4">
|
||||||
<div class="space-y-4">
|
<h3 class="text-gray-700 font-medium mb-2">Danger Zone</h3>
|
||||||
{% for team_info in user_teams %}
|
<p class="text-sm text-gray-600 mb-3">Permanently delete your account and all of your data.</p>
|
||||||
<div class="border rounded-lg overflow-hidden">
|
<button class="border border-red-600 text-red-600 py-2 px-4 rounded-md hover:bg-red-600 hover:text-white transition" disabled>
|
||||||
<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">
|
|
||||||
Delete Account
|
Delete Account
|
||||||
</a>
|
<span class="text-xs">(Coming Soon)</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,15 +2,13 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="max-w-md mx-auto my-8">
|
<div class="max-w-md mx-auto my-8">
|
||||||
<div class="bg-white p-8 rounded-lg shadow-md">
|
<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 -->
|
<!-- Messages/Alerts -->
|
||||||
{% if messages %}
|
{% if error %}
|
||||||
{% for message in messages %}
|
<div class="mb-4 p-3 rounded bg-red-100 text-red-700">
|
||||||
<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 %}">
|
{{ error }}
|
||||||
{{ message.text }}
|
</div>
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form action="/auth/register" method="post" class="space-y-4">
|
<form action="/auth/register" method="post" class="space-y-4">
|
||||||
@@ -20,20 +18,17 @@
|
|||||||
type="text"
|
type="text"
|
||||||
id="username"
|
id="username"
|
||||||
name="username"
|
name="username"
|
||||||
value="{{ username or '' }}"
|
|
||||||
required
|
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"
|
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>
|
||||||
|
|
||||||
<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
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
id="email"
|
id="email"
|
||||||
name="email"
|
name="email"
|
||||||
value="{{ email or '' }}"
|
|
||||||
required
|
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"
|
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"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
required
|
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"
|
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>
|
<p class="text-xs text-gray-500 mt-1">At least 8 characters</p>
|
||||||
@@ -58,36 +54,26 @@
|
|||||||
id="confirm_password"
|
id="confirm_password"
|
||||||
name="confirm_password"
|
name="confirm_password"
|
||||||
required
|
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"
|
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>
|
||||||
|
|
||||||
<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>
|
<div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="mt-6 pt-6 border-t border-gray-200 text-center">
|
<div class="mt-6 pt-6 border-t border-gray-200 text-center">
|
||||||
<p class="text-gray-600">Already have an account?</p>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,33 +1,15 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="max-w-md mx-auto my-12">
|
<div class="max-w-md mx-auto my-8">
|
||||||
<div class="bg-white p-8 rounded-lg shadow-md text-center">
|
<div class="bg-white p-8 rounded-lg shadow-md">
|
||||||
<div class="mb-6">
|
<div class="text-center">
|
||||||
<div class="inline-block p-4 rounded-full bg-green-100">
|
<i class="fas fa-check-circle text-green-500 text-5xl mb-4"></i>
|
||||||
<i class="fas fa-check-circle text-irish-green text-5xl"></i>
|
<h1 class="text-2xl font-bold text-irish-green mb-3">Registration Successful!</h1>
|
||||||
</div>
|
<p class="text-gray-600 mb-6">Your account has been created successfully.</p>
|
||||||
</div>
|
|
||||||
|
<a href="/auth/login" class="inline-block bg-irish-green text-white py-2 px-6 rounded-md hover:bg-opacity-90 transition">
|
||||||
<h1 class="text-2xl font-bold text-irish-green mb-4">Registration Successful!</h1>
|
Log In
|
||||||
<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>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+11
-1
@@ -44,7 +44,7 @@
|
|||||||
<!-- Desktop Navigation -->
|
<!-- Desktop Navigation -->
|
||||||
<div class="flex justify-between items-center py-4">
|
<div class="flex justify-between items-center py-4">
|
||||||
<div class="flex items-center space-x-3">
|
<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>
|
<h1 class="text-2xl font-bold font-garamond">LeagueLedger</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -62,6 +62,11 @@
|
|||||||
<a href="/dashboard" class="hover:text-golden-ale transition-colors duration-200">
|
<a href="/dashboard" class="hover:text-golden-ale transition-colors duration-200">
|
||||||
<i class="fas fa-tachometer-alt"></i> Dashboard
|
<i class="fas fa-tachometer-alt"></i> Dashboard
|
||||||
</a>
|
</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">
|
<a href="/about" class="hover:text-golden-ale transition-colors duration-200">
|
||||||
About
|
About
|
||||||
</a>
|
</a>
|
||||||
@@ -111,6 +116,11 @@
|
|||||||
<a href="/dashboard" class="hover:bg-green-800 py-2 px-3 rounded-md transition-colors duration-200">
|
<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
|
<i class="fas fa-tachometer-alt"></i> Dashboard
|
||||||
</a>
|
</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">
|
<a href="/about" class="hover:bg-green-800 py-2 px-3 rounded-md transition-colors duration-200">
|
||||||
About
|
About
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<div class="bg-irish-green text-white p-6">
|
<div class="bg-irish-green text-white p-6">
|
||||||
<div class="flex flex-col sm:flex-row items-center">
|
<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">
|
<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>
|
||||||
<div class="text-center sm:text-left">
|
<div class="text-center sm:text-left">
|
||||||
<h1 class="text-2xl font-bold">John Quizmaster</h1>
|
<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 justify-between">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="w-10 h-10 rounded-full bg-gray-200 mr-3 overflow-hidden">
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<p class="font-medium">{{ member.user.username }}</p>
|
<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
|
# 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["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 ..db import SessionLocal, Base
|
||||||
from ..models import User, Team, TeamMembership, QRTicket
|
from ..models import User, Team, TeamMembership, QRTicket
|
||||||
from ..templates_config import templates
|
from ..templates_config import templates
|
||||||
|
from ..auth import require_admin
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -64,6 +65,7 @@ def get_relationships(model_class: Type[Base]) -> Dict[str, str]:
|
|||||||
return relationships
|
return relationships
|
||||||
|
|
||||||
@router.get("/", response_class=HTMLResponse)
|
@router.get("/", response_class=HTMLResponse)
|
||||||
|
@require_admin
|
||||||
async def admin_home(request: Request):
|
async def admin_home(request: Request):
|
||||||
"""Admin dashboard home."""
|
"""Admin dashboard home."""
|
||||||
model_list = [(key, name) for key, (_, name) in MODELS.items()]
|
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)
|
@router.get("/{model_name}", response_class=HTMLResponse)
|
||||||
|
@require_admin
|
||||||
async def list_records(
|
async def list_records(
|
||||||
request: Request,
|
request: Request,
|
||||||
model_name: str,
|
model_name: str,
|
||||||
@@ -124,6 +127,7 @@ async def list_records(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@router.get("/{model_name}/new", response_class=HTMLResponse)
|
@router.get("/{model_name}/new", response_class=HTMLResponse)
|
||||||
|
@require_admin
|
||||||
async def create_record_form(
|
async def create_record_form(
|
||||||
request: Request,
|
request: Request,
|
||||||
model_name: str,
|
model_name: str,
|
||||||
@@ -164,6 +168,7 @@ async def create_record_form(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@router.post("/{model_name}/new")
|
@router.post("/{model_name}/new")
|
||||||
|
@require_admin
|
||||||
async def create_record(
|
async def create_record(
|
||||||
request: Request,
|
request: Request,
|
||||||
model_name: str,
|
model_name: str,
|
||||||
@@ -212,6 +217,7 @@ async def create_record(
|
|||||||
return RedirectResponse(f"/admin/{model_name}", status_code=303)
|
return RedirectResponse(f"/admin/{model_name}", status_code=303)
|
||||||
|
|
||||||
@router.get("/{model_name}/{record_id}", response_class=HTMLResponse)
|
@router.get("/{model_name}/{record_id}", response_class=HTMLResponse)
|
||||||
|
@require_admin
|
||||||
async def edit_record_form(
|
async def edit_record_form(
|
||||||
request: Request,
|
request: Request,
|
||||||
model_name: str,
|
model_name: str,
|
||||||
@@ -263,6 +269,7 @@ async def edit_record_form(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@router.post("/{model_name}/{record_id}")
|
@router.post("/{model_name}/{record_id}")
|
||||||
|
@require_admin
|
||||||
async def update_record(
|
async def update_record(
|
||||||
request: Request,
|
request: Request,
|
||||||
model_name: str,
|
model_name: str,
|
||||||
@@ -311,7 +318,9 @@ async def update_record(
|
|||||||
return RedirectResponse(f"/admin/{model_name}", status_code=303)
|
return RedirectResponse(f"/admin/{model_name}", status_code=303)
|
||||||
|
|
||||||
@router.get("/{model_name}/{record_id}/delete")
|
@router.get("/{model_name}/{record_id}/delete")
|
||||||
|
@require_admin
|
||||||
async def delete_record(
|
async def delete_record(
|
||||||
|
request: Request,
|
||||||
model_name: str,
|
model_name: str,
|
||||||
record_id: int,
|
record_id: int,
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
|
|||||||
+75
-128
@@ -1,137 +1,84 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
from fastapi import APIRouter, Request, Depends, HTTPException
|
||||||
Dashboard views for user-specific information.
|
|
||||||
"""
|
|
||||||
from fastapi import APIRouter, Depends, Request, Form
|
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
||||||
from sqlalchemy.orm import Session
|
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 ..db import get_db
|
||||||
from ..models import User, Team, TeamMembership, QRTicket
|
|
||||||
from ..templates_config import templates
|
from ..templates_config import templates
|
||||||
|
from ..auth import require_login
|
||||||
|
from .. import models
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
def get_db():
|
@router.get("/")
|
||||||
db = SessionLocal()
|
@require_login
|
||||||
|
def user_dashboard(request: Request, db: Session = Depends(get_db)):
|
||||||
|
"""User dashboard showing teams, events and stats"""
|
||||||
try:
|
try:
|
||||||
yield db
|
user = request.session.get("user")
|
||||||
finally:
|
user_id = user.get("id")
|
||||||
db.close()
|
|
||||||
|
# Initialize default values in case of errors
|
||||||
def get_or_create_default_user(db: Session):
|
team_count = 0
|
||||||
"""Get user ID 1 or create it if it doesn't exist."""
|
total_points = 0
|
||||||
user = db.query(User).filter_by(id=1).first()
|
event_count = 0
|
||||||
if not user:
|
recent_events = []
|
||||||
# Create a default user
|
user_teams = []
|
||||||
user = User(
|
|
||||||
username="default_user",
|
# Check if TeamMember model exists before querying
|
||||||
email="default@example.com",
|
if hasattr(models, "TeamMember"):
|
||||||
hashed_password="placeholder"
|
# 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)
|
except Exception as e:
|
||||||
db.commit()
|
print(f"Dashboard error: {str(e)}")
|
||||||
db.refresh(user)
|
raise HTTPException(status_code=500, detail=f"Dashboard error: {str(e)}")
|
||||||
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}
|
|
||||||
)
|
|
||||||
|
|||||||
+6
-1
@@ -13,7 +13,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "3306:3306"
|
- "3306:3306"
|
||||||
volumes:
|
volumes:
|
||||||
- db_data:/var/lib/mysql
|
- db_data:/var/lib/mysql:delegated
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$$MYSQL_ROOT_PASSWORD"]
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$$MYSQL_ROOT_PASSWORD"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
@@ -38,6 +38,11 @@ services:
|
|||||||
DEBUG: "True"
|
DEBUG: "True"
|
||||||
SESSION_MAX_AGE: "86400" # 24 hours
|
SESSION_MAX_AGE: "86400" # 24 hours
|
||||||
COOKIE_SECURE: "False" # Set to True in production with HTTPS
|
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:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
+17
-13
@@ -1,14 +1,18 @@
|
|||||||
fastapi
|
fastapi>=0.95.0
|
||||||
uvicorn[standard]
|
uvicorn[standard]>=0.21.1
|
||||||
SQLAlchemy
|
sqlalchemy>=2.0.9
|
||||||
mysqlclient
|
pymysql>=1.0.3
|
||||||
Jinja2
|
cryptography>=40.0.2
|
||||||
python-multipart
|
python-multipart>=0.0.6
|
||||||
passlib[bcrypt]
|
authlib>=1.2.0
|
||||||
qrcode
|
# Specify specific versions for passlib and bcrypt to avoid compatibility issues
|
||||||
email-validator
|
passlib==1.7.4
|
||||||
pillow
|
|
||||||
python-jose[cryptography]
|
|
||||||
itsdangerous
|
|
||||||
bcrypt==4.0.1
|
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