const { query } = require('../../utils/db') // SQL for the recovery_codes table. Each row is one bcrypt-hashed, single-use // backup code. The raw codes are shown to the user exactly once at generation and // never stored in the clear. // Bulk-insert freshly generated code hashes for a user. `hashes` is an array of // bcrypt strings. One multi-row INSERT keeps generation atomic-ish and cheap. async function insertMany(userId, hashes) { if (!hashes || hashes.length === 0) return 0 const values = hashes.map(() => '(?, ?)').join(', ') const params = [] for (const h of hashes) params.push(userId, h) const res = await query( `INSERT INTO recovery_codes (user_id, code_hash) VALUES ${values}`, params, ) return Number(res.affectedRows || 0) } // All not-yet-used codes for a user (hashes included — this is the verify path, // server-side only). Ordered by id so verification is deterministic. async function listUnusedForUser(userId) { return query( 'SELECT id, code_hash FROM recovery_codes WHERE user_id = ? AND used_at IS NULL ORDER BY id', [userId], ) } // Count a user's remaining (unused) codes — for the status endpoint (never the // codes themselves). async function countUnusedForUser(userId) { const rows = await query( 'SELECT COUNT(*) AS n FROM recovery_codes WHERE user_id = ? AND used_at IS NULL', [userId], ) return Number(rows[0]?.n || 0) } // Mark one code row used (single-use). Guarded on used_at IS NULL so a race can // only consume it once. Returns rows changed. async function markUsed(id) { const res = await query( 'UPDATE recovery_codes SET used_at = NOW() WHERE id = ? AND used_at IS NULL', [id], ) return Number(res.affectedRows || 0) } // Delete every code for a user. Used both when regenerating (replace the set) and // on TOTP disable / password change/reset. Returns rows removed. async function deleteAllForUser(userId) { const res = await query('DELETE FROM recovery_codes WHERE user_id = ?', [userId]) return Number(res.affectedRows || 0) } module.exports = { insertMany, listUnusedForUser, countUnusedForUser, markUsed, deleteAllForUser, }