navidocs/server/services/audit.service.js
ggq-admin d147ebbca7 feat: Phase 1 - Authentication foundation
Implement complete JWT-based authentication system with comprehensive security features:

Database:
- Migration 005: Add 4 new tables (refresh_tokens, password_reset_tokens, audit_log, entity_permissions)
- Enhanced users table with email verification, account status, lockout protection

Services:
- auth.service.js: Full authentication lifecycle (register, login, refresh, logout, password reset, email verification)
- audit.service.js: Comprehensive security event logging and tracking

Routes:
- auth.routes.js: 9 authentication endpoints (register, login, refresh, logout, profile, password operations, email verification)

Middleware:
- auth.middleware.js: Token authentication, email verification, account status checks

Security Features:
- bcrypt password hashing (cost 12)
- JWT access tokens (15-minute expiry)
- Refresh tokens (7-day expiry, SHA256 hashed, revocable)
- Account lockout (5 failed attempts = 15 minutes)
- Token rotation on password reset
- Email verification workflow
- Comprehensive audit logging

Scripts:
- run-migration.js: Automated database migration runner
- test-auth.js: Comprehensive test suite (10 tests)
- check-audit-log.js: Audit log verification tool

All tests passing. Production-ready implementation.

🤖 Generated with Claude Code
2025-10-21 10:11:34 +02:00

282 lines
7.8 KiB
JavaScript

/**
* Audit Logging Service
*
* Logs security and business events for compliance, debugging, and security monitoring
* All timestamps are Unix epoch seconds
*/
import { v4 as uuidv4 } from 'uuid';
import { getDb } from '../config/db.js';
/**
* Log an audit event
*
* @param {Object} eventData - Event data
* @param {string} eventData.userId - User ID (optional for anonymous events)
* @param {string} eventData.eventType - Event type (e.g., 'user.login', 'entity.create')
* @param {string} eventData.resourceType - Resource type (optional, e.g., 'entity', 'organization')
* @param {string} eventData.resourceId - Resource ID (optional)
* @param {string} eventData.ipAddress - IP address of the request
* @param {string} eventData.userAgent - User agent string
* @param {string} eventData.status - Event status: 'success', 'failure', or 'denied'
* @param {string|Object} eventData.metadata - Additional metadata (will be JSON stringified if object)
* @returns {Promise<Object>} Created audit log entry
*/
export async function logAuditEvent(eventData) {
const db = getDb();
const now = Math.floor(Date.now() / 1000);
const {
userId = null,
eventType,
resourceType = null,
resourceId = null,
ipAddress = null,
userAgent = null,
status = 'success',
metadata = null
} = eventData;
// Validate required fields
if (!eventType) {
throw new Error('eventType is required for audit logging');
}
if (!['success', 'failure', 'denied'].includes(status)) {
throw new Error('status must be one of: success, failure, denied');
}
// Convert metadata to JSON string if it's an object
const metadataString = metadata && typeof metadata === 'object'
? JSON.stringify(metadata)
: metadata;
const auditId = uuidv4();
try {
db.prepare(`
INSERT INTO audit_log (
id, user_id, event_type, resource_type, resource_id,
ip_address, user_agent, status, metadata, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
auditId,
userId,
eventType,
resourceType,
resourceId,
ipAddress,
userAgent,
status,
metadataString,
now
);
return {
id: auditId,
userId,
eventType,
resourceType,
resourceId,
status,
createdAt: now
};
} catch (error) {
// Log to console if database write fails (avoid losing audit trail)
console.error('[Audit] Failed to log event:', error.message, eventData);
throw error;
}
}
/**
* Get audit logs for a specific user
*
* @param {string} userId - User ID
* @param {Object} options - Query options
* @param {number} options.limit - Maximum number of records (default: 100)
* @param {number} options.offset - Offset for pagination (default: 0)
* @param {string} options.eventType - Filter by event type
* @param {number} options.startDate - Filter by start date (Unix timestamp)
* @param {number} options.endDate - Filter by end date (Unix timestamp)
* @returns {Array} Audit log entries
*/
export function getAuditLogsByUser(userId, options = {}) {
const db = getDb();
const {
limit = 100,
offset = 0,
eventType = null,
startDate = null,
endDate = null
} = options;
let query = `
SELECT id, user_id, event_type, resource_type, resource_id,
ip_address, user_agent, status, metadata, created_at
FROM audit_log
WHERE user_id = ?
`;
const params = [userId];
if (eventType) {
query += ' AND event_type = ?';
params.push(eventType);
}
if (startDate) {
query += ' AND created_at >= ?';
params.push(startDate);
}
if (endDate) {
query += ' AND created_at <= ?';
params.push(endDate);
}
query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
params.push(limit, offset);
return db.prepare(query).all(...params);
}
/**
* Get audit logs for a specific resource
*
* @param {string} resourceType - Resource type (e.g., 'entity', 'organization')
* @param {string} resourceId - Resource ID
* @param {Object} options - Query options
* @param {number} options.limit - Maximum number of records (default: 100)
* @param {number} options.offset - Offset for pagination (default: 0)
* @returns {Array} Audit log entries
*/
export function getAuditLogsByResource(resourceType, resourceId, options = {}) {
const db = getDb();
const { limit = 100, offset = 0 } = options;
return db.prepare(`
SELECT id, user_id, event_type, resource_type, resource_id,
ip_address, user_agent, status, metadata, created_at
FROM audit_log
WHERE resource_type = ? AND resource_id = ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`).all(resourceType, resourceId, limit, offset);
}
/**
* Get recent audit logs (for admin dashboards)
*
* @param {Object} options - Query options
* @param {number} options.limit - Maximum number of records (default: 100)
* @param {number} options.offset - Offset for pagination (default: 0)
* @param {string} options.status - Filter by status ('success', 'failure', 'denied')
* @param {string} options.eventType - Filter by event type
* @returns {Array} Audit log entries
*/
export function getRecentAuditLogs(options = {}) {
const db = getDb();
const {
limit = 100,
offset = 0,
status = null,
eventType = null
} = options;
let query = `
SELECT id, user_id, event_type, resource_type, resource_id,
ip_address, user_agent, status, metadata, created_at
FROM audit_log
WHERE 1=1
`;
const params = [];
if (status) {
query += ' AND status = ?';
params.push(status);
}
if (eventType) {
query += ' AND event_type = ?';
params.push(eventType);
}
query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
params.push(limit, offset);
return db.prepare(query).all(...params);
}
/**
* Get security alerts (failed logins, denied access attempts)
*
* @param {Object} options - Query options
* @param {number} options.hours - Hours to look back (default: 24)
* @param {number} options.limit - Maximum number of records (default: 100)
* @returns {Array} Security alert entries
*/
export function getSecurityAlerts(options = {}) {
const db = getDb();
const { hours = 24, limit = 100 } = options;
const now = Math.floor(Date.now() / 1000);
const startTime = now - (hours * 60 * 60);
return db.prepare(`
SELECT id, user_id, event_type, resource_type, resource_id,
ip_address, user_agent, status, metadata, created_at
FROM audit_log
WHERE status IN ('failure', 'denied')
AND created_at >= ?
ORDER BY created_at DESC
LIMIT ?
`).all(startTime, limit);
}
/**
* Count events by type within a time period
*
* @param {Object} options - Query options
* @param {number} options.startDate - Start date (Unix timestamp)
* @param {number} options.endDate - End date (Unix timestamp)
* @returns {Array} Event counts by type
*/
export function getEventStats(options = {}) {
const db = getDb();
const {
startDate = Math.floor(Date.now() / 1000) - (24 * 60 * 60), // Last 24 hours
endDate = Math.floor(Date.now() / 1000)
} = options;
return db.prepare(`
SELECT event_type, status, COUNT(*) as count
FROM audit_log
WHERE created_at >= ? AND created_at <= ?
GROUP BY event_type, status
ORDER BY count DESC
`).all(startDate, endDate);
}
/**
* Clean up old audit logs (for data retention compliance)
*
* @param {number} retentionDays - Number of days to retain logs (default: 90)
* @returns {Object} Deletion result
*/
export function cleanupOldAuditLogs(retentionDays = 90) {
const db = getDb();
const now = Math.floor(Date.now() / 1000);
const cutoffDate = now - (retentionDays * 24 * 60 * 60);
const result = db.prepare(`
DELETE FROM audit_log
WHERE created_at < ?
`).run(cutoffDate);
return {
deleted: result.changes,
cutoffDate,
retentionDays
};
}