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">← 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>