const { query } = require('../../utils/db') const PUBLIC_COLS = 'id, username, role, status, email, email_verified, totp_enabled, created_at, last_login_at' // passwordHash may be null (SSO-provisioned players who have not set one yet). // email/status/emailVerified are optional so existing admin-create callers are // unaffected. async function insertUser({ username, passwordHash = null, role = 'admin', email = null, status = 'active', emailVerified = false, }) { const res = await query( 'INSERT INTO users (username, password_hash, role, email, status, email_verified) VALUES (?, ?, ?, ?, ?, ?)', [username, passwordHash, role, email, status, emailVerified ? 1 : 0], ) return res.insertId } async function findByUsername(username) { const rows = await query('SELECT * FROM users WHERE username = ? LIMIT 1', [username]) return rows[0] || null } async function findById(id) { const rows = await query('SELECT * FROM users WHERE id = ? LIMIT 1', [id]) return rows[0] || null } async function listUsers() { return query(`SELECT ${PUBLIC_COLS} FROM users ORDER BY id ASC`) } async function updateUser(id, fields) { const cols = [] const params = [] for (const [key, val] of Object.entries(fields)) { cols.push(`${key} = ?`) params.push(val) } if (cols.length === 0) return params.push(id) await query(`UPDATE users SET ${cols.join(', ')} WHERE id = ?`, params) } async function deleteUser(id) { return query('DELETE FROM users WHERE id = ?', [id]) } async function countUsers() { const rows = await query('SELECT COUNT(*) AS c FROM users') return Number(rows[0].c) } async function countAdmins() { const rows = await query("SELECT COUNT(*) AS c FROM users WHERE role = 'admin'") return Number(rows[0].c) } async function touchLastLogin(id, ip = null) { return query('UPDATE users SET last_login_at = NOW(), last_login_ip = ? WHERE id = ?', [ip, id]) } // Move the "tokens valid after" cutoff to now, invalidating every session token // issued before this instant (password change / log out everywhere). requireAuth // compares each session's issued-at against this column. async function bumpTokensValidAfter(id) { return query('UPDATE users SET tokens_valid_after = NOW() WHERE id = ?', [id]) } // Set the cutoff to an explicit instant. Used when re-issuing the caller's own // session right after a password change: the bump above revokes everything at // NOW(), and requireAuth's cutoff test is inclusive (createdAt <= cutoff), so a // freshly-minted token sharing that same wall-clock second would be revoked too. // Rewinding the cutoff a hair below the new token's issued-at lets it survive // while still revoking every older session. async function setTokensValidAfter(id, when) { return query('UPDATE users SET tokens_valid_after = ? WHERE id = ?', [when, id]) } // Store a (not-yet-enabled) TOTP secret for a user. Enabling is a separate step // so a secret is never trusted until the user has confirmed one code. async function setTotpSecret(id, secret) { return query('UPDATE users SET totp_secret = ?, totp_enabled = 0 WHERE id = ?', [secret, id]) } async function enableTotp(id) { return query('UPDATE users SET totp_enabled = 1 WHERE id = ?', [id]) } async function disableTotp(id) { return query('UPDATE users SET totp_secret = NULL, totp_enabled = 0 WHERE id = ?', [id]) } module.exports = { insertUser, findByUsername, findById, listUsers, updateUser, deleteUser, countUsers, countAdmins, touchLastLogin, bumpTokensValidAfter, setTokensValidAfter, setTotpSecret, enableTotp, disableTotp, }