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/com.tranquil.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="#/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>