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

@@ -4,8 +4,6 @@ Creates and configures the FastAPI app with routes, templates,
static files, and structured logging.
"""
import os
import secrets
from pathlib import Path
import structlog
@@ -23,7 +21,6 @@ from app.models.user import User
from app.config import get_settings
from app.database import get_engine, get_db_session
from app.logging_config import setup_logging
from app.routes.auth import router as auth_router
from app.routes.exercises import router as exercises_router
from app.routes.history import router as history_router
from app.routes.health import router as health_router
@@ -34,9 +31,8 @@ from app.routes.workouts import router as workouts_router
from app.routes.dashboard import router as dashboard_router
from app.routes.schedule import router as schedule_router
from app.services.seed_service import SeedService
from app.services.auth_service import AuthService
from app.services.user_service import UserService
from app.utils.auth import SESSION_COOKIE_NAME, NotAuthenticatedError
from app.utils.auth import NoProfileSelectedError
logger = structlog.get_logger(__name__)
@@ -47,30 +43,21 @@ STATIC_DIR = _BASE_DIR / "static"
class NavContextMiddleware(BaseHTTPMiddleware):
"""Injects admin, profiles, and active_profile into request.state for templates."""
"""Injects profiles and active_profile into request.state for templates."""
async def dispatch(self, request: StarletteRequest, call_next: RequestResponseEndpoint) -> Response:
request.state.admin = None
request.state.profiles = []
request.state.active_profile = None
token = request.cookies.get(SESSION_COOKIE_NAME)
if token and hasattr(request.app.state, "engine"):
if hasattr(request.app.state, "engine"):
try:
with Session(request.app.state.engine) as session:
secret_key = getattr(request.app.state, "secret_key", "")
auth_service = AuthService(session, secret_key=secret_key)
user_id = auth_service.validate_session_token(token)
if user_id:
admin = session.get(User, user_id)
if admin and admin.is_admin:
request.state.admin = admin
user_service = UserService(session)
request.state.profiles = user_service.list_users(exclude_admin=True)
user_service = UserService(session)
request.state.profiles = user_service.list_users()
profile_id = request.cookies.get("active_profile_id")
if profile_id and profile_id.isdigit():
request.state.active_profile = user_service.get_user_by_id(int(profile_id))
profile_id = request.cookies.get("active_profile_id")
if profile_id and profile_id.isdigit():
request.state.active_profile = user_service.get_user_by_id(int(profile_id))
except Exception:
pass
@@ -92,10 +79,10 @@ def create_app() -> FastAPI:
version="0.1.0",
)
# Redirect unauthenticated requests to login
@app.exception_handler(NotAuthenticatedError)
async def _not_authenticated_handler(request, exc):
return RedirectResponse(url="/login", status_code=302)
# Redirect to home when no profile is selected
@app.exception_handler(NoProfileSelectedError)
async def _no_profile_handler(request, exc):
return RedirectResponse(url="/", status_code=302)
# Mount static files
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
@@ -104,14 +91,10 @@ def create_app() -> FastAPI:
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
app.state.templates = templates
# Secret key for session signing
app.state.secret_key = os.environ.get("SECRET_KEY", secrets.token_hex(32))
# Nav context middleware — injects admin/profiles/active_profile into request.state
# Nav context middleware — injects profiles/active_profile into request.state
app.add_middleware(NavContextMiddleware)
# Register route modules
app.include_router(auth_router)
app.include_router(exercises_router)
app.include_router(health_router)
app.include_router(history_router)