Implement OAuth login with Authentik integration, update user management, and enhance team functionalities
- Added OAuth login functionality using Authentik, allowing users to log in via OpenID. - Updated user registration and login processes to handle OAuth users. - Enhanced team management features, including joining and leaving teams, and displaying user-specific team information. - Improved error handling and user feedback for team actions. - Added new database migrations for OAuth-related fields in the users table. - Updated templates to reflect changes in user authentication and team management. - Refactored dashboard and leaderboard views to include user context and team memberships.
This commit is contained in:
@@ -0,0 +1,98 @@
|
|||||||
|
import os
|
||||||
|
from httpx_oauth.clients.openid import OpenID
|
||||||
|
from httpx_oauth.oauth2 import GetAccessTokenError
|
||||||
|
from fastapi import HTTPException, Request
|
||||||
|
from starlette.responses import RedirectResponse
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
import json
|
||||||
|
import httpx
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
class AuthentikOAuth:
|
||||||
|
def __init__(self):
|
||||||
|
self.client_id = os.getenv("AUTHENTIK_CLIENT_ID", "dRXLBdTdG6JSHqkcM0ZQBPwBVMBrG6SF32LZ1XAT")
|
||||||
|
self.client_secret = os.getenv("AUTHENTIK_CLIENT_SECRET",
|
||||||
|
"hn1aKecLeYj1tVc7QtsavrWjSOF4t7Ty1akVTmUqvIFJF1y0H3Myv7InUxAX6E2GLpMxxhhZZ2aUSJ9VEQz7zGcMbgUeMStxx2U7bEQxmuOGjZf0XJbOBGjdwGZYJlz7")
|
||||||
|
self.config_url = os.getenv("AUTHENTIK_CONFIG_URL",
|
||||||
|
"https://authentik.hosterra.net/application/o/leagueledger/.well-known/openid-configuration")
|
||||||
|
self.client = None
|
||||||
|
self.initialize_client()
|
||||||
|
|
||||||
|
def initialize_client(self):
|
||||||
|
try:
|
||||||
|
self.client = OpenID(
|
||||||
|
client_id=self.client_id,
|
||||||
|
client_secret=self.client_secret,
|
||||||
|
openid_configuration_endpoint=self.config_url,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error initializing Authentik OAuth client: {str(e)}")
|
||||||
|
self.client = None
|
||||||
|
|
||||||
|
async def get_login_url(self, request: Request, redirect_uri: str) -> str:
|
||||||
|
if not self.client:
|
||||||
|
self.initialize_client()
|
||||||
|
|
||||||
|
if not self.client:
|
||||||
|
raise HTTPException(status_code=500, detail="OAuth client could not be initialized")
|
||||||
|
|
||||||
|
try:
|
||||||
|
authorization_url = await self.client.get_authorization_url(
|
||||||
|
redirect_uri=redirect_uri,
|
||||||
|
scope=["openid", "email", "profile"],
|
||||||
|
state=str(request.session.get("session_id", "")),
|
||||||
|
)
|
||||||
|
return authorization_url
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting authorization URL: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"OAuth error: {str(e)}")
|
||||||
|
|
||||||
|
async def get_user_info(self, request: Request, redirect_uri: str, code: str) -> Dict[str, Any]:
|
||||||
|
if not self.client:
|
||||||
|
self.initialize_client()
|
||||||
|
|
||||||
|
if not self.client:
|
||||||
|
raise HTTPException(status_code=500, detail="OAuth client could not be initialized")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Exchange code for token
|
||||||
|
token = await self.client.get_access_token(
|
||||||
|
code=code,
|
||||||
|
redirect_uri=redirect_uri,
|
||||||
|
)
|
||||||
|
|
||||||
|
access_token = token.get("access_token")
|
||||||
|
if not access_token:
|
||||||
|
raise HTTPException(status_code=400, detail="Could not get access token")
|
||||||
|
|
||||||
|
# Get user info from OpenID userinfo endpoint
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
# Get the configuration to find the userinfo_endpoint
|
||||||
|
config_response = await client.get(self.config_url)
|
||||||
|
if config_response.status_code != 200:
|
||||||
|
raise HTTPException(status_code=500, detail="Could not fetch OpenID configuration")
|
||||||
|
|
||||||
|
config = config_response.json()
|
||||||
|
userinfo_endpoint = config.get("userinfo_endpoint")
|
||||||
|
|
||||||
|
if not userinfo_endpoint:
|
||||||
|
raise HTTPException(status_code=500, detail="UserInfo endpoint not found in OpenID configuration")
|
||||||
|
|
||||||
|
# Make request to userinfo endpoint
|
||||||
|
headers = {"Authorization": f"Bearer {access_token}"}
|
||||||
|
user_response = await client.get(userinfo_endpoint, headers=headers)
|
||||||
|
|
||||||
|
if user_response.status_code != 200:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Error fetching user info: {user_response.text}")
|
||||||
|
|
||||||
|
return user_response.json()
|
||||||
|
|
||||||
|
except GetAccessTokenError as e:
|
||||||
|
error_description = e.args[0]
|
||||||
|
raise HTTPException(status_code=400, detail=f"OAuth error: {error_description}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting user info: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"OAuth error: {str(e)}")
|
||||||
|
|
||||||
|
# Instantiate the OAuth client for the application to use
|
||||||
|
authentik_oauth = AuthentikOAuth()
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
from sqlalchemy import text
|
||||||
|
from .db import engine
|
||||||
|
|
||||||
|
def apply_migrations():
|
||||||
|
"""Apply all pending database migrations."""
|
||||||
|
|
||||||
|
# Check and add OAuth columns to users table
|
||||||
|
try:
|
||||||
|
with engine.connect() as conn:
|
||||||
|
# Check if the OAuth columns exist
|
||||||
|
result = conn.execute(text("""
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'users'
|
||||||
|
AND column_name = 'is_oauth_user'
|
||||||
|
"""))
|
||||||
|
|
||||||
|
if result.fetchone()[0] == 0:
|
||||||
|
print("Adding OAuth columns to users table...")
|
||||||
|
|
||||||
|
# Add the OAuth columns
|
||||||
|
conn.execute(text("""
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN is_oauth_user BOOLEAN DEFAULT FALSE,
|
||||||
|
ADD COLUMN oauth_id VARCHAR(255) NULL,
|
||||||
|
ADD COLUMN oauth_provider VARCHAR(50) NULL,
|
||||||
|
ADD COLUMN picture VARCHAR(255) NULL
|
||||||
|
"""))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print("OAuth columns added successfully.")
|
||||||
|
else:
|
||||||
|
print("OAuth columns already exist in users table.")
|
||||||
|
|
||||||
|
# Check if the is_admin column exists in the users table
|
||||||
|
result = conn.execute(text("""
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'users'
|
||||||
|
AND column_name = 'is_admin'
|
||||||
|
"""))
|
||||||
|
|
||||||
|
if result.fetchone()[0] == 0:
|
||||||
|
print("Adding is_admin column to users table...")
|
||||||
|
|
||||||
|
# Add the is_admin column to users table
|
||||||
|
conn.execute(text("""
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN is_admin BOOLEAN DEFAULT FALSE
|
||||||
|
"""))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print("is_admin column added successfully to users table.")
|
||||||
|
else:
|
||||||
|
print("is_admin column already exists in users table.")
|
||||||
|
|
||||||
|
# Check if the owner_id column exists in the teams table
|
||||||
|
result = conn.execute(text("""
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'teams'
|
||||||
|
AND column_name = 'owner_id'
|
||||||
|
"""))
|
||||||
|
|
||||||
|
if result.fetchone()[0] == 0:
|
||||||
|
print("Adding owner_id column to teams table...")
|
||||||
|
|
||||||
|
# Add the owner_id column to teams table
|
||||||
|
conn.execute(text("""
|
||||||
|
ALTER TABLE teams
|
||||||
|
ADD COLUMN owner_id INT NULL,
|
||||||
|
ADD CONSTRAINT fk_teams_owner
|
||||||
|
FOREIGN KEY (owner_id) REFERENCES users(id)
|
||||||
|
ON DELETE SET NULL
|
||||||
|
"""))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print("owner_id column added successfully to teams table.")
|
||||||
|
else:
|
||||||
|
print("owner_id column already exists in teams table.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error applying migrations: {str(e)}")
|
||||||
|
raise
|
||||||
+46
-5
@@ -6,16 +6,24 @@ from fastapi.templating import Jinja2Templates
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import os
|
import os
|
||||||
from starlette.middleware.sessions import SessionMiddleware
|
from starlette.middleware.sessions import SessionMiddleware
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
from .db import init_db, engine
|
from .db import init_db, engine, get_db
|
||||||
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, static, pages
|
from .views import qr, redeem, teams, admin, leaderboard, dashboard, static, pages, auth
|
||||||
from .db_init import seed_db
|
from .db_init import seed_db
|
||||||
|
from .db_migrations import apply_migrations
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
# Create tables on startup
|
# Create tables on startup
|
||||||
init_db()
|
init_db()
|
||||||
|
|
||||||
|
# Apply any pending database migrations
|
||||||
|
apply_migrations()
|
||||||
|
|
||||||
# Seed database with initial test data
|
# Seed database with initial test data
|
||||||
# In a production app, you would handle this differently
|
# In a production app, you would handle this differently
|
||||||
seed_db()
|
seed_db()
|
||||||
@@ -24,7 +32,7 @@ seed_db()
|
|||||||
app = FastAPI(title="LeagueLedger")
|
app = FastAPI(title="LeagueLedger")
|
||||||
|
|
||||||
# Add SessionMiddleware with a secure secret key
|
# Add SessionMiddleware with a secure secret key
|
||||||
app.add_middleware(SessionMiddleware, secret_key="your-very-secret-session-key")
|
app.add_middleware(SessionMiddleware, secret_key=os.getenv("SESSION_SECRET_KEY", "your-very-secret-session-key"))
|
||||||
|
|
||||||
# Mount static files
|
# Mount static files
|
||||||
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
||||||
@@ -41,7 +49,15 @@ async def add_template_globals(request: Request, call_next):
|
|||||||
"""Add template globals"""
|
"""Add template globals"""
|
||||||
try:
|
try:
|
||||||
# Update template globals for all templates
|
# Update template globals for all templates
|
||||||
templates.env.globals["current_user"] = None
|
user = None
|
||||||
|
if "user_id" in request.session and request.session.get("is_authenticated"):
|
||||||
|
# Mock user object - in a real app, you'd fetch this from the database
|
||||||
|
user = {
|
||||||
|
"id": request.session["user_id"],
|
||||||
|
"username": request.session.get("username", "User"),
|
||||||
|
"is_admin": request.session.get("is_admin", False)
|
||||||
|
}
|
||||||
|
templates.env.globals["current_user"] = user
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error setting template globals: {str(e)}")
|
print(f"Error setting template globals: {str(e)}")
|
||||||
|
|
||||||
@@ -51,13 +67,21 @@ async def add_template_globals(request: Request, call_next):
|
|||||||
|
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
async def read_root(request: Request):
|
async def read_root(request: Request):
|
||||||
|
user = None
|
||||||
|
if "user_id" in request.session and request.session.get("is_authenticated"):
|
||||||
|
user = {
|
||||||
|
"id": request.session["user_id"],
|
||||||
|
"username": request.session.get("username", "User"),
|
||||||
|
"is_admin": request.session.get("is_admin", False)
|
||||||
|
}
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"index.html",
|
"index.html",
|
||||||
{"request": request, "user": None}
|
{"request": request, "user": user}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Routers
|
# Routers
|
||||||
app.include_router(pages.router, tags=["Pages"]) # Pages router for index and static pages
|
app.include_router(pages.router, tags=["Pages"]) # Pages router for index and static pages
|
||||||
|
app.include_router(auth.router) # Include the auth router
|
||||||
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"])
|
||||||
@@ -70,3 +94,20 @@ app.include_router(static.router, tags=["Static"]) # Include the static router
|
|||||||
@app.get("/scan")
|
@app.get("/scan")
|
||||||
async def scan_redirect():
|
async def scan_redirect():
|
||||||
return RedirectResponse("/redeem/scan", status_code=303)
|
return RedirectResponse("/redeem/scan", status_code=303)
|
||||||
|
|
||||||
|
# Add convenience routes for auth paths
|
||||||
|
@app.get("/login")
|
||||||
|
async def login_redirect():
|
||||||
|
return RedirectResponse("/auth/login", status_code=303)
|
||||||
|
|
||||||
|
@app.get("/register")
|
||||||
|
async def register_redirect():
|
||||||
|
return RedirectResponse("/auth/register", status_code=303)
|
||||||
|
|
||||||
|
@app.get("/profile")
|
||||||
|
async def profile_redirect():
|
||||||
|
return RedirectResponse("/auth/profile", status_code=303)
|
||||||
|
|
||||||
|
@app.get("/logout")
|
||||||
|
async def logout_redirect():
|
||||||
|
return RedirectResponse("/auth/logout", status_code=303)
|
||||||
|
|||||||
+18
-5
@@ -2,15 +2,16 @@
|
|||||||
from sqlalchemy import Column, Integer, String, ForeignKey, Boolean, DateTime, Text, Float
|
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 sqlalchemy.ext.declarative import declarative_base
|
||||||
from .db import Base
|
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
class User(Base):
|
class User(Base):
|
||||||
__tablename__ = "users"
|
__tablename__ = "users"
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
username = Column(String(50), unique=True, index=True, nullable=False)
|
username = Column(String(50), unique=True, index=True, nullable=False)
|
||||||
email = Column(String(255), unique=True, index=True, nullable=False)
|
email = Column(String(100), unique=True, index=True, nullable=False)
|
||||||
hashed_password = Column(String(255), nullable=True)
|
hashed_password = Column(String(255), nullable=True) # Can be null for OAuth users
|
||||||
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())
|
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||||
is_active = Column(Boolean, default=True)
|
is_active = Column(Boolean, default=True)
|
||||||
@@ -21,11 +22,21 @@ class User(Base):
|
|||||||
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)
|
||||||
|
|
||||||
|
# OAuth fields
|
||||||
|
is_oauth_user = Column(Boolean, default=False)
|
||||||
|
oauth_id = Column(String(255), nullable=True)
|
||||||
|
oauth_provider = Column(String(50), nullable=True)
|
||||||
|
picture = Column(String(255), nullable=True) # URL to profile picture
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
memberships = relationship("TeamMembership", back_populates="user")
|
memberships = relationship("TeamMembership", back_populates="user")
|
||||||
teams = relationship("TeamMember", back_populates="user")
|
teams = relationship("TeamMember", back_populates="user")
|
||||||
points = relationship("UserPoints", back_populates="user")
|
points = relationship("UserPoints", back_populates="user")
|
||||||
events_attended = relationship("EventAttendee", back_populates="user")
|
events_attended = relationship("EventAttendee", back_populates="user")
|
||||||
|
owned_teams = relationship("Team", back_populates="owner")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<User {self.username}>"
|
||||||
|
|
||||||
|
|
||||||
class OAuthAccount(Base):
|
class OAuthAccount(Base):
|
||||||
@@ -50,10 +61,12 @@ class Team(Base):
|
|||||||
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
|
||||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||||
|
owner_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
memberships = relationship("TeamMembership", back_populates="team")
|
memberships = relationship("TeamMembership", back_populates="team")
|
||||||
members = relationship("TeamMember", back_populates="team")
|
members = relationship("TeamMember", back_populates="team")
|
||||||
|
owner = relationship("User", back_populates="owned_teams")
|
||||||
|
|
||||||
|
|
||||||
class TeamMembership(Base):
|
class TeamMembership(Base):
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
fastapi==0.88.0
|
|
||||||
uvicorn==0.20.0
|
|
||||||
jinja2==3.1.6
|
|
||||||
babel==2.11.0
|
|
||||||
python-gettext==5.0
|
|
||||||
aiofiles==22.1.0
|
|
||||||
python-multipart==0.0.18
|
|
||||||
pydantic==1.10.13
|
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
<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">
|
<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">
|
<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 }}
|
<i class="fas fa-sign-in-alt mr-2"></i> Authentik
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,6 +50,36 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Teams Section -->
|
||||||
|
<div class="bg-white p-6 rounded-lg shadow-md">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h2 class="text-xl font-semibold text-irish-green">Your Teams</h2>
|
||||||
|
<a href="/teams" class="text-irish-green hover:underline">View All</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if user_teams %}
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{% for team in user_teams %}
|
||||||
|
<div class="border rounded-lg p-4 hover:bg-gray-50">
|
||||||
|
<h3 class="font-medium mb-2">{{ team.name }}</h3>
|
||||||
|
<div class="flex justify-between text-sm">
|
||||||
|
<span>Rank: #{{ team.rank|default('N/A') }}</span>
|
||||||
|
<span>{{ team.points|default(0) }} pts</span>
|
||||||
|
</div>
|
||||||
|
<a href="/teams/{{ team.id }}" class="block w-full bg-irish-green bg-opacity-10 text-irish-green text-center mt-3 py-1 rounded">
|
||||||
|
View Team
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-gray-500 mb-4">You haven't joined any teams yet.</p>
|
||||||
|
<a href="/teams" class="inline-block bg-irish-green text-white py-2 px-4 rounded-md hover:bg-opacity-90 transition">
|
||||||
|
Join a Team
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Quick Actions -->
|
<!-- Quick Actions -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="bg-white p-6 rounded-lg shadow-md">
|
<div class="bg-white p-6 rounded-lg shadow-md">
|
||||||
|
|||||||
@@ -16,9 +16,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button class="bg-white text-irish-green font-medium py-2 px-4 rounded-md hover:bg-opacity-90">
|
{% if not user %}
|
||||||
<i class="fas fa-share-alt mr-1"></i> Share Team
|
<a href="/auth/login?next=/teams/{{ team.id }}" class="bg-white text-irish-green font-medium py-2 px-4 rounded-md hover:bg-opacity-90">
|
||||||
</button>
|
Login to Join
|
||||||
|
</a>
|
||||||
|
{% elif not is_team_member %}
|
||||||
|
<form action="/teams/join/{{ team.id }}" method="post" class="inline">
|
||||||
|
<button type="submit" class="bg-white text-irish-green font-medium py-2 px-4 rounded-md hover:bg-opacity-90">
|
||||||
|
Join Team
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<button class="bg-white text-irish-green font-medium py-2 px-4 rounded-md hover:bg-opacity-90">
|
||||||
|
<i class="fas fa-share-alt mr-1"></i> Share Team
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -156,34 +168,54 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Team Management -->
|
<!-- Team Management -->
|
||||||
|
{% if is_user_admin %}
|
||||||
<div class="bg-white rounded-lg shadow-md p-6">
|
<div class="bg-white rounded-lg shadow-md p-6">
|
||||||
<h2 class="text-xl font-semibold text-irish-green mb-4">Team Management</h2>
|
<h2 class="text-xl font-semibold text-irish-green mb-4">Team Management</h2>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div>
|
{% if is_user_owner %}
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Team Name</label>
|
<div class="bg-irish-green bg-opacity-10 p-3 rounded-md mb-4">
|
||||||
<div class="flex">
|
<p class="text-irish-green font-medium">You are the owner of this team</p>
|
||||||
<input type="text" value="{{ team.name }}" class="flex-grow border border-gray-300 rounded-l-md px-3 py-2" {% if not is_user_admin %}disabled{% endif %}>
|
|
||||||
<button class="bg-irish-green text-white px-4 py-2 rounded-r-md" {% if not is_user_admin %}disabled{% endif %}>Save</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% elif is_user_admin %}
|
||||||
|
<div class="bg-golden-ale bg-opacity-10 p-3 rounded-md mb-4">
|
||||||
<div>
|
<p class="text-golden-ale font-medium">You are an admin of this team</p>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Team Privacy</label>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<input type="checkbox" id="public-team" name="is_public" class="mr-2"
|
|
||||||
{% if team.is_public %}checked{% endif %}
|
|
||||||
{% if not is_user_admin %}disabled{% endif %}>
|
|
||||||
<label for="public-team">Make team publicly joinable (no invitation needed)</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% endif %}
|
||||||
|
|
||||||
<div class="pt-4 border-t">
|
<form action="/teams/{{ team.id }}/update" method="post">
|
||||||
<button class="bg-red-600 hover:bg-red-700 text-white font-medium py-2 px-4 rounded-md">
|
<div class="mb-4">
|
||||||
Leave Team
|
<label for="team_name" class="block text-sm font-medium text-gray-700 mb-2">Team Name</label>
|
||||||
</button>
|
<div class="flex">
|
||||||
</div>
|
<input type="text" id="team_name" name="team_name" value="{{ team.name }}" class="flex-grow border border-gray-300 rounded-l-md px-3 py-2">
|
||||||
|
<button type="submit" class="bg-irish-green text-white px-4 py-2 rounded-r-md">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Team Privacy</label>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input type="checkbox" id="is_public" name="is_public" class="mr-2"
|
||||||
|
{% if team.is_public %}checked{% endif %}>
|
||||||
|
<label for="is_public">Make team publicly joinable (no invitation needed)</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Leave team option - only for members -->
|
||||||
|
{% if is_team_member and not is_user_owner %}
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 mt-6">
|
||||||
|
<h2 class="text-xl font-semibold text-irish-green mb-4">Team Membership</h2>
|
||||||
|
<p class="mb-4">You are currently a member of this team.</p>
|
||||||
|
<form action="/teams/{{ team.id }}/leave" method="post">
|
||||||
|
<button type="submit" class="bg-red-600 hover:bg-red-700 text-white font-medium py-2 px-4 rounded-md">
|
||||||
|
Leave Team
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+73
-20
@@ -1,6 +1,13 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2 class="text-2xl font-bold mb-6">Teams</h2>
|
<h2 class="text-2xl font-bold mb-6">Teams</h2>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-6" role="alert">
|
||||||
|
<span class="block sm:inline">{{ error }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="grid md:grid-cols-2 gap-6">
|
<div class="grid md:grid-cols-2 gap-6">
|
||||||
<div class="bg-white p-6 rounded-lg shadow-md">
|
<div class="bg-white p-6 rounded-lg shadow-md">
|
||||||
<h3 class="text-xl font-semibold mb-4" style="color: var(--irish-green);">Available Teams</h3>
|
<h3 class="text-xl font-semibold mb-4" style="color: var(--irish-green);">Available Teams</h3>
|
||||||
@@ -8,14 +15,26 @@
|
|||||||
<ul class="space-y-3">
|
<ul class="space-y-3">
|
||||||
{% for team in teams %}
|
{% for team in teams %}
|
||||||
<li class="border-b pb-2 flex justify-between items-center">
|
<li class="border-b pb-2 flex justify-between items-center">
|
||||||
<span class="font-medium">{{ team.name }}</span>
|
<a href="/teams/{{ team.id }}" class="font-medium hover:text-irish-green">{{ team.name }}</a>
|
||||||
<form action="/teams/join/{{ team.id }}" method="post" class="inline">
|
{% if user %}
|
||||||
<button type="submit"
|
{% if team.id in user_team_ids %}
|
||||||
class="px-3 py-1 text-sm rounded-md"
|
<span class="px-3 py-1 text-sm rounded-md bg-gray-200 text-gray-800">Member</span>
|
||||||
style="background-color: var(--golden-ale); color: var(--black-stout);">
|
{% else %}
|
||||||
Join Team
|
<form action="/teams/join/{{ team.id }}" method="post" class="inline">
|
||||||
</button>
|
<button type="submit"
|
||||||
</form>
|
class="px-3 py-1 text-sm rounded-md"
|
||||||
|
style="background-color: var(--golden-ale); color: var(--black-stout);">
|
||||||
|
Join Team
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<a href="/auth/login?next=/teams"
|
||||||
|
class="px-3 py-1 text-sm rounded-md"
|
||||||
|
style="background-color: var(--cream-white); color: var(--irish-green); border: 1px solid var(--irish-green);">
|
||||||
|
Login to Join
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -26,22 +45,56 @@
|
|||||||
|
|
||||||
<div class="bg-white p-6 rounded-lg shadow-md">
|
<div class="bg-white p-6 rounded-lg shadow-md">
|
||||||
<h3 class="text-xl font-semibold mb-4" style="color: var(--irish-green);">Create New Team</h3>
|
<h3 class="text-xl font-semibold mb-4" style="color: var(--irish-green);">Create New Team</h3>
|
||||||
<form action="/teams/create" method="post" class="mt-4">
|
{% if user %}
|
||||||
<div class="mb-4">
|
<form action="/teams/create" method="post" class="mt-4">
|
||||||
<label for="name" class="block text-sm font-medium mb-1">Team Name</label>
|
<div class="mb-4">
|
||||||
<input type="text" name="name" id="name" placeholder="Enter team name" required
|
<label for="name" class="block text-sm font-medium mb-1">Team Name</label>
|
||||||
class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2"
|
<input type="text" name="name" id="name" placeholder="Enter team name" required
|
||||||
style="border-color: var(--irish-green); focus:ring-color: var(--irish-green);">
|
class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2"
|
||||||
|
style="border-color: var(--irish-green); focus:ring-color: var(--irish-green);">
|
||||||
|
</div>
|
||||||
|
<button type="submit"
|
||||||
|
class="w-full px-4 py-2 text-white rounded-md font-medium"
|
||||||
|
style="background-color: var(--irish-green);">
|
||||||
|
Create Team
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<div class="p-4 bg-gray-100 rounded-md">
|
||||||
|
<p class="text-center mb-2">You need to be logged in to create a team</p>
|
||||||
|
<a href="/auth/login"
|
||||||
|
class="block w-full text-center px-4 py-2 text-white rounded-md font-medium"
|
||||||
|
style="background-color: var(--irish-green);">
|
||||||
|
Log In
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit"
|
{% endif %}
|
||||||
class="w-full px-4 py-2 text-white rounded-md font-medium"
|
|
||||||
style="background-color: var(--irish-green);">
|
|
||||||
Create Team
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if user and user_team_ids %}
|
||||||
|
<div class="mt-8">
|
||||||
|
<h2 class="text-2xl font-bold mb-4">Your Teams</h2>
|
||||||
|
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{% for team in teams %}
|
||||||
|
{% if team.id in user_team_ids %}
|
||||||
|
<div class="bg-white p-6 rounded-lg shadow-md">
|
||||||
|
<h3 class="text-xl font-semibold mb-2" style="color: var(--irish-green);">{{ team.name }}</h3>
|
||||||
|
<div class="mb-4">
|
||||||
|
<span class="text-gray-600">{{ team.description|default("No description available", true)|truncate(120) }}</span>
|
||||||
|
</div>
|
||||||
|
<a href="/teams/{{ team.id }}"
|
||||||
|
class="block text-center w-full px-4 py-2 text-white rounded-md font-medium"
|
||||||
|
style="background-color: var(--irish-green);">
|
||||||
|
View Team
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="mt-10 bg-white p-6 rounded-lg shadow-md">
|
<div class="mt-10 bg-white p-6 rounded-lg shadow-md">
|
||||||
<h3 class="text-xl font-semibold mb-4" style="color: var(--irish-green);">About Teams</h3>
|
<h3 class="text-xl font-semibold mb-4" style="color: var(--irish-green);">About Teams</h3>
|
||||||
<p class="mb-4">
|
<p class="mb-4">
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
from fastapi import APIRouter, Request, Depends, Form, HTTPException, status
|
||||||
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from typing import Optional
|
||||||
|
import secrets
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
from starlette.status import HTTP_303_SEE_OTHER, HTTP_302_FOUND
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from ..db import get_db
|
||||||
|
from ..models import User
|
||||||
|
from ..auth.oauth import authentik_oauth
|
||||||
|
from ..templates_config import templates
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/auth", tags=["Auth"])
|
||||||
|
|
||||||
|
@router.get("/login", response_class=HTMLResponse)
|
||||||
|
async def login_page(request: Request, error: Optional[str] = None, message: Optional[str] = None):
|
||||||
|
"""Login page route"""
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"auth/login.html",
|
||||||
|
{"request": request, "error": error, "message": message,
|
||||||
|
"show_oauth": True, "oauth_provider_name": "Authentik"}
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.post("/login", response_class=HTMLResponse)
|
||||||
|
async def login_post(
|
||||||
|
request: Request,
|
||||||
|
username: str = Form(...),
|
||||||
|
password: str = Form(...),
|
||||||
|
remember: Optional[bool] = Form(False),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Handle login form submission"""
|
||||||
|
# This is a placeholder - implement real login logic here
|
||||||
|
error = "This login method is not fully implemented yet"
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"auth/login.html",
|
||||||
|
{"request": request, "error": error, "show_oauth": True, "oauth_provider_name": "Authentik"}
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get("/register", response_class=HTMLResponse)
|
||||||
|
async def register_page(request: Request, error: Optional[str] = None):
|
||||||
|
"""Registration page route"""
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"auth/register.html",
|
||||||
|
{"request": request, "error": error}
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.post("/register", response_class=HTMLResponse)
|
||||||
|
async def register_post(
|
||||||
|
request: Request,
|
||||||
|
username: str = Form(...),
|
||||||
|
email: str = Form(...),
|
||||||
|
password: str = Form(...),
|
||||||
|
confirm_password: str = Form(...),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Handle registration form submission"""
|
||||||
|
# This is a placeholder - implement real registration logic here
|
||||||
|
if password != confirm_password:
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"auth/register.html",
|
||||||
|
{"request": request, "error": "Passwords do not match"}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check username and email uniqueness, then create user
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"auth/registration_success.html",
|
||||||
|
{"request": request}
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get("/oauth-login")
|
||||||
|
async def oauth_login(request: Request):
|
||||||
|
"""Start the OAuth login flow"""
|
||||||
|
# Generate the redirect URI
|
||||||
|
base_url = str(request.base_url)
|
||||||
|
redirect_uri = f"{base_url}auth/oauth-callback"
|
||||||
|
|
||||||
|
# Request a login URL from the Authentik provider
|
||||||
|
try:
|
||||||
|
# Set a session ID to validate the callback
|
||||||
|
if "session_id" not in request.session:
|
||||||
|
request.session["session_id"] = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Get the authorization URL - make sure to await it
|
||||||
|
auth_url = await authentik_oauth.get_login_url(request, redirect_uri)
|
||||||
|
|
||||||
|
# Redirect to the authorization URL
|
||||||
|
return RedirectResponse(auth_url)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"OAuth login error: {str(e)}")
|
||||||
|
return RedirectResponse(
|
||||||
|
f"/auth/login?error=OAuth+login+failed:+{str(e)}",
|
||||||
|
status_code=HTTP_303_SEE_OTHER
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get("/oauth-callback")
|
||||||
|
async def oauth_callback(request: Request, code: Optional[str] = None, state: Optional[str] = None, error: Optional[str] = None, db: Session = Depends(get_db)):
|
||||||
|
"""Handle the OAuth callback"""
|
||||||
|
if error:
|
||||||
|
return RedirectResponse(
|
||||||
|
f"/auth/login?error=OAuth+login+failed:+{error}",
|
||||||
|
status_code=HTTP_303_SEE_OTHER
|
||||||
|
)
|
||||||
|
|
||||||
|
if not code:
|
||||||
|
return RedirectResponse(
|
||||||
|
"/auth/login?error=No+authorization+code+received",
|
||||||
|
status_code=HTTP_303_SEE_OTHER
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate the redirect URI that matches the one used in the initial request
|
||||||
|
base_url = str(request.base_url)
|
||||||
|
redirect_uri = f"{base_url}auth/oauth-callback"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get user info from the provider
|
||||||
|
user_info = await authentik_oauth.get_user_info(request, redirect_uri, code)
|
||||||
|
|
||||||
|
if not user_info:
|
||||||
|
return RedirectResponse(
|
||||||
|
"/auth/login?error=Could+not+retrieve+user+information",
|
||||||
|
status_code=HTTP_303_SEE_OTHER
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract user details from OAuth info
|
||||||
|
sub = user_info.get("sub", "")
|
||||||
|
email = user_info.get("email", "")
|
||||||
|
name = user_info.get("preferred_username", "") or user_info.get("name", "") or email.split("@")[0]
|
||||||
|
picture = user_info.get("picture", None)
|
||||||
|
|
||||||
|
# Check if the user already exists
|
||||||
|
user = db.query(User).filter(User.email == email).first()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
print(f"Creating new user with email {email} and username {name}")
|
||||||
|
# Create a new user
|
||||||
|
user = User(
|
||||||
|
username=name,
|
||||||
|
email=email,
|
||||||
|
oauth_id=sub,
|
||||||
|
is_oauth_user=True,
|
||||||
|
oauth_provider="authentik",
|
||||||
|
picture=picture,
|
||||||
|
is_verified=True # OAuth users are considered verified
|
||||||
|
)
|
||||||
|
db.add(user)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(user)
|
||||||
|
else:
|
||||||
|
# Update existing user's OAuth information
|
||||||
|
if not user.is_oauth_user:
|
||||||
|
user.is_oauth_user = True
|
||||||
|
user.oauth_id = sub
|
||||||
|
user.oauth_provider = "authentik"
|
||||||
|
|
||||||
|
# Update profile picture if available
|
||||||
|
if picture and not user.picture:
|
||||||
|
user.picture = picture
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Set session data
|
||||||
|
request.session["user_id"] = user.id
|
||||||
|
request.session["username"] = user.username
|
||||||
|
request.session["is_authenticated"] = True
|
||||||
|
request.session["is_admin"] = user.is_admin
|
||||||
|
|
||||||
|
return RedirectResponse("/dashboard", status_code=HTTP_303_SEE_OTHER)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"OAuth callback error: {str(e)}")
|
||||||
|
return RedirectResponse(
|
||||||
|
f"/auth/login?error=OAuth+login+failed:+{str(e)}",
|
||||||
|
status_code=HTTP_303_SEE_OTHER
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get("/logout")
|
||||||
|
async def logout(request: Request):
|
||||||
|
"""Log out the user"""
|
||||||
|
request.session.clear()
|
||||||
|
return RedirectResponse("/", status_code=HTTP_303_SEE_OTHER)
|
||||||
|
|
||||||
|
@router.get("/profile", response_class=HTMLResponse)
|
||||||
|
async def profile_page(request: Request):
|
||||||
|
"""User profile page"""
|
||||||
|
# Get the user ID from the session
|
||||||
|
user_id = request.session.get("user_id")
|
||||||
|
|
||||||
|
if not user_id:
|
||||||
|
return RedirectResponse("/auth/login", status_code=HTTP_303_SEE_OTHER)
|
||||||
|
|
||||||
|
# Mock user data - in a real app, you'd fetch this from the database
|
||||||
|
user = {
|
||||||
|
"id": user_id,
|
||||||
|
"username": request.session.get("username", "User"),
|
||||||
|
"email": "user@example.com",
|
||||||
|
"is_admin": request.session.get("is_admin", False),
|
||||||
|
"created_at": "2023-01-01 12:00:00",
|
||||||
|
"picture": None
|
||||||
|
}
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"auth/profile.html",
|
||||||
|
{"request": request, "user": user}
|
||||||
|
)
|
||||||
+38
-15
@@ -14,13 +14,17 @@ router = APIRouter()
|
|||||||
def user_dashboard(request: Request, db: Session = Depends(get_db)):
|
def user_dashboard(request: Request, db: Session = Depends(get_db)):
|
||||||
"""User dashboard showing teams, events and stats"""
|
"""User dashboard showing teams, events and stats"""
|
||||||
try:
|
try:
|
||||||
# Fetch user data directly from the database
|
# Get user ID from session instead of using hardcoded admin
|
||||||
user = db.query(models.User).filter_by(email="admin@example.com").first() # Example user lookup
|
user_id = request.session.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
# Redirect to login if not authenticated
|
||||||
|
return RedirectResponse("/auth/login", status_code=303)
|
||||||
|
|
||||||
|
# Fetch user data from the database using session user ID
|
||||||
|
user = db.query(models.User).get(user_id)
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
|
||||||
user_id = user.id
|
|
||||||
|
|
||||||
# Initialize default values in case of errors
|
# Initialize default values in case of errors
|
||||||
team_count = 0
|
team_count = 0
|
||||||
total_points = 0
|
total_points = 0
|
||||||
@@ -28,9 +32,20 @@ def user_dashboard(request: Request, db: Session = Depends(get_db)):
|
|||||||
recent_events = []
|
recent_events = []
|
||||||
user_teams = []
|
user_teams = []
|
||||||
|
|
||||||
# Check if TeamMember model exists before querying
|
# Get team memberships - check both TeamMember and TeamMembership models
|
||||||
if hasattr(models, "TeamMember"):
|
if hasattr(models, "TeamMembership"):
|
||||||
# Get the team count for this user
|
# Primary check - use TeamMembership model
|
||||||
|
team_count = db.query(func.count(models.TeamMembership.team_id))\
|
||||||
|
.filter(models.TeamMembership.user_id == user_id)\
|
||||||
|
.scalar() or 0
|
||||||
|
|
||||||
|
# Get user teams
|
||||||
|
user_teams = db.query(models.Team)\
|
||||||
|
.join(models.TeamMembership)\
|
||||||
|
.filter(models.TeamMembership.user_id == user_id)\
|
||||||
|
.all()
|
||||||
|
elif hasattr(models, "TeamMember"):
|
||||||
|
# Fallback to TeamMember model if TeamMembership doesn't exist
|
||||||
team_count = db.query(func.count(models.TeamMember.team_id))\
|
team_count = db.query(func.count(models.TeamMember.team_id))\
|
||||||
.filter(models.TeamMember.user_id == user_id)\
|
.filter(models.TeamMember.user_id == user_id)\
|
||||||
.scalar() or 0
|
.scalar() or 0
|
||||||
@@ -41,15 +56,23 @@ def user_dashboard(request: Request, db: Session = Depends(get_db)):
|
|||||||
.filter(models.TeamMember.user_id == user_id)\
|
.filter(models.TeamMember.user_id == user_id)\
|
||||||
.all()
|
.all()
|
||||||
|
|
||||||
# Check if UserPoints model exists before querying
|
# Get total points from QRCode redemptions
|
||||||
if hasattr(models, "UserPoints"):
|
# First try directly from QRCodes tied to user
|
||||||
# Get the total points safely
|
total_points_result = db.query(func.sum(models.QRCode.points))\
|
||||||
total_points_result = db.query(func.sum(models.UserPoints.points))\
|
.filter(models.QRCode.redeemed_by == user_id, models.QRCode.used == True)\
|
||||||
.filter(models.UserPoints.user_id == user_id)\
|
.first()
|
||||||
.first()
|
|
||||||
|
|
||||||
if total_points_result and total_points_result[0]:
|
if total_points_result and total_points_result[0]:
|
||||||
total_points = total_points_result[0]
|
total_points = total_points_result[0]
|
||||||
|
else:
|
||||||
|
# Fallback to UserPoints model if available
|
||||||
|
if hasattr(models, "UserPoints"):
|
||||||
|
points_result = db.query(func.sum(models.UserPoints.points))\
|
||||||
|
.filter(models.UserPoints.user_id == user_id)\
|
||||||
|
.first()
|
||||||
|
|
||||||
|
if points_result and points_result[0]:
|
||||||
|
total_points = points_result[0]
|
||||||
|
|
||||||
# Check if EventAttendee model exists before querying
|
# Check if EventAttendee model exists before querying
|
||||||
if hasattr(models, "EventAttendee") and hasattr(models, "Event"):
|
if hasattr(models, "EventAttendee") and hasattr(models, "Event"):
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from sqlalchemy import func, desc
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from ..db import SessionLocal
|
from ..db import SessionLocal
|
||||||
from ..models import Team, TeamMembership, QRCode
|
from ..models import Team, TeamMembership, QRCode, User
|
||||||
from ..templates_config import templates
|
from ..templates_config import templates
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -29,6 +29,12 @@ async def show_leaderboard(
|
|||||||
):
|
):
|
||||||
"""Show the leaderboard with team rankings."""
|
"""Show the leaderboard with team rankings."""
|
||||||
|
|
||||||
|
# Get user from session for navbar
|
||||||
|
user = None
|
||||||
|
user_id = request.session.get("user_id")
|
||||||
|
if user_id:
|
||||||
|
user = db.query(User).get(user_id)
|
||||||
|
|
||||||
# Define cutoff date based on timeframe
|
# Define cutoff date based on timeframe
|
||||||
cutoff_date = None
|
cutoff_date = None
|
||||||
if timeframe == "week":
|
if timeframe == "week":
|
||||||
@@ -81,6 +87,7 @@ async def show_leaderboard(
|
|||||||
"teams": ranked_teams,
|
"teams": ranked_teams,
|
||||||
"top_teams": top_teams,
|
"top_teams": top_teams,
|
||||||
"timeframe": timeframe,
|
"timeframe": timeframe,
|
||||||
"time_label": time_label
|
"time_label": time_label,
|
||||||
|
"user": user # Add user to the context
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
+77
-11
@@ -2,45 +2,111 @@
|
|||||||
"""
|
"""
|
||||||
Router for static content pages like about, contact, privacy, and terms.
|
Router for static content pages like about, contact, privacy, and terms.
|
||||||
"""
|
"""
|
||||||
from fastapi import APIRouter, Request
|
from fastapi import APIRouter, Request, Depends
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse
|
||||||
from ..templates_config import templates
|
from ..templates_config import templates
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from ..db import get_db
|
||||||
|
from .. import models
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/", response_class=HTMLResponse)
|
@router.get("/", response_class=HTMLResponse)
|
||||||
def index(request: Request):
|
def index(request: Request, db: Session = Depends(get_db)):
|
||||||
"""Home page."""
|
"""Home page."""
|
||||||
|
# Get user from session for navbar
|
||||||
|
user = None
|
||||||
|
user_id = request.session.get("user_id")
|
||||||
|
if user_id:
|
||||||
|
user = db.query(models.User).get(user_id)
|
||||||
|
|
||||||
return templates.TemplateResponse("index.html", {
|
return templates.TemplateResponse("index.html", {
|
||||||
"request": request,
|
"request": request,
|
||||||
"now": datetime.now
|
"now": datetime.now,
|
||||||
|
"user": user
|
||||||
})
|
})
|
||||||
|
|
||||||
@router.get("/about", response_class=HTMLResponse)
|
@router.get("/about", response_class=HTMLResponse)
|
||||||
def about(request: Request):
|
def about(request: Request, db: Session = Depends(get_db)):
|
||||||
"""About page."""
|
"""About page."""
|
||||||
|
# Get user from session for navbar
|
||||||
|
user = None
|
||||||
|
user_id = request.session.get("user_id")
|
||||||
|
if user_id:
|
||||||
|
user = db.query(models.User).get(user_id)
|
||||||
|
|
||||||
return templates.TemplateResponse("about.html", {
|
return templates.TemplateResponse("about.html", {
|
||||||
"request": request
|
"request": request,
|
||||||
|
"user": user
|
||||||
})
|
})
|
||||||
|
|
||||||
@router.get("/contact", response_class=HTMLResponse)
|
@router.get("/contact", response_class=HTMLResponse)
|
||||||
def contact(request: Request):
|
def contact(request: Request, db: Session = Depends(get_db)):
|
||||||
"""Contact page."""
|
"""Contact page."""
|
||||||
|
# Get user from session for navbar
|
||||||
|
user = None
|
||||||
|
user_id = request.session.get("user_id")
|
||||||
|
if user_id:
|
||||||
|
user = db.query(models.User).get(user_id)
|
||||||
|
|
||||||
return templates.TemplateResponse("contact.html", {
|
return templates.TemplateResponse("contact.html", {
|
||||||
"request": request
|
"request": request,
|
||||||
|
"user": user
|
||||||
})
|
})
|
||||||
|
|
||||||
@router.get("/privacy", response_class=HTMLResponse)
|
@router.get("/privacy", response_class=HTMLResponse)
|
||||||
def privacy(request: Request):
|
def privacy(request: Request, db: Session = Depends(get_db)):
|
||||||
"""Privacy policy page."""
|
"""Privacy policy page."""
|
||||||
|
# Get user from session for navbar
|
||||||
|
user = None
|
||||||
|
user_id = request.session.get("user_id")
|
||||||
|
if user_id:
|
||||||
|
user = db.query(models.User).get(user_id)
|
||||||
|
|
||||||
return templates.TemplateResponse("privacy.html", {
|
return templates.TemplateResponse("privacy.html", {
|
||||||
"request": request
|
"request": request,
|
||||||
|
"user": user
|
||||||
})
|
})
|
||||||
|
|
||||||
@router.get("/terms", response_class=HTMLResponse)
|
@router.get("/terms", response_class=HTMLResponse)
|
||||||
def terms(request: Request):
|
def terms(request: Request, db: Session = Depends(get_db)):
|
||||||
"""Terms and conditions page."""
|
"""Terms and conditions page."""
|
||||||
|
# Get user from session for navbar
|
||||||
|
user = None
|
||||||
|
user_id = request.session.get("user_id")
|
||||||
|
if user_id:
|
||||||
|
user = db.query(models.User).get(user_id)
|
||||||
|
|
||||||
return templates.TemplateResponse("terms.html", {
|
return templates.TemplateResponse("terms.html", {
|
||||||
"request": request
|
"request": request,
|
||||||
|
"user": user
|
||||||
|
})
|
||||||
|
|
||||||
|
@router.get("/cookies", response_class=HTMLResponse)
|
||||||
|
def cookies(request: Request, db: Session = Depends(get_db)):
|
||||||
|
"""Cookie policy page."""
|
||||||
|
# Get user from session for navbar
|
||||||
|
user = None
|
||||||
|
user_id = request.session.get("user_id")
|
||||||
|
if user_id:
|
||||||
|
user = db.query(models.User).get(user_id)
|
||||||
|
|
||||||
|
return templates.TemplateResponse("cookies.html", {
|
||||||
|
"request": request,
|
||||||
|
"user": user
|
||||||
|
})
|
||||||
|
|
||||||
|
@router.get("/impressum", response_class=HTMLResponse)
|
||||||
|
def impressum(request: Request, db: Session = Depends(get_db)):
|
||||||
|
"""Imprint/Impressum page."""
|
||||||
|
# Get user from session for navbar
|
||||||
|
user = None
|
||||||
|
user_id = request.session.get("user_id")
|
||||||
|
if user_id:
|
||||||
|
user = db.query(models.User).get(user_id)
|
||||||
|
|
||||||
|
return templates.TemplateResponse("impressum.html", {
|
||||||
|
"request": request,
|
||||||
|
"user": user
|
||||||
})
|
})
|
||||||
|
|||||||
+195
-49
@@ -10,7 +10,7 @@ from datetime import datetime, timedelta
|
|||||||
import random # For demo data
|
import random # For demo data
|
||||||
|
|
||||||
from ..db import SessionLocal
|
from ..db import SessionLocal
|
||||||
from ..models import Team, TeamMembership, User, QRCode, TeamAchievement
|
from ..models import Team, TeamMembership, User, QRCode, TeamAchievement, TeamMember
|
||||||
from ..schemas import TeamCreate
|
from ..schemas import TeamCreate
|
||||||
from ..templates_config import templates
|
from ..templates_config import templates
|
||||||
|
|
||||||
@@ -30,12 +30,26 @@ def list_teams(request: Request, db: Session = Depends(get_db)):
|
|||||||
# Get the user's teams to highlight teams they're already in
|
# Get the user's teams to highlight teams they're already in
|
||||||
user_team_ids = []
|
user_team_ids = []
|
||||||
|
|
||||||
|
# Get user from session for navbar
|
||||||
|
user = None
|
||||||
|
user_id = request.session.get("user_id")
|
||||||
|
if user_id:
|
||||||
|
user = db.query(User).get(user_id)
|
||||||
|
# Get teams that user is a member of
|
||||||
|
memberships = db.query(TeamMembership).filter(TeamMembership.user_id == user_id).all()
|
||||||
|
user_team_ids = [membership.team_id for membership in memberships]
|
||||||
|
|
||||||
|
# Get error message if present
|
||||||
|
error = request.query_params.get("error")
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"teams.html",
|
"teams.html",
|
||||||
{
|
{
|
||||||
"request": request,
|
"request": request,
|
||||||
"teams": teams,
|
"teams": teams,
|
||||||
"user_team_ids": user_team_ids,
|
"user_team_ids": user_team_ids,
|
||||||
|
"user": user,
|
||||||
|
"error": error,
|
||||||
"brand_colors": {
|
"brand_colors": {
|
||||||
"irish_green": "#006837",
|
"irish_green": "#006837",
|
||||||
"golden_ale": "#FFB400",
|
"golden_ale": "#FFB400",
|
||||||
@@ -48,31 +62,153 @@ def list_teams(request: Request, db: Session = Depends(get_db)):
|
|||||||
|
|
||||||
@router.post("/create")
|
@router.post("/create")
|
||||||
def create_team(request: Request, name: str = Form(...), db: Session = Depends(get_db)):
|
def create_team(request: Request, name: str = Form(...), db: Session = Depends(get_db)):
|
||||||
# Create team
|
# Check if user is logged in
|
||||||
new_team = Team(name=name)
|
user_id = request.session.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
return RedirectResponse("/auth/login?next=/teams", status_code=303)
|
||||||
|
|
||||||
|
# Get the user
|
||||||
|
user = db.query(User).get(user_id)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse("/auth/login", status_code=303)
|
||||||
|
|
||||||
|
# Check if team name already exists
|
||||||
|
existing_team = db.query(Team).filter(Team.name == name).first()
|
||||||
|
if existing_team:
|
||||||
|
# Return to teams page with error message
|
||||||
|
# In a real app, you'd add error handling/flash messages
|
||||||
|
return RedirectResponse("/teams/?error=Team+name+already+exists", status_code=303)
|
||||||
|
|
||||||
|
# Create team with the user as owner
|
||||||
|
new_team = Team(name=name, owner_id=user_id)
|
||||||
db.add(new_team)
|
db.add(new_team)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(new_team)
|
db.refresh(new_team)
|
||||||
|
|
||||||
|
# Make the user an admin of the team in TeamMembership
|
||||||
|
team_membership = TeamMembership(
|
||||||
|
user_id=user_id,
|
||||||
|
team_id=new_team.id,
|
||||||
|
is_admin=True # User becomes admin of the team
|
||||||
|
)
|
||||||
|
db.add(team_membership)
|
||||||
|
|
||||||
|
# Check if TeamMember model exists in the database
|
||||||
|
try:
|
||||||
|
# Use a safer approach to check if the model exists and is usable
|
||||||
|
if 'team_members' in inspect(db.bind).get_table_names():
|
||||||
|
# Create TeamMember relationship as well
|
||||||
|
team_member = TeamMember(
|
||||||
|
user_id=user_id,
|
||||||
|
team_id=new_team.id,
|
||||||
|
is_captain=True # User becomes captain in TeamMember model
|
||||||
|
)
|
||||||
|
db.add(team_member)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Could not create TeamMember record: {str(e)}")
|
||||||
|
# Continue even if this fails - TeamMembership is primary relationship
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
return RedirectResponse("/teams/", status_code=303)
|
return RedirectResponse("/teams/", status_code=303)
|
||||||
|
|
||||||
@router.post("/join/{team_id}")
|
@router.post("/join/{team_id}")
|
||||||
def join_team(request: Request, team_id: int, db: Session = Depends(get_db)):
|
def join_team(request: Request, team_id: int, db: Session = Depends(get_db)):
|
||||||
|
# Check if user is logged in
|
||||||
|
user_id = request.session.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
return RedirectResponse("/auth/login?next=/teams", status_code=303)
|
||||||
|
|
||||||
|
# Get user
|
||||||
|
user = db.query(User).get(user_id)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse("/auth/login", status_code=303)
|
||||||
|
|
||||||
|
# Find the team
|
||||||
team = db.query(Team).filter_by(id=team_id).first()
|
team = db.query(Team).filter_by(id=team_id).first()
|
||||||
if not team:
|
if not team:
|
||||||
return RedirectResponse("/teams/", status_code=303)
|
return RedirectResponse("/teams/?error=Team+not+found", status_code=303)
|
||||||
|
|
||||||
# Check if membership exists
|
# Check if membership exists
|
||||||
existing = db.query(TeamMembership).filter_by(team_id=team.id).first()
|
existing = db.query(TeamMembership)\
|
||||||
|
.filter_by(user_id=user_id, team_id=team.id)\
|
||||||
|
.first()
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
return RedirectResponse("/teams/", status_code=303)
|
return RedirectResponse("/teams/?error=You+are+already+a+member+of+this+team", status_code=303)
|
||||||
|
|
||||||
# Create membership
|
# Create membership
|
||||||
new_member = TeamMembership(team_id=team.id, is_admin=False)
|
new_member = TeamMembership(
|
||||||
|
user_id=user_id,
|
||||||
|
team_id=team.id,
|
||||||
|
is_admin=False
|
||||||
|
)
|
||||||
db.add(new_member)
|
db.add(new_member)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
return RedirectResponse("/teams/", status_code=303)
|
# Redirect to the team detail page
|
||||||
|
return RedirectResponse(f"/teams/{team_id}", status_code=303)
|
||||||
|
|
||||||
|
@router.post("/{team_id}/leave")
|
||||||
|
def leave_team(request: Request, team_id: int, db: Session = Depends(get_db)):
|
||||||
|
"""Allow a user to leave a team"""
|
||||||
|
# Check if user is logged in
|
||||||
|
user_id = request.session.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
return RedirectResponse("/auth/login?next=/teams", status_code=303)
|
||||||
|
|
||||||
|
# Get team
|
||||||
|
team = db.query(Team).filter_by(id=team_id).first()
|
||||||
|
if not team:
|
||||||
|
raise HTTPException(status_code=404, detail="Team not found")
|
||||||
|
|
||||||
|
# Can't leave if you're the owner
|
||||||
|
if team.owner_id == user_id:
|
||||||
|
return RedirectResponse(f"/teams/{team_id}?error=Team+owner+cannot+leave", status_code=303)
|
||||||
|
|
||||||
|
# Find membership
|
||||||
|
membership = db.query(TeamMembership)\
|
||||||
|
.filter(TeamMembership.user_id == user_id, TeamMembership.team_id == team_id)\
|
||||||
|
.first()
|
||||||
|
|
||||||
|
if not membership:
|
||||||
|
return RedirectResponse("/teams/?error=You+are+not+a+member+of+this+team", status_code=303)
|
||||||
|
|
||||||
|
# Delete the team membership
|
||||||
|
db.delete(membership)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return RedirectResponse("/teams/?message=Successfully+left+the+team", status_code=303)
|
||||||
|
|
||||||
|
@router.post("/{team_id}/update")
|
||||||
|
def update_team(
|
||||||
|
request: Request,
|
||||||
|
team_id: int,
|
||||||
|
team_name: str = Form(...),
|
||||||
|
is_public: bool = Form(False),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Update team details."""
|
||||||
|
team = db.query(Team).filter_by(id=team_id).first()
|
||||||
|
|
||||||
|
if not team:
|
||||||
|
raise HTTPException(status_code=404, detail="Team not found")
|
||||||
|
|
||||||
|
# Check if user is admin
|
||||||
|
membership = db.query(TeamMembership).filter_by(
|
||||||
|
team_id=team.id,
|
||||||
|
is_admin=True
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not membership:
|
||||||
|
raise HTTPException(status_code=403, detail="You don't have permission to update this team")
|
||||||
|
|
||||||
|
# Update team details
|
||||||
|
team.name = team_name
|
||||||
|
team.is_public = is_public
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return RedirectResponse(f"/teams/{team_id}", status_code=303)
|
||||||
|
|
||||||
@router.get("/{team_id}", response_class=HTMLResponse)
|
@router.get("/{team_id}", response_class=HTMLResponse)
|
||||||
def team_detail(request: Request, team_id: int, db: Session = Depends(get_db)):
|
def team_detail(request: Request, team_id: int, db: Session = Depends(get_db)):
|
||||||
@@ -82,6 +218,21 @@ def team_detail(request: Request, team_id: int, db: Session = Depends(get_db)):
|
|||||||
if not team:
|
if not team:
|
||||||
raise HTTPException(status_code=404, detail="Team not found")
|
raise HTTPException(status_code=404, detail="Team not found")
|
||||||
|
|
||||||
|
# Get user from session for navbar
|
||||||
|
user = None
|
||||||
|
user_id = request.session.get("user_id")
|
||||||
|
is_team_member = False
|
||||||
|
|
||||||
|
if user_id:
|
||||||
|
user = db.query(User).get(user_id)
|
||||||
|
|
||||||
|
# Check if user is a team member
|
||||||
|
team_membership = db.query(TeamMembership)\
|
||||||
|
.filter(TeamMembership.user_id == user_id, TeamMembership.team_id == team_id)\
|
||||||
|
.first()
|
||||||
|
|
||||||
|
is_team_member = team_membership is not None
|
||||||
|
|
||||||
# Get team members with admin status
|
# Get team members with admin status
|
||||||
memberships = db.query(TeamMembership).filter_by(team_id=team_id).all()
|
memberships = db.query(TeamMembership).filter_by(team_id=team_id).all()
|
||||||
team_members = []
|
team_members = []
|
||||||
@@ -90,6 +241,10 @@ def team_detail(request: Request, team_id: int, db: Session = Depends(get_db)):
|
|||||||
for membership in memberships:
|
for membership in memberships:
|
||||||
member = db.query(User).filter_by(id=membership.user_id).first()
|
member = db.query(User).filter_by(id=membership.user_id).first()
|
||||||
if member:
|
if member:
|
||||||
|
# Check if current user is admin of this team
|
||||||
|
if user and user.id == membership.user_id and membership.is_admin:
|
||||||
|
is_user_admin = True
|
||||||
|
|
||||||
# Use joined_at if available, otherwise use placeholder
|
# Use joined_at if available, otherwise use placeholder
|
||||||
joined_date = getattr(membership, 'joined_at', None) or datetime.now() - timedelta(days=random.randint(30, 180))
|
joined_date = getattr(membership, 'joined_at', None) or datetime.now() - timedelta(days=random.randint(30, 180))
|
||||||
if isinstance(joined_date, datetime):
|
if isinstance(joined_date, datetime):
|
||||||
@@ -105,21 +260,39 @@ def team_detail(request: Request, team_id: int, db: Session = Depends(get_db)):
|
|||||||
"joined": f"{month_name} {year}"
|
"joined": f"{month_name} {year}"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Also check if user is the owner
|
||||||
|
is_user_owner = user and team.owner_id == user.id
|
||||||
|
if is_user_owner:
|
||||||
|
is_user_admin = True # Owner has admin privileges
|
||||||
|
|
||||||
# Get total points
|
# Get total points
|
||||||
total_points = db.query(func.sum(QRCode.points)).filter(
|
total_points = db.query(func.sum(QRCode.points)).filter(
|
||||||
QRCode.redeemed_at_team == team_id
|
QRCode.redeemed_at_team == team_id
|
||||||
).scalar() or 0
|
).scalar() or 0
|
||||||
|
|
||||||
# Calculate rank based on points
|
# Calculate rank based on points - using a safer approach
|
||||||
higher_teams = db.query(func.count(Team.id)).join(
|
try:
|
||||||
QRCode,
|
# First, get the aggregated points for all teams
|
||||||
QRCode.redeemed_at_team == Team.id,
|
team_points = db.query(
|
||||||
isouter=True
|
QRCode.redeemed_at_team,
|
||||||
).group_by(Team.id).having(
|
func.sum(QRCode.points).label('total')
|
||||||
func.sum(QRCode.points) > total_points
|
).filter(
|
||||||
).scalar() or 0
|
QRCode.redeemed_at_team != None
|
||||||
|
).group_by(QRCode.redeemed_at_team).all()
|
||||||
team_rank = higher_teams + 1
|
|
||||||
|
# Sort them by points (descending)
|
||||||
|
sorted_teams = sorted(team_points, key=lambda x: x.total or 0, reverse=True)
|
||||||
|
|
||||||
|
# Find our team's position
|
||||||
|
team_rank = 1
|
||||||
|
for idx, team_data in enumerate(sorted_teams):
|
||||||
|
if team_data.redeemed_at_team == team_id:
|
||||||
|
team_rank = idx + 1
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error calculating team rank: {e}")
|
||||||
|
team_rank = 1 # Default to 1st place on error
|
||||||
|
|
||||||
# Generate points data (with fallbacks for missing columns)
|
# Generate points data (with fallbacks for missing columns)
|
||||||
points_this_month = 65 # Default value
|
points_this_month = 65 # Default value
|
||||||
@@ -205,37 +378,10 @@ def team_detail(request: Request, team_id: int, db: Session = Depends(get_db)):
|
|||||||
"activities": activities,
|
"activities": activities,
|
||||||
"performance": performance,
|
"performance": performance,
|
||||||
"is_user_admin": is_user_admin,
|
"is_user_admin": is_user_admin,
|
||||||
|
"is_user_owner": is_user_owner,
|
||||||
|
"is_team_member": is_team_member,
|
||||||
"days_ago": days_ago,
|
"days_ago": days_ago,
|
||||||
"founded_date": founded_date_str
|
"founded_date": founded_date_str,
|
||||||
|
"user": user # Add user to the context
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.post("/{team_id}/update")
|
|
||||||
def update_team(
|
|
||||||
request: Request,
|
|
||||||
team_id: int,
|
|
||||||
team_name: str = Form(...),
|
|
||||||
is_public: bool = Form(False),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Update team details."""
|
|
||||||
team = db.query(Team).filter_by(id=team_id).first()
|
|
||||||
|
|
||||||
if not team:
|
|
||||||
raise HTTPException(status_code=404, detail="Team not found")
|
|
||||||
|
|
||||||
# Check if user is admin
|
|
||||||
membership = db.query(TeamMembership).filter_by(
|
|
||||||
team_id=team.id,
|
|
||||||
is_admin=True
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if not membership:
|
|
||||||
raise HTTPException(status_code=403, detail="You don't have permission to update this team")
|
|
||||||
|
|
||||||
# Update team details
|
|
||||||
team.name = team_name
|
|
||||||
team.is_public = is_public
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
return RedirectResponse(f"/teams/{team_id}", status_code=303)
|
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ passlib>=1.7.4
|
|||||||
itsdangerous>=2.1.2
|
itsdangerous>=2.1.2
|
||||||
bcrypt>=4.0.1
|
bcrypt>=4.0.1
|
||||||
|
|
||||||
|
# OAuth client
|
||||||
|
httpx-oauth>=0.10.0
|
||||||
|
|
||||||
# Templates and UI
|
# Templates and UI
|
||||||
jinja2>=3.1.2
|
jinja2>=3.1.2
|
||||||
aiofiles>=23.2.1
|
aiofiles>=23.2.1
|
||||||
|
|||||||
Reference in New Issue
Block a user