adding password strength meter and session ui
Deploy / deploy (push) Successful in 34s

This commit is contained in:
2026-05-07 23:51:33 +02:00
parent 69a6da90c5
commit d7632748ab
8 changed files with 256 additions and 1 deletions
+138 -1
View File
@@ -78,6 +78,12 @@ if (!isset($_SESSION["logged_in"]) || $_SESSION["logged_in"] !== true) {
<li class="nav-item" role="presentation">
<a class="nav-link" id="domains-tab" data-bs-toggle="tab" href="#domains" role="tab" aria-controls="domains" aria-selected="false"><span class="material-icons">language</span></a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="activity-tab" data-bs-toggle="tab" href="#activity" role="tab" aria-controls="activity" aria-selected="false"><span class="material-icons">history</span></a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="sessions-tab" data-bs-toggle="tab" href="#sessions" role="tab" aria-controls="sessions" aria-selected="false"><span class="material-icons">devices</span></a>
</li>
<?php
if($_SESSION["permissions"][0]==="1"){
echo('<li class="nav-item" role="presentation">
@@ -143,7 +149,11 @@ if (!isset($_SESSION["logged_in"]) || $_SESSION["logged_in"] !== true) {
<!-- New Password -->
<div class="mb-3">
<label for="new-password" class="form-label">New Password</label>
<input type="password" class="form-control" id="new-password" placeholder="Enter new password" required>
<input type="password" class="form-control" id="new-password" placeholder="Enter new password" required oninput="updatePasswordStrength()">
<div id="passwordStrengthBar" class="progress mt-2" style="height: 6px; display:none;">
<div id="passwordStrengthFill" class="progress-bar" role="progressbar" style="width: 0%"></div>
</div>
<small id="passwordStrengthText" class="form-text mt-1"></small>
</div>
<!-- Confirm New Password -->
@@ -181,6 +191,17 @@ if (!isset($_SESSION["logged_in"]) || $_SESSION["logged_in"] !== true) {
<div id="confirmedDomainsList" class="list-group"></div>
<p id="noDomainsMessage" class="text-muted mt-3" style="display:none;">No external domains approved yet.</p>
</div>
<div class="tab-pane fade" id="activity" role="tabpanel" aria-labelledby="activity-tab">
<p>Recent activity on your account.</p>
<div id="activityLogList" class="list-group" style="max-height: 400px; overflow-y: auto;"></div>
<p id="noActivityMessage" class="text-muted mt-3" style="display:none;">No activity recorded yet.</p>
</div>
<div class="tab-pane fade" id="sessions" role="tabpanel" aria-labelledby="sessions-tab">
<p>These devices have been &quot;remembered&quot; and can log in without authentication.</p>
<div id="sessionsList" class="list-group"></div>
<p id="noSessionsMessage" class="text-muted mt-3" style="display:none;">No remembered sessions.</p>
<button class="btn btn-danger mt-3" onclick="revokeAllSessions()">Revoke all sessions</button>
</div>
</div>
</div>
</div>
@@ -550,6 +571,45 @@ function generate2FAQRCode(issuer, accountName, secret) {
height: 300
});
}
function updatePasswordStrength() {
const pw = document.getElementById('new-password').value;
const bar = document.getElementById('passwordStrengthBar');
const fill = document.getElementById('passwordStrengthFill');
const text = document.getElementById('passwordStrengthText');
if (pw.length === 0) {
bar.style.display = 'none';
text.textContent = '';
return;
}
bar.style.display = 'block';
let score = 0;
if (pw.length >= 8) score++;
if (pw.length >= 12) score++;
if (pw.length >= 16) score++;
if (/[a-z]/.test(pw) && /[A-Z]/.test(pw)) score++;
if (/\d/.test(pw)) score++;
if (/[^a-zA-Z0-9]/.test(pw)) score++;
const levels = [
{ min: 0, label: 'Very weak', class: 'bg-danger', pct: 10 },
{ min: 1, label: 'Weak', class: 'bg-danger', pct: 25 },
{ min: 2, label: 'Fair', class: 'bg-warning', pct: 45 },
{ min: 3, label: 'Good', class: 'bg-info', pct: 65 },
{ min: 4, label: 'Strong', class: 'bg-primary', pct: 80 },
{ min: 5, label: 'Very strong', class: 'bg-success', pct: 100 },
];
let level = levels[0];
for (const l of levels) {
if (score >= l.min) level = l;
}
fill.className = 'progress-bar ' + level.class;
fill.style.width = level.pct + '%';
text.textContent = level.label + ' (' + pw.length + ' characters)';
text.className = 'form-text mt-1 text-' + (score >= 3 ? 'success' : score >= 2 ? 'warning' : 'danger');
}
//webauthn js
async function createRegistration() {
@@ -788,7 +848,84 @@ function generate2FAQRCode(issuer, accountName, secret) {
if (domainsTab) {
domainsTab.addEventListener('shown.bs.tab', loadConfirmedDomains);
}
const activityTab = document.getElementById('activity-tab');
if (activityTab) {
activityTab.addEventListener('shown.bs.tab', loadActivityLog);
}
const sessionsTab = document.getElementById('sessions-tab');
if (sessionsTab) {
sessionsTab.addEventListener('shown.bs.tab', loadSessions);
}
});
function loadActivityLog() {
fetch('/api/account/get_activity_log.php')
.then(r => r.json())
.then(data => {
const list = document.getElementById('activityLogList');
const noMsg = document.getElementById('noActivityMessage');
list.innerHTML = '';
if (!data.entries || data.entries.length === 0) {
noMsg.style.display = 'block';
return;
}
noMsg.style.display = 'none';
data.entries.forEach(e => {
const item = document.createElement('div');
item.className = 'list-group-item';
const actionLabels = {
'login': 'Login',
'password_change': 'Password changed',
'2fa_enabled': '2FA enabled',
'2fa_disabled': '2FA disabled',
'sessions_revoked': 'Sessions revoked',
};
const label = actionLabels[e.action] || e.action;
item.innerHTML = '<div class="d-flex w-100 justify-content-between"><strong>' + label + '</strong><small class="text-muted">' + e.created_at + '</small></div>' +
'<small class="text-muted">' + (e.ip ? e.ip + ' &middot; ' : '') + (e.user_agent ? e.user_agent.substring(0, 60) + '...' : '') + '</small>' +
(e.details ? '<br><small>' + e.details + '</small>' : '');
list.appendChild(item);
});
});
}
function loadSessions() {
fetch('/api/account/manage_sessions.php')
.then(r => r.json())
.then(data => {
const list = document.getElementById('sessionsList');
const noMsg = document.getElementById('noSessionsMessage');
list.innerHTML = '';
if (!data.sessions || data.sessions.length === 0) {
noMsg.style.display = 'block';
return;
}
noMsg.style.display = 'none';
data.sessions.forEach(s => {
const item = document.createElement('div');
item.className = 'list-group-item d-flex justify-content-between align-items-center';
item.innerHTML = '<span><strong>' + (s.user_agent || 'Unknown device') + '</strong></span>';
list.appendChild(item);
});
});
}
function revokeAllSessions() {
fetch('/api/account/manage_sessions.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': window.csrfToken
}
}).then(r => r.json()).then(data => {
if (data.success) {
loadSessions();
showSuccessModal(data.message || 'All sessions revoked.');
} else {
showErrorModal(data.message || 'Failed to revoke sessions.');
}
});
}
</script>
</body>