const jwt = require('jsonwebtoken'); const { getDb } = require('../models/db'); const JWT_SECRET = process.env.JWT_SECRET || 'changeme_super_secret'; // Classify a User-Agent string into 'mobile' or 'desktop'. // Tablets are treated as mobile (one shared slot). function getDeviceClass(ua) { if (!ua) return 'desktop'; const s = ua.toLowerCase(); if (/mobile|android(?!.*tablet)|iphone|ipod|blackberry|windows phone|opera mini|silk/.test(s)) return 'mobile'; if (/tablet|ipad|kindle|playbook|android/.test(s)) return 'mobile'; return 'desktop'; } function authMiddleware(req, res, next) { const token = req.headers.authorization?.split(' ')[1] || req.cookies?.token; if (!token) return res.status(401).json({ error: 'Unauthorized' }); try { const decoded = jwt.verify(token, JWT_SECRET); const db = getDb(); const user = db.prepare('SELECT * FROM users WHERE id = ? AND status = ?').get(decoded.id, 'active'); if (!user) return res.status(401).json({ error: 'User not found or suspended' }); // Per-device enforcement: token must match an active session row const session = db.prepare('SELECT * FROM active_sessions WHERE user_id = ? AND token = ?').get(decoded.id, token); if (!session) { return res.status(401).json({ error: 'Session expired. Please log in again.' }); } req.user = user; req.token = token; req.device = session.device; next(); } catch (e) { return res.status(401).json({ error: 'Invalid token' }); } } function adminMiddleware(req, res, next) { if (req.user?.role !== 'admin') return res.status(403).json({ error: 'Admin only' }); next(); } // Allows admins OR members of groups designated as Tool Managers function teamManagerMiddleware(req, res, next) { if (req.user?.role === 'admin') return next(); const db = getDb(); // Prefer unified key, fall back to legacy keys for older installs const tmSetting = db.prepare("SELECT value FROM settings WHERE key = 'team_tool_managers'").get(); const gmSetting = db.prepare("SELECT value FROM settings WHERE key = 'team_group_managers'").get(); const allowedGroupIds = [ ...new Set([ ...JSON.parse(tmSetting?.value || '[]'), ...JSON.parse(gmSetting?.value || '[]'), ]) ]; if (allowedGroupIds.length === 0) return res.status(403).json({ error: 'Access denied' }); const member = db.prepare(` SELECT 1 FROM user_group_members WHERE user_id = ? AND user_group_id IN (${allowedGroupIds.map(() => '?').join(',')}) `).get(req.user.id, ...allowedGroupIds); if (!member) return res.status(403).json({ error: 'Access denied' }); next(); } function generateToken(userId) { return jwt.sign({ id: userId }, JWT_SECRET, { expiresIn: '30d' }); } // Upsert the active session for this user+device class. // Displaces any prior session on the same device class; the other device class is unaffected. function setActiveSession(userId, token, userAgent) { const db = getDb(); const device = getDeviceClass(userAgent); db.prepare(` INSERT INTO active_sessions (user_id, device, token, ua, created_at) VALUES (?, ?, ?, ?, datetime('now')) ON CONFLICT(user_id, device) DO UPDATE SET token = ?, ua = ?, created_at = datetime('now') `).run(userId, device, token, userAgent || null, token, userAgent || null); return device; } // Clear one device slot on logout, or all slots (no device arg) for suspend/delete function clearActiveSession(userId, device) { const db = getDb(); if (device) { db.prepare('DELETE FROM active_sessions WHERE user_id = ? AND device = ?').run(userId, device); } else { db.prepare('DELETE FROM active_sessions WHERE user_id = ?').run(userId); } } module.exports = { authMiddleware, adminMiddleware, teamManagerMiddleware, generateToken, setActiveSession, clearActiveSession, getDeviceClass };