import time from fastapi import APIRouter, Depends, Form, Request from .. import audit, totp from ..users import User from .deps import (FLASH_ERROR, SESSION_UID, SESSION_VERSION, current_user, flash, redirect, render) router = APIRouter() PENDING_UID = "pending_uid" PENDING_TS = "pending_ts" PENDING_TTL_SECONDS = 300 STATUS_UNAUTHORIZED = 401 STATUS_TOO_MANY = 429 MSG_LOGIN_FAILED = "Anmeldung fehlgeschlagen." MSG_BLOCKED = "Zu viele Fehlversuche. Bitte später erneut versuchen." MSG_TOTP_INVALID = "Code ungültig." AUDIT_TARGET_MAX = 64 def _complete_login(request: Request, user: User): request.session.clear() request.session[SESSION_UID] = user.id request.session[SESSION_VERSION] = user.session_version audit.record(request.app.state.db, user.username, "login", user.username) return redirect("/") def _pending_uid(request: Request) -> int | None: uid = request.session.get(PENDING_UID) started = request.session.get(PENDING_TS, 0) if not uid or time.time() - started > PENDING_TTL_SECONDS: return None return uid @router.get("/login") def login_form(request: Request): return render(request, "login.html") @router.post("/login") def login(request: Request, username: str = Form(""), password: str = Form("")): state = request.app.state key = username.lower() if state.limiter.blocked(key): flash(request, MSG_BLOCKED, FLASH_ERROR) return render(request, "login.html", status_code=STATUS_TOO_MANY) user = state.users.authenticate(username, password) if user is None: state.limiter.record(key) audit.record(state.db, username[:AUDIT_TARGET_MAX], "login.failed", username[:AUDIT_TARGET_MAX]) flash(request, MSG_LOGIN_FAILED, FLASH_ERROR) return render(request, "login.html", status_code=STATUS_UNAUTHORIZED) state.limiter.reset(key) if user.has_totp: request.session.clear() request.session[PENDING_UID] = user.id request.session[PENDING_TS] = time.time() return redirect("/login/totp") return _complete_login(request, user) @router.get("/login/totp") def totp_form(request: Request): if _pending_uid(request) is None: return redirect("/login") return render(request, "login_totp.html") @router.post("/login/totp") def totp_submit(request: Request, code: str = Form("")): uid = _pending_uid(request) if uid is None: return redirect("/login") state = request.app.state key = f"totp:{uid}" if state.limiter.blocked(key): request.session.clear() flash(request, MSG_BLOCKED, FLASH_ERROR) return render(request, "login.html", status_code=STATUS_TOO_MANY) user = state.users.get(uid) secret = state.users.totp_secret(uid) if user is None or user.disabled or not secret or not totp.verify(secret, code): state.limiter.record(key) flash(request, MSG_TOTP_INVALID, FLASH_ERROR) return render(request, "login_totp.html", status_code=STATUS_UNAUTHORIZED) state.limiter.reset(key) return _complete_login(request, user) @router.post("/logout") def logout(request: Request, user: User = Depends(current_user)): audit.record(request.app.state.db, user.username, "logout", user.username) request.session.clear() return redirect("/login")