fix: typer exit codes, JWT 7d expiry, shared test fixtures, AdminTab query, mantine 9.6.3

This commit is contained in:
Jonas Miederer 2026-09-30 22:27:58 +02:00
parent 03af41e837
commit 696db34c90
16 changed files with 210 additions and 251 deletions

View file

@ -36,11 +36,3 @@ def require_admin(user: Annotated[User, Depends(get_current_user)]) -> User:
if user.role != "admin":
raise HTTPException(status_code=403, detail="admin only")
return user
def resolve_tenant(user: User | None, explicit: str | None, default: str = "default") -> str:
"""Tenant comes from the session; the body/query param is a deprecated
fallback for unauthenticated callers."""
if user is not None:
return user.tenant_id
return explicit or default

View file

@ -4,9 +4,10 @@ Keep API handlers thin: all persistence lives here. Tokens and session
JWTs are bearer secrets — log user/request ids only, never secret values.
Sessions are stateless HS256 JWTs (sub=user.id, email, role, tenant_id,
iat, exp=30d, jti) in the HttpOnly session cookie. No session table:
logout clears the cookie; role/tenant changes apply at next login (or
immediately for deleted users — the user row is reloaded per request).
iat, exp=7d, jti) in the HttpOnly session cookie. No session table and no
revocation list: logout clears the cookie client-side, and a stolen token
is valid until its short expiry. Role/tenant changes apply at next login
(deleted users fail closed — the user row is reloaded per request).
"""
from __future__ import annotations
@ -28,7 +29,10 @@ from app.settings import settings
logger = logging.getLogger(__name__)
MAGIC_LINK_TTL = timedelta(minutes=15)
SESSION_TTL = timedelta(days=30)
# Session window is deliberately short: sessions are stateless JWTs with
# no server-side revocation (logout only clears the cookie), so a stolen
# token stays valid until expiry. 7 days bounds that window.
SESSION_TTL = timedelta(days=7)
# Public request endpoint: at most this many requests per email per hour.
REQUESTS_PER_EMAIL_PER_HOUR = 5

View file

@ -36,6 +36,10 @@ def _blocking_startup_sync() -> None:
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
task: asyncio.Task | None = None
stop: asyncio.Event | None = None
if not settings.jwt_secret:
# Fail-closed sessions need a signing secret; without one every
# login 500s. Warn loudly instead of crashing the whole API.
logger.error("JWT_SECRET is empty — logins will fail until it is set")
if settings.flows_watch_enabled:
await asyncio.to_thread(_blocking_startup_sync)
stop = asyncio.Event()