this repo has no description
1<script lang="ts">
2 import { getAuthState } from '../lib/auth.svelte'
3 import { navigate } from '../lib/router.svelte'
4 import { _ } from '../lib/i18n'
5 import { formatDateTime } from '../lib/date'
6
7 interface AuditEntry {
8 id: string
9 delegatedDid: string
10 actorDid: string
11 controllerDid: string | null
12 actionType: string
13 actionDetails: Record<string, unknown> | null
14 createdAt: string
15 }
16
17 const auth = getAuthState()
18 let loading = $state(true)
19 let error = $state<string | null>(null)
20 let entries = $state<AuditEntry[]>([])
21 let total = $state(0)
22 let offset = $state(0)
23 const limit = 20
24
25 $effect(() => {
26 if (!auth.loading && !auth.session) {
27 navigate('/login')
28 }
29 })
30
31 $effect(() => {
32 if (auth.session) {
33 loadAuditLog()
34 }
35 })
36
37 async function loadAuditLog() {
38 if (!auth.session) return
39 loading = true
40 error = null
41
42 try {
43 const response = await fetch(
44 `/xrpc/_delegation.getAuditLog?limit=${limit}&offset=${offset}`,
45 {
46 headers: { 'Authorization': `Bearer ${auth.session.accessJwt}` }
47 }
48 )
49
50 if (!response.ok) {
51 const data = await response.json()
52 error = data.message || data.error || $_('delegation.failedToLoadAuditLog')
53 return
54 }
55
56 const data = await response.json()
57 entries = data.entries || []
58 total = data.total || 0
59 } catch (e) {
60 error = $_('delegation.failedToLoadAuditLog')
61 } finally {
62 loading = false
63 }
64 }
65
66 function prevPage() {
67 if (offset > 0) {
68 offset = Math.max(0, offset - limit)
69 loadAuditLog()
70 }
71 }
72
73 function nextPage() {
74 if (offset + limit < total) {
75 offset = offset + limit
76 loadAuditLog()
77 }
78 }
79
80 function formatActionType(type: string): string {
81 const labels: Record<string, string> = {
82 'GrantCreated': $_('delegation.actionGrantCreated'),
83 'GrantRevoked': $_('delegation.actionGrantRevoked'),
84 'ScopesModified': $_('delegation.actionScopesModified'),
85 'TokenIssued': $_('delegation.actionTokenIssued'),
86 'RepoWrite': $_('delegation.actionRepoWrite'),
87 'BlobUpload': $_('delegation.actionBlobUpload'),
88 'AccountAction': $_('delegation.actionAccountAction')
89 }
90 return labels[type] || type
91 }
92
93 function formatActionDetails(details: Record<string, unknown> | null): string {
94 if (!details) return ''
95 const parts: string[] = []
96 for (const [key, value] of Object.entries(details)) {
97 const formattedKey = key.replace(/_/g, ' ')
98 parts.push(`${formattedKey}: ${JSON.stringify(value)}`)
99 }
100 return parts.join(', ')
101 }
102
103 function truncateDid(did: string): string {
104 if (did.length <= 30) return did
105 return did.substring(0, 20) + '...' + did.substring(did.length - 6)
106 }
107</script>
108
109<div class="page">
110 <header>
111 <a href="/app/controllers" class="back">{$_('delegation.backToControllers')}</a>
112 <h1>{$_('delegation.auditLogTitle')}</h1>
113 </header>
114
115 {#if loading}
116 <p class="loading">{$_('delegation.loading')}</p>
117 {:else}
118 {#if error}
119 <div class="message error">{error}</div>
120 {/if}
121
122 {#if entries.length === 0}
123 <p class="empty">{$_('delegation.noActivity')}</p>
124 {:else}
125 <div class="audit-list">
126 {#each entries as entry}
127 <div class="audit-entry">
128 <div class="entry-header">
129 <span class="action-type">{formatActionType(entry.actionType)}</span>
130 <span class="timestamp">{formatDateTime(entry.createdAt)}</span>
131 </div>
132 <div class="entry-details">
133 <div class="detail">
134 <span class="label">{$_('delegation.actor')}</span>
135 <span class="value did" title={entry.actorDid}>{truncateDid(entry.actorDid)}</span>
136 </div>
137 {#if entry.controllerDid}
138 <div class="detail">
139 <span class="label">{$_('delegation.controller')}</span>
140 <span class="value did" title={entry.controllerDid}>{truncateDid(entry.controllerDid)}</span>
141 </div>
142 {/if}
143 <div class="detail">
144 <span class="label">{$_('delegation.account')}</span>
145 <span class="value did" title={entry.delegatedDid}>{truncateDid(entry.delegatedDid)}</span>
146 </div>
147 {#if entry.actionDetails}
148 <div class="detail">
149 <span class="label">{$_('delegation.details')}</span>
150 <span class="value details">{formatActionDetails(entry.actionDetails)}</span>
151 </div>
152 {/if}
153 </div>
154 </div>
155 {/each}
156 </div>
157
158 <div class="pagination">
159 <button
160 class="ghost"
161 onclick={prevPage}
162 disabled={offset === 0}
163 >
164 {$_('delegation.previous')}
165 </button>
166 <span class="page-info">
167 {$_('delegation.showing', { values: { start: offset + 1, end: Math.min(offset + limit, total), total } })}
168 </span>
169 <button
170 class="ghost"
171 onclick={nextPage}
172 disabled={offset + limit >= total}
173 >
174 {$_('delegation.next')}
175 </button>
176 </div>
177 {/if}
178
179 <div class="actions-bar">
180 <button class="ghost" onclick={loadAuditLog}>{$_('delegation.refresh')}</button>
181 </div>
182 {/if}
183</div>
184
185<style>
186 .page {
187 max-width: var(--width-lg);
188 margin: 0 auto;
189 padding: var(--space-7);
190 }
191
192 header {
193 margin-bottom: var(--space-7);
194 }
195
196 .back {
197 color: var(--text-secondary);
198 text-decoration: none;
199 font-size: var(--text-sm);
200 }
201
202 .back:hover {
203 color: var(--accent);
204 }
205
206 h1 {
207 margin: var(--space-2) 0 0 0;
208 }
209
210 .loading,
211 .empty {
212 text-align: center;
213 color: var(--text-secondary);
214 padding: var(--space-7);
215 }
216
217 .message.error {
218 padding: var(--space-3);
219 background: var(--error-bg);
220 border: 1px solid var(--error-border);
221 border-radius: var(--radius-md);
222 color: var(--error-text);
223 margin-bottom: var(--space-4);
224 }
225
226 .audit-list {
227 display: flex;
228 flex-direction: column;
229 gap: var(--space-3);
230 margin-bottom: var(--space-4);
231 }
232
233 .audit-entry {
234 background: var(--bg-secondary);
235 border: 1px solid var(--border-color);
236 border-radius: var(--radius-lg);
237 padding: var(--space-4);
238 }
239
240 .entry-header {
241 display: flex;
242 justify-content: space-between;
243 align-items: center;
244 margin-bottom: var(--space-3);
245 flex-wrap: wrap;
246 gap: var(--space-2);
247 }
248
249 .action-type {
250 font-weight: var(--font-semibold);
251 color: var(--text-primary);
252 }
253
254 .timestamp {
255 font-size: var(--text-sm);
256 color: var(--text-muted);
257 }
258
259 .entry-details {
260 display: flex;
261 flex-direction: column;
262 gap: var(--space-2);
263 }
264
265 .detail {
266 font-size: var(--text-sm);
267 display: flex;
268 gap: var(--space-2);
269 align-items: baseline;
270 flex-wrap: wrap;
271 }
272
273 .detail .label {
274 color: var(--text-secondary);
275 min-width: 80px;
276 }
277
278 .detail .value {
279 color: var(--text-primary);
280 }
281
282 .detail .value.did {
283 font-family: var(--font-mono);
284 font-size: var(--text-xs);
285 word-break: break-all;
286 }
287
288 .detail .value.details {
289 font-size: var(--text-xs);
290 color: var(--text-muted);
291 word-break: break-word;
292 }
293
294 .pagination {
295 display: flex;
296 justify-content: center;
297 align-items: center;
298 gap: var(--space-4);
299 margin: var(--space-5) 0;
300 }
301
302 .pagination button {
303 padding: var(--space-2) var(--space-4);
304 font-size: var(--text-sm);
305 }
306
307 .page-info {
308 font-size: var(--text-sm);
309 color: var(--text-secondary);
310 }
311
312 .actions-bar {
313 display: flex;
314 gap: var(--space-2);
315 flex-wrap: wrap;
316 }
317
318 .actions-bar button {
319 padding: var(--space-2) var(--space-4);
320 font-size: var(--text-sm);
321 }
322</style>