Initial commit: StreamFlow IPTV platform

This commit is contained in:
aiulian25 2025-12-17 00:42:43 +00:00
commit 73a8ae9ffd
1240 changed files with 278451 additions and 0 deletions

129
backend/middleware/auth.js Normal file
View file

@ -0,0 +1,129 @@
const jwt = require('jsonwebtoken');
const logger = require('../utils/logger');
const db = require('../database/db').db;
const { SESSION_POLICY } = require('../utils/passwordPolicy');
const JWT_SECRET = process.env.JWT_SECRET || 'change_this_in_production';
const authenticate = (req, res, next) => {
// Check Authorization header first, then query parameter
let token = req.headers.authorization?.split(' ')[1];
if (!token && req.query.token) {
token = req.query.token;
}
if (!token) {
logger.info('[AUTH] No token provided');
return res.status(401).json({ error: 'Authentication required' });
}
// CWE-532: Do not log tokens or token details - they are credentials
logger.info('[AUTH] Verifying authentication token');
try {
const decoded = jwt.verify(token, JWT_SECRET);
logger.info(`[AUTH] Token verified for user ${decoded.userId}`);
// Check session activity and idle timeout
db.get(
'SELECT * FROM active_sessions WHERE session_token = ? AND user_id = ?',
[token, decoded.userId],
(err, session) => {
if (err) {
logger.error('Session check error:', err);
return res.status(500).json({ error: 'Session validation failed' });
}
if (!session) {
logger.info('[AUTH] Session not found for token in database');
return res.status(401).json({ error: 'Session not found or expired', sessionExpired: true });
}
logger.info('[AUTH] Session found, checking expiry');
// Check if session has expired (absolute timeout)
const now = new Date();
const expiresAt = new Date(session.expires_at);
if (now >= expiresAt) {
// Delete expired session
db.run('DELETE FROM active_sessions WHERE id = ?', [session.id]);
return res.status(401).json({ error: 'Session expired', sessionExpired: true });
}
// Check idle timeout (2 hours by default)
const lastActivity = new Date(session.last_activity);
const idleTimeMs = now - lastActivity;
const idleTimeoutMs = SESSION_POLICY.idleTimeout * 60 * 60 * 1000; // Convert hours to ms
if (idleTimeMs > idleTimeoutMs) {
// Session idle for too long - terminate it
db.run('DELETE FROM active_sessions WHERE id = ?', [session.id]);
logger.info(`Session ${session.id} terminated due to idle timeout (${idleTimeMs}ms idle)`);
return res.status(401).json({ error: 'Session expired due to inactivity', sessionExpired: true });
}
// Update last activity
db.run(
'UPDATE active_sessions SET last_activity = ? WHERE id = ?',
[now.toISOString(), session.id],
(updateErr) => {
if (updateErr) {
logger.error('Failed to update session activity:', updateErr);
}
}
);
req.user = decoded;
req.sessionId = session.id;
next();
}
);
} catch (error) {
logger.error('Authentication error:', error);
logger.error(`[AUTH] JWT Verification Failed: ${error.name} - ${error.message}`);
// Provide more specific error messages
let errorMessage = 'Invalid or expired token';
if (error.name === 'TokenExpiredError') {
errorMessage = 'Token has expired';
} else if (error.name === 'JsonWebTokenError') {
errorMessage = 'Invalid token';
}
res.status(401).json({
error: errorMessage,
sessionExpired: true // This triggers automatic logout on frontend
});
}
};
const authorize = (...roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
};
// Convenience middleware for admin-only routes
const requireAdmin = (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' });
}
next();
};
module.exports = { authenticate, authorize, requireAdmin };

View file

@ -0,0 +1,360 @@
/**
* Validation Middleware
* Provides reusable validation middleware for common request patterns
*/
const {
validateUsername,
validateEmail,
validateUrl,
validatePlaylistName,
validateChannelName,
validateDescription,
validateFilename,
validateSettingKey,
validateInteger,
validateBoolean,
validateJSON,
sanitizeObject
} = require('../utils/inputValidator');
const logger = require('../utils/logger');
/**
* Generic validation middleware factory
*/
function createValidationMiddleware(validators) {
return (req, res, next) => {
const errors = {};
const sanitized = {};
let hasErrors = false;
// Validate body parameters
if (validators.body) {
for (const [field, validator] of Object.entries(validators.body)) {
const value = req.body[field];
const result = validator(value);
if (!result.valid) {
errors[field] = result.errors;
hasErrors = true;
} else if (result.sanitized !== undefined) {
sanitized[field] = result.sanitized;
}
}
}
// Validate query parameters
if (validators.query) {
for (const [field, validator] of Object.entries(validators.query)) {
const value = req.query[field];
const result = validator(value);
if (!result.valid) {
errors[`query.${field}`] = result.errors;
hasErrors = true;
} else if (result.sanitized !== undefined) {
if (!req.sanitizedQuery) req.sanitizedQuery = {};
req.sanitizedQuery[field] = result.sanitized;
}
}
}
// Validate params
if (validators.params) {
for (const [field, validator] of Object.entries(validators.params)) {
const value = req.params[field];
const result = validator(value);
if (!result.valid) {
errors[`params.${field}`] = result.errors;
hasErrors = true;
} else if (result.sanitized !== undefined) {
if (!req.sanitizedParams) req.sanitizedParams = {};
req.sanitizedParams[field] = result.sanitized;
}
}
}
if (hasErrors) {
logger.warn('Validation failed:', { errors, path: req.path, ip: req.ip });
return res.status(400).json({
error: 'Validation failed',
details: errors
});
}
// Replace request data with sanitized versions
if (Object.keys(sanitized).length > 0) {
req.body = { ...req.body, ...sanitized };
}
next();
};
}
/**
* Validate playlist creation/update
*/
const validatePlaylist = createValidationMiddleware({
body: {
name: validatePlaylistName,
url: (value) => validateUrl(value, false),
category: (value) => {
if (!value) return { valid: true, errors: [], sanitized: null };
return validateDescription(value);
},
type: (value) => {
if (!value) return { valid: true, errors: [], sanitized: 'live' };
const allowed = ['live', 'vod', 'series', 'radio'];
if (!allowed.includes(value)) {
return { valid: false, errors: ['Invalid playlist type'], sanitized: null };
}
return { valid: true, errors: [], sanitized: value };
}
}
});
/**
* Validate channel update
*/
const validateChannelUpdate = createValidationMiddleware({
body: {
name: (value) => {
if (!value) return { valid: true, errors: [], sanitized: undefined };
return validateChannelName(value);
},
group_name: (value) => {
if (!value) return { valid: true, errors: [], sanitized: undefined };
return validateDescription(value);
}
}
});
/**
* Validate settings
*/
const validateSettings = createValidationMiddleware({
params: {
key: validateSettingKey
},
body: {
value: (value) => {
// Settings can be strings, numbers, booleans, or JSON objects
if (value === undefined || value === null) {
return { valid: false, errors: ['Value is required'], sanitized: null };
}
// If it's an object, validate as JSON
if (typeof value === 'object') {
return validateJSON(value, 100000);
}
return { valid: true, errors: [], sanitized: value };
}
}
});
/**
* Validate ID parameter
*/
const validateIdParam = createValidationMiddleware({
params: {
id: (value) => validateInteger(value, 1, Number.MAX_SAFE_INTEGER)
}
});
/**
* Validate channelId parameter
*/
const validateChannelIdParam = createValidationMiddleware({
params: {
channelId: (value) => validateInteger(value, 1, Number.MAX_SAFE_INTEGER)
}
});
/**
* Validate pagination parameters
*/
const validatePagination = createValidationMiddleware({
query: {
limit: (value) => {
if (!value) return { valid: true, errors: [], sanitized: 100 };
return validateInteger(value, 1, 1000);
},
offset: (value) => {
if (!value) return { valid: true, errors: [], sanitized: 0 };
return validateInteger(value, 0, Number.MAX_SAFE_INTEGER);
}
}
});
/**
* Validate search query
*/
const validateSearch = createValidationMiddleware({
query: {
search: (value) => {
if (!value) return { valid: true, errors: [], sanitized: undefined };
return validateDescription(value);
},
q: (value) => {
if (!value) return { valid: true, errors: [], sanitized: undefined };
return validateDescription(value);
}
}
});
/**
* Validate user creation
*/
const validateUserCreation = createValidationMiddleware({
body: {
username: validateUsername,
email: validateEmail,
password: (value) => {
if (!value || typeof value !== 'string') {
return { valid: false, errors: ['Password is required'], sanitized: null };
}
if (value.length < 8) {
return { valid: false, errors: ['Password must be at least 8 characters'], sanitized: null };
}
if (value.length > 128) {
return { valid: false, errors: ['Password must not exceed 128 characters'], sanitized: null };
}
return { valid: true, errors: [], sanitized: value };
},
role: (value) => {
const allowed = ['user', 'admin'];
if (!allowed.includes(value)) {
return { valid: false, errors: ['Invalid role'], sanitized: null };
}
return { valid: true, errors: [], sanitized: value };
}
}
});
/**
* Validate user update
*/
const validateUserUpdate = createValidationMiddleware({
params: {
id: (value) => validateInteger(value, 1, Number.MAX_SAFE_INTEGER)
},
body: {
username: (value) => {
if (!value) return { valid: true, errors: [], sanitized: undefined };
return validateUsername(value);
},
email: (value) => {
if (!value) return { valid: true, errors: [], sanitized: undefined };
return validateEmail(value);
},
role: (value) => {
if (!value) return { valid: true, errors: [], sanitized: undefined };
const allowed = ['user', 'admin'];
if (!allowed.includes(value)) {
return { valid: false, errors: ['Invalid role'], sanitized: null };
}
return { valid: true, errors: [], sanitized: value };
},
is_active: (value) => {
if (value === undefined) return { valid: true, errors: [], sanitized: undefined };
return validateBoolean(value);
}
}
});
/**
* Validate filename for file operations
*/
const validateFileOperation = createValidationMiddleware({
params: {
filename: validateFilename
}
});
/**
* Validate bulk delete operations
*/
const validateBulkDelete = createValidationMiddleware({
body: {
ids: (value) => {
if (!Array.isArray(value)) {
return { valid: false, errors: ['IDs must be an array'], sanitized: null };
}
if (value.length === 0) {
return { valid: false, errors: ['At least one ID required'], sanitized: null };
}
if (value.length > 1000) {
return { valid: false, errors: ['Cannot delete more than 1000 items at once'], sanitized: null };
}
const errors = [];
const sanitized = [];
for (let i = 0; i < value.length; i++) {
const result = validateInteger(value[i], 1, Number.MAX_SAFE_INTEGER);
if (!result.valid) {
errors.push(`Invalid ID at index ${i}`);
} else {
sanitized.push(result.sanitized);
}
}
if (errors.length > 0) {
return { valid: false, errors, sanitized: null };
}
return { valid: true, errors: [], sanitized };
}
}
});
/**
* Generic text field validator
*/
function validateTextField(maxLength = 1000, required = true) {
return (value) => {
if (!value || value === '') {
if (required) {
return { valid: false, errors: ['This field is required'], sanitized: null };
}
return { valid: true, errors: [], sanitized: '' };
}
if (typeof value !== 'string') {
return { valid: false, errors: ['Must be a string'], sanitized: null };
}
const trimmed = value.trim();
if (required && trimmed.length === 0) {
return { valid: false, errors: ['This field is required'], sanitized: null };
}
if (trimmed.length > maxLength) {
return { valid: false, errors: [`Must not exceed ${maxLength} characters`], sanitized: null };
}
const result = validateDescription(trimmed);
return result;
};
}
module.exports = {
createValidationMiddleware,
validatePlaylist,
validateChannelUpdate,
validateSettings,
validateIdParam,
validateChannelIdParam,
validatePagination,
validateSearch,
validateUserCreation,
validateUserUpdate,
validateFileOperation,
validateBulkDelete,
validateTextField
};

View file

@ -0,0 +1,84 @@
const rateLimit = require('express-rate-limit');
/**
* Strict rate limiter for authentication endpoints
* 5 requests per 15 minutes
*/
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5,
message: { error: 'Too many authentication attempts, please try again later' },
standardHeaders: true,
legacyHeaders: false,
});
/**
* Moderate rate limiter for data modification endpoints
* (Create, Update, Delete operations)
* 30 requests per 15 minutes
*/
const modifyLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 30,
message: { error: 'Too many modification requests, please slow down' },
standardHeaders: true,
legacyHeaders: false,
});
/**
* Lenient rate limiter for read operations
* 100 requests per 15 minutes
*/
const readLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
message: { error: 'Too many requests, please slow down' },
standardHeaders: true,
legacyHeaders: false,
});
/**
* Moderate rate limiter for resource-intensive operations
* (Streaming, backup, file uploads)
* Increased to 1000/min to support HLS streaming which makes many segment requests
*/
const heavyLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 1000,
message: { error: 'Too many resource-intensive requests, please wait' },
standardHeaders: true,
legacyHeaders: false,
});
/**
* Very strict limiter for backup/restore operations
* 3 requests per hour
*/
const backupLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 3,
message: { error: 'Too many backup operations, please wait before trying again' },
standardHeaders: true,
legacyHeaders: false,
});
/**
* General API rate limiter
* 200 requests per 15 minutes
*/
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 200,
message: { error: 'Too many API requests, please try again later' },
standardHeaders: true,
legacyHeaders: false,
});
module.exports = {
authLimiter,
modifyLimiter,
readLimiter,
heavyLimiter,
backupLimiter,
apiLimiter
};

525
backend/middleware/rbac.js Normal file
View file

@ -0,0 +1,525 @@
const logger = require('../utils/logger');
const { db } = require('../database/db');
/**
* RBAC (Role-Based Access Control) Middleware
*
* Implements granular permission checking following the principle of least privilege.
* Each user has roles, and each role has specific permissions.
* This prevents over-privileged accounts and limits attack surface.
*/
// Define all available permissions in the system
const PERMISSIONS = {
// User Management
'users.view': 'View user list and details',
'users.create': 'Create new users',
'users.edit': 'Edit existing users',
'users.delete': 'Delete users',
'users.manage_roles': 'Assign and modify user roles',
'users.unlock': 'Unlock locked user accounts',
'users.reset_password': 'Reset user passwords',
// Session Management
'sessions.view_own': 'View own active sessions',
'sessions.view_all': 'View all user sessions',
'sessions.terminate_own': 'Terminate own sessions',
'sessions.terminate_any': 'Terminate any user session',
'sessions.view_stats': 'View session statistics',
// Content Management
'playlists.view': 'View playlists',
'playlists.create': 'Create playlists',
'playlists.edit': 'Edit playlists',
'playlists.delete': 'Delete playlists',
'playlists.import': 'Import M3U files',
'channels.view': 'View channels',
'channels.edit': 'Edit channel details',
'channels.upload_logo': 'Upload custom channel logos',
'channels.delete_logo': 'Delete custom channel logos',
'favorites.view': 'View favorites',
'favorites.manage': 'Add/remove favorites',
'history.view_own': 'View own watch history',
'history.view_all': 'View all user watch history',
'history.delete_own': 'Delete own watch history',
'history.delete_any': 'Delete any user watch history',
// System & Settings
'settings.view': 'View application settings',
'settings.edit': 'Modify application settings',
'stats.view': 'View analytics and statistics',
'stats.view_detailed': 'View detailed analytics (user activity, etc.)',
'backup.view': 'View available backups',
'backup.create': 'Create backups',
'backup.restore': 'Restore from backups',
'backup.delete': 'Delete backups',
'backup.download': 'Download backups',
// Security Management
'security.view_sessions': 'View security session dashboard',
'security.view_csp': 'View CSP violation dashboard',
'security.manage_2fa': 'Manage two-factor authentication',
'security.view_audit': 'View audit logs',
// Search & Discovery
'search.use': 'Use search functionality',
'search.admin': 'Search users and system settings',
// VPN & Network
'vpn.view': 'View VPN settings',
'vpn.configure': 'Configure VPN settings',
'vpn.connect': 'Connect/disconnect VPN'
};
// Default role definitions with their permissions
const DEFAULT_ROLES = {
'admin': {
name: 'Administrator',
description: 'Full system access',
permissions: Object.keys(PERMISSIONS), // Admins have all permissions
is_system_role: true
},
'moderator': {
name: 'Moderator',
description: 'Content management and user support',
permissions: [
// User viewing (but not management)
'users.view',
// Content management
'playlists.view',
'playlists.create',
'playlists.edit',
'playlists.delete',
'playlists.import',
'channels.view',
'channels.edit',
'channels.upload_logo',
'channels.delete_logo',
// History management
'history.view_all',
'history.delete_any',
// Settings (view only)
'settings.view',
// Statistics
'stats.view',
'stats.view_detailed',
// Own sessions
'sessions.view_own',
'sessions.terminate_own',
// Own favorites
'favorites.view',
'favorites.manage',
// Search
'search.use',
// Own security
'security.manage_2fa'
],
is_system_role: true
},
'user': {
name: 'Regular User',
description: 'Standard user with content access',
permissions: [
// Own playlists
'playlists.view',
'playlists.create',
'playlists.edit',
'playlists.delete',
'playlists.import',
// Channels (view and customize)
'channels.view',
'channels.upload_logo',
'channels.delete_logo',
// Own favorites
'favorites.view',
'favorites.manage',
// Own history
'history.view_own',
'history.delete_own',
// Own settings
'settings.view',
'settings.edit',
// Own sessions
'sessions.view_own',
'sessions.terminate_own',
// Search (basic)
'search.use',
// Own security
'security.manage_2fa',
// VPN (if enabled)
'vpn.view',
'vpn.configure',
'vpn.connect'
],
is_system_role: true
},
'viewer': {
name: 'Viewer',
description: 'Read-only access for content viewing',
permissions: [
// View only
'playlists.view',
'channels.view',
'favorites.view',
'favorites.manage',
'history.view_own',
'history.delete_own',
'settings.view',
'sessions.view_own',
'sessions.terminate_own',
'search.use',
'security.manage_2fa'
],
is_system_role: true
}
};
/**
* Cache for user permissions to reduce database queries
* Format: { userId: { permissions: [...], expires: timestamp } }
*/
const permissionCache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
/**
* Clear permission cache for a specific user
*/
const clearUserPermissionCache = (userId) => {
permissionCache.delete(userId);
logger.info(`Permission cache cleared for user ${userId}`);
};
/**
* Clear entire permission cache (call after role/permission changes)
*/
const clearAllPermissionCache = () => {
permissionCache.clear();
logger.info('All permission cache cleared');
};
/**
* Get user permissions from database with caching
*/
const getUserPermissions = (userId) => {
return new Promise((resolve, reject) => {
// Check cache first
const cached = permissionCache.get(userId);
if (cached && cached.expires > Date.now()) {
return resolve(cached.permissions);
}
// Query database
db.get(
`SELECT u.role, r.permissions
FROM users u
LEFT JOIN roles r ON u.role = r.role_key
WHERE u.id = ? AND u.is_active = 1`,
[userId],
(err, result) => {
if (err) {
logger.error('Error fetching user permissions:', err);
return reject(err);
}
if (!result) {
return reject(new Error('User not found or inactive'));
}
// Parse permissions (stored as JSON string)
let permissions = [];
try {
if (result.permissions) {
permissions = JSON.parse(result.permissions);
} else {
// Fallback to default role permissions if not in database
const defaultRole = DEFAULT_ROLES[result.role];
permissions = defaultRole ? defaultRole.permissions : [];
}
} catch (parseErr) {
logger.error('Error parsing permissions:', parseErr);
permissions = [];
}
// Cache the result
permissionCache.set(userId, {
permissions,
expires: Date.now() + CACHE_TTL
});
resolve(permissions);
}
);
});
};
/**
* Check if user has a specific permission
*/
const hasPermission = async (userId, permission) => {
try {
const permissions = await getUserPermissions(userId);
return permissions.includes(permission);
} catch (error) {
logger.error('Permission check failed:', error);
return false;
}
};
/**
* Check if user has ANY of the specified permissions
*/
const hasAnyPermission = async (userId, permissionList) => {
try {
const permissions = await getUserPermissions(userId);
return permissionList.some(p => permissions.includes(p));
} catch (error) {
logger.error('Permission check failed:', error);
return false;
}
};
/**
* Check if user has ALL of the specified permissions
*/
const hasAllPermissions = async (userId, permissionList) => {
try {
const permissions = await getUserPermissions(userId);
return permissionList.every(p => permissions.includes(p));
} catch (error) {
logger.error('Permission check failed:', error);
return false;
}
};
/**
* Middleware: Require specific permission(s)
* Usage: requirePermission('users.view')
* Usage: requirePermission(['users.view', 'users.edit']) - requires ANY
*/
const requirePermission = (requiredPermissions) => {
// Normalize to array
const permissionList = Array.isArray(requiredPermissions)
? requiredPermissions
: [requiredPermissions];
return async (req, res, next) => {
if (!req.user || !req.user.userId) {
return res.status(401).json({
error: 'Authentication required',
code: 'AUTH_REQUIRED'
});
}
try {
// Bypass permission check for user ID 1 (first admin) or users with role 'admin'
if (req.user.userId === 1 || req.user.role === 'admin') {
req.userPermissions = Object.keys(PERMISSIONS); // Grant all permissions
return next();
}
const userPermissions = await getUserPermissions(req.user.userId);
// Check if user has any of the required permissions
const hasAccess = permissionList.some(p => userPermissions.includes(p));
if (!hasAccess) {
logger.warn(`Access denied: User ${req.user.userId} lacks permission(s): ${permissionList.join(', ')}`);
return res.status(403).json({
error: 'Insufficient permissions',
code: 'INSUFFICIENT_PERMISSIONS',
required: permissionList,
details: 'You do not have the required permissions to perform this action'
});
}
// Attach user permissions to request for further checks
req.userPermissions = userPermissions;
next();
} catch (error) {
logger.error('Permission check error:', error);
res.status(500).json({
error: 'Permission validation failed',
code: 'PERMISSION_CHECK_FAILED'
});
}
};
};
/**
* Middleware: Require ALL specified permissions
* Usage: requireAllPermissions(['users.view', 'users.edit'])
*/
const requireAllPermissions = (permissionList) => {
return async (req, res, next) => {
if (!req.user || !req.user.userId) {
return res.status(401).json({
error: 'Authentication required',
code: 'AUTH_REQUIRED'
});
}
try {
// Bypass permission check for user ID 1 (first admin) or users with role 'admin'
if (req.user.userId === 1 || req.user.role === 'admin') {
req.userPermissions = Object.keys(PERMISSIONS); // Grant all permissions
return next();
}
const userPermissions = await getUserPermissions(req.user.userId);
// Check if user has ALL required permissions
const hasAccess = permissionList.every(p => userPermissions.includes(p));
if (!hasAccess) {
logger.warn(`Access denied: User ${req.user.userId} lacks all permissions: ${permissionList.join(', ')}`);
return res.status(403).json({
error: 'Insufficient permissions',
code: 'INSUFFICIENT_PERMISSIONS',
required: permissionList,
details: 'You do not have all the required permissions to perform this action'
});
}
req.userPermissions = userPermissions;
next();
} catch (error) {
logger.error('Permission check error:', error);
res.status(500).json({
error: 'Permission validation failed',
code: 'PERMISSION_CHECK_FAILED'
});
}
};
};
/**
* Initialize roles table and seed default roles
*/
const initializeRoles = () => {
db.serialize(() => {
// Create roles table
db.run(`CREATE TABLE IF NOT EXISTS roles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
role_key TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
description TEXT,
permissions TEXT NOT NULL,
is_system_role BOOLEAN DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`, (err) => {
if (err) {
logger.error('Failed to create roles table:', err);
return;
}
// Seed default roles
Object.entries(DEFAULT_ROLES).forEach(([roleKey, roleData]) => {
db.run(
`INSERT OR IGNORE INTO roles (role_key, name, description, permissions, is_system_role)
VALUES (?, ?, ?, ?, ?)`,
[
roleKey,
roleData.name,
roleData.description,
JSON.stringify(roleData.permissions),
roleData.is_system_role ? 1 : 0
],
(err) => {
if (err) {
logger.error(`Failed to seed role ${roleKey}:`, err);
} else {
logger.info(`✓ Role seeded: ${roleKey}`);
}
}
);
});
// Create permission audit log table
db.run(`CREATE TABLE IF NOT EXISTS permission_audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
action TEXT NOT NULL,
target_type TEXT NOT NULL,
target_id INTEGER,
old_value TEXT,
new_value TEXT,
ip_address TEXT,
user_agent TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`, (err) => {
if (err) {
logger.error('Failed to create permission_audit_log table:', err);
} else {
logger.info('✓ Permission audit log table created');
}
});
});
});
};
/**
* Log permission-related actions for audit trail
*/
const logPermissionAction = (userId, action, targetType, targetId, oldValue, newValue, req) => {
const ipAddress = req?.ip || req?.connection?.remoteAddress || 'unknown';
const userAgent = req?.headers['user-agent'] || 'unknown';
db.run(
`INSERT INTO permission_audit_log
(user_id, action, target_type, target_id, old_value, new_value, ip_address, user_agent)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[
userId,
action,
targetType,
targetId || null,
oldValue ? JSON.stringify(oldValue) : null,
newValue ? JSON.stringify(newValue) : null,
ipAddress,
userAgent
],
(err) => {
if (err) {
logger.error('Failed to log permission action:', err);
}
}
);
};
module.exports = {
PERMISSIONS,
DEFAULT_ROLES,
requirePermission,
requireAllPermissions,
hasPermission,
hasAnyPermission,
hasAllPermissions,
getUserPermissions,
clearUserPermissionCache,
clearAllPermissionCache,
initializeRoles,
logPermissionAction
};

View file

@ -0,0 +1,349 @@
/**
* Enhanced Security Middleware
* Implements account lockout, password expiry, and session management
*/
const { db } = require('../database/db');
const { ACCOUNT_LOCKOUT, PASSWORD_EXPIRY, SESSION_POLICY } = require('../utils/passwordPolicy');
const SecurityAuditLogger = require('../utils/securityAudit');
const logger = require('../utils/logger');
/**
* Check if account is locked
*/
async function checkAccountLockout(userId) {
return new Promise((resolve, reject) => {
db.get(
'SELECT locked_until FROM users WHERE id = ?',
[userId],
(err, user) => {
if (err) return reject(err);
if (!user) return resolve({ locked: false });
if (user.locked_until) {
const lockoutEnd = new Date(user.locked_until);
const now = new Date();
if (now < lockoutEnd) {
const remainingMinutes = Math.ceil((lockoutEnd - now) / 60000);
return resolve({
locked: true,
remainingMinutes,
message: `Account locked. Try again in ${remainingMinutes} minutes.`
});
} else {
// Lockout expired, clear it
db.run('UPDATE users SET locked_until = NULL, failed_login_attempts = 0 WHERE id = ?', [userId]);
return resolve({ locked: false });
}
}
resolve({ locked: false });
}
);
});
}
/**
* Record failed login attempt
*/
async function recordFailedLogin(username, ip, userAgent) {
try {
// Log to audit
await SecurityAuditLogger.logLoginFailure(username, 'Invalid credentials', { ip, userAgent });
// Get user ID
const user = await new Promise((resolve, reject) => {
db.get('SELECT id, failed_login_attempts FROM users WHERE username = ? OR email = ?', [username, username], (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
if (!user) return;
const failedAttempts = (user.failed_login_attempts || 0) + 1;
// Update failed attempts
await new Promise((resolve, reject) => {
db.run(
'UPDATE users SET failed_login_attempts = ?, last_failed_login = ? WHERE id = ?',
[failedAttempts, new Date().toISOString(), user.id],
(err) => err ? reject(err) : resolve()
);
});
// Check if lockout threshold reached
if (failedAttempts >= ACCOUNT_LOCKOUT.maxFailedAttempts) {
const lockUntil = new Date(Date.now() + ACCOUNT_LOCKOUT.lockoutDuration).toISOString();
await new Promise((resolve, reject) => {
db.run(
'UPDATE users SET locked_until = ? WHERE id = ?',
[lockUntil, user.id],
(err) => err ? reject(err) : resolve()
);
});
await SecurityAuditLogger.logAccountLockout(user.id, {
ip,
userAgent,
failedAttempts
});
logger.warn(`Account locked for user ${username} after ${failedAttempts} failed attempts`);
}
} catch (error) {
logger.error('Error recording failed login:', error);
}
}
/**
* Clear failed login attempts on successful login
*/
async function clearFailedAttempts(userId) {
return new Promise((resolve, reject) => {
db.run(
'UPDATE users SET failed_login_attempts = 0, last_failed_login = NULL WHERE id = ?',
[userId],
(err) => err ? reject(err) : resolve()
);
});
}
/**
* Check if password has expired
*/
async function checkPasswordExpiry(userId) {
if (!PASSWORD_EXPIRY.enabled) {
return { expired: false, warning: false };
}
return new Promise((resolve, reject) => {
db.get(
'SELECT password_changed_at, password_expires_at FROM users WHERE id = ?',
[userId],
(err, user) => {
if (err) return reject(err);
if (!user) return resolve({ expired: false, warning: false });
const now = new Date();
let expiryDate;
if (user.password_expires_at) {
expiryDate = new Date(user.password_expires_at);
} else if (user.password_changed_at) {
expiryDate = new Date(user.password_changed_at);
expiryDate.setDate(expiryDate.getDate() + PASSWORD_EXPIRY.expiryDays);
} else {
// No password change date, set expiry from now
expiryDate = new Date();
expiryDate.setDate(expiryDate.getDate() + PASSWORD_EXPIRY.expiryDays);
}
const daysUntilExpiry = Math.ceil((expiryDate - now) / (24 * 60 * 60 * 1000));
if (daysUntilExpiry <= 0) {
return resolve({
expired: true,
warning: false,
message: 'Your password has expired. Please change it to continue.',
gracePeriodDays: PASSWORD_EXPIRY.gracePeriodDays
});
}
if (daysUntilExpiry <= PASSWORD_EXPIRY.warningDays) {
return resolve({
expired: false,
warning: true,
daysRemaining: daysUntilExpiry,
message: `Your password will expire in ${daysUntilExpiry} days. Please change it soon.`
});
}
resolve({ expired: false, warning: false });
}
);
});
}
/**
* Update password expiry date
*/
async function updatePasswordExpiry(userId) {
if (!PASSWORD_EXPIRY.enabled) return;
const now = new Date();
const expiryDate = new Date(now);
expiryDate.setDate(expiryDate.getDate() + PASSWORD_EXPIRY.expiryDays);
return new Promise((resolve, reject) => {
db.run(
'UPDATE users SET password_changed_at = ?, password_expires_at = ? WHERE id = ?',
[now.toISOString(), expiryDate.toISOString(), userId],
(err) => err ? reject(err) : resolve()
);
});
}
/**
* Middleware: Check account lockout before authentication
*/
const enforceAccountLockout = async (req, res, next) => {
const { username } = req.body;
if (!username) {
return next();
}
try {
// Get user
const user = await new Promise((resolve, reject) => {
db.get('SELECT id FROM users WHERE username = ? OR email = ?', [username, username], (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
if (!user) {
return next(); // User doesn't exist, let auth handle it
}
// Check lockout
const lockoutStatus = await checkAccountLockout(user.id);
if (lockoutStatus.locked) {
return res.status(423).json({
error: lockoutStatus.message,
remainingMinutes: lockoutStatus.remainingMinutes,
locked: true
});
}
next();
} catch (error) {
logger.error('Account lockout check error:', error);
next();
}
};
/**
* Middleware: Check password expiry after authentication
*/
const enforcePasswordExpiry = async (req, res, next) => {
if (!req.user || !req.user.userId) {
return next();
}
try {
const expiryStatus = await checkPasswordExpiry(req.user.userId);
if (expiryStatus.expired) {
return res.status(403).json({
error: expiryStatus.message,
passwordExpired: true,
gracePeriodDays: expiryStatus.gracePeriodDays,
requirePasswordChange: true
});
}
if (expiryStatus.warning) {
// Add warning header but allow request
res.setHeader('X-Password-Expiry-Warning', expiryStatus.message);
res.setHeader('X-Password-Days-Remaining', expiryStatus.daysRemaining.toString());
}
next();
} catch (error) {
logger.error('Password expiry check error:', error);
next();
}
};
/**
* Manage active sessions
*/
async function createSession(userId, token, req) {
const ip = req.ip || req.headers['x-forwarded-for'] || req.connection.remoteAddress;
const userAgent = req.headers['user-agent'];
const expiresAt = new Date(Date.now() + SESSION_POLICY.absoluteTimeout);
// Check concurrent sessions
const activeSessions = await new Promise((resolve, reject) => {
db.all(
'SELECT COUNT(*) as count FROM active_sessions WHERE user_id = ? AND expires_at > ?',
[userId, new Date().toISOString()],
(err, rows) => err ? reject(err) : resolve(rows[0].count)
);
});
if (activeSessions >= SESSION_POLICY.maxConcurrentSessions) {
// Remove oldest session
await new Promise((resolve, reject) => {
db.run(
'DELETE FROM active_sessions WHERE id IN (SELECT id FROM active_sessions WHERE user_id = ? ORDER BY last_activity ASC LIMIT 1)',
[userId],
(err) => err ? reject(err) : resolve()
);
});
}
// Create new session
return new Promise((resolve, reject) => {
db.run(
'INSERT INTO active_sessions (user_id, session_token, ip_address, user_agent, expires_at) VALUES (?, ?, ?, ?, ?)',
[userId, token, ip, userAgent, expiresAt.toISOString()],
(err) => err ? reject(err) : resolve()
);
});
}
/**
* Update session activity
*/
async function updateSessionActivity(token) {
return new Promise((resolve, reject) => {
db.run(
'UPDATE active_sessions SET last_activity = ? WHERE session_token = ?',
[new Date().toISOString(), token],
(err) => err ? reject(err) : resolve()
);
});
}
/**
* Cleanup expired sessions
*/
async function cleanupExpiredSessions() {
return new Promise((resolve, reject) => {
db.run(
'DELETE FROM active_sessions WHERE expires_at < ?',
[new Date().toISOString()],
function(err) {
if (err) reject(err);
else {
if (this.changes > 0) {
logger.info(`Cleaned up ${this.changes} expired sessions`);
}
resolve(this.changes);
}
}
);
});
}
// Run session cleanup every hour
setInterval(cleanupExpiredSessions, 60 * 60 * 1000);
module.exports = {
checkAccountLockout,
recordFailedLogin,
clearFailedAttempts,
checkPasswordExpiry,
updatePasswordExpiry,
enforceAccountLockout,
enforcePasswordExpiry,
createSession,
updateSessionActivity,
cleanupExpiredSessions
};