const crypto = require('crypto'); const { get, run } = require('./sqlite'); const JWT_SECRET = process.env.JWT_SECRET || 'greenlens-dev-secret-change-in-prod'; const TOKEN_EXPIRY_SECONDS = 365 * 24 * 3600; // 1 year // ─── Minimal JWT (HS256, no external deps) ───────────────────────────────── const b64url = (input) => { const str = typeof input === 'string' ? input : JSON.stringify(input); return Buffer.from(str).toString('base64url'); }; const b64urlDecode = (str) => Buffer.from(str, 'base64url').toString(); const signJwt = (payload) => { const header = b64url({ alg: 'HS256', typ: 'JWT' }); const body = b64url(payload); const sig = crypto.createHmac('sha256', JWT_SECRET).update(`${header}.${body}`).digest('base64url'); return `${header}.${body}.${sig}`; }; const verifyJwt = (token) => { if (!token || typeof token !== 'string') return null; const parts = token.split('.'); if (parts.length !== 3) return null; const [header, body, sig] = parts; const expected = crypto.createHmac('sha256', JWT_SECRET).update(`${header}.${body}`).digest('base64url'); if (sig !== expected) return null; try { const payload = JSON.parse(b64urlDecode(body)); if (payload.exp && Math.floor(Date.now() / 1000) > payload.exp) return null; return payload; } catch { return null; } }; const issueToken = (userId, email, name) => signJwt({ sub: userId, email, name, iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + TOKEN_EXPIRY_SECONDS, }); // ─── Password hashing ────────────────────────────────────────────────────── const hashPassword = (password) => crypto.createHmac('sha256', JWT_SECRET).update(password).digest('hex'); // ─── Schema ──────────────────────────────────────────────────────────────── const ensureAuthSchema = async (db) => { await run( db, `CREATE TABLE IF NOT EXISTS auth_users ( id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE COLLATE NOCASE, name TEXT NOT NULL DEFAULT '', password_hash TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) )`, ); }; // ─── Operations ─────────────────────────────────────────────────────────── const signUp = async (db, email, name, password) => { const normalizedEmail = email.trim().toLowerCase(); const existing = await get(db, 'SELECT id FROM auth_users WHERE email = ?', [normalizedEmail]); if (existing) { const err = new Error('Email already in use.'); err.code = 'EMAIL_TAKEN'; err.status = 409; throw err; } const id = `usr_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`; await run(db, 'INSERT INTO auth_users (id, email, name, password_hash) VALUES (?, ?, ?, ?)', [ id, normalizedEmail, name.trim(), hashPassword(password), ]); return { id, email: normalizedEmail, name: name.trim() }; }; const login = async (db, email, password) => { const normalizedEmail = email.trim().toLowerCase(); const user = await get(db, 'SELECT id, email, name, password_hash FROM auth_users WHERE email = ?', [normalizedEmail]); if (!user) { const err = new Error('No account found for this email.'); err.code = 'USER_NOT_FOUND'; err.status = 401; throw err; } if (user.password_hash !== hashPassword(password)) { const err = new Error('Wrong password.'); err.code = 'WRONG_PASSWORD'; err.status = 401; throw err; } return { id: user.id, email: user.email, name: user.name }; }; module.exports = { ensureAuthSchema, signUp, login, issueToken, verifyJwt };