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