74 lines
2.8 KiB
JavaScript
74 lines
2.8 KiB
JavaScript
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();
|
|
}
|
|
|
|
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, generateToken, setActiveSession, clearActiveSession, getDeviceClass };
|