Bound credentials, brake password changes, cap bug reports, page the user list, structure every error (B-94, SEC-54, SEC-55, API-09, API-15, API-16, API-19, API-21, API-28, API-33, REG-12, AUD-42)

- login and password-change fields: at most 256 characters
- wrong current password: per-user brake (5 free, 2s doubling to 300s), 429 even for the
  right password while locked; password_changed audited in the same transaction
- POST /bug-reports: 10 per IP per hour, consumed only by a stored report
- GET /admin/users: limit/after_id paging, the panel walks the pages
- 401/403/404/405/422/503 all answer {code, message, params}; the 422 omits the input
- static files revalidated on every use (Cache-Control: no-cache)
- error.* keys for the new codes in all seven languages

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-10-18 00:59:25 +02:00
1 parent c1b192f6d1
commit 24e5fc3d8b
23 files changed
+374 -111

No files matched your search

+12 -1
View File
@@ -1,10 +1,12 @@
from fastapi import APIRouter, Depends, status
from fastapi import APIRouter, Depends, Request, status
from pydantic import BaseModel, Field, field_validator
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.client_ip import client_ip
from app.api.timeutil import isoformat_utc
from app.auth.dependencies import get_current_user, get_optional_user
from app.auth.routes import _rate_limited_error, _rate_limiters
from app.db.models import BugReport, User
from app.db.session import get_session
@@ -39,9 +41,17 @@ class BugReportResponse(BaseModel):
@router.post("", response_model=BugReportResponse, status_code=status.HTTP_201_CREATED)
async def create_bug_report(
body: BugReportCreate,
request: Request,
user: User | None = Depends(get_optional_user),
session: AsyncSession = Depends(get_session),
) -> BugReportResponse:
# B-94 (SEC-55): a quota per IP, checked before the insert and consumed only by a report
# that was stored (validation failures never get here).
quota = _rate_limiters(request).bug_reports
ip_key = f"ip:{client_ip(request)}"
retry_after = quota.retry_after(ip_key)
if retry_after > 0:
raise _rate_limited_error(retry_after)
report = BugReport(
description=body.description,
contact=body.contact,
@@ -49,6 +59,7 @@ async def create_bug_report(
)
session.add(report)
await session.commit()
quota.record(ip_key)
return BugReportResponse(id=report.id)