From d7632748ab429cdcb977ef2b7f5ec18ae044ee3b Mon Sep 17 00:00:00 2001 From: janis steiner Date: Thu, 7 May 2026 23:51:33 +0200 Subject: [PATCH] adding password strength meter and session ui --- app-code/account/index.php | 139 +++++++++++++++++++++- app-code/api/account/get_activity_log.php | 25 ++++ app-code/api/account/manage_sessions.php | 48 ++++++++ app-code/api/account/update_2fa.php | 2 + app-code/api/account/update_pw.php | 1 + app-code/api/login/redirect.php | 5 + app-code/api/utils/security.php | 13 ++ app-code/install/create_db.php | 24 ++++ 8 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 app-code/api/account/get_activity_log.php create mode 100644 app-code/api/account/manage_sessions.php 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) { + + @@ -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) {
+
+

Recent activity on your account.

+
+ +
+
+

These devices have been "remembered" and can log in without authentication.

+
+ + +
@@ -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.'); + } + }); + } diff --git a/app-code/api/account/get_activity_log.php b/app-code/api/account/get_activity_log.php new file mode 100644 index 0000000..6a30af4 --- /dev/null +++ b/app-code/api/account/get_activity_log.php @@ -0,0 +1,25 @@ + true, 'entries' => $entries]); +?> \ No newline at end of file diff --git a/app-code/api/account/manage_sessions.php b/app-code/api/account/manage_sessions.php new file mode 100644 index 0000000..c7325ed --- /dev/null +++ b/app-code/api/account/manage_sessions.php @@ -0,0 +1,48 @@ + $row['id'], + 'user_agent' => $row['agent'], + 'auth_token' => substr($row['auth_token'], 0, 16) . '...' + ]; + } + mysqli_stmt_close($stmt); + echo json_encode(['success' => true, 'sessions' => $sessions]); + +} elseif ($method === 'POST') { + require_csrf_token(); + $input = json_decode(file_get_contents('php://input'), true); + + $sql = "DELETE FROM keepmeloggedin WHERE user_id = ?"; + $stmt = mysqli_prepare($conn, $sql); + mysqli_stmt_bind_param($stmt, 'i', $user_id); + mysqli_stmt_execute($stmt); + mysqli_stmt_close($stmt); + + delete_cookie("auth_token"); + log_activity($conn, $user_id, 'sessions_revoked', 'All remembered sessions deleted'); + + echo json_encode(['success' => true, 'message' => 'All sessions revoked.']); +} else { + echo json_encode(['success' => false, 'message' => 'Invalid request method.'], 405); +} +?> \ No newline at end of file diff --git a/app-code/api/account/update_2fa.php b/app-code/api/account/update_2fa.php index 2921791..8f74d91 100644 --- a/app-code/api/account/update_2fa.php +++ b/app-code/api/account/update_2fa.php @@ -60,6 +60,7 @@ if($data->enable_2fa==true){ if ($update_stmt->execute()) { unset($_SESSION["pending_2fa_secret"]); clear_rate_limit($conn, 'setup_2fa', (string)$id); + log_activity($conn, $id, '2fa_enabled', ''); echo json_encode(['success' => true, 'message' => '2FA enabled.']); } else { echo json_encode(['success' => false, 'message' => 'Failed to enable 2fa.']); @@ -76,6 +77,7 @@ if($data->enable_2fa==false){ if ($update_stmt = $conn->prepare($sql)) { $update_stmt->bind_param("i",$id); if ($update_stmt->execute()) { + log_activity($conn, $id, '2fa_disabled', ''); echo json_encode(['success' => true, 'message' => '2FA disabled.']); } else { echo json_encode(['success' => false, 'message' => 'Failed to disable 2fa.']); diff --git a/app-code/api/account/update_pw.php b/app-code/api/account/update_pw.php index 1c49c4d..b56b5f3 100644 --- a/app-code/api/account/update_pw.php +++ b/app-code/api/account/update_pw.php @@ -69,6 +69,7 @@ if (isset($data->old_password) && isset($data->new_password)) { if ($update_stmt = $conn->prepare($update_sql)) { $update_stmt->bind_param("ssi", $hashed_password, $new_pepper, $user_id); if ($update_stmt->execute()) { + log_activity($conn, $user_id, 'password_change', ''); echo json_encode(['success' => true, 'message' => 'Password updated successfully.']); } else { echo json_encode(['success' => false, 'message' => 'Failed to update password.']); diff --git a/app-code/api/login/redirect.php b/app-code/api/login/redirect.php index 992cf8c..1f41138 100644 --- a/app-code/api/login/redirect.php +++ b/app-code/api/login/redirect.php @@ -135,6 +135,11 @@ else if ($_SESSION["needs_auth"]===false && $_SESSION["mfa_authenticated"]==1 && curl_close($ch); } + + //log activity + if($_SESSION["logged_in"]!==true){ + log_activity($conn, $user_id, 'login', 'Login to ' . ($send_to ?: '/account/')); + } $_SESSION["logged_in"]=true; echo(json_encode($data)); diff --git a/app-code/api/utils/security.php b/app-code/api/utils/security.php index cc365a2..f6d04ba 100644 --- a/app-code/api/utils/security.php +++ b/app-code/api/utils/security.php @@ -295,6 +295,19 @@ function append_auth_token_to_redirect(string $redirect, string $auth_token): st return $redirect . $separator . 'auth=' . rawurlencode($auth_token); } +function log_activity(mysqli $conn, int $user_id, string $action, string $details = ''): void +{ + $forwarded_for = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? ''; + $ip = trim(explode(',', $forwarded_for)[0]); + $user_agent = $_SERVER['HTTP_USER_AGENT'] ?? ''; + + $sql = "INSERT INTO activity_log (user_id, action, ip, user_agent, details) VALUES (?, ?, ?, ?, ?)"; + $stmt = mysqli_prepare($conn, $sql); + mysqli_stmt_bind_param($stmt, 'issss', $user_id, $action, $ip, $user_agent, $details); + mysqli_stmt_execute($stmt); + mysqli_stmt_close($stmt); +} + function is_external_domain(string $url): ?string { if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) { diff --git a/app-code/install/create_db.php b/app-code/install/create_db.php index 23a6247..ce71f7c 100644 --- a/app-code/install/create_db.php +++ b/app-code/install/create_db.php @@ -176,6 +176,30 @@ + $sql="CREATE TABLE IF NOT EXISTS activity_log ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + action VARCHAR(64) NOT NULL, + ip VARCHAR(45), + user_agent TEXT, + details TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_user_id (user_id), + INDEX idx_created_at (created_at) + );"; + + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + if($success!==1){ echo '