321 lines
11 KiB
JavaScript
321 lines
11 KiB
JavaScript
/**
|
|
* Enhanced Password Validation Component
|
|
* Provides real-time password strength validation and feedback
|
|
*
|
|
* Usage:
|
|
* new PasswordValidator({
|
|
* passwordField: '#password',
|
|
* confirmField: '#verify_password',
|
|
* strengthIndicator: '#password-strength',
|
|
* requirementsContainer: '.password-checklist'
|
|
* });
|
|
*/
|
|
|
|
class PasswordValidator {
|
|
constructor(options = {}) {
|
|
this.options = {
|
|
passwordField: '#password',
|
|
confirmField: '#verify_password',
|
|
strengthIndicator: '#password-strength',
|
|
strengthText: '.password-strength-text',
|
|
requirementsContainer: '.password-checklist',
|
|
matchIndicator: '.password-match-indicator',
|
|
matchText: '.password-match-text',
|
|
toggleButtons: '.password-toggle',
|
|
minLength: 8,
|
|
maxLength: 128,
|
|
requireUppercase: true,
|
|
requireLowercase: true,
|
|
requireNumbers: true,
|
|
requireSpecialChars: true,
|
|
checkCommonPasswords: true,
|
|
...options
|
|
};
|
|
|
|
this.translations = {
|
|
very_weak: 'Very Weak',
|
|
weak: 'Weak',
|
|
fair: 'Fair',
|
|
good: 'Good',
|
|
strong: 'Strong',
|
|
match: 'Passwords match',
|
|
no_match: 'Passwords do not match',
|
|
...options.translations
|
|
};
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.bindEvents();
|
|
this.initializeToggles();
|
|
}
|
|
|
|
bindEvents() {
|
|
const passwordField = $(this.options.passwordField);
|
|
const confirmField = $(this.options.confirmField);
|
|
|
|
// Real-time password validation
|
|
passwordField.on('input', (e) => {
|
|
const password = $(e.target).val();
|
|
this.validatePassword(password);
|
|
this.checkPasswordMatch();
|
|
});
|
|
|
|
// Real-time confirmation validation
|
|
confirmField.on('input', () => {
|
|
this.checkPasswordMatch();
|
|
});
|
|
|
|
// Form submission validation
|
|
passwordField.closest('form').on('submit', (e) => {
|
|
if (!this.isFormValid()) {
|
|
e.preventDefault();
|
|
this.showValidationErrors();
|
|
}
|
|
});
|
|
}
|
|
|
|
initializeToggles() {
|
|
$(this.options.toggleButtons).on('click', (e) => {
|
|
const button = $(e.currentTarget);
|
|
const targetSelector = button.data('target');
|
|
const target = $(targetSelector);
|
|
const icon = button.find('i');
|
|
|
|
if (target.attr('type') === 'password') {
|
|
target.attr('type', 'text');
|
|
icon.removeClass('fa-eye').addClass('fa-eye-slash');
|
|
} else {
|
|
target.attr('type', 'password');
|
|
icon.removeClass('fa-eye-slash').addClass('fa-eye');
|
|
}
|
|
});
|
|
}
|
|
|
|
validatePassword(password) {
|
|
const checks = {
|
|
length: password.length >= this.options.minLength && password.length <= this.options.maxLength,
|
|
uppercase: this.options.requireUppercase ? /[A-Z]/.test(password) : true,
|
|
lowercase: this.options.requireLowercase ? /[a-z]/.test(password) : true,
|
|
number: this.options.requireNumbers ? /[0-9]/.test(password) : true,
|
|
special: this.options.requireSpecialChars ? /[^A-Za-z0-9]/.test(password) : true,
|
|
notCommon: this.options.checkCommonPasswords ? !this.isCommonPassword(password) : true
|
|
};
|
|
|
|
this.updateRequirementIndicators(checks);
|
|
|
|
const strength = this.calculateStrength(password, checks);
|
|
this.updateStrengthIndicator(strength);
|
|
|
|
return {
|
|
isValid: Object.values(checks).every(Boolean),
|
|
strength: strength,
|
|
checks: checks
|
|
};
|
|
}
|
|
|
|
updateRequirementIndicators(checks) {
|
|
const container = $(this.options.requirementsContainer);
|
|
|
|
container.find('.requirement').each((index, element) => {
|
|
const requirement = $(element);
|
|
const rule = requirement.data('rule');
|
|
const icon = requirement.find('i');
|
|
|
|
if (checks[rule]) {
|
|
requirement.addClass('valid');
|
|
icon.removeClass('fa-times text-danger').addClass('fa-check text-success');
|
|
} else {
|
|
requirement.removeClass('valid');
|
|
icon.removeClass('fa-check text-success').addClass('fa-times text-danger');
|
|
}
|
|
});
|
|
}
|
|
|
|
calculateStrength(password, checks) {
|
|
if (!password) return 0;
|
|
|
|
let score = 0;
|
|
const passedChecks = Object.values(checks).filter(Boolean).length;
|
|
const totalChecks = Object.keys(checks).length;
|
|
|
|
// Base score from requirement checks (60% of total)
|
|
score += (passedChecks / totalChecks) * 60;
|
|
|
|
// Length bonus (20% of total)
|
|
if (password.length >= 16) score += 20;
|
|
else if (password.length >= 12) score += 15;
|
|
else if (password.length >= 10) score += 10;
|
|
else if (password.length >= 8) score += 5;
|
|
|
|
// Character variety bonus (10% of total)
|
|
const uniqueChars = new Set(password.toLowerCase()).size;
|
|
const varietyRatio = uniqueChars / password.length;
|
|
if (varietyRatio > 0.8) score += 10;
|
|
else if (varietyRatio > 0.6) score += 7;
|
|
else if (varietyRatio > 0.4) score += 5;
|
|
|
|
// Pattern penalties (10% of total)
|
|
if (!this.hasSequentialChars(password)) score += 5;
|
|
if (!this.hasRepeatedChars(password)) score += 5;
|
|
|
|
// Convert score to 0-5 scale
|
|
return Math.min(Math.floor(score / 20), 5);
|
|
}
|
|
|
|
updateStrengthIndicator(strength) {
|
|
const indicator = $(this.options.strengthIndicator);
|
|
const textElement = $(this.options.strengthText);
|
|
|
|
const strengthLevels = [
|
|
this.translations.very_weak,
|
|
this.translations.weak,
|
|
this.translations.fair,
|
|
this.translations.good,
|
|
this.translations.strong
|
|
];
|
|
|
|
indicator.attr('data-strength', strength);
|
|
textElement.text(strengthLevels[strength] || strengthLevels[0]);
|
|
|
|
// Update text color
|
|
const colorClasses = ['text-danger', 'text-danger', 'text-warning', 'text-info', 'text-success'];
|
|
textElement.removeClass('text-danger text-warning text-info text-success')
|
|
.addClass(colorClasses[strength] || 'text-danger');
|
|
}
|
|
|
|
checkPasswordMatch() {
|
|
const password = $(this.options.passwordField).val();
|
|
const confirmPassword = $(this.options.confirmField).val();
|
|
const matchIndicator = $(this.options.matchIndicator);
|
|
const matchText = $(this.options.matchText);
|
|
|
|
if (!confirmPassword) {
|
|
matchIndicator.hide();
|
|
return false;
|
|
}
|
|
|
|
matchIndicator.show();
|
|
|
|
const isMatch = password === confirmPassword;
|
|
|
|
if (isMatch) {
|
|
matchText.removeClass('text-danger').addClass('text-success')
|
|
.text(this.translations.match);
|
|
} else {
|
|
matchText.removeClass('text-success').addClass('text-danger')
|
|
.text(this.translations.no_match);
|
|
}
|
|
|
|
return isMatch;
|
|
}
|
|
|
|
isFormValid() {
|
|
const password = $(this.options.passwordField).val();
|
|
const validation = this.validatePassword(password);
|
|
const passwordsMatch = this.checkPasswordMatch();
|
|
|
|
return validation.isValid && passwordsMatch;
|
|
}
|
|
|
|
showValidationErrors() {
|
|
const password = $(this.options.passwordField).val();
|
|
const validation = this.validatePassword(password);
|
|
|
|
if (!validation.isValid) {
|
|
// Scroll to password field and highlight issues
|
|
$(this.options.passwordField)[0].scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'center'
|
|
});
|
|
|
|
// Add shake animation to password field
|
|
$(this.options.passwordField).addClass('shake');
|
|
setTimeout(() => {
|
|
$(this.options.passwordField).removeClass('shake');
|
|
}, 600);
|
|
}
|
|
}
|
|
|
|
// Utility methods
|
|
isCommonPassword(password) {
|
|
const commonPasswords = [
|
|
'password', 'password123', '123456', '123456789', 'qwerty',
|
|
'abc123', 'password1', 'admin', 'administrator', 'root',
|
|
'guest', 'test', 'demo', 'welcome', 'login', 'user',
|
|
'12345678', '1234567890', 'qwerty123', 'letmein',
|
|
'monkey', 'dragon', 'master', 'shadow', 'superman'
|
|
];
|
|
|
|
return commonPasswords.includes(password.toLowerCase());
|
|
}
|
|
|
|
hasSequentialChars(password) {
|
|
const lower = password.toLowerCase();
|
|
for (let i = 0; i < lower.length - 2; i++) {
|
|
const char1 = lower.charCodeAt(i);
|
|
const char2 = lower.charCodeAt(i + 1);
|
|
const char3 = lower.charCodeAt(i + 2);
|
|
|
|
if ((char2 === char1 + 1 && char3 === char2 + 1) ||
|
|
(char2 === char1 - 1 && char3 === char2 - 1)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
hasRepeatedChars(password) {
|
|
return /(.)\1{3,}/.test(password);
|
|
}
|
|
|
|
// Public API methods
|
|
getPasswordStrength() {
|
|
const password = $(this.options.passwordField).val();
|
|
return this.calculateStrength(password, this.getChecks(password));
|
|
}
|
|
|
|
getChecks(password) {
|
|
return {
|
|
length: password.length >= this.options.minLength && password.length <= this.options.maxLength,
|
|
uppercase: this.options.requireUppercase ? /[A-Z]/.test(password) : true,
|
|
lowercase: this.options.requireLowercase ? /[a-z]/.test(password) : true,
|
|
number: this.options.requireNumbers ? /[0-9]/.test(password) : true,
|
|
special: this.options.requireSpecialChars ? /[^A-Za-z0-9]/.test(password) : true,
|
|
notCommon: this.options.checkCommonPasswords ? !this.isCommonPassword(password) : true
|
|
};
|
|
}
|
|
|
|
reset() {
|
|
$(this.options.strengthIndicator).attr('data-strength', 0);
|
|
$(this.options.strengthText).text('');
|
|
$(this.options.matchIndicator).hide();
|
|
$(this.options.requirementsContainer + ' .requirement')
|
|
.removeClass('valid')
|
|
.find('i')
|
|
.removeClass('fa-check text-success')
|
|
.addClass('fa-times text-danger');
|
|
}
|
|
}
|
|
|
|
// Auto-initialize if jQuery is available
|
|
if (typeof $ !== 'undefined') {
|
|
$(document).ready(function() {
|
|
// Auto-initialize on pages with password fields
|
|
if ($('.password-field, .password-strength-container').length > 0) {
|
|
window.passwordValidator = new PasswordValidator();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Export for module systems
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = PasswordValidator;
|
|
}
|
|
|
|
// Global fallback
|
|
if (typeof window !== 'undefined') {
|
|
window.PasswordValidator = PasswordValidator;
|
|
}
|