195 lines
5.5 KiB
JavaScript
195 lines
5.5 KiB
JavaScript
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);
|
|
}
|