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