373 lines
10 KiB
TypeScript
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();
|