diff --git a/Overlord-Server/public/assets/change-password.js b/Overlord-Server/public/assets/change-password.js index 8c64e33..2da7eac 100644 --- a/Overlord-Server/public/assets/change-password.js +++ b/Overlord-Server/public/assets/change-password.js @@ -1,9 +1,12 @@ +import { createPasswordPolicyChecklist, loadPasswordPolicy } from "./password-policy.js"; + const form = document.getElementById("change-password-form"); const currentPass = document.getElementById("current-pass"); const newPass = document.getElementById("new-pass"); const confirmPass = document.getElementById("confirm-pass"); const errEl = document.getElementById("error"); const successEl = document.getElementById("success"); +const policyEl = document.getElementById("password-policy"); const tempToken = sessionStorage.getItem("temp_token"); const tempUser = sessionStorage.getItem("temp_user"); @@ -14,6 +17,14 @@ if (!tempToken || !tempUser) { const user = JSON.parse(tempUser); +const passwordChecklist = createPasswordPolicyChecklist({ + passwordInput: newPass, + confirmInput: confirmPass, + container: policyEl, +}); + +loadPasswordPolicy().then((policy) => passwordChecklist.setPolicy(policy)); + if (typeof anime !== "undefined") { const particles = document.querySelectorAll(".particle"); particles.forEach((particle, i) => { @@ -131,15 +142,12 @@ form?.addEventListener("submit", async (e) => { return; } - if (newPass.value.length < 6) { - errEl.textContent = "New password must be at least 6 characters"; + const policyError = passwordChecklist.firstError(); + if (policyError) { + errEl.textContent = policyError; return; } - if (currentPass.value === newPass.value) { - errEl.textContent = "New password must be different from current password"; - return; - } try { const res = await fetch(`/api/users/${user.id}/password`, { diff --git a/Overlord-Server/public/assets/main.css b/Overlord-Server/public/assets/main.css index 8cd658e..9b9ceb5 100644 --- a/Overlord-Server/public/assets/main.css +++ b/Overlord-Server/public/assets/main.css @@ -1195,6 +1195,63 @@ article.card[data-bookmarked="true"] { color: #6b7a98; } +.password-policy-card { + margin-top: 10px; + padding: 10px 12px; + border: 1px solid rgba(148, 163, 184, 0.16); + border-radius: var(--radius-md); + background: rgba(15, 23, 42, 0.48); +} + +.password-policy-title { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; + color: #cbd5e1; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.02em; + text-transform: uppercase; +} + +.password-policy-title i { + color: #93c5fd; +} + +.password-policy-list { + display: grid; + grid-template-columns: 1fr; + gap: 6px; + margin: 0; + padding: 0; + list-style: none; +} + +.password-policy-item { + display: flex; + align-items: center; + gap: 8px; + color: #94a3b8; + font-size: 12px; + line-height: 1.35; + transition: color 160ms ease; +} + +.password-policy-item i { + width: 14px; + color: #64748b; + font-size: 11px; +} + +.password-policy-item.is-met { + color: #86efac; +} + +.password-policy-item.is-met i { + color: #22c55e; +} + .button.primary { background: linear-gradient(120deg, #4f6bff, #715dff); border-color: rgba(129, 140, 248, 0.55); diff --git a/Overlord-Server/public/assets/password-policy.js b/Overlord-Server/public/assets/password-policy.js new file mode 100644 index 0000000..7d313ff --- /dev/null +++ b/Overlord-Server/public/assets/password-policy.js @@ -0,0 +1,123 @@ +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 = ` +
Custom name for the installed executable. On Windows: used as filename (svchost → svchost.exe). On macOS: also used as the LaunchAgent label — must start with com. (e.g. com.apple.updater).Leave blank for a default randomized name.