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"> <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> <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>
<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 <?php
if($_SESSION["permissions"][0]==="1"){ if($_SESSION["permissions"][0]==="1"){
echo('<li class="nav-item" role="presentation"> echo('<li class="nav-item" role="presentation">
@@ -143,7 +149,11 @@ if (!isset($_SESSION["logged_in"]) || $_SESSION["logged_in"] !== true) {
<!-- New Password --> <!-- New Password -->
<div class="mb-3"> <div class="mb-3">
<label for="new-password" class="form-label">New Password</label> <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> </div>
<!-- Confirm New Password --> <!-- Confirm New Password -->
@@ -181,6 +191,17 @@ if (!isset($_SESSION["logged_in"]) || $_SESSION["logged_in"] !== true) {
<div id="confirmedDomainsList" class="list-group"></div> <div id="confirmedDomainsList" class="list-group"></div>
<p id="noDomainsMessage" class="text-muted mt-3" style="display:none;">No external domains approved yet.</p> <p id="noDomainsMessage" class="text-muted mt-3" style="display:none;">No external domains approved yet.</p>
</div> </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> </div>
</div> </div>
@@ -550,6 +571,45 @@ function generate2FAQRCode(issuer, accountName, secret) {
height: 300 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 //webauthn js
async function createRegistration() { async function createRegistration() {
@@ -788,7 +848,84 @@ function generate2FAQRCode(issuer, accountName, secret) {
if (domainsTab) { if (domainsTab) {
domainsTab.addEventListener('shown.bs.tab', loadConfirmedDomains); 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> </script>
</body> </body>
+25
View File
@@ -0,0 +1,25 @@
<?php
include "../utils/security.php";
secure_session_start();
header('Content-Type: application/json');
require_logged_in();
include "../../config/config.php";
$conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD, $DB_DATABASE);
$user_id = $_SESSION['id'];
$sql = "SELECT id, action, ip, user_agent, details, created_at FROM activity_log WHERE user_id = ? ORDER BY created_at DESC LIMIT 50";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'i', $user_id);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
$entries = [];
while ($row = mysqli_fetch_assoc($result)) {
$entries[] = $row;
}
mysqli_stmt_close($stmt);
echo json_encode(['success' => true, 'entries' => $entries]);
?>
+48
View File
@@ -0,0 +1,48 @@
<?php
include "../utils/security.php";
secure_session_start();
header('Content-Type: application/json');
require_logged_in();
include "../../config/config.php";
$conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD, $DB_DATABASE);
$user_id = $_SESSION['id'];
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
$sql = "SELECT id, agent, auth_token FROM keepmeloggedin WHERE user_id = ? ORDER BY id DESC";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'i', $user_id);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
$sessions = [];
while ($row = mysqli_fetch_assoc($result)) {
$sessions[] = [
'id' => $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);
}
?>
+2
View File
@@ -60,6 +60,7 @@ if($data->enable_2fa==true){
if ($update_stmt->execute()) { if ($update_stmt->execute()) {
unset($_SESSION["pending_2fa_secret"]); unset($_SESSION["pending_2fa_secret"]);
clear_rate_limit($conn, 'setup_2fa', (string)$id); clear_rate_limit($conn, 'setup_2fa', (string)$id);
log_activity($conn, $id, '2fa_enabled', '');
echo json_encode(['success' => true, 'message' => '2FA enabled.']); echo json_encode(['success' => true, 'message' => '2FA enabled.']);
} else { } else {
echo json_encode(['success' => false, 'message' => 'Failed to enable 2fa.']); 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)) { if ($update_stmt = $conn->prepare($sql)) {
$update_stmt->bind_param("i",$id); $update_stmt->bind_param("i",$id);
if ($update_stmt->execute()) { if ($update_stmt->execute()) {
log_activity($conn, $id, '2fa_disabled', '');
echo json_encode(['success' => true, 'message' => '2FA disabled.']); echo json_encode(['success' => true, 'message' => '2FA disabled.']);
} else { } else {
echo json_encode(['success' => false, 'message' => 'Failed to disable 2fa.']); echo json_encode(['success' => false, 'message' => 'Failed to disable 2fa.']);
+1
View File
@@ -69,6 +69,7 @@ if (isset($data->old_password) && isset($data->new_password)) {
if ($update_stmt = $conn->prepare($update_sql)) { if ($update_stmt = $conn->prepare($update_sql)) {
$update_stmt->bind_param("ssi", $hashed_password, $new_pepper, $user_id); $update_stmt->bind_param("ssi", $hashed_password, $new_pepper, $user_id);
if ($update_stmt->execute()) { if ($update_stmt->execute()) {
log_activity($conn, $user_id, 'password_change', '');
echo json_encode(['success' => true, 'message' => 'Password updated successfully.']); echo json_encode(['success' => true, 'message' => 'Password updated successfully.']);
} else { } else {
echo json_encode(['success' => false, 'message' => 'Failed to update password.']); echo json_encode(['success' => false, 'message' => 'Failed to update password.']);
+5
View File
@@ -135,6 +135,11 @@ else if ($_SESSION["needs_auth"]===false && $_SESSION["mfa_authenticated"]==1 &&
curl_close($ch); 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; $_SESSION["logged_in"]=true;
echo(json_encode($data)); echo(json_encode($data));
+13
View File
@@ -295,6 +295,19 @@ function append_auth_token_to_redirect(string $redirect, string $auth_token): st
return $redirect . $separator . 'auth=' . rawurlencode($auth_token); 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 function is_external_domain(string $url): ?string
{ {
if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) { if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) {
+24
View File
@@ -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 '<br><div class="alert alert-success" role="alert">
Table activity_log created successfully!
</div>';
} else {
$success=0;
echo '<br><div class="alert alert-danger" role="alert">
Error creating activity_log: ' . $conn->error .'
</div>';
}
if($success!==1){ if($success!==1){
echo '<br><div class="alert alert-danger" role="alert"> echo '<br><div class="alert alert-danger" role="alert">
There was an error creating the databases. Please try again or contact support at: <a href="mailto:info.jakach@gmail.com">info.jakach@gmail.com</a> There was an error creating the databases. Please try again or contact support at: <a href="mailto:info.jakach@gmail.com">info.jakach@gmail.com</a>