Our Personal Data Server from scratch! tranquil.farm
oauth atproto pds rust postgresql objectstorage fun

chore: inline delegation audit page #53

merged opened by oyster.cafe targeting main from feat/cross-pds-delegation
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:3fwecdnvtcscjnrx2p4n7alz/sh.tangled.repo.pull/3mhbovxxr3p22
+241 -327
Diff #0
+1 -1
frontend/src/App.svelte
··· 91 91 '/comms', 92 92 '/repo', 93 93 '/controllers', 94 - '/delegation-audit', 94 + 95 95 '/invite-codes', 96 96 '/did-document', 97 97 '/admin',
+1 -1
frontend/src/components/HandleInput.svelte
··· 6 6 disabled?: boolean 7 7 placeholder?: string 8 8 id?: string 9 - autocomplete?: string 9 + autocomplete?: HTMLInputElement['autocomplete'] 10 10 onInput: (value: string) => void 11 11 onDomainChange: (domain: string) => void 12 12 }
+226 -5
frontend/src/components/dashboard/ControllersContent.svelte
··· 1 1 <script lang="ts"> 2 - import { onMount } from 'svelte' 2 + import { onMount, onDestroy } from 'svelte' 3 3 import { _ } from '../../lib/i18n' 4 4 import { api } from '../../lib/api' 5 5 import { toast } from '../../lib/toast.svelte' 6 6 import { formatDateTime } from '../../lib/date' 7 - import { routes, getFullUrl } from '../../lib/router.svelte' 8 - import type { Session, DelegationController, DelegationControlledAccount, DelegationScopePreset } from '../../lib/types/api' 7 + import type { Session, DelegationController, DelegationControlledAccount, DelegationScopePreset, DelegationAuditEntry } from '../../lib/types/api' 9 8 import { unsafeAsDid, unsafeAsScopeSet, unsafeAsHandle, unsafeAsEmail } from '../../lib/types/branded' 10 9 import type { Did, Handle, ScopeSet } from '../../lib/types/branded' 10 + import LoadMoreSentinel from '../LoadMoreSentinel.svelte' 11 11 12 12 interface Props { 13 13 session: Session ··· 123 123 let creatingDelegated = $state(false) 124 124 125 125 onMount(async () => { 126 - await loadData() 126 + await Promise.all([loadData(), loadAuditLog()]) 127 + pollInterval = setInterval(pollAuditLog, 15_000) 127 128 }) 128 129 129 130 async function loadData() { ··· 227 228 if ((scopes as string) === '') return $_('delegation.scopeViewer') 228 229 return $_('delegation.scopeCustom') 229 230 } 231 + 232 + interface AuditEntry { 233 + id: string 234 + delegatedDid: string 235 + actorDid: string 236 + actionType: string 237 + actionDetails: Record<string, unknown> | null 238 + createdAt: string 239 + } 240 + 241 + let auditLoading = $state(true) 242 + let auditLoadingMore = $state(false) 243 + let auditEntries = $state<AuditEntry[]>([]) 244 + let auditHasMore = $state(true) 245 + let auditOffset = $state(0) 246 + const auditLimit = 20 247 + let pollInterval: ReturnType<typeof setInterval> | null = null 248 + 249 + onDestroy(() => { 250 + if (pollInterval) clearInterval(pollInterval) 251 + }) 252 + 253 + async function loadAuditLog() { 254 + auditLoading = true 255 + auditOffset = 0 256 + try { 257 + const result = await api.getDelegationAuditLog(session.accessJwt, auditLimit, 0) 258 + if (result.ok && result.value) { 259 + const rawEntries = Array.isArray(result.value.entries) ? result.value.entries : [] 260 + auditEntries = rawEntries.map(mapAuditEntry) 261 + const total = result.value.total ?? 0 262 + auditHasMore = auditEntries.length < total 263 + auditOffset = auditEntries.length 264 + } else { 265 + auditEntries = [] 266 + auditHasMore = false 267 + } 268 + } catch { 269 + toast.error($_('delegation.failedToLoadAudit')) 270 + auditEntries = [] 271 + auditHasMore = false 272 + } finally { 273 + auditLoading = false 274 + } 275 + } 276 + 277 + async function pollAuditLog() { 278 + try { 279 + const result = await api.getDelegationAuditLog(session.accessJwt, auditLimit, 0) 280 + if (result.ok && result.value) { 281 + const rawEntries = Array.isArray(result.value.entries) ? result.value.entries : [] 282 + const latest = rawEntries.map(mapAuditEntry) 283 + if (latest.length > 0 && (auditEntries.length === 0 || latest[0].id !== auditEntries[0].id)) { 284 + auditEntries = latest 285 + const total = result.value.total ?? 0 286 + auditHasMore = auditEntries.length < total 287 + auditOffset = auditEntries.length 288 + } 289 + } else if (result.ok === false) { 290 + if (pollInterval) { clearInterval(pollInterval); pollInterval = null } 291 + } 292 + } catch { 293 + if (pollInterval) { clearInterval(pollInterval); pollInterval = null } 294 + } 295 + } 296 + 297 + async function loadMoreAuditEntries() { 298 + if (auditLoadingMore || !auditHasMore) return 299 + auditLoadingMore = true 300 + try { 301 + const result = await api.getDelegationAuditLog(session.accessJwt, auditLimit, auditOffset) 302 + if (result.ok && result.value) { 303 + const rawEntries = Array.isArray(result.value.entries) ? result.value.entries : [] 304 + const newEntries = rawEntries.map(mapAuditEntry) 305 + auditEntries = [...auditEntries, ...newEntries] 306 + const total = result.value.total ?? 0 307 + auditHasMore = auditEntries.length < total 308 + auditOffset = auditEntries.length 309 + } 310 + } catch { 311 + toast.error($_('delegation.failedToLoadAudit')) 312 + } finally { 313 + auditLoadingMore = false 314 + } 315 + } 316 + 317 + function mapAuditEntry(e: DelegationAuditEntry): AuditEntry { 318 + const parsed: Record<string, unknown> | null = e.details 319 + ? (() => { try { return JSON.parse(e.details!) as Record<string, unknown> } catch { return null } })() 320 + : null 321 + return { 322 + id: e.id, 323 + delegatedDid: e.target_did ?? '', 324 + actorDid: e.actor_did, 325 + actionType: e.action, 326 + actionDetails: parsed, 327 + createdAt: e.created_at, 328 + } 329 + } 330 + 331 + function formatActionType(type: string): string { 332 + const labels: Record<string, string> = { 333 + 'GrantCreated': $_('delegation.actionGrantCreated'), 334 + 'GrantRevoked': $_('delegation.actionGrantRevoked'), 335 + 'ScopesModified': $_('delegation.actionScopesModified'), 336 + 'TokenIssued': $_('delegation.actionTokenIssued'), 337 + 'RepoWrite': $_('delegation.actionRepoWrite'), 338 + 'BlobUpload': $_('delegation.actionBlobUpload'), 339 + 'AccountAction': $_('delegation.actionAccountAction'), 340 + } 341 + return labels[type] || type 342 + } 343 + 344 + function formatActionDetails(details: Record<string, unknown> | null): string { 345 + if (!details) return '' 346 + return Object.entries(details) 347 + .map(([key, value]) => `${key.replace(/_/g, ' ')}: ${JSON.stringify(value)}`) 348 + .join(', ') 349 + } 350 + 230 351 </script> 231 352 232 353 <div class="controllers"> ··· 454 575 {$_('delegation.createDelegatedAccountButton')} 455 576 </button> 456 577 {/if} 578 + 579 + <div class="constraint-notice"> 580 + <p>{$_('delegation.controlledAccountsLocalOnly')}</p> 581 + </div> 457 582 </section> 458 583 459 584 <section class="section"> ··· 461 586 <h3>{$_('delegation.auditLog')}</h3> 462 587 <p class="section-description">{$_('delegation.auditLogDesc')}</p> 463 588 </div> 464 - <a href={getFullUrl(routes.delegationAudit)} class="btn-link">{$_('delegation.viewAuditLog')}</a> 589 + 590 + {#if auditLoading} 591 + <div class="loading">{$_('common.loading')}</div> 592 + {:else if auditEntries.length === 0} 593 + <p class="empty">{$_('delegation.noAuditEntries')}</p> 594 + {:else} 595 + <div class="audit-entries"> 596 + {#each auditEntries as entry} 597 + <div class="audit-entry"> 598 + <div class="audit-entry-header"> 599 + <span class="action-type">{formatActionType(entry.actionType)}</span> 600 + <span class="audit-entry-date">{formatDateTime(entry.createdAt)}</span> 601 + </div> 602 + <div class="audit-entry-details"> 603 + <div class="detail"> 604 + <span class="label">{$_('delegation.actor')}</span> 605 + <span class="value did">{entry.actorDid}</span> 606 + </div> 607 + {#if entry.delegatedDid} 608 + <div class="detail"> 609 + <span class="label">{$_('delegation.target')}</span> 610 + <span class="value did">{entry.delegatedDid}</span> 611 + </div> 612 + {/if} 613 + {#if entry.actionDetails} 614 + <div class="detail"> 615 + <span class="label">{$_('delegation.details')}</span> 616 + <span class="value audit-details-value">{formatActionDetails(entry.actionDetails)}</span> 617 + </div> 618 + {/if} 619 + </div> 620 + </div> 621 + {/each} 622 + </div> 623 + 624 + <LoadMoreSentinel hasMore={auditHasMore} loading={auditLoadingMore} onLoadMore={loadMoreAuditEntries} /> 625 + {/if} 465 626 </section> 466 627 {/if} 467 628 </div> ··· 505 666 border: 1px solid var(--border-color); 506 667 border-radius: var(--radius-md); 507 668 padding: var(--space-4); 669 + margin-top: var(--space-4); 508 670 } 509 671 510 672 .constraint-notice p { ··· 846 1008 border: 1px solid var(--info-border, var(--border-color)); 847 1009 } 848 1010 1011 + .audit-entries { 1012 + display: flex; 1013 + flex-direction: column; 1014 + gap: var(--space-3); 1015 + } 1016 + 1017 + .audit-entry { 1018 + background: var(--bg-card); 1019 + border: 1px solid var(--border-color); 1020 + border-radius: var(--radius-lg); 1021 + padding: var(--space-4); 1022 + } 1023 + 1024 + .audit-entry-header { 1025 + display: flex; 1026 + justify-content: space-between; 1027 + align-items: center; 1028 + margin-bottom: var(--space-3); 1029 + } 1030 + 1031 + .action-type { 1032 + font-weight: var(--font-medium); 1033 + padding: var(--space-1) var(--space-2); 1034 + background: var(--accent); 1035 + color: var(--text-inverse); 1036 + border-radius: var(--radius-md); 1037 + font-size: var(--text-sm); 1038 + } 1039 + 1040 + .audit-entry-date { 1041 + font-size: var(--text-sm); 1042 + color: var(--text-secondary); 1043 + } 1044 + 1045 + .audit-entry-details { 1046 + display: flex; 1047 + flex-direction: column; 1048 + gap: var(--space-2); 1049 + } 1050 + 1051 + .audit-details-value { 1052 + font-family: var(--font-mono); 1053 + font-size: var(--text-xs); 1054 + word-break: break-word; 1055 + } 1056 + 849 1057 @media (max-width: 600px) { 850 1058 .item-card { 851 1059 flex-direction: column; ··· 861 1069 width: 100%; 862 1070 text-align: center; 863 1071 } 1072 + 1073 + .audit-entry-header { 1074 + flex-direction: column; 1075 + align-items: flex-start; 1076 + gap: var(--space-2); 1077 + } 1078 + 1079 + .audit-entry-details .value.did { 1080 + overflow: hidden; 1081 + text-overflow: ellipsis; 1082 + white-space: nowrap; 1083 + max-width: 60vw; 1084 + } 864 1085 } 865 1086 </style>
-282
frontend/src/components/dashboard/DelegationAuditContent.svelte
··· 1 - <script lang="ts"> 2 - import { onMount } from 'svelte' 3 - import { _ } from '../../lib/i18n' 4 - import { api } from '../../lib/api' 5 - import { toast } from '../../lib/toast.svelte' 6 - import { formatDateTime } from '../../lib/date' 7 - import type { Session, DelegationAuditEntry } from '../../lib/types/api' 8 - import LoadMoreSentinel from '../LoadMoreSentinel.svelte' 9 - 10 - interface Props { 11 - session: Session 12 - } 13 - 14 - let { session }: Props = $props() 15 - 16 - interface AuditEntry { 17 - id: string 18 - delegatedDid: string 19 - actorDid: string 20 - controllerDid: string | null 21 - actionType: string 22 - actionDetails: Record<string, unknown> | null 23 - createdAt: string 24 - } 25 - 26 - let loading = $state(true) 27 - let loadingMore = $state(false) 28 - let entries = $state<AuditEntry[]>([]) 29 - let hasMore = $state(true) 30 - let offset = $state(0) 31 - const limit = 20 32 - 33 - onMount(async () => { 34 - await loadAuditLog() 35 - }) 36 - 37 - async function loadAuditLog() { 38 - loading = true 39 - offset = 0 40 - try { 41 - const result = await api.getDelegationAuditLog(session.accessJwt, limit, 0) 42 - if (result.ok && result.value) { 43 - const rawEntries = Array.isArray(result.value.entries) ? result.value.entries : [] 44 - entries = rawEntries.map(mapEntry) 45 - const total = result.value.total ?? 0 46 - hasMore = entries.length < total 47 - offset = entries.length 48 - } else { 49 - entries = [] 50 - hasMore = false 51 - } 52 - } catch { 53 - toast.error($_('delegation.failedToLoadAudit')) 54 - entries = [] 55 - hasMore = false 56 - } finally { 57 - loading = false 58 - } 59 - } 60 - 61 - async function loadMoreEntries() { 62 - if (loadingMore || !hasMore) return 63 - loadingMore = true 64 - try { 65 - const result = await api.getDelegationAuditLog(session.accessJwt, limit, offset) 66 - if (result.ok && result.value) { 67 - const rawEntries = Array.isArray(result.value.entries) ? result.value.entries : [] 68 - const newEntries = rawEntries.map(mapEntry) 69 - entries = [...entries, ...newEntries] 70 - const total = result.value.total ?? 0 71 - hasMore = entries.length < total 72 - offset = entries.length 73 - } 74 - } catch { 75 - toast.error($_('delegation.failedToLoadAudit')) 76 - } finally { 77 - loadingMore = false 78 - } 79 - } 80 - 81 - function mapEntry(e: DelegationAuditEntry): AuditEntry { 82 - return { 83 - id: e.id, 84 - delegatedDid: e.target_did ?? '', 85 - actorDid: e.actor_did, 86 - controllerDid: null, 87 - actionType: e.action, 88 - actionDetails: e.details ? JSON.parse(e.details) : null, 89 - createdAt: e.created_at 90 - } 91 - } 92 - 93 - function formatActionType(type: string): string { 94 - const labels: Record<string, string> = { 95 - 'GrantCreated': $_('delegation.actionGrantCreated'), 96 - 'GrantRevoked': $_('delegation.actionGrantRevoked'), 97 - 'ScopesModified': $_('delegation.actionScopesModified'), 98 - 'TokenIssued': $_('delegation.actionTokenIssued'), 99 - 'RepoWrite': $_('delegation.actionRepoWrite'), 100 - 'BlobUpload': $_('delegation.actionBlobUpload'), 101 - 'AccountAction': $_('delegation.actionAccountAction') 102 - } 103 - return labels[type] || type 104 - } 105 - 106 - function formatActionDetails(details: Record<string, unknown> | null): string { 107 - if (!details) return '' 108 - return Object.entries(details) 109 - .map(([key, value]) => `${key.replace(/_/g, ' ')}: ${JSON.stringify(value)}`) 110 - .join(', ') 111 - } 112 - 113 - function truncateDid(did: string): string { 114 - if (did.length <= 30) return did 115 - return did.substring(0, 20) + '...' + did.substring(did.length - 6) 116 - } 117 - </script> 118 - 119 - <div class="audit"> 120 - <div class="actions-bar"> 121 - <button type="button" class="ghost" onclick={() => loadAuditLog()} disabled={loading}> 122 - {loading ? $_('common.loading') : $_('delegation.refresh')} 123 - </button> 124 - </div> 125 - 126 - {#if loading} 127 - <div class="loading">{$_('common.loading')}</div> 128 - {:else if entries.length === 0} 129 - <p class="empty">{$_('delegation.noAuditEntries')}</p> 130 - {:else} 131 - <div class="entries"> 132 - {#each entries as entry} 133 - <div class="entry"> 134 - <div class="entry-header"> 135 - <span class="action-type">{formatActionType(entry.actionType)}</span> 136 - <span class="entry-date">{formatDateTime(entry.createdAt)}</span> 137 - </div> 138 - <div class="entry-details"> 139 - <div class="detail"> 140 - <span class="label">{$_('delegation.actor')}</span> 141 - <span class="value did" title={entry.actorDid}>{truncateDid(entry.actorDid)}</span> 142 - </div> 143 - {#if entry.delegatedDid} 144 - <div class="detail"> 145 - <span class="label">{$_('delegation.target')}</span> 146 - <span class="value did" title={entry.delegatedDid}>{truncateDid(entry.delegatedDid)}</span> 147 - </div> 148 - {/if} 149 - {#if entry.actionDetails} 150 - <div class="detail"> 151 - <span class="label">{$_('delegation.details')}</span> 152 - <span class="value details">{formatActionDetails(entry.actionDetails)}</span> 153 - </div> 154 - {/if} 155 - </div> 156 - </div> 157 - {/each} 158 - </div> 159 - 160 - <LoadMoreSentinel {hasMore} loading={loadingMore} onLoadMore={loadMoreEntries} /> 161 - {/if} 162 - </div> 163 - 164 - <style> 165 - .audit { 166 - max-width: var(--width-lg); 167 - } 168 - 169 - .actions-bar { 170 - display: flex; 171 - justify-content: flex-end; 172 - margin-bottom: var(--space-4); 173 - } 174 - 175 - .ghost { 176 - background: transparent; 177 - border: 1px solid var(--border-color); 178 - color: var(--text-primary); 179 - padding: var(--space-2) var(--space-4); 180 - border-radius: var(--radius-md); 181 - cursor: pointer; 182 - } 183 - 184 - .ghost:hover:not(:disabled) { 185 - border-color: var(--accent); 186 - } 187 - 188 - .ghost:disabled { 189 - opacity: 0.6; 190 - cursor: not-allowed; 191 - } 192 - 193 - .loading, 194 - .empty { 195 - color: var(--text-secondary); 196 - padding: var(--space-6); 197 - text-align: center; 198 - } 199 - 200 - .entries { 201 - display: flex; 202 - flex-direction: column; 203 - gap: var(--space-3); 204 - } 205 - 206 - .entry { 207 - background: var(--bg-secondary); 208 - border: 1px solid var(--border-color); 209 - border-radius: var(--radius-lg); 210 - padding: var(--space-4); 211 - } 212 - 213 - .entry-header { 214 - display: flex; 215 - justify-content: space-between; 216 - align-items: center; 217 - margin-bottom: var(--space-3); 218 - } 219 - 220 - .action-type { 221 - font-weight: var(--font-medium); 222 - padding: var(--space-1) var(--space-2); 223 - background: var(--accent); 224 - color: var(--text-inverse); 225 - border-radius: var(--radius-md); 226 - font-size: var(--text-sm); 227 - } 228 - 229 - .entry-date { 230 - font-size: var(--text-sm); 231 - color: var(--text-secondary); 232 - } 233 - 234 - .entry-details { 235 - display: flex; 236 - flex-direction: column; 237 - gap: var(--space-2); 238 - } 239 - 240 - .detail { 241 - display: flex; 242 - gap: var(--space-2); 243 - font-size: var(--text-sm); 244 - } 245 - 246 - .detail .label { 247 - color: var(--text-secondary); 248 - min-width: 60px; 249 - } 250 - 251 - .detail .value { 252 - color: var(--text-primary); 253 - } 254 - 255 - .detail .value.did { 256 - font-family: var(--font-mono); 257 - font-size: var(--text-xs); 258 - } 259 - 260 - .detail .value.details { 261 - font-family: var(--font-mono); 262 - font-size: var(--text-xs); 263 - word-break: break-word; 264 - } 265 - 266 - @media (max-width: 500px) { 267 - .entry-header { 268 - flex-direction: column; 269 - align-items: flex-start; 270 - gap: var(--space-2); 271 - } 272 - 273 - .detail { 274 - flex-direction: column; 275 - gap: var(--space-1); 276 - } 277 - 278 - .detail .label { 279 - min-width: unset; 280 - } 281 - } 282 - </style>
-5
frontend/src/components/dashboard/InviteCodesContent.svelte
··· 255 255 flex-shrink: 0; 256 256 } 257 257 258 - .danger-text { 259 - color: var(--error-text); 260 - flex-shrink: 0; 261 - } 262 - 263 258 .code-meta { 264 259 display: flex; 265 260 gap: var(--space-4);
+1 -1
frontend/src/lib/types/routes.ts
··· 9 9 comms: "/comms", 10 10 repo: "/repo", 11 11 controllers: "/controllers", 12 - delegationAudit: "/delegation-audit", 12 + 13 13 actAs: "/act-as", 14 14 didDocument: "/did-document", 15 15 migrate: "/migrate",
+1 -4
frontend/src/locales/en.json
··· 144 144 "navComms": "Communication Preferences", 145 145 "navRepo": "Repository Explorer", 146 146 "navDelegation": "Delegation", 147 - "navDelegationAudit": "Delegation Audit", 148 147 "navAdmin": "Admin Panel", 149 148 "navDidDocument": "DID Document", 150 149 "migrated": "Migrated", ··· 836 835 "controllerRemoved": "Controller removed successfully", 837 836 "controlledAccounts": "Controlled Accounts", 838 837 "controlledAccountsDesc": "Accounts you can act on behalf of", 838 + "controlledAccountsLocalOnly": "Only accounts on this PDS are shown. Another PDS may have granted you controller access separately", 839 839 "noControlledAccounts": "You do not have access to any delegated accounts.", 840 840 "actAs": "Act As", 841 841 "cannotControlAccounts": "You cannot control other accounts because this account has controllers. An account can either have controllers or control other accounts, but not both.", ··· 848 848 "accountCreated": "Created delegated account: {handle}", 849 849 "auditLog": "Audit Log", 850 850 "auditLogDesc": "View all delegation activity", 851 - "viewAuditLog": "View Audit Log", 852 851 "scopeOwner": "Owner", 853 852 "scopeViewer": "Viewer", 854 853 "scopeCustom": "Custom", ··· 856 855 "details": "Details", 857 856 "previous": "Previous", 858 857 "next": "Next", 859 - "refresh": "Refresh", 860 858 "actionGrantCreated": "Grant Created", 861 859 "actionGrantRevoked": "Grant Revoked", 862 860 "actionScopesModified": "Scopes Modified", ··· 866 864 "actionAccountAction": "Account Action", 867 865 "noAuditEntries": "No audit entries", 868 866 "target": "Target", 869 - "pageInfo": "{start} - {end} of {total}", 870 867 "failedToLoadAudit": "Failed to load audit log" 871 868 }, 872 869 "actAs": {
+1 -4
frontend/src/locales/fi.json
··· 124 124 "dashboard": { 125 125 "title": "Hallintapaneeli", 126 126 "accountManager": "Tilinhallinta", 127 - "navDelegationAudit": "Delegointiloki", 128 127 "switchAccount": "Vaihda tiliรค", 129 128 "addAnotherAccount": "Lisรครค toinen tili", 130 129 "signOut": "Kirjaudu ulos @{handle}", ··· 835 834 "controllerRemoved": "Hallinnoija poistettu", 836 835 "controlledAccounts": "Hallinnoidut tilit", 837 836 "controlledAccountsDesc": "Tilit, joiden puolesta voit toimia", 837 + "controlledAccountsLocalOnly": "Vain tรคmรคn PDS:n tilit nรคytetรครคn. Toinen PDS on voinut myรถntรครค sinulle ohjausoikeuden erikseen", 838 838 "noControlledAccounts": "Sinulla ei ole pรครคsyรค delegoituihin tileihin.", 839 839 "actAs": "Toimi kรคyttรคjรคnรค", 840 840 "cannotControlAccounts": "Et voi hallinnoida muita tilejรค, koska tรคllรค tilillรค on hallinnoijia. Tili voi joko olla hallinnoija tai hallinnoidaan, mutta ei molempia.", ··· 847 847 "accountCreated": "Delegoitu tili luotu: {handle}", 848 848 "auditLog": "Tapahtumaloki", 849 849 "auditLogDesc": "Nรคytรค kaikki delegointitoiminta", 850 - "viewAuditLog": "Nรคytรค tapahtumaloki", 851 850 "scopeOwner": "Omistaja", 852 851 "scopeViewer": "Katsoja", 853 852 "scopeCustom": "Mukautettu", ··· 855 854 "details": "Tiedot", 856 855 "previous": "Edellinen", 857 856 "next": "Seuraava", 858 - "refresh": "Pรคivitรค", 859 857 "actionGrantCreated": "Oikeus luotu", 860 858 "actionGrantRevoked": "Oikeus peruttu", 861 859 "actionScopesModified": "Oikeuksia muokattu", ··· 865 863 "actionAccountAction": "Tilitoiminto", 866 864 "noAuditEntries": "Ei lokimerkintรถjรค", 867 865 "target": "Kohde", 868 - "pageInfo": "{start} - {end} / {total}", 869 866 "failedToLoadAudit": "Lokin lataus epรคonnistui" 870 867 }, 871 868 "actAs": {
+1 -4
frontend/src/locales/ja.json
··· 124 124 "dashboard": { 125 125 "title": "ใƒ€ใƒƒใ‚ทใƒฅใƒœใƒผใƒ‰", 126 126 "accountManager": "ใ‚ขใ‚ซใ‚ฆใƒณใƒˆ็ฎก็†", 127 - "navDelegationAudit": "ๅง”ไปป็›ฃๆŸป", 128 127 "switchAccount": "ใ‚ขใ‚ซใ‚ฆใƒณใƒˆๅˆ‡ๆ›ฟ", 129 128 "addAnotherAccount": "ๅˆฅใฎใ‚ขใ‚ซใ‚ฆใƒณใƒˆใ‚’่ฟฝๅŠ ", 130 129 "signOut": "@{handle} ใ‹ใ‚‰ใ‚ตใ‚คใƒณใ‚ขใ‚ฆใƒˆ", ··· 834 833 "actionAccountAction": "ใ‚ขใ‚ซใ‚ฆใƒณใƒˆใ‚ขใ‚ฏใ‚ทใƒงใƒณ", 835 834 "previous": "ๅ‰ใธ", 836 835 "next": "ๆฌกใธ", 837 - "refresh": "ๆ›ดๆ–ฐ", 838 836 "adding": "่ฟฝๅŠ ไธญ...", 839 837 "accessLevel": "ใ‚ขใ‚ฏใ‚ปใ‚นใƒฌใƒ™ใƒซ", 840 838 "addControllerButton": "+ ใ‚ณใƒณใƒˆใƒญใƒผใƒฉใƒผใ‚’่ฟฝๅŠ ", ··· 848 846 "cannotAddControllers": "ไป–ใฎใ‚ขใ‚ซใ‚ฆใƒณใƒˆใ‚’็ฎก็†ใ—ใฆใ„ใ‚‹ใŸใ‚ใ€ใ‚ณใƒณใƒˆใƒญใƒผใƒฉใƒผใ‚’่ฟฝๅŠ ใงใใพใ›ใ‚“ใ€‚ใ‚ขใ‚ซใ‚ฆใƒณใƒˆใฏใ‚ณใƒณใƒˆใƒญใƒผใƒฉใƒผใ‚’ๆŒใคใ‹ใ€ไป–ใฎใ‚ขใ‚ซใ‚ฆใƒณใƒˆใ‚’็ฎก็†ใ™ใ‚‹ใ‹ใฎใ„ใšใ‚Œใ‹ใฎใฟๅฏ่ƒฝใงใ™ใ€‚", 849 847 "cannotControlAccounts": "ใ“ใฎใ‚ขใ‚ซใ‚ฆใƒณใƒˆใซใฏใ‚ณใƒณใƒˆใƒญใƒผใƒฉใƒผใŒใ„ใ‚‹ใŸใ‚ใ€ไป–ใฎใ‚ขใ‚ซใ‚ฆใƒณใƒˆใ‚’็ฎก็†ใงใใพใ›ใ‚“ใ€‚ใ‚ขใ‚ซใ‚ฆใƒณใƒˆใฏใ‚ณใƒณใƒˆใƒญใƒผใƒฉใƒผใ‚’ๆŒใคใ‹ใ€ไป–ใฎใ‚ขใ‚ซใ‚ฆใƒณใƒˆใ‚’็ฎก็†ใ™ใ‚‹ใ‹ใฎใ„ใšใ‚Œใ‹ใฎใฟๅฏ่ƒฝใงใ™ใ€‚", 850 848 "controlledAccountsDesc": "ใ‚ใชใŸใŒไปฃใ‚ใ‚Šใซๆ“ไฝœใงใใ‚‹ใ‚ขใ‚ซใ‚ฆใƒณใƒˆ", 849 + "controlledAccountsLocalOnly": "ใ“ใฎPDSไธŠใฎใ‚ขใ‚ซใ‚ฆใƒณใƒˆใฎใฟ่กจ็คบใ•ใ‚Œใพใ™ใ€‚ๅˆฅใฎPDSใŒใ‚ณใƒณใƒˆใƒญใƒผใƒฉใƒผใ‚ขใ‚ฏใ‚ปใ‚นใ‚’ๅ€‹ๅˆฅใซไป˜ไธŽใ—ใฆใ„ใ‚‹ๅ ดๅˆใŒใ‚ใ‚Šใพใ™", 851 850 "controllerAdded": "ใ‚ณใƒณใƒˆใƒญใƒผใƒฉใƒผใ‚’่ฟฝๅŠ ใ—ใพใ—ใŸ", 852 851 "controllerDid": "ใ‚ณใƒณใƒˆใƒญใƒผใƒฉใƒผDID", 853 852 "controllerRemoved": "ใ‚ณใƒณใƒˆใƒญใƒผใƒฉใƒผใ‚’ๅ‰Š้™คใ—ใพใ—ใŸ", ··· 860 859 "inactive": "้žใ‚ขใ‚ฏใƒ†ใ‚ฃใƒ–", 861 860 "remove": "ๅ‰Š้™ค", 862 861 "removeConfirm": "ใ“ใฎใ‚ณใƒณใƒˆใƒญใƒผใƒฉใƒผใ‚’ๅ‰Š้™คใ—ใพใ™ใ‹๏ผŸ", 863 - "viewAuditLog": "็›ฃๆŸปใƒญใ‚ฐใ‚’่กจ็คบ", 864 862 "yourAccessLevel": "ใ‚ใชใŸใฎใ‚ขใ‚ฏใ‚ปใ‚นใƒฌใƒ™ใƒซ", 865 863 "accountCreated": "ๅง”ไปปใ‚ขใ‚ซใ‚ฆใƒณใƒˆใ‚’ไฝœๆˆใ—ใพใ—ใŸ: {handle}", 866 864 "noAuditEntries": "็›ฃๆŸปใ‚จใƒณใƒˆใƒชใชใ—", 867 865 "target": "ๅฏพ่ฑก", 868 - "pageInfo": "{start} - {end} / {total}", 869 866 "failedToLoadAudit": "็›ฃๆŸปใƒญใ‚ฐใฎ่ชญใฟ่พผใฟใซๅคฑๆ•—ใ—ใพใ—ใŸ" 870 867 }, 871 868 "actAs": {
+1 -4
frontend/src/locales/ko.json
··· 124 124 "dashboard": { 125 125 "title": "๋Œ€์‹œ๋ณด๋“œ", 126 126 "accountManager": "๊ณ„์ • ๊ด€๋ฆฌ", 127 - "navDelegationAudit": "์œ„์ž„ ๊ฐ์‚ฌ", 128 127 "switchAccount": "๊ณ„์ • ์ „ํ™˜", 129 128 "addAnotherAccount": "๋‹ค๋ฅธ ๊ณ„์ • ์ถ”๊ฐ€", 130 129 "signOut": "@{handle} ๋กœ๊ทธ์•„์›ƒ", ··· 834 833 "actionAccountAction": "๊ณ„์ • ์ž‘์—…", 835 834 "previous": "์ด์ „", 836 835 "next": "๋‹ค์Œ", 837 - "refresh": "์ƒˆ๋กœ๊ณ ์นจ", 838 836 "adding": "์ถ”๊ฐ€ ์ค‘...", 839 837 "accessLevel": "์•ก์„ธ์Šค ์ˆ˜์ค€", 840 838 "addControllerButton": "+ ์ปจํŠธ๋กค๋Ÿฌ ์ถ”๊ฐ€", ··· 848 846 "cannotAddControllers": "๋‹ค๋ฅธ ๊ณ„์ •์„ ๊ด€๋ฆฌํ•˜๊ณ  ์žˆ์–ด ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ณ„์ •์€ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๊ฐ€์ง€๊ฑฐ๋‚˜ ๋‹ค๋ฅธ ๊ณ„์ •์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ๋‘˜ ๋‹ค๋Š” ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.", 849 847 "cannotControlAccounts": "์ด ๊ณ„์ •์— ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ์žˆ์–ด ๋‹ค๋ฅธ ๊ณ„์ •์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ณ„์ •์€ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๊ฐ€์ง€๊ฑฐ๋‚˜ ๋‹ค๋ฅธ ๊ณ„์ •์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ๋‘˜ ๋‹ค๋Š” ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.", 850 848 "controlledAccountsDesc": "๊ท€ํ•˜๊ฐ€ ๋Œ€์‹  ์ž‘์—…ํ•  ์ˆ˜ ์žˆ๋Š” ๊ณ„์ •", 849 + "controlledAccountsLocalOnly": "์ด PDS์˜ ๊ณ„์ •๋งŒ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ๋‹ค๋ฅธ PDS์—์„œ ๋ณ„๋„๋กœ ์ปจํŠธ๋กค๋Ÿฌ ์•ก์„ธ์Šค๋ฅผ ๋ถ€์—ฌํ–ˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค", 851 850 "controllerAdded": "์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค", 852 851 "controllerDid": "์ปจํŠธ๋กค๋Ÿฌ DID", 853 852 "controllerRemoved": "์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ์ œ๊ฑฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค", ··· 860 859 "inactive": "๋น„ํ™œ์„ฑ", 861 860 "remove": "์ œ๊ฑฐ", 862 861 "removeConfirm": "์ด ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ์ œ๊ฑฐํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?", 863 - "viewAuditLog": "๊ฐ์‚ฌ ๋กœ๊ทธ ๋ณด๊ธฐ", 864 862 "yourAccessLevel": "๊ท€ํ•˜์˜ ์•ก์„ธ์Šค ์ˆ˜์ค€", 865 863 "accountCreated": "์œ„์ž„ ๊ณ„์ •์ด ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค: {handle}", 866 864 "noAuditEntries": "๊ฐ์‚ฌ ํ•ญ๋ชฉ ์—†์Œ", 867 865 "target": "๋Œ€์ƒ", 868 - "pageInfo": "{start} - {end} / {total}", 869 866 "failedToLoadAudit": "๊ฐ์‚ฌ ๋กœ๊ทธ ๋กœ๋”ฉ ์‹คํŒจ" 870 867 }, 871 868 "actAs": {
+1 -4
frontend/src/locales/sv.json
··· 124 124 "dashboard": { 125 125 "title": "Kontrollpanel", 126 126 "accountManager": "Kontohantering", 127 - "navDelegationAudit": "Delegeringsgranskning", 128 127 "switchAccount": "Byt konto", 129 128 "addAnotherAccount": "Lรคgg till ett annat konto", 130 129 "signOut": "Logga ut @{handle}", ··· 834 833 "actionAccountAction": "Kontoรฅtgรคrd", 835 834 "previous": "Fรถregรฅende", 836 835 "next": "Nรคsta", 837 - "refresh": "Uppdatera", 838 836 "adding": "Lรคgger till...", 839 837 "accessLevel": "ร…tkomstnivรฅ", 840 838 "addControllerButton": "+ Lรคgg till kontrollant", ··· 848 846 "cannotAddControllers": "Du kan inte lรคgga till kontrollanter eftersom detta konto kontrollerar andra konton. Ett konto kan antingen ha kontrollanter eller kontrollera andra konton, men inte bรฅda.", 849 847 "cannotControlAccounts": "Du kan inte kontrollera andra konton eftersom detta konto har kontrollanter. Ett konto kan antingen ha kontrollanter eller kontrollera andra konton, men inte bรฅda.", 850 848 "controlledAccountsDesc": "Konton du kan agera fรถr", 849 + "controlledAccountsLocalOnly": "Endast konton pรฅ denna PDS visas. En annan PDS kan ha beviljat dig kontrollรฅtkomst separat", 851 850 "controllerAdded": "Kontrollant tillagd", 852 851 "controllerDid": "Kontrollant-DID", 853 852 "controllerRemoved": "Kontrollant borttagen", ··· 860 859 "inactive": "Inaktiv", 861 860 "remove": "Ta bort", 862 861 "removeConfirm": "Vill du ta bort denna kontrollant?", 863 - "viewAuditLog": "Visa granskningslogg", 864 862 "yourAccessLevel": "Din รฅtkomstnivรฅ", 865 863 "accountCreated": "Skapade delegerat konto: {handle}", 866 864 "noAuditEntries": "Inga granskningsposter", 867 865 "target": "Mรฅl", 868 - "pageInfo": "{start} - {end} av {total}", 869 866 "failedToLoadAudit": "Kunde inte ladda granskningslogg" 870 867 }, 871 868 "actAs": {
+1 -4
frontend/src/locales/zh.json
··· 124 124 "dashboard": { 125 125 "title": "ๆŽงๅˆถๅฐ", 126 126 "accountManager": "่ดฆๆˆท็ฎก็†", 127 - "navDelegationAudit": "ๅง”ๆ‰˜ๅฎก่ฎก", 128 127 "switchAccount": "ๅˆ‡ๆข่ดฆๆˆท", 129 128 "addAnotherAccount": "ๆทปๅŠ ๅ…ถไป–่ดฆๆˆท", 130 129 "signOut": "้€€ๅ‡บ @{handle}", ··· 835 834 "actionAccountAction": "่ดฆๆˆทๆ“ไฝœ", 836 835 "previous": "ไธŠไธ€้กต", 837 836 "next": "ไธ‹ไธ€้กต", 838 - "refresh": "ๅˆทๆ–ฐ", 839 837 "adding": "ๆทปๅŠ ไธญ...", 840 838 "accessLevel": "่ฎฟ้—ฎ็บงๅˆซ", 841 839 "addControllerButton": "+ ๆทปๅŠ ๆŽงๅˆถ่€…", ··· 849 847 "cannotAddControllers": "ๅ› ไธบๆญค่ดฆๆˆทๆญฃๅœจๆŽงๅˆถๅ…ถไป–่ดฆๆˆท๏ผŒๆ‰€ไปฅๆ— ๆณ•ๆทปๅŠ ๆŽงๅˆถ่€…ใ€‚่ดฆๆˆทๅช่ƒฝๆ‹ฅๆœ‰ๆŽงๅˆถ่€…ๆˆ–ๆŽงๅˆถๅ…ถไป–่ดฆๆˆท๏ผŒไธ่ƒฝๅŒๆ—ถไธค่€…ๅ…ผๅค‡ใ€‚", 850 848 "cannotControlAccounts": "ๅ› ไธบๆญค่ดฆๆˆทๆœ‰ๆŽงๅˆถ่€…๏ผŒๆ‰€ไปฅๆ— ๆณ•ๆŽงๅˆถๅ…ถไป–่ดฆๆˆทใ€‚่ดฆๆˆทๅช่ƒฝๆ‹ฅๆœ‰ๆŽงๅˆถ่€…ๆˆ–ๆŽงๅˆถๅ…ถไป–่ดฆๆˆท๏ผŒไธ่ƒฝๅŒๆ—ถไธค่€…ๅ…ผๅค‡ใ€‚", 851 849 "controlledAccountsDesc": "ๆ‚จๅฏไปฅไปฃ็†ๆ“ไฝœ็š„่ดฆๆˆท", 850 + "controlledAccountsLocalOnly": "ไป…ๆ˜พ็คบๆญคPDSไธŠ็š„่ดฆๆˆทใ€‚ๅ…ถไป–PDSๅฏ่ƒฝๅทฒๅ•็‹ฌๆŽˆไบˆๆ‚จๆŽงๅˆถ่€…่ฎฟ้—ฎๆƒ้™", 852 851 "controllerAdded": "ๆŽงๅˆถ่€…ๅทฒๆทปๅŠ ", 853 852 "controllerDid": "ๆŽงๅˆถ่€… DID", 854 853 "controllerRemoved": "ๆŽงๅˆถ่€…ๅทฒ็งป้™ค", ··· 861 860 "inactive": "ๆœชๆฟ€ๆดป", 862 861 "remove": "็งป้™ค", 863 862 "removeConfirm": "็กฎๅฎš่ฆ็งป้™คๆญคๆŽงๅˆถ่€…ๅ—๏ผŸ", 864 - "viewAuditLog": "ๆŸฅ็œ‹ๅฎก่ฎกๆ—ฅๅฟ—", 865 863 "yourAccessLevel": "ๆ‚จ็š„่ฎฟ้—ฎ็บงๅˆซ", 866 864 "noAuditEntries": "ๆ— ๅฎก่ฎก่ฎฐๅฝ•", 867 865 "target": "็›ฎๆ ‡", 868 - "pageInfo": "{start} - {end} / {total}", 869 866 "failedToLoadAudit": "ๅŠ ่ฝฝๅฎก่ฎกๆ—ฅๅฟ—ๅคฑ่ดฅ" 870 867 }, 871 868 "actAs": {
+5 -7
frontend/src/routes/Dashboard.svelte
··· 24 24 import InviteCodesContent from '../components/dashboard/InviteCodesContent.svelte' 25 25 import DidDocumentContent from '../components/dashboard/DidDocumentContent.svelte' 26 26 import AdminContent from '../components/dashboard/AdminContent.svelte' 27 - import DelegationAuditContent from '../components/dashboard/DelegationAuditContent.svelte' 28 27 29 - type Section = 'settings' | 'security' | 'sessions' | 'app-passwords' | 'comms' | 'repo' | 'controllers' | 'delegation-audit' | 'invite-codes' | 'did-document' | 'admin' 28 + type Section = 'settings' | 'security' | 'sessions' | 'app-passwords' | 'comms' | 'repo' | 'controllers' | 'invite-codes' | 'did-document' | 'admin' 30 29 31 30 const auth = $derived(getAuthState()) 32 31 let dropdownOpen = $state(false) ··· 72 71 '/comms': 'comms', 73 72 '/repo': 'repo', 74 73 '/controllers': 'controllers', 75 - '/delegation-audit': 'delegation-audit', 74 + 76 75 '/invite-codes': 'invite-codes', 77 76 '/did-document': 'did-document', 78 77 '/admin': 'admin', ··· 147 146 'comms': '/comms', 148 147 'repo': '/repo', 149 148 'controllers': '/controllers', 150 - 'delegation-audit': '/delegation-audit', 149 + 151 150 'invite-codes': '/invite-codes', 152 151 'did-document': '/did-document', 153 152 'admin': '/admin', ··· 176 175 { id: 'comms', label: $_('dashboard.navComms'), show: session?.accountKind !== 'migrated' }, 177 176 { id: 'repo', label: $_('dashboard.navRepo'), show: session?.accountKind !== 'migrated' }, 178 177 { id: 'controllers', label: $_('dashboard.navDelegation'), show: session?.accountKind !== 'migrated' }, 179 - { id: 'delegation-audit', label: $_('dashboard.navDelegationAudit'), show: session?.accountKind !== 'migrated' }, 178 + 180 179 { id: 'invite-codes', label: $_('dashboard.navInviteCodes'), show: inviteCodesEnabled && (session?.isAdmin ?? false) && session?.accountKind !== 'migrated' }, 181 180 { id: 'did-document', label: $_('dashboard.navDidDocument'), show: isPdsHostedDidWeb || session?.accountKind === 'migrated', highlight: session?.accountKind === 'migrated' ? 'migrated' : 'did-web' }, 182 181 { id: 'admin', label: $_('dashboard.navAdmin'), show: session?.isAdmin ?? false, highlight: 'admin' }, ··· 303 302 <RepoContent {session} /> 304 303 {:else if currentSection === 'controllers'} 305 304 <ControllersContent {session} /> 306 - {:else if currentSection === 'delegation-audit'} 307 - <DelegationAuditContent {session} /> 305 + 308 306 {:else if currentSection === 'invite-codes'} 309 307 <InviteCodesContent {session} /> 310 308 {:else if currentSection === 'did-document'}
+1 -1
frontend/src/tests/migration/atproto-client.test.ts
··· 1 - import { beforeEach, describe, expect, it } from "vitest"; 1 + import { beforeEach, describe, expect, it, vi } from "vitest"; 2 2 import { 3 3 AtprotoClient, 4 4 base64UrlDecode,

History

1 round 0 comments
sign up or login to add to the discussion
oyster.cafe submitted #0
1 commit
expand
chore: inline delegation audit page
expand 0 comments
pull request successfully merged