mirror of
https://github.com/screentinker/screentinker.git
synced 2026-05-15 07:32:23 -06:00
- Device WebSocket authentication: devices get a device_token on registration, must present it on reconnect. All WS events require prior auth. Timing-safe token comparison. - IDOR fixes: ownership checks on schedules (device, week), layouts (all CRUD, zones, duplicate, device assign), video-walls (content, device-config). - XSS prevention: shared esc() helper in utils.js, fixed 13 innerHTML injection points across 9 frontend files. - OAuth hardening: no longer silently overwrites auth_provider on accounts with local passwords (returns 409). - JWT pinned to HS256 for sign and verify. - Password policy: change endpoint now requires 8 chars (was 6). - HSTS header enabled (max-age 1 year, includeSubDomains). - Stripe webhook rejects unsigned payloads when no secret configured. - Screenshot size validation (max 2MB base64). - Rate limiting on exports, imports, content operations. - Content file serving checks playlist_items instead of old assignments. - Content ownership verified in device-groups assign-content. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
68 lines
2.2 KiB
JavaScript
68 lines
2.2 KiB
JavaScript
const jwt = require('jsonwebtoken');
|
|
const config = require('../config');
|
|
const { db } = require('../db/database');
|
|
|
|
function generateToken(user) {
|
|
return jwt.sign(
|
|
{ id: user.id, email: user.email, role: user.role },
|
|
config.jwtSecret,
|
|
{ algorithm: 'HS256', expiresIn: config.jwtExpiry }
|
|
);
|
|
}
|
|
|
|
function verifyToken(token) {
|
|
return jwt.verify(token, config.jwtSecret, { algorithms: ['HS256'] });
|
|
}
|
|
|
|
// Express middleware - requires valid JWT
|
|
function requireAuth(req, res, next) {
|
|
const authHeader = req.headers.authorization;
|
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
return res.status(401).json({ error: 'Authentication required' });
|
|
}
|
|
|
|
try {
|
|
const token = authHeader.split(' ')[1];
|
|
const decoded = verifyToken(token);
|
|
const user = db.prepare('SELECT id, email, name, role, auth_provider, avatar_url, plan_id FROM users WHERE id = ?').get(decoded.id);
|
|
if (!user) return res.status(401).json({ error: 'User not found' });
|
|
req.user = user;
|
|
next();
|
|
} catch (err) {
|
|
return res.status(401).json({ error: 'Invalid or expired token' });
|
|
}
|
|
}
|
|
|
|
// Optional auth - sets req.user if token present, continues either way
|
|
function optionalAuth(req, res, next) {
|
|
const authHeader = req.headers.authorization;
|
|
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
try {
|
|
const token = authHeader.split(' ')[1];
|
|
const decoded = verifyToken(token);
|
|
req.user = db.prepare('SELECT id, email, name, role, auth_provider, avatar_url, plan_id FROM users WHERE id = ?').get(decoded.id);
|
|
} catch (err) {
|
|
// Token invalid, continue without user
|
|
}
|
|
}
|
|
next();
|
|
}
|
|
|
|
// Require admin role (admin or superadmin)
|
|
function requireAdmin(req, res, next) {
|
|
if (!req.user || !['admin', 'superadmin'].includes(req.user.role)) {
|
|
return res.status(403).json({ error: 'Admin access required' });
|
|
}
|
|
next();
|
|
}
|
|
|
|
// Require superadmin role (platform owner only)
|
|
function requireSuperAdmin(req, res, next) {
|
|
if (!req.user || req.user.role !== 'superadmin') {
|
|
return res.status(403).json({ error: 'Platform admin access required' });
|
|
}
|
|
next();
|
|
}
|
|
|
|
module.exports = { generateToken, verifyToken, requireAuth, optionalAuth, requireAdmin, requireSuperAdmin };
|