Files
2026-07-21 13:44:33 -05:00

124 lines
3.5 KiB
JavaScript

const DEFAULT_POLICY = {
minLength: 6,
requireUppercase: false,
requireLowercase: false,
requireNumber: false,
requireSymbol: false,
};
function normalizePolicy(raw) {
const source = raw && typeof raw === "object" ? raw : {};
return {
minLength: Math.min(128, Math.max(6, Number(source.minLength) || DEFAULT_POLICY.minLength)),
requireUppercase: Boolean(source.requireUppercase),
requireLowercase: Boolean(source.requireLowercase),
requireNumber: Boolean(source.requireNumber),
requireSymbol: Boolean(source.requireSymbol),
};
}
export async function loadPasswordPolicy() {
try {
const res = await fetch("/api/registration/status", { credentials: "include" });
if (!res.ok) return DEFAULT_POLICY;
const data = await res.json().catch(() => ({}));
return normalizePolicy(data.passwordPolicy);
} catch {
return DEFAULT_POLICY;
}
}
function buildRules(password, confirmPassword, policy, includeMatch) {
const rules = [
{
id: "minLength",
label: `At least ${policy.minLength} characters`,
met: password.length >= policy.minLength,
},
];
if (policy.requireUppercase) {
rules.push({ id: "uppercase", label: "One uppercase letter", met: /[A-Z]/.test(password) });
}
if (policy.requireLowercase) {
rules.push({ id: "lowercase", label: "One lowercase letter", met: /[a-z]/.test(password) });
}
if (policy.requireNumber) {
rules.push({ id: "number", label: "One number", met: /[0-9]/.test(password) });
}
if (policy.requireSymbol) {
rules.push({ id: "symbol", label: "One symbol", met: /[^A-Za-z0-9]/.test(password) });
}
if (includeMatch) {
rules.push({
id: "match",
label: "Passwords match",
met: password.length > 0 && confirmPassword.length > 0 && password === confirmPassword,
});
}
return rules;
}
function render(container, rules) {
container.innerHTML = `
<div class="password-policy-title">
<i class="fa-solid fa-shield-halved"></i>
<span>Password requirements</span>
</div>
<ul class="password-policy-list">
${rules
.map(
(rule) => `
<li class="password-policy-item ${rule.met ? "is-met" : "is-unmet"}" data-password-rule="${rule.id}">
<i class="fa-solid ${rule.met ? "fa-circle-check" : "fa-circle"}"></i>
<span>${rule.label}</span>
</li>
`,
)
.join("")}
</ul>
`;
}
export function createPasswordPolicyChecklist({ passwordInput, confirmInput, container }) {
let policy = DEFAULT_POLICY;
const includeMatch = Boolean(confirmInput);
function currentRules() {
return buildRules(passwordInput?.value || "", confirmInput?.value || "", policy, includeMatch);
}
function update() {
if (!container) return;
render(container, currentRules());
}
function firstError() {
const failed = currentRules().find((rule) => !rule.met);
return failed ? failed.label : null;
}
function isSatisfied() {
return firstError() === null;
}
function setPolicy(nextPolicy) {
policy = normalizePolicy(nextPolicy);
if (passwordInput) {
passwordInput.minLength = policy.minLength;
passwordInput.placeholder = `At least ${policy.minLength} characters`;
}
if (confirmInput) {
confirmInput.minLength = policy.minLength;
}
update();
}
passwordInput?.addEventListener("input", update);
confirmInput?.addEventListener("input", update);
update();
return { firstError, isSatisfied, setPolicy, update };
}