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 { api, ApiError } from '../lib/api' 5 const auth = getAuthState() 6 let loading = $state(true) 7 let error = $state<string | null>(null) 8 let sessions = $state<Array<{ 9 id: string 10 createdAt: string 11 expiresAt: string 12 isCurrent: boolean 13 }>>([]) 14 $effect(() => { 15 if (!auth.loading && !auth.session) { 16 navigate('/login') 17 } 18 }) 19 $effect(() => { 20 if (auth.session) { 21 loadSessions() 22 } 23 }) 24 async function loadSessions() { 25 if (!auth.session) return 26 loading = true 27 error = null 28 try { 29 const result = await api.listSessions(auth.session.accessJwt) 30 sessions = result.sessions 31 } catch (e) { 32 error = e instanceof ApiError ? e.message : 'Failed to load sessions' 33 } finally { 34 loading = false 35 } 36 } 37 async function revokeSession(sessionId: string, isCurrent: boolean) { 38 if (!auth.session) return 39 const msg = isCurrent 40 ? 'This will log you out of this session. Continue?' 41 : 'Revoke this session?' 42 if (!confirm(msg)) return 43 try { 44 await api.revokeSession(auth.session.accessJwt, sessionId) 45 if (isCurrent) { 46 navigate('/login') 47 } else { 48 sessions = sessions.filter(s => s.id !== sessionId) 49 } 50 } catch (e) { 51 error = e instanceof ApiError ? e.message : 'Failed to revoke session' 52 } 53 } 54 function formatDate(dateStr: string): string { 55 return new Date(dateStr).toLocaleString() 56 } 57 function timeAgo(dateStr: string): string { 58 const date = new Date(dateStr) 59 const now = new Date() 60 const diff = now.getTime() - date.getTime() 61 const days = Math.floor(diff / (1000 * 60 * 60 * 24)) 62 const hours = Math.floor(diff / (1000 * 60 * 60)) 63 const minutes = Math.floor(diff / (1000 * 60)) 64 if (days > 0) return `${days} day${days > 1 ? 's' : ''} ago` 65 if (hours > 0) return `${hours} hour${hours > 1 ? 's' : ''} ago` 66 if (minutes > 0) return `${minutes} minute${minutes > 1 ? 's' : ''} ago` 67 return 'Just now' 68 } 69</script> 70<div class="page"> 71 <header> 72 <a href="#/dashboard" class="back">&larr; Dashboard</a> 73 <h1>Active Sessions</h1> 74 </header> 75 {#if loading} 76 <p class="loading">Loading sessions...</p> 77 {:else} 78 {#if error} 79 <div class="message error">{error}</div> 80 {/if} 81 {#if sessions.length === 0} 82 <p class="empty">No active sessions found.</p> 83 {:else} 84 <div class="sessions-list"> 85 {#each sessions as session} 86 <div class="session-card" class:current={session.isCurrent}> 87 <div class="session-info"> 88 <div class="session-header"> 89 {#if session.isCurrent} 90 <span class="badge current">Current Session</span> 91 {:else} 92 <span class="session-label">Session</span> 93 {/if} 94 </div> 95 <div class="session-details"> 96 <div class="detail"> 97 <span class="label">Created:</span> 98 <span class="value">{timeAgo(session.createdAt)}</span> 99 </div> 100 <div class="detail"> 101 <span class="label">Expires:</span> 102 <span class="value">{formatDate(session.expiresAt)}</span> 103 </div> 104 </div> 105 </div> 106 <div class="session-actions"> 107 <button 108 class="revoke-btn" 109 class:danger={!session.isCurrent} 110 onclick={() => revokeSession(session.id, session.isCurrent)} 111 > 112 {session.isCurrent ? 'Sign Out' : 'Revoke'} 113 </button> 114 </div> 115 </div> 116 {/each} 117 </div> 118 <button class="refresh-btn" onclick={loadSessions}>Refresh</button> 119 {/if} 120 {/if} 121</div> 122<style> 123 .page { 124 max-width: 600px; 125 margin: 0 auto; 126 padding: 2rem; 127 } 128 header { 129 margin-bottom: 2rem; 130 } 131 .back { 132 color: var(--text-secondary); 133 text-decoration: none; 134 font-size: 0.875rem; 135 } 136 .back:hover { 137 color: var(--accent); 138 } 139 h1 { 140 margin: 0.5rem 0 0 0; 141 } 142 .loading, .empty { 143 text-align: center; 144 color: var(--text-secondary); 145 padding: 2rem; 146 } 147 .message { 148 padding: 0.75rem; 149 border-radius: 4px; 150 margin-bottom: 1rem; 151 } 152 .message.error { 153 background: var(--error-bg); 154 border: 1px solid var(--error-border); 155 color: var(--error-text); 156 } 157 .sessions-list { 158 display: flex; 159 flex-direction: column; 160 gap: 1rem; 161 } 162 .session-card { 163 background: var(--bg-secondary); 164 border: 1px solid var(--border-color); 165 border-radius: 8px; 166 padding: 1rem; 167 display: flex; 168 justify-content: space-between; 169 align-items: center; 170 } 171 .session-card.current { 172 border-color: var(--accent); 173 background: var(--bg-card); 174 } 175 .session-header { 176 margin-bottom: 0.5rem; 177 } 178 .session-label { 179 font-weight: 500; 180 color: var(--text-secondary); 181 } 182 .badge { 183 display: inline-block; 184 padding: 0.125rem 0.5rem; 185 border-radius: 4px; 186 font-size: 0.75rem; 187 font-weight: 500; 188 } 189 .badge.current { 190 background: var(--accent); 191 color: white; 192 } 193 .session-details { 194 display: flex; 195 flex-direction: column; 196 gap: 0.25rem; 197 } 198 .detail { 199 font-size: 0.875rem; 200 } 201 .detail .label { 202 color: var(--text-secondary); 203 margin-right: 0.5rem; 204 } 205 .detail .value { 206 color: var(--text-primary); 207 } 208 .revoke-btn { 209 padding: 0.5rem 1rem; 210 border: 1px solid var(--border-color); 211 border-radius: 4px; 212 background: transparent; 213 color: var(--text-primary); 214 cursor: pointer; 215 font-size: 0.875rem; 216 } 217 .revoke-btn:hover { 218 background: var(--bg-card); 219 } 220 .revoke-btn.danger { 221 border-color: var(--error-text); 222 color: var(--error-text); 223 } 224 .revoke-btn.danger:hover { 225 background: var(--error-bg); 226 } 227 .refresh-btn { 228 margin-top: 1rem; 229 padding: 0.5rem 1rem; 230 background: transparent; 231 border: 1px solid var(--border-color); 232 border-radius: 4px; 233 cursor: pointer; 234 color: var(--text-primary); 235 } 236 .refresh-btn:hover { 237 background: var(--bg-card); 238 border-color: var(--accent); 239 } 240</style>