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:
43
app/main.py
43
app/main.py
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user