| Server IP : 213.255.246.8 / Your IP : 216.73.217.2 Web Server : Apache System : Linux dublin.stapolin.com 5.14.0-362.18.1.el9_3.x86_64 #1 SMP PREEMPT_DYNAMIC Mon Jan 29 07:05:48 EST 2024 x86_64 User : stapolin ( 1019) PHP Version : 8.4.24 Disable Function : exec,passthru,shell_exec,system MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /home/stapolin/public_html/dash.stapolin.com/ |
Upload File : |
<?php
/**
* Failed Logins Page
* Lists failed_login records from st_activity (Users); option to add selected IPs to st_blocked_ip, filter by website, delete entries.
*/
require_once __DIR__ . '/includes/functions.php';
require_once __DIR__ . '/includes/auth.php';
requireLogin();
$blockMessage = '';
$blockError = '';
$filterWebsiteId = isset($_GET['website_id']) ? (int) $_GET['website_id'] : null;
if ($filterWebsiteId === 0) {
$filterWebsiteId = null;
}
// Handle delete action first so we can redirect before any output (headers not yet sent)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_entries_submit'])) {
if (verifyCsrfToken($_POST['csrf_token'] ?? '')) {
$toBlock = isset($_POST['to_block']) && is_array($_POST['to_block']) ? $_POST['to_block'] : [];
$db = getDb();
$deleted = 0;
foreach ($toBlock as $value) {
$data = json_decode($value, true);
if (!is_array($data) || !isset($data['website_id'], $data['histid'])) {
continue;
}
$wid = (int) $data['website_id'];
$hid = (int) $data['histid'];
if ($wid > 0 && $hid > 0) {
$deleted += $db->execute('DELETE FROM st_activity WHERE object_type = ? AND website_id = ? AND histid = ? AND LOWER(action) = ?', ['Users', $wid, $hid, 'failed_login']);
}
}
if ($deleted > 0) {
$redirectUrl = 'failed-logins.php?deleted=' . $deleted;
if (!empty($_POST['filter_website_id'])) {
$redirectUrl .= '&website_id=' . (int) $_POST['filter_website_id'];
}
header('Location: ' . $redirectUrl);
exit;
}
} else {
$blockError = 'Invalid request. Please try again.';
}
}
// Handle add-to-blocked action (POST with CSRF)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_to_blocked'])) {
if (!verifyCsrfToken($_POST['csrf_token'] ?? '')) {
$blockError = 'Invalid request. Please try again.';
} else {
$toBlock = isset($_POST['to_block']) && is_array($_POST['to_block']) ? $_POST['to_block'] : [];
$added = 0;
$skipped = 0;
$db = getDb();
foreach ($toBlock as $value) {
$data = json_decode($value, true);
if (!is_array($data) || empty($data['ip'])) {
continue;
}
$ip = trim($data['ip']);
$loc = isset($data['loc']) ? trim($data['loc']) : null;
if ($ip === '') {
continue;
}
// Skip if this IP is already in blocked list
$exists = $db->fetchOne("SELECT 1 FROM st_blocked_ip WHERE blocked_ip = ?", [$ip]);
if ($exists) {
$skipped++;
continue;
}
try {
$db->insert('st_blocked_ip', [
'blocked_ip' => $ip,
'blocked_ip_location' => $loc !== '' ? $loc : null,
'created_at' => date('Y-m-d H:i:s')
]);
$added++;
// Remove failed_login records for this IP from st_activity (they've been copied to blocked list)
$db->execute("DELETE FROM st_activity WHERE object_type = 'Users' AND LOWER(action) = 'failed_login' AND hist_ip = ?", [$ip]);
} catch (Exception $e) {
$blockError = 'Error adding IP: ' . $e->getMessage();
break;
}
}
if ($blockError === '' && ($added > 0 || $skipped > 0)) {
$parts = [];
if ($added > 0) {
$parts[] = $added . ' IP' . ($added !== 1 ? 's' : '') . ' added to blocked list';
}
if ($skipped > 0) {
$parts[] = $skipped . ' already blocked';
}
$blockMessage = implode('. ', $parts);
} elseif ($blockError === '' && empty($toBlock)) {
$blockError = 'No rows selected.';
}
}
}
try {
$websites = getWebsites();
$failedLogins = getFailedLogins(500, $filterWebsiteId);
} catch (Exception $e) {
$websites = [];
$failedLogins = [];
}
// Success message from redirect after delete
if (isset($_GET['deleted']) && is_numeric($_GET['deleted']) && (int) $_GET['deleted'] > 0) {
$blockMessage = (int) $_GET['deleted'] . ' record' . ((int) $_GET['deleted'] !== 1 ? 's' : '') . ' deleted.';
}
$filterWebsiteUrl = '';
if ($filterWebsiteId !== null) {
foreach ($websites as $w) {
if ((int) $w['website_id'] === $filterWebsiteId) {
$filterWebsiteUrl = $w['website_url'];
break;
}
}
}
$pageTitle = 'Failed Logins';
require_once __DIR__ . '/includes/header.php';
?>
<!-- Page Header -->
<div class="flex items-center justify-between mb-6">
<div>
<p class="text-muted-foreground">Failed login attempts from client sites. Select IPs to add to the blocked list.</p>
</div>
<a href="blocked-ips.php" class="btn-secondary px-4 py-2 rounded-md text-sm font-medium flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/>
</svg>
View Blocked IPs
</a>
</div>
<?php if ($blockMessage): ?>
<div class="mb-6 p-4 rounded-md bg-green-500/10 border border-green-500/20 text-green-600 dark:text-green-400" data-testid="alert-block-success">
<?= sanitize($blockMessage) ?>
</div>
<?php endif; ?>
<?php if ($blockError): ?>
<div class="mb-6 p-4 rounded-md bg-red-500/10 border border-red-500/20 text-red-600 dark:text-red-400" data-testid="alert-block-error">
<?= sanitize($blockError) ?>
</div>
<?php endif; ?>
<!-- Website filter -->
<div class="card rounded-lg p-6 mb-6">
<h2 class="text-lg font-semibold mb-4">Filter by website</h2>
<form method="GET" class="flex flex-wrap items-end gap-4">
<div class="min-w-[200px] flex-1">
<label for="filter_website_id" class="block text-sm font-medium text-muted-foreground mb-1">Website (dropdown)</label>
<select id="filter_website_id" name="website_id" class="w-full px-3 py-2 rounded-md text-sm">
<option value="">All websites</option>
<?php foreach ($websites as $w): ?>
<option value="<?= (int) $w['website_id'] ?>" <?= $filterWebsiteId === (int) $w['website_id'] ? 'selected' : '' ?>><?= sanitize($w['website_url']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="min-w-[200px] flex-1">
<label for="filter_website_search" class="block text-sm font-medium text-muted-foreground mb-1">Or type to search</label>
<input type="text" id="filter_website_search" list="website-datalist" placeholder="Type website URL..."
value="<?= sanitize($filterWebsiteUrl) ?>"
class="w-full px-3 py-2 rounded-md text-sm" autocomplete="off">
<datalist id="website-datalist">
<?php foreach ($websites as $w): ?>
<option value="<?= sanitize($w['website_url']) ?>">
<?php endforeach; ?>
</datalist>
</div>
<div class="flex gap-2">
<button type="submit" class="btn-primary px-4 py-2 rounded-md text-sm font-medium">Apply filter</button>
<a href="failed-logins.php" class="btn-secondary px-4 py-2 rounded-md text-sm font-medium">Clear</a>
</div>
</form>
</div>
<!-- Failed Logins Table -->
<div class="card rounded-lg overflow-hidden">
<form method="POST" id="failed-logins-form">
<input type="hidden" name="csrf_token" value="<?= sanitize(generateCsrfToken()) ?>">
<input type="hidden" name="filter_website_id" value="<?= $filterWebsiteId !== null ? (int) $filterWebsiteId : '' ?>">
<div class="px-6 py-4 border-b flex items-center justify-between gap-4 flex-wrap">
<h2 class="text-lg font-semibold">Failed Logins</h2>
<div class="flex items-center gap-3">
<span class="text-sm text-muted-foreground"><?= count($failedLogins) ?> record<?= count($failedLogins) !== 1 ? 's' : '' ?></span>
<button type="submit" name="add_to_blocked" value="1"
class="btn-primary px-4 py-2 rounded-md text-sm font-medium flex items-center gap-2"
data-testid="button-add-to-blocked">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/>
</svg>
Add selected to blocked list
</button>
<button type="submit" name="delete_entries_submit" value="1"
class="btn-secondary px-4 py-2 rounded-md text-sm font-medium flex items-center gap-2 text-red-600 dark:text-red-400 hover:bg-red-500/10">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
Delete selected
</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-muted/50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider w-10">
<input type="checkbox" id="select-all-failed" class="rounded border-gray-300" data-testid="checkbox-select-all" title="Select all">
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Username</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">IP Address</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Location</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Website</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Time</th>
</tr>
</thead>
<tbody class="divide-y">
<?php if (empty($failedLogins)): ?>
<tr>
<td colspan="6" class="px-6 py-12 text-center text-muted-foreground">
<div class="flex flex-col items-center gap-2">
<svg class="w-12 h-12 text-muted-foreground/50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
<p>No failed logins found</p>
</div>
</td>
</tr>
<?php else: ?>
<?php foreach ($failedLogins as $row): ?>
<?php
$payload = json_encode([
'ip' => $row['hist_ip'] ?? '',
'loc' => $row['action_location'] ?? '',
'website_id' => (int) ($row['website_id'] ?? 0),
'histid' => (int) ($row['histid'] ?? 0)
]);
$payloadAttr = sanitize($payload);
?>
<tr class="table-row">
<td class="px-6 py-4 whitespace-nowrap">
<input type="checkbox"
name="to_block[]"
value="<?= $payloadAttr ?>"
class="failed-row-checkbox rounded border-gray-300"
data-testid="checkbox-block-ip">
</td>
<td class="px-6 py-4 whitespace-nowrap font-medium"><?= sanitize($row['target_username'] ?? '—') ?></td>
<td class="px-6 py-4 whitespace-nowrap font-mono text-sm"><?= sanitize($row['hist_ip'] ?? '—') ?></td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground"><?= $row['action_location'] !== null && $row['action_location'] !== '' ? sanitize($row['action_location']) : '—' ?></td>
<td class="px-6 py-4 text-sm text-muted-foreground"><?= sanitize($row['website_url'] ?? '—') ?></td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground"><?= formatDateTime($row['hist_time']) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</form>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var selectAll = document.getElementById('select-all-failed');
var checkboxes = document.querySelectorAll('.failed-row-checkbox');
if (selectAll) {
selectAll.addEventListener('change', function() {
checkboxes.forEach(function(cb) { cb.checked = selectAll.checked; });
});
}
// Sync "type to search" input to dropdown: when user picks from datalist, set select value
var filterSearch = document.getElementById('filter_website_search');
var filterSelect = document.getElementById('filter_website_id');
if (filterSearch && filterSelect) {
filterSearch.addEventListener('change', function() {
var url = this.value.trim();
for (var i = 0; i < filterSelect.options.length; i++) {
if (filterSelect.options[i].text === url) {
filterSelect.value = filterSelect.options[i].value;
return;
}
}
filterSelect.value = '';
});
}
// Confirm before delete
var form = document.getElementById('failed-logins-form');
if (form) {
form.addEventListener('submit', function(e) {
if (e.submitter && e.submitter.name === 'delete_entries_submit') {
var checked = document.querySelectorAll('.failed-row-checkbox:checked');
if (checked.length === 0) {
e.preventDefault();
alert('Please select at least one entry to delete.');
return;
}
if (!confirm('Delete ' + checked.length + ' selected record(s)? This cannot be undone.')) {
e.preventDefault();
}
}
});
}
});
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>