import { getDb } from './db'; import bcrypt from 'bcryptjs'; import crypto from 'crypto'; const SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000; const CHALLENGE_TTL_MS = 5 * 60 * 1000; export function verifyPassword(email: string, password: string): string | null { const db = getDb(); const user = db.query('SELECT id, password_hash FROM users WHERE email = ?').get(email) as any; if (!user || !user.password_hash) return null; return bcrypt.compareSync(password, user.password_hash) ? user.id : null; } export function createUser(email: string, password: string, nostrPubkey?: string): string { const db = getDb(); const id = crypto.randomUUID(); const hash = password ? bcrypt.hashSync(password, 10) : null; db.query('INSERT INTO users (id, email, password_hash, nostr_pubkey) VALUES (?, ?, ?, ?)').run(id, email, hash, nostrPubkey || null); return id; } export function findOrCreateNostrUser(pubkey: string): { userId: string; created: boolean } { const db = getDb(); const user = db.query('SELECT id FROM users WHERE nostr_pubkey = ?').get(pubkey) as any; if (user) return { userId: user.id, created: false }; const id = crypto.randomUUID(); db.query('INSERT INTO users (id, nostr_pubkey) VALUES (?, ?)').run(id, pubkey); return { userId: id, created: true }; } export function createSession(userId: string): string { const db = getDb(); const token = crypto.randomBytes(32).toString('hex'); db.query('INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, ?)').run(token, userId, Date.now() + SESSION_TTL_MS); return token; } export function getUserFromSession(token: string): any { const db = getDb(); const session = db.query('SELECT user_id, expires_at FROM sessions WHERE token = ?').get(token) as any; if (!session) return null; if (session.expires_at < Date.now()) { db.query('DELETE FROM sessions WHERE token = ?').run(token); return null; } return db.query('SELECT id, email, nostr_pubkey, is_admin, delivery_prefs, name, phone, password_hash FROM users WHERE id = ?').get(session.user_id); } export function deleteSession(token: string) { getDb().query('DELETE FROM sessions WHERE token = ?').run(token); } export function updateUserPrefs(userId: string, prefs: { name?: string; phone?: string; email?: string; delivery_prefs?: string }): any { const db = getDb(); const sets: string[] = []; const vals: any[] = []; if (prefs.name !== undefined && prefs.name !== '') { sets.push('name = ?'); vals.push(prefs.name); } if (prefs.phone !== undefined && prefs.phone !== '') { sets.push('phone = ?'); vals.push(prefs.phone); } if (prefs.email !== undefined && prefs.email !== '') { sets.push('email = ?'); vals.push(prefs.email); } if (prefs.delivery_prefs !== undefined) { sets.push('delivery_prefs = ?'); vals.push(prefs.delivery_prefs); } if (sets.length === 0) return null; vals.push(userId); db.query(`UPDATE users SET ${sets.join(', ')} WHERE id = ?`).run(...vals); return db.query('SELECT id, email, nostr_pubkey, is_admin, delivery_prefs, name, phone FROM users WHERE id = ?').get(userId); } export function setUserPassword(userId: string, password: string): boolean { if (!password || password.length < 6) return false; const hash = bcrypt.hashSync(password, 10); getDb().query('UPDATE users SET password_hash = ? WHERE id = ?').run(hash, userId); return true; } export function issueNostrChallenge(): string { const db = getDb(); const challenge = crypto.randomBytes(32).toString('hex'); db.query('DELETE FROM nostr_challenges WHERE created_at < ?').run(Date.now() - CHALLENGE_TTL_MS); db.query('INSERT INTO nostr_challenges (challenge, created_at) VALUES (?, ?)').run(challenge, Date.now()); return challenge; } function consumeChallenge(challenge: string): boolean { const db = getDb(); const row = db.query('SELECT created_at FROM nostr_challenges WHERE challenge = ?').get(challenge) as any; if (!row) return false; db.query('DELETE FROM nostr_challenges WHERE challenge = ?').run(challenge); return row.created_at >= Date.now() - CHALLENGE_TTL_MS; } // verifies a NIP-07 signed auth event: kind 22242, a ["challenge", ] // tag matching a server-issued challenge, signed by `pubkey`. The event id is // recomputed server-side; the client-supplied id is ignored. export function verifyNostrAuthEvent(ev: any): string | null { try { if (!ev || ev.kind !== 22242) return null; if (typeof ev.pubkey !== 'string' || !/^[0-9a-f]{64}$/.test(ev.pubkey)) return null; if (typeof ev.sig !== 'string' || !/^[0-9a-f]{128}$/.test(ev.sig)) return null; if (!Array.isArray(ev.tags)) return null; const tag = ev.tags.find((t: any) => Array.isArray(t) && t[0] === 'challenge' && typeof t[1] === 'string'); if (!tag) return null; if (Math.abs(Date.now() / 1000 - ev.created_at) > 600) return null; if (!consumeChallenge(tag[1])) return null; const serialized = JSON.stringify([0, ev.pubkey, ev.created_at, ev.kind, ev.tags, ev.content ?? '']); const id = crypto.createHash('sha256').update(new TextEncoder().encode(serialized)).digest(); const { schnorr } = require('@noble/curves/secp256k1.js'); const ok = schnorr.verify(Buffer.from(ev.sig, 'hex'), id, Buffer.from(ev.pubkey, 'hex')); return ok ? ev.pubkey : null; } catch { return null; } }