audit: 33-agent comprehensive audit + critical fixes

Full team audit run, all reports verbatim in docs/AUDIT-2026-05-12.md
(5900+ lines, 30+ critical findings). Already-fixed this commit:
- permission-overrides PUT: self-target block + RolePermissions allow-list + cross-tenant guard
- /api/auth/resolve-identifier: rate-limit + synthetic miss-email kill enumeration
- admin email-change: rotates account.accountId + revokes sessions
- middleware: token-gated email confirm/cancel routes whitelisted
- NAV_CATALOG: 10 dead-link sweeps to existing /admin/<x> targets

Feature work landing same commit: optional username sign-in
(migration 0054), per-user permission overrides (0055) with three-state
matrix tabbed inside UserForm, user disable button, role + outcome +
stage label normalisation across the platform, admin email-change
with auto-notification template.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 16:52:35 +02:00
parent 660553c074
commit 4b9743a594
31 changed files with 7042 additions and 81 deletions

View File

@@ -0,0 +1,52 @@
import { z } from 'zod';
/**
* Canonical username shape.
*
* - 2..30 characters (yes, 2 — initials like "dm" are real and the
* director uses them)
* - lowercase letters, digits, `.`, `_`, `-`
* - case-insensitive uniqueness is enforced by a partial unique index on
* LOWER(username) in migration 0054.
*
* The same regex lives on the DB CHECK constraint, so any insert that
* slips past the API still gets rejected at the DB layer.
*/
export const USERNAME_REGEX = /^[a-z0-9._-]{2,30}$/;
export const usernameSchema = z
.string()
.transform((s) => s.trim().toLowerCase())
.refine(
(s) => USERNAME_REGEX.test(s),
'Use 230 lowercase letters, digits, dot, underscore, or hyphen.',
);
/** Reserved names that the API rejects even if the regex would accept them.
* Keeps obvious confusables out of customer-facing URLs / mentions. */
export const RESERVED_USERNAMES = new Set([
'admin',
'administrator',
'root',
'system',
'support',
'noreply',
'no-reply',
'help',
'security',
'me',
'self',
'api',
'auth',
'login',
'logout',
'signin',
'signup',
'register',
'undefined',
'null',
]);
export function isReservedUsername(username: string): boolean {
return RESERVED_USERNAMES.has(username.trim().toLowerCase());
}