diff --git a/app-code/account/index.php b/app-code/account/index.php
index be9b820..a4d55c1 100644
--- a/app-code/account/index.php
+++ b/app-code/account/index.php
@@ -78,6 +78,12 @@ if (!isset($_SESSION["logged_in"]) || $_SESSION["logged_in"] !== true) {
language
+
+ history
+
+
+ devices
+
@@ -143,7 +149,11 @@ if (!isset($_SESSION["logged_in"]) || $_SESSION["logged_in"] !== true) {
@@ -181,6 +191,17 @@ if (!isset($_SESSION["logged_in"]) || $_SESSION["logged_in"] !== true) {
No external domains approved yet.
+
+
Recent activity on your account.
+
+
No activity recorded yet.
+
+
+
These devices have been "remembered" and can log in without authentication.
+
+
No remembered sessions.
+
+
@@ -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 = '' + label + '' + e.created_at + '
' +
+ '' + (e.ip ? e.ip + ' · ' : '') + (e.user_agent ? e.user_agent.substring(0, 60) + '...' : '') + '' +
+ (e.details ? '
' + e.details + '' : '');
+ 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 = '' + (s.user_agent || 'Unknown device') + '';
+ 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.');
+ }
+ });
+ }