const { query } = require('../../utils/db') // SQL for the mobile SSO authorization bridge (mobile_auth_sessions + // mobile_auth_codes). The authorization code is stored as a sha256 hash only // (hashing is done in the .model layer, mirroring mobileSessions); the PKCE // code_challenge is a hash by construction and is stored as-supplied. Both // tables self-prune on expiry (pruneExpired). See docs BACKEND_DESIGN §3/§4. // ── mobile_auth_sessions ─────────────────────────────────────────────────── // Insert a pending bridge session. expiresAt is a JS Date (or ms epoch). async function insertSession({ sessionId, provider, codeChallenge, redirectUri, state, expiresAt }) { const res = await query( `INSERT INTO mobile_auth_sessions (session_id, provider, code_challenge, redirect_uri, state, expires_at) VALUES (?, ?, ?, ?, ?, ?)`, [sessionId, provider, codeChallenge, redirectUri, state, new Date(expiresAt)], ) return res.insertId } // Look up a bridge session by its opaque session_id. Returns the row or null. async function getSession(sessionId) { const rows = await query('SELECT * FROM mobile_auth_sessions WHERE session_id = ? LIMIT 1', [sessionId]) return rows[0] || null } // Mark a still-pending, unexpired session `completed` and attach the resolved // user. Guards on status + expiry so a replayed callback can't re-complete a // consumed/expired session. Returns rows changed (0 = not eligible). async function completeSession(sessionId, userId) { const res = await query( `UPDATE mobile_auth_sessions SET status = 'completed', user_id = ? WHERE session_id = ? AND status = 'pending' AND expires_at > NOW()`, [userId, sessionId], ) return Number(res.affectedRows || 0) } // Record that the user asked to trust this device on the Custom Tab TOTP form. // Guarded on status + expiry for the same reason completeSession is: a replayed // TOTP post must not re-arm a session that has already been consumed. Stores a // boolean only — the trust token is minted at /exchange and never lands here. async function setTrustDevice(sessionId) { const res = await query( `UPDATE mobile_auth_sessions SET trust_device = 1 WHERE session_id = ? AND status = 'pending' AND expires_at > NOW()`, [sessionId], ) return Number(res.affectedRows || 0) } // Mark a session `consumed` after a successful token exchange (stamps used_at). async function consumeSession(sessionId) { const res = await query( "UPDATE mobile_auth_sessions SET status = 'consumed', used_at = NOW() WHERE session_id = ?", [sessionId], ) return Number(res.affectedRows || 0) } // ── mobile_auth_codes ────────────────────────────────────────────────────── // Insert a freshly minted authorization code (by hash). async function insertCode({ codeHash, userId, sessionId, expiresAt }) { const res = await query( `INSERT INTO mobile_auth_codes (code_hash, user_id, session_id, expires_at) VALUES (?, ?, ?, ?)`, [codeHash, userId, sessionId, new Date(expiresAt)], ) return res.insertId } // Return an authorization-code row only if it is still redeemable: not used and // not expired. Returns the row (incl. user_id, session_id) or null. async function findValidCode(codeHash) { const rows = await query( `SELECT * FROM mobile_auth_codes WHERE code_hash = ? AND used_at IS NULL AND expires_at > NOW() LIMIT 1`, [codeHash], ) return rows[0] || null } // Atomically mark a code used (single-use gate). Only affects a not-yet-used row, // so a concurrent double-redeem sees affectedRows = 0 on the loser. Returns rows // changed. async function markCodeUsed(codeHash) { const res = await query( 'UPDATE mobile_auth_codes SET used_at = NOW() WHERE code_hash = ? AND used_at IS NULL', [codeHash], ) return Number(res.affectedRows || 0) } // Housekeeping: drop long-dead rows from both bridge tables (expired, or codes // already used). Keeps the tables from growing without bound. Returns rows removed. async function pruneExpired() { const a = await query('DELETE FROM mobile_auth_codes WHERE expires_at < NOW() OR used_at IS NOT NULL') const b = await query("DELETE FROM mobile_auth_sessions WHERE expires_at < NOW() OR status = 'consumed'") return Number(a.affectedRows || 0) + Number(b.affectedRows || 0) } module.exports = { insertSession, getSession, completeSession, setTrustDevice, consumeSession, insertCode, findValidCode, markCodeUsed, pruneExpired, }