SPB Git

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%
4.9 KB · 129 lines javascript
Raw Blame History
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