// ── TOTP (RFC 6238) helpers ──────────────────────────────────────────────── // // Thin wrapper around speakeasy so the controllers stay small and the verify // logic is unit-testable in isolation. TOTP is opt-in per user: we generate a // base32 secret, show the user a QR (otpauth URL) to add to their authenticator, // confirm one code before enabling, and verify a code at login for users who // have it enabled. const speakeasy = require('speakeasy') const QRCode = require('qrcode') const ISSUER = process.env.TOTP_ISSUER || 'UOMysticmoon' // Generate a new secret. Returns the base32 secret to persist plus the otpauth // URL to encode in a QR code. function generateSecret(username) { const secret = speakeasy.generateSecret({ length: 20, name: `${ISSUER} (${username})`, issuer: ISSUER, }) return { base32: secret.base32, otpauthUrl: secret.otpauth_url } } // Render an otpauth URL to a PNG data URL for . async function qrDataUrl(otpauthUrl) { return QRCode.toDataURL(otpauthUrl) } // Verify a user-supplied 6-digit code against a stored base32 secret. A window // of 1 tolerates minor clock skew (±30s). Returns false for missing inputs // rather than throwing. function verifyCode(base32Secret, token) { if (!base32Secret || !token) return false return speakeasy.totp.verify({ secret: base32Secret, encoding: 'base32', token: String(token).trim(), window: 1, }) } module.exports = { generateSecret, qrDataUrl, verifyCode, ISSUER }