1import { html } from '../../shared/lib/html.js'; 2 3export const title = 'Account Settings - Bolt Scribe'; 4 5export default (html, state, actions) => { 6 // Bind forms via addEventListener with STATIC inputs (no signal-bound 7 // value=). A signal-bound value= fights the browser: any re-render resets 8 // the field to the signal's value, so each keystroke gets wiped. Reading 9 // input.value on submit is what reliably works (same pattern as 10 // register/login). Both the change-password form and the delete-account 11 // confirmation are bound here in a single microtask. 12 if (typeof window !== 'undefined') { 13 window.queueMicrotask(() => { 14 const article = document.querySelector('[data-page="settings"]'); 15 if (!article) return; 16 17 // ---------------- Change password ---------------- 18 const form = article.querySelector('form[data-form="change-password"]'); 19 if (form && !form.hasAttribute('data-bound')) { 20 form.setAttribute('data-bound', 'true'); 21 22 const currentInput = form.querySelector('#currentPassword'); 23 const newInput = form.querySelector('#newPassword'); 24 const confirmInput = form.querySelector('#confirmPassword'); 25 const submitBtn = form.querySelector('button[type="submit"]'); 26 const errorHost = article.querySelector('[data-errors="change-password"]'); 27 28 const errorMessages = { 29 required_field: 'Please fill in all fields.', 30 password_too_short: 'New password must be at least 8 characters.', 31 passwords_mismatch: 'New passwords do not match.', 32 invalid_password: 'Current password is incorrect.', 33 network_error: 'Network error. Please try again.', 34 generic_error: 'Something went wrong. Please try again.', 35 }; 36 37 const showError = (key) => { 38 errorHost.innerHTML = key 39 ? `<div class="alert alert-error">${errorMessages[key] || errorMessages.generic_error}</div>` 40 : ''; 41 }; 42 43 const setLoading = (loading) => { 44 submitBtn.disabled = loading; 45 submitBtn.textContent = loading ? 'Changing...' : 'Change Password'; 46 [currentInput, newInput, confirmInput].forEach((i) => { if (i) i.disabled = loading; }); 47 }; 48 49 form.addEventListener('submit', async (e) => { 50 e.preventDefault(); 51 showError(null); 52 53 const current = currentInput.value; 54 const next = newInput.value; 55 const conf = confirmInput.value; 56 57 if (!current || !next || !conf) return showError('required_field'); 58 if (next.length < 8) return showError('password_too_short'); 59 if (next !== conf) return showError('passwords_mismatch'); 60 61 setLoading(true); 62 try { 63 const response = await fetch('/api/auth/change-password', { 64 method: 'POST', 65 headers: { 'Content-Type': 'application/json' }, 66 body: JSON.stringify({ currentPassword: current, newPassword: next }), 67 }); 68 if (response.ok) { 69 currentInput.value = ''; 70 newInput.value = ''; 71 confirmInput.value = ''; 72 errorHost.innerHTML = '<div class="alert alert-success">Password changed successfully.</div>'; 73 } else { 74 const result = await response.json().catch(() => ({})); 75 showError(result.error?.code === 'invalid_password' ? 'invalid_password' : 'generic_error'); 76 } 77 } catch (err) { 78 console.error('Change password error:', err); 79 showError('network_error'); 80 } finally { 81 setLoading(false); 82 } 83 }); 84 } 85 86 // ---------------- Delete account ---------------- 87 const deleteSection = article.querySelector('[data-section="delete-account"]'); 88 if (deleteSection && !deleteSection.hasAttribute('data-bound')) { 89 deleteSection.setAttribute('data-bound', 'true'); 90 91 const revealBtn = deleteSection.querySelector('[data-action="reveal-delete"]'); 92 const confirmPanel = deleteSection.querySelector('[data-panel="delete-confirm"]'); 93 const deletePasswordInput = confirmPanel?.querySelector('#deletePassword'); 94 const deleteConfirmInput = confirmPanel?.querySelector('#deleteConfirmWord'); 95 const deleteBtn = confirmPanel?.querySelector('button[data-action="confirm-delete"]'); 96 const cancelBtn = confirmPanel?.querySelector('button[data-action="cancel-delete"]'); 97 const deleteErrorHost = deleteSection.querySelector('[data-errors="delete-account"]'); 98 99 // The confirmation word is the user's email (strong intent signal, 100 // already known to the authenticated user). Lowercased for a 101 // forgiving comparison. 102 const confirmWord = (state.auth?.user?.email || '').toLowerCase(); 103 const confirmLabel = confirmWord || 'your email'; 104 105 const setDeleteError = (msg) => { 106 deleteErrorHost.innerHTML = msg 107 ? `<div class="alert alert-error">${msg}</div>` 108 : ''; 109 }; 110 111 const setDeleting = (deleting) => { 112 deleteBtn.disabled = deleting; 113 deleteBtn.textContent = deleting ? 'Deleting...' : 'Delete My A
113ccount'; 114 if (deletePasswordInput) deletePasswordInput.disabled = deleting; 115 if (deleteConfirmInput) deleteConfirmInput.disabled = deleting; 116 }; 117 118 // Reveal the confirmation panel. 119 const openConfirm = () => { 120 revealBtn.hidden = true; 121 confirmPanel.hidden = false; 122 // Stamp the dynamic confirmation-word label in the UI. 123 const wordLabel = confirmPanel.querySelector('[data-confirm-word]'); 124 if (wordLabel) wordLabel.textContent = confirmLabel; 125 if (deletePasswordInput) deletePasswordInput.focus(); 126 }; 127 128 const closeConfirm = () => { 129 confirmPanel.hidden = true; 130 revealBtn.hidden = false; 131 if (deletePasswordInput) deletePasswordInput.value = ''; 132 if (deleteConfirmInput) deleteConfirmInput.value = ''; 133 setDeleteError(''); 134 }; 135 136 if (revealBtn) revealBtn.addEventListener('click', openConfirm); 137 if (cancelBtn) cancelBtn.addEventListener('click', closeConfirm); 138 139 // Enable the final delete button only when the password is non-empty 140 // AND the confirmation word matches the user's email. 141 const updateDeleteEnabled = () => { 142 const pw = deletePasswordInput?.value || ''; 143 const word = (deleteConfirmInput?.value || '').toLowerCase(); 144 if (deleteBtn) { 145 deleteBtn.disabled = !pw || (confirmWord ? word !== confirmWord : !word); 146 } 147 }; 148 deletePasswordInput?.addEventListener('input', updateDeleteEnabled); 149 deleteConfirmInput?.addEventListener('input', updateDeleteEnabled); 150 151 if (deleteBtn) { 152 deleteBtn.addEventListener('click', async (e) => { 153 e.preventDefault(); 154 setDeleteError(''); 155 156 const password = deletePasswordInput.value; 157 const word = (deleteConfirmInput.value || '').toLowerCase(); 158 if (!password) return setDeleteError('Enter your password.'); 159 if (confirmWord && word !== confirmWord) { 160 return setDeleteError(`Type ${confirmLabel} to confirm.`); 161 } 162 163 setDeleting(true); 164 try { 165 await actions.deleteAccount(password); 166 // On success, actions.deleteAccount logs out and redirects to '/', 167 // so the code below normally doesn't run. 168 } catch (err) { 169 console.error('Delete account error:', err); 170 setDeleting(false); 171 // 401 (wrong password) or any failure â uniform message, do not 172 // leak whether the account exists. 173 const msg = err?.message?.includes('Unauthorized') || err?.message?.includes('Invalid') 174 ? 'Password is incorrect.' 175 : 'Could not delete the account. Please try again.'; 176 setDeleteError(msg); 177 } 178 }); 179 } 180 } 181 }); 182 } 183 184 return html` 185 <main> 186 <article data-page="settings"> 187 <header> 188 <h1>Account Settings</h1> 189 <p>Manage your account preferences and security</p> 190 </header> 191 192 <nav> 193 <a href="/dashboard">Back to Dashboard</a> 194 <a href="/subscription">Subscription</a> 195 </nav> 196 197 <section> 198 <h2>Change Password</h2> 199 200 <div data-errors="change-password"></div> 201 202 <form data-form="change-password"> 203 <div class="form-group"> 204 <label class="form-label" for="currentPassword">Current Password</label> 205 <input type="password" id="currentPassword" name="currentPassword" class="form-input" required autocomplete="current-password" /> 206 </div> 207 208 <div class="form-group"> 209 <label class="form-label" for="newPassword">New Password</label> 210 <input type="password" id="newPassword" name="newPassword" class="form-input" required minlength="8" autocomplete="new-password" /> 211 <span class="form-hint">12+ characters, or 8+ with mixed case and numbers</span> 212 </div> 213 214 <div class="form-group"> 215 <label class="form-label" for="confirmPassword">Confirm New Password</label> 216 <input type="password" id="confirmPassword" name="confirmPassword" class="form-input" required minlength="8" autocomplete="new-password" /> 217 </div> 218 219 <button type="submit" class="btn btn-primary">Change Password</button> 220 </form> 221 </section> 222 223 <section data-section="delete-account"> 224 <h2>Danger Zone</h2> 225 <p>Deleting your account is permanent. It removes your profile, usage history, templates, and subscription. Stripe billing records may be retained by Stripe per their own policy. This cannot be undone.</p> 226 227 <div data-errors="delete-account"></div> 228 229 <button type="button" class="btn btn-danger" data-action="reveal-delete">Delete account</button> 230 231 <div data-panel="delete-confirm" hidden> 232 <p>To confirm, enter your password and type <strong data-confirm-word>your email</strong> below.</p> 233 234 <div class="form-group"> 235 <label class="form-label" for="deletePassword">Password</label> 236 <input type="password" id="deletePassword" name="deletePassword" class="form-input" autocomplete="current-password" /> 237 </div> 238 239 <div class="form-group"> 240 <label class="form-label" for="deleteConfirmWord">Type your email to confirm</label> 241 <input type="text" id="deleteConfirmWord" name="deleteConfirmWord" class="form-input" autocomplete="off" /> 242 </div> 243 244 <button type="button" class="btn btn-danger" data-action="confirm-delete" disabled>
244Delete My Account</button> 245 <button type="button" class="btn" data-action="cancel-delete">Cancel</button> 246 </div> 247 </section> 248 </article> 249 </main> 250 `; 251};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.