const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const COMMON_PASSWORDS = new Set([ "password", "password123", "qwerty", "letmein", "welcome", "admin", "iloveyou", "monkey", "dragon", "abc123", ]); const DISPOSABLE_EMAIL_DOMAINS = new Set([ "10minutemail.com", "dispostable.com", "emailondeck.com", "fakemail.net", "getnada.com", "guerrillamail.com", "maildrop.cc", "mailinator.com", "sharklasers.com", "temp-mail.org", "tempmail.com", "throwawaymail.com", "trashmail.com", "yopmail.com", ]); function hasSequentialCharacters(value) { const normalizedValue = value.toLowerCase(); for (let index = 0; index <= normalizedValue.length - 4; index += 1) { const characters = normalizedValue.slice(index, index + 4); const codes = [...characters].map((character) => character.charCodeAt(0)); const ascending = codes.every((code, characterIndex) => characterIndex === 0 || code === codes[characterIndex - 1] + 1); const descending = codes.every((code, characterIndex) => characterIndex === 0 || code === codes[characterIndex - 1] - 1); if ((ascending || descending) && /^[a-z0-9]+$/.test(characters)) { return true; } } return false; } function getEmailParts(email) { const atIndex = email.lastIndexOf("@"); return { localPart: email.slice(0, atIndex), domain: email.slice(atIndex + 1), }; } function isDisposableEmail(email) { const { domain } = getEmailParts(email); return [...DISPOSABLE_EMAIL_DOMAINS].some( (disposableDomain) => domain === disposableDomain || domain.endsWith(`.${disposableDomain}`), ); } function hasControlCharacter(value) { return [...value].some((character) => { const code = character.charCodeAt(0); return code <= 0x1f || code === 0x7f; }); } function validateName(name) { const trimmedName = name.trim(); if (hasControlCharacter(name)) { return "Input contains unsupported control characters."; } if (trimmedName.length < 1 || trimmedName.length > 100) { return "Name must be between 1 and 100 characters."; } return null; } function validateUsername(username) { const trimmedUsername = username.trim(); if (hasControlCharacter(username) || trimmedUsername.length < 1 || trimmedUsername.length > 50 || /\s/.test(trimmedUsername)) { return "Username must be between 1 and 50 characters and cannot contain whitespace."; } return null; } function validateEmail(email, { checkDisposable = false } = {}) { const normalizedEmail = email.trim().toLowerCase(); if ( hasControlCharacter(email) || normalizedEmail.length < 1 || normalizedEmail.length > 254 || /\s/.test(normalizedEmail) || !EMAIL_PATTERN.test(normalizedEmail) ) { return "Enter a valid email address without whitespace."; } if (checkDisposable && isDisposableEmail(normalizedEmail)) { return "Disposable or temporary email addresses are not allowed."; } return null; } function validatePassword(password, username, email) { if (hasControlCharacter(password) || password.length < 1 || password.length > 128) { return "Password must be between 1 and 128 characters."; } if (password.length < 12) { return "Password must be at least 12 characters long."; } if (!/[a-z]/.test(password) || !/[A-Z]/.test(password) || !/\d/.test(password) || !/[^A-Za-z0-9\s]/.test(password)) { return "Password must contain a lowercase letter, uppercase letter, number, and special character."; } const passwordForComparison = password.toLowerCase(); const trimmedUsername = username.trim(); const normalizedEmail = email.trim().toLowerCase(); const localPart = EMAIL_PATTERN.test(normalizedEmail) ? getEmailParts(normalizedEmail).localPart : ""; if ( (trimmedUsername && passwordForComparison.includes(trimmedUsername.toLowerCase())) || (localPart && passwordForComparison.includes(localPart.toLowerCase())) ) { return "Password cannot contain your username or email address."; } if (COMMON_PASSWORDS.has(passwordForComparison)) { return "Choose a less common password."; } if (/(.)\1\1/.test(password) || hasSequentialCharacters(password)) { return "Password cannot contain repeated or sequential character patterns."; } return null; } export function getRegistrationFieldErrors(form) { return { name: validateName(form.name), username: validateUsername(form.username), email: validateEmail(form.email, { checkDisposable: true }), password: validatePassword(form.password, form.username, form.email), }; } function validateLoginPassword(password) { if (password.length < 12 || password.length > 128) { return "Password must be at least 12 characters."; } return null; } export function getLoginFieldErrors(form) { return { email: validateEmail(form.email), password: validateLoginPassword(form.password), }; } export function validateRegistration(form) { const errors = getRegistrationFieldErrors(form); return errors.name || errors.username || errors.email || errors.password || null; } export function validateLogin({ email, password }) { const errors = getLoginFieldErrors({ email, password }); return errors.email || errors.password || null; } export function normalizeRegistrationData({ name, username, email, password }) { return { name: name.trim(), username: username.trim(), email: email.trim().toLowerCase(), password, }; } export function normalizeLoginData({ email, password }) { return { email: email.trim().toLowerCase(), password, }; } export function isControlCharacterPresent(value) { return hasControlCharacter(value); }