diff --git a/backend/src/auth/AuthService.ts b/backend/src/auth/AuthService.ts index 48c9540..8e20f18 100644 --- a/backend/src/auth/AuthService.ts +++ b/backend/src/auth/AuthService.ts @@ -3,7 +3,7 @@ */ import bcrypt from 'bcrypt'; import { randomUUID } from 'crypto'; -import type { DatabaseInterface, DBUser } from '../database/DatabaseInterface.js'; +import type { DatabaseInterface, DBUser, DBSession } from '../database/DatabaseInterface.js'; const SALT_ROUNDS = 10; const SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours @@ -20,8 +20,6 @@ export interface AuthResult { } export class AuthService { - private sessions = new Map(); - constructor(private db: DatabaseInterface) {} async register(email: string, password: string, name: string, role: string = 'planer'): Promise { @@ -64,27 +62,27 @@ export class AuthService { createSession(userId: string): Session { const token = randomUUID(); - const session: Session = { - token, - userId, - expiresAt: Date.now() + SESSION_TTL_MS, - }; - this.sessions.set(token, session); - return session; + const expiresAt = Date.now() + SESSION_TTL_MS; + this.db.createSession({ token, user_id: userId, expires_at: expiresAt }); + return { token, userId, expiresAt }; } validateSession(token: string): Session | null { - const session = this.sessions.get(token); - if (!session) return null; - if (Date.now() > session.expiresAt) { - this.sessions.delete(token); + const dbSession = this.db.getSession(token); + if (!dbSession) return null; + if (Date.now() > dbSession.expires_at) { + this.db.deleteSession(token); return null; } - return session; + return { + token: dbSession.token, + userId: dbSession.user_id, + expiresAt: dbSession.expires_at, + }; } destroySession(token: string): void { - this.sessions.delete(token); + this.db.deleteSession(token); } getUserFromSession(token: string): DBUser | null { diff --git a/backend/src/database/DatabaseInterface.ts b/backend/src/database/DatabaseInterface.ts index 8925cce..8cd7c71 100644 --- a/backend/src/database/DatabaseInterface.ts +++ b/backend/src/database/DatabaseInterface.ts @@ -72,6 +72,13 @@ export interface DBUser { updated_at: string; } +export interface DBSession { + token: string; + user_id: string; + expires_at: number; + created_at: string; +} + export interface DatabaseInterface { init(): Promise; close(): void; @@ -119,4 +126,10 @@ export interface DatabaseInterface { // Settings getSetting(key: string): DBSetting | null; setSetting(key: string, value: string): void; + + // Sessions + createSession(data: Partial): DBSession; + getSession(token: string): DBSession | null; + deleteSession(token: string): boolean; + deleteExpiredSessions(): void; } diff --git a/backend/src/database/SqliteAdapter.ts b/backend/src/database/SqliteAdapter.ts index 4f7d1ec..79777f3 100644 --- a/backend/src/database/SqliteAdapter.ts +++ b/backend/src/database/SqliteAdapter.ts @@ -5,9 +5,10 @@ import Database from 'better-sqlite3'; import { readFileSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; +import { randomUUID } from 'crypto'; import type { DatabaseInterface, DBProject, DBDrawing, DBLayer, - DBElement, DBBlock, DBSetting, DBUser, + DBElement, DBBlock, DBSetting, DBUser, DBSession, } from './DatabaseInterface.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -243,4 +244,25 @@ export class SqliteAdapter implements DatabaseInterface { "INSERT INTO settings (key, value, updated_at) VALUES (?, ?, datetime('now')) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = datetime('now')", ).run(key, value); } + + // ─── Sessions ─────────────────────────────────────── + createSession(data: Partial): DBSession { + const token = data.token ?? randomUUID(); + this.db.prepare( + 'INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, ?)', + ).run(token, data.user_id!, data.expires_at!); + return this.getSession(token)!; + } + + getSession(token: string): DBSession | null { + return (this.db.prepare('SELECT * FROM sessions WHERE token = ?').get(token) as DBSession) ?? null; + } + + deleteSession(token: string): boolean { + return this.db.prepare('DELETE FROM sessions WHERE token = ?').run(token).changes > 0; + } + + deleteExpiredSessions(): void { + this.db.prepare('DELETE FROM sessions WHERE expires_at < ?').run(Date.now()); + } } diff --git a/backend/src/database/schema.sql b/backend/src/database/schema.sql index cfd8935..0fb66e7 100644 --- a/backend/src/database/schema.sql +++ b/backend/src/database/schema.sql @@ -83,6 +83,15 @@ CREATE TABLE IF NOT EXISTS blocks ( FOREIGN KEY (drawing_id) REFERENCES drawings(id) ON DELETE CASCADE ); +-- ─── Sessions ────────────────────────────────────── +CREATE TABLE IF NOT EXISTS sessions ( + token TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + expires_at INTEGER NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); + -- ─── Settings ─────────────────────────────────────── CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY,