Files
mywhoosh2garmin/app/web/routes.py
Bastian Wagner d2ac079870 fix: reject empty credential fields with 400 instead of 500
Add an explicit non-empty check before encrypting user-submitted
email/password fields in the create and update user routes, so a
request that bypasses the HTML `required` attribute gets a clean
400 instead of an unhandled ValueError from CredentialCipher.encrypt
propagating as a 500. Applies to all four credential fields on
create, and to the two email fields on update (the password-blank-
means-keep-existing behavior on update is unchanged).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 09:50:05 +02:00

201 lines
6.8 KiB
Python

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="app/web/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)