Build the core user-facing experience with admin login (bcrypt + signed session cookies), profile switcher, workout day viewer with warmups and exercise cards, and HTMX-powered exercise browser with search/filter. - AuthService with bcrypt password verification and itsdangerous session tokens - Auth dependency redirects to /login (303) for unauthenticated requests - NavContextMiddleware injects admin/profiles/active_profile into all templates - Profile management (list, switch, edit) with cookie-based active profile - Workout day viewer shows warmups + exercises + per-user programming targets - Exercise browser with HTMX filter dropdowns (no page reloads) - Flash message partial for success/error feedback - 12 new tests (66 total passing) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
152 lines
4.2 KiB
Python
152 lines
4.2 KiB
Python
"""Profile management routes.
|
|
|
|
Admin can view, create, edit user profiles and switch the active profile.
|
|
"""
|
|
|
|
import structlog
|
|
from fastapi import APIRouter, Depends, Request
|
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
from sqlmodel import Session
|
|
|
|
from app.database import get_db_session
|
|
from app.models.user import User
|
|
from app.services.user_service import UserService
|
|
from app.utils.auth import get_current_admin_user, get_active_profile_id
|
|
|
|
logger = structlog.get_logger(__name__)
|
|
|
|
router = APIRouter(prefix="/profiles", tags=["profiles"])
|
|
|
|
|
|
@router.get("", response_class=HTMLResponse)
|
|
async def list_profiles(
|
|
request: Request,
|
|
session: Session = Depends(get_db_session),
|
|
admin: User = Depends(get_current_admin_user),
|
|
):
|
|
"""List all user profiles for the admin.
|
|
|
|
Args:
|
|
request: The incoming HTTP request.
|
|
session: Database session.
|
|
admin: The authenticated admin user.
|
|
|
|
Returns:
|
|
Rendered profile list page.
|
|
"""
|
|
user_service = UserService(session)
|
|
profiles = user_service.list_users(exclude_admin=True)
|
|
active_profile_id = get_active_profile_id(request)
|
|
|
|
templates = request.app.state.templates
|
|
return templates.TemplateResponse("pages/profiles.html", {
|
|
"request": request,
|
|
"profiles": profiles,
|
|
"active_profile_id": active_profile_id,
|
|
"admin": admin,
|
|
})
|
|
|
|
|
|
@router.post("/switch")
|
|
async def switch_profile(
|
|
request: Request,
|
|
session: Session = Depends(get_db_session),
|
|
admin: User = Depends(get_current_admin_user),
|
|
):
|
|
"""Switch the active user profile.
|
|
|
|
Sets a cookie with the selected profile ID.
|
|
|
|
Args:
|
|
request: The incoming HTTP request.
|
|
session: Database session.
|
|
admin: The authenticated admin user.
|
|
|
|
Returns:
|
|
Redirect to the referring page with profile cookie set.
|
|
"""
|
|
form = await request.form()
|
|
profile_id = form.get("profile_id", "")
|
|
|
|
# Validate profile exists and is not an admin
|
|
user_service = UserService(session)
|
|
profile = user_service.get_user_by_id(int(profile_id)) if profile_id.isdigit() else None
|
|
|
|
referer = request.headers.get("referer", "/")
|
|
response = RedirectResponse(url=referer, status_code=303)
|
|
|
|
if profile and not profile.is_admin:
|
|
response.set_cookie(
|
|
key="active_profile_id",
|
|
value=str(profile.id),
|
|
httponly=True,
|
|
samesite="lax",
|
|
max_age=86400,
|
|
)
|
|
logger.info("profile_switched", profile_id=profile.id, name=profile.display_name)
|
|
else:
|
|
logger.warning("profile_switch_failed", profile_id=profile_id)
|
|
|
|
return response
|
|
|
|
|
|
@router.get("/{profile_id}/edit", response_class=HTMLResponse)
|
|
async def edit_profile_page(
|
|
profile_id: int,
|
|
request: Request,
|
|
session: Session = Depends(get_db_session),
|
|
admin: User = Depends(get_current_admin_user),
|
|
):
|
|
"""Render the profile edit form.
|
|
|
|
Args:
|
|
profile_id: The profile ID to edit.
|
|
request: The incoming HTTP request.
|
|
session: Database session.
|
|
admin: The authenticated admin user.
|
|
|
|
Returns:
|
|
Rendered profile edit page.
|
|
"""
|
|
user_service = UserService(session)
|
|
profile = user_service.get_user_by_id(profile_id)
|
|
|
|
templates = request.app.state.templates
|
|
return templates.TemplateResponse("pages/profile_edit.html", {
|
|
"request": request,
|
|
"profile": profile,
|
|
"admin": admin,
|
|
})
|
|
|
|
|
|
@router.post("/{profile_id}/edit")
|
|
async def update_profile(
|
|
profile_id: int,
|
|
request: Request,
|
|
session: Session = Depends(get_db_session),
|
|
admin: User = Depends(get_current_admin_user),
|
|
):
|
|
"""Process profile edit form submission.
|
|
|
|
Args:
|
|
profile_id: The profile ID to update.
|
|
request: The incoming HTTP request.
|
|
session: Database session.
|
|
admin: The authenticated admin user.
|
|
|
|
Returns:
|
|
Redirect to profiles page.
|
|
"""
|
|
form = await request.form()
|
|
user_service = UserService(session)
|
|
|
|
user_service.update_user(
|
|
profile_id,
|
|
display_name=form.get("display_name", ""),
|
|
height=form.get("height", ""),
|
|
weight=form.get("weight", ""),
|
|
goals=form.get("goals", ""),
|
|
)
|
|
|
|
return RedirectResponse(url="/profiles", status_code=303)
|