const express = require('express'); const router = express.Router(); const speakeasy = require('speakeasy'); const QRCode = require('qrcode'); const crypto = require('crypto'); const { authenticate } = require('../middleware/auth'); const { modifyLimiter, authLimiter, readLimiter } = require('../middleware/rateLimiter'); const { db } = require('../database/db'); const logger = require('../utils/logger'); const { promisify } = require('util'); const dbRun = promisify(db.run.bind(db)); const dbGet = promisify(db.get.bind(db)); const dbAll = promisify(db.all.bind(db)); // Generate 2FA secret and QR code router.post('/setup', authenticate, modifyLimiter, async (req, res) => { try { const userId = req.user.userId; // Check if user already has 2FA enabled const user = await dbGet('SELECT two_factor_enabled FROM users WHERE id = ?', [userId]); if (user.two_factor_enabled) { return res.status(400).json({ error: '2FA is already enabled' }); } // Generate secret with StreamFlow branding const secret = speakeasy.generateSecret({ name: `StreamFlow IPTV:${req.user.username || 'User'}`, issuer: 'StreamFlow' }); // Generate QR code with custom options for better readability const qrCodeDataURL = await QRCode.toDataURL(secret.otpauth_url, { width: 300, margin: 2, color: { dark: '#000000', light: '#FFFFFF' }, errorCorrectionLevel: 'H' }); // Store secret temporarily (not enabled yet) await dbRun( 'UPDATE users SET two_factor_secret = ? WHERE id = ?', [secret.base32, userId] ); res.json({ secret: secret.base32, qrCode: qrCodeDataURL, manualEntryKey: secret.base32 }); } catch (error) { logger.error('2FA setup error:', error); res.status(500).json({ error: 'Failed to setup 2FA' }); } }); // Verify and enable 2FA router.post('/enable', authenticate, authLimiter, async (req, res) => { try { const { token } = req.body; const userId = req.user.userId; if (!token) { return res.status(400).json({ error: 'Verification token required' }); } // Get user's secret const user = await dbGet( 'SELECT two_factor_secret FROM users WHERE id = ?', [userId] ); if (!user.two_factor_secret) { return res.status(400).json({ error: '2FA not set up. Call /setup first' }); } // Verify token const verified = speakeasy.totp.verify({ secret: user.two_factor_secret, encoding: 'base32', token: token, window: 2 // Allow 2 time steps before/after }); if (!verified) { return res.status(400).json({ error: 'Invalid verification code' }); } // Generate backup codes const backupCodes = []; for (let i = 0; i < 10; i++) { const code = crypto.randomBytes(4).toString('hex').toUpperCase(); backupCodes.push(code); await dbRun( 'INSERT INTO two_factor_backup_codes (user_id, code) VALUES (?, ?)', [userId, code] ); } // Enable 2FA await dbRun( 'UPDATE users SET two_factor_enabled = 1 WHERE id = ?', [userId] ); logger.info(`2FA enabled for user ${userId}`); res.json({ success: true, message: '2FA enabled successfully', backupCodes: backupCodes }); } catch (error) { logger.error('2FA enable error:', error); res.status(500).json({ error: 'Failed to enable 2FA' }); } }); // Disable 2FA router.post('/disable', authenticate, authLimiter, async (req, res) => { try { const { password, token } = req.body; const userId = req.user.userId; if (!password) { return res.status(400).json({ error: 'Password required to disable 2FA' }); } // Verify password const bcrypt = require('bcrypt'); const user = await dbGet('SELECT password, two_factor_secret FROM users WHERE id = ?', [userId]); const validPassword = await bcrypt.compare(password, user.password); if (!validPassword) { return res.status(401).json({ error: 'Invalid password' }); } // Verify 2FA token const verified = speakeasy.totp.verify({ secret: user.two_factor_secret, encoding: 'base32', token: token, window: 2 }); if (!verified) { return res.status(400).json({ error: 'Invalid 2FA code' }); } // Disable 2FA and remove secret await dbRun( 'UPDATE users SET two_factor_enabled = 0, two_factor_secret = NULL WHERE id = ?', [userId] ); // Delete all backup codes await dbRun('DELETE FROM two_factor_backup_codes WHERE user_id = ?', [userId]); logger.info(`2FA disabled for user ${userId}`); res.json({ success: true, message: '2FA disabled successfully' }); } catch (error) { logger.error('2FA disable error:', error); res.status(500).json({ error: 'Failed to disable 2FA' }); } }); // Verify 2FA token (for login) router.post('/verify', authLimiter, async (req, res) => { try { const { userId, token } = req.body; if (!userId || !token) { return res.status(400).json({ error: 'User ID and token required' }); } // Get user's secret const user = await dbGet( 'SELECT two_factor_secret, two_factor_enabled FROM users WHERE id = ?', [userId] ); if (!user || !user.two_factor_enabled) { return res.status(400).json({ error: '2FA not enabled for this user' }); } // Check if it's a backup code const backupCode = await dbGet( 'SELECT id FROM two_factor_backup_codes WHERE user_id = ? AND code = ? AND used = 0', [userId, token.toUpperCase()] ); if (backupCode) { // Mark backup code as used await dbRun( 'UPDATE two_factor_backup_codes SET used = 1, used_at = CURRENT_TIMESTAMP WHERE id = ?', [backupCode.id] ); logger.info(`Backup code used for user ${userId}`); return res.json({ valid: true, method: 'backup_code' }); } // Verify TOTP token const verified = speakeasy.totp.verify({ secret: user.two_factor_secret, encoding: 'base32', token: token, window: 2 }); if (verified) { return res.json({ valid: true, method: 'totp' }); } else { return res.status(400).json({ error: 'Invalid 2FA code' }); } } catch (error) { logger.error('2FA verify error:', error); res.status(500).json({ error: 'Failed to verify 2FA code' }); } }); // Get backup codes router.get('/backup-codes', authenticate, readLimiter, async (req, res) => { try { const userId = req.user.userId; const codes = await dbAll( 'SELECT code, used, used_at, created_at FROM two_factor_backup_codes WHERE user_id = ? ORDER BY created_at DESC', [userId] ); res.json(codes); } catch (error) { logger.error('Get backup codes error:', error); res.status(500).json({ error: 'Failed to retrieve backup codes' }); } }); // Regenerate backup codes router.post('/backup-codes/regenerate', authenticate, modifyLimiter, async (req, res) => { try { const { password, token } = req.body; const userId = req.user.userId; if (!password || !token) { return res.status(400).json({ error: 'Password and 2FA token required' }); } // Verify password const bcrypt = require('bcrypt'); const user = await dbGet('SELECT password, two_factor_secret FROM users WHERE id = ?', [userId]); const validPassword = await bcrypt.compare(password, user.password); if (!validPassword) { return res.status(401).json({ error: 'Invalid password' }); } // Verify 2FA token const verified = speakeasy.totp.verify({ secret: user.two_factor_secret, encoding: 'base32', token: token, window: 2 }); if (!verified) { return res.status(400).json({ error: 'Invalid 2FA code' }); } // Delete old backup codes await dbRun('DELETE FROM two_factor_backup_codes WHERE user_id = ?', [userId]); // Generate new backup codes const backupCodes = []; for (let i = 0; i < 10; i++) { const code = crypto.randomBytes(4).toString('hex').toUpperCase(); backupCodes.push(code); await dbRun( 'INSERT INTO two_factor_backup_codes (user_id, code) VALUES (?, ?)', [userId, code] ); } logger.info(`Backup codes regenerated for user ${userId}`); res.json({ success: true, message: 'Backup codes regenerated', backupCodes: backupCodes }); } catch (error) { logger.error('Backup codes regenerate error:', error); res.status(500).json({ error: 'Failed to regenerate backup codes' }); } }); // Check 2FA status router.get('/status', authenticate, readLimiter, async (req, res) => { try { const userId = req.user.userId; const user = await dbGet( 'SELECT two_factor_enabled FROM users WHERE id = ?', [userId] ); const backupCodesCount = await dbGet( 'SELECT COUNT(*) as total, SUM(CASE WHEN used = 0 THEN 1 ELSE 0 END) as unused FROM two_factor_backup_codes WHERE user_id = ?', [userId] ); res.json({ enabled: !!user.two_factor_enabled, backupCodesTotal: backupCodesCount.total || 0, backupCodesUnused: backupCodesCount.unused || 0 }); } catch (error) { logger.error('2FA status error:', error); res.status(500).json({ error: 'Failed to get 2FA status' }); } }); module.exports = router;