Files
filing-cabinet-web/src/validation/auth.js
T

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);
}