export const PASSWORD_POLICY = Object.freeze({ minLength: 8, requireLetter: true, requireNumber: true, disallowSameAsCurrent: true, }); export const PASSWORD_POLICY_REASON = Object.freeze({ MIN_LENGTH: "MIN_LENGTH", MISSING_LETTER: "MISSING_LETTER", MISSING_NUMBER: "MISSING_NUMBER", SAME_AS_CURRENT: "SAME_AS_CURRENT", }); function hasLetter(value) { return /[A-Za-z]/.test(String(value || "")); } function hasNumber(value) { return /\d/.test(String(value || "")); } /** * Validate a new password against the project's explicit policy. * * @param {{ newPassword?: unknown, currentPassword?: unknown }} input * @returns {{ * ok: boolean, * reasons: string[], * policy: { minLength: number, requireLetter: boolean, requireNumber: boolean, disallowSameAsCurrent: boolean } * }} */ export function validateNewPassword({ newPassword, currentPassword } = {}) { const pw = typeof newPassword === "string" ? newPassword : ""; const current = typeof currentPassword === "string" ? currentPassword : null; const reasons = []; if (pw.length < PASSWORD_POLICY.minLength) { reasons.push(PASSWORD_POLICY_REASON.MIN_LENGTH); } if (PASSWORD_POLICY.requireLetter && !hasLetter(pw)) { reasons.push(PASSWORD_POLICY_REASON.MISSING_LETTER); } if (PASSWORD_POLICY.requireNumber && !hasNumber(pw)) { reasons.push(PASSWORD_POLICY_REASON.MISSING_NUMBER); } if ( PASSWORD_POLICY.disallowSameAsCurrent && current !== null && pw === current ) { reasons.push(PASSWORD_POLICY_REASON.SAME_AS_CURRENT); } const uniqueReasons = Array.from(new Set(reasons)); return { ok: uniqueReasons.length === 0, reasons: uniqueReasons, policy: { ...PASSWORD_POLICY }, }; }