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:
1 parent
c1b192f6d1
commit
24e5fc3d8b
23 files changed
+374
-111
No files matched your search
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user