Mercurial
view mrjunejune/src/account/password.html @ 273:e02e2036ef84 default tip
add Layer 2 JRPG component system
Add reusable content and window modals, an isolated component sandbox, shared cyberpunk scroll areas, production-safe cache freshness, and server-rendered JRPG panel state.
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sat, 08 Aug 2026 02:08:08 -0700 |
| parents | 04fee26ecce0 |
| children |
line wrap: on
line source
<!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>