Remove all authentication (login, sessions, bcrypt, itsdangerous) since the app runs on a private homelab LAN. Replace with a profile picker landing page and cookie-based profile selection (1-year expiry). - Add Alembic migration to drop password_hash/is_admin columns - Delete auth service, auth routes, login template, and auth tests - Rewrite app/utils/auth.py with NoProfileSelectedError and require_active_profile dependency - Add profile creation flow (GET/POST /profiles/create) - Rewrite home page as profile picker with card layout - Update all route files to use profile dependency instead of admin auth - Remove bcrypt and itsdangerous from requirements - Remove admin_username/admin_password from config - Update all tests for new profile-based access model Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
169 lines
5.1 KiB
Python
169 lines
5.1 KiB
Python
"""Profile management routes.
|
|
|
|
Users can view, create, edit 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 require_active_profile, 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),
|
|
profile: User = Depends(require_active_profile),
|
|
):
|
|
"""List all user profiles."""
|
|
user_service = UserService(session)
|
|
profiles = user_service.list_users()
|
|
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,
|
|
})
|
|
|
|
|
|
@router.post("/switch")
|
|
async def switch_profile(
|
|
request: Request,
|
|
session: Session = Depends(get_db_session),
|
|
):
|
|
"""Switch the active user profile.
|
|
|
|
Sets a cookie with the selected profile ID (1 year expiry).
|
|
"""
|
|
form = await request.form()
|
|
profile_id = form.get("profile_id", "")
|
|
|
|
user_service = UserService(session)
|
|
profile = user_service.get_user_by_id(int(profile_id)) if profile_id.isdigit() else None
|
|
|
|
response = RedirectResponse(url="/workouts", status_code=303)
|
|
|
|
if profile:
|
|
response.set_cookie(
|
|
key="active_profile_id",
|
|
value=str(profile.id),
|
|
httponly=True,
|
|
samesite="lax",
|
|
max_age=31536000, # 1 year
|
|
)
|
|
logger.info("profile_switched", profile_id=profile.id, name=profile.display_name)
|
|
else:
|
|
logger.warning("profile_switch_failed", profile_id=profile_id)
|
|
response = RedirectResponse(url="/", status_code=303)
|
|
|
|
return response
|
|
|
|
|
|
@router.get("/create", response_class=HTMLResponse)
|
|
async def create_profile_form(request: Request):
|
|
"""Render the create-profile form."""
|
|
templates = request.app.state.templates
|
|
return templates.TemplateResponse("pages/profile_create.html", {
|
|
"request": request,
|
|
})
|
|
|
|
|
|
@router.post("/create")
|
|
async def create_profile(
|
|
request: Request,
|
|
session: Session = Depends(get_db_session),
|
|
):
|
|
"""Create a new profile, set cookie, redirect to workouts."""
|
|
form = await request.form()
|
|
display_name = form.get("display_name", "").strip()
|
|
|
|
if not display_name:
|
|
templates = request.app.state.templates
|
|
return templates.TemplateResponse("pages/profile_create.html", {
|
|
"request": request,
|
|
"error": "Display name is required.",
|
|
})
|
|
|
|
user_service = UserService(session)
|
|
# Generate username from display name
|
|
username = display_name.lower().replace(" ", "_")
|
|
|
|
# Ensure uniqueness
|
|
existing = user_service.get_user_by_username(username)
|
|
if existing:
|
|
templates = request.app.state.templates
|
|
return templates.TemplateResponse("pages/profile_create.html", {
|
|
"request": request,
|
|
"error": "A profile with that name already exists.",
|
|
})
|
|
|
|
profile = user_service.create_user(
|
|
username=username,
|
|
display_name=display_name,
|
|
height=form.get("height", "").strip() or None,
|
|
weight=form.get("weight", "").strip() or None,
|
|
goals=form.get("goals", "").strip() or None,
|
|
)
|
|
|
|
response = RedirectResponse(url="/workouts", status_code=303)
|
|
response.set_cookie(
|
|
key="active_profile_id",
|
|
value=str(profile.id),
|
|
httponly=True,
|
|
samesite="lax",
|
|
max_age=31536000, # 1 year
|
|
)
|
|
logger.info("profile_created", profile_id=profile.id, name=profile.display_name)
|
|
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),
|
|
active_profile: User = Depends(require_active_profile),
|
|
):
|
|
"""Render the profile edit form."""
|
|
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,
|
|
})
|
|
|
|
|
|
@router.post("/{profile_id}/edit")
|
|
async def update_profile(
|
|
profile_id: int,
|
|
request: Request,
|
|
session: Session = Depends(get_db_session),
|
|
active_profile: User = Depends(require_active_profile),
|
|
):
|
|
"""Process profile edit form submission."""
|
|
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)
|