| 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 async function userCount(): Promise<number> { |
| const row = await getDb().get<{ c: number }>('SELECT COUNT(*) AS c FROM users'); |
| return row?.c ?? 0; |
| } |
|
|
| |
| export async function createUser(email: string, password: string): Promise<SessionUser> { |
| const db = getDb(); |
| const normalized = normalizeEmail(email); |
| const existing = await db.get('SELECT id FROM users WHERE email = ?', [normalized]); |
| if (existing) { |
| const err = new Error('An account with that email already exists') as any; |
| err.code = 'email_taken'; |
| throw err; |
| } |
| const result = await db.run('INSERT INTO users (email, password_hash) VALUES (?, ?)', |
| [normalized, hashPassword(password)]); |
| return { userId: Number(result.lastInsertRowid), email: normalized }; |
| } |
|
|
| |
| export async function verifyCredentials(email: string, password: string): Promise<SessionUser | null> { |
| const db = getDb(); |
| const row = await db.get<{ id: number; email: string; password_hash: string }>( |
| 'SELECT id, email, password_hash FROM users WHERE email = ?', |
| [normalizeEmail(email)], |
| ); |
| if (!row) return null; |
| if (!verifyPassword(password, row.password_hash)) return null; |
| return { userId: row.id, email: row.email }; |
| } |
|
|
| |
| export async function createSession(userId: number): Promise<string> { |
| const token = crypto.randomBytes(32).toString('hex'); |
| await getDb().run('INSERT INTO sessions (token_hash, user_id, expires_at_ms) VALUES (?, ?, ?)', |
| [sha256(token), userId, Date.now() + SESSION_TTL_MS]); |
| return token; |
| } |
|
|
| |
| export async function validateSession(token: string | undefined | null): Promise<SessionUser | null> { |
| if (!token) return null; |
| const db = getDb(); |
| const row = await db.get<{ user_id: number; expires_at_ms: number; email: string }>(` |
| 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 = ? |
| `, [sha256(token)]); |
| if (!row) return null; |
| if (row.expires_at_ms < Date.now()) { |
| await db.run('DELETE FROM sessions WHERE token_hash = ?', [sha256(token)]); |
| return null; |
| } |
| return { userId: row.user_id, email: row.email }; |
| } |
|
|
| export async function deleteSession(token: string | undefined | null): Promise<void> { |
| if (!token) return; |
| await getDb().run('DELETE FROM sessions WHERE token_hash = ?', [sha256(token)]); |
| } |
|
|