Files
picpeak/backend/src/utils/passwordValidation.js
T
paul 8588133a4e fix: critical database connection pool exhaustion issues
- 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>
2025-07-24 16:57:07 +02:00

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
};