mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-08-27 19:36:49 +00:00
chore: inline delegation audit page
This commit is contained in:
@@ -91,7 +91,7 @@
|
||||
'/comms',
|
||||
'/repo',
|
||||
'/controllers',
|
||||
'/delegation-audit',
|
||||
|
||||
'/invite-codes',
|
||||
'/did-document',
|
||||
'/admin',
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
disabled?: boolean
|
||||
placeholder?: string
|
||||
id?: string
|
||||
autocomplete?: string
|
||||
autocomplete?: HTMLInputElement['autocomplete']
|
||||
onInput: (value: string) => void
|
||||
onDomainChange: (domain: string) => void
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { onMount, onDestroy } from 'svelte'
|
||||
import { _ } from '../../lib/i18n'
|
||||
import { api } from '../../lib/api'
|
||||
import { toast } from '../../lib/toast.svelte'
|
||||
import { formatDateTime } from '../../lib/date'
|
||||
import { routes, getFullUrl } from '../../lib/router.svelte'
|
||||
import type { Session, DelegationController, DelegationControlledAccount, DelegationScopePreset } from '../../lib/types/api'
|
||||
import type { Session, DelegationController, DelegationControlledAccount, DelegationScopePreset, DelegationAuditEntry } from '../../lib/types/api'
|
||||
import { unsafeAsDid, unsafeAsScopeSet, unsafeAsHandle, unsafeAsEmail } from '../../lib/types/branded'
|
||||
import type { Did, Handle, ScopeSet } from '../../lib/types/branded'
|
||||
import LoadMoreSentinel from '../LoadMoreSentinel.svelte'
|
||||
|
||||
interface Props {
|
||||
session: Session
|
||||
@@ -123,7 +123,8 @@
|
||||
let creatingDelegated = $state(false)
|
||||
|
||||
onMount(async () => {
|
||||
await loadData()
|
||||
await Promise.all([loadData(), loadAuditLog()])
|
||||
pollInterval = setInterval(pollAuditLog, 15_000)
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
@@ -227,6 +228,126 @@
|
||||
if ((scopes as string) === '') return $_('delegation.scopeViewer')
|
||||
return $_('delegation.scopeCustom')
|
||||
}
|
||||
|
||||
interface AuditEntry {
|
||||
id: string
|
||||
delegatedDid: string
|
||||
actorDid: string
|
||||
actionType: string
|
||||
actionDetails: Record<string, unknown> | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
let auditLoading = $state(true)
|
||||
let auditLoadingMore = $state(false)
|
||||
let auditEntries = $state<AuditEntry[]>([])
|
||||
let auditHasMore = $state(true)
|
||||
let auditOffset = $state(0)
|
||||
const auditLimit = 20
|
||||
let pollInterval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
onDestroy(() => {
|
||||
if (pollInterval) clearInterval(pollInterval)
|
||||
})
|
||||
|
||||
async function loadAuditLog() {
|
||||
auditLoading = true
|
||||
auditOffset = 0
|
||||
try {
|
||||
const result = await api.getDelegationAuditLog(session.accessJwt, auditLimit, 0)
|
||||
if (result.ok && result.value) {
|
||||
const rawEntries = Array.isArray(result.value.entries) ? result.value.entries : []
|
||||
auditEntries = rawEntries.map(mapAuditEntry)
|
||||
const total = result.value.total ?? 0
|
||||
auditHasMore = auditEntries.length < total
|
||||
auditOffset = auditEntries.length
|
||||
} else {
|
||||
auditEntries = []
|
||||
auditHasMore = false
|
||||
}
|
||||
} catch {
|
||||
toast.error($_('delegation.failedToLoadAudit'))
|
||||
auditEntries = []
|
||||
auditHasMore = false
|
||||
} finally {
|
||||
auditLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function pollAuditLog() {
|
||||
try {
|
||||
const result = await api.getDelegationAuditLog(session.accessJwt, auditLimit, 0)
|
||||
if (result.ok && result.value) {
|
||||
const rawEntries = Array.isArray(result.value.entries) ? result.value.entries : []
|
||||
const latest = rawEntries.map(mapAuditEntry)
|
||||
if (latest.length > 0 && (auditEntries.length === 0 || latest[0].id !== auditEntries[0].id)) {
|
||||
auditEntries = latest
|
||||
const total = result.value.total ?? 0
|
||||
auditHasMore = auditEntries.length < total
|
||||
auditOffset = auditEntries.length
|
||||
}
|
||||
} else if (result.ok === false) {
|
||||
if (pollInterval) { clearInterval(pollInterval); pollInterval = null }
|
||||
}
|
||||
} catch {
|
||||
if (pollInterval) { clearInterval(pollInterval); pollInterval = null }
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMoreAuditEntries() {
|
||||
if (auditLoadingMore || !auditHasMore) return
|
||||
auditLoadingMore = true
|
||||
try {
|
||||
const result = await api.getDelegationAuditLog(session.accessJwt, auditLimit, auditOffset)
|
||||
if (result.ok && result.value) {
|
||||
const rawEntries = Array.isArray(result.value.entries) ? result.value.entries : []
|
||||
const newEntries = rawEntries.map(mapAuditEntry)
|
||||
auditEntries = [...auditEntries, ...newEntries]
|
||||
const total = result.value.total ?? 0
|
||||
auditHasMore = auditEntries.length < total
|
||||
auditOffset = auditEntries.length
|
||||
}
|
||||
} catch {
|
||||
toast.error($_('delegation.failedToLoadAudit'))
|
||||
} finally {
|
||||
auditLoadingMore = false
|
||||
}
|
||||
}
|
||||
|
||||
function mapAuditEntry(e: DelegationAuditEntry): AuditEntry {
|
||||
const parsed: Record<string, unknown> | null = e.details
|
||||
? (() => { try { return JSON.parse(e.details!) as Record<string, unknown> } catch { return null } })()
|
||||
: null
|
||||
return {
|
||||
id: e.id,
|
||||
delegatedDid: e.target_did ?? '',
|
||||
actorDid: e.actor_did,
|
||||
actionType: e.action,
|
||||
actionDetails: parsed,
|
||||
createdAt: e.created_at,
|
||||
}
|
||||
}
|
||||
|
||||
function formatActionType(type: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
'GrantCreated': $_('delegation.actionGrantCreated'),
|
||||
'GrantRevoked': $_('delegation.actionGrantRevoked'),
|
||||
'ScopesModified': $_('delegation.actionScopesModified'),
|
||||
'TokenIssued': $_('delegation.actionTokenIssued'),
|
||||
'RepoWrite': $_('delegation.actionRepoWrite'),
|
||||
'BlobUpload': $_('delegation.actionBlobUpload'),
|
||||
'AccountAction': $_('delegation.actionAccountAction'),
|
||||
}
|
||||
return labels[type] || type
|
||||
}
|
||||
|
||||
function formatActionDetails(details: Record<string, unknown> | null): string {
|
||||
if (!details) return ''
|
||||
return Object.entries(details)
|
||||
.map(([key, value]) => `${key.replace(/_/g, ' ')}: ${JSON.stringify(value)}`)
|
||||
.join(', ')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="controllers">
|
||||
@@ -454,6 +575,10 @@
|
||||
{$_('delegation.createDelegatedAccountButton')}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<div class="constraint-notice">
|
||||
<p>{$_('delegation.controlledAccountsLocalOnly')}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
@@ -461,7 +586,43 @@
|
||||
<h3>{$_('delegation.auditLog')}</h3>
|
||||
<p class="section-description">{$_('delegation.auditLogDesc')}</p>
|
||||
</div>
|
||||
<a href={getFullUrl(routes.delegationAudit)} class="btn-link">{$_('delegation.viewAuditLog')}</a>
|
||||
|
||||
{#if auditLoading}
|
||||
<div class="loading">{$_('common.loading')}</div>
|
||||
{:else if auditEntries.length === 0}
|
||||
<p class="empty">{$_('delegation.noAuditEntries')}</p>
|
||||
{:else}
|
||||
<div class="audit-entries">
|
||||
{#each auditEntries as entry}
|
||||
<div class="audit-entry">
|
||||
<div class="audit-entry-header">
|
||||
<span class="action-type">{formatActionType(entry.actionType)}</span>
|
||||
<span class="audit-entry-date">{formatDateTime(entry.createdAt)}</span>
|
||||
</div>
|
||||
<div class="audit-entry-details">
|
||||
<div class="detail">
|
||||
<span class="label">{$_('delegation.actor')}</span>
|
||||
<span class="value did">{entry.actorDid}</span>
|
||||
</div>
|
||||
{#if entry.delegatedDid}
|
||||
<div class="detail">
|
||||
<span class="label">{$_('delegation.target')}</span>
|
||||
<span class="value did">{entry.delegatedDid}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if entry.actionDetails}
|
||||
<div class="detail">
|
||||
<span class="label">{$_('delegation.details')}</span>
|
||||
<span class="value audit-details-value">{formatActionDetails(entry.actionDetails)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<LoadMoreSentinel hasMore={auditHasMore} loading={auditLoadingMore} onLoadMore={loadMoreAuditEntries} />
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -505,6 +666,7 @@
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-4);
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
.constraint-notice p {
|
||||
@@ -846,6 +1008,52 @@
|
||||
border: 1px solid var(--info-border, var(--border-color));
|
||||
}
|
||||
|
||||
.audit-entries {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.audit-entry {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.audit-entry-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.action-type {
|
||||
font-weight: var(--font-medium);
|
||||
padding: var(--space-1) var(--space-2);
|
||||
background: var(--accent);
|
||||
color: var(--text-inverse);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.audit-entry-date {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.audit-entry-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.audit-details-value {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-xs);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.item-card {
|
||||
flex-direction: column;
|
||||
@@ -861,5 +1069,18 @@
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.audit-entry-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.audit-entry-details .value.did {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 60vw;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,282 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { _ } from '../../lib/i18n'
|
||||
import { api } from '../../lib/api'
|
||||
import { toast } from '../../lib/toast.svelte'
|
||||
import { formatDateTime } from '../../lib/date'
|
||||
import type { Session, DelegationAuditEntry } from '../../lib/types/api'
|
||||
import LoadMoreSentinel from '../LoadMoreSentinel.svelte'
|
||||
|
||||
interface Props {
|
||||
session: Session
|
||||
}
|
||||
|
||||
let { session }: Props = $props()
|
||||
|
||||
interface AuditEntry {
|
||||
id: string
|
||||
delegatedDid: string
|
||||
actorDid: string
|
||||
controllerDid: string | null
|
||||
actionType: string
|
||||
actionDetails: Record<string, unknown> | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
let loading = $state(true)
|
||||
let loadingMore = $state(false)
|
||||
let entries = $state<AuditEntry[]>([])
|
||||
let hasMore = $state(true)
|
||||
let offset = $state(0)
|
||||
const limit = 20
|
||||
|
||||
onMount(async () => {
|
||||
await loadAuditLog()
|
||||
})
|
||||
|
||||
async function loadAuditLog() {
|
||||
loading = true
|
||||
offset = 0
|
||||
try {
|
||||
const result = await api.getDelegationAuditLog(session.accessJwt, limit, 0)
|
||||
if (result.ok && result.value) {
|
||||
const rawEntries = Array.isArray(result.value.entries) ? result.value.entries : []
|
||||
entries = rawEntries.map(mapEntry)
|
||||
const total = result.value.total ?? 0
|
||||
hasMore = entries.length < total
|
||||
offset = entries.length
|
||||
} else {
|
||||
entries = []
|
||||
hasMore = false
|
||||
}
|
||||
} catch {
|
||||
toast.error($_('delegation.failedToLoadAudit'))
|
||||
entries = []
|
||||
hasMore = false
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMoreEntries() {
|
||||
if (loadingMore || !hasMore) return
|
||||
loadingMore = true
|
||||
try {
|
||||
const result = await api.getDelegationAuditLog(session.accessJwt, limit, offset)
|
||||
if (result.ok && result.value) {
|
||||
const rawEntries = Array.isArray(result.value.entries) ? result.value.entries : []
|
||||
const newEntries = rawEntries.map(mapEntry)
|
||||
entries = [...entries, ...newEntries]
|
||||
const total = result.value.total ?? 0
|
||||
hasMore = entries.length < total
|
||||
offset = entries.length
|
||||
}
|
||||
} catch {
|
||||
toast.error($_('delegation.failedToLoadAudit'))
|
||||
} finally {
|
||||
loadingMore = false
|
||||
}
|
||||
}
|
||||
|
||||
function mapEntry(e: DelegationAuditEntry): AuditEntry {
|
||||
return {
|
||||
id: e.id,
|
||||
delegatedDid: e.target_did ?? '',
|
||||
actorDid: e.actor_did,
|
||||
controllerDid: null,
|
||||
actionType: e.action,
|
||||
actionDetails: e.details ? JSON.parse(e.details) : null,
|
||||
createdAt: e.created_at
|
||||
}
|
||||
}
|
||||
|
||||
function formatActionType(type: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
'GrantCreated': $_('delegation.actionGrantCreated'),
|
||||
'GrantRevoked': $_('delegation.actionGrantRevoked'),
|
||||
'ScopesModified': $_('delegation.actionScopesModified'),
|
||||
'TokenIssued': $_('delegation.actionTokenIssued'),
|
||||
'RepoWrite': $_('delegation.actionRepoWrite'),
|
||||
'BlobUpload': $_('delegation.actionBlobUpload'),
|
||||
'AccountAction': $_('delegation.actionAccountAction')
|
||||
}
|
||||
return labels[type] || type
|
||||
}
|
||||
|
||||
function formatActionDetails(details: Record<string, unknown> | null): string {
|
||||
if (!details) return ''
|
||||
return Object.entries(details)
|
||||
.map(([key, value]) => `${key.replace(/_/g, ' ')}: ${JSON.stringify(value)}`)
|
||||
.join(', ')
|
||||
}
|
||||
|
||||
function truncateDid(did: string): string {
|
||||
if (did.length <= 30) return did
|
||||
return did.substring(0, 20) + '...' + did.substring(did.length - 6)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="audit">
|
||||
<div class="actions-bar">
|
||||
<button type="button" class="ghost" onclick={() => loadAuditLog()} disabled={loading}>
|
||||
{loading ? $_('common.loading') : $_('delegation.refresh')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="loading">{$_('common.loading')}</div>
|
||||
{:else if entries.length === 0}
|
||||
<p class="empty">{$_('delegation.noAuditEntries')}</p>
|
||||
{:else}
|
||||
<div class="entries">
|
||||
{#each entries as entry}
|
||||
<div class="entry">
|
||||
<div class="entry-header">
|
||||
<span class="action-type">{formatActionType(entry.actionType)}</span>
|
||||
<span class="entry-date">{formatDateTime(entry.createdAt)}</span>
|
||||
</div>
|
||||
<div class="entry-details">
|
||||
<div class="detail">
|
||||
<span class="label">{$_('delegation.actor')}</span>
|
||||
<span class="value did" title={entry.actorDid}>{truncateDid(entry.actorDid)}</span>
|
||||
</div>
|
||||
{#if entry.delegatedDid}
|
||||
<div class="detail">
|
||||
<span class="label">{$_('delegation.target')}</span>
|
||||
<span class="value did" title={entry.delegatedDid}>{truncateDid(entry.delegatedDid)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if entry.actionDetails}
|
||||
<div class="detail">
|
||||
<span class="label">{$_('delegation.details')}</span>
|
||||
<span class="value details">{formatActionDetails(entry.actionDetails)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<LoadMoreSentinel {hasMore} loading={loadingMore} onLoadMore={loadMoreEntries} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.audit {
|
||||
max-width: var(--width-lg);
|
||||
}
|
||||
|
||||
.actions-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.ghost {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ghost:hover:not(:disabled) {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.ghost:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.empty {
|
||||
color: var(--text-secondary);
|
||||
padding: var(--space-6);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.entries {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.entry {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.entry-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.action-type {
|
||||
font-weight: var(--font-medium);
|
||||
padding: var(--space-1) var(--space-2);
|
||||
background: var(--accent);
|
||||
color: var(--text-inverse);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.entry-date {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.entry-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.detail {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.detail .label {
|
||||
color: var(--text-secondary);
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.detail .value {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.detail .value.did {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.detail .value.details {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-xs);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.entry-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.detail {
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.detail .label {
|
||||
min-width: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -255,11 +255,6 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.danger-text {
|
||||
color: var(--error-text);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.code-meta {
|
||||
display: flex;
|
||||
gap: var(--space-4);
|
||||
|
||||
@@ -9,7 +9,7 @@ export const routes = {
|
||||
comms: "/comms",
|
||||
repo: "/repo",
|
||||
controllers: "/controllers",
|
||||
delegationAudit: "/delegation-audit",
|
||||
|
||||
actAs: "/act-as",
|
||||
didDocument: "/did-document",
|
||||
migrate: "/migrate",
|
||||
|
||||
@@ -144,7 +144,6 @@
|
||||
"navComms": "Communication Preferences",
|
||||
"navRepo": "Repository Explorer",
|
||||
"navDelegation": "Delegation",
|
||||
"navDelegationAudit": "Delegation Audit",
|
||||
"navAdmin": "Admin Panel",
|
||||
"navDidDocument": "DID Document",
|
||||
"migrated": "Migrated",
|
||||
@@ -836,6 +835,7 @@
|
||||
"controllerRemoved": "Controller removed successfully",
|
||||
"controlledAccounts": "Controlled Accounts",
|
||||
"controlledAccountsDesc": "Accounts you can act on behalf of",
|
||||
"controlledAccountsLocalOnly": "Only accounts on this PDS are shown. Another PDS may have granted you controller access separately",
|
||||
"noControlledAccounts": "You do not have access to any delegated accounts.",
|
||||
"actAs": "Act As",
|
||||
"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,7 +848,6 @@
|
||||
"accountCreated": "Created delegated account: {handle}",
|
||||
"auditLog": "Audit Log",
|
||||
"auditLogDesc": "View all delegation activity",
|
||||
"viewAuditLog": "View Audit Log",
|
||||
"scopeOwner": "Owner",
|
||||
"scopeViewer": "Viewer",
|
||||
"scopeCustom": "Custom",
|
||||
@@ -856,7 +855,6 @@
|
||||
"details": "Details",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"refresh": "Refresh",
|
||||
"actionGrantCreated": "Grant Created",
|
||||
"actionGrantRevoked": "Grant Revoked",
|
||||
"actionScopesModified": "Scopes Modified",
|
||||
@@ -866,7 +864,6 @@
|
||||
"actionAccountAction": "Account Action",
|
||||
"noAuditEntries": "No audit entries",
|
||||
"target": "Target",
|
||||
"pageInfo": "{start} - {end} of {total}",
|
||||
"failedToLoadAudit": "Failed to load audit log"
|
||||
},
|
||||
"actAs": {
|
||||
|
||||
@@ -124,7 +124,6 @@
|
||||
"dashboard": {
|
||||
"title": "Hallintapaneeli",
|
||||
"accountManager": "Tilinhallinta",
|
||||
"navDelegationAudit": "Delegointiloki",
|
||||
"switchAccount": "Vaihda tiliä",
|
||||
"addAnotherAccount": "Lisää toinen tili",
|
||||
"signOut": "Kirjaudu ulos @{handle}",
|
||||
@@ -835,6 +834,7 @@
|
||||
"controllerRemoved": "Hallinnoija poistettu",
|
||||
"controlledAccounts": "Hallinnoidut tilit",
|
||||
"controlledAccountsDesc": "Tilit, joiden puolesta voit toimia",
|
||||
"controlledAccountsLocalOnly": "Vain tämän PDS:n tilit näytetään. Toinen PDS on voinut myöntää sinulle ohjausoikeuden erikseen",
|
||||
"noControlledAccounts": "Sinulla ei ole pääsyä delegoituihin tileihin.",
|
||||
"actAs": "Toimi käyttäjänä",
|
||||
"cannotControlAccounts": "Et voi hallinnoida muita tilejä, koska tällä tilillä on hallinnoijia. Tili voi joko olla hallinnoija tai hallinnoidaan, mutta ei molempia.",
|
||||
@@ -847,7 +847,6 @@
|
||||
"accountCreated": "Delegoitu tili luotu: {handle}",
|
||||
"auditLog": "Tapahtumaloki",
|
||||
"auditLogDesc": "Näytä kaikki delegointitoiminta",
|
||||
"viewAuditLog": "Näytä tapahtumaloki",
|
||||
"scopeOwner": "Omistaja",
|
||||
"scopeViewer": "Katsoja",
|
||||
"scopeCustom": "Mukautettu",
|
||||
@@ -855,7 +854,6 @@
|
||||
"details": "Tiedot",
|
||||
"previous": "Edellinen",
|
||||
"next": "Seuraava",
|
||||
"refresh": "Päivitä",
|
||||
"actionGrantCreated": "Oikeus luotu",
|
||||
"actionGrantRevoked": "Oikeus peruttu",
|
||||
"actionScopesModified": "Oikeuksia muokattu",
|
||||
@@ -865,7 +863,6 @@
|
||||
"actionAccountAction": "Tilitoiminto",
|
||||
"noAuditEntries": "Ei lokimerkintöjä",
|
||||
"target": "Kohde",
|
||||
"pageInfo": "{start} - {end} / {total}",
|
||||
"failedToLoadAudit": "Lokin lataus epäonnistui"
|
||||
},
|
||||
"actAs": {
|
||||
|
||||
@@ -124,7 +124,6 @@
|
||||
"dashboard": {
|
||||
"title": "ダッシュボード",
|
||||
"accountManager": "アカウント管理",
|
||||
"navDelegationAudit": "委任監査",
|
||||
"switchAccount": "アカウント切替",
|
||||
"addAnotherAccount": "別のアカウントを追加",
|
||||
"signOut": "@{handle} からサインアウト",
|
||||
@@ -834,7 +833,6 @@
|
||||
"actionAccountAction": "アカウントアクション",
|
||||
"previous": "前へ",
|
||||
"next": "次へ",
|
||||
"refresh": "更新",
|
||||
"adding": "追加中...",
|
||||
"accessLevel": "アクセスレベル",
|
||||
"addControllerButton": "+ コントローラーを追加",
|
||||
@@ -848,6 +846,7 @@
|
||||
"cannotAddControllers": "他のアカウントを管理しているため、コントローラーを追加できません。アカウントはコントローラーを持つか、他のアカウントを管理するかのいずれかのみ可能です。",
|
||||
"cannotControlAccounts": "このアカウントにはコントローラーがいるため、他のアカウントを管理できません。アカウントはコントローラーを持つか、他のアカウントを管理するかのいずれかのみ可能です。",
|
||||
"controlledAccountsDesc": "あなたが代わりに操作できるアカウント",
|
||||
"controlledAccountsLocalOnly": "このPDS上のアカウントのみ表示されます。別のPDSがコントローラーアクセスを個別に付与している場合があります",
|
||||
"controllerAdded": "コントローラーを追加しました",
|
||||
"controllerDid": "コントローラーDID",
|
||||
"controllerRemoved": "コントローラーを削除しました",
|
||||
@@ -860,12 +859,10 @@
|
||||
"inactive": "非アクティブ",
|
||||
"remove": "削除",
|
||||
"removeConfirm": "このコントローラーを削除しますか?",
|
||||
"viewAuditLog": "監査ログを表示",
|
||||
"yourAccessLevel": "あなたのアクセスレベル",
|
||||
"accountCreated": "委任アカウントを作成しました: {handle}",
|
||||
"noAuditEntries": "監査エントリなし",
|
||||
"target": "対象",
|
||||
"pageInfo": "{start} - {end} / {total}",
|
||||
"failedToLoadAudit": "監査ログの読み込みに失敗しました"
|
||||
},
|
||||
"actAs": {
|
||||
|
||||
@@ -124,7 +124,6 @@
|
||||
"dashboard": {
|
||||
"title": "대시보드",
|
||||
"accountManager": "계정 관리",
|
||||
"navDelegationAudit": "위임 감사",
|
||||
"switchAccount": "계정 전환",
|
||||
"addAnotherAccount": "다른 계정 추가",
|
||||
"signOut": "@{handle} 로그아웃",
|
||||
@@ -834,7 +833,6 @@
|
||||
"actionAccountAction": "계정 작업",
|
||||
"previous": "이전",
|
||||
"next": "다음",
|
||||
"refresh": "새로고침",
|
||||
"adding": "추가 중...",
|
||||
"accessLevel": "액세스 수준",
|
||||
"addControllerButton": "+ 컨트롤러 추가",
|
||||
@@ -848,6 +846,7 @@
|
||||
"cannotAddControllers": "다른 계정을 관리하고 있어 컨트롤러를 추가할 수 없습니다. 계정은 컨트롤러를 가지거나 다른 계정을 관리할 수 있지만 둘 다는 불가능합니다.",
|
||||
"cannotControlAccounts": "이 계정에 컨트롤러가 있어 다른 계정을 관리할 수 없습니다. 계정은 컨트롤러를 가지거나 다른 계정을 관리할 수 있지만 둘 다는 불가능합니다.",
|
||||
"controlledAccountsDesc": "귀하가 대신 작업할 수 있는 계정",
|
||||
"controlledAccountsLocalOnly": "이 PDS의 계정만 표시됩니다. 다른 PDS에서 별도로 컨트롤러 액세스를 부여했을 수 있습니다",
|
||||
"controllerAdded": "컨트롤러가 추가되었습니다",
|
||||
"controllerDid": "컨트롤러 DID",
|
||||
"controllerRemoved": "컨트롤러가 제거되었습니다",
|
||||
@@ -860,12 +859,10 @@
|
||||
"inactive": "비활성",
|
||||
"remove": "제거",
|
||||
"removeConfirm": "이 컨트롤러를 제거하시겠습니까?",
|
||||
"viewAuditLog": "감사 로그 보기",
|
||||
"yourAccessLevel": "귀하의 액세스 수준",
|
||||
"accountCreated": "위임 계정이 생성되었습니다: {handle}",
|
||||
"noAuditEntries": "감사 항목 없음",
|
||||
"target": "대상",
|
||||
"pageInfo": "{start} - {end} / {total}",
|
||||
"failedToLoadAudit": "감사 로그 로딩 실패"
|
||||
},
|
||||
"actAs": {
|
||||
|
||||
@@ -124,7 +124,6 @@
|
||||
"dashboard": {
|
||||
"title": "Kontrollpanel",
|
||||
"accountManager": "Kontohantering",
|
||||
"navDelegationAudit": "Delegeringsgranskning",
|
||||
"switchAccount": "Byt konto",
|
||||
"addAnotherAccount": "Lägg till ett annat konto",
|
||||
"signOut": "Logga ut @{handle}",
|
||||
@@ -834,7 +833,6 @@
|
||||
"actionAccountAction": "Kontoåtgärd",
|
||||
"previous": "Föregående",
|
||||
"next": "Nästa",
|
||||
"refresh": "Uppdatera",
|
||||
"adding": "Lägger till...",
|
||||
"accessLevel": "Åtkomstnivå",
|
||||
"addControllerButton": "+ Lägg till kontrollant",
|
||||
@@ -848,6 +846,7 @@
|
||||
"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.",
|
||||
"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.",
|
||||
"controlledAccountsDesc": "Konton du kan agera för",
|
||||
"controlledAccountsLocalOnly": "Endast konton på denna PDS visas. En annan PDS kan ha beviljat dig kontrollåtkomst separat",
|
||||
"controllerAdded": "Kontrollant tillagd",
|
||||
"controllerDid": "Kontrollant-DID",
|
||||
"controllerRemoved": "Kontrollant borttagen",
|
||||
@@ -860,12 +859,10 @@
|
||||
"inactive": "Inaktiv",
|
||||
"remove": "Ta bort",
|
||||
"removeConfirm": "Vill du ta bort denna kontrollant?",
|
||||
"viewAuditLog": "Visa granskningslogg",
|
||||
"yourAccessLevel": "Din åtkomstnivå",
|
||||
"accountCreated": "Skapade delegerat konto: {handle}",
|
||||
"noAuditEntries": "Inga granskningsposter",
|
||||
"target": "Mål",
|
||||
"pageInfo": "{start} - {end} av {total}",
|
||||
"failedToLoadAudit": "Kunde inte ladda granskningslogg"
|
||||
},
|
||||
"actAs": {
|
||||
|
||||
@@ -124,7 +124,6 @@
|
||||
"dashboard": {
|
||||
"title": "控制台",
|
||||
"accountManager": "账户管理",
|
||||
"navDelegationAudit": "委托审计",
|
||||
"switchAccount": "切换账户",
|
||||
"addAnotherAccount": "添加其他账户",
|
||||
"signOut": "退出 @{handle}",
|
||||
@@ -835,7 +834,6 @@
|
||||
"actionAccountAction": "账户操作",
|
||||
"previous": "上一页",
|
||||
"next": "下一页",
|
||||
"refresh": "刷新",
|
||||
"adding": "添加中...",
|
||||
"accessLevel": "访问级别",
|
||||
"addControllerButton": "+ 添加控制者",
|
||||
@@ -849,6 +847,7 @@
|
||||
"cannotAddControllers": "因为此账户正在控制其他账户,所以无法添加控制者。账户只能拥有控制者或控制其他账户,不能同时两者兼备。",
|
||||
"cannotControlAccounts": "因为此账户有控制者,所以无法控制其他账户。账户只能拥有控制者或控制其他账户,不能同时两者兼备。",
|
||||
"controlledAccountsDesc": "您可以代理操作的账户",
|
||||
"controlledAccountsLocalOnly": "仅显示此PDS上的账户。其他PDS可能已单独授予您控制者访问权限",
|
||||
"controllerAdded": "控制者已添加",
|
||||
"controllerDid": "控制者 DID",
|
||||
"controllerRemoved": "控制者已移除",
|
||||
@@ -861,11 +860,9 @@
|
||||
"inactive": "未激活",
|
||||
"remove": "移除",
|
||||
"removeConfirm": "确定要移除此控制者吗?",
|
||||
"viewAuditLog": "查看审计日志",
|
||||
"yourAccessLevel": "您的访问级别",
|
||||
"noAuditEntries": "无审计记录",
|
||||
"target": "目标",
|
||||
"pageInfo": "{start} - {end} / {total}",
|
||||
"failedToLoadAudit": "加载审计日志失败"
|
||||
},
|
||||
"actAs": {
|
||||
|
||||
@@ -24,9 +24,8 @@
|
||||
import InviteCodesContent from '../components/dashboard/InviteCodesContent.svelte'
|
||||
import DidDocumentContent from '../components/dashboard/DidDocumentContent.svelte'
|
||||
import AdminContent from '../components/dashboard/AdminContent.svelte'
|
||||
import DelegationAuditContent from '../components/dashboard/DelegationAuditContent.svelte'
|
||||
|
||||
type Section = 'settings' | 'security' | 'sessions' | 'app-passwords' | 'comms' | 'repo' | 'controllers' | 'delegation-audit' | 'invite-codes' | 'did-document' | 'admin'
|
||||
type Section = 'settings' | 'security' | 'sessions' | 'app-passwords' | 'comms' | 'repo' | 'controllers' | 'invite-codes' | 'did-document' | 'admin'
|
||||
|
||||
const auth = $derived(getAuthState())
|
||||
let dropdownOpen = $state(false)
|
||||
@@ -72,7 +71,7 @@
|
||||
'/comms': 'comms',
|
||||
'/repo': 'repo',
|
||||
'/controllers': 'controllers',
|
||||
'/delegation-audit': 'delegation-audit',
|
||||
|
||||
'/invite-codes': 'invite-codes',
|
||||
'/did-document': 'did-document',
|
||||
'/admin': 'admin',
|
||||
@@ -147,7 +146,7 @@
|
||||
'comms': '/comms',
|
||||
'repo': '/repo',
|
||||
'controllers': '/controllers',
|
||||
'delegation-audit': '/delegation-audit',
|
||||
|
||||
'invite-codes': '/invite-codes',
|
||||
'did-document': '/did-document',
|
||||
'admin': '/admin',
|
||||
@@ -176,7 +175,7 @@
|
||||
{ id: 'comms', label: $_('dashboard.navComms'), show: session?.accountKind !== 'migrated' },
|
||||
{ id: 'repo', label: $_('dashboard.navRepo'), show: session?.accountKind !== 'migrated' },
|
||||
{ id: 'controllers', label: $_('dashboard.navDelegation'), show: session?.accountKind !== 'migrated' },
|
||||
{ id: 'delegation-audit', label: $_('dashboard.navDelegationAudit'), show: session?.accountKind !== 'migrated' },
|
||||
|
||||
{ id: 'invite-codes', label: $_('dashboard.navInviteCodes'), show: inviteCodesEnabled && (session?.isAdmin ?? false) && session?.accountKind !== 'migrated' },
|
||||
{ id: 'did-document', label: $_('dashboard.navDidDocument'), show: isPdsHostedDidWeb || session?.accountKind === 'migrated', highlight: session?.accountKind === 'migrated' ? 'migrated' : 'did-web' },
|
||||
{ id: 'admin', label: $_('dashboard.navAdmin'), show: session?.isAdmin ?? false, highlight: 'admin' },
|
||||
@@ -303,8 +302,7 @@
|
||||
<RepoContent {session} />
|
||||
{:else if currentSection === 'controllers'}
|
||||
<ControllersContent {session} />
|
||||
{:else if currentSection === 'delegation-audit'}
|
||||
<DelegationAuditContent {session} />
|
||||
|
||||
{:else if currentSection === 'invite-codes'}
|
||||
<InviteCodesContent {session} />
|
||||
{:else if currentSection === 'did-document'}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
AtprotoClient,
|
||||
base64UrlDecode,
|
||||
|
||||
Reference in New Issue
Block a user