"""Auth dependencies for admin routes. Two ``Depends(...)`` helpers: - :func:`get_current_user` — returns a :class:`User` or ``None`` based on the signed ``cb_session`` cookie. Never raises. - :func:`require_admin` — same lookup but raises an HTTP 303 redirect to ``/admin/login`` if no user is authenticated. Used by every route that must be logged-in. Cookie handling --------------- The cookie is read via ``request.cookies`` (Starlette strips secure / httponly flags off by the time the app sees it; they only affect how the browser stores and presents the cookie). Unsigning, hashing, and DB lookup are delegated to :class:`app.services.sessions.SessionService`. """ from __future__ import annotations from typing import Optional from fastapi import Depends, HTTPException, Request from sqlalchemy import text from app.models.entities import User from app.models.mappers import row_to_user from app.services.sessions import COOKIE_NAME, SessionService def _get_session_service(request: Request) -> SessionService: """Return the app-scoped :class:`SessionService` for DI. Private to this module — routes and other dependencies resolve the service via :func:`get_current_user` / :func:`require_admin` rather than reaching across the dependency graph. """ return request.app.state.session_service def get_current_user( request: Request, sessions: SessionService = Depends(_get_session_service), ) -> Optional[User]: """Return the authenticated :class:`User` or ``None``. Never raises. A malformed / expired / revoked cookie simply resolves to ``None`` so that un-authed viewers can hit admin login pages without tripping an exception handler. """ cookie_value = request.cookies.get(COOKIE_NAME) session = sessions.lookup(cookie_value) if session is None: return None # We need the user row to render "Welcome, " on the # admin index. Query directly here instead of adding a full # UserService for one call site. engine = request.app.state.engine with engine.connect() as conn: row = conn.execute( text( "SELECT id, email, display_name, created_at," " last_login_at, active" " FROM users WHERE id = :id AND active = 1 LIMIT 1" ), {"id": session.user_id}, ).mappings().first() if row is None: return None return row_to_user(row) def require_admin( user: Optional[User] = Depends(get_current_user), ) -> User: """Return the authenticated user or redirect to login. Uses a 303 "See Other" so the browser switches to GET on the followup request — correct behavior for both initial page loads and the post-consume redirect chain. """ if user is None: raise HTTPException( status_code=303, headers={"Location": "/admin/login"}, ) return user