This commit is contained in:
+138
-1
@@ -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 "remembered" 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 + ' · ' : '') + (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>
|
||||
|
||||
@@ -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]);
|
||||
?>
|
||||
@@ -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);
|
||||
}
|
||||
?>
|
||||
@@ -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.']);
|
||||
|
||||
@@ -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.']);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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://')) {
|
||||
|
||||
@@ -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){
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user