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());
|
|||
|
|
}
|