Mercurial
diff mrjunejune/src/account/password.html @ 264:04fee26ecce0
add authenticated JRPG conversation platform
Add reusable auth/session storage, owned conversation recovery, guest quotas, admin workflows, URL-routed conversation UI, mobile frame support, and parallel browser acceptance.
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Fri, 07 Aug 2026 07:34:12 -0700 |
| parents | |
| children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/src/account/password.html Fri Aug 07 07:34:12 2026 -0700 @@ -0,0 +1,237 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + {{/parts/base_head.html}} + <title>Change password — MrJuneJune</title> + <style> + main { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 60vh; + padding: var(--zenbu-sys-padding-lg, 2rem) var(--zenbu-sys-padding-md, 1rem); + } + + .password-card { + width: 100%; + max-width: 400px; + } + + .password-card h2 { + margin: 0 0 var(--zenbu-sys-padding-md, 1.25rem); + font-family: "More", sans-serif; + font-size: 1.4rem; + font-weight: 700; + text-align: center; + } + + .password-form { + display: flex; + flex-direction: column; + gap: var(--zenbu-sys-padding-sm, 0.75rem); + } + + .password-actions { + display: flex; + justify-content: flex-end; + margin-top: var(--zenbu-sys-padding-xs, 0.5rem); + } + + #passwordError { + display: none; + padding: 0.6rem 0.75rem; + border-radius: 6px; + background: color-mix(in srgb, var(--zenbu-sys-color-error, #c0392b) 12%, transparent); + color: var(--zenbu-sys-color-error, #c0392b); + font-size: 0.875rem; + } + + #passwordError[aria-hidden="false"] { + display: block; + } + + #passwordSuccess { + display: none; + padding: 0.6rem 0.75rem; + border-radius: 6px; + background: color-mix(in srgb, var(--zenbu-sys-color-success, #27ae60) 12%, transparent); + color: var(--zenbu-sys-color-success, #27ae60); + font-size: 0.875rem; + } + + #passwordSuccess[aria-hidden="false"] { + display: block; + } + </style> +</head> +<body> + {{/parts/header.html}} + + <main> + <div class="password-card"> + <zen-heading size="xl"> + <h2>Change password</h2> + </zen-heading> + + <div id="passwordError" role="alert" aria-live="assertive" aria-hidden="true"></div> + <div id="passwordSuccess" role="status" aria-live="polite" aria-hidden="true"></div> + + <form class="password-form" id="passwordForm" novalidate> + <zen-field size="md"> + <label for="currentPassword">Current password</label> + <input + id="currentPassword" + name="currentPassword" + type="password" + autocomplete="current-password" + required + > + </zen-field> + + <zen-field size="md"> + <label for="newPassword">New password</label> + <input + id="newPassword" + name="newPassword" + type="password" + autocomplete="new-password" + required + minlength="12" + maxlength="1024" + > + </zen-field> + + <zen-field size="md"> + <label for="confirmPassword">Confirm new password</label> + <input + id="confirmPassword" + name="confirmPassword" + type="password" + autocomplete="new-password" + required + minlength="12" + maxlength="1024" + > + </zen-field> + + <div class="password-actions"> + <zen-button size="md"> + <button type="submit" id="passwordSubmit">Change password</button> + </zen-button> + </div> + </form> + </div> + </main> + + <script> + (function () { + 'use strict'; + + const form = document.getElementById('passwordForm'); + const errorEl = document.getElementById('passwordError'); + const successEl = document.getElementById('passwordSuccess'); + const submitBtn = document.getElementById('passwordSubmit'); + + function showError(message) { + errorEl.textContent = message; + errorEl.setAttribute('aria-hidden', 'false'); + successEl.setAttribute('aria-hidden', 'true'); + } + + function showSuccess(message) { + successEl.textContent = message; + successEl.setAttribute('aria-hidden', 'false'); + errorEl.setAttribute('aria-hidden', 'true'); + } + + function hideMessages() { + errorEl.textContent = ''; + errorEl.setAttribute('aria-hidden', 'true'); + successEl.textContent = ''; + successEl.setAttribute('aria-hidden', 'true'); + } + + async function fetchSession() { + const resp = await fetch('/api/auth/session', { + credentials: 'same-origin', + }); + if (!resp.ok) throw new Error('session unavailable'); + return resp.json(); + } + + form.addEventListener('submit', async function (evt) { + evt.preventDefault(); + hideMessages(); + + const currentPassword = form.currentPassword.value; + const newPassword = form.newPassword.value; + const confirmPassword = form.confirmPassword.value; + + if (!currentPassword) { + showError('Please enter your current password.'); + return; + } + if (!newPassword || newPassword.length < 12) { + showError('New password must be at least 12 characters.'); + return; + } + if (newPassword !== confirmPassword) { + showError('New passwords do not match.'); + return; + } + + submitBtn.disabled = true; + + try { + const session = await fetchSession(); + if (session.kind !== 'user') { + showError('You must be signed in to change your password.'); + return; + } + + const csrfToken = session.csrfToken; + + const resp = await fetch('/api/auth/password', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Origin': window.location.origin, + }, + credentials: 'same-origin', + body: JSON.stringify({ currentPassword, newPassword, csrfToken }), + }); + + const data = await resp.json(); + + if (!resp.ok) { + if (resp.status === 401) { + showError('Current password is incorrect.'); + } else if (resp.status === 400 && data.error && + data.error.code === 'password_policy') { + showError(data.error.message || + 'New password must be at least 12 characters.'); + } else { + showError('Password change failed. Please try again.'); + } + return; + } + + showSuccess('Password changed successfully.'); + form.reset(); + + /* Redirect to home after a brief delay */ + setTimeout(function () { + window.location.href = '/jrpg'; + }, 1500); + + } catch (_) { + showError('Password change failed. Please try again.'); + } finally { + submitBtn.disabled = false; + } + }); + })(); + </script> +</body> +</html>