app.py was ~1300 lines with every route, helper, and middleware mixed
together. Split into:
- app.py (~100 lines): FastAPI bootstrap, lifespan, /health, security
headers, Jinja filter registration, include_router calls
- common.py: shared helpers (templates, apply_client, base_context,
_is_htmx, client_ip, require_internal, time helpers, filter helpers,
apply-gate helpers, _kick_apply / _finish_apply_background,
_bg_tasks, _spawn, _mask_secret, _has_running_application, BERLIN_TZ)
- routes/auth.py: /login (GET+POST), /logout
- routes/wohnungen.py: /, /partials/wohnungen, /partials/wohnung/{id},
/flat-images/{slug}/{idx}, /actions/apply|reject|unreject|auto-apply|
submit-forms|reset-circuit|filters|enrich-all|enrich-flat; owns
_wohnungen_context + _wohnungen_partial_or_redirect
- routes/bewerbungen.py: /bewerbungen, /bewerbungen/{id}/report.zip
- routes/einstellungen.py: /einstellungen, /einstellungen/{section},
/actions/profile|notifications|account/password|partner/*; owns
VALID_SECTIONS
- routes/admin.py: /logs redirect, /admin, /admin/{section},
/logs/export.csv, /actions/users/*|secrets; owns ADMIN_SECTIONS,
_parse_date_range, _collect_events
- routes/internal.py: /internal/flats|heartbeat|error|secrets
Route-diff before/after is empty — all 41 routes + /static mount
preserved. No behavior changes, pure mechanical split.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
59 lines
2.1 KiB
Python
59 lines
2.1 KiB
Python
"""Authentication routes: login page, login submit, logout."""
|
|
from fastapi import APIRouter, Form, Request, status
|
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
|
|
import db
|
|
from auth import (
|
|
clear_session_cookie,
|
|
current_user,
|
|
issue_session_cookie,
|
|
rate_limit_login,
|
|
verify_login,
|
|
)
|
|
from common import client_ip, templates
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/login", response_class=HTMLResponse)
|
|
def login_form(request: Request, error: str | None = None):
|
|
if current_user(request):
|
|
return RedirectResponse("/", status_code=303)
|
|
return templates.TemplateResponse("login.html", {"request": request, "error": error})
|
|
|
|
|
|
@router.post("/login")
|
|
def login_submit(request: Request, username: str = Form(...), password: str = Form(...)):
|
|
ip = client_ip(request)
|
|
if not rate_limit_login(ip):
|
|
db.log_audit(username or "?", "login_rate_limited", ip=ip)
|
|
db.log_error(source="web", kind="rate_limit", summary=f"login throttled for {ip}",
|
|
context={"username": username or ""})
|
|
return templates.TemplateResponse(
|
|
"login.html",
|
|
{"request": request, "error": "Zu viele Versuche. Bitte später erneut."},
|
|
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
|
)
|
|
user = verify_login(username, password)
|
|
if not user:
|
|
db.log_audit(username or "?", "login_failed", ip=ip)
|
|
return templates.TemplateResponse(
|
|
"login.html",
|
|
{"request": request, "error": "Login fehlgeschlagen."},
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
)
|
|
response = RedirectResponse("/", status_code=303)
|
|
issue_session_cookie(response, user["id"])
|
|
db.log_audit(user["username"], "login_success", user_id=user["id"], ip=ip)
|
|
return response
|
|
|
|
|
|
@router.post("/logout")
|
|
def logout(request: Request):
|
|
u = current_user(request)
|
|
response = RedirectResponse("/login", status_code=303)
|
|
clear_session_cookie(response)
|
|
if u:
|
|
db.log_audit(u["username"], "logout", user_id=u["id"], ip=client_ip(request))
|
|
return response
|