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

+10 -3
View File
@@ -34,8 +34,8 @@ async function callAdmin(method, path, body) {
const headers = { 'Content-Type': 'application/json', 'X-Admin-Token': adminToken };
const res = await fetch(path, { method, headers, body: body ? JSON.stringify(body) : undefined });
const data = await res.json().catch(() => ({}));
// detail is a bare string on the admin endpoints, but the shared dependencies
// (auth) answer with the structured {code, message} form of app/api/errors.py.
// Every error answers with the structured {code, message, params} detail (API-16);
// the bare-string fallback only covers a proxy's own error page.
if (!res.ok) throw new Error(data.detail?.message || data.detail || res.statusText);
return data;
}
@@ -230,7 +230,14 @@ async function adminSave() {
async function loadUsers() {
try {
const users = await callAdmin('GET', '/admin/users');
// B-94 (API-33): the list comes in pages of up to 500, by ascending id.
const users = [];
for (;;) {
const afterId = users.length ? users[users.length - 1].id : 0;
const page = await callAdmin('GET', `/admin/users?limit=500&after_id=${afterId}`);
users.push(...page);
if (page.length < 500) break;
}
const tbody = document.getElementById('users-tbody');
tbody.innerHTML = users.map((u) => `
<tr>