commit
32a36cceff
444 files changed
+67186
No files matched your search
@@ -0,0 +1,93 @@
|
||||
const AuditLog = require('../models/AuditLog');
|
||||
|
||||
/**
|
||||
* Middleware to log data changes to audit_logs table
|
||||
*
|
||||
* Usage:
|
||||
* const { captureAuditLog } = require('../middleware/auditLogger');
|
||||
*
|
||||
* router.put('/endpoint/:id', async (req, res) => {
|
||||
* const oldData = await Model.getById(id); // Fetch current data before update
|
||||
*
|
||||
* // Perform update
|
||||
* const newData = await Model.update(id, req.body);
|
||||
*
|
||||
* // Log the change
|
||||
* await captureAuditLog('table_name', id, 'UPDATE', oldData, newData);
|
||||
*
|
||||
* res.json({ success: true, data: newData });
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Capture audit log for a data change
|
||||
* @param {string} tableName - Name of the table being modified
|
||||
* @param {string|number} recordId - ID of the record being modified
|
||||
* @param {string} action - Action type: CREATE, UPDATE, DELETE
|
||||
* @param {Object} oldValues - Previous values (null for CREATE)
|
||||
* @param {Object} newValues - New values (null for DELETE)
|
||||
* @returns {Promise<Object>} Created audit log entry
|
||||
*/
|
||||
async function captureAuditLog(tableName, recordId, action, oldValues = null, newValues = null) {
|
||||
try {
|
||||
// Determine which fields changed
|
||||
const changedFields = [];
|
||||
|
||||
if (action === 'UPDATE' && oldValues && newValues) {
|
||||
// Compare old and new values to find changed fields
|
||||
for (const key in newValues) {
|
||||
if (oldValues.hasOwnProperty(key)) {
|
||||
// Handle different types of values
|
||||
const oldVal = oldValues[key];
|
||||
const newVal = newValues[key];
|
||||
|
||||
// Deep comparison for objects/arrays, simple comparison otherwise
|
||||
if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
|
||||
changedFields.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only log if there are actual changes (for UPDATE) or always for CREATE/DELETE
|
||||
if (action !== 'UPDATE' || changedFields.length > 0) {
|
||||
return await AuditLog.create(
|
||||
tableName,
|
||||
recordId,
|
||||
action,
|
||||
oldValues,
|
||||
newValues,
|
||||
changedFields
|
||||
);
|
||||
}
|
||||
|
||||
return null; // No changes to log
|
||||
} catch (error) {
|
||||
// Log error but don't fail the main operation
|
||||
console.error('Error capturing audit log:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to extract only relevant fields for logging
|
||||
* (excludes timestamps and internal fields)
|
||||
* @param {Object} data - Data object
|
||||
* @param {string[]} excludeFields - Fields to exclude (default: created_at, updated_at)
|
||||
* @returns {Object} Filtered data object
|
||||
*/
|
||||
function filterAuditFields(data, excludeFields = ['created_at', 'updated_at']) {
|
||||
if (!data) return null;
|
||||
|
||||
const filtered = { ...data };
|
||||
excludeFields.forEach(field => {
|
||||
delete filtered[field];
|
||||
});
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
captureAuditLog,
|
||||
filterAuditFields
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
const crypto = require('crypto');
|
||||
const { query } = require('../database/db');
|
||||
|
||||
/**
|
||||
* Hash an API key using SHA-256
|
||||
*/
|
||||
function hashApiKey(key) {
|
||||
return crypto.createHash('sha256').update(key).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new API key with 'cpa_' prefix
|
||||
*/
|
||||
function generateApiKey() {
|
||||
const randomBytes = crypto.randomBytes(24);
|
||||
return 'cpa_' + randomBytes.toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware to validate API key from X-API-Key header
|
||||
*/
|
||||
async function validateApiKey(req, res, next) {
|
||||
const apiKey = req.headers['x-api-key'];
|
||||
|
||||
if (!apiKey) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'API key is required. Provide X-API-Key header.'
|
||||
});
|
||||
}
|
||||
|
||||
if (!apiKey.startsWith('cpa_')) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Invalid API key format'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const keyHash = hashApiKey(apiKey);
|
||||
|
||||
const result = await query(
|
||||
`SELECT id, name, is_active, expires_at, rate_limit, allowed_ips
|
||||
FROM api_keys
|
||||
WHERE key_hash = $1`,
|
||||
[keyHash]
|
||||
);
|
||||
|
||||
const keyRecord = result.rows[0];
|
||||
|
||||
if (!keyRecord) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Invalid API key'
|
||||
});
|
||||
}
|
||||
|
||||
if (!keyRecord.is_active) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'API key is inactive'
|
||||
});
|
||||
}
|
||||
|
||||
if (keyRecord.expires_at) {
|
||||
const expiryDate = new Date(keyRecord.expires_at);
|
||||
if (expiryDate < new Date()) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'API key has expired'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (keyRecord.allowed_ips) {
|
||||
const allowedIps = keyRecord.allowed_ips.split(',').map(ip => ip.trim());
|
||||
const clientIp = req.ip || req.connection.remoteAddress;
|
||||
if (!allowedIps.includes(clientIp)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'IP address not allowed for this API key'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update last_used_at
|
||||
await query('UPDATE api_keys SET last_used_at = CURRENT_TIMESTAMP WHERE id = $1', [keyRecord.id]);
|
||||
|
||||
req.apiKey = {
|
||||
id: keyRecord.id,
|
||||
name: keyRecord.name,
|
||||
rateLimit: keyRecord.rate_limit
|
||||
};
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('API key validation error:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'Authentication error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateApiKey,
|
||||
generateApiKey,
|
||||
hashApiKey
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Error Handler Middleware
|
||||
* Sanitizes errors before sending to client (security best practice)
|
||||
*/
|
||||
|
||||
function errorHandler(err, req, res, _next) {
|
||||
// Log full error internally (with stack trace)
|
||||
console.error('[ERROR]', {
|
||||
timestamp: new Date().toISOString(),
|
||||
path: req.path,
|
||||
method: req.method,
|
||||
error: err.message,
|
||||
code: err.code,
|
||||
stack: process.env.NODE_ENV !== 'production' ? err.stack : undefined,
|
||||
});
|
||||
|
||||
// Determine error type
|
||||
const statusCode = err.statusCode || 500;
|
||||
const isClientError = statusCode >= 400 && statusCode < 500;
|
||||
|
||||
// Send sanitized response to client
|
||||
// IMPORTANT: Never expose internal error messages in production
|
||||
res.status(statusCode).json({
|
||||
success: false,
|
||||
error: isClientError ? err.message : 'Internal server error',
|
||||
code: err.code || 'INTERNAL_ERROR',
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { errorHandler };
|
||||
Reference in new issue
Block a user