Files
pn-new-crm/src/lib/validators/username.ts
Matt 4b9743a594 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>
2026-05-12 16:52:35 +02:00

53 lines
1.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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());
}