/** * Input validation utilities for the Ultimate Lists App * Provides validation functions and sanitization for user inputs */ export interface ValidationResult { isValid: boolean error?: string } /** * Sanitize string input by trimming whitespace */ export function sanitizeString(input: string | null | undefined): string { if (input === null || input === undefined) { return '' } return input.trim() } /** * Validate list title */ export function validateListTitle(title: string): ValidationResult { const sanitized = sanitizeString(title) if (!sanitized) { return { isValid: false, error: 'List title is required' } } if (sanitized.length > 200) { return { isValid: false, error: 'List title must be less than 200 characters' } } return { isValid: true } } /** * Validate item/task title */ export function validateItemTitle(title: string): ValidationResult { const sanitized = sanitizeString(title) if (!sanitized) { return { isValid: false, error: 'Task title is required' } } if (sanitized.length > 200) { return { isValid: false, error: 'Task title must be less than 200 characters' } } return { isValid: true } } /** * Validate hex color format */ export function validateColor(color: string): ValidationResult { const hexColorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/ if (!hexColorRegex.test(color)) { return { isValid: false, error: 'Invalid color format. Use hex format (e.g., #3B82F6)' } } return { isValid: true } } /** * Validate priority value */ export function validatePriority(priority: string): ValidationResult { const validPriorities = ['low', 'medium', 'high'] if (!validPriorities.includes(priority)) { return { isValid: false, error: 'Priority must be low, medium, or high' } } return { isValid: true } } /** * Validate date string format (ISO 8601 or YYYY-MM-DD) */ export function validateDateString(dateString: string): ValidationResult { if (!dateString) { return { isValid: true } // Empty date is valid (optional field) } // Try to parse the date const date = new Date(dateString) // Check if date is valid if (isNaN(date.getTime())) { return { isValid: false, error: 'Invalid date format. Use YYYY-MM-DD format' } } // Check if date is reasonable (not too far in past or future) const minDate = new Date('1900-01-01') const maxDate = new Date('2100-12-31') if (date < minDate || date > maxDate) { return { isValid: false, error: 'Date must be between 1900 and 2100' } } return { isValid: true } } /** * Validate tag string */ export function validateTag(tag: string): ValidationResult { const sanitized = sanitizeString(tag) if (!sanitized) { return { isValid: false, error: 'Tag cannot be empty' } } if (sanitized.length > 50) { return { isValid: false, error: 'Tag must be less than 50 characters' } } // Disallow special characters that might cause issues const invalidCharsRegex = /[<>\"']/ if (invalidCharsRegex.test(sanitized)) { return { isValid: false, error: 'Tag contains invalid characters' } } return { isValid: true } } /** * Validate array of tags */ export function validateTags(tags: string[]): ValidationResult { if (!Array.isArray(tags)) { return { isValid: false, error: 'Tags must be an array' } } if (tags.length > 20) { return { isValid: false, error: 'Maximum 20 tags allowed' } } for (const tag of tags) { const result = validateTag(tag) if (!result.isValid) { return result } } return { isValid: true } } /** * Validate description text */ export function validateDescription(description: string | null | undefined): ValidationResult { if (!description) { return { isValid: true } // Description is optional } if (description.length > 2000) { return { isValid: false, error: 'Description must be less than 2000 characters' } } return { isValid: true } } /** * Validate notes text */ export function validateNotes(notes: string | null | undefined): ValidationResult { if (!notes) { return { isValid: true } // Notes are optional } if (notes.length > 5000) { return { isValid: false, error: 'Notes must be less than 5000 characters' } } return { isValid: true } } /** * Sanitize HTML to prevent XSS attacks * Basic implementation - strips HTML tags */ export function sanitizeHtml(input: string): string { return input.replace(/<[^>]*>/g, '') } /** * Validate and sanitize list creation data */ export function validateListData(data: { title: string; color?: string }): { isValid: boolean errors: Record sanitized: { title: string; color: string } } { const errors: Record = {} // Validate title const titleResult = validateListTitle(data.title) if (!titleResult.isValid) { errors.title = titleResult.error! } // Validate color if provided const color = data.color || '#3B82F6' const colorResult = validateColor(color) if (!colorResult.isValid) { errors.color = colorResult.error! } return { isValid: Object.keys(errors).length === 0, errors, sanitized: { title: sanitizeString(data.title), color, }, } } /** * Validate and sanitize item creation data */ export function validateItemData(data: { title: string description?: string | null priority?: string dueDate?: string | null tags?: string[] notes?: string | null }): { isValid: boolean errors: Record sanitized: { title: string description: string | null priority: 'low' | 'medium' | 'high' dueDate: Date | null tags: string[] notes: string | null } } { const errors: Record = {} // Validate title const titleResult = validateItemTitle(data.title) if (!titleResult.isValid) { errors.title = titleResult.error! } // Validate description const descResult = validateDescription(data.description) if (!descResult.isValid) { errors.description = descResult.error! } // Validate priority const priority = data.priority || 'medium' const priorityResult = validatePriority(priority) if (!priorityResult.isValid) { errors.priority = priorityResult.error! } // Validate due date let dueDate: Date | null = null if (data.dueDate) { const dateResult = validateDateString(data.dueDate) if (!dateResult.isValid) { errors.dueDate = dateResult.error! } else { dueDate = new Date(data.dueDate) } } // Validate tags const tags = data.tags || [] const tagsResult = validateTags(tags) if (!tagsResult.isValid) { errors.tags = tagsResult.error! } // Validate notes const notesResult = validateNotes(data.notes) if (!notesResult.isValid) { errors.notes = notesResult.error! } return { isValid: Object.keys(errors).length === 0, errors, sanitized: { title: sanitizeString(data.title), description: data.description ? sanitizeString(data.description) : null, priority: priority as 'low' | 'medium' | 'high', dueDate, tags: tags.map(sanitizeString), notes: data.notes ? sanitizeString(data.notes) : null, }, } }