mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 16:31:12 -05:00
108 lines
4.0 KiB
TypeScript
108 lines
4.0 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
// Reserved usernames for security and system purposes
|
|
const FORBIDDEN_USERNAMES = new Set([
|
|
// System/Admin accounts
|
|
'admin', 'administrator', 'moderator', 'mod', 'owner', 'root', 'system', 'support',
|
|
'staff', 'team', 'official', 'verified', 'bot', 'api', 'service',
|
|
|
|
// Company/Brand protection
|
|
'thrillwiki', 'lovable', 'supabase', 'cloudflare',
|
|
|
|
// Common system routes/pages
|
|
'www', 'mail', 'email', 'ftp', 'blog', 'forum', 'shop', 'store', 'app', 'mobile',
|
|
'help', 'support', 'contact', 'about', 'terms', 'privacy', 'security', 'legal',
|
|
'login', 'signup', 'register', 'signin', 'signout', 'logout', 'auth', 'oauth',
|
|
'profile', 'profiles', 'user', 'users', 'account', 'accounts', 'settings',
|
|
'dashboard', 'console', 'panel', 'manage', 'management',
|
|
|
|
// Technical terms
|
|
'null', 'undefined', 'true', 'false', 'delete', 'remove', 'test', 'demo',
|
|
'localhost', 'example', 'temp', 'temporary', 'guest', 'anonymous', 'anon',
|
|
|
|
// Offensive prevention (basic)
|
|
'fuck', 'shit', 'damn', 'hell', 'ass', 'bitch', 'bastard', 'crap',
|
|
'nazi', 'hitler', 'stalin', 'terrorist', 'kill', 'death', 'murder',
|
|
|
|
// Impersonation prevention
|
|
'ceo', 'president', 'manager', 'director', 'executive', 'founder'
|
|
]);
|
|
|
|
export const usernameSchema = z
|
|
.string()
|
|
.min(3, 'Username must be at least 3 characters')
|
|
.max(30, 'Username must be less than 30 characters')
|
|
.regex(
|
|
/^[a-zA-Z0-9]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$/,
|
|
'Username must start and end with letters/numbers, and can only contain letters, numbers, hyphens, and underscores'
|
|
)
|
|
.refine(
|
|
(val) => !/[-_]{2,}/.test(val),
|
|
'Username cannot contain consecutive hyphens or underscores'
|
|
)
|
|
.transform(val => val.toLowerCase())
|
|
.refine(val => !FORBIDDEN_USERNAMES.has(val), 'This username is not allowed');
|
|
|
|
// Display name validation with content filtering
|
|
export const displayNameSchema = z
|
|
.string()
|
|
.max(100, 'Display name must be less than 100 characters')
|
|
.refine(val => {
|
|
if (!val) return true;
|
|
const lowerVal = val.toLowerCase();
|
|
// Check for basic offensive content in display names
|
|
const offensiveTerms = ['nazi', 'hitler', 'terrorist', 'kill', 'murder', 'fuck', 'shit'];
|
|
return !offensiveTerms.some(term => lowerVal.includes(term));
|
|
}, 'Display name contains inappropriate content')
|
|
.optional();
|
|
|
|
// Password validation schema with complexity requirements
|
|
export const passwordSchema = z.object({
|
|
currentPassword: z.string().min(1, 'Current password is required'),
|
|
newPassword: z.string()
|
|
.min(8, 'Password must be at least 8 characters')
|
|
.max(128, 'Password must be less than 128 characters')
|
|
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
|
|
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
|
|
.regex(/[0-9]/, 'Password must contain at least one number')
|
|
.regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character'),
|
|
confirmPassword: z.string()
|
|
}).refine(data => data.newPassword === data.confirmPassword, {
|
|
message: "Passwords don't match",
|
|
path: ["confirmPassword"]
|
|
});
|
|
|
|
// Bio field validation with sanitization
|
|
export const bioSchema = z.string()
|
|
.max(500, 'Bio must be less than 500 characters')
|
|
.transform(val => val?.trim())
|
|
.refine(
|
|
val => !val || !/[<>]/.test(val),
|
|
'Bio cannot contain HTML tags'
|
|
)
|
|
.optional();
|
|
|
|
// Personal location field validation with sanitization
|
|
export const personalLocationSchema = z.string()
|
|
.max(100, 'Location must be less than 100 characters')
|
|
.transform(val => val?.trim())
|
|
.refine(
|
|
val => !val || !/[<>{}]/.test(val),
|
|
'Location cannot contain special characters'
|
|
)
|
|
.optional();
|
|
|
|
// Preferred pronouns validation
|
|
export const preferredPronounsSchema = z
|
|
.string()
|
|
.trim()
|
|
.max(20, { message: "Pronouns must be less than 20 characters" })
|
|
.optional();
|
|
|
|
export const profileEditSchema = z.object({
|
|
username: usernameSchema,
|
|
display_name: displayNameSchema,
|
|
bio: bioSchema,
|
|
});
|
|
|
|
export type ProfileEditForm = z.infer<typeof profileEditSchema>; |