From ff337bc36b4ee7446999c4968f8d23ba77d73e6a Mon Sep 17 00:00:00 2001 From: KingKDot Date: Tue, 21 Jul 2026 13:44:33 -0500 Subject: [PATCH] better password shit --- .../public/assets/change-password.js | 20 ++- Overlord-Server/public/assets/main.css | 57 ++++++++ .../public/assets/password-policy.js | 123 ++++++++++++++++++ Overlord-Server/public/assets/register.js | 23 +++- Overlord-Server/public/build.html | 2 +- Overlord-Server/public/change-password.html | 2 + Overlord-Server/public/register.html | 5 +- Overlord-Server/public/users.html | 3 +- .../src/server/routes/registration-routes.ts | 12 ++ .../users-routes.must-change-password.test.ts | 103 +++++++++++++++ .../src/server/routes/users-routes.ts | 2 +- Overlord-Server/src/users.ts | 5 +- Overlord-Server/test/e2e/smoke.e2e.ts | 20 +++ 13 files changed, 360 insertions(+), 17 deletions(-) create mode 100644 Overlord-Server/public/assets/password-policy.js create mode 100644 Overlord-Server/src/server/routes/users-routes.must-change-password.test.ts 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 = ` +
+ + Password requirements +
+ + `; +} + +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 }; +} diff --git a/Overlord-Server/public/assets/register.js b/Overlord-Server/public/assets/register.js index 9e525b3..81c0b7d 100644 --- a/Overlord-Server/public/assets/register.js +++ b/Overlord-Server/public/assets/register.js @@ -1,8 +1,22 @@ +import { createPasswordPolicyChecklist, loadPasswordPolicy } from "./password-policy.js"; + const form = document.getElementById("register-form"); const errorEl = document.getElementById("error"); const successEl = document.getElementById("success"); const keyGroup = document.getElementById("key-group"); const keyInput = document.getElementById("reg-key"); +const passwordInput = document.getElementById("reg-pass"); +const confirmPasswordInput = document.getElementById("reg-pass-confirm"); +const policyEl = document.getElementById("password-policy"); + +const passwordChecklist = createPasswordPolicyChecklist({ + passwordInput, + confirmInput: confirmPasswordInput, + container: policyEl, +}); + +loadPasswordPolicy().then((policy) => passwordChecklist.setPolicy(policy)); + async function checkStatus() { try { @@ -87,16 +101,17 @@ form.addEventListener("submit", async (e) => { successEl.style.display = "none"; const username = document.getElementById("reg-user").value.trim(); - const password = document.getElementById("reg-pass").value; - const confirmPassword = document.getElementById("reg-pass-confirm").value; + const password = passwordInput.value; + const confirmPassword = confirmPasswordInput.value; const key = keyInput.value.trim(); if (password !== confirmPassword) { errorEl.textContent = "Passwords do not match."; return; } - if (password.length < 6) { - errorEl.textContent = "Password must be at least 6 characters."; + const policyError = passwordChecklist.firstError(); + if (policyError) { + errorEl.textContent = policyError; return; } diff --git a/Overlord-Server/public/build.html b/Overlord-Server/public/build.html index f7f2dc0..72b5149 100644 --- a/Overlord-Server/public/build.html +++ b/Overlord-Server/public/build.html @@ -1007,7 +1007,7 @@ Startup File Name

Custom name for the installed executable. On Windows: used as filename (svchost → svchost.exe). On macOS: also used as the LaunchAgent label — Leave blank for a default randomized name.

diff --git a/Overlord-Server/public/change-password.html b/Overlord-Server/public/change-password.html index d50bfe0..6d5e0d9 100644 --- a/Overlord-Server/public/change-password.html +++ b/Overlord-Server/public/change-password.html @@ -77,6 +77,8 @@ +
+