307 lines
11 KiB
HTML
307 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Change Password</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
|
<style>
|
|
* { border-radius: 0 !important; }
|
|
body { font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', monospace; background: #fff; color: #000; padding: 20px; }
|
|
.box { border: 3px solid #000; background: #fff; }
|
|
|
|
/* Chunky Buttons */
|
|
button, .button {
|
|
border: 2px solid #000;
|
|
background: #fff;
|
|
padding: 4px 10px;
|
|
cursor: pointer;
|
|
font-size: 11px;
|
|
font-weight: 900;
|
|
text-decoration: none;
|
|
text-transform: uppercase;
|
|
box-shadow: 3px 3px 0px #000;
|
|
}
|
|
button:hover, .button:hover { background: #000; color: #fff; box-shadow: none; transform: translate(2px, 2px); }
|
|
button:active { background: #ff0000; color: #fff; }
|
|
|
|
.nav-link { font-weight: 900; text-decoration: underline; text-transform: uppercase; font-size: 12px; }
|
|
.nav-link:hover { background: #000; color: #fff; }
|
|
|
|
/* Inputs */
|
|
.field-group { margin-bottom: 20px; }
|
|
label {
|
|
display: block;
|
|
font-size: 11px;
|
|
font-weight: 900;
|
|
text-transform: uppercase;
|
|
margin-bottom: 4px;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
input[type="password"] {
|
|
width: 100%;
|
|
border: 3px solid #000;
|
|
padding: 10px 12px;
|
|
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', monospace;
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
background: #fff;
|
|
outline: none;
|
|
box-sizing: border-box;
|
|
transition: background 0.1s;
|
|
}
|
|
input[type="password"]:focus {
|
|
background: #ffff00;
|
|
border-color: #000;
|
|
}
|
|
|
|
/* Strength Meter */
|
|
.strength-bar-wrap {
|
|
height: 8px;
|
|
border: 2px solid #000;
|
|
margin-top: 6px;
|
|
background: #eee;
|
|
overflow: hidden;
|
|
}
|
|
.strength-bar {
|
|
height: 100%;
|
|
width: 0%;
|
|
transition: width 0.2s, background 0.2s;
|
|
}
|
|
.strength-label {
|
|
font-size: 10px;
|
|
font-weight: 900;
|
|
text-transform: uppercase;
|
|
margin-top: 3px;
|
|
}
|
|
|
|
/* Error / Success */
|
|
.msg {
|
|
border: 3px solid #000;
|
|
padding: 10px 14px;
|
|
font-size: 12px;
|
|
font-weight: 900;
|
|
text-transform: uppercase;
|
|
display: none;
|
|
margin-bottom: 20px;
|
|
}
|
|
.msg-error { background: #ff0000; color: #fff; }
|
|
.msg-success { background: #00ff00; color: #000; }
|
|
|
|
/* Match indicator */
|
|
.match-hint {
|
|
font-size: 10px;
|
|
font-weight: 900;
|
|
text-transform: uppercase;
|
|
margin-top: 4px;
|
|
min-height: 14px;
|
|
}
|
|
.match-ok { color: #007700; }
|
|
.match-bad { color: #ff0000; }
|
|
|
|
/* Submit button — big */
|
|
#submit-btn {
|
|
width: 100%;
|
|
padding: 14px;
|
|
font-size: 16px;
|
|
border: 4px solid #000;
|
|
box-shadow: 6px 6px 0px #000;
|
|
}
|
|
#submit-btn:hover { box-shadow: none; transform: translate(4px, 4px); }
|
|
|
|
/* Requirements list */
|
|
.req-list { list-style: none; padding: 0; margin: 0; }
|
|
.req-list li {
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
padding: 2px 0;
|
|
display: flex;
|
|
gap: 6px;
|
|
align-items: center;
|
|
}
|
|
.req-list li span.icon { font-style: normal; font-size: 12px; }
|
|
.req-met { color: #007700; }
|
|
.req-unmet { color: #aaa; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="max-w-lg mx-auto">
|
|
<header class="mb-6 border-b-8 border-black pb-4 flex justify-between items-start">
|
|
<div>
|
|
<h1 class="text-4xl font-black uppercase tracking-tighter leading-none">Change_Password</h1>
|
|
</div>
|
|
<div class="flex flex-col items-end gap-2">
|
|
<a href="/admin" class="nav-link">← BACK_TO_ADMIN</a>
|
|
<a href="/logout" class="nav-link text-red-600">LOGOUT_SESSION</a>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="box p-6">
|
|
|
|
<div id="msg-error" class="msg msg-error">⚠ ERROR: Passwords do not match.</div>
|
|
<div id="msg-success" class="msg msg-success">✓ PASSWORD UPDATED. Credential change committed.</div>
|
|
|
|
<div class="field-group">
|
|
<label for="current-pw">Current_Password</label>
|
|
<input type="password" id="current-pw" placeholder="••••••••••••" autocomplete="current-password">
|
|
</div>
|
|
|
|
<div class="field-group">
|
|
<label for="new-pw">New_Password</label>
|
|
<input type="password" id="new-pw" placeholder="••••••••••••" autocomplete="new-password" oninput="onNewPwInput()">
|
|
<div class="strength-bar-wrap"><div class="strength-bar" id="strength-bar"></div></div>
|
|
<div class="strength-label" id="strength-label" style="color:#aaa">—</div>
|
|
</div>
|
|
|
|
<div class="box p-3 mb-6 bg-gray-50">
|
|
<div class="text-[10px] font-black uppercase mb-2 tracking-widest">Requirements</div>
|
|
<ul class="req-list" id="req-list">
|
|
<li id="req-len" class="req-unmet"><span class="icon">□</span> Min 8 characters</li>
|
|
<li id="req-upper" class="req-unmet"><span class="icon">□</span> Uppercase letter</li>
|
|
<li id="req-num" class="req-unmet"><span class="icon">□</span> Number</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="field-group">
|
|
<label for="confirm-pw">Confirm_New_Password</label>
|
|
<input type="password" id="confirm-pw" placeholder="••••••••••••" autocomplete="new-password" oninput="onConfirmInput()">
|
|
<div class="match-hint" id="match-hint"></div>
|
|
</div>
|
|
|
|
<button id="submit-btn" onclick="handleSubmit()">
|
|
CHANGE PASSWORD
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
const requirements = [
|
|
{ id: 'req-len', test: v => v.length >= 8 },
|
|
{ id: 'req-upper', test: v => /[A-Z]/.test(v) },
|
|
{ id: 'req-num', test: v => /[0-9]/.test(v) },
|
|
];
|
|
|
|
function getStrength(v) {
|
|
const score = requirements.filter(r => r.test(v)).length * 2;
|
|
if (v.length === 0) return { score: 0, label: '—', color: '#eee', pct: 0 };
|
|
if (score <= 1) return { score, label: 'WEAK', color: '#ff0000', pct: 20 };
|
|
if (score === 2) return { score, label: 'POOR', color: '#ff6600', pct: 40 };
|
|
if (score === 3) return { score, label: 'FAIR', color: '#ffcc00', pct: 60 };
|
|
if (score === 4) return { score, label: 'STRONG', color: '#88cc00', pct: 80 };
|
|
return { score, label: 'MAXIMUM', color: '#00cc44', pct: 100 };
|
|
}
|
|
|
|
function onNewPwInput() {
|
|
const v = document.getElementById('new-pw').value;
|
|
const { label, color, pct } = getStrength(v);
|
|
|
|
const bar = document.getElementById('strength-bar');
|
|
bar.style.width = pct + '%';
|
|
bar.style.background = color;
|
|
|
|
const lbl = document.getElementById('strength-label');
|
|
lbl.innerText = label;
|
|
lbl.style.color = pct === 0 ? '#aaa' : color;
|
|
|
|
requirements.forEach(r => {
|
|
const el = document.getElementById(r.id);
|
|
const met = r.test(v);
|
|
el.className = met ? 'req-met' : 'req-unmet';
|
|
el.querySelector('.icon').innerText = met ? '■' : '□';
|
|
});
|
|
|
|
onConfirmInput();
|
|
}
|
|
|
|
function onConfirmInput() {
|
|
const nv = document.getElementById('new-pw').value;
|
|
const cv = document.getElementById('confirm-pw').value;
|
|
const hint = document.getElementById('match-hint');
|
|
if (!cv) { hint.innerText = ''; return; }
|
|
if (nv === cv) {
|
|
hint.innerText = '✓ PASSWORDS MATCH';
|
|
hint.className = 'match-hint match-ok';
|
|
} else {
|
|
hint.innerText = '✗ MISMATCH';
|
|
hint.className = 'match-hint match-bad';
|
|
}
|
|
}
|
|
|
|
function hideMessages() {
|
|
document.getElementById('msg-error').style.display = 'none';
|
|
document.getElementById('msg-success').style.display = 'none';
|
|
}
|
|
|
|
async function handleSubmit() {
|
|
hideMessages();
|
|
|
|
const current = document.getElementById('current-pw').value;
|
|
const nv = document.getElementById('new-pw').value;
|
|
const cv = document.getElementById('confirm-pw').value;
|
|
|
|
if (!current || !nv || !cv) {
|
|
showError('All fields are required.');
|
|
return;
|
|
}
|
|
if (nv !== cv) {
|
|
showError('Passwords do not match.');
|
|
return;
|
|
}
|
|
|
|
const { score } = getStrength(nv);
|
|
if (score < 3) {
|
|
showError('Password is too weak.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch('/api/user/change-password', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
old_password: current,
|
|
new_password: nv
|
|
})
|
|
});
|
|
|
|
const data = await res.json().catch(() => ({}));
|
|
|
|
if (!res.ok) {
|
|
showError(data.error || 'Something went wrong.');
|
|
return;
|
|
}
|
|
|
|
showSuccess('Password updated successfully.');
|
|
|
|
document.getElementById('current-pw').value = '';
|
|
document.getElementById('new-pw').value = '';
|
|
document.getElementById('confirm-pw').value = '';
|
|
onNewPwInput();
|
|
|
|
setTimeout(() => {
|
|
window.location.href = '/login';
|
|
}, 3000);
|
|
|
|
} catch (err) {
|
|
showError('Network error. Try again.');
|
|
}
|
|
}
|
|
|
|
function showSuccess(msg) {
|
|
const el = document.getElementById('msg-success');
|
|
el.innerText = '✓ ' + msg;
|
|
el.style.display = 'block';
|
|
}
|
|
|
|
function showError(msg) {
|
|
const el = document.getElementById('msg-error');
|
|
el.innerText = '⚠ ERROR: ' + msg;
|
|
el.style.display = 'block';
|
|
}
|
|
</script>
|
|
|
|
</body>
|
|
</html> |