PageSourceSearch

https://boltscribe.ink/assets/views/settings.js

js boltscribe.ink collected 2026-10-02 01:57:14 UTC 11,217 bytes, 251 lines download raw bytes

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.