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