Spreadsheet-UX op een echte relationele engine

main

Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.

2.6 kB Ruw bestand
import { createHash, randomBytes } from 'node:crypto';
import type { Account } from '@wellformed/shared';
import { toAccount } from './accounts.js';
import type { ControlDb } from './db.js';

export const SESSION_COOKIE = 'wellformed_session';

/**
 * Sessions are stored hashed, so a leaked control database cannot be replayed.
 *
 * One session spans every workspace the account belongs to. Which workspace a
 * request touches comes from its URL, and is authorised by a membership lookup
 * on each request — never by anything carried in the cookie.
 */
function hashToken(token: string): string {
  return createHash('sha256').update(token).digest('hex');
}

export interface IssuedSession {
  token: string;
  expiresAt: Date;
}

export function createSession(
  control: ControlDb,
  accountId: string,
  ttlMs: number,
  userAgent?: string,
): IssuedSession {
  const token = randomBytes(32).toString('base64url');
  const expiresAt = new Date(Date.now() + ttlMs);

  control.sqlite
    .prepare('INSERT INTO session (token_hash, account_id, expires_at, user_agent) VALUES (?, ?, ?, ?)')
    .run(hashToken(token), accountId, expiresAt.toISOString(), userAgent?.slice(0, 300) ?? null);

  return { token, expiresAt };
}

export function resolveSession(control: ControlDb, token: string | undefined): Account | null {
  if (!token) return null;

  const row = control.sqlite
    .prepare<
      [string],
      {
        id: string;
        email: string;
        name: string;
        disabled: number;
        created_at: string;
        last_seen_at: string | null;
        verified_at: string | null;
        expires_at: string;
      }
    >(
      `SELECT a.id, a.email, a.name, a.disabled, a.created_at, a.last_seen_at, a.verified_at,
              s.expires_at
       FROM session s JOIN account a ON a.id = s.account_id
       WHERE s.token_hash = ?`,
    )
    .get(hashToken(token));

  if (!row) return null;
  if (row.disabled === 1) return null;
  if (new Date(row.expires_at).getTime() <= Date.now()) {
    destroySession(control, token);
    return null;
  }

  return toAccount(row);
}

export function destroySession(control: ControlDb, token: string): void {
  control.sqlite.prepare('DELETE FROM session WHERE token_hash = ?').run(hashToken(token));
}

export function destroyAllSessions(control: ControlDb, accountId: string): void {
  control.sqlite.prepare('DELETE FROM session WHERE account_id = ?').run(accountId);
}

export function pruneExpiredSessions(control: ControlDb): number {
  const info = control.sqlite
    .prepare(`DELETE FROM session WHERE expires_at <= datetime('now')`)
    .run();
  return info.changes;
}