Spaces:
Sleeping
Sleeping
File size: 3,244 Bytes
ed57015 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | import crypto from 'crypto';
import { getDb } from '../db/index.js';
import { hashPassword, verifyPassword } from '../lib/password.js';
// Dashboard authentication: email + password accounts with opaque session
// tokens. Distinct from the unified API key, which authenticates the /v1 proxy
// for apps — this gates the /api/* admin surface for the human operator (#35).
const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
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;
}
/** Create a user. Throws { code: 'email_taken' } if the email already exists. */
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 };
}
/** Verify credentials. Returns the user on success, null on failure. */
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 };
}
/** Mint a session and return the raw token (only the hash is persisted). */
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;
}
/** Resolve a session token to its user, or null if missing/expired. */
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));
}
|