Fix jwt-token

This commit is contained in:
2025-09-20 20:41:30 +02:00
parent 11b460dc07
commit 8ed1c141eb
7 changed files with 1148 additions and 1 deletions

View File

@@ -9,6 +9,14 @@ class SecurityLogger {
// Ensure log directory exists
this.ensureLogDirectory();
// Initialize models reference (will be set when needed)
this.models = null;
}
// Set models reference for database logging
setModels(models) {
this.models = models;
}
ensureLogDirectory() {
@@ -23,7 +31,7 @@ class SecurityLogger {
}
}
logSecurityEvent(level, message, metadata = {}) {
async logSecurityEvent(level, message, metadata = {}) {
const timestamp = new Date().toISOString();
const logEntry = {
timestamp,
@@ -44,6 +52,49 @@ class SecurityLogger {
console.error('Failed to write to security log file:', error.message);
}
}
// Store in database if models are available
if (this.models && this.models.AuditLog) {
try {
await this.models.AuditLog.create({
timestamp: new Date(),
level: level.toUpperCase(),
action: metadata.action || 'unknown',
message,
user_id: metadata.userId || null,
username: metadata.username || null,
tenant_id: metadata.tenantId || null,
tenant_slug: metadata.tenantSlug || null,
ip_address: metadata.ip || null,
user_agent: metadata.userAgent || null,
path: metadata.path || null,
metadata: metadata,
success: this.determineSuccess(level, metadata)
});
} catch (error) {
console.error('Failed to store audit log in database:', error.message);
}
}
}
determineSuccess(level, metadata) {
// Determine if the action was successful based on level and metadata
if (metadata.hasOwnProperty('success')) {
return metadata.success;
}
// Assume success for info level, failure for error/critical
switch (level.toUpperCase()) {
case 'INFO':
return true;
case 'WARNING':
return null; // Neutral
case 'ERROR':
case 'CRITICAL':
return false;
default:
return null;
}
}
logIPRestriction(ip, tenant, userAgent, denied = true) {

View File

@@ -0,0 +1,103 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('audit_logs', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
timestamp: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.NOW
},
level: {
type: Sequelize.ENUM('INFO', 'WARNING', 'ERROR', 'CRITICAL'),
allowNull: false
},
action: {
type: Sequelize.STRING(100),
allowNull: false,
comment: 'The action performed (e.g., logo_upload, logo_removal)'
},
message: {
type: Sequelize.TEXT,
allowNull: false,
comment: 'Human-readable description of the event'
},
user_id: {
type: Sequelize.INTEGER,
allowNull: true,
comment: 'ID of the user who performed the action',
references: {
model: 'users',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'SET NULL'
},
username: {
type: Sequelize.STRING(255),
allowNull: true,
comment: 'Username of the user who performed the action'
},
tenant_id: {
type: Sequelize.INTEGER,
allowNull: true,
comment: 'ID of the tenant affected by the action',
references: {
model: 'tenants',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'SET NULL'
},
tenant_slug: {
type: Sequelize.STRING(255),
allowNull: true,
comment: 'Slug of the tenant affected by the action'
},
ip_address: {
type: Sequelize.STRING(45),
allowNull: true,
comment: 'IP address of the user (supports IPv6)'
},
user_agent: {
type: Sequelize.TEXT,
allowNull: true,
comment: 'User agent string from the request'
},
path: {
type: Sequelize.STRING(500),
allowNull: true,
comment: 'Request path that triggered the action'
},
metadata: {
type: Sequelize.JSON,
allowNull: true,
comment: 'Additional metadata about the event'
},
success: {
type: Sequelize.BOOLEAN,
allowNull: true,
comment: 'Whether the action was successful'
}
});
// Add indexes for performance
await queryInterface.addIndex('audit_logs', ['timestamp']);
await queryInterface.addIndex('audit_logs', ['action']);
await queryInterface.addIndex('audit_logs', ['user_id']);
await queryInterface.addIndex('audit_logs', ['tenant_id']);
await queryInterface.addIndex('audit_logs', ['level']);
await queryInterface.addIndex('audit_logs', ['timestamp', 'action']);
await queryInterface.addIndex('audit_logs', ['tenant_id', 'timestamp']);
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('audit_logs');
}
};

118
server/models/AuditLog.js Normal file
View File

@@ -0,0 +1,118 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const AuditLog = sequelize.define('AuditLog', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
timestamp: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
},
level: {
type: DataTypes.ENUM('INFO', 'WARNING', 'ERROR', 'CRITICAL'),
allowNull: false
},
action: {
type: DataTypes.STRING(100),
allowNull: false,
comment: 'The action performed (e.g., logo_upload, logo_removal)'
},
message: {
type: DataTypes.TEXT,
allowNull: false,
comment: 'Human-readable description of the event'
},
user_id: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'ID of the user who performed the action'
},
username: {
type: DataTypes.STRING(255),
allowNull: true,
comment: 'Username of the user who performed the action'
},
tenant_id: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'ID of the tenant affected by the action'
},
tenant_slug: {
type: DataTypes.STRING(255),
allowNull: true,
comment: 'Slug of the tenant affected by the action'
},
ip_address: {
type: DataTypes.STRING(45),
allowNull: true,
comment: 'IP address of the user (supports IPv6)'
},
user_agent: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'User agent string from the request'
},
path: {
type: DataTypes.STRING(500),
allowNull: true,
comment: 'Request path that triggered the action'
},
metadata: {
type: DataTypes.JSON,
allowNull: true,
comment: 'Additional metadata about the event'
},
success: {
type: DataTypes.BOOLEAN,
allowNull: true,
comment: 'Whether the action was successful'
}
}, {
tableName: 'audit_logs',
timestamps: false, // We use our own timestamp field
indexes: [
{
fields: ['timestamp']
},
{
fields: ['action']
},
{
fields: ['user_id']
},
{
fields: ['tenant_id']
},
{
fields: ['level']
},
{
fields: ['timestamp', 'action']
},
{
fields: ['tenant_id', 'timestamp']
}
]
});
// Define associations
AuditLog.associate = function(models) {
// Association with User
AuditLog.belongsTo(models.User, {
foreignKey: 'user_id',
as: 'user'
});
// Association with Tenant
AuditLog.belongsTo(models.Tenant, {
foreignKey: 'tenant_id',
as: 'tenant'
});
};
return AuditLog;
};

View File

@@ -1304,4 +1304,261 @@ router.post('/tenants/:tenantId/deactivate', async (req, res) => {
}
});
/**
* GET /management/audit-logs
* Retrieve security audit logs with filtering and pagination
*/
router.get('/audit-logs', requireManagementAuth, async (req, res) => {
try {
const {
page = 1,
limit = 50,
level,
action,
tenantId,
userId,
startDate,
endDate,
search
} = req.query;
// Build where clause for filtering
const where = {};
if (level) {
where.level = level.toUpperCase();
}
if (action) {
where.action = { [Op.like]: `%${action}%` };
}
if (tenantId) {
where.tenant_id = tenantId;
}
if (userId) {
where.user_id = userId;
}
if (startDate || endDate) {
where.timestamp = {};
if (startDate) {
where.timestamp[Op.gte] = new Date(startDate);
}
if (endDate) {
where.timestamp[Op.lte] = new Date(endDate);
}
}
if (search) {
where[Op.or] = [
{ message: { [Op.like]: `%${search}%` } },
{ username: { [Op.like]: `%${search}%` } },
{ tenant_slug: { [Op.like]: `%${search}%` } }
];
}
// Calculate offset for pagination
const offset = (parseInt(page) - 1) * parseInt(limit);
// Get audit logs with associated data
const { AuditLog } = require('../models');
const { count, rows: auditLogs } = await AuditLog.findAndCountAll({
where,
include: [
{
model: User,
as: 'user',
attributes: ['id', 'username', 'email'],
required: false
},
{
model: Tenant,
as: 'tenant',
attributes: ['id', 'name', 'slug'],
required: false
}
],
order: [['timestamp', 'DESC']],
limit: parseInt(limit),
offset: offset
});
// Calculate pagination info
const totalPages = Math.ceil(count / parseInt(limit));
const hasNextPage = parseInt(page) < totalPages;
const hasPrevPage = parseInt(page) > 1;
// Log the management access
console.log(`[MANAGEMENT AUDIT] ${new Date().toISOString()} - Admin ${req.managementUser.username} accessed audit logs`);
res.json({
success: true,
data: {
auditLogs,
pagination: {
currentPage: parseInt(page),
totalPages,
totalCount: count,
limit: parseInt(limit),
hasNextPage,
hasPrevPage
},
filters: {
level,
action,
tenantId,
userId,
startDate,
endDate,
search
}
}
});
} catch (error) {
console.error('Management: Error retrieving audit logs:', error);
res.status(500).json({
success: false,
message: 'Failed to retrieve audit logs'
});
}
});
/**
* GET /management/audit-logs/actions
* Get list of available audit log actions for filtering
*/
router.get('/audit-logs/actions', requireManagementAuth, async (req, res) => {
try {
const { AuditLog } = require('../models');
const actions = await AuditLog.findAll({
attributes: [[AuditLog.sequelize.fn('DISTINCT', AuditLog.sequelize.col('action')), 'action']],
where: {
action: { [Op.ne]: null }
},
raw: true
});
res.json({
success: true,
data: actions.map(item => item.action).filter(Boolean).sort()
});
} catch (error) {
console.error('Management: Error retrieving audit log actions:', error);
res.status(500).json({
success: false,
message: 'Failed to retrieve audit log actions'
});
}
});
/**
* GET /management/audit-logs/summary
* Get audit log summary statistics
*/
router.get('/audit-logs/summary', requireManagementAuth, async (req, res) => {
try {
const { timeframe = '24h' } = req.query;
// Calculate time range
const now = new Date();
let startTime;
switch (timeframe) {
case '1h':
startTime = new Date(now.getTime() - 60 * 60 * 1000);
break;
case '24h':
startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000);
break;
case '7d':
startTime = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
break;
case '30d':
startTime = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
break;
default:
startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000);
}
const { AuditLog } = require('../models');
// Get summary statistics
const [totalLogs, successfulActions, failedActions, warningActions, criticalActions] = await Promise.all([
AuditLog.count({
where: { timestamp: { [Op.gte]: startTime } }
}),
AuditLog.count({
where: {
timestamp: { [Op.gte]: startTime },
success: true
}
}),
AuditLog.count({
where: {
timestamp: { [Op.gte]: startTime },
success: false
}
}),
AuditLog.count({
where: {
timestamp: { [Op.gte]: startTime },
level: 'WARNING'
}
}),
AuditLog.count({
where: {
timestamp: { [Op.gte]: startTime },
level: 'CRITICAL'
}
})
]);
// Get top actions
const topActions = await AuditLog.findAll({
attributes: [
'action',
[AuditLog.sequelize.fn('COUNT', AuditLog.sequelize.col('action')), 'count']
],
where: {
timestamp: { [Op.gte]: startTime },
action: { [Op.ne]: null }
},
group: ['action'],
order: [[AuditLog.sequelize.literal('count'), 'DESC']],
limit: 10,
raw: true
});
res.json({
success: true,
data: {
timeframe,
period: {
start: startTime.toISOString(),
end: now.toISOString()
},
summary: {
totalLogs,
successfulActions,
failedActions,
warningActions,
criticalActions
},
topActions
}
});
} catch (error) {
console.error('Management: Error retrieving audit log summary:', error);
res.status(500).json({
success: false,
message: 'Failed to retrieve audit log summary'
});
}
});
module.exports = router;

View File

@@ -17,6 +17,10 @@ const { securityLogger } = require('../middleware/logger');
// Initialize multi-tenant auth
const multiAuth = new MultiTenantAuth();
// Initialize SecurityLogger with models
const models = require('../models');
securityLogger.setModels(models);
// Configure multer for logo uploads
const storage = multer.diskStorage({
destination: function (req, file, cb) {