Add per-device active sessions with revocation (#69)

Replaces the stateless signed-cookie session (bare user_id) with a
real server-side sessions table -- the cookie now just carries an
opaque session id, resolved against the DB on every request. Each
session records IP address (respects X-Forwarded-For), a parsed
device label, and last-seen time (throttled updates, not written on
every request).

New GET/DELETE /api/auth/sessions endpoints and an "Active sessions"
section in Profile settings let a user see every device they're
logged in from and revoke one they don't recognize -- including their
own current session, which just signs them out.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-28 20:22:50 -06:00
co-authored by Claude Sonnet 5
parent 278f8bb995
commit b26643527d
15 changed files with 554 additions and 14 deletions
+11 -1
View File
@@ -1,5 +1,5 @@
import { apiFetch, ApiError, NetworkError } from './client'
import type { User } from '../types'
import type { User, UserSession } from '../types'
// No register() here: this is an invite-only site. Accounts are created by
// an operator via the backend CLI (`python -m app.cli create-user`), not
@@ -16,6 +16,16 @@ export function logout(): Promise<void> {
return apiFetch<void>('/api/auth/logout', { method: 'POST' })
}
// #69: every device/browser currently logged into this account, newest
// last-seen first -- see backend's app/schemas/session.py.
export function listSessions(): Promise<UserSession[]> {
return apiFetch<UserSession[]>('/api/auth/sessions')
}
export function revokeSession(sessionId: string): Promise<void> {
return apiFetch<void>(`/api/auth/sessions/${sessionId}`, { method: 'DELETE' })
}
export function me(): Promise<User> {
return apiFetch<User>('/api/auth/me')
}