diff --git a/client/src/components/management/AuditLogs.jsx b/client/src/components/management/AuditLogs.jsx
new file mode 100644
index 0000000..e65fb05
--- /dev/null
+++ b/client/src/components/management/AuditLogs.jsx
@@ -0,0 +1,519 @@
+import React, { useState, useEffect } from 'react';
+import { useTranslation } from '../../utils/tempTranslations';
+
+const AuditLogs = () => {
+ const { t } = useTranslation();
+ const [auditLogs, setAuditLogs] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [filters, setFilters] = useState({
+ page: 1,
+ limit: 50,
+ level: '',
+ action: '',
+ tenantId: '',
+ userId: '',
+ startDate: '',
+ endDate: '',
+ search: ''
+ });
+ const [pagination, setPagination] = useState({});
+ const [availableActions, setAvailableActions] = useState([]);
+ const [summary, setSummary] = useState({});
+
+ useEffect(() => {
+ fetchAuditLogs();
+ fetchAvailableActions();
+ fetchSummary();
+ }, [filters]);
+
+ const fetchAuditLogs = async () => {
+ try {
+ setLoading(true);
+ const queryParams = new URLSearchParams();
+
+ Object.keys(filters).forEach(key => {
+ if (filters[key]) {
+ queryParams.append(key, filters[key]);
+ }
+ });
+
+ const token = localStorage.getItem('managementToken');
+ const response = await fetch(`/api/management/audit-logs?${queryParams}`, {
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch audit logs');
+ }
+
+ const data = await response.json();
+ setAuditLogs(data.data.auditLogs);
+ setPagination(data.data.pagination);
+ setError(null);
+ } catch (err) {
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const fetchAvailableActions = async () => {
+ try {
+ const token = localStorage.getItem('managementToken');
+ const response = await fetch('/api/management/audit-logs/actions', {
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setAvailableActions(data.data);
+ }
+ } catch (err) {
+ console.error('Failed to fetch available actions:', err);
+ }
+ };
+
+ const fetchSummary = async () => {
+ try {
+ const token = localStorage.getItem('managementToken');
+ const response = await fetch('/api/management/audit-logs/summary', {
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setSummary(data.data);
+ }
+ } catch (err) {
+ console.error('Failed to fetch summary:', err);
+ }
+ };
+
+ const handleFilterChange = (key, value) => {
+ setFilters(prev => ({
+ ...prev,
+ [key]: value,
+ page: 1 // Reset to first page when filtering
+ }));
+ };
+
+ const handlePageChange = (newPage) => {
+ setFilters(prev => ({
+ ...prev,
+ page: newPage
+ }));
+ };
+
+ const formatTimestamp = (timestamp) => {
+ return new Date(timestamp).toLocaleString();
+ };
+
+ const getLevelBadgeClass = (level) => {
+ switch (level) {
+ case 'INFO':
+ return 'bg-blue-100 text-blue-800';
+ case 'WARNING':
+ return 'bg-yellow-100 text-yellow-800';
+ case 'ERROR':
+ return 'bg-red-100 text-red-800';
+ case 'CRITICAL':
+ return 'bg-red-200 text-red-900 font-bold';
+ default:
+ return 'bg-gray-100 text-gray-800';
+ }
+ };
+
+ const getSuccessIndicator = (success) => {
+ if (success === true) {
+ return ✓;
+ } else if (success === false) {
+ return ✗;
+ }
+ return -;
+ };
+
+ const clearFilters = () => {
+ setFilters({
+ page: 1,
+ limit: 50,
+ level: '',
+ action: '',
+ tenantId: '',
+ userId: '',
+ startDate: '',
+ endDate: '',
+ search: ''
+ });
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ {t('management.auditLogs') || 'Security Audit Logs'}
+
+
+
+
+ {/* Summary Statistics */}
+ {summary.summary && (
+
+
+
+
+
+
+ -
+ {t('management.totalLogs') || 'Total Logs'}
+
+ -
+ {summary.summary.totalLogs}
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+ {t('management.successfulActions') || 'Successful'}
+
+ -
+ {summary.summary.successfulActions}
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+ {t('management.failedActions') || 'Failed'}
+
+ -
+ {summary.summary.failedActions}
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+ {t('management.warnings') || 'Warnings'}
+
+ -
+ {summary.summary.warningActions}
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+ {t('management.critical') || 'Critical'}
+
+ -
+ {summary.summary.criticalActions}
+
+
+
+
+
+
+
+ )}
+
+ {/* Filters */}
+
+
+ {t('common.filters') || 'Filters'}
+
+
+
+ {/* Search */}
+
+
+ handleFilterChange('search', e.target.value)}
+ placeholder={t('management.searchPlaceholder') || 'Search logs...'}
+ className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
+ />
+
+
+ {/* Level */}
+
+
+
+
+
+ {/* Action */}
+
+
+
+
+
+ {/* Date Range */}
+
+
+
+
+
+
+ {pagination.totalCount} {t('management.totalEntries') || 'total entries'}
+
+
+
+
+ {/* Audit Logs Table */}
+
+ {loading ? (
+
+
+
{t('common.loading') || 'Loading...'}
+
+ ) : error ? (
+
+ {t('common.error') || 'Error'}: {error}
+
+ ) : auditLogs.length === 0 ? (
+
+ {t('management.noAuditLogs') || 'No audit logs found'}
+
+ ) : (
+
+
+
+
+ |
+ {t('management.timestamp') || 'Timestamp'}
+ |
+
+ {t('management.level') || 'Level'}
+ |
+
+ {t('management.action') || 'Action'}
+ |
+
+ {t('management.user') || 'User'}
+ |
+
+ {t('management.tenant') || 'Tenant'}
+ |
+
+ {t('management.message') || 'Message'}
+ |
+
+ {t('management.success') || 'Success'}
+ |
+
+ {t('management.ipAddress') || 'IP Address'}
+ |
+
+
+
+ {auditLogs.map((log) => (
+
+ |
+ {formatTimestamp(log.timestamp)}
+ |
+
+
+ {log.level}
+
+ |
+
+ {log.action}
+ |
+
+ {log.username || '-'}
+ |
+
+ {log.tenant_slug || '-'}
+ |
+
+ {log.message}
+ |
+
+ {getSuccessIndicator(log.success)}
+ |
+
+ {log.ip_address || '-'}
+ |
+
+ ))}
+
+
+
+ )}
+
+
+ {/* Pagination */}
+ {pagination.totalPages > 1 && (
+
+
+
+
+
+
+
+
+ {t('common.showing') || 'Showing'}{' '}
+ {((pagination.currentPage - 1) * pagination.limit) + 1}
+ {' '}{t('common.to') || 'to'}{' '}
+
+ {Math.min(pagination.currentPage * pagination.limit, pagination.totalCount)}
+
+ {' '}{t('common.of') || 'of'}{' '}
+ {pagination.totalCount}
+ {' '}{t('common.results') || 'results'}
+
+
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default AuditLogs;
\ No newline at end of file
diff --git a/client/src/utils/tempTranslations.js b/client/src/utils/tempTranslations.js
index e667556..0f109be 100644
--- a/client/src/utils/tempTranslations.js
+++ b/client/src/utils/tempTranslations.js
@@ -448,6 +448,38 @@ const translations = {
type: 'Type',
name: 'Name',
description: 'Description',
+ filters: 'Filters',
+ clearFilters: 'Clear Filters',
+ refresh: 'Refresh',
+ search: 'Search',
+ loading: 'Loading...',
+ error: 'Error',
+ showing: 'Showing',
+ to: 'to',
+ results: 'results'
+ },
+ management: {
+ auditLogs: 'Security Audit Logs',
+ totalLogs: 'Total Logs',
+ successfulActions: 'Successful Actions',
+ failedActions: 'Failed Actions',
+ warnings: 'Warnings',
+ critical: 'Critical Events',
+ level: 'Level',
+ action: 'Action',
+ user: 'User',
+ tenant: 'Tenant',
+ message: 'Message',
+ success: 'Success',
+ ipAddress: 'IP Address',
+ timestamp: 'Timestamp',
+ dateRange: 'Date Range',
+ searchPlaceholder: 'Search logs, users, or tenants...',
+ noAuditLogs: 'No audit logs found',
+ totalEntries: 'total entries',
+ logoManagement: 'Logo Management Events',
+ securityEvents: 'Security Events',
+ auditTrail: 'Audit Trail',
actions: 'Actions'
}
},
@@ -867,6 +899,69 @@ const translations = {
delete: 'Ta bort',
edit: 'Redigera',
view: 'Visa',
+ add: 'Lägg till',
+ update: 'Uppdatera',
+ create: 'Skapa',
+ remove: 'Ta bort',
+ close: 'Stäng',
+ open: 'Öppna',
+ back: 'Tillbaka',
+ continue: 'Fortsätt',
+ submit: 'Skicka',
+ confirm: 'Bekräfta',
+ yes: 'Ja',
+ no: 'Nej',
+ ok: 'OK',
+ apply: 'Tillämpa',
+ reset: 'Återställ',
+ clear: 'Rensa',
+ all: 'Alla',
+ none: 'Ingen',
+ selected: 'Vald',
+ total: 'Totalt',
+ page: 'Sida',
+ of: 'av',
+ previous: 'Föregående',
+ next: 'Nästa',
+ first: 'Första',
+ last: 'Sista',
+ date: 'Datum',
+ time: 'Tid',
+ status: 'Status',
+ type: 'Typ',
+ name: 'Namn',
+ description: 'Beskrivning',
+ filters: 'Filter',
+ clearFilters: 'Rensa filter',
+ refresh: 'Uppdatera',
+ search: 'Sök',
+ showing: 'Visar',
+ to: 'till',
+ results: 'resultat'
+ },
+ management: {
+ auditLogs: 'Säkerhetsgranskningsloggar',
+ totalLogs: 'Totala loggar',
+ successfulActions: 'Lyckade åtgärder',
+ failedActions: 'Misslyckade åtgärder',
+ warnings: 'Varningar',
+ critical: 'Kritiska händelser',
+ level: 'Nivå',
+ action: 'Åtgärd',
+ user: 'Användare',
+ tenant: 'Klient',
+ message: 'Meddelande',
+ success: 'Framgång',
+ ipAddress: 'IP-adress',
+ timestamp: 'Tidsstämpel',
+ dateRange: 'Datumintervall',
+ searchPlaceholder: 'Sök loggar, användare eller klienter...',
+ noAuditLogs: 'Inga granskningsloggar hittades',
+ totalEntries: 'totala poster',
+ logoManagement: 'Logotyphanteringshändelser',
+ securityEvents: 'Säkerhetshändelser',
+ auditTrail: 'Granskningsspår',
+ view: 'Visa',
close: 'Stäng',
refresh: 'Uppdatera',
search: 'Sök',
diff --git a/server/middleware/logger.js b/server/middleware/logger.js
index 359137a..3249f4b 100644
--- a/server/middleware/logger.js
+++ b/server/middleware/logger.js
@@ -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) {
diff --git a/server/migrations/20250920-add-audit-logs.js b/server/migrations/20250920-add-audit-logs.js
new file mode 100644
index 0000000..e07d460
--- /dev/null
+++ b/server/migrations/20250920-add-audit-logs.js
@@ -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');
+ }
+};
\ No newline at end of file
diff --git a/server/models/AuditLog.js b/server/models/AuditLog.js
new file mode 100644
index 0000000..8c8477b
--- /dev/null
+++ b/server/models/AuditLog.js
@@ -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;
+};
\ No newline at end of file
diff --git a/server/routes/management.js b/server/routes/management.js
index f4f30a6..ced7ce1 100644
--- a/server/routes/management.js
+++ b/server/routes/management.js
@@ -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;
diff --git a/server/routes/tenant.js b/server/routes/tenant.js
index 1b0c7ee..96d1f55 100644
--- a/server/routes/tenant.js
+++ b/server/routes/tenant.js
@@ -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) {