馃 distributed transcription service thistle.dunkirk.sh
1/** 2 * Input validation utilities 3 */ 4 5// RFC 5322 compliant email regex (simplified but comprehensive) 6const EMAIL_REGEX = 7 /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; 8 9// Validation limits 10export const VALIDATION_LIMITS = { 11 EMAIL_MAX: 320, // RFC 5321 12 NAME_MAX: 255, 13 PASSWORD_HASH_LENGTH: 64, // PBKDF2 hex output 14 COURSE_CODE_MAX: 50, 15 COURSE_NAME_MAX: 500, 16 PROFESSOR_NAME_MAX: 255, 17 SEMESTER_MAX: 50, 18 CLASS_ID_MAX: 100, 19}; 20 21export interface ValidationResult { 22 valid: boolean; 23 error?: string; 24} 25 26/** 27 * Validate email address 28 */ 29export function validateEmail(email: unknown): ValidationResult { 30 if (typeof email !== "string") { 31 return { valid: false, error: "Email must be a string" }; 32 } 33 34 const trimmed = email.trim(); 35 36 if (trimmed.length === 0) { 37 return { valid: false, error: "Email is required" }; 38 } 39 40 if (trimmed.length > VALIDATION_LIMITS.EMAIL_MAX) { 41 return { 42 valid: false, 43 error: `Email must be less than ${VALIDATION_LIMITS.EMAIL_MAX} characters`, 44 }; 45 } 46 47 if (!EMAIL_REGEX.test(trimmed)) { 48 return { valid: false, error: "Invalid email format" }; 49 } 50 51 return { valid: true }; 52} 53 54/** 55 * Validate name (user name, professor name, etc.) 56 */ 57export function validateName( 58 name: unknown, 59 fieldName = "Name", 60): ValidationResult { 61 if (typeof name !== "string") { 62 return { valid: false, error: `${fieldName} must be a string` }; 63 } 64 65 const trimmed = name.trim(); 66 67 if (trimmed.length === 0) { 68 return { valid: false, error: `${fieldName} is required` }; 69 } 70 71 if (trimmed.length > VALIDATION_LIMITS.NAME_MAX) { 72 return { 73 valid: false, 74 error: `${fieldName} must be less than ${VALIDATION_LIMITS.NAME_MAX} characters`, 75 }; 76 } 77 78 return { valid: true }; 79} 80 81/** 82 * Validate password hash format (client-side PBKDF2) 83 */ 84export function validatePasswordHash(password: unknown): ValidationResult { 85 if (typeof password !== "string") { 86 return { valid: false, error: "Password must be a string" }; 87 } 88 89 // Client sends PBKDF2 as hex string 90 if ( 91 password.length !== VALIDATION_LIMITS.PASSWORD_HASH_LENGTH || 92 !/^[0-9a-f]+$/.test(password) 93 ) { 94 return { valid: false, error: "Invalid password format" }; 95 } 96 97 return { valid: true }; 98} 99 100/** 101 * Validate course code 102 */ 103export function validateCourseCode(courseCode: unknown): ValidationResult { 104 if (typeof courseCode !== "string") { 105 return { valid: false, error: "Course code must be a string" }; 106 } 107 108 const trimmed = courseCode.trim(); 109 110 if (trimmed.length === 0) { 111 return { valid: false, error: "Course code is required" }; 112 } 113 114 if (trimmed.length > VALIDATION_LIMITS.COURSE_CODE_MAX) { 115 return { 116 valid: false, 117 error: `Course code must be less than ${VALIDATION_LIMITS.COURSE_CODE_MAX} characters`, 118 }; 119 } 120 121 return { valid: true }; 122} 123 124/** 125 * Validate course/class name 126 */ 127export function validateCourseName(courseName: unknown): ValidationResult { 128 if (typeof courseName !== "string") { 129 return { valid: false, error: "Course name must be a string" }; 130 } 131 132 const trimmed = courseName.trim(); 133 134 if (trimmed.length === 0) { 135 return { valid: false, error: "Course name is required" }; 136 } 137 138 if (trimmed.length > VALIDATION_LIMITS.COURSE_NAME_MAX) { 139 return { 140 valid: false, 141 error: `Course name must be less than ${VALIDATION_LIMITS.COURSE_NAME_MAX} characters`, 142 }; 143 } 144 145 return { valid: true }; 146} 147 148/** 149 * Validate semester 150 */ 151export function validateSemester(semester: unknown): ValidationResult { 152 if (typeof semester !== "string") { 153 return { valid: false, error: "Semester must be a string" }; 154 } 155 156 const trimmed = semester.trim(); 157 158 if (trimmed.length === 0) { 159 return { valid: false, error: "Semester is required" }; 160 } 161 162 if (trimmed.length > VALIDATION_LIMITS.SEMESTER_MAX) { 163 return { 164 valid: false, 165 error: `Semester must be less than ${VALIDATION_LIMITS.SEMESTER_MAX} characters`, 166 }; 167 } 168 169 // Optional: validate it's a known semester value 170 const validSemesters = ["fall", "spring", "summer", "winter"]; 171 if (!validSemesters.includes(trimmed.toLowerCase())) { 172 return { 173 valid: false, 174 error: "Semester must be Fall, Spring, Summer, or Winter", 175 }; 176 } 177 178 return { valid: true }; 179} 180 181/** 182 * Validate year 183 */ 184export function validateYear(year: unknown): ValidationResult { 185 if (typeof year !== "number") { 186 return { valid: false, error: "Year must be a number" }; 187 } 188 189 const currentYear = new Date().getFullYear(); 190 const minYear = 2000; 191 const maxYear = currentYear + 5; 192 193 if (year < minYear || year > maxYear) { 194 return { 195 valid: false, 196 error: `Year must be between ${minYear} and ${maxYear}`, 197 }; 198 } 199 200 return { valid: true }; 201} 202 203/** 204 * Validate class ID format 205 */ 206export function validateClassId(classId: unknown): ValidationResult { 207 if (typeof classId !== "string") { 208 return { valid: false, error: "Class ID must be a string" }; 209 } 210 211 if (classId.length === 0) { 212 return { valid: false, error: "Class ID is required" }; 213 } 214 215 if (classId.length > VALIDATION_LIMITS.CLASS_ID_MAX) { 216 return { 217 valid: false, 218 error: `Class ID must be less than ${VALIDATION_LIMITS.CLASS_ID_MAX} characters`, 219 }; 220 } 221 222 return { valid: true }; 223}