Add session management: track active sessions, show on profile, allow revocation
Some checks failed
Deploy / Deploy Production (push) Has been skipped
Deploy / Deploy Preview (push) Failing after 1s
Deploy / Cleanup Preview (push) Has been skipped
CI / UI / Build (pull_request) Failing after 13s
CI / Scraper / Lint (pull_request) Successful in 19s
CI / Scraper / Test (pull_request) Successful in 20s
CI / Scraper / Build (pull_request) Successful in 12s
Some checks failed
Deploy / Deploy Production (push) Has been skipped
Deploy / Deploy Preview (push) Failing after 1s
Deploy / Cleanup Preview (push) Has been skipped
CI / UI / Build (pull_request) Failing after 13s
CI / Scraper / Lint (pull_request) Successful in 19s
CI / Scraper / Test (pull_request) Successful in 20s
CI / Scraper / Build (pull_request) Successful in 12s
- Add user_sessions PocketBase collection (user_id, session_id, user_agent, ip, created/last_seen) - Extend auth token format to include a per-login authSessionId (4th segment) - Hook validates authSessionId against DB on each request; revoked sessions are cleared immediately - Login/register create a session record capturing user-agent and IP - Profile page shows all active sessions with current session highlighted; per-session End/Sign out buttons - GET /api/sessions and DELETE /api/sessions/[id] endpoints for client-side revocation - Backward compatible: legacy 3-segment tokens pass through without DB check
This commit is contained in:
32
ui/src/routes/api/sessions/+server.ts
Normal file
32
ui/src/routes/api/sessions/+server.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { json, error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { listUserSessions } from '$lib/server/pocketbase';
|
||||
import { log } from '$lib/server/logger';
|
||||
|
||||
/**
|
||||
* GET /api/sessions
|
||||
* Returns all active sessions for the logged-in user.
|
||||
*/
|
||||
export const GET: RequestHandler = async ({ locals }) => {
|
||||
if (!locals.user) {
|
||||
error(401, 'Not logged in');
|
||||
}
|
||||
|
||||
try {
|
||||
const sessions = await listUserSessions(locals.user.id);
|
||||
// Don't expose raw session_id to the client — only the record ID for revocation
|
||||
const safe = sessions.map((s) => ({
|
||||
id: s.id,
|
||||
user_agent: s.user_agent,
|
||||
ip: s.ip,
|
||||
created_at: s.created_at,
|
||||
last_seen: s.last_seen,
|
||||
// Tell the client whether this is the currently active session
|
||||
is_current: s.session_id === locals.user!.authSessionId
|
||||
}));
|
||||
return json({ sessions: safe });
|
||||
} catch (e) {
|
||||
log.error('sessions', 'GET failed', { err: String(e) });
|
||||
error(500, 'Failed to load sessions');
|
||||
}
|
||||
};
|
||||
41
ui/src/routes/api/sessions/[id]/+server.ts
Normal file
41
ui/src/routes/api/sessions/[id]/+server.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { json, error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { revokeUserSession } from '$lib/server/pocketbase';
|
||||
import { log } from '$lib/server/logger';
|
||||
|
||||
/**
|
||||
* DELETE /api/sessions/[id]
|
||||
* Revokes a specific session by its PocketBase record ID.
|
||||
* Only the owner can revoke their own sessions.
|
||||
*/
|
||||
export const DELETE: RequestHandler = async ({ params, locals, cookies }) => {
|
||||
if (!locals.user) {
|
||||
error(401, 'Not logged in');
|
||||
}
|
||||
|
||||
const recordId = params.id;
|
||||
if (!recordId) {
|
||||
error(400, 'Session ID required');
|
||||
}
|
||||
|
||||
try {
|
||||
const ok = await revokeUserSession(recordId, locals.user.id);
|
||||
if (!ok) {
|
||||
error(404, 'Session not found or not yours');
|
||||
}
|
||||
|
||||
// If the user is terminating their own current session, clear their auth cookie
|
||||
// so they get logged out immediately (the hook would do this on the next request anyway,
|
||||
// but clearing it here gives instant feedback for the "end this session" flow).
|
||||
// For other sessions, we leave the cookie intact.
|
||||
// We detect "current session" via authSessionId — but since the client sends the
|
||||
// record ID (not the session_id), we rely on the UI to redirect after ending its own session.
|
||||
|
||||
log.info('sessions', 'session revoked', { recordId, userId: locals.user.id });
|
||||
return json({ ok: true });
|
||||
} catch (e) {
|
||||
if (e instanceof Error && 'status' in e) throw e; // re-throw SvelteKit errors
|
||||
log.error('sessions', 'DELETE failed', { recordId, err: String(e) });
|
||||
error(500, 'Failed to revoke session');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user