Jul 2nd - Optimized code
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* Shared password-strength UI: live rule checklist, strength meter,
|
||||
* confirm-password match indicator, and show/hide toggle.
|
||||
*
|
||||
* The RULES below mirror validate_password() in
|
||||
* app/services/validation_service.py exactly. This is the single
|
||||
* client-side copy of that policy — every password form (register,
|
||||
* profile, admin create/edit user, password reset) includes this file
|
||||
* instead of re-implementing the checks, so the two can never drift
|
||||
* out of sync with each other. The server remains the source of truth;
|
||||
* this file only gives the user live feedback before they submit.
|
||||
*/
|
||||
(function (global) {
|
||||
'use strict';
|
||||
|
||||
const RULES = {
|
||||
len : pw => pw.length >= 8 && pw.length <= 128,
|
||||
upper : pw => /[A-Z]/.test(pw),
|
||||
digit : pw => /\d/.test(pw),
|
||||
special: pw => /[!@#$%^&*()\-_=+\[\]{};:'",.<>?/\\|`~]/.test(pw),
|
||||
};
|
||||
|
||||
const LEVELS = [
|
||||
{ label: 'Very Weak', color: '#ef4444', pct: 12 },
|
||||
{ label: 'Weak', color: '#f97316', pct: 30 },
|
||||
{ label: 'Fair', color: '#eab308', pct: 52 },
|
||||
{ label: 'Good', color: '#22c55e', pct: 76 },
|
||||
{ label: 'Strong', color: '#059669', pct: 100 },
|
||||
];
|
||||
|
||||
function score(pw) {
|
||||
let s = 0;
|
||||
if (RULES.len(pw)) s++;
|
||||
if (RULES.upper(pw)) s++;
|
||||
if (RULES.digit(pw)) s++;
|
||||
if (RULES.special(pw)) s++;
|
||||
if (pw.length >= 16) s++; // bonus for extra length
|
||||
return s;
|
||||
}
|
||||
|
||||
function allRulesMet(pw) {
|
||||
return RULES.len(pw) && RULES.upper(pw) && RULES.digit(pw) && RULES.special(pw);
|
||||
}
|
||||
|
||||
function byId(idOrEl) {
|
||||
return typeof idOrEl === 'string' ? document.getElementById(idOrEl) : idOrEl;
|
||||
}
|
||||
|
||||
function setRuleEl(el, met) {
|
||||
if (!el) return;
|
||||
el.classList.toggle('met', met);
|
||||
el.classList.toggle('unmet', !met);
|
||||
const icon = el.querySelector('.ri');
|
||||
if (icon) {
|
||||
icon.innerHTML = met
|
||||
? '<i class="bi bi-check-circle-fill"></i>'
|
||||
: '<i class="bi bi-circle"></i>';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleVisibility(inputEl, btnEl) {
|
||||
if (!inputEl || !btnEl) return;
|
||||
const showing = inputEl.type === 'password';
|
||||
inputEl.type = showing ? 'text' : 'password';
|
||||
const icon = btnEl.querySelector('i');
|
||||
if (icon) icon.className = showing ? 'bi bi-eye-slash' : 'bi bi-eye';
|
||||
btnEl.title = showing ? 'Hide password' : 'Show password';
|
||||
}
|
||||
|
||||
/**
|
||||
* Wire up a password field with a live strength bar + rule checklist,
|
||||
* and optionally a confirm-password field with a match indicator.
|
||||
*
|
||||
* opts:
|
||||
* password - id/element of the password <input> (required)
|
||||
* confirm - id/element of the confirm-password <input> (optional)
|
||||
* strengthBar - id/element of the strength bar fill
|
||||
* strengthLabel - id/element of the strength label text
|
||||
* strengthWrap - id/element to show/hide with the strength bar
|
||||
* rulesWrap - id/element of the checklist container to show/hide
|
||||
* rules - { len, upper, digit, special } -> ids/elements of checklist rows
|
||||
* matchMsg - id/element for the confirm-match message
|
||||
* toggles - [{ input, button }, ...] show/hide password buttons
|
||||
* onChange() - called after every recompute (e.g. to gate a submit button)
|
||||
*/
|
||||
function init(opts) {
|
||||
const pwEl = byId(opts.password);
|
||||
if (!pwEl) return null;
|
||||
|
||||
const confirmEl = byId(opts.confirm);
|
||||
const barEl = byId(opts.strengthBar);
|
||||
const labelEl = byId(opts.strengthLabel);
|
||||
const strengthWrapEl = byId(opts.strengthWrap);
|
||||
const rulesWrapEl = byId(opts.rulesWrap);
|
||||
const matchMsgEl = byId(opts.matchMsg);
|
||||
const ruleEls = {};
|
||||
if (opts.rules) {
|
||||
Object.keys(opts.rules).forEach(k => { ruleEls[k] = byId(opts.rules[k]); });
|
||||
}
|
||||
|
||||
function renderStrength() {
|
||||
const pw = pwEl.value;
|
||||
|
||||
if (pw.length === 0) {
|
||||
if (barEl) barEl.style.width = '0%';
|
||||
if (labelEl) { labelEl.textContent = ''; labelEl.style.color = ''; }
|
||||
if (strengthWrapEl) strengthWrapEl.style.display = 'none';
|
||||
if (rulesWrapEl) rulesWrapEl.style.display = 'none';
|
||||
pwEl.classList.remove('valid', 'invalid');
|
||||
} else {
|
||||
if (strengthWrapEl) strengthWrapEl.style.display = 'block';
|
||||
if (rulesWrapEl) rulesWrapEl.style.display = 'grid';
|
||||
Object.keys(ruleEls).forEach(k => setRuleEl(ruleEls[k], RULES[k](pw)));
|
||||
|
||||
const s = score(pw);
|
||||
const lvl = LEVELS[Math.max(0, s - 1)] || LEVELS[0];
|
||||
if (barEl) { barEl.style.width = lvl.pct + '%'; barEl.style.background = lvl.color; }
|
||||
if (labelEl) {
|
||||
const shieldIcon = s >= 4 ? 'shield-fill' : s >= 2 ? 'shield-half' : 'shield';
|
||||
labelEl.innerHTML = `<i class="bi bi-${shieldIcon}"></i> ${lvl.label}`;
|
||||
labelEl.style.color = lvl.color;
|
||||
}
|
||||
|
||||
const met = allRulesMet(pw);
|
||||
pwEl.classList.toggle('valid', met);
|
||||
pwEl.classList.toggle('invalid', !met);
|
||||
}
|
||||
|
||||
renderMatch();
|
||||
if (opts.onChange) opts.onChange();
|
||||
}
|
||||
|
||||
function renderMatch() {
|
||||
if (!confirmEl) return;
|
||||
const pw = pwEl.value;
|
||||
const pw2 = confirmEl.value;
|
||||
|
||||
if (pw2.length === 0) {
|
||||
if (matchMsgEl) { matchMsgEl.innerHTML = ''; matchMsgEl.classList.remove('ok', 'fail'); }
|
||||
confirmEl.classList.remove('valid', 'invalid');
|
||||
return;
|
||||
}
|
||||
|
||||
const ok = pw === pw2;
|
||||
if (matchMsgEl) {
|
||||
matchMsgEl.innerHTML = ok
|
||||
? '<i class="bi bi-check-circle-fill"></i> Passwords match'
|
||||
: '<i class="bi bi-x-circle-fill"></i> Passwords do not match';
|
||||
matchMsgEl.classList.toggle('ok', ok);
|
||||
matchMsgEl.classList.toggle('fail', !ok);
|
||||
}
|
||||
confirmEl.classList.toggle('valid', ok);
|
||||
confirmEl.classList.toggle('invalid', !ok);
|
||||
}
|
||||
|
||||
pwEl.addEventListener('input', renderStrength);
|
||||
if (confirmEl) {
|
||||
confirmEl.addEventListener('input', function () {
|
||||
renderMatch();
|
||||
if (opts.onChange) opts.onChange();
|
||||
});
|
||||
}
|
||||
|
||||
if (opts.toggles) {
|
||||
opts.toggles.forEach(({ input, button }) => {
|
||||
const inputEl = byId(input);
|
||||
const buttonEl = byId(button);
|
||||
if (buttonEl) buttonEl.addEventListener('click', () => toggleVisibility(inputEl, buttonEl));
|
||||
});
|
||||
}
|
||||
|
||||
return { allRulesMet, score, renderStrength, renderMatch };
|
||||
}
|
||||
|
||||
global.PasswordStrength = { init, RULES, allRulesMet, score };
|
||||
})(window);
|
||||
Reference in New Issue
Block a user