this repo has no description
1<script lang="ts">
2 import { getAuthState, logout } from '../lib/auth.svelte'
3 import { navigate } from '../lib/router.svelte'
4 import { api, ApiError } from '../lib/api'
5
6 const auth = getAuthState()
7
8 let message = $state<{ type: 'success' | 'error'; text: string } | null>(null)
9
10 let emailLoading = $state(false)
11 let newEmail = $state('')
12 let emailToken = $state('')
13 let emailTokenRequired = $state(false)
14
15 let handleLoading = $state(false)
16 let newHandle = $state('')
17
18 let deleteLoading = $state(false)
19 let deletePassword = $state('')
20 let deleteToken = $state('')
21 let deleteTokenSent = $state(false)
22
23 $effect(() => {
24 if (!auth.loading && !auth.session) {
25 navigate('/login')
26 }
27 })
28
29 function showMessage(type: 'success' | 'error', text: string) {
30 message = { type, text }
31 setTimeout(() => {
32 if (message?.text === text) message = null
33 }, 5000)
34 }
35
36 async function handleRequestEmailUpdate(e: Event) {
37 e.preventDefault()
38 if (!auth.session || !newEmail) return
39
40 emailLoading = true
41 message = null
42
43 try {
44 const result = await api.requestEmailUpdate(auth.session.accessJwt)
45 emailTokenRequired = result.tokenRequired
46 if (emailTokenRequired) {
47 showMessage('success', 'Verification code sent to your current email')
48 } else {
49 await api.updateEmail(auth.session.accessJwt, newEmail)
50 showMessage('success', 'Email updated successfully')
51 newEmail = ''
52 }
53 } catch (e) {
54 showMessage('error', e instanceof ApiError ? e.message : 'Failed to update email')
55 } finally {
56 emailLoading = false
57 }
58 }
59
60 async function handleConfirmEmailUpdate(e: Event) {
61 e.preventDefault()
62 if (!auth.session || !newEmail || !emailToken) return
63
64 emailLoading = true
65 message = null
66
67 try {
68 await api.updateEmail(auth.session.accessJwt, newEmail, emailToken)
69 showMessage('success', 'Email updated successfully')
70 newEmail = ''
71 emailToken = ''
72 emailTokenRequired = false
73 } catch (e) {
74 showMessage('error', e instanceof ApiError ? e.message : 'Failed to update email')
75 } finally {
76 emailLoading = false
77 }
78 }
79
80 async function handleUpdateHandle(e: Event) {
81 e.preventDefault()
82 if (!auth.session || !newHandle) return
83
84 handleLoading = true
85 message = null
86
87 try {
88 await api.updateHandle(auth.session.accessJwt, newHandle)
89 showMessage('success', 'Handle updated successfully')
90 newHandle = ''
91 } catch (e) {
92 showMessage('error', e instanceof ApiError ? e.message : 'Failed to update handle')
93 } finally {
94 handleLoading = false
95 }
96 }
97
98 async function handleRequestDelete() {
99 if (!auth.session) return
100
101 deleteLoading = true
102 message = null
103
104 try {
105 await api.requestAccountDelete(auth.session.accessJwt)
106 deleteTokenSent = true
107 showMessage('success', 'Deletion confirmation sent to your email')
108 } catch (e) {
109 showMessage('error', e instanceof ApiError ? e.message : 'Failed to request deletion')
110 } finally {
111 deleteLoading = false
112 }
113 }
114
115 async function handleConfirmDelete(e: Event) {
116 e.preventDefault()
117 if (!auth.session || !deletePassword || !deleteToken) return
118
119 if (!confirm('Are you absolutely sure you want to delete your account? This cannot be undone.')) {
120 return
121 }
122
123 deleteLoading = true
124 message = null
125
126 try {
127 await api.deleteAccount(auth.session.did, deletePassword, deleteToken)
128 await logout()
129 navigate('/login')
130 } catch (e) {
131 showMessage('error', e instanceof ApiError ? e.message : 'Failed to delete account')
132 } finally {
133 deleteLoading = false
134 }
135 }
136</script>
137
138<div class="page">
139 <header>
140 <a href="#/dashboard" class="back">← Dashboard</a>
141 <h1>Account Settings</h1>
142 </header>
143
144 {#if message}
145 <div class="message {message.type}">{message.text}</div>
146 {/if}
147
148 <section>
149 <h2>Change Email</h2>
150 {#if auth.session?.email}
151 <p class="current">Current: {auth.session.email}</p>
152 {/if}
153
154 {#if emailTokenRequired}
155 <form onsubmit={handleConfirmEmailUpdate}>
156 <div class="field">
157 <label for="email-token">Verification Code</label>
158 <input
159 id="email-token"
160 type="text"
161 bind:value={emailToken}
162 placeholder="Enter code from email"
163 disabled={emailLoading}
164 required
165 />
166 </div>
167 <div class="actions">
168 <button type="submit" disabled={emailLoading || !emailToken}>
169 {emailLoading ? 'Updating...' : 'Confirm Email Change'}
170 </button>
171 <button type="button" class="secondary" onclick={() => { emailTokenRequired = false; emailToken = '' }}>
172 Cancel
173 </button>
174 </div>
175 </form>
176 {:else}
177 <form onsubmit={handleRequestEmailUpdate}>
178 <div class="field">
179 <label for="new-email">New Email</label>
180 <input
181 id="new-email"
182 type="email"
183 bind:value={newEmail}
184 placeholder="new@example.com"
185 disabled={emailLoading}
186 required
187 />
188 </div>
189 <button type="submit" disabled={emailLoading || !newEmail}>
190 {emailLoading ? 'Requesting...' : 'Change Email'}
191 </button>
192 </form>
193 {/if}
194 </section>
195
196 <section>
197 <h2>Change Handle</h2>
198 {#if auth.session}
199 <p class="current">Current: @{auth.session.handle}</p>
200 {/if}
201
202 <form onsubmit={handleUpdateHandle}>
203 <div class="field">
204 <label for="new-handle">New Handle</label>
205 <input
206 id="new-handle"
207 type="text"
208 bind:value={newHandle}
209 placeholder="newhandle.bsky.social"
210 disabled={handleLoading}
211 required
212 />
213 </div>
214 <button type="submit" disabled={handleLoading || !newHandle}>
215 {handleLoading ? 'Updating...' : 'Change Handle'}
216 </button>
217 </form>
218 </section>
219
220 <section class="danger-zone">
221 <h2>Delete Account</h2>
222 <p class="warning">This action is irreversible. All your data will be permanently deleted.</p>
223
224 {#if deleteTokenSent}
225 <form onsubmit={handleConfirmDelete}>
226 <div class="field">
227 <label for="delete-token">Confirmation Code (from email)</label>
228 <input
229 id="delete-token"
230 type="text"
231 bind:value={deleteToken}
232 placeholder="Enter confirmation code"
233 disabled={deleteLoading}
234 required
235 />
236 </div>
237 <div class="field">
238 <label for="delete-password">Your Password</label>
239 <input
240 id="delete-password"
241 type="password"
242 bind:value={deletePassword}
243 placeholder="Enter your password"
244 disabled={deleteLoading}
245 required
246 />
247 </div>
248 <div class="actions">
249 <button type="submit" class="danger" disabled={deleteLoading || !deleteToken || !deletePassword}>
250 {deleteLoading ? 'Deleting...' : 'Permanently Delete Account'}
251 </button>
252 <button type="button" class="secondary" onclick={() => { deleteTokenSent = false; deleteToken = ''; deletePassword = '' }}>
253 Cancel
254 </button>
255 </div>
256 </form>
257 {:else}
258 <button class="danger" onclick={handleRequestDelete} disabled={deleteLoading}>
259 {deleteLoading ? 'Requesting...' : 'Request Account Deletion'}
260 </button>
261 {/if}
262 </section>
263</div>
264
265<style>
266 .page {
267 max-width: 600px;
268 margin: 0 auto;
269 padding: 2rem;
270 }
271
272 header {
273 margin-bottom: 2rem;
274 }
275
276 .back {
277 color: var(--text-secondary);
278 text-decoration: none;
279 font-size: 0.875rem;
280 }
281
282 .back:hover {
283 color: var(--accent);
284 }
285
286 h1 {
287 margin: 0.5rem 0 0 0;
288 }
289
290 .message {
291 padding: 0.75rem;
292 border-radius: 4px;
293 margin-bottom: 1rem;
294 }
295
296 .message.success {
297 background: var(--success-bg);
298 border: 1px solid var(--success-border);
299 color: var(--success-text);
300 }
301
302 .message.error {
303 background: var(--error-bg);
304 border: 1px solid var(--error-border);
305 color: var(--error-text);
306 }
307
308 section {
309 padding: 1.5rem;
310 background: var(--bg-secondary);
311 border-radius: 8px;
312 margin-bottom: 1.5rem;
313 }
314
315 section h2 {
316 margin: 0 0 0.5rem 0;
317 font-size: 1.125rem;
318 }
319
320 .current {
321 color: var(--text-secondary);
322 font-size: 0.875rem;
323 margin-bottom: 1rem;
324 }
325
326 .field {
327 margin-bottom: 1rem;
328 }
329
330 label {
331 display: block;
332 font-size: 0.875rem;
333 font-weight: 500;
334 margin-bottom: 0.25rem;
335 }
336
337 input {
338 width: 100%;
339 padding: 0.75rem;
340 border: 1px solid var(--border-color-light);
341 border-radius: 4px;
342 font-size: 1rem;
343 box-sizing: border-box;
344 background: var(--bg-input);
345 color: var(--text-primary);
346 }
347
348 input:focus {
349 outline: none;
350 border-color: var(--accent);
351 }
352
353 button {
354 padding: 0.75rem 1.5rem;
355 background: var(--accent);
356 color: white;
357 border: none;
358 border-radius: 4px;
359 cursor: pointer;
360 font-size: 1rem;
361 }
362
363 button:hover:not(:disabled) {
364 background: var(--accent-hover);
365 }
366
367 button:disabled {
368 opacity: 0.6;
369 cursor: not-allowed;
370 }
371
372 button.secondary {
373 background: transparent;
374 color: var(--text-secondary);
375 border: 1px solid var(--border-color-light);
376 }
377
378 button.secondary:hover:not(:disabled) {
379 background: var(--bg-secondary);
380 }
381
382 button.danger {
383 background: var(--error-text);
384 }
385
386 button.danger:hover:not(:disabled) {
387 background: #900;
388 }
389
390 .actions {
391 display: flex;
392 gap: 0.5rem;
393 }
394
395 .danger-zone {
396 background: var(--error-bg);
397 border: 1px solid var(--error-border);
398 }
399
400 .danger-zone h2 {
401 color: var(--error-text);
402 }
403
404 .warning {
405 color: var(--error-text);
406 font-size: 0.875rem;
407 margin-bottom: 1rem;
408 }
409</style>