mediahub-fe/lib/registration-utils.ts

373 lines
10 KiB
TypeScript

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<string, { count: number; lastAttempt: number }> = 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();