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>