from pathlib import Path from fastapi import APIRouter, Form, HTTPException, Request, status from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates from app.auth.admin import password_matches, require_admin from app.auth.csrf import ensure_csrf_token, validate_csrf from app.db.models import SyncUser from app.db.repositories import UserRepository from app.security.credentials import CredentialCipher from app.web.forms import UserFormData router = APIRouter() templates = Jinja2Templates(directory=str(Path(__file__).resolve().parent / "templates")) def _get_user_or_404(repository: UserRepository, user_id: int) -> SyncUser: user = repository.get(user_id) if user is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") return user def _cipher(request: Request) -> CredentialCipher: return CredentialCipher(request.app.state.settings.credential_encryption_key) def _require_non_empty(value: str, field_name: str) -> None: if not value.strip(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"{field_name} must not be empty", ) @router.get("/login", response_class=HTMLResponse) def login_page(request: Request): return templates.TemplateResponse(request, "login.html", {"csrf_token": ensure_csrf_token(request)}) @router.post("/login") def login( request: Request, password: str = Form(...), csrf_token: str = Form(...), ): validate_csrf(request, csrf_token) settings = request.app.state.settings if not password_matches(password, settings.admin_password): return templates.TemplateResponse( request, "login.html", {"csrf_token": ensure_csrf_token(request), "error": "Invalid password"}, status_code=401, ) request.session["admin_authenticated"] = True return RedirectResponse("/", status_code=303) @router.get("/", response_class=HTMLResponse) def dashboard(request: Request): require_admin(request) with request.app.state.session_factory() as session: users = UserRepository(session).list_all() return templates.TemplateResponse( request, "dashboard.html", {"users": users, "csrf_token": ensure_csrf_token(request)}, ) @router.get("/users/new", response_class=HTMLResponse) def new_user_page(request: Request): require_admin(request) return templates.TemplateResponse( request, "users/form.html", { "csrf_token": ensure_csrf_token(request), "user": None, "form_action": "/users", "mywhoosh_email": "", "garmin_email": "", }, ) @router.post("/users") def create_user( request: Request, csrf_token: str = Form(...), name: str = Form(...), mywhoosh_email: str = Form(""), mywhoosh_password: str = Form(""), garmin_email: str = Form(""), garmin_password: str = Form(""), enabled: str | None = Form(None), ): require_admin(request) validate_csrf(request, csrf_token) _require_non_empty(mywhoosh_email, "mywhoosh_email") _require_non_empty(mywhoosh_password, "mywhoosh_password") _require_non_empty(garmin_email, "garmin_email") _require_non_empty(garmin_password, "garmin_password") form = UserFormData( name=name, mywhoosh_email=mywhoosh_email, mywhoosh_password=mywhoosh_password, garmin_email=garmin_email, garmin_password=garmin_password, enabled=enabled is not None, ) cipher = _cipher(request) with request.app.state.session_factory() as session: repository = UserRepository(session) user = repository.create( name=form.name.strip(), enabled=form.enabled, mywhoosh_email_enc=cipher.encrypt(form.mywhoosh_email.strip()), mywhoosh_password_enc=cipher.encrypt(form.mywhoosh_password), garmin_email_enc=cipher.encrypt(form.garmin_email.strip()), garmin_password_enc=cipher.encrypt(form.garmin_password), ) user_id = user.id return RedirectResponse(f"/users/{user_id}", status_code=303) @router.get("/users/{user_id}", response_class=HTMLResponse) def user_detail(request: Request, user_id: int): require_admin(request) with request.app.state.session_factory() as session: user = _get_user_or_404(UserRepository(session), user_id) return templates.TemplateResponse( request, "users/detail.html", { "csrf_token": ensure_csrf_token(request), "user": user, }, ) @router.get("/users/{user_id}/edit", response_class=HTMLResponse) def edit_user_page(request: Request, user_id: int): require_admin(request) cipher = _cipher(request) with request.app.state.session_factory() as session: user = _get_user_or_404(UserRepository(session), user_id) return templates.TemplateResponse( request, "users/form.html", { "csrf_token": ensure_csrf_token(request), "user": user, "form_action": f"/users/{user_id}", "mywhoosh_email": cipher.decrypt(user.mywhoosh_email_enc), "garmin_email": cipher.decrypt(user.garmin_email_enc), }, ) @router.post("/users/{user_id}") def update_user( request: Request, user_id: int, csrf_token: str = Form(...), name: str = Form(...), mywhoosh_email: str = Form(""), mywhoosh_password: str = Form(""), garmin_email: str = Form(""), garmin_password: str = Form(""), enabled: str | None = Form(None), ): require_admin(request) validate_csrf(request, csrf_token) _require_non_empty(mywhoosh_email, "mywhoosh_email") _require_non_empty(garmin_email, "garmin_email") form = UserFormData( name=name, mywhoosh_email=mywhoosh_email, mywhoosh_password=mywhoosh_password, garmin_email=garmin_email, garmin_password=garmin_password, enabled=enabled is not None, ) cipher = _cipher(request) with request.app.state.session_factory() as session: repository = UserRepository(session) user = _get_user_or_404(repository, user_id) values = { "name": form.name.strip(), "enabled": form.enabled, "mywhoosh_email_enc": cipher.encrypt(form.mywhoosh_email.strip()), "garmin_email_enc": cipher.encrypt(form.garmin_email.strip()), } if form.mywhoosh_password: values["mywhoosh_password_enc"] = cipher.encrypt(form.mywhoosh_password) if form.garmin_password: values["garmin_password_enc"] = cipher.encrypt(form.garmin_password) repository.update(user, **values) return RedirectResponse(f"/users/{user_id}", status_code=303)