Files
thrilltrack-explorer/src-old/lib/validation.ts

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>;