Source: domain/validator.js

import {calculateAge} from "./module.js";

/**
 * Validates all parameters for a person
 * @param {Object} person - { birthDate, zip, identity, email }
 * @param {Date} person.birthDate - Date of birth
 * @param {string} person.zip - French zip code (5 digits)
 * @param {string} person.city - City name (letters, accents, espace, hyphens)
 * @param {string} person.firstName - First name (letters, accents, hyphens)
 * @param {string} person.lastName - Last name (letters, accents, hyphens)
 * @param {string} person.email - Email address
 * @returns {true} if everything is valid
 * @throws {Error} MISSING_PARAM, PARAM_TYPE_ERROR, UNDERAGE, INVALID_ZIP, INVALID_CITY, INVALID_FIRST_NAME, INVALID_LAST_NAME, XSS_DETECTED, INVALID_EMAIL
 */
export function validatePerson(person) {
    if (!person || typeof person !== "object") {
        throw new Error("MISSING_PARAM: person");
    }

    if (!person.birthDate) throw new Error("MISSING_PARAM: birthDate");
    if (!person.zip) throw new Error("MISSING_PARAM: zip");
    if (!person.city) throw new Error("MISSING_PARAM: city");
    if (!person.firstName) throw new Error("MISSING_PARAM: firstName");
    if (!person.lastName) throw new Error("MISSING_PARAM: lastName");
    if (!person.email) throw new Error("MISSING_PARAM: email");

    if (!(person.birthDate instanceof Date)) throw new TypeError("PARAM_TYPE_ERROR: birthDate must be a Date");
    if (typeof person.zip !== "string") throw new TypeError("PARAM_TYPE_ERROR: zip must be a string");
    if (typeof person.city !== "string") throw new TypeError("PARAM_TYPE_ERROR: city must be a string");
    if (typeof person.firstName !== "string") throw new TypeError("PARAM_TYPE_ERROR: firstName must be a string");
    if (typeof person.lastName !== "string") throw new TypeError("PARAM_TYPE_ERROR: lastName must be a string");
    if (typeof person.email !== "string") throw new TypeError("PARAM_TYPE_ERROR: email must be a string");

    validateAge(person.birthDate);
    validateZipCode(person.zip);
    validateCity(person.city)
    validateName(person.firstName);
    validateName(person.lastName);
    validateEmail(person.email);

    return true;
}

/**
 * Validates a person's age based on birth date.
 * Rejects if under 18 years old, in the future, or too far in the past.
 *
 * @param {Date} birthDate - Date of birth
 * @returns {boolean} Returns true if age is 18 or older
 *
 * @throws {TypeError} INVALID_DATE - If birthDate is not a valid Date object or cannot be parsed
 * @throws {Error} FUTURE_DATE - If birthDate is in the future
 * @throws {Error} BIRTHDATE_TOO_OLD - If birthDate is before 1900
 * @throws {Error} UNDERAGE - If age < 18
 */
export function validateAge(birthDate) {
    if (!(birthDate instanceof Date) || Number.isNaN(birthDate.getTime())) {
        throw new TypeError("INVALID_DATE");
    }

    const now = new Date();
    if (birthDate > now) throw new Error("FUTURE_DATE");

    const year = birthDate.getFullYear();
    if (year < 1900) throw new Error("BIRTHDATE_TOO_OLD")

    const age = calculateAge({birth: birthDate});
    if (age < 18) throw new Error("UNDERAGE");

    return true;
}

/**
 * Validates a zip code.
 * Must be exactly 5 digits.
 * @param {string} zip - Zip code
 * @returns {boolean} Returns true if zip is valid
 * @throws {Error} Throws "INVALID_ZIP" if zip is invalid
 */
export function validateZipCode(zip) {
    if (typeof zip !== "string" || !/^\d{5}$/.test(zip)) {
        throw new Error("INVALID_ZIP");
    }
    return true;
}

/**
 * Validate a city name.
 * Only letters, accents, spaces and hyphens are allowed.
 * Rejects simple XSS patterns.
 * @param {string} city - The city name to validate
 * @returns {boolean} true if valid
 * @throws {Error} INVALID_CITY if format is incorrect
 * @throws {Error} XSS_DETECTED if a basic XSS pattern is detected
 */
export function validateCity(city) {
    if (typeof city !== "string") throw new Error("INVALID_CITY");
    if (/<[^>]*>/.test(city)) throw new Error("XSS_DETECTED");
    if (!/^[A-Za-zÀ-ÖØ-öø-ÿ\s-]+$/.test(city)) {
        throw new Error("INVALID_CITY");
    }
    return true;
}


/**
 * Validate a name (first or last) and return specific error messages
 * @param {string} name - The name to validate
 * @param {string} type - "firstName" or "lastName" for custom error messages
 * @returns {boolean} true if valid
 * @throws {Error} with message INVALID_FIRST_NAME or INVALID_LAST_NAME, XSS_DETECTED
 */
export function validateName(name, type = "firstName") {
    if (typeof name !== "string") throw new Error(type === "firstName" ? "INVALID_FIRST_NAME" : "INVALID_LAST_NAME");
    if (/<[^>]*>/.test(name)) throw new Error("XSS_DETECTED");
    if (!/^[A-Za-zÀ-ÖØ-öø-ÿ-]+$/.test(name)) {
        throw new Error(type === "firstName" ? "INVALID_FIRST_NAME" : "INVALID_LAST_NAME");
    }
    return true;
}

/**
 * Validates an email address.
 * Must be in standard email format.
 * @param {string} email - Email to validate
 * @returns {boolean} Returns true if email is valid
 * @throws {Error} Throws "INVALID_EMAIL" if email format is incorrect
 */
export function validateEmail(email) {
    if (typeof email !== "string") {
        throw new Error("INVALID_EMAIL");
    }

    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
    if (!emailRegex.test(email.trim())) {
        throw new Error("INVALID_EMAIL");
    }
    return true;
}