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

@@ -15,7 +15,7 @@ from app.database import get_db_session
from app.models.user import User
from app.services.log_service import LogService
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__)
@@ -26,20 +26,12 @@ router = APIRouter(prefix="/log", tags=["logging"])
async def log_set(
request: Request,
session: Session = Depends(get_db_session),
admin: User = Depends(get_current_admin_user),
profile: User = Depends(require_active_profile),
):
"""Log a single set for an exercise.
Creates the workout session if it doesn't exist yet (auto-create).
Returns the updated log entries partial for this exercise.
Args:
request: The incoming HTTP request.
session: Database session.
admin: The authenticated admin user.
Returns:
Rendered log entries partial for this exercise.
"""
form = await request.form()
exercise_id = int(form.get("exercise_id", 0))
@@ -49,18 +41,10 @@ async def log_set(
weight = form.get("weight", "")
felt_easy = form.get("felt_easy") == "on"
active_profile_id = get_active_profile_id(request)
if not active_profile_id:
templates = request.app.state.templates
return templates.TemplateResponse("partials/flash_message.html", {
"request": request,
"flash_error": "No profile selected. Switch profiles first.",
})
# Get or create today's session
ws_service = WorkoutSessionService(session)
ws = ws_service.get_or_create_session(
user_id=active_profile_id,
user_id=profile.id,
workout_day_id=workout_day_id,
session_date=date.today(),
)
@@ -96,19 +80,9 @@ async def edit_log(
log_id: int,
request: Request,
session: Session = Depends(get_db_session),
admin: User = Depends(get_current_admin_user),
profile: User = Depends(require_active_profile),
):
"""Edit an existing log entry.
Args:
log_id: The log entry ID.
request: The incoming HTTP request.
session: Database session.
admin: The authenticated admin user.
Returns:
Rendered updated log entry partial.
"""
"""Edit an existing log entry."""
form = await request.form()
log_service = LogService(session)
@@ -140,19 +114,9 @@ async def delete_log(
log_id: int,
request: Request,
session: Session = Depends(get_db_session),
admin: User = Depends(get_current_admin_user),
profile: User = Depends(require_active_profile),
):
"""Delete a log entry.
Args:
log_id: The log entry ID.
request: The incoming HTTP request.
session: Database session.
admin: The authenticated admin user.
Returns:
Rendered updated log entries partial.
"""
"""Delete a log entry."""
log_service = LogService(session)
log = log_service.get_log_by_id(log_id)