SPB Git forge

spb/spinza

Public
8commits 1branches 0releases
1.6 MBsize
maindefault branch
16 days agolast push
TypeScript 97.6% SQL 1.4% JavaScript 0.5%
2.7 KB · 79 lines typescript
Raw Blame History
1import { z } from "zod";2import { BET_LEVELS, DEFAULT_PROFANITY, RESERVED_USERNAMES } from "./constants";34export const USERNAME_RE = /^[a-z0-9][a-z0-9_-]{2,23}$/;56export function normalizeUsername(raw: string): string {7  return raw.trim().toLowerCase();8}910export interface UsernameCheck {11  ok: boolean;12  reason?: "length" | "charset" | "reserved" | "profanity";13}1415export function checkUsername(raw: string, extraProfanity: string[] = []): UsernameCheck {16  const u = normalizeUsername(raw);17  if (u.length < 3 || u.length > 24) return { ok: false, reason: "length" };18  if (!USERNAME_RE.test(u)) return { ok: false, reason: "charset" };19  if (RESERVED_USERNAMES.has(u)) return { ok: false, reason: "reserved" };20  const squashed = u.replace(/[_-]/g, "");21  for (const w of [...DEFAULT_PROFANITY, ...extraProfanity]) {22    if (w && squashed.includes(w.toLowerCase())) return { ok: false, reason: "profanity" };23  }24  return { ok: true };25}2627export const usernameSchema = z28  .string()29  .min(3)30  .max(24)31  .transform(normalizeUsername)32  .refine((u) => USERNAME_RE.test(u), "Use 3–24 lowercase letters, numbers, _ or -");3334export const passwordSchema = z.string().min(8, "At least 8 characters").max(256);3536export const registerSchema = z37  .object({38    username: usernameSchema,39    password: passwordSchema,40    confirmPassword: z.string(),41    ageConfirmed: z.literal(true, { error: "You must confirm you are 18 or older" }),42  })43  .refine((d) => d.password === d.confirmPassword, { message: "Passwords do not match", path: ["confirmPassword"] });4445export const loginSchema = z.object({46  username: usernameSchema,47  password: z.string().min(1).max(256),48});4950export const RECOVERY_RE = /^SPZ-[A-Z2-9]{4}-[A-Z2-9]{4}-[A-Z2-9]{4}$/;5152export const recoverSchema = z.object({53  username: usernameSchema,54  recoveryCode: z55    .string()56    .transform((s) => s.trim().toUpperCase().replace(/\s+/g, ""))57    .refine((s) => RECOVERY_RE.test(s), "Recovery code format: SPZ-XXXX-XXXX-XXXX"),58  newPassword: passwordSchema,59});6061export const spinSchema = z.object({62  bet: z.number().int().refine((b) => (BET_LEVELS as readonly number[]).includes(b), "Invalid bet"),63  clientRoundId: z.string().uuid(),64});6566export const settingsSchema = z.object({67  soundEnabled: z.boolean().optional(),68  musicVolume: z.number().min(0).max(1).optional(),69  effectsVolume: z.number().min(0).max(1).optional(),70  masterVolume: z.number().min(0).max(1).optional(),71  reduceMotion: z.boolean().optional(),72  animationIntensity: z.enum(["low", "medium", "high"]).optional(),73  sessionReminderMinutes: z.number().int().min(0).max(240).optional(),74  breakReminder: z.boolean().optional(),75  leaderboardOptIn: z.boolean().optional(),76});7778export type SettingsInput = z.infer<typeof settingsSchema>;79