| import crypto from 'crypto';
|
| import { getDb } from '../db/index.js';
|
| import { hashPassword, verifyPassword } from '../lib/password.js';
|
|
|
|
|
|
|
|
|
|
|
| const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
|
| export interface SessionUser {
|
| userId: number;
|
| email: string;
|
| }
|
|
|
| function sha256(s: string): string {
|
| return crypto.createHash('sha256').update(s).digest('hex');
|
| }
|
|
|
| function normalizeEmail(email: string): string {
|
| return email.trim().toLowerCase();
|
| }
|
|
|
| export function userCount(): number {
|
| const row = getDb().prepare('SELECT COUNT(*) AS c FROM users').get() as { c: number };
|
| return row.c;
|
| }
|
|
|
|
|
| export function createUser(email: string, password: string): SessionUser {
|
| const db = getDb();
|
| const normalized = normalizeEmail(email);
|
| const existing = db.prepare('SELECT id FROM users WHERE email = ?').get(normalized);
|
| if (existing) {
|
| const err = new Error('An account with that email already exists') as any;
|
| err.code = 'email_taken';
|
| throw err;
|
| }
|
| const result = db.prepare('INSERT INTO users (email, password_hash) VALUES (?, ?)')
|
| .run(normalized, hashPassword(password));
|
| return { userId: Number(result.lastInsertRowid), email: normalized };
|
| }
|
|
|
|
|
| export function verifyCredentials(email: string, password: string): SessionUser | null {
|
| const db = getDb();
|
| const row = db.prepare('SELECT id, email, password_hash FROM users WHERE email = ?')
|
| .get(normalizeEmail(email)) as { id: number; email: string; password_hash: string } | undefined;
|
| if (!row) return null;
|
| if (!verifyPassword(password, row.password_hash)) return null;
|
| return { userId: row.id, email: row.email };
|
| }
|
|
|
|
|
| export function createSession(userId: number): string {
|
| const token = crypto.randomBytes(32).toString('hex');
|
| getDb().prepare('INSERT INTO sessions (token_hash, user_id, expires_at_ms) VALUES (?, ?, ?)')
|
| .run(sha256(token), userId, Date.now() + SESSION_TTL_MS);
|
| return token;
|
| }
|
|
|
|
|
| export function validateSession(token: string | undefined | null): SessionUser | null {
|
| if (!token) return null;
|
| const db = getDb();
|
| const row = db.prepare(`
|
| SELECT s.user_id, s.expires_at_ms, u.email
|
| FROM sessions s JOIN users u ON u.id = s.user_id
|
| WHERE s.token_hash = ?
|
| `).get(sha256(token)) as { user_id: number; expires_at_ms: number; email: string } | undefined;
|
| if (!row) return null;
|
| if (row.expires_at_ms < Date.now()) {
|
| db.prepare('DELETE FROM sessions WHERE token_hash = ?').run(sha256(token));
|
| return null;
|
| }
|
| return { userId: row.user_id, email: row.email };
|
| }
|
|
|
| export function deleteSession(token: string | undefined | null): void {
|
| if (!token) return;
|
| getDb().prepare('DELETE FROM sessions WHERE token_hash = ?').run(sha256(token));
|
| }
|
|
|