Fix jwt-token

This commit is contained in:
2025-09-24 04:57:07 +02:00
parent 02ce9d343b
commit 6c28330af3
6 changed files with 772 additions and 21 deletions

View File

@@ -154,42 +154,57 @@ function defineModels() {
]
});
// SecurityLog model (optional, might not exist in all installations)
// SecurityLog model - IMPORTANT: Security logs have different retention policies (much longer)
models.SecurityLog = sequelize.define('SecurityLog', {
id: {
type: DataTypes.INTEGER,
type: DataTypes.UUID,
primaryKey: true,
autoIncrement: true
defaultValue: DataTypes.UUIDV4
},
tenant_id: {
type: DataTypes.UUID,
allowNull: true
},
timestamp: {
type: DataTypes.DATE,
event_type: {
type: DataTypes.STRING(50),
allowNull: false
},
level: {
severity: {
type: DataTypes.STRING(20),
allowNull: false
},
username: {
type: DataTypes.STRING(100),
allowNull: true
},
ip_address: {
type: DataTypes.INET,
allowNull: true
},
country_code: {
type: DataTypes.STRING(2),
allowNull: true
},
message: {
type: DataTypes.TEXT,
allowNull: false
},
metadata: {
type: DataTypes.JSONB,
defaultValue: {}
created_at: {
type: DataTypes.DATE,
allowNull: false
}
}, {
tableName: 'security_logs',
timestamps: false,
indexes: [
{
fields: ['tenant_id', 'timestamp']
fields: ['tenant_id', 'created_at']
},
{
fields: ['timestamp']
fields: ['event_type', 'created_at']
},
{
fields: ['ip_address', 'created_at']
}
]
});

View File

@@ -165,11 +165,12 @@ class DataRetentionService {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - effectiveRetentionDays);
console.log(`🧹 Cleaning tenant ${tenant.slug} - removing data older than ${effectiveRetentionDays} days (before ${cutoffDate.toISOString()})`);
console.log(`🧹 Cleaning tenant ${tenant.slug} - removing operational data older than ${effectiveRetentionDays} days (before ${cutoffDate.toISOString()})`);
console.log(`📋 Note: Security logs and audit trails are preserved and not subject to automatic cleanup`);
const { DroneDetection, Heartbeat, SecurityLog } = await getModels();
const { DroneDetection, Heartbeat } = await getModels();
// Clean up drone detections
// Clean up drone detections (operational data)
const deletedDetections = await DroneDetection.destroy({
where: {
tenant_id: tenant.id,
@@ -179,7 +180,7 @@ class DataRetentionService {
}
});
// Clean up heartbeats
// Clean up heartbeats (operational data)
const deletedHeartbeats = await Heartbeat.destroy({
where: {
tenant_id: tenant.id,
@@ -189,23 +190,30 @@ class DataRetentionService {
}
});
// Clean up security logs (if they have tenant_id)
// Clean up security logs - MUCH LONGER retention (7 years for compliance)
// Security logs should only be cleaned up after 7 years, not the standard retention period
let deletedLogs = 0;
try {
const securityLogCutoffDate = new Date();
securityLogCutoffDate.setFullYear(securityLogCutoffDate.getFullYear() - 7); // 7 years retention
deletedLogs = await SecurityLog.destroy({
where: {
tenant_id: tenant.id,
timestamp: {
[Op.lt]: cutoffDate
created_at: {
[Op.lt]: securityLogCutoffDate
}
}
});
if (deletedLogs > 0) {
console.log(`🔐 Cleaned ${deletedLogs} security logs older than 7 years for tenant ${tenant.slug}`);
}
} catch (error) {
// SecurityLog might not have tenant_id field
console.log(`⚠️ Skipping security logs for tenant ${tenant.slug}: ${error.message}`);
console.log(`⚠️ Error cleaning security logs for tenant ${tenant.slug}: ${error.message}`);
}
console.log(`✅ Tenant ${tenant.slug}: Deleted ${deletedDetections} detections, ${deletedHeartbeats} heartbeats, ${deletedLogs} logs`);
console.log(`✅ Tenant ${tenant.slug}: Deleted ${deletedDetections} detections, ${deletedHeartbeats} heartbeats, ${deletedLogs} security logs (7yr retention)`);
return {
detections: deletedDetections,

View File

@@ -0,0 +1,124 @@
const { DataTypes } = require('sequelize');
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('security_logs', {
id: {
type: DataTypes.UUID,
defaultValue: Sequelize.UUIDV4,
primaryKey: true,
allowNull: false
},
tenant_id: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'tenants',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'SET NULL'
},
event_type: {
type: DataTypes.STRING(50),
allowNull: false
},
severity: {
type: DataTypes.STRING(20),
allowNull: false,
defaultValue: 'info'
},
user_id: {
type: DataTypes.UUID,
allowNull: true
},
username: {
type: DataTypes.STRING(100),
allowNull: true
},
ip_address: {
type: DataTypes.INET,
allowNull: true
},
client_ip: {
type: DataTypes.INET,
allowNull: true
},
user_agent: {
type: DataTypes.TEXT,
allowNull: true
},
rdns: {
type: DataTypes.STRING(255),
allowNull: true
},
country_code: {
type: DataTypes.STRING(2),
allowNull: true
},
country_name: {
type: DataTypes.STRING(100),
allowNull: true
},
city: {
type: DataTypes.STRING(100),
allowNull: true
},
is_high_risk_country: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
message: {
type: DataTypes.TEXT,
allowNull: false
},
metadata: {
type: DataTypes.JSONB,
allowNull: true,
defaultValue: {}
},
session_id: {
type: DataTypes.STRING(255),
allowNull: true
},
request_id: {
type: DataTypes.STRING(255),
allowNull: true
},
endpoint: {
type: DataTypes.STRING(255),
allowNull: true
},
method: {
type: DataTypes.STRING(10),
allowNull: true
},
status_code: {
type: DataTypes.INTEGER,
allowNull: true
},
alerted: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
created_at: {
type: DataTypes.DATE,
defaultValue: Sequelize.NOW,
allowNull: false
}
});
// Add indexes for performance
await queryInterface.addIndex('security_logs', ['tenant_id', 'created_at']);
await queryInterface.addIndex('security_logs', ['event_type', 'created_at']);
await queryInterface.addIndex('security_logs', ['ip_address', 'created_at']);
await queryInterface.addIndex('security_logs', ['username', 'created_at']);
await queryInterface.addIndex('security_logs', ['severity', 'created_at']);
await queryInterface.addIndex('security_logs', ['country_code', 'is_high_risk_country']);
await queryInterface.addIndex('security_logs', ['alerted', 'severity', 'created_at']);
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('security_logs');
}
};

View File

@@ -0,0 +1,160 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const SecurityLog = sequelize.define('SecurityLog', {
id: {
type: DataTypes.UUID,
defaultValue: sequelize.Sequelize.UUIDV4,
primaryKey: true
},
tenant_id: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'tenants',
key: 'id'
},
comment: 'Tenant ID for multi-tenant isolation (null for system-wide events)'
},
event_type: {
type: DataTypes.STRING(50),
allowNull: false,
comment: 'Type of security event (login_failed, login_success, suspicious_pattern, etc.)'
},
severity: {
type: DataTypes.STRING(20),
allowNull: false,
defaultValue: 'info',
validate: {
isIn: [['low', 'medium', 'high', 'critical']]
},
comment: 'Severity level of the security event'
},
user_id: {
type: DataTypes.UUID,
allowNull: true,
comment: 'User ID if applicable'
},
username: {
type: DataTypes.STRING(100),
allowNull: true,
comment: 'Username involved in the event'
},
ip_address: {
type: DataTypes.INET,
allowNull: true,
comment: 'Client IP address'
},
client_ip: {
type: DataTypes.INET,
allowNull: true,
comment: 'Real client IP (if behind proxy/load balancer)'
},
user_agent: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'User agent string'
},
rdns: {
type: DataTypes.STRING(255),
allowNull: true,
comment: 'Reverse DNS lookup of IP address'
},
country_code: {
type: DataTypes.STRING(2),
allowNull: true,
comment: 'ISO country code from IP geolocation'
},
country_name: {
type: DataTypes.STRING(100),
allowNull: true,
comment: 'Country name from IP geolocation'
},
city: {
type: DataTypes.STRING(100),
allowNull: true,
comment: 'City from IP geolocation'
},
is_high_risk_country: {
type: DataTypes.BOOLEAN,
defaultValue: false,
comment: 'Whether the country is flagged as high-risk'
},
message: {
type: DataTypes.TEXT,
allowNull: false,
comment: 'Detailed description of the security event'
},
metadata: {
type: DataTypes.JSONB,
allowNull: true,
defaultValue: {},
comment: 'Additional event-specific data'
},
session_id: {
type: DataTypes.STRING(255),
allowNull: true,
comment: 'Session ID if applicable'
},
request_id: {
type: DataTypes.STRING(255),
allowNull: true,
comment: 'Request ID for correlation'
},
endpoint: {
type: DataTypes.STRING(255),
allowNull: true,
comment: 'API endpoint or URL involved'
},
method: {
type: DataTypes.STRING(10),
allowNull: true,
comment: 'HTTP method'
},
status_code: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'HTTP response status code'
},
alerted: {
type: DataTypes.BOOLEAN,
defaultValue: false,
comment: 'Whether super admins have been alerted about this event'
},
created_at: {
type: DataTypes.DATE,
defaultValue: sequelize.Sequelize.NOW,
comment: 'When the event occurred'
}
}, {
tableName: 'security_logs',
timestamps: true,
createdAt: 'created_at',
updatedAt: false, // Security logs should not be updated
indexes: [
{
fields: ['tenant_id', 'created_at']
},
{
fields: ['event_type', 'created_at']
},
{
fields: ['ip_address', 'created_at']
},
{
fields: ['username', 'created_at']
},
{
fields: ['severity', 'created_at']
},
{
fields: ['country_code', 'is_high_risk_country']
},
{
fields: ['alerted', 'severity', 'created_at']
}
]
});
return SecurityLog;
};

View File

@@ -391,9 +391,14 @@ router.get('/', authenticateToken, requireRole(['admin']), async (req, res) => {
* Called by multi-tenant auth middleware
*/
async function loginLocal(req, res, next) {
const securityLogger = require('../services/securityLogService');
try {
// Validate required fields
if (!req.body.username || !req.body.password) {
// Log validation failure
await securityLogger.logFailedLogin(req, req.body.username || 'unknown', 'Missing credentials');
return res.status(400).json({
success: false,
message: 'Validation error for field' +
@@ -444,6 +449,10 @@ async function loginLocal(req, res, next) {
if (!user) {
console.log(`❌ Authentication failed for "${username}" in tenant "${req.tenant?.id}" - User not found`);
// Log failed login attempt
await securityLogger.logFailedLogin(req, username, 'User not found');
return res.status(401).json({
success: false,
message: 'Invalid credentials'
@@ -452,6 +461,10 @@ async function loginLocal(req, res, next) {
if (!user.is_active) {
console.log(`❌ Authentication failed for "${username}" in tenant "${req.tenant?.id}" - Account is inactive`);
// Log failed login attempt
await securityLogger.logFailedLogin(req, username, 'Account is inactive');
return res.status(401).json({
success: false,
message: 'Account is inactive'
@@ -462,6 +475,10 @@ async function loginLocal(req, res, next) {
if (!passwordMatch) {
console.log(`❌ Authentication failed for "${username}" in tenant "${req.tenant?.id}" - Invalid password`);
// Log failed login attempt
await securityLogger.logFailedLogin(req, username, 'Invalid password');
return res.status(401).json({
success: false,
message: 'Invalid credentials'
@@ -470,6 +487,13 @@ async function loginLocal(req, res, next) {
console.log(`✅ Authentication successful for "${username}" in tenant "${req.tenant?.id}"`);
// Check if this IP had recent failed attempts
const clientIP = securityLogger.getRealClientIP(req) || securityLogger.getClientIP(req);
const hadRecentFailures = securityLogger.hadRecentFailedAttempts(clientIP);
// Log successful login
await securityLogger.logSuccessfulLogin(req, user, hadRecentFailures);
// Update last login
await user.update({ last_login: new Date() });

View File

@@ -0,0 +1,420 @@
const dns = require('dns');
const { promisify } = require('util');
const reverseLookup = promisify(dns.reverse);
class SecurityLogService {
constructor() {
// High-risk countries (ISO 2-letter codes)
this.HIGH_RISK_COUNTRIES = new Set([
'RU', // Russia
'CN', // China
'KP', // North Korea
'IR', // Iran
'BY', // Belarus
// Add more as needed
]);
// Cache for IP lookups to avoid repeated API calls
this.ipCache = new Map();
this.rdnsCache = new Map();
// Suspicious activity tracking
this.recentFailedLogins = new Map(); // IP -> array of timestamps
this.recentSuccessfulLogins = new Map(); // IP -> array of timestamps
// Clean up old tracking data every hour
setInterval(() => this.cleanupTrackingData(), 60 * 60 * 1000);
}
/**
* Log a security event
*/
async logSecurityEvent(eventData) {
try {
const SecurityLog = require('../models/SecurityLog');
// Enhance event data with IP information
const enhancedData = await this.enhanceEventData(eventData);
// Create the security log entry
const securityLog = await SecurityLog.create(enhancedData);
// Check for suspicious patterns if this is a login event
if (eventData.event_type.includes('login')) {
await this.checkSuspiciousPatterns(enhancedData);
}
console.log(`🔐 Security event logged: ${eventData.event_type} from ${eventData.ip_address}`);
return securityLog;
} catch (error) {
console.error('❌ Failed to log security event:', error.message);
// Don't throw - security logging failures shouldn't break the main application
}
}
/**
* Log failed login attempt
*/
async logFailedLogin(req, username, reason = 'Invalid credentials') {
const eventData = {
event_type: 'login_failed',
severity: 'medium',
username: username,
ip_address: this.getClientIP(req),
client_ip: this.getRealClientIP(req),
user_agent: req.get('User-Agent'),
endpoint: req.originalUrl || req.url,
method: req.method,
status_code: 401,
message: `Failed login attempt for user '${username}': ${reason}`,
metadata: {
reason: reason,
referer: req.get('Referer'),
origin: req.get('Origin'),
timestamp: new Date().toISOString()
},
session_id: req.sessionID,
request_id: req.id || req.get('X-Request-ID')
};
// Track failed login for pattern detection
const clientIP = eventData.client_ip || eventData.ip_address;
this.trackFailedLogin(clientIP, username);
return await this.logSecurityEvent(eventData);
}
/**
* Log successful login
*/
async logSuccessfulLogin(req, user, wasAfterFailedAttempt = false) {
const severity = wasAfterFailedAttempt ? 'high' : 'low';
const eventData = {
event_type: 'login_success',
severity: severity,
user_id: user.id,
username: user.username || user.email,
ip_address: this.getClientIP(req),
client_ip: this.getRealClientIP(req),
user_agent: req.get('User-Agent'),
endpoint: req.originalUrl || req.url,
method: req.method,
status_code: 200,
message: `Successful login for user '${user.username || user.email}'${wasAfterFailedAttempt ? ' (after failed attempts)' : ''}`,
metadata: {
user_id: user.id,
after_failed_attempt: wasAfterFailedAttempt,
referer: req.get('Referer'),
origin: req.get('Origin'),
timestamp: new Date().toISOString()
},
session_id: req.sessionID,
request_id: req.id || req.get('X-Request-ID')
};
// Track successful login for pattern detection
const clientIP = eventData.client_ip || eventData.ip_address;
this.trackSuccessfulLogin(clientIP);
return await this.logSecurityEvent(eventData);
}
/**
* Enhance event data with IP geolocation and reverse DNS
*/
async enhanceEventData(eventData) {
const clientIP = eventData.client_ip || eventData.ip_address;
if (clientIP) {
// Get geolocation data
const geoData = await this.getIPGeolocation(clientIP);
if (geoData) {
eventData.country_code = geoData.country_code;
eventData.country_name = geoData.country_name;
eventData.city = geoData.city;
eventData.is_high_risk_country = this.HIGH_RISK_COUNTRIES.has(geoData.country_code);
}
// Get reverse DNS
eventData.rdns = await this.getRDNS(clientIP);
// Enhance logging for high-risk countries
if (eventData.is_high_risk_country) {
eventData.severity = this.escalateSeverity(eventData.severity);
eventData.message += ` [HIGH-RISK COUNTRY: ${eventData.country_name}]`;
}
}
return eventData;
}
/**
* Get client IP address, handling proxies
*/
getClientIP(req) {
return req.ip ||
req.connection?.remoteAddress ||
req.socket?.remoteAddress ||
req.connection?.socket?.remoteAddress ||
'127.0.0.1';
}
/**
* Get real client IP from headers (X-Forwarded-For, X-Real-IP, etc.)
*/
getRealClientIP(req) {
const xForwardedFor = req.get('X-Forwarded-For');
if (xForwardedFor) {
return xForwardedFor.split(',')[0].trim();
}
return req.get('X-Real-IP') ||
req.get('X-Client-IP') ||
req.get('CF-Connecting-IP') || // Cloudflare
this.getClientIP(req);
}
/**
* Get IP geolocation (stub - implement with actual service)
*/
async getIPGeolocation(ip) {
// Skip localhost and private IPs
if (ip === '127.0.0.1' || ip === '::1' || ip.startsWith('192.168.') || ip.startsWith('10.') || ip.startsWith('172.')) {
return {
country_code: 'XX',
country_name: 'Local/Private',
city: 'Local'
};
}
// Check cache first
if (this.ipCache.has(ip)) {
return this.ipCache.get(ip);
}
try {
// TODO: Integrate with actual IP geolocation service (MaxMind, ipapi, etc.)
// For now, return a placeholder
const geoData = {
country_code: 'US',
country_name: 'United States',
city: 'Unknown'
};
// Cache for 24 hours
this.ipCache.set(ip, geoData);
setTimeout(() => this.ipCache.delete(ip), 24 * 60 * 60 * 1000);
return geoData;
} catch (error) {
console.error(`Failed to get geolocation for IP ${ip}:`, error.message);
return null;
}
}
/**
* Get reverse DNS for IP
*/
async getRDNS(ip) {
// Skip localhost and private IPs
if (ip === '127.0.0.1' || ip === '::1' || ip.startsWith('192.168.') || ip.startsWith('10.') || ip.startsWith('172.')) {
return 'localhost';
}
// Check cache first
if (this.rdnsCache.has(ip)) {
return this.rdnsCache.get(ip);
}
try {
const hostnames = await reverseLookup(ip);
const rdns = hostnames[0] || null;
// Cache for 1 hour
this.rdnsCache.set(ip, rdns);
setTimeout(() => this.rdnsCache.delete(ip), 60 * 60 * 1000);
return rdns;
} catch (error) {
// RDNS lookup failed - not uncommon
return null;
}
}
/**
* Track failed login for pattern detection
*/
trackFailedLogin(ip, username) {
if (!this.recentFailedLogins.has(ip)) {
this.recentFailedLogins.set(ip, []);
}
this.recentFailedLogins.get(ip).push({
timestamp: Date.now(),
username: username
});
// Keep only last 24 hours
const oneDayAgo = Date.now() - (24 * 60 * 60 * 1000);
this.recentFailedLogins.set(ip,
this.recentFailedLogins.get(ip).filter(attempt => attempt.timestamp > oneDayAgo)
);
}
/**
* Track successful login for pattern detection
*/
trackSuccessfulLogin(ip) {
if (!this.recentSuccessfulLogins.has(ip)) {
this.recentSuccessfulLogins.set(ip, []);
}
this.recentSuccessfulLogins.get(ip).push(Date.now());
// Keep only last 24 hours
const oneDayAgo = Date.now() - (24 * 60 * 60 * 1000);
this.recentSuccessfulLogins.set(ip,
this.recentSuccessfulLogins.get(ip).filter(timestamp => timestamp > oneDayAgo)
);
}
/**
* Check for suspicious login patterns
*/
async checkSuspiciousPatterns(eventData) {
const clientIP = eventData.client_ip || eventData.ip_address;
// Pattern 1: Successful login after multiple failed attempts
if (eventData.event_type === 'login_success') {
const failedAttempts = this.recentFailedLogins.get(clientIP) || [];
const recentFailures = failedAttempts.filter(attempt =>
attempt.timestamp > (Date.now() - 60 * 60 * 1000) // Last hour
);
if (recentFailures.length >= 3) {
await this.alertSuperAdmins('suspicious_login_pattern', {
pattern: 'successful_after_failures',
ip: clientIP,
username: eventData.username,
failed_attempts: recentFailures.length,
country: eventData.country_name,
message: `Successful login for ${eventData.username} from ${clientIP} after ${recentFailures.length} failed attempts`
});
}
}
// Pattern 2: Multiple failed logins from different IPs to different usernames (distributed attack)
await this.checkDistributedAttack();
}
/**
* Check for distributed brute force attacks
*/
async checkDistributedAttack() {
const now = Date.now();
const fiveMinutesAgo = now - (5 * 60 * 1000);
let totalFailedLogins = 0;
let uniqueIPs = new Set();
let uniqueUsernames = new Set();
for (const [ip, attempts] of this.recentFailedLogins.entries()) {
const recentAttempts = attempts.filter(attempt => attempt.timestamp > fiveMinutesAgo);
if (recentAttempts.length > 0) {
totalFailedLogins += recentAttempts.length;
uniqueIPs.add(ip);
recentAttempts.forEach(attempt => uniqueUsernames.add(attempt.username));
}
}
// Alert if we see many failed logins from many IPs to many usernames
if (totalFailedLogins >= 20 && uniqueIPs.size >= 5 && uniqueUsernames.size >= 5) {
await this.alertSuperAdmins('distributed_attack', {
pattern: 'distributed_brute_force',
total_attempts: totalFailedLogins,
unique_ips: uniqueIPs.size,
unique_usernames: uniqueUsernames.size,
message: `Distributed attack detected: ${totalFailedLogins} failed logins from ${uniqueIPs.size} IPs targeting ${uniqueUsernames.size} usernames in the last 5 minutes`
});
}
}
/**
* Alert super admins about suspicious activity
*/
async alertSuperAdmins(alertType, details) {
try {
// Log the alert as a critical security event
await this.logSecurityEvent({
event_type: 'security_alert',
severity: 'critical',
message: `SECURITY ALERT: ${details.message}`,
metadata: {
alert_type: alertType,
...details
},
alerted: true
});
// TODO: Send actual notifications (email, SMS, Slack, etc.)
console.log(`🚨 SECURITY ALERT - ${alertType.toUpperCase()}:`, details.message);
} catch (error) {
console.error('❌ Failed to alert super admins:', error.message);
}
}
/**
* Escalate severity level for high-risk events
*/
escalateSeverity(currentSeverity) {
const severityLevels = { low: 1, medium: 2, high: 3, critical: 4 };
const reverseLevels = { 1: 'low', 2: 'medium', 3: 'high', 4: 'critical' };
const currentLevel = severityLevels[currentSeverity] || 1;
const newLevel = Math.min(currentLevel + 1, 4);
return reverseLevels[newLevel];
}
/**
* Clean up old tracking data
*/
cleanupTrackingData() {
const oneDayAgo = Date.now() - (24 * 60 * 60 * 1000);
for (const [ip, attempts] of this.recentFailedLogins.entries()) {
const filtered = attempts.filter(attempt => attempt.timestamp > oneDayAgo);
if (filtered.length === 0) {
this.recentFailedLogins.delete(ip);
} else {
this.recentFailedLogins.set(ip, filtered);
}
}
for (const [ip, timestamps] of this.recentSuccessfulLogins.entries()) {
const filtered = timestamps.filter(timestamp => timestamp > oneDayAgo);
if (filtered.length === 0) {
this.recentSuccessfulLogins.delete(ip);
} else {
this.recentSuccessfulLogins.set(ip, filtered);
}
}
}
/**
* Check if IP recently had failed login followed by successful login
*/
hadRecentFailedAttempts(ip) {
const failedAttempts = this.recentFailedLogins.get(ip) || [];
const recentFailures = failedAttempts.filter(attempt =>
attempt.timestamp > (Date.now() - 60 * 60 * 1000) // Last hour
);
return recentFailures.length > 0;
}
}
module.exports = new SecurityLogService();