8588133a4e
- Disabled duplicate email service (emailService.js) that was creating redundant connections - Increased connection pool size from 10 to 25 for production environment - Extended session timeout cache from 5 to 30 minutes to reduce DB queries - Added connection retry logic with exponential backoff for transient failures - Fixed password validation to use retry wrapper and correct setting key - Updated public settings and gallery middleware to handle connection failures gracefully These changes address the "Connection terminated unexpectedly" errors in production by: 1. Reducing unnecessary database connections 2. Increasing available connection pool capacity 3. Implementing automatic retry for transient connection failures 4. Caching frequently accessed data for longer periods 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
362 lines
11 KiB
JavaScript
362 lines
11 KiB
JavaScript
/**
|
|
* Password Validation and Security Utilities
|
|
* Implements strong password requirements and security checks
|
|
*/
|
|
|
|
const zxcvbn = require('zxcvbn');
|
|
const logger = require('./logger');
|
|
|
|
// Configuration
|
|
const PASSWORD_CONFIG = {
|
|
minLength: 8, // Reduced from 12 to 8 for better usability
|
|
requireUppercase: true,
|
|
requireLowercase: true,
|
|
requireNumbers: true,
|
|
requireSpecialChars: false, // Made optional for gallery passwords
|
|
preventCommonPasswords: true,
|
|
minStrengthScore: 2, // Reduced from 3 to 2 (moderate strength)
|
|
bcryptRounds: parseInt(process.env.BCRYPT_ROUNDS) || 12 // Configurable, default 12
|
|
};
|
|
|
|
// Common passwords to block (extend this list)
|
|
const COMMON_PASSWORDS = [
|
|
'password', 'password123', 'admin123', 'welcome123', 'test123',
|
|
'qwerty', 'abc123', '123456', 'password1', 'admin',
|
|
'letmein', 'welcome', 'monkey', 'dragon', 'baseball'
|
|
];
|
|
|
|
/**
|
|
* Validate password meets security requirements
|
|
* @param {string} password - Password to validate
|
|
* @param {Object} options - Optional configuration overrides
|
|
* @returns {Object} - { valid: boolean, errors: string[], score: number, feedback: Object }
|
|
*/
|
|
function validatePassword(password, options = {}) {
|
|
const config = { ...PASSWORD_CONFIG, ...options };
|
|
const errors = [];
|
|
|
|
// Check if password exists
|
|
if (!password || typeof password !== 'string') {
|
|
return {
|
|
valid: false,
|
|
errors: ['Password is required'],
|
|
score: 0,
|
|
feedback: {}
|
|
};
|
|
}
|
|
|
|
// Check minimum length
|
|
if (password.length < config.minLength) {
|
|
errors.push(`Password must be at least ${config.minLength} characters long`);
|
|
}
|
|
|
|
// Check uppercase requirement
|
|
if (config.requireUppercase && !/[A-Z]/.test(password)) {
|
|
errors.push('Password must contain at least one uppercase letter');
|
|
}
|
|
|
|
// Check lowercase requirement
|
|
if (config.requireLowercase && !/[a-z]/.test(password)) {
|
|
errors.push('Password must contain at least one lowercase letter');
|
|
}
|
|
|
|
// Check number requirement
|
|
if (config.requireNumbers && !/[0-9]/.test(password)) {
|
|
errors.push('Password must contain at least one number');
|
|
}
|
|
|
|
// Check special character requirement
|
|
if (config.requireSpecialChars && !/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) {
|
|
errors.push('Password must contain at least one special character');
|
|
}
|
|
|
|
// Check against common passwords
|
|
if (config.preventCommonPasswords) {
|
|
const lowerPassword = password.toLowerCase();
|
|
if (COMMON_PASSWORDS.includes(lowerPassword)) {
|
|
errors.push('This password is too common. Please choose a more unique password');
|
|
}
|
|
}
|
|
|
|
// Skip zxcvbn check if explicitly disabled (for gallery passwords)
|
|
if (options.skipStrengthCheck) {
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors,
|
|
score: 2, // Default moderate score for gallery passwords
|
|
feedback: {}
|
|
};
|
|
}
|
|
|
|
// Use zxcvbn for strength analysis
|
|
const strength = zxcvbn(password);
|
|
|
|
// Check minimum strength score
|
|
if (strength.score < config.minStrengthScore) {
|
|
errors.push('Password is too weak. Please choose a stronger password');
|
|
}
|
|
|
|
// Add zxcvbn suggestions
|
|
if (strength.feedback.suggestions.length > 0) {
|
|
errors.push(...strength.feedback.suggestions);
|
|
}
|
|
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors,
|
|
score: strength.score,
|
|
feedback: {
|
|
warning: strength.feedback.warning,
|
|
suggestions: strength.feedback.suggestions,
|
|
crackTime: strength.crack_times_display.offline_slow_hashing_1e4_per_second
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get complexity settings from database
|
|
* @returns {Object} - Password complexity configuration
|
|
*/
|
|
async function getPasswordComplexitySettings() {
|
|
try {
|
|
const { db, withRetry } = require('../database/db');
|
|
|
|
// Use retry wrapper to handle connection failures
|
|
const settings = await withRetry(async () => {
|
|
return await db('app_settings')
|
|
.where('setting_key', 'security_password_complexity_level')
|
|
.first();
|
|
});
|
|
|
|
if (!settings || !settings.setting_value) {
|
|
return 'moderate'; // Default
|
|
}
|
|
|
|
const value = typeof settings.setting_value === 'string'
|
|
? JSON.parse(settings.setting_value)
|
|
: settings.setting_value;
|
|
|
|
return value;
|
|
} catch (error) {
|
|
logger.error('Failed to get password complexity settings:', error);
|
|
return 'moderate'; // Default on error - ensures app continues working
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get password configuration based on complexity level
|
|
* @param {string} complexityLevel - Complexity level (simple, moderate, strong, very_strong)
|
|
* @returns {Object} - Password configuration
|
|
*/
|
|
function getPasswordConfigForComplexity(complexityLevel) {
|
|
const configs = {
|
|
simple: {
|
|
minLength: 6,
|
|
requireUppercase: false,
|
|
requireLowercase: false,
|
|
requireNumbers: false,
|
|
requireSpecialChars: false,
|
|
preventCommonPasswords: true,
|
|
minStrengthScore: 0
|
|
},
|
|
moderate: {
|
|
minLength: 8,
|
|
requireUppercase: true,
|
|
requireLowercase: true,
|
|
requireNumbers: true,
|
|
requireSpecialChars: false,
|
|
preventCommonPasswords: true,
|
|
minStrengthScore: 2
|
|
},
|
|
strong: {
|
|
minLength: 12,
|
|
requireUppercase: true,
|
|
requireLowercase: true,
|
|
requireNumbers: true,
|
|
requireSpecialChars: false,
|
|
preventCommonPasswords: true,
|
|
minStrengthScore: 3
|
|
},
|
|
very_strong: {
|
|
minLength: 12,
|
|
requireUppercase: true,
|
|
requireLowercase: true,
|
|
requireNumbers: true,
|
|
requireSpecialChars: true,
|
|
preventCommonPasswords: true,
|
|
minStrengthScore: 3
|
|
}
|
|
};
|
|
|
|
return configs[complexityLevel] || configs.moderate;
|
|
}
|
|
|
|
/**
|
|
* Validate password for specific contexts (admin, gallery)
|
|
* @param {string} password - Password to validate
|
|
* @param {string} context - Context ('admin' or 'gallery')
|
|
* @param {Object} userData - Additional user data for context-aware validation
|
|
* @returns {Object} - Validation result
|
|
*/
|
|
async function validatePasswordInContext(password, context, userData = {}) {
|
|
// For gallery context, use dynamic complexity settings
|
|
if (context === 'gallery') {
|
|
// Get complexity settings from database
|
|
const complexityLevel = await getPasswordComplexitySettings();
|
|
|
|
// Get configuration for the complexity level
|
|
const galleryOptions = {
|
|
...getPasswordConfigForComplexity(complexityLevel),
|
|
skipStrengthCheck: complexityLevel === 'simple' // Skip zxcvbn for simple passwords
|
|
};
|
|
|
|
// Base validation with gallery-specific options
|
|
const result = validatePassword(password, galleryOptions);
|
|
|
|
// Override validation for common date formats
|
|
// Allow passwords like "04.07.2025", "04/07/2025", "04-07-2025"
|
|
const datePattern = /^\d{1,2}[.\/-]\d{1,2}[.\/-]\d{4}$/;
|
|
if (datePattern.test(password)) {
|
|
// Date format is valid for gallery passwords
|
|
return {
|
|
valid: true,
|
|
errors: [],
|
|
score: 2,
|
|
feedback: {}
|
|
};
|
|
}
|
|
|
|
// Additional gallery-specific checks
|
|
if (password.length < 6) {
|
|
result.valid = false;
|
|
result.errors = ['Password must be at least 6 characters long'];
|
|
}
|
|
|
|
// Check if it's too simple (e.g., just "123456")
|
|
if (/^\d{1,6}$/.test(password)) {
|
|
result.valid = false;
|
|
result.errors.push('Password cannot be just numbers. Consider using a date format like "04.07.2025"');
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Base validation for other contexts
|
|
const result = validatePassword(password);
|
|
|
|
// Context-specific validation
|
|
if (context === 'admin') {
|
|
// Admins need stronger passwords
|
|
if (result.score < 4) {
|
|
result.valid = false;
|
|
result.errors.push('Admin passwords must be very strong (score 4/4)');
|
|
}
|
|
|
|
// Check password doesn't contain username
|
|
if (userData.username && password.toLowerCase().includes(userData.username.toLowerCase())) {
|
|
result.valid = false;
|
|
result.errors.push('Password must not contain your username');
|
|
}
|
|
|
|
// Check password doesn't contain email
|
|
if (userData.email) {
|
|
const emailUser = userData.email.split('@')[0];
|
|
if (password.toLowerCase().includes(emailUser.toLowerCase())) {
|
|
result.valid = false;
|
|
result.errors.push('Password must not contain parts of your email');
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Generate a secure random password
|
|
* @param {Object} options - Generation options
|
|
* @returns {string} - Generated password
|
|
*/
|
|
function generateSecurePassword(options = {}) {
|
|
const config = {
|
|
length: options.length || 16,
|
|
includeUppercase: options.includeUppercase !== false,
|
|
includeLowercase: options.includeLowercase !== false,
|
|
includeNumbers: options.includeNumbers !== false,
|
|
includeSpecialChars: options.includeSpecialChars !== false,
|
|
excludeAmbiguous: options.excludeAmbiguous !== false
|
|
};
|
|
|
|
let charset = '';
|
|
|
|
if (config.includeLowercase) {
|
|
charset += config.excludeAmbiguous ? 'abcdefghjkmnpqrstuvwxyz' : 'abcdefghijklmnopqrstuvwxyz';
|
|
}
|
|
|
|
if (config.includeUppercase) {
|
|
charset += config.excludeAmbiguous ? 'ABCDEFGHJKLMNPQRSTUVWXYZ' : 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
}
|
|
|
|
if (config.includeNumbers) {
|
|
charset += config.excludeAmbiguous ? '23456789' : '0123456789';
|
|
}
|
|
|
|
if (config.includeSpecialChars) {
|
|
charset += '!@#$%^&*()_+-=[]{}|;:,.<>?';
|
|
}
|
|
|
|
if (charset.length === 0) {
|
|
throw new Error('At least one character type must be included');
|
|
}
|
|
|
|
// Generate password
|
|
const crypto = require('crypto');
|
|
let password = '';
|
|
|
|
for (let i = 0; i < config.length; i++) {
|
|
const randomIndex = crypto.randomInt(charset.length);
|
|
password += charset[randomIndex];
|
|
}
|
|
|
|
// Ensure password meets requirements
|
|
const validation = validatePassword(password);
|
|
if (!validation.valid) {
|
|
// Recursively generate until we get a valid password
|
|
return generateSecurePassword(options);
|
|
}
|
|
|
|
return password;
|
|
}
|
|
|
|
/**
|
|
* Get bcrypt rounds configuration
|
|
* @returns {number} - Number of bcrypt rounds to use
|
|
*/
|
|
function getBcryptRounds() {
|
|
return PASSWORD_CONFIG.bcryptRounds;
|
|
}
|
|
|
|
/**
|
|
* Log password validation failures for security monitoring
|
|
* @param {string} context - Context of validation failure
|
|
* @param {Array} errors - Validation errors
|
|
* @param {Object} metadata - Additional metadata
|
|
*/
|
|
function logPasswordValidationFailure(context, errors, metadata = {}) {
|
|
logger.warn('Password validation failed', {
|
|
context,
|
|
errorCount: errors.length,
|
|
errors: errors.slice(0, 3), // Log first 3 errors only
|
|
...metadata
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
validatePassword,
|
|
validatePasswordInContext,
|
|
generateSecurePassword,
|
|
getBcryptRounds,
|
|
logPasswordValidationFailure,
|
|
getPasswordComplexitySettings,
|
|
getPasswordConfigForComplexity,
|
|
PASSWORD_CONFIG
|
|
}; |