Files
SneakySwole/app/routes/profiles.py
Phillip Tarrant 576d3bbb68 feat: replace admin auth with cookie-based profile picker
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>
2026-03-13 12:40:54 -05:00

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)