Jul 2nd - Optimized code

This commit is contained in:
2026-07-02 14:36:06 -04:00
parent d27ccb4ddd
commit c633960e3d
8 changed files with 478 additions and 554 deletions
+5 -1
View File
@@ -23,7 +23,7 @@ def validate_password(password: str, confirm: str) -> str | None:
Rules
-----
- Password and confirmation must match.
- Minimum length: 8 characters.
- Length: 8-128 characters.
- Must contain at least one uppercase letter (A-Z).
- Must contain at least one digit (0-9).
- Must contain at least one special character (!@#$%^&* etc.).
@@ -38,6 +38,10 @@ def validate_password(password: str, confirm: str) -> str | None:
return 'Passwords do not match.'
if len(password) < 8:
return 'Password must be at least 8 characters.'
# Upper bound prevents a pathologically long input from driving up the
# cost of password hashing (which scales with input size) on the server.
if len(password) > 128:
return 'Password must be no more than 128 characters.'
if not re.search(r'[A-Z]', password):
return 'Password must contain at least one uppercase letter.'
if not re.search(r'\d', password):
+54
View File
@@ -0,0 +1,54 @@
/*
* Shared password-strength UI: show/hide toggle, strength meter, rule
* checklist, and confirm-match indicator. Used on the register, profile,
* admin create/edit user, and password reset forms.
*
* Relies on the --bg, --border, --border2, --muted, --success, --danger,
* --accent CSS custom properties already defined by each page's theme
* (see base.html for admin/auth pages, or the page's own :root block for
* the standalone auth pages).
*/
.pw-wrap { position: relative; }
.pw-wrap input { padding-right: 42px; }
.pw-toggle {
position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
background: none; border: none; color: var(--muted); cursor: pointer;
font-size: 16px; padding: 4px; line-height: 1; transition: color .15s;
}
.pw-toggle:hover { color: var(--accent); }
.pw-strength-wrap { margin-top: 8px; }
.pw-strength-track {
height: 4px; background: var(--border); border-radius: 2px;
overflow: hidden; margin-bottom: 6px;
}
.pw-strength-fill {
height: 100%; border-radius: 2px; width: 0%;
transition: width .3s ease, background .3s ease;
}
.pw-strength-label {
font-size: 11px; font-weight: 600; letter-spacing: .3px;
display: flex; align-items: center; gap: 5px; transition: color .3s;
}
.pw-rules {
margin-top: 8px; display: grid; grid-template-columns: 1fr 1fr; gap: 3px 12px;
background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
padding: 10px 12px;
}
.pw-rule { display: flex; align-items: center; gap: 5px; font-size: 11.5px; color: var(--muted); transition: color .2s; }
.pw-rule .ri { font-size: 13px; width: 14px; text-align: center; transition: color .2s, transform .15s; }
.pw-rule.met { color: var(--success); }
.pw-rule.met .ri { color: var(--success); transform: scale(1.1); }
.pw-rule.unmet .ri { color: var(--border2); }
.pw-match {
font-size: 11.5px; margin-top: 6px; display: flex; align-items: center; gap: 5px;
font-weight: 600; letter-spacing: .2px; min-height: 18px; transition: color .2s;
}
.pw-match.ok { color: var(--success); }
.pw-match.fail { color: var(--danger); }
.pw-input.valid { border-color: var(--success) !important; box-shadow: 0 0 0 3px rgba(5,150,105,.08) !important; }
.pw-input.invalid { border-color: var(--danger) !important; box-shadow: 0 0 0 3px rgba(220,38,38,.08) !important; }
+176
View File
@@ -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);
+38 -123
View File
@@ -3,26 +3,7 @@
{% block page_title %}User Management{% endblock %}
{% block head %}
<style>
/* ── Password rule checklist ── */
.pw-rules{
margin-top:8px;display:grid;grid-template-columns:1fr 1fr;gap:3px 12px;
background:var(--bg);border:1px solid var(--border);border-radius:8px;
padding:10px 12px;
}
.pw-rule{
display:flex;align-items:center;gap:5px;
font-size:11.5px;color:var(--muted);
transition:color .2s;
}
.pw-rule .ri{
font-size:13px;width:14px;text-align:center;
transition:color .2s, transform .15s;
}
.pw-rule.met{color:var(--success);}
.pw-rule.met .ri{color:var(--success);transform:scale(1.1);}
.pw-rule.unmet .ri{color:var(--border2);}
</style>
<link rel="stylesheet" href="{{ url_for('static', filename='css/password-strength.css') }}"/>
{% endblock %}
{% block content %}
@@ -120,36 +101,39 @@
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label">Password *</label>
<div style="position:relative;">
<input type="password" class="form-control" name="password"
id="pw-field" required minlength="8"
<div class="pw-wrap">
<input type="password" class="form-control pw-input" name="password"
id="pw-field" required minlength="8" maxlength="128"
placeholder="Min. 8 characters"
style="padding-right:44px;"/>
<button type="button" onclick="togglePw('pw-field','eye1')"
style="position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:none;color:var(--muted);cursor:pointer;padding:4px;">
<i class="bi bi-eye" id="eye1"></i>
autocomplete="new-password"/>
<button type="button" class="pw-toggle" id="pw-field-toggle"
title="Show / hide password" tabindex="-1">
<i class="bi bi-eye"></i>
</button>
</div>
</div>
<div class="col-md-6">
<label class="form-label">Confirm Password *</label>
<div style="position:relative;">
<input type="password" class="form-control" name="confirm_password"
id="pw-field2" required minlength="8"
<div class="pw-wrap">
<input type="password" class="form-control pw-input" name="confirm_password"
id="pw-field2" required minlength="8" maxlength="128"
placeholder="Repeat password"
style="padding-right:44px;"/>
<button type="button" onclick="togglePw('pw-field2','eye2')"
style="position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:none;color:var(--muted);cursor:pointer;padding:4px;">
<i class="bi bi-eye" id="eye2"></i>
autocomplete="new-password"/>
<button type="button" class="pw-toggle" id="pw-field2-toggle"
title="Show / hide password" tabindex="-1">
<i class="bi bi-eye"></i>
</button>
</div>
<div class="pw-match" id="pw-match-msg"></div>
</div>
<div class="col-12">
<!-- Password strength bar -->
<div style="height:4px;background:var(--border);border-radius:2px;margin-top:4px;">
<div id="pw-strength-bar" style="height:100%;border-radius:2px;width:0%;transition:width .3s,background .3s;"></div>
<!-- Strength bar -->
<div class="pw-strength-wrap" id="pw-strength-wrap" style="display:none;">
<div class="pw-strength-track">
<div class="pw-strength-fill" id="pw-strength-bar"></div>
</div>
<div class="pw-strength-label" id="pw-strength-label"></div>
</div>
<div id="pw-strength-label" style="font-size:11px;color:var(--muted);margin-top:4px;"></div>
<!-- Rule checklist — mirrors server-side validate_password() rules -->
<div class="pw-rules" id="pw-rules-box" style="display:none;">
@@ -189,100 +173,31 @@
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/password-strength.js') }}"></script>
<script>
// ── Show/hide password ────────────────────────────────────────────────────────
function togglePw(fieldId, iconId) {
const field = document.getElementById(fieldId);
const icon = document.getElementById(iconId);
if (field.type === 'password') {
field.type = 'text';
icon.className = 'bi bi-eye-slash';
} else {
field.type = 'password';
icon.className = 'bi bi-eye';
}
}
// ── Password strength + rule checklist ────────────────────────────────────────
// Mirrors server-side validate_password() rules in validation_service.py.
// Any change to the server rules must be reflected here too.
const PW_RULES = {
len : pw => pw.length >= 8,
upper : pw => /[A-Z]/.test(pw),
digit : pw => /\d/.test(pw),
special: pw => /[!@#$%^&*()\-_=+\[\]{};:'",.<>?/\\|`~]/.test(pw),
};
function setPwRule(id, met) {
const el = document.getElementById('pw-rule-' + id);
if (!el) return;
el.className = 'pw-rule ' + (met ? 'met' : 'unmet');
el.querySelector('.ri').innerHTML = met
? '<i class="bi bi-check-circle-fill"></i>'
: '<i class="bi bi-circle"></i>';
}
document.getElementById('pw-field').addEventListener('input', function () {
const val = this.value;
const bar = document.getElementById('pw-strength-bar');
const label = document.getElementById('pw-strength-label');
const rules = document.getElementById('pw-rules-box');
if (val.length === 0) {
bar.style.width = '0%';
label.textContent = '';
rules.style.display = 'none';
return;
}
rules.style.display = 'grid';
setPwRule('len', PW_RULES.len(val));
setPwRule('upper', PW_RULES.upper(val));
setPwRule('digit', PW_RULES.digit(val));
setPwRule('special', PW_RULES.special(val));
let score = 0;
if (PW_RULES.len(val)) score++;
if (PW_RULES.upper(val)) score++;
if (PW_RULES.digit(val)) score++;
if (PW_RULES.special(val)) score++;
if (val.length >= 16) score++; // bonus for extra length
const levels = [
{ pct: '20%', bg: '#dc2626', text: 'Very weak' },
{ pct: '40%', bg: '#d97706', text: 'Weak' },
{ pct: '60%', bg: '#ca8a04', text: 'Fair' },
{ pct: '80%', bg: '#16a34a', text: 'Strong' },
{ pct: '100%', bg: '#059669', text: 'Very strong' },
];
const lvl = levels[Math.min(score - 1, 4)] || levels[0];
bar.style.width = lvl.pct;
bar.style.background = lvl.bg;
label.textContent = lvl.text;
label.style.color = lvl.bg;
const pwStrength = PasswordStrength.init({
password : 'pw-field',
confirm : 'pw-field2',
strengthBar : 'pw-strength-bar',
strengthLabel: 'pw-strength-label',
strengthWrap : 'pw-strength-wrap',
rulesWrap : 'pw-rules-box',
rules: { len: 'pw-rule-len', upper: 'pw-rule-upper', digit: 'pw-rule-digit', special: 'pw-rule-special' },
matchMsg : 'pw-match-msg',
toggles: [
{ input: 'pw-field', button: 'pw-field-toggle' },
{ input: 'pw-field2', button: 'pw-field2-toggle' },
],
});
// ── Client-side password match check before submit ────────────────────────────
// ── Block submit on a real mismatch (server re-validates regardless) ──────────
document.querySelector('form').addEventListener('submit', function (e) {
const pw = document.getElementById('pw-field').value;
const pw2 = document.getElementById('pw-field2').value;
if (pw !== pw2) {
e.preventDefault();
document.getElementById('pw-field2').style.borderColor = 'var(--danger)';
const msg = document.createElement('div');
msg.style.cssText = 'font-size:12px;color:var(--danger);margin-top:4px;';
msg.textContent = 'Passwords do not match.';
const existing = document.getElementById('pw-match-msg');
if (existing) existing.remove();
msg.id = 'pw-match-msg';
document.getElementById('pw-field2').insertAdjacentElement('afterend', msg);
pwStrength.renderMatch();
}
});
document.getElementById('pw-field2').addEventListener('input', function () {
this.style.borderColor = '';
const msg = document.getElementById('pw-match-msg');
if (msg) msg.remove();
});
</script>
{% endblock %}
+43 -98
View File
@@ -3,26 +3,7 @@
{% block page_title %}Edit User{% endblock %}
{% block head %}
<style>
/* ── Password rule checklist ── */
.pw-rules{
margin-top:8px;display:grid;grid-template-columns:1fr 1fr;gap:3px 12px;
background:var(--bg);border:1px solid var(--border);border-radius:8px;
padding:10px 12px;
}
.pw-rule{
display:flex;align-items:center;gap:5px;
font-size:11.5px;color:var(--muted);
transition:color .2s;
}
.pw-rule .ri{
font-size:13px;width:14px;text-align:center;
transition:color .2s, transform .15s;
}
.pw-rule.met{color:var(--success);}
.pw-rule.met .ri{color:var(--success);transform:scale(1.1);}
.pw-rule.unmet .ri{color:var(--border2);}
</style>
<link rel="stylesheet" href="{{ url_for('static', filename='css/password-strength.css') }}"/>
{% endblock %}
{% block content %}
@@ -67,18 +48,38 @@
<div class="col-12"><hr style="border-color:var(--border);"/></div>
<div class="col-md-6">
<label class="form-label">New Password <span style="color:var(--muted);font-weight:400;">(optional)</span></label>
<input type="password" class="form-control" id="new-password" name="new_password" placeholder="Leave blank to keep current"/>
<div class="pw-wrap">
<input type="password" class="form-control pw-input" id="new-password" name="new_password"
maxlength="128" placeholder="Leave blank to keep current"
autocomplete="new-password"/>
<button type="button" class="pw-toggle" id="new-password-toggle"
title="Show / hide password" tabindex="-1">
<i class="bi bi-eye"></i>
</button>
</div>
<div style="font-size:11px;color:var(--muted);margin-top:5px;">Leave both fields blank to keep the current password.</div>
</div>
<div class="col-md-6">
<label class="form-label">Confirm Password</label>
<input type="password" class="form-control" id="confirm-password" name="confirm_password" placeholder="Repeat new password"/>
<div class="pw-wrap">
<input type="password" class="form-control pw-input" id="confirm-password" name="confirm_password"
maxlength="128" placeholder="Repeat new password"
autocomplete="new-password"/>
<button type="button" class="pw-toggle" id="confirm-password-toggle"
title="Show / hide password" tabindex="-1">
<i class="bi bi-eye"></i>
</button>
</div>
<div class="pw-match" id="pw-match-msg"></div>
</div>
<div class="col-12">
<!-- Password strength bar -->
<div style="height:4px;background:var(--border);border-radius:2px;margin-top:4px;" id="pw-strength-track">
<div id="pw-strength-bar" style="height:100%;border-radius:2px;width:0%;transition:width .3s,background .3s;"></div>
<!-- Strength bar -->
<div class="pw-strength-wrap" id="pw-strength-wrap" style="display:none;">
<div class="pw-strength-track">
<div class="pw-strength-fill" id="pw-strength-bar"></div>
</div>
<div class="pw-strength-label" id="pw-strength-label"></div>
</div>
<div id="pw-strength-label" style="font-size:11px;color:var(--muted);margin-top:4px;"></div>
<!-- Rule checklist — mirrors server-side validate_password() rules -->
<div class="pw-rules" id="pw-rules-box" style="display:none;">
@@ -113,87 +114,31 @@
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/password-strength.js') }}"></script>
<script>
// ── Password strength + rule checklist ────────────────────────────────────────
// Mirrors server-side validate_password() rules in validation_service.py.
// Any change to the server rules must be reflected here too.
const PW_RULES = {
len : pw => pw.length >= 8,
upper : pw => /[A-Z]/.test(pw),
digit : pw => /\d/.test(pw),
special: pw => /[!@#$%^&*()\-_=+\[\]{};:'",.<>?/\\|`~]/.test(pw),
};
function setPwRule(id, met) {
const el = document.getElementById('pw-rule-' + id);
if (!el) return;
el.className = 'pw-rule ' + (met ? 'met' : 'unmet');
el.querySelector('.ri').innerHTML = met
? '<i class="bi bi-check-circle-fill"></i>'
: '<i class="bi bi-circle"></i>';
}
document.getElementById('new-password').addEventListener('input', function () {
const val = this.value;
const bar = document.getElementById('pw-strength-bar');
const label = document.getElementById('pw-strength-label');
const rules = document.getElementById('pw-rules-box');
if (val.length === 0) {
bar.style.width = '0%';
label.textContent = '';
rules.style.display = 'none';
return;
}
rules.style.display = 'grid';
setPwRule('len', PW_RULES.len(val));
setPwRule('upper', PW_RULES.upper(val));
setPwRule('digit', PW_RULES.digit(val));
setPwRule('special', PW_RULES.special(val));
let score = 0;
if (PW_RULES.len(val)) score++;
if (PW_RULES.upper(val)) score++;
if (PW_RULES.digit(val)) score++;
if (PW_RULES.special(val)) score++;
if (val.length >= 16) score++; // bonus for extra length
const levels = [
{ pct: '20%', bg: '#dc2626', text: 'Very weak' },
{ pct: '40%', bg: '#d97706', text: 'Weak' },
{ pct: '60%', bg: '#ca8a04', text: 'Fair' },
{ pct: '80%', bg: '#16a34a', text: 'Strong' },
{ pct: '100%', bg: '#059669', text: 'Very strong' },
];
const lvl = levels[Math.min(score - 1, 4)] || levels[0];
bar.style.width = lvl.pct;
bar.style.background = lvl.bg;
label.textContent = lvl.text;
label.style.color = lvl.bg;
const pwStrength = PasswordStrength.init({
password : 'new-password',
confirm : 'confirm-password',
strengthBar : 'pw-strength-bar',
strengthLabel: 'pw-strength-label',
strengthWrap : 'pw-strength-wrap',
rulesWrap : 'pw-rules-box',
rules: { len: 'pw-rule-len', upper: 'pw-rule-upper', digit: 'pw-rule-digit', special: 'pw-rule-special' },
matchMsg : 'pw-match-msg',
toggles: [
{ input: 'new-password', button: 'new-password-toggle' },
{ input: 'confirm-password', button: 'confirm-password-toggle' },
],
});
// ── Client-side password match check before submit ────────────────────────────
// ── Block submit on a real mismatch (server re-validates regardless) ──────────
document.querySelector('form').addEventListener('submit', function (e) {
const pw = document.getElementById('new-password').value;
const pw2 = document.getElementById('confirm-password').value;
if (pw !== pw2) {
e.preventDefault();
document.getElementById('confirm-password').style.borderColor = 'var(--danger)';
const msg = document.createElement('div');
msg.style.cssText = 'font-size:12px;color:var(--danger);margin-top:4px;';
msg.textContent = 'Passwords do not match.';
const existing = document.getElementById('pw-match-msg');
if (existing) existing.remove();
msg.id = 'pw-match-msg';
document.getElementById('confirm-password').insertAdjacentElement('afterend', msg);
pwStrength.renderMatch();
}
});
document.getElementById('confirm-password').addEventListener('input', function () {
this.style.borderColor = '';
const msg = document.getElementById('pw-match-msg');
if (msg) msg.remove();
});
</script>
{% endblock %}
+41 -106
View File
@@ -2,6 +2,10 @@
{% block title %}My Profile{% endblock %}
{% block page_title %}My Profile{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/password-strength.css') }}"/>
{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-7">
@@ -73,51 +77,42 @@
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label">New Password</label>
<!-- Show/hide toggle wrapper -->
<div style="position:relative;">
<input type="password" class="form-control" name="new_password"
id="prof-pw" placeholder="Min. 8 characters"
autocomplete="new-password"
oninput="profPwInput()" style="padding-right:40px;"/>
<button type="button" id="prof-pw-toggle"
onclick="profToggle('prof-pw','prof-pw-toggle')"
tabindex="-1"
style="position:absolute;right:10px;top:50%;transform:translateY(-50%);
background:none;border:none;color:var(--muted);cursor:pointer;font-size:15px;">
<div class="pw-wrap">
<input type="password" class="form-control pw-input" name="new_password"
id="prof-pw" maxlength="128" placeholder="Min. 8 characters"
autocomplete="new-password"/>
<button type="button" class="pw-toggle" id="prof-pw-toggle"
title="Show / hide password" tabindex="-1">
<i class="bi bi-eye"></i>
</button>
</div>
<!-- Strength bar (hidden until typing starts) -->
<div id="prof-strength-wrap" style="display:none;margin-top:6px;">
<div style="height:4px;background:var(--border);border-radius:2px;overflow:hidden;margin-bottom:5px;">
<div id="prof-strength-bar" style="height:100%;border-radius:2px;width:0%;transition:width .3s,background .3s;"></div>
<div class="pw-strength-wrap" id="prof-strength-wrap" style="display:none;">
<div class="pw-strength-track">
<div class="pw-strength-fill" id="prof-strength-bar"></div>
</div>
<div id="prof-strength-label" style="font-size:11px;font-weight:600;font-family:'Space Mono',monospace;display:flex;align-items:center;gap:5px;"></div>
<div class="pw-strength-label" id="prof-strength-label"></div>
</div>
<!-- Rule checklist -->
<div id="prof-rules" style="display:none;margin-top:7px;background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:9px 12px;display:grid;grid-template-columns:1fr 1fr;gap:3px 10px;">
<div class="prof-rule" id="prof-rule-len" style="display:flex;align-items:center;gap:5px;font-size:11.5px;color:var(--muted);"><span class="prof-ri"><i class="bi bi-circle"></i></span> 8+ characters</div>
<div class="prof-rule" id="prof-rule-upper" style="display:flex;align-items:center;gap:5px;font-size:11.5px;color:var(--muted);"><span class="prof-ri"><i class="bi bi-circle"></i></span> Uppercase letter</div>
<div class="prof-rule" id="prof-rule-digit" style="display:flex;align-items:center;gap:5px;font-size:11.5px;color:var(--muted);"><span class="prof-ri"><i class="bi bi-circle"></i></span> Number</div>
<div class="prof-rule" id="prof-rule-special"style="display:flex;align-items:center;gap:5px;font-size:11.5px;color:var(--muted);"><span class="prof-ri"><i class="bi bi-circle"></i></span> Special character</div>
<div class="pw-rules" id="prof-rules" style="display:none;">
<div class="pw-rule unmet" id="prof-rule-len"><span class="ri"><i class="bi bi-circle"></i></span> 8+ characters</div>
<div class="pw-rule unmet" id="prof-rule-upper"><span class="ri"><i class="bi bi-circle"></i></span> Uppercase letter</div>
<div class="pw-rule unmet" id="prof-rule-digit"><span class="ri"><i class="bi bi-circle"></i></span> Number</div>
<div class="pw-rule unmet" id="prof-rule-special"><span class="ri"><i class="bi bi-circle"></i></span> Special character</div>
</div>
</div>
<div class="col-md-6">
<label class="form-label">Confirm Password</label>
<div style="position:relative;">
<input type="password" class="form-control" name="confirm_password"
id="prof-pw2" placeholder="Repeat password"
autocomplete="new-password"
oninput="profConfirmInput()" style="padding-right:40px;"/>
<button type="button" id="prof-pw2-toggle"
onclick="profToggle('prof-pw2','prof-pw2-toggle')"
tabindex="-1"
style="position:absolute;right:10px;top:50%;transform:translateY(-50%);
background:none;border:none;color:var(--muted);cursor:pointer;font-size:15px;">
<div class="pw-wrap">
<input type="password" class="form-control pw-input" name="confirm_password"
id="prof-pw2" maxlength="128" placeholder="Repeat password"
autocomplete="new-password"/>
<button type="button" class="pw-toggle" id="prof-pw2-toggle"
title="Show / hide password" tabindex="-1">
<i class="bi bi-eye"></i>
</button>
</div>
<div id="prof-match-msg" style="font-size:11.5px;margin-top:6px;font-family:'Space Mono',monospace;font-weight:600;min-height:18px;"></div>
<div class="pw-match" id="prof-match-msg"></div>
</div>
</div>
@@ -132,82 +127,22 @@
</div>
{% block scripts %}
<script src="{{ url_for('static', filename='js/password-strength.js') }}"></script>
<script>
// ── Profile page password strength — mirrors register.html rules exactly ──────
const PROF_RULES = {
len: pw => pw.length >= 8,
upper: pw => /[A-Z]/.test(pw),
digit: pw => /\d/.test(pw),
special: pw => /[!@#$%^&*()\-_=+\[\]{};:'",.<>?/\|`~]/.test(pw),
};
const PROF_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 profScore(pw) {
return [PROF_RULES.len,PROF_RULES.upper,PROF_RULES.digit,PROF_RULES.special]
.filter(r => r(pw)).length + (pw.length >= 16 ? 1 : 0);
}
function profSetRule(id, met) {
const el = document.getElementById('prof-rule-' + id);
if (!el) return;
el.style.color = met ? 'var(--success)' : 'var(--muted)';
el.querySelector('.prof-ri').innerHTML = met
? '<i class="bi bi-check-circle-fill" style="color:var(--success);"></i>'
: '<i class="bi bi-circle"></i>';
}
function profPwInput() {
const pw = document.getElementById('prof-pw').value;
const wrap = document.getElementById('prof-strength-wrap');
const rules = document.getElementById('prof-rules');
const bar = document.getElementById('prof-strength-bar');
const lbl = document.getElementById('prof-strength-label');
if (!pw) {
wrap.style.display = 'none'; rules.style.display = 'none';
document.getElementById('prof-pw').classList.remove('is-valid','is-invalid');
profConfirmInput(); return;
}
wrap.style.display = 'block'; rules.style.display = 'grid';
profSetRule('len', PROF_RULES.len(pw));
profSetRule('upper', PROF_RULES.upper(pw));
profSetRule('digit', PROF_RULES.digit(pw));
profSetRule('special', PROF_RULES.special(pw));
const score = profScore(pw);
const lvl = PROF_LEVELS[Math.max(0, score - 1)] || PROF_LEVELS[0];
bar.style.width = lvl.pct + '%';
bar.style.background = lvl.color;
lbl.style.color = lvl.color;
const icon = score >= 4 ? 'shield-fill' : score >= 2 ? 'shield-half' : 'shield';
lbl.innerHTML = `<i class="bi bi-${icon}"></i> ${lvl.label}`;
const allMet = Object.values(PROF_RULES).every(r => r(pw));
document.getElementById('prof-pw').classList.toggle('is-valid', allMet);
document.getElementById('prof-pw').classList.toggle('is-invalid', !allMet);
profConfirmInput();
}
function profConfirmInput() {
const pw = document.getElementById('prof-pw').value;
const pw2 = document.getElementById('prof-pw2').value;
const msg = document.getElementById('prof-match-msg');
const el2 = document.getElementById('prof-pw2');
if (!pw2) { msg.innerHTML=''; el2.classList.remove('is-valid','is-invalid'); return; }
const ok = pw === pw2;
msg.innerHTML = ok
? '<span style="color:var(--success);"><i class="bi bi-check-circle-fill me-1"></i>Passwords match</span>'
: '<span style="color:var(--danger);"><i class="bi bi-x-circle-fill me-1"></i>Passwords do not match</span>';
el2.classList.toggle('is-valid', ok);
el2.classList.toggle('is-invalid', !ok);
}
function profToggle(inputId, btnId) {
const inp = document.getElementById(inputId);
const btn = document.getElementById(btnId);
const isPassword = inp.type === 'password';
inp.type = isPassword ? 'text' : 'password';
btn.querySelector('i').className = isPassword ? 'bi bi-eye-slash' : 'bi bi-eye';
}
PasswordStrength.init({
password : 'prof-pw',
confirm : 'prof-pw2',
strengthBar : 'prof-strength-bar',
strengthLabel: 'prof-strength-label',
strengthWrap : 'prof-strength-wrap',
rulesWrap : 'prof-rules',
rules: { len: 'prof-rule-len', upper: 'prof-rule-upper', digit: 'prof-rule-digit', special: 'prof-rule-special' },
matchMsg : 'prof-match-msg',
toggles: [
{ input: 'prof-pw', button: 'prof-pw-toggle' },
{ input: 'prof-pw2', button: 'prof-pw2-toggle' },
],
});
</script>
{% endblock %}
{% endblock %}
+38 -190
View File
@@ -6,6 +6,7 @@
<style>:root { --accent: {{ branding.primary_color }}; --accent-h: {{ branding.primary_color }}; }</style>
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:opsz,wght@9..40,400;9..40,500;9..40,600&display=swap" rel="stylesheet"/>
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet"/>
<link rel="stylesheet" href="{{ url_for('static', filename='css/password-strength.css') }}"/>
<style>
:root{
--bg:#f0f4f8;--surface:#ffffff;--border:#e2e8f0;--border2:#cbd5e1;
@@ -31,61 +32,10 @@
input.valid{border-color:var(--success);box-shadow:0 0 0 3px rgba(5,150,105,.08);}
input.invalid{border-color:var(--danger);box-shadow:0 0 0 3px rgba(220,38,38,.08);}
/* ── Password input wrapper (for show/hide toggle) ── */
.pw-wrap{position:relative;}
.pw-wrap input{padding-right:42px;}
.pw-toggle{
position:absolute;right:12px;top:50%;transform:translateY(-50%);
background:none;border:none;color:var(--muted);cursor:pointer;
font-size:16px;padding:2px;line-height:1;
transition:color .15s;
}
.pw-toggle:hover{color:var(--accent);}
/* ── Strength bar ── */
.strength-wrap{margin-top:8px;}
.strength-bar-track{
height:4px;background:var(--border);border-radius:2px;
overflow:hidden;margin-bottom:6px;
}
.strength-bar-fill{
height:100%;border-radius:2px;width:0%;
transition:width .3s ease,background .3s ease;
}
.strength-label{
font-size:11px;font-weight:600;letter-spacing:.3px;
font-family:'Space Mono',monospace;
transition:color .3s;
display:flex;align-items:center;gap:5px;
}
/* ── Rule checklist ── */
.rules{
margin-top:8px;display:grid;grid-template-columns:1fr 1fr;gap:3px 12px;
background:var(--bg);border:1px solid var(--border);border-radius:8px;
padding:10px 12px;
}
.rule{
display:flex;align-items:center;gap:5px;
font-size:11.5px;color:var(--muted);
transition:color .2s;
}
.rule .ri{
font-size:13px;width:14px;text-align:center;
transition:color .2s, transform .15s;
}
.rule.met{color:var(--success);}
.rule.met .ri{color:var(--success);transform:scale(1.1);}
.rule.unmet .ri{color:var(--border2);}
/* ── Confirm match indicator ── */
.match-msg{
font-size:11.5px;margin-top:6px;display:flex;align-items:center;gap:5px;
font-family:'Space Mono',monospace;font-weight:600;letter-spacing:.2px;
min-height:18px;transition:color .2s;
}
.match-msg.ok{color:var(--success);}
.match-msg.fail{color:var(--danger);}
/* Register page renders the strength label/match message in the
monospace brand font; the shared stylesheet leaves font-family
unset so it inherits from the page. */
.pw-strength-label, .pw-match{font-family:'Space Mono',monospace;}
/* ── Submit ── */
.btn{width:100%;background:var(--accent);border:none;color:#fff;border-radius:9px;padding:12px;font-size:14px;font-weight:600;cursor:pointer;transition:background .15s;font-family:inherit;margin-top:4px;box-shadow:0 2px 6px rgba(37,99,235,.3);}
@@ -152,11 +102,10 @@
<label>Password *</label>
<div class="pw-wrap">
<input type="password" name="password" id="pw" required
maxlength="128"
placeholder="Min. 8 characters"
autocomplete="new-password"
oninput="onPasswordInput()"/>
autocomplete="new-password"/>
<button type="button" class="pw-toggle" id="pw-toggle"
onclick="toggleVis('pw','pw-toggle')"
title="Show / hide password"
tabindex="-1">
<i class="bi bi-eye"></i>
@@ -164,28 +113,28 @@
</div>
<!-- Strength bar -->
<div class="strength-wrap" id="strength-wrap" style="display:none;">
<div class="strength-bar-track">
<div class="strength-bar-fill" id="strength-bar"></div>
<div class="pw-strength-wrap" id="strength-wrap" style="display:none;">
<div class="pw-strength-track">
<div class="pw-strength-fill" id="strength-bar"></div>
</div>
<div class="strength-label" id="strength-label"></div>
<div class="pw-strength-label" id="strength-label"></div>
</div>
<!-- Rule checklist — mirrors server-side validate_password() rules -->
<div class="rules" id="rules-box" style="display:none;">
<div class="rule unmet" id="rule-len">
<div class="pw-rules" id="rules-box" style="display:none;">
<div class="pw-rule unmet" id="rule-len">
<span class="ri"><i class="bi bi-circle"></i></span>
<span>8+ characters</span>
</div>
<div class="rule unmet" id="rule-upper">
<div class="pw-rule unmet" id="rule-upper">
<span class="ri"><i class="bi bi-circle"></i></span>
<span>Uppercase letter</span>
</div>
<div class="rule unmet" id="rule-digit">
<div class="pw-rule unmet" id="rule-digit">
<span class="ri"><i class="bi bi-circle"></i></span>
<span>Number</span>
</div>
<div class="rule unmet" id="rule-special">
<div class="pw-rule unmet" id="rule-special">
<span class="ri"><i class="bi bi-circle"></i></span>
<span>Special character</span>
</div>
@@ -197,17 +146,16 @@
<label>Confirm Password *</label>
<div class="pw-wrap">
<input type="password" name="confirm_password" id="pw2" required
maxlength="128"
placeholder="Repeat password"
autocomplete="new-password"
oninput="onConfirmInput()"/>
autocomplete="new-password"/>
<button type="button" class="pw-toggle" id="pw2-toggle"
onclick="toggleVis('pw2','pw2-toggle')"
title="Show / hide password"
tabindex="-1">
<i class="bi bi-eye"></i>
</button>
</div>
<div class="match-msg" id="match-msg"></div>
<div class="pw-match" id="match-msg"></div>
</div>
<button type="submit" class="btn" id="submit-btn">
@@ -217,133 +165,33 @@
<div class="links">Already have an account? <a href="{{ url_for('auth.login') }}">Sign in</a></div>
</div>
<script src="{{ url_for('static', filename='js/password-strength.js') }}"></script>
<script>
// ── Mirrors server-side validate_password() rules in validation_service.py ──
// Any change to the server rules must be reflected here too.
const RULES = {
len : pw => pw.length >= 8,
upper : pw => /[A-Z]/.test(pw),
digit : pw => /\d/.test(pw),
special: pw => /[!@#$%^&*()\-_=+\[\]{};:'",.<>?/\\|`~]/.test(pw),
};
// Strength scoring: each rule = 1 point; bonus point for length >= 16
function scorePassword(pw) {
let score = 0;
if (RULES.len(pw)) score++;
if (RULES.upper(pw)) score++;
if (RULES.digit(pw)) score++;
if (RULES.special(pw)) score++;
if (pw.length >= 16) score++; // bonus for extra length
return score; // 05
}
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 setRule(id, met) {
const el = document.getElementById('rule-' + id);
if (!el) return;
el.className = 'rule ' + (met ? 'met' : 'unmet');
el.querySelector('.ri').innerHTML = met
? '<i class="bi bi-check-circle-fill"></i>'
: '<i class="bi bi-circle"></i>';
}
function onPasswordInput() {
const pw = document.getElementById('pw').value;
const wrap = document.getElementById('strength-wrap');
const rules = document.getElementById('rules-box');
const bar = document.getElementById('strength-bar');
const lbl = document.getElementById('strength-label');
if (pw.length === 0) {
wrap.style.display = 'none';
rules.style.display = 'none';
document.getElementById('pw').classList.remove('valid','invalid');
updateSubmitState();
onConfirmInput();
return;
}
wrap.style.display = 'block';
rules.style.display = 'grid';
// Update rule checklist
setRule('len', RULES.len(pw));
setRule('upper', RULES.upper(pw));
setRule('digit', RULES.digit(pw));
setRule('special', RULES.special(pw));
// Update strength bar
const score = scorePassword(pw);
const lvl = LEVELS[Math.max(0, score - 1)] || LEVELS[0];
bar.style.width = lvl.pct + '%';
bar.style.background = lvl.color;
lbl.style.color = lvl.color;
lbl.innerHTML = `<i class="bi bi-shield${score >= 4 ? '-fill' : score >= 2 ? '-half' : ''}"></i> ${lvl.label}`;
// Border feedback on password field
const allMet = RULES.len(pw) && RULES.upper(pw) && RULES.digit(pw) && RULES.special(pw);
document.getElementById('pw').classList.toggle('valid', allMet);
document.getElementById('pw').classList.toggle('invalid', !allMet);
onConfirmInput();
updateSubmitState();
}
function onConfirmInput() {
const pw = document.getElementById('pw').value;
const pw2 = document.getElementById('pw2').value;
const msg = document.getElementById('match-msg');
const el2 = document.getElementById('pw2');
if (pw2.length === 0) {
msg.textContent = '';
msg.className = 'match-msg';
el2.classList.remove('valid','invalid');
updateSubmitState();
return;
}
if (pw === pw2) {
msg.innerHTML = '<i class="bi bi-check-circle-fill"></i> Passwords match';
msg.className = 'match-msg ok';
el2.classList.add('valid');
el2.classList.remove('invalid');
} else {
msg.innerHTML = '<i class="bi bi-x-circle-fill"></i> Passwords do not match';
msg.className = 'match-msg fail';
el2.classList.add('invalid');
el2.classList.remove('valid');
}
updateSubmitState();
}
const pwStrength = PasswordStrength.init({
password : 'pw',
confirm : 'pw2',
strengthBar : 'strength-bar',
strengthLabel: 'strength-label',
strengthWrap : 'strength-wrap',
rulesWrap : 'rules-box',
rules: { len: 'rule-len', upper: 'rule-upper', digit: 'rule-digit', special: 'rule-special' },
matchMsg : 'match-msg',
toggles: [
{ input: 'pw', button: 'pw-toggle' },
{ input: 'pw2', button: 'pw2-toggle' },
],
onChange: updateSubmitState,
});
function updateSubmitState() {
const pw = document.getElementById('pw').value;
const pw2 = document.getElementById('pw2').value;
const btn = document.getElementById('submit-btn');
const allMet = RULES.len(pw) && RULES.upper(pw) && RULES.digit(pw) && RULES.special(pw);
const match = pw === pw2 && pw2.length > 0;
// Disable only when user has started typing in either field and criteria
// are not met — never disable before they've interacted with the fields.
// Disable only when the user has started typing in either field and
// criteria are not met — never disable before they've interacted.
const hasStarted = pw.length > 0 || pw2.length > 0;
btn.disabled = hasStarted && !(allMet && match);
}
function toggleVis(inputId, btnId) {
const inp = document.getElementById(inputId);
const btn = document.getElementById(btnId);
const isPassword = inp.type === 'password';
inp.type = isPassword ? 'text' : 'password';
btn.querySelector('i').className = isPassword ? 'bi bi-eye-slash' : 'bi bi-eye';
btn.title = isPassword ? 'Hide password' : 'Show password';
btn.disabled = hasStarted && !(PasswordStrength.allRulesMet(pw) && match);
}
</script>
</body>
+79 -32
View File
@@ -6,8 +6,13 @@
<style>:root { --accent: {{ branding.primary_color }}; --accent-h: {{ branding.primary_color }}; }</style>
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:opsz,wght@9..40,400;9..40,500;9..40,600&display=swap" rel="stylesheet"/>
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet"/>
<link rel="stylesheet" href="{{ url_for('static', filename='css/password-strength.css') }}"/>
<style>
:root{--bg:#f0f4f8;--border:#e2e8f0;--accent:#2563eb;--accent-h:#1d4ed8;--text:#0f172a;--text2:#334155;--muted:#64748b;--danger:#dc2626;--danger-bg:#fef2f2;}
:root{
--bg:#f0f4f8;--border:#e2e8f0;--border2:#cbd5e1;
--accent:#2563eb;--accent-h:#1d4ed8;--text:#0f172a;--text2:#334155;--muted:#64748b;
--success:#059669;--danger:#dc2626;--danger-bg:#fef2f2;
}
*{box-sizing:border-box;margin:0;padding:0;}
body{font-family:'DM Sans',sans-serif;background:var(--bg);min-height:100vh;display:flex;align-items:center;justify-content:center;}
.bg-grid{position:fixed;inset:0;background-image:linear-gradient(var(--border) 1px,transparent 1px),linear-gradient(90deg,var(--border) 1px,transparent 1px);background-size:48px 48px;opacity:.6;pointer-events:none;}
@@ -23,13 +28,11 @@
input::placeholder{color:#94a3b8;}
.btn{width:100%;background:var(--accent);border:none;color:#fff;border-radius:9px;padding:12px;font-size:14px;font-weight:600;cursor:pointer;transition:background .15s;font-family:inherit;box-shadow:0 2px 6px rgba(37,99,235,.3);}
.btn:hover{background:var(--accent-h);}
.btn:disabled{background:#93c5fd;cursor:not-allowed;box-shadow:none;}
.links{text-align:center;margin-top:18px;font-size:13px;color:var(--muted);}
.links a{color:var(--accent);}
.alert{border-radius:8px;padding:11px 14px;font-size:13px;margin-bottom:18px;}
.alert-danger{background:var(--danger-bg);color:var(--danger);border:1px solid #fecaca;}
/* strength meter */
#pw-strength-bar{height:4px;border-radius:2px;transition:width .3s,background .3s;background:#e2e8f0;width:0;}
#pw-strength-text{font-size:11px;color:var(--muted);margin-top:4px;}
</style>
</head>
<body>
@@ -53,46 +56,90 @@
{% endfor %}
{% endwith %}
<form method="POST">
<form method="POST" id="reset-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="form-group">
<label>New Password</label>
<input type="password" name="password" id="pw-new" required placeholder="Min. 8 characters" autofocus
oninput="checkStrength(this.value)"/>
<div style="margin-top:6px;background:#e2e8f0;border-radius:2px;height:4px;">
<div id="pw-strength-bar"></div>
<div class="pw-wrap">
<input type="password" name="password" id="pw-new" required maxlength="128"
placeholder="Min. 8 characters" autofocus
autocomplete="new-password"/>
<button type="button" class="pw-toggle" id="pw-new-toggle"
title="Show / hide password" tabindex="-1">
<i class="bi bi-eye"></i>
</button>
</div>
<!-- Strength bar -->
<div class="pw-strength-wrap" id="strength-wrap" style="display:none;">
<div class="pw-strength-track">
<div class="pw-strength-fill" id="strength-bar"></div>
</div>
<div class="pw-strength-label" id="strength-label"></div>
</div>
<!-- Rule checklist — mirrors server-side validate_password() rules -->
<div class="pw-rules" id="rules-box" style="display:none;">
<div class="pw-rule unmet" id="rule-len">
<span class="ri"><i class="bi bi-circle"></i></span>
<span>8+ characters</span>
</div>
<div class="pw-rule unmet" id="rule-upper">
<span class="ri"><i class="bi bi-circle"></i></span>
<span>Uppercase letter</span>
</div>
<div class="pw-rule unmet" id="rule-digit">
<span class="ri"><i class="bi bi-circle"></i></span>
<span>Number</span>
</div>
<div class="pw-rule unmet" id="rule-special">
<span class="ri"><i class="bi bi-circle"></i></span>
<span>Special character</span>
</div>
</div>
<div id="pw-strength-text"></div>
</div>
<div class="form-group">
<label>Confirm Password</label>
<input type="password" name="confirm_password" required placeholder="Repeat password"/>
<div class="pw-wrap">
<input type="password" name="confirm_password" id="pw-confirm" required maxlength="128"
placeholder="Repeat password"
autocomplete="new-password"/>
<button type="button" class="pw-toggle" id="pw-confirm-toggle"
title="Show / hide password" tabindex="-1">
<i class="bi bi-eye"></i>
</button>
</div>
<button type="submit" class="btn"><i class="bi bi-lock me-2"></i>Update Password</button>
<div class="pw-match" id="match-msg"></div>
</div>
<button type="submit" class="btn" id="submit-btn"><i class="bi bi-lock me-2"></i>Update Password</button>
</form>
<div class="links"><a href="{{ url_for('auth.login') }}">← Back to Sign In</a></div>
</div>
<script src="{{ url_for('static', filename='js/password-strength.js') }}"></script>
<script>
function checkStrength(pw) {
let score = 0;
if (pw.length >= 8) score++;
if (/[A-Z]/.test(pw)) score++;
if (/[0-9]/.test(pw)) score++;
if (/[^A-Za-z0-9]/.test(pw)) score++;
const bar = document.getElementById('pw-strength-bar');
const text = document.getElementById('pw-strength-text');
const levels = [
{ w: '0%', bg: '#e2e8f0', label: '' },
{ w: '25%', bg: '#dc2626', label: 'Weak' },
{ w: '50%', bg: '#d97706', label: 'Fair' },
{ w: '75%', bg: '#2563eb', label: 'Good' },
{ w: '100%', bg: '#059669', label: 'Strong' },
];
const lvl = levels[score] || levels[0];
bar.style.width = lvl.w;
bar.style.background = lvl.bg;
text.textContent = lvl.label;
text.style.color = lvl.bg;
const pwStrength = PasswordStrength.init({
password : 'pw-new',
confirm : 'pw-confirm',
strengthBar : 'strength-bar',
strengthLabel: 'strength-label',
strengthWrap : 'strength-wrap',
rulesWrap : 'rules-box',
rules: { len: 'rule-len', upper: 'rule-upper', digit: 'rule-digit', special: 'rule-special' },
matchMsg : 'match-msg',
toggles: [
{ input: 'pw-new', button: 'pw-new-toggle' },
{ input: 'pw-confirm', button: 'pw-confirm-toggle' },
],
onChange: updateSubmitState,
});
function updateSubmitState() {
const pw = document.getElementById('pw-new').value;
const pw2 = document.getElementById('pw-confirm').value;
const btn = document.getElementById('submit-btn');
const match = pw === pw2 && pw2.length > 0;
const hasStarted = pw.length > 0 || pw2.length > 0;
btn.disabled = hasStarted && !(PasswordStrength.allRulesMet(pw) && match);
}
</script>
</body>