import { RegistrationFormData, JournalistRegistrationData, PersonnelRegistrationData, GeneralRegistrationData, InstituteData, UserCategory, PasswordValidation, TimerState, Association } from "@/types/registration"; import { toast } from "sonner"; // Constants export const REGISTRATION_CONSTANTS = { OTP_TIMEOUT: 60000, // 1 minute in milliseconds MAX_OTP_ATTEMPTS: 3, PASSWORD_MIN_LENGTH: 8, USERNAME_MIN_LENGTH: 3, USERNAME_MAX_LENGTH: 50, } as const; // Association data export const ASSOCIATIONS: Association[] = [ { id: "1", name: "PWI (Persatuan Wartawan Indonesia)", value: "PWI" }, { id: "2", name: "IJTI (Ikatan Jurnalis Televisi Indonesia)", value: "IJTI" }, { id: "3", name: "PFI (Pewarta Foto Indonesia)", value: "PFI" }, { id: "4", name: "AJI (Asosiasi Jurnalis Indonesia)", value: "AJI" }, { id: "5", name: "Other Identity", value: "Wartawan" }, ]; // Password validation utility export const validatePassword = (password: string, confirmPassword?: string): PasswordValidation => { const errors: string[] = []; let strength: 'weak' | 'medium' | 'strong' = 'weak'; // Check minimum length if (password.length < REGISTRATION_CONSTANTS.PASSWORD_MIN_LENGTH) { errors.push(`Password must be at least ${REGISTRATION_CONSTANTS.PASSWORD_MIN_LENGTH} characters`); } // Check for uppercase letter if (!/[A-Z]/.test(password)) { errors.push("Password must contain at least one uppercase letter"); } // Check for lowercase letter if (!/[a-z]/.test(password)) { errors.push("Password must contain at least one lowercase letter"); } // Check for number if (!/\d/.test(password)) { errors.push("Password must contain at least one number"); } // Check for special character if (!/[@$!%*?&]/.test(password)) { errors.push("Password must contain at least one special character (@$!%*?&)"); } // Check password confirmation if (confirmPassword && password !== confirmPassword) { errors.push("Passwords don't match"); } // Determine strength if (password.length >= 12 && errors.length === 0) { strength = 'strong'; } else if (password.length >= 8 && errors.length <= 1) { strength = 'medium'; } return { isValid: errors.length === 0, errors, strength, }; }; // Username validation utility export const validateUsername = (username: string): { isValid: boolean; error?: string } => { if (username.length < REGISTRATION_CONSTANTS.USERNAME_MIN_LENGTH) { return { isValid: false, error: `Username must be at least ${REGISTRATION_CONSTANTS.USERNAME_MIN_LENGTH} characters` }; } if (username.length > REGISTRATION_CONSTANTS.USERNAME_MAX_LENGTH) { return { isValid: false, error: `Username must be less than ${REGISTRATION_CONSTANTS.USERNAME_MAX_LENGTH} characters` }; } if (!/^[a-zA-Z0-9._-]+$/.test(username)) { return { isValid: false, error: "Username can only contain letters, numbers, dots, underscores, and hyphens" }; } return { isValid: true }; }; // Email validation utility export const validateEmail = (email: string): { isValid: boolean; error?: string } => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!email) { return { isValid: false, error: "Email is required" }; } if (!emailRegex.test(email)) { return { isValid: false, error: "Please enter a valid email address" }; } return { isValid: true }; }; // Phone number validation utility export const validatePhoneNumber = (phoneNumber: string): { isValid: boolean; error?: string } => { const phoneRegex = /^[0-9+\-\s()]+$/; if (!phoneNumber) { return { isValid: false, error: "Phone number is required" }; } if (!phoneRegex.test(phoneNumber)) { return { isValid: false, error: "Please enter a valid phone number" }; } return { isValid: true }; }; // Timer utility export const createTimer = (duration: number = REGISTRATION_CONSTANTS.OTP_TIMEOUT): TimerState => { return { countdown: duration, isActive: true, isExpired: false, }; }; export const formatTime = (milliseconds: number): string => { const minutes = Math.floor(milliseconds / 60000); const seconds = Math.floor((milliseconds % 60000) / 1000); return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; }; // Data sanitization utility export const sanitizeRegistrationData = (data: RegistrationFormData): RegistrationFormData => { return { ...data, firstName: data.firstName.trim(), username: data.username.trim().toLowerCase(), email: data.email.trim().toLowerCase(), phoneNumber: data.phoneNumber.trim(), address: data.address.trim(), }; }; export const sanitizeInstituteData = (data: InstituteData): InstituteData => { return { name: data.name.trim(), address: data.address.trim(), }; }; // Category validation utility export const isValidCategory = (category: string): category is UserCategory => { return category === "6" || category === "7" || category === "general"; }; export const getCategoryLabel = (category: UserCategory): string => { switch (category) { case "6": return "Journalist"; case "7": return "Personnel"; case "general": return "Public"; default: return "Unknown"; } }; // Category to role ID conversion utility export const getCategoryRoleId = (category: UserCategory): number => { switch (category) { case "6": return 6; // Journalist case "7": return 7; // Personnel case "general": return 5; // Public (general) default: return 5; // Default to public } }; // Error handling utilities export const showRegistrationError = (error: any, defaultMessage: string = "Registration failed") => { const message = error?.message || error?.data?.message || defaultMessage; toast.error(message); console.error("Registration error:", error); }; export const showRegistrationSuccess = (message: string = "Registration successful") => { toast.success(message); }; export const showRegistrationInfo = (message: string) => { toast.info(message); }; // Data transformation utilities export const transformRegistrationData = ( data: RegistrationFormData, category: UserCategory, userData: any, instituteId?: number ): any => { const baseData = { firstName: data.firstName, lastName: data.firstName, // Using firstName as lastName for now username: data.username, phoneNumber: data.phoneNumber, email: data.email, address: data.address, provinceId: Number(data.provinsi), cityId: Number(data.kota), districtId: Number(data.kecamatan), password: data.password, roleId: getCategoryRoleId(category), }; // Add category-specific data if (category === "6") { return { ...baseData, memberIdentity: userData?.journalistCertificate, instituteId: instituteId || 1, }; } else if (category === "7") { return { ...baseData, memberIdentity: userData?.policeNumber, instituteId: 1, }; } else { return { ...baseData, memberIdentity: null, instituteId: 1, }; } }; // Form data utilities export const createInitialFormData = (category: UserCategory) => { const baseData = { firstName: "", username: "", phoneNumber: "", email: "", address: "", provinsi: "", kota: "", kecamatan: "", password: "", passwordConf: "", }; if (category === "6") { return { ...baseData, journalistCertificate: "", association: "", }; } else if (category === "7") { return { ...baseData, policeNumber: "", }; } return baseData; }; // Validation utilities export const validateIdentityData = ( data: JournalistRegistrationData | PersonnelRegistrationData | GeneralRegistrationData, category: UserCategory ): { isValid: boolean; errors: string[] } => { const errors: string[] = []; // Email validation const emailValidation = validateEmail(data.email); if (!emailValidation.isValid) { errors.push(emailValidation.error!); } // Category-specific validation if (category === "6") { const journalistData = data as JournalistRegistrationData; if (!journalistData.journalistCertificate?.trim()) { errors.push("Journalist certificate number is required"); } if (!journalistData.association?.trim()) { errors.push("Association is required"); } } else if (category === "7") { const personnelData = data as PersonnelRegistrationData; if (!personnelData.policeNumber?.trim()) { errors.push("Police number is required"); } } return { isValid: errors.length === 0, errors, }; }; // Rate limiting utility export class RegistrationRateLimiter { private attempts: Map = new Map(); private readonly maxAttempts: number; private readonly windowMs: number; constructor(maxAttempts: number = REGISTRATION_CONSTANTS.MAX_OTP_ATTEMPTS, windowMs: number = 300000) { this.maxAttempts = maxAttempts; this.windowMs = windowMs; } canAttempt(identifier: string): boolean { const attempt = this.attempts.get(identifier); if (!attempt) return true; const now = Date.now(); if (now - attempt.lastAttempt > this.windowMs) { this.attempts.delete(identifier); return true; } return attempt.count < this.maxAttempts; } recordAttempt(identifier: string): void { const attempt = this.attempts.get(identifier); const now = Date.now(); if (attempt) { attempt.count++; attempt.lastAttempt = now; } else { this.attempts.set(identifier, { count: 1, lastAttempt: now }); } } getRemainingAttempts(identifier: string): number { const attempt = this.attempts.get(identifier); if (!attempt) return this.maxAttempts; const now = Date.now(); if (now - attempt.lastAttempt > this.windowMs) { this.attempts.delete(identifier); return this.maxAttempts; } return Math.max(0, this.maxAttempts - attempt.count); } reset(identifier: string): void { this.attempts.delete(identifier); } } // Export rate limiter instance export const registrationRateLimiter = new RegistrationRateLimiter();