Mercurial
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 263:ee04e4e69fed | 264:04fee26ecce0 |
|---|---|
| 1 <!DOCTYPE html> | |
| 2 <html lang="en"> | |
| 3 <head> | |
| 4 {{/parts/base_head.html}} | |
| 5 <title>Change password — MrJuneJune</title> | |
| 6 <style> | |
| 7 main { | |
| 8 display: flex; | |
| 9 flex-direction: column; | |
| 10 align-items: center; | |
| 11 justify-content: center; | |
| 12 min-height: 60vh; | |
| 13 padding: var(--zenbu-sys-padding-lg, 2rem) var(--zenbu-sys-padding-md, 1rem); | |
| 14 } | |
| 15 | |
| 16 .password-card { | |
| 17 width: 100%; | |
| 18 max-width: 400px; | |
| 19 } | |
| 20 | |
| 21 .password-card h2 { | |
| 22 margin: 0 0 var(--zenbu-sys-padding-md, 1.25rem); | |
| 23 font-family: "More", sans-serif; | |
| 24 font-size: 1.4rem; | |
| 25 font-weight: 700; | |
| 26 text-align: center; | |
| 27 } | |
| 28 | |
| 29 .password-form { | |
| 30 display: flex; | |
| 31 flex-direction: column; | |
| 32 gap: var(--zenbu-sys-padding-sm, 0.75rem); | |
| 33 } | |
| 34 | |
| 35 .password-actions { | |
| 36 display: flex; | |
| 37 justify-content: flex-end; | |
| 38 margin-top: var(--zenbu-sys-padding-xs, 0.5rem); | |
| 39 } | |
| 40 | |
| 41 #passwordError { | |
| 42 display: none; | |
| 43 padding: 0.6rem 0.75rem; | |
| 44 border-radius: 6px; | |
| 45 background: color-mix(in srgb, var(--zenbu-sys-color-error, #c0392b) 12%, transparent); | |
| 46 color: var(--zenbu-sys-color-error, #c0392b); | |
| 47 font-size: 0.875rem; | |
| 48 } | |
| 49 | |
| 50 #passwordError[aria-hidden="false"] { | |
| 51 display: block; | |
| 52 } | |
| 53 | |
| 54 #passwordSuccess { | |
| 55 display: none; | |
| 56 padding: 0.6rem 0.75rem; | |
| 57 border-radius: 6px; | |
| 58 background: color-mix(in srgb, var(--zenbu-sys-color-success, #27ae60) 12%, transparent); | |
| 59 color: var(--zenbu-sys-color-success, #27ae60); | |
| 60 font-size: 0.875rem; | |
| 61 } | |
| 62 | |
| 63 #passwordSuccess[aria-hidden="false"] { | |
| 64 display: block; | |
| 65 } | |
| 66 </style> | |
| 67 </head> | |
| 68 <body> | |
| 69 {{/parts/header.html}} | |
| 70 | |
| 71 <main> | |
| 72 <div class="password-card"> | |
| 73 <zen-heading size="xl"> | |
| 74 <h2>Change password</h2> | |
| 75 </zen-heading> | |
| 76 | |
| 77 <div id="passwordError" role="alert" aria-live="assertive" aria-hidden="true"></div> | |
| 78 <div id="passwordSuccess" role="status" aria-live="polite" aria-hidden="true"></div> | |
| 79 | |
| 80 <form class="password-form" id="passwordForm" novalidate> | |
| 81 <zen-field size="md"> | |
| 82 <label for="currentPassword">Current password</label> | |
| 83 <input | |
| 84 id="currentPassword" | |
| 85 name="currentPassword" | |
| 86 type="password" | |
| 87 autocomplete="current-password" | |
| 88 required | |
| 89 > | |
| 90 </zen-field> | |
| 91 | |
| 92 <zen-field size="md"> | |
| 93 <label for="newPassword">New password</label> | |
| 94 <input | |
| 95 id="newPassword" | |
| 96 name="newPassword" | |
| 97 type="password" | |
| 98 autocomplete="new-password" | |
| 99 required | |
| 100 minlength="12" | |
| 101 maxlength="1024" | |
| 102 > | |
| 103 </zen-field> | |
| 104 | |
| 105 <zen-field size="md"> | |
| 106 <label for="confirmPassword">Confirm new password</label> | |
| 107 <input | |
| 108 id="confirmPassword" | |
| 109 name="confirmPassword" | |
| 110 type="password" | |
| 111 autocomplete="new-password" | |
| 112 required | |
| 113 minlength="12" | |
| 114 maxlength="1024" | |
| 115 > | |
| 116 </zen-field> | |
| 117 | |
| 118 <div class="password-actions"> | |
| 119 <zen-button size="md"> | |
| 120 <button type="submit" id="passwordSubmit">Change password</button> | |
| 121 </zen-button> | |
| 122 </div> | |
| 123 </form> | |
| 124 </div> | |
| 125 </main> | |
| 126 | |
| 127 <script> | |
| 128 (function () { | |
| 129 'use strict'; | |
| 130 | |
| 131 const form = document.getElementById('passwordForm'); | |
| 132 const errorEl = document.getElementById('passwordError'); | |
| 133 const successEl = document.getElementById('passwordSuccess'); | |
| 134 const submitBtn = document.getElementById('passwordSubmit'); | |
| 135 | |
| 136 function showError(message) { | |
| 137 errorEl.textContent = message; | |
| 138 errorEl.setAttribute('aria-hidden', 'false'); | |
| 139 successEl.setAttribute('aria-hidden', 'true'); | |
| 140 } | |
| 141 | |
| 142 function showSuccess(message) { | |
| 143 successEl.textContent = message; | |
| 144 successEl.setAttribute('aria-hidden', 'false'); | |
| 145 errorEl.setAttribute('aria-hidden', 'true'); | |
| 146 } | |
| 147 | |
| 148 function hideMessages() { | |
| 149 errorEl.textContent = ''; | |
| 150 errorEl.setAttribute('aria-hidden', 'true'); | |
| 151 successEl.textContent = ''; | |
| 152 successEl.setAttribute('aria-hidden', 'true'); | |
| 153 } | |
| 154 | |
| 155 async function fetchSession() { | |
| 156 const resp = await fetch('/api/auth/session', { | |
| 157 credentials: 'same-origin', | |
| 158 }); | |
| 159 if (!resp.ok) throw new Error('session unavailable'); | |
| 160 return resp.json(); | |
| 161 } | |
| 162 | |
| 163 form.addEventListener('submit', async function (evt) { | |
| 164 evt.preventDefault(); | |
| 165 hideMessages(); | |
| 166 | |
| 167 const currentPassword = form.currentPassword.value; | |
| 168 const newPassword = form.newPassword.value; | |
| 169 const confirmPassword = form.confirmPassword.value; | |
| 170 | |
| 171 if (!currentPassword) { | |
| 172 showError('Please enter your current password.'); | |
| 173 return; | |
| 174 } | |
| 175 if (!newPassword || newPassword.length < 12) { | |
| 176 showError('New password must be at least 12 characters.'); | |
| 177 return; | |
| 178 } | |
| 179 if (newPassword !== confirmPassword) { | |
| 180 showError('New passwords do not match.'); | |
| 181 return; | |
| 182 } | |
| 183 | |
| 184 submitBtn.disabled = true; | |
| 185 | |
| 186 try { | |
| 187 const session = await fetchSession(); | |
| 188 if (session.kind !== 'user') { | |
| 189 showError('You must be signed in to change your password.'); | |
| 190 return; | |
| 191 } | |
| 192 | |
| 193 const csrfToken = session.csrfToken; | |
| 194 | |
| 195 const resp = await fetch('/api/auth/password', { | |
| 196 method: 'POST', | |
| 197 headers: { | |
| 198 'Content-Type': 'application/json', | |
| 199 'Origin': window.location.origin, | |
| 200 }, | |
| 201 credentials: 'same-origin', | |
| 202 body: JSON.stringify({ currentPassword, newPassword, csrfToken }), | |
| 203 }); | |
| 204 | |
| 205 const data = await resp.json(); | |
| 206 | |
| 207 if (!resp.ok) { | |
| 208 if (resp.status === 401) { | |
| 209 showError('Current password is incorrect.'); | |
| 210 } else if (resp.status === 400 && data.error && | |
| 211 data.error.code === 'password_policy') { | |
| 212 showError(data.error.message || | |
| 213 'New password must be at least 12 characters.'); | |
| 214 } else { | |
| 215 showError('Password change failed. Please try again.'); | |
| 216 } | |
| 217 return; | |
| 218 } | |
| 219 | |
| 220 showSuccess('Password changed successfully.'); | |
| 221 form.reset(); | |
| 222 | |
| 223 /* Redirect to home after a brief delay */ | |
| 224 setTimeout(function () { | |
| 225 window.location.href = '/jrpg'; | |
| 226 }, 1500); | |
| 227 | |
| 228 } catch (_) { | |
| 229 showError('Password change failed. Please try again.'); | |
| 230 } finally { | |
| 231 submitBtn.disabled = false; | |
| 232 } | |
| 233 }); | |
| 234 })(); | |
| 235 </script> | |
| 236 </body> | |
| 237 </html> |