From 467be99da3f6937733926e32d1b7badef36b94e1 Mon Sep 17 00:00:00 2001 From: Lewis Date: Wed, 18 Mar 2026 18:36:16 +0200 Subject: [PATCH] refactor(frontend): extract dashboard sections --- .../dashboard/AccountOverview.svelte | 60 ---- .../components/dashboard/AdminContent.svelte | 77 ++--- .../dashboard/ControllersContent.svelte | 28 +- .../dashboard/MigrationContent.svelte | 53 ---- .../dashboard/PasskeySection.svelte | 166 +++++++++++ .../dashboard/PasswordSection.svelte | 265 +++++++++++++++++ .../dashboard/SessionsContent.svelte | 8 +- .../components/dashboard/TotpSection.svelte | 279 ++++++++++++++++++ frontend/src/styles/dashboard.css | 47 ++- 9 files changed, 765 insertions(+), 218 deletions(-) delete mode 100644 frontend/src/components/dashboard/AccountOverview.svelte delete mode 100644 frontend/src/components/dashboard/MigrationContent.svelte create mode 100644 frontend/src/components/dashboard/PasskeySection.svelte create mode 100644 frontend/src/components/dashboard/PasswordSection.svelte create mode 100644 frontend/src/components/dashboard/TotpSection.svelte diff --git a/frontend/src/components/dashboard/AccountOverview.svelte b/frontend/src/components/dashboard/AccountOverview.svelte deleted file mode 100644 index 2c40d94..0000000 --- a/frontend/src/components/dashboard/AccountOverview.svelte +++ /dev/null @@ -1,60 +0,0 @@ - - -
-
-
{$_('dashboard.handle')}
-
- @{session.handle} - {#if session.isAdmin} - {$_('dashboard.admin')} - {/if} - {#if session.accountKind === 'migrated'} - {$_('dashboard.migrated')} - {:else if session.accountKind === 'deactivated'} - {$_('dashboard.deactivated')} - {/if} -
-
{$_('dashboard.did')}
-
{session.did}
- {#if session.contactKind === 'channel'} -
{$_('dashboard.primaryContact')}
-
- {#if session.preferredChannel === 'email'} - {session.email || $_('register.email')} - {:else if session.preferredChannel === 'discord'} - {$_('register.discord')} - {:else if session.preferredChannel === 'telegram'} - {$_('register.telegram')} - {:else if session.preferredChannel === 'signal'} - {$_('register.signal')} - {:else} - {session.preferredChannel} - {/if} - {#if session.preferredChannelVerified} - {$_('dashboard.verified')} - {:else} - {$_('dashboard.unverified')} - {/if} -
- {:else if session.contactKind === 'email'} -
{$_('register.email')}
-
- {session.email} - {#if session.emailConfirmed} - {$_('dashboard.verified')} - {:else} - {$_('dashboard.unverified')} - {/if} -
- {/if} -
-
diff --git a/frontend/src/components/dashboard/AdminContent.svelte b/frontend/src/components/dashboard/AdminContent.svelte index 2298e37..dac667b 100644 --- a/frontend/src/components/dashboard/AdminContent.svelte +++ b/frontend/src/components/dashboard/AdminContent.svelte @@ -11,6 +11,8 @@ setColors as setGlobalColors, setHasLogo as setGlobalHasLogo } from '../../lib/serverConfig.svelte' + import LoadMoreSentinel from '../LoadMoreSentinel.svelte' + import { portal } from '../../lib/portal' interface Props { session: Session @@ -32,7 +34,6 @@ indexedAt: string emailConfirmedAt?: string deactivatedAt?: string - invitesDisabled?: boolean } let stats = $state(null) @@ -41,6 +42,8 @@ let usersLoading = $state(false) let searchQuery = $state('') let usersCursor = $state(undefined) + let usersHasMore = $state(true) + let searchDebounce: ReturnType | null = null let selectedUser = $state(null) let userActionLoading = $state(false) @@ -63,7 +66,7 @@ let serverConfigLoading = $state(false) onMount(async () => { - await Promise.all([loadStats(), loadServerConfig()]) + await Promise.all([loadStats(), loadServerConfig(), loadUsers(true)]) }) async function loadStats() { @@ -82,6 +85,7 @@ if (reset) { users = [] usersCursor = undefined + usersHasMore = true } try { const result = await api.searchAccounts(session.accessJwt, { @@ -91,6 +95,7 @@ }) users = reset ? result.accounts : [...users, ...result.accounts] usersCursor = result.cursor + usersHasMore = !!result.cursor } catch { toast.error($_('admin.failedToLoadUsers')) } finally { @@ -98,9 +103,10 @@ } } - function handleSearch(e: Event) { - e.preventDefault() - loadUsers(true) + function onSearchInput(value: string) { + searchQuery = value + if (searchDebounce) clearTimeout(searchDebounce) + searchDebounce = setTimeout(() => loadUsers(true), 300) } function formatBytes(bytes: number): string { @@ -216,7 +222,6 @@ indexedAt: details.indexedAt, emailConfirmedAt: details.emailConfirmedAt, deactivatedAt: details.deactivatedAt, - invitesDisabled: details.invitesDisabled } } catch { } finally { @@ -228,26 +233,6 @@ selectedUser = null } - async function toggleUserInvites() { - if (!selectedUser) return - userActionLoading = true - try { - if (selectedUser.invitesDisabled) { - await api.enableAccountInvites(session.accessJwt, unsafeAsDid(selectedUser.did)) - selectedUser = { ...selectedUser, invitesDisabled: false } - toast.success($_('admin.invitesEnabled')) - } else { - await api.disableAccountInvites(session.accessJwt, unsafeAsDid(selectedUser.did)) - selectedUser = { ...selectedUser, invitesDisabled: true } - toast.success($_('admin.invitesDisabled')) - } - } catch (e) { - toast.error(e instanceof ApiError ? e.message : $_('admin.failedToToggleInvites')) - } finally { - userActionLoading = false - } - } - async function deleteUserAccount() { if (!selectedUser) return if (!confirm($_('admin.deleteConfirm', { values: { handle: selectedUser.handle } }))) return @@ -372,16 +357,12 @@

{$_('admin.userManagement')}

- + onSearchInput(e.currentTarget.value)} + placeholder={$_('admin.searchPlaceholder')} + /> {#if users.length === 0 && !usersLoading}

{$_('admin.searchToSeeUsers')}

@@ -412,18 +393,14 @@ {/each} - {#if usersCursor} - - {/if} + loadUsers(false)} /> {/if}
{#if selectedUser} -
- {$_('sessions.created')} - {timeAgo(s.createdAt)} + {$_('sessions.created')} + {timeAgo(s.createdAt)}
- {$_('sessions.expires')} - {formatDate(s.expiresAt)} + {$_('sessions.expires')} + {formatDate(s.expiresAt)}
diff --git a/frontend/src/components/dashboard/TotpSection.svelte b/frontend/src/components/dashboard/TotpSection.svelte new file mode 100644 index 0000000..7bf9036 --- /dev/null +++ b/frontend/src/components/dashboard/TotpSection.svelte @@ -0,0 +1,279 @@ + + +
+

{$_('security.totp')}

+ + {#if totpSetup.step === 'idle'} + {#if totpEnabled} +
{$_('security.totpEnabled')}
+ + {#if !showDisableForm && !showRegenForm} +
+ + +
+ {/if} + + {#if showRegenForm} +
+

{$_('security.regenerateBackupCodes')}

+

{$_('security.regenerateConfirm')}

+
+ + +
+
+ + +
+
+ + +
+
+ {/if} + + {#if showDisableForm} +
+

{$_('security.disableTotp')}

+

{$_('security.disableTotpWarning')}

+
+ + +
+
+ + +
+
+ + +
+
+ {/if} + {:else} +
{$_('security.totpDisabled')}
+ + {/if} + {:else if totpSetup.step === 'qr'} + {@const qrData = totpSetup as TotpQr} +
+

{$_('security.totpSetupInstructions')}

+
+ TOTP QR Code +
+
+ {$_('security.cantScan')} + {qrData.totpUri.split('secret=')[1]?.split('&')[0] || ''} +
+ +
+ {:else if totpSetup.step === 'verify'} + {@const verifyData = totpSetup} +
+

{$_('security.totpCodePlaceholder')}

+
+ +
+ + +
+
+
+ {:else if totpSetup.step === 'backup'} +
+

{$_('security.backupCodes')}

+

{$_('security.backupCodesDescription')}

+
+ {#each totpSetup.backupCodes as code} + {code} + {/each} +
+
+ + +
+
+ {/if} +
diff --git a/frontend/src/styles/dashboard.css b/frontend/src/styles/dashboard.css index 0370584..c4eb8cc 100644 --- a/frontend/src/styles/dashboard.css +++ b/frontend/src/styles/dashboard.css @@ -279,13 +279,19 @@ button.dropdown-item.logout-item { display: flex; align-items: center; gap: var(--space-1); - padding: 0; + padding: var(--space-2) var(--space-3); background: transparent; border: none; - color: var(--secondary); + color: var(--text-secondary); font-size: var(--text-base); cursor: pointer; margin-bottom: var(--space-2); + min-height: 44px; +} + +.back-button:hover:not(:disabled) { + background: var(--bg-secondary); + color: var(--text-primary); } .back-arrow { @@ -376,11 +382,6 @@ button.dropdown-item.logout-item { } } -.overview { - background: var(--bg-secondary); - padding: var(--space-6); -} - .overview dl { display: grid; grid-template-columns: auto 1fr; @@ -429,7 +430,6 @@ button.dropdown-item.logout-item { } } - .current { color: var(--text-secondary); font-size: var(--text-sm); @@ -607,7 +607,10 @@ code.record { } .password-actions, -.totp-actions, +.totp-actions { + display: flex; + gap: var(--space-3); +} .remove-password-form { background: var(--error-bg); @@ -949,12 +952,12 @@ code.record { font-size: var(--text-sm); } -.sessions .detail .label { +.detail-label { color: var(--text-secondary); margin-right: var(--space-2); } -.sessions .detail .value { +.detail-value { color: var(--text-primary); } @@ -1688,16 +1691,7 @@ button.record-item:hover { font-size: var(--text-sm); } -.controllers .detail .label { - color: var(--text-secondary); - margin-right: var(--space-2); -} - -.controllers .detail .value { - color: var(--text-primary); -} - -.controllers .detail .value.did { +.detail-value-did { font-family: var(--font-mono); font-size: var(--text-xs); word-break: break-all; @@ -2092,8 +2086,6 @@ button.typeahead-item:hover { gap: var(--space-3); } - - .item-id { font-weight: var(--font-medium); font-family: var(--font-mono); @@ -2174,12 +2166,6 @@ button.typeahead-item:hover { font-size: var(--text-base); } -.feature-list { - list-style: none; - padding: 0; - margin: 0 0 var(--space-4) 0; -} - .feature-list li { padding: var(--space-2) 0; padding-left: var(--space-4); @@ -2263,6 +2249,7 @@ button.typeahead-item:hover { } button.user-item-btn:hover { + background: var(--bg-secondary); border-color: var(--secondary); } @@ -2406,7 +2393,7 @@ button.remove-logo:hover { margin-bottom: var(--space-5); } -.admin .definition-list .mono { +.definition-mono { font-family: var(--font-mono); font-size: var(--text-xs); word-break: break-all;