spb/drive Public
SPB Drive — self-hosted personal cloud drive (files, previews, sharing) on the MacLustr cluster.
JavaScript 82.7%
CSS 10.6%
Nunjucks 3.6%
Shell 1.8%
SQL 1.3%
1/**2 * ─────────────────────────────────────────────3 * SPB Drive — Personal Cloud Drive4 * ─────────────────────────────────────────────5 * Author : Simon-Pierre Boucher6 * Contact : contact@spboucher.ai7 * File : src/auth/session.mjs8 * Purpose : Cookie sessions (httpOnly/Secure/Lax) + API bearer tokens9 * License : MIT © Simon-Pierre Boucher10 * ─────────────────────────────────────────────11 */1213import { createHash, randomBytes } from 'node:crypto';14import { config } from '../config.mjs';15import { getDb, logActivity } from '../db/db.mjs';1617export const SESSION_COOKIE = 'spbdrive_sid';1819const sha256 = (s) => createHash('sha256').update(s).digest('hex');2021/**22 * Create a session and return the raw cookie value (only the hash is stored).23 * @param {{remember?: boolean, ip?: string, ua?: string}} [opts]24 * @returns {{token: string, expiresAt: number, maxAgeSec: number}}25 */26export function createSession({ remember = false, ip = '', ua = '' } = {}) {27 const token = randomBytes(32).toString('base64url');28 const now = Date.now();29 const ttl = remember ? config.rememberTtlMs : config.sessionTtlMs;30 const expiresAt = now + ttl;31 getDb()32 .prepare(33 `INSERT INTO sessions (token_hash, created, last_seen, expires_at, remember, ip, ua)34 VALUES (?, ?, ?, ?, ?, ?, ?)`,35 )36 .run(sha256(token), now, now, expiresAt, remember ? 1 : 0, ip, ua.slice(0, 300));37 logActivity('auth.login', { ip });38 return { token, expiresAt, maxAgeSec: Math.floor(ttl / 1000) };39}4041/**42 * Validate a session cookie value; slides the expiry forward on use.43 * @returns {object|null} session row or null44 */45export function getSession(token) {46 if (!token) return null;47 const db = getDb();48 const row = db.prepare('SELECT * FROM sessions WHERE token_hash = ?').get(sha256(token));49 if (!row) return null;50 const now = Date.now();51 if (row.expires_at < now) {52 db.prepare('DELETE FROM sessions WHERE id = ?').run(row.id);53 return null;54 }55 // Sliding renewal, throttled to once a minute to avoid write churn.56 if (now - row.last_seen > 60_000) {57 const ttl = row.remember ? config.rememberTtlMs : config.sessionTtlMs;58 db.prepare('UPDATE sessions SET last_seen = ?, expires_at = ? WHERE id = ?').run(59 now, now + ttl, row.id,60 );61 }62 return row;63}6465/** Destroy one session by raw cookie value. */66export function destroySession(token) {67 if (!token) return;68 getDb().prepare('DELETE FROM sessions WHERE token_hash = ?').run(sha256(token));69}7071/** List active sessions (for the Settings page). */72export function listSessions() {73 return getDb()74 .prepare('SELECT id, created, last_seen, expires_at, remember, ip, ua FROM sessions WHERE expires_at > ? ORDER BY last_seen DESC')75 .all(Date.now());76}7778/** Revoke a session by row id; returns true if one was deleted. */79export function revokeSession(id) {80 return getDb().prepare('DELETE FROM sessions WHERE id = ?').run(id).changes > 0;81}8283/** Revoke every session (logout everywhere). */84export function revokeAllSessions() {85 getDb().prepare('DELETE FROM sessions').run();86 logActivity('auth.logout_everywhere');87}8889// ── API tokens (used by the spbdrive CLI) ───────────────────────────────9091/**92 * Create a named API token; the raw value is shown once and stored hashed.93 * @returns {{id: number, token: string}}94 */95export function createApiToken(name) {96 const token = `spbd_${randomBytes(24).toString('base64url')}`;97 const info = getDb()98 .prepare('INSERT INTO api_tokens (name, token_hash, created) VALUES (?, ?, ?)')99 .run(String(name || 'token').slice(0, 80), sha256(token), Date.now());100 logActivity('auth.api_token_created', { detail: name });101 return { id: Number(info.lastInsertRowid), token };102}103104/** Validate a Bearer token; returns the token row or null. */105export function getApiToken(token) {106 if (!token) return null;107 const db = getDb();108 const row = db.prepare('SELECT * FROM api_tokens WHERE token_hash = ?').get(sha256(token));109 if (row) db.prepare('UPDATE api_tokens SET last_used = ? WHERE id = ?').run(Date.now(), row.id);110 return row;111}112113/** List API tokens (names + metadata only). */114export function listApiTokens() {115 return getDb()116 .prepare('SELECT id, name, created, last_used FROM api_tokens ORDER BY created DESC')117 .all();118}119120/** Delete an API token by id. */121export function revokeApiToken(id) {122 return getDb().prepare('DELETE FROM api_tokens WHERE id = ?').run(id).changes > 0;123}124125/** Purge expired sessions (daily maintenance). */126export function pruneSessions() {127 return getDb().prepare('DELETE FROM sessions WHERE expires_at < ?').run(Date.now()).changes;128}129