Jul 2nd - Add password strength, suggestion when create/edit user
This commit is contained in:
@@ -2,6 +2,29 @@
|
|||||||
{% block title %}Create User{% endblock %}
|
{% block title %}Create User{% endblock %}
|
||||||
{% block page_title %}User Management{% endblock %}
|
{% 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>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-lg-7">
|
<div class="col-lg-7">
|
||||||
@@ -127,6 +150,26 @@
|
|||||||
<div id="pw-strength-bar" style="height:100%;border-radius:2px;width:0%;transition:width .3s,background .3s;"></div>
|
<div id="pw-strength-bar" style="height:100%;border-radius:2px;width:0%;transition:width .3s,background .3s;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="pw-strength-label" style="font-size:11px;color:var(--muted);margin-top:4px;"></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;">
|
||||||
|
<div class="pw-rule unmet" id="pw-rule-len">
|
||||||
|
<span class="ri"><i class="bi bi-circle"></i></span>
|
||||||
|
<span>8+ characters</span>
|
||||||
|
</div>
|
||||||
|
<div class="pw-rule unmet" id="pw-rule-upper">
|
||||||
|
<span class="ri"><i class="bi bi-circle"></i></span>
|
||||||
|
<span>Uppercase letter</span>
|
||||||
|
</div>
|
||||||
|
<div class="pw-rule unmet" id="pw-rule-digit">
|
||||||
|
<span class="ri"><i class="bi bi-circle"></i></span>
|
||||||
|
<span>Number</span>
|
||||||
|
</div>
|
||||||
|
<div class="pw-rule unmet" id="pw-rule-special">
|
||||||
|
<span class="ri"><i class="bi bi-circle"></i></span>
|
||||||
|
<span>Special character</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -160,18 +203,52 @@ function togglePw(fieldId, iconId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Password strength indicator ───────────────────────────────────────────────
|
// ── 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 () {
|
document.getElementById('pw-field').addEventListener('input', function () {
|
||||||
const val = this.value;
|
const val = this.value;
|
||||||
let score = 0;
|
|
||||||
if (val.length >= 8) score++;
|
|
||||||
if (val.length >= 12) score++;
|
|
||||||
if (/[A-Z]/.test(val) && /[a-z]/.test(val))score++;
|
|
||||||
if (/[0-9]/.test(val)) score++;
|
|
||||||
if (/[^A-Za-z0-9]/.test(val)) score++;
|
|
||||||
|
|
||||||
const bar = document.getElementById('pw-strength-bar');
|
const bar = document.getElementById('pw-strength-bar');
|
||||||
const label = document.getElementById('pw-strength-label');
|
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 = [
|
const levels = [
|
||||||
{ pct: '20%', bg: '#dc2626', text: 'Very weak' },
|
{ pct: '20%', bg: '#dc2626', text: 'Very weak' },
|
||||||
{ pct: '40%', bg: '#d97706', text: 'Weak' },
|
{ pct: '40%', bg: '#d97706', text: 'Weak' },
|
||||||
@@ -179,11 +256,6 @@ document.getElementById('pw-field').addEventListener('input', function () {
|
|||||||
{ pct: '80%', bg: '#16a34a', text: 'Strong' },
|
{ pct: '80%', bg: '#16a34a', text: 'Strong' },
|
||||||
{ pct: '100%', bg: '#059669', text: 'Very strong' },
|
{ pct: '100%', bg: '#059669', text: 'Very strong' },
|
||||||
];
|
];
|
||||||
if (val.length === 0) {
|
|
||||||
bar.style.width = '0%';
|
|
||||||
label.textContent = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const lvl = levels[Math.min(score - 1, 4)] || levels[0];
|
const lvl = levels[Math.min(score - 1, 4)] || levels[0];
|
||||||
bar.style.width = lvl.pct;
|
bar.style.width = lvl.pct;
|
||||||
bar.style.background = lvl.bg;
|
bar.style.background = lvl.bg;
|
||||||
|
|||||||
@@ -2,6 +2,29 @@
|
|||||||
{% block title %}Edit User{% endblock %}
|
{% block title %}Edit User{% endblock %}
|
||||||
{% block page_title %}Edit User{% endblock %}
|
{% 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>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
@@ -50,6 +73,33 @@
|
|||||||
<label class="form-label">Confirm Password</label>
|
<label class="form-label">Confirm Password</label>
|
||||||
<input type="password" class="form-control" id="confirm-password" name="confirm_password" placeholder="Repeat new password"/>
|
<input type="password" class="form-control" id="confirm-password" name="confirm_password" placeholder="Repeat new password"/>
|
||||||
</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>
|
||||||
|
</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;">
|
||||||
|
<div class="pw-rule unmet" id="pw-rule-len">
|
||||||
|
<span class="ri"><i class="bi bi-circle"></i></span>
|
||||||
|
<span>8+ characters</span>
|
||||||
|
</div>
|
||||||
|
<div class="pw-rule unmet" id="pw-rule-upper">
|
||||||
|
<span class="ri"><i class="bi bi-circle"></i></span>
|
||||||
|
<span>Uppercase letter</span>
|
||||||
|
</div>
|
||||||
|
<div class="pw-rule unmet" id="pw-rule-digit">
|
||||||
|
<span class="ri"><i class="bi bi-circle"></i></span>
|
||||||
|
<span>Number</span>
|
||||||
|
</div>
|
||||||
|
<div class="pw-rule unmet" id="pw-rule-special">
|
||||||
|
<span class="ri"><i class="bi bi-circle"></i></span>
|
||||||
|
<span>Special character</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2 mt-4">
|
<div class="d-flex gap-2 mt-4">
|
||||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check2 me-2"></i>Save</button>
|
<button type="submit" class="btn btn-primary"><i class="bi bi-check2 me-2"></i>Save</button>
|
||||||
@@ -64,6 +114,67 @@
|
|||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<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;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Client-side password match check before submit ────────────────────────────
|
||||||
document.querySelector('form').addEventListener('submit', function (e) {
|
document.querySelector('form').addEventListener('submit', function (e) {
|
||||||
const pw = document.getElementById('new-password').value;
|
const pw = document.getElementById('new-password').value;
|
||||||
const pw2 = document.getElementById('confirm-password').value;
|
const pw2 = document.getElementById('confirm-password').value;
|
||||||
|
|||||||
Reference in New Issue
Block a user