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:
@@ -1,69 +1,27 @@
|
||||
"""Authentication dependencies for FastAPI route protection.
|
||||
"""Profile selection utilities for FastAPI route protection.
|
||||
|
||||
Provides dependency functions that verify the admin session cookie
|
||||
and return the authenticated User, or redirect to /login.
|
||||
Provides dependency functions that check the active_profile_id cookie
|
||||
and return the selected User profile, or redirect to /.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import structlog
|
||||
from fastapi import Depends, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
from sqlmodel import Session
|
||||
|
||||
from app.database import get_db_session
|
||||
from app.models.user import User
|
||||
from app.services.auth_service import AuthService
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class NotAuthenticatedError(Exception):
|
||||
"""Raised when a request lacks valid authentication."""
|
||||
|
||||
# Cookie name for the admin session
|
||||
SESSION_COOKIE_NAME = "session"
|
||||
|
||||
|
||||
def get_current_admin_user(request: Request, session: Session = Depends(get_db_session)) -> User:
|
||||
"""FastAPI dependency that extracts and validates the admin session.
|
||||
|
||||
Reads the session cookie, validates the token, and returns the
|
||||
authenticated admin User. Redirects to /login (303) if not authenticated.
|
||||
|
||||
Args:
|
||||
request: The incoming HTTP request.
|
||||
session: Database session (injected by FastAPI).
|
||||
|
||||
Returns:
|
||||
The authenticated admin User.
|
||||
|
||||
Raises:
|
||||
RedirectResponse: 303 redirect to /login if no valid admin session.
|
||||
"""
|
||||
token = request.cookies.get(SESSION_COOKIE_NAME)
|
||||
if not token:
|
||||
raise _login_redirect()
|
||||
|
||||
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 is None:
|
||||
raise _login_redirect()
|
||||
|
||||
user = session.get(User, user_id)
|
||||
if user is None or not user.is_admin:
|
||||
raise _login_redirect()
|
||||
|
||||
return user
|
||||
class NoProfileSelectedError(Exception):
|
||||
"""Raised when a request lacks a valid profile selection."""
|
||||
|
||||
|
||||
def get_active_profile_id(request: Request) -> Optional[int]:
|
||||
"""Extract the active profile ID from the session cookie.
|
||||
|
||||
The admin selects which user profile to view/log as. This is stored
|
||||
in a separate cookie called 'active_profile_id'.
|
||||
"""Extract the active profile ID from the cookie.
|
||||
|
||||
Args:
|
||||
request: The incoming HTTP request.
|
||||
@@ -77,11 +35,28 @@ def get_active_profile_id(request: Request) -> Optional[int]:
|
||||
return None
|
||||
|
||||
|
||||
def _login_redirect():
|
||||
"""Create a redirect exception to the login page.
|
||||
def require_active_profile(request: Request, session: Session = Depends(get_db_session)) -> User:
|
||||
"""FastAPI dependency that requires a valid profile selection.
|
||||
|
||||
Reads the active_profile_id cookie, loads the profile from DB,
|
||||
and raises NoProfileSelectedError if missing or invalid.
|
||||
|
||||
Args:
|
||||
request: The incoming HTTP request.
|
||||
session: Database session (injected by FastAPI).
|
||||
|
||||
Returns:
|
||||
A NotAuthenticatedError handled by a registered exception handler
|
||||
in main.py that sends a 302 redirect to /login.
|
||||
The selected User profile.
|
||||
|
||||
Raises:
|
||||
NoProfileSelectedError: If no valid profile is selected.
|
||||
"""
|
||||
return NotAuthenticatedError()
|
||||
profile_id = get_active_profile_id(request)
|
||||
if profile_id is None:
|
||||
raise NoProfileSelectedError()
|
||||
|
||||
user = session.get(User, profile_id)
|
||||
if user is None:
|
||||
raise NoProfileSelectedError()
|
||||
|
||||
return user
|
||||
|
||||
Reference in New Issue
Block a user