this repo has no description
1<script lang="ts">
2 import { getAuthState, logout, switchAccount } from '../lib/auth.svelte'
3 import { navigate } from '../lib/router.svelte'
4 import { _ } from '../lib/i18n'
5 import { api } from '../lib/api'
6 import { onMount } from 'svelte'
7
8 const auth = getAuthState()
9 let dropdownOpen = $state(false)
10 let switching = $state(false)
11 let inviteCodesEnabled = $state(false)
12
13 const isDidWeb = $derived(auth.session?.did?.startsWith('did:web:') ?? false)
14
15 onMount(async () => {
16 try {
17 const serverInfo = await api.describeServer()
18 inviteCodesEnabled = serverInfo.inviteCodeRequired
19 } catch {
20 inviteCodesEnabled = false
21 }
22 })
23
24 $effect(() => {
25 if (!auth.loading && !auth.session) {
26 navigate('/login')
27 }
28 })
29
30 async function handleLogout() {
31 await logout()
32 navigate('/login')
33 }
34
35 async function handleSwitchAccount(did: string) {
36 switching = true
37 dropdownOpen = false
38 try {
39 await switchAccount(did)
40 } catch {
41 navigate('/login')
42 } finally {
43 switching = false
44 }
45 }
46
47 function toggleDropdown() {
48 dropdownOpen = !dropdownOpen
49 }
50
51 function closeDropdown(e: MouseEvent) {
52 const target = e.target as HTMLElement
53 if (!target.closest('.account-dropdown')) {
54 dropdownOpen = false
55 }
56 }
57
58 $effect(() => {
59 if (dropdownOpen) {
60 document.addEventListener('click', closeDropdown)
61 return () => document.removeEventListener('click', closeDropdown)
62 }
63 })
64
65 let otherAccounts = $derived(
66 auth.savedAccounts.filter(a => a.did !== auth.session?.did)
67 )
68</script>
69
70{#if auth.session}
71 <div class="dashboard">
72 <header>
73 <h1>{$_('dashboard.title')}</h1>
74 <div class="account-dropdown">
75 <button class="account-trigger" onclick={toggleDropdown} disabled={switching}>
76 <span class="account-handle">@{auth.session.handle}</span>
77 <span class="dropdown-arrow">{dropdownOpen ? '▲' : '▼'}</span>
78 </button>
79 {#if dropdownOpen}
80 <div class="dropdown-menu">
81 {#if otherAccounts.length > 0}
82 <div class="dropdown-section">
83 <span class="dropdown-label">{$_('dashboard.switchAccount')}</span>
84 {#each otherAccounts as account}
85 <button type="button" class="dropdown-item" onclick={() => handleSwitchAccount(account.did)}>
86 @{account.handle}
87 </button>
88 {/each}
89 </div>
90 <div class="dropdown-divider"></div>
91 {/if}
92 <button type="button" class="dropdown-item" onclick={() => { dropdownOpen = false; navigate('/login') }}>
93 {$_('dashboard.addAnotherAccount')}
94 </button>
95 <div class="dropdown-divider"></div>
96 <button type="button" class="dropdown-item logout-item" onclick={handleLogout}>
97 {$_('dashboard.signOut', { values: { handle: auth.session.handle } })}
98 </button>
99 </div>
100 {/if}
101 </div>
102 </header>
103
104 {#if auth.session.status === 'migrated'}
105 <div class="migrated-banner">
106 <strong>{$_('dashboard.migratedTitle')}</strong>
107 <p>{$_('dashboard.migratedMessage', { values: { pds: auth.session.migratedToPds || 'another PDS' } })}</p>
108 </div>
109 {:else if auth.session.status === 'deactivated' || auth.session.active === false}
110 <div class="deactivated-banner">
111 <strong>{$_('dashboard.deactivatedTitle')}</strong>
112 <p>{$_('dashboard.deactivatedMessage')}</p>
113 </div>
114 {/if}
115
116 <section class="account-overview">
117 <h2>{$_('dashboard.accountOverview')}</h2>
118 <dl>
119 <dt>{$_('dashboard.handle')}</dt>
120 <dd>
121 @{auth.session.handle}
122 {#if auth.session.isAdmin}
123 <span class="badge admin">{$_('dashboard.admin')}</span>
124 {/if}
125 {#if auth.session.status === 'migrated'}
126 <span class="badge migrated">{$_('dashboard.migrated')}</span>
127 {:else if auth.session.status === 'deactivated' || auth.session.active === false}
128 <span class="badge deactivated">{$_('dashboard.deactivated')}</span>
129 {/if}
130 </dd>
131 <dt>{$_('dashboard.did')}</dt>
132 <dd class="mono">{auth.session.did}</dd>
133 {#if auth.session.preferredChannel}
134 <dt>{$_('dashboard.primaryContact')}</dt>
135 <dd>
136 {#if auth.session.preferredChannel === 'email'}
137 {auth.session.email || $_('register.email')}
138 {:else if auth.session.preferredChannel === 'discord'}
139 {$_('register.discord')}
140 {:else if auth.session.preferredChannel === 'telegram'}
141 {$_('register.telegram')}
142 {:else if auth.session.preferredChannel === 'signal'}
143 {$_('register.signal')}
144 {:else}
145 {auth.session.preferredChannel}
146 {/if}
147 {#if auth.session.preferredChannelVerified}
148 <span class="badge success">{$_('dashboard.verified')}</span>
149 {:else}
150 <span class="badge warning">{$_('dashboard.unverified')}</span>
151 {/if}
152 </dd>
153 {:else if auth.session.email}
154 <dt>{$_('register.email')}</dt>
155 <dd>
156 {auth.session.email}
157 {#if auth.session.emailConfirmed}
158 <span class="badge success">{$_('dashboard.verified')}</span>
159 {:else}
160 <span class="badge warning">{$_('dashboard.unverified')}</span>
161 {/if}
162 </dd>
163 {/if}
164 </dl>
165 </section>
166
167 <nav class="nav-grid">
168 {#if auth.session.status === 'migrated'}
169 <a href="/app/did-document" class="nav-card migrated-card">
170 <h3>{$_('dashboard.navDidDocument')}</h3>
171 <p>{$_('dashboard.navDidDocumentDesc')}</p>
172 </a>
173 <a href="/app/sessions" class="nav-card">
174 <h3>{$_('dashboard.navSessions')}</h3>
175 <p>{$_('dashboard.navSessionsDesc')}</p>
176 </a>
177 <a href="/app/security" class="nav-card">
178 <h3>{$_('dashboard.navSecurity')}</h3>
179 <p>{$_('dashboard.navSecurityDesc')}</p>
180 </a>
181 <a href="/app/settings" class="nav-card">
182 <h3>{$_('dashboard.navSettings')}</h3>
183 <p>{$_('dashboard.navSettingsDesc')}</p>
184 </a>
185 <a href="/app/migrate" class="nav-card">
186 <h3>{$_('dashboard.navMigrateAgain')}</h3>
187 <p>{$_('dashboard.navMigrateAgainDesc')}</p>
188 </a>
189 {:else}
190 <a href="/app/app-passwords" class="nav-card">
191 <h3>{$_('dashboard.navAppPasswords')}</h3>
192 <p>{$_('dashboard.navAppPasswordsDesc')}</p>
193 </a>
194 <a href="/app/sessions" class="nav-card">
195 <h3>{$_('dashboard.navSessions')}</h3>
196 <p>{$_('dashboard.navSessionsDesc')}</p>
197 </a>
198 {#if inviteCodesEnabled && auth.session.isAdmin}
199 <a href="/app/invite-codes" class="nav-card">
200 <h3>{$_('dashboard.navInviteCodes')}</h3>
201 <p>{$_('dashboard.navInviteCodesDesc')}</p>
202 </a>
203 {/if}
204 <a href="/app/settings" class="nav-card">
205 <h3>{$_('dashboard.navSettings')}</h3>
206 <p>{$_('dashboard.navSettingsDesc')}</p>
207 </a>
208 <a href="/app/security" class="nav-card">
209 <h3>{$_('dashboard.navSecurity')}</h3>
210 <p>{$_('dashboard.navSecurityDesc')}</p>
211 </a>
212 <a href="/app/comms" class="nav-card">
213 <h3>{$_('dashboard.navComms')}</h3>
214 <p>{$_('dashboard.navCommsDesc')}</p>
215 </a>
216 <a href="/app/repo" class="nav-card">
217 <h3>{$_('dashboard.navRepo')}</h3>
218 <p>{$_('dashboard.navRepoDesc')}</p>
219 </a>
220 <a href="/app/controllers" class="nav-card">
221 <h3>{$_('dashboard.navDelegation')}</h3>
222 <p>{$_('dashboard.navDelegationDesc')}</p>
223 </a>
224 {#if isDidWeb}
225 <a href="/app/did-document" class="nav-card did-web-card">
226 <h3>{$_('dashboard.navDidDocument')}</h3>
227 <p>{$_('dashboard.navDidDocumentDescActive')}</p>
228 </a>
229 {/if}
230 <a href="/app/migrate" class="nav-card">
231 <h3>{$_('migration.navTitle')}</h3>
232 <p>{$_('migration.navDesc')}</p>
233 </a>
234 {#if auth.session.isAdmin}
235 <a href="/app/admin" class="nav-card admin-card">
236 <h3>{$_('dashboard.navAdmin')}</h3>
237 <p>{$_('dashboard.navAdminDesc')}</p>
238 </a>
239 {/if}
240 {/if}
241 </nav>
242 </div>
243{:else if auth.loading}
244 <div class="loading">{$_('common.loading')}</div>
245{/if}
246
247<style>
248 .dashboard {
249 max-width: var(--width-xl);
250 margin: 0 auto;
251 padding: var(--space-7);
252 }
253
254 header {
255 display: flex;
256 justify-content: space-between;
257 align-items: center;
258 margin-bottom: var(--space-7);
259 }
260
261 header h1 {
262 margin: 0;
263 }
264
265 .account-dropdown {
266 position: relative;
267 }
268
269 .account-trigger {
270 display: flex;
271 align-items: center;
272 gap: var(--space-3);
273 padding: var(--space-3) var(--space-5);
274 background: transparent;
275 border: 1px solid var(--border-color);
276 border-radius: var(--radius-md);
277 cursor: pointer;
278 color: var(--text-primary);
279 }
280
281 .account-trigger:hover:not(:disabled) {
282 background: var(--bg-secondary);
283 }
284
285 .account-trigger:disabled {
286 opacity: 0.6;
287 cursor: not-allowed;
288 }
289
290 .account-trigger .account-handle {
291 font-weight: var(--font-medium);
292 }
293
294 .dropdown-arrow {
295 font-size: 0.625rem;
296 color: var(--text-secondary);
297 }
298
299 .dropdown-menu {
300 position: absolute;
301 top: 100%;
302 right: 0;
303 margin-top: var(--space-2);
304 min-width: 200px;
305 background: var(--bg-card);
306 border: 1px solid var(--border-color);
307 border-radius: var(--radius-xl);
308 box-shadow: var(--shadow-lg);
309 z-index: 100;
310 overflow: hidden;
311 }
312
313 .dropdown-section {
314 padding: var(--space-3) 0;
315 }
316
317 .dropdown-label {
318 display: block;
319 padding: var(--space-2) var(--space-5);
320 font-size: var(--text-xs);
321 color: var(--text-muted);
322 text-transform: uppercase;
323 letter-spacing: 0.05em;
324 }
325
326 .dropdown-item {
327 display: block;
328 width: 100%;
329 padding: var(--space-4) var(--space-5);
330 background: transparent;
331 border: none;
332 text-align: left;
333 cursor: pointer;
334 color: var(--text-primary);
335 font-size: var(--text-sm);
336 }
337
338 .dropdown-item:hover {
339 background: var(--bg-secondary);
340 }
341
342 .dropdown-item.logout-item {
343 color: var(--error-text);
344 }
345
346 .dropdown-divider {
347 height: 1px;
348 background: var(--border-color);
349 margin: 0;
350 }
351
352 section {
353 background: var(--bg-secondary);
354 padding: var(--space-6);
355 border-radius: var(--radius-xl);
356 margin-bottom: var(--space-7);
357 }
358
359 section h2 {
360 margin: 0 0 var(--space-4) 0;
361 font-size: var(--text-xl);
362 }
363
364 dl {
365 display: grid;
366 grid-template-columns: auto 1fr;
367 gap: var(--space-3) var(--space-5);
368 margin: 0;
369 }
370
371 dt {
372 font-weight: var(--font-medium);
373 color: var(--text-secondary);
374 }
375
376 dd {
377 margin: 0;
378 }
379
380 .mono {
381 font-family: ui-monospace, monospace;
382 font-size: var(--text-sm);
383 word-break: break-all;
384 }
385
386 .badge {
387 display: inline-block;
388 padding: var(--space-1) var(--space-3);
389 border-radius: var(--radius-md);
390 font-size: var(--text-xs);
391 margin-left: var(--space-3);
392 }
393
394 .badge.success {
395 background: var(--success-bg);
396 color: var(--success-text);
397 }
398
399 .badge.warning {
400 background: var(--warning-bg);
401 color: var(--warning-text);
402 }
403
404 .badge.admin {
405 background: var(--accent);
406 color: var(--text-inverse);
407 }
408
409 .badge.deactivated {
410 background: var(--warning-bg);
411 color: var(--warning-text);
412 border: 1px solid var(--warning-border);
413 }
414
415 .badge.migrated {
416 background: var(--info-bg, #e0f2fe);
417 color: var(--info-text, #0369a1);
418 border: 1px solid var(--info-border, #7dd3fc);
419 }
420
421 .nav-grid {
422 display: grid;
423 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
424 gap: var(--space-4);
425 }
426
427 .nav-card {
428 display: block;
429 padding: var(--space-6);
430 background: var(--bg-card);
431 border: 1px solid var(--border-color);
432 border-radius: var(--radius-xl);
433 text-decoration: none;
434 color: inherit;
435 transition: border-color var(--transition-normal), box-shadow var(--transition-normal);
436 }
437
438 .nav-card:hover {
439 border-color: var(--accent);
440 box-shadow: 0 2px 8px var(--accent-muted);
441 }
442
443 .nav-card h3 {
444 margin: 0 0 var(--space-3) 0;
445 color: var(--accent);
446 }
447
448 .nav-card p {
449 margin: 0;
450 color: var(--text-secondary);
451 font-size: var(--text-sm);
452 }
453
454 .nav-card.admin-card {
455 border-color: var(--accent);
456 background: linear-gradient(135deg, var(--bg-card) 0%, var(--accent-muted) 100%);
457 }
458
459 .nav-card.admin-card:hover {
460 box-shadow: 0 2px 12px var(--accent-muted);
461 }
462
463 .loading {
464 text-align: center;
465 padding: var(--space-9);
466 color: var(--text-secondary);
467 }
468
469 .deactivated-banner {
470 background: var(--warning-bg);
471 border: 1px solid var(--warning-border);
472 border-radius: var(--radius-xl);
473 padding: var(--space-5) var(--space-6);
474 margin-bottom: var(--space-7);
475 }
476
477 .deactivated-banner strong {
478 color: var(--warning-text);
479 font-size: var(--text-base);
480 }
481
482 .deactivated-banner p {
483 margin: var(--space-3) 0 0 0;
484 color: var(--warning-text);
485 font-size: var(--text-sm);
486 }
487
488 .migrated-banner {
489 background: var(--info-bg, #e0f2fe);
490 border: 1px solid var(--info-border, #7dd3fc);
491 border-radius: var(--radius-xl);
492 padding: var(--space-5) var(--space-6);
493 margin-bottom: var(--space-7);
494 }
495
496 .migrated-banner strong {
497 color: var(--info-text, #0369a1);
498 font-size: var(--text-base);
499 }
500
501 .migrated-banner p {
502 margin: var(--space-3) 0 0 0;
503 color: var(--info-text, #0369a1);
504 font-size: var(--text-sm);
505 }
506
507 .nav-card.migrated-card {
508 border-color: var(--info-border, #7dd3fc);
509 background: linear-gradient(135deg, var(--bg-card) 0%, var(--info-bg, #e0f2fe) 100%);
510 }
511
512 .nav-card.migrated-card:hover {
513 box-shadow: 0 2px 12px var(--info-bg, #e0f2fe);
514 }
515
516 .nav-card.migrated-card h3 {
517 color: var(--info-text, #0369a1);
518 }
519
520 .nav-card.did-web-card {
521 border-color: var(--accent);
522 background: linear-gradient(135deg, var(--bg-card) 0%, var(--accent-muted) 100%);
523 }
524
525 .nav-card.did-web-card:hover {
526 box-shadow: 0 2px 12px var(--accent-muted);
527 }
528</style>