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>
53 lines
1.3 KiB
TypeScript
53 lines
1.3 KiB
TypeScript
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 2–30 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());
|
||
}
|