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>
This commit is contained in:
2026-03-13 12:40:54 -05:00
parent 3dc0171639
commit 576d3bbb68
44 changed files with 523 additions and 1024 deletions

View File

@@ -18,7 +18,7 @@ from app.services.exercise_service import ExerciseService
from app.services.log_service import LogService
from app.services.progression_service import ProgressionService
from app.services.workout_session_service import WorkoutSessionService
from app.utils.auth import get_current_admin_user, get_active_profile_id
from app.utils.auth import require_active_profile, get_active_profile_id
logger = structlog.get_logger(__name__)
@@ -29,18 +29,9 @@ router = APIRouter(prefix="/workouts", tags=["workouts"])
async def workout_days_list(
request: Request,
session: Session = Depends(get_db_session),
admin: User = Depends(get_current_admin_user),
profile: User = Depends(require_active_profile),
):
"""List all workout days as clickable cards.
Args:
request: The incoming HTTP request.
session: Database session.
admin: The authenticated admin user.
Returns:
Rendered workout days list page.
"""
"""List all workout days as clickable cards."""
exercise_service = ExerciseService(session)
days = exercise_service.list_workout_days()
@@ -48,7 +39,6 @@ async def workout_days_list(
return templates.TemplateResponse("pages/workout_days.html", {
"request": request,
"days": days,
"admin": admin,
})
@@ -57,19 +47,9 @@ async def workout_day_detail(
day_name: str,
request: Request,
session: Session = Depends(get_db_session),
admin: User = Depends(get_current_admin_user),
profile: User = Depends(require_active_profile),
):
"""Display a full workout day -- warmups + exercises with form cues.
Args:
day_name: The workout day name (e.g., "push", "pull").
request: The incoming HTTP request.
session: Database session.
admin: The authenticated admin user.
Returns:
Rendered workout day detail page.
"""
"""Display a full workout day -- warmups + exercises with form cues."""
exercise_service = ExerciseService(session)
# Normalize day name for DB lookup (e.g., "push" -> "Push", "full-body" -> "Full Body")
@@ -78,19 +58,15 @@ async def workout_day_detail(
warmups = exercise_service.list_warmups()
exercises = exercise_service.list_exercises(workout_day=day_display)
# Get active profile's programming if set
active_profile_id = get_active_profile_id(request)
# Get active profile's programming
active_profile_id = profile.id
programs = {}
active_profile = None
existing_logs = {}
if active_profile_id:
active_profile = session.get(User, active_profile_id)
if active_profile:
statement = select(UserExerciseProgram).where(
UserExerciseProgram.user_id == active_profile_id
)
for prog in session.exec(statement).all():
programs[prog.exercise_id] = prog
statement = select(UserExerciseProgram).where(
UserExerciseProgram.user_id == active_profile_id
)
for prog in session.exec(statement).all():
programs[prog.exercise_id] = prog
# Look up the workout day ID for logging forms
days = exercise_service.list_workout_days()
@@ -102,15 +78,14 @@ async def workout_day_detail(
# Get progression suggestions for each exercise
suggestions = {}
if active_profile_id:
progression = ProgressionService(session)
for exercise in exercises:
suggestions[exercise.id] = progression.get_suggestion(
active_profile_id, exercise.id,
)
progression = ProgressionService(session)
for exercise in exercises:
suggestions[exercise.id] = progression.get_suggestion(
active_profile_id, exercise.id,
)
# Load existing logs for today's session (if any)
if active_profile_id and workout_day_id:
if workout_day_id:
ws_service = WorkoutSessionService(session)
ws = ws_service.get_or_create_session(
user_id=active_profile_id,
@@ -129,9 +104,8 @@ async def workout_day_detail(
"warmups": warmups,
"exercises": exercises,
"programs": programs,
"active_profile": active_profile,
"active_profile": profile,
"existing_logs": existing_logs,
"suggestions": suggestions,
"workout_day_id": workout_day_id,
"admin": admin,
})