789 lines
43 KiB
HTML
789 lines
43 KiB
HTML
@include(snippets/header)
|
|
|
|
<div class="pagewrapper">
|
|
<div id="main" class="admin-container">
|
|
<div class="container" style="max-width: 1200px; margin: 0 auto; padding: 20px 15px;">
|
|
<div style="margin-bottom: 25px;">
|
|
<a href="/admin" style="color: var(--accent); text-decoration: none; font-size: 0.9rem; display: inline-flex; align-items: center; gap: 6px; margin-bottom: 12px;">
|
|
<i class="fa fa-arrow-left"></i> Back to Admin Dashboard
|
|
</a>
|
|
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px;">
|
|
<div>
|
|
<h2 style="margin: 0; font-weight: 800; letter-spacing: -0.5px; display: flex; align-items: center; gap: 10px;">
|
|
<i class="fa-solid fa-shield-halved" style="color: var(--accent);"></i> Ban & Fingerprint Manager
|
|
</h2>
|
|
<p style="color: #aaa; margin: 6px 0 0 0; font-size: 0.95rem;">
|
|
Inspect, track, and manage OpenSSH key fingerprints, IP bans, anonymous identities, and audit activity.
|
|
</p>
|
|
</div>
|
|
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
|
<button onclick="openBanModal('fingerprint')" class="btn-upload" style="width: auto; padding: 8px 16px; background: #e74c3c; border-color: #c0392b; font-size: 0.85rem; cursor: pointer; border-radius: 4px; color: #fff;">
|
|
<i class="fa-solid fa-ban"></i> Ban Fingerprint
|
|
</button>
|
|
<button onclick="openBanModal('hardware')" class="btn-upload" style="width: auto; padding: 8px 16px; background: #8e44ad; border-color: #7d3c98; font-size: 0.85rem; cursor: pointer; border-radius: 4px; color: #fff;">
|
|
<i class="fa-solid fa-microchip"></i> Ban Hardware ID
|
|
</button>
|
|
<button onclick="openBanModal('ip')" class="btn-upload" style="width: auto; padding: 8px 16px; background: #d35400; border-color: #ba4a00; font-size: 0.85rem; cursor: pointer; border-radius: 4px; color: #fff;">
|
|
<i class="fa-solid fa-network-wired"></i> Ban IP
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Stats Cards -->
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 25px;">
|
|
<div style="background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 6px; padding: 16px; text-align: center;">
|
|
<div style="font-size: 0.8rem; color: #888; text-transform: uppercase; letter-spacing: 0.5px;">Banned Fingerprints</div>
|
|
<div style="font-size: 1.8rem; font-weight: 800; color: #ff6b6b; margin-top: 4px;">{{ bannedFingerprints ? bannedFingerprints.length : 0 }}</div>
|
|
</div>
|
|
<div style="background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 6px; padding: 16px; text-align: center;">
|
|
<div style="font-size: 0.8rem; color: #888; text-transform: uppercase; letter-spacing: 0.5px;">Banned Hardware IDs</div>
|
|
<div style="font-size: 1.8rem; font-weight: 800; color: #cc5de8; margin-top: 4px;">{{ bannedHardware ? bannedHardware.length : 0 }}</div>
|
|
</div>
|
|
<div style="background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 6px; padding: 16px; text-align: center;">
|
|
<div style="font-size: 0.8rem; color: #888; text-transform: uppercase; letter-spacing: 0.5px;">Banned IP Addresses</div>
|
|
<div style="font-size: 1.8rem; font-weight: 800; color: #ffa94d; margin-top: 4px;">{{ bannedIps ? bannedIps.length : 0 }}</div>
|
|
</div>
|
|
<div style="background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 6px; padding: 16px; text-align: center;">
|
|
<div style="font-size: 0.8rem; color: #888; text-transform: uppercase; letter-spacing: 0.5px;">Known Anonymous Keys</div>
|
|
<div style="font-size: 1.8rem; font-weight: 800; color: #69db7c; margin-top: 4px;">{{ anonIdentities ? anonIdentities.length : 0 }}</div>
|
|
</div>
|
|
<div style="background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 6px; padding: 16px; text-align: center;">
|
|
<div style="font-size: 0.8rem; color: #888; text-transform: uppercase; letter-spacing: 0.5px;">Recent Logged Actions</div>
|
|
<div style="font-size: 1.8rem; font-weight: 800; color: #74c0fc; margin-top: 4px;">{{ recentActivity ? recentActivity.length : 0 }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Navigation Tabs -->
|
|
<div style="display: flex; gap: 8px; border-bottom: 1px solid rgba(255,255,255,0.12); margin-bottom: 20px; overflow-x: auto;">
|
|
<button onclick="switchTab('fingerprints')" id="tab-btn-fingerprints" class="admin-tab-btn active-tab">
|
|
<i class="fa-solid fa-key"></i> Banned Fingerprints ({{ bannedFingerprints ? bannedFingerprints.length : 0 }})
|
|
</button>
|
|
<button onclick="switchTab('hardware')" id="tab-btn-hardware" class="admin-tab-btn">
|
|
<i class="fa-solid fa-microchip"></i> Banned Hardware ({{ bannedHardware ? bannedHardware.length : 0 }})
|
|
</button>
|
|
<button onclick="switchTab('ips')" id="tab-btn-ips" class="admin-tab-btn">
|
|
<i class="fa-solid fa-network-wired"></i> Banned IPs ({{ bannedIps ? bannedIps.length : 0 }})
|
|
</button>
|
|
<button onclick="switchTab('identities')" id="tab-btn-identities" class="admin-tab-btn">
|
|
<i class="fa-solid fa-user-secret"></i> Anonymous Identities ({{ anonIdentities ? anonIdentities.length : 0 }})
|
|
</button>
|
|
<button onclick="switchTab('activity')" id="tab-btn-activity" class="admin-tab-btn">
|
|
<i class="fa-solid fa-list-check"></i> Activity Audit ({{ recentActivity ? recentActivity.length : 0 }})
|
|
</button>
|
|
</div>
|
|
|
|
<!-- TAB 1: BANNED FINGERPRINTS -->
|
|
<div id="tab-pane-fingerprints" class="tab-pane active-pane">
|
|
<div class="upload-form" style="background: rgba(0,0,0,0.3); padding: 15px; border-radius: 6px;">
|
|
<table class="responsive-table clean-table" style="width: 100%;">
|
|
<thead>
|
|
<tr>
|
|
<th>Fingerprint</th>
|
|
<th>Linked Account</th>
|
|
<th>Reason</th>
|
|
<th>Banned By</th>
|
|
<th>Date</th>
|
|
<th>Expires</th>
|
|
<th>Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@if(bannedFingerprints && bannedFingerprints.length > 0)
|
|
@each(bannedFingerprints as fp)
|
|
<tr>
|
|
<td data-label="Fingerprint">
|
|
<div style="display: flex; align-items: center; gap: 6px;">
|
|
<code class="clickable-code" onclick="copyText('{{ fp.fingerprint }}')" title="Click to copy full fingerprint">
|
|
{{ fp.fingerprint }}
|
|
</code>
|
|
<button onclick="copyText('{{ fp.fingerprint }}')" class="btn-icon" title="Copy fingerprint">
|
|
<i class="fa-regular fa-copy"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
<td data-label="Linked Account">
|
|
@if(fp.anon_login)
|
|
<a href="/user/{{ fp.anon_login }}" target="_blank" style="color: var(--accent); font-weight: bold; text-decoration: none;">
|
|
<i class="fa-solid fa-user-secret"></i> {!! fp.anon_user || fp.anon_login !!}
|
|
</a>
|
|
@else
|
|
<span style="color: #888;">—</span>
|
|
@endif
|
|
</td>
|
|
<td data-label="Reason">
|
|
<span style="color: #ff8787; font-weight: 500;">{{ fp.reason || 'No reason specified' }}</span>
|
|
</td>
|
|
<td data-label="Banned By">
|
|
{{ fp.banned_by_user || 'System / Admin' }}
|
|
</td>
|
|
<td data-label="Date">
|
|
{{ fp.created_at ? new Date(fp.created_at).toLocaleString() : '—' }}
|
|
</td>
|
|
<td data-label="Expires">
|
|
@if(fp.expires_at)
|
|
<span style="color: #fcc419;">{{ new Date(fp.expires_at).toLocaleString() }}</span>
|
|
@else
|
|
<span style="color: #ff6b6b; font-weight: bold;">Permanent</span>
|
|
@endif
|
|
</td>
|
|
<td data-label="Action">
|
|
<button onclick="unbanFingerprint('{{ fp.fingerprint }}')" class="btn-remove" style="padding: 4px 10px; font-size: 0.8rem; background: #27ae60; border-color: #2ecc71; color: #fff; cursor: pointer; border-radius: 4px;">
|
|
<i class="fa-solid fa-unlock"></i> Unban
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
@endeach
|
|
@else
|
|
<tr>
|
|
<td colspan="7" style="text-align: center; padding: 30px; color: #888;">
|
|
<i class="fa-solid fa-circle-check" style="color: #51cf66; margin-right: 6px;"></i> No banned fingerprints.
|
|
</td>
|
|
</tr>
|
|
@endif
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- TAB: BANNED HARDWARE -->
|
|
<div id="tab-pane-hardware" class="tab-pane" style="display: none;">
|
|
<div class="upload-form" style="background: rgba(0,0,0,0.3); padding: 15px; border-radius: 6px;">
|
|
<table class="responsive-table clean-table" style="width: 100%;">
|
|
<thead>
|
|
<tr>
|
|
<th>Hardware Fingerprint</th>
|
|
<th>Reason</th>
|
|
<th>Banned By</th>
|
|
<th>Date</th>
|
|
<th>Expires</th>
|
|
<th>Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@if(bannedHardware && bannedHardware.length > 0)
|
|
@each(bannedHardware as hw)
|
|
<tr>
|
|
<td data-label="Hardware ID">
|
|
<div style="display: flex; align-items: center; gap: 6px;">
|
|
<code class="clickable-code" onclick="copyText('{{ hw.hw_fingerprint }}')" title="Click to copy hardware ID">
|
|
{{ hw.hw_fingerprint.substring(0, 24) }}...
|
|
</code>
|
|
<button onclick="copyText('{{ hw.hw_fingerprint }}')" class="btn-icon" title="Copy Hardware ID">
|
|
<i class="fa-regular fa-copy"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
<td data-label="Reason">
|
|
<span style="color: #ff8787;">{{ hw.reason || 'Banned' }}</span>
|
|
</td>
|
|
<td data-label="Banned By">
|
|
@if(hw.banned_by_user)
|
|
<span style="color: #aaa;">{{ hw.banned_by_user }}</span>
|
|
@else
|
|
<span style="color: #777;">System</span>
|
|
@endif
|
|
</td>
|
|
<td data-label="Date">
|
|
{{ hw.created_at ? new Date(hw.created_at).toLocaleDateString() : '—' }}
|
|
</td>
|
|
<td data-label="Expires">
|
|
@if(hw.expires_at)
|
|
<span style="color: #fcc419;">{{ new Date(hw.expires_at).toLocaleString() }}</span>
|
|
@else
|
|
<span style="color: #ff6b6b; font-weight: bold;">Permanent</span>
|
|
@endif
|
|
</td>
|
|
<td data-label="Action">
|
|
<button onclick="unbanHardware('{{ hw.hw_fingerprint }}')" class="btn-remove" style="padding: 4px 10px; font-size: 0.8rem; background: #27ae60; border-color: #2ecc71; color: #fff; cursor: pointer; border-radius: 4px;">
|
|
<i class="fa-solid fa-unlock"></i> Unban
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
@endeach
|
|
@else
|
|
<tr>
|
|
<td colspan="6" style="text-align: center; padding: 30px; color: #888;">
|
|
<i class="fa-solid fa-circle-check" style="color: #51cf66; margin-right: 6px;"></i> No banned hardware fingerprints.
|
|
</td>
|
|
</tr>
|
|
@endif
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- TAB 2: BANNED IPS -->
|
|
<div id="tab-pane-ips" class="tab-pane" style="display: none;">
|
|
<div class="upload-form" style="background: rgba(0,0,0,0.3); padding: 15px; border-radius: 6px;">
|
|
<table class="responsive-table clean-table" style="width: 100%;">
|
|
<thead>
|
|
<tr>
|
|
<th>IP / Hash</th>
|
|
<th>Reason</th>
|
|
<th>Banned By</th>
|
|
<th>Date</th>
|
|
<th>Expires</th>
|
|
<th>Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@if(bannedIps && bannedIps.length > 0)
|
|
@each(bannedIps as item)
|
|
<tr>
|
|
<td data-label="IP / Hash">
|
|
<div style="display: flex; align-items: center; gap: 6px;">
|
|
<code class="clickable-code" onclick="copyText('{{ item.ip }}')" title="Click to copy IP">
|
|
{{ item.ip }}
|
|
</code>
|
|
<button onclick="copyText('{{ item.ip }}')" class="btn-icon" title="Copy IP">
|
|
<i class="fa-regular fa-copy"></i>
|
|
</button>
|
|
</div>
|
|
@if(item.ip_hash && item.ip_hash !== item.ip)
|
|
<div style="font-size: 0.75rem; color: #777; margin-top: 3px;">
|
|
Hash: {{ item.ip_hash.substring(0, 16) }}...
|
|
</div>
|
|
@endif
|
|
</td>
|
|
<td data-label="Reason">
|
|
<span style="color: #ff8787; font-weight: 500;">{{ item.reason || 'No reason specified' }}</span>
|
|
</td>
|
|
<td data-label="Banned By">
|
|
{{ item.banned_by_user || 'System / Admin' }}
|
|
</td>
|
|
<td data-label="Date">
|
|
{{ item.created_at ? new Date(item.created_at).toLocaleString() : '—' }}
|
|
</td>
|
|
<td data-label="Expires">
|
|
@if(item.expires_at)
|
|
<span style="color: #fcc419;">{{ new Date(item.expires_at).toLocaleString() }}</span>
|
|
@else
|
|
<span style="color: #ff6b6b; font-weight: bold;">Permanent</span>
|
|
@endif
|
|
</td>
|
|
<td data-label="Action">
|
|
<button onclick="unbanIp('{{ item.ip }}')" class="btn-remove" style="padding: 4px 10px; font-size: 0.8rem; background: #27ae60; border-color: #2ecc71; color: #fff; cursor: pointer; border-radius: 4px;">
|
|
<i class="fa-solid fa-unlock"></i> Unban
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
@endeach
|
|
@else
|
|
<tr>
|
|
<td colspan="6" style="text-align: center; padding: 30px; color: #888;">
|
|
<i class="fa-solid fa-circle-check" style="color: #51cf66; margin-right: 6px;"></i> No banned IP addresses.
|
|
</td>
|
|
</tr>
|
|
@endif
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- TAB 3: ANONYMOUS IDENTITIES -->
|
|
<div id="tab-pane-identities" class="tab-pane" style="display: none;">
|
|
<div class="upload-form" style="background: rgba(0,0,0,0.3); padding: 15px; border-radius: 6px;">
|
|
<table class="responsive-table clean-table" style="width: 100%;">
|
|
<thead>
|
|
<tr>
|
|
<th>Account</th>
|
|
<th>Public Key Fingerprint</th>
|
|
<th>Hardware ID</th>
|
|
<th>Created / IP</th>
|
|
<th>Last Seen / IP</th>
|
|
<th>Status</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@if(anonIdentities && anonIdentities.length > 0)
|
|
@each(anonIdentities as idn)
|
|
<tr>
|
|
<td data-label="Account">
|
|
@if(idn.login)
|
|
<a href="/user/{{ idn.login }}" target="_blank" style="color: var(--accent); font-weight: bold; text-decoration: none; display: inline-flex; align-items: center; gap: 5px;">
|
|
<i class="fa-solid fa-user-secret"></i> {!! idn.user || idn.login !!}
|
|
</a>
|
|
<div style="font-size: 0.75rem; color: #888;">UID: {{ idn.user_id }}</div>
|
|
@else
|
|
<span style="color: #888;">UID: {{ idn.user_id }} (Deleted)</span>
|
|
@endif
|
|
</td>
|
|
<td data-label="Fingerprint">
|
|
<div style="display: flex; align-items: center; gap: 6px;">
|
|
<code class="clickable-code" onclick="copyText('{{ idn.fingerprint }}')" title="Click to copy full fingerprint">
|
|
{{ idn.fingerprint.substring(0, 22) }}...
|
|
</code>
|
|
<button onclick="copyText('{{ idn.fingerprint }}')" class="btn-icon" title="Copy fingerprint">
|
|
<i class="fa-regular fa-copy"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
<td data-label="Hardware ID">
|
|
@if(idn.hw_fingerprint)
|
|
<div style="display: flex; align-items: center; gap: 6px;">
|
|
<code class="clickable-code" onclick="copyText('{{ idn.hw_fingerprint }}')" title="Click to copy full hardware ID">
|
|
{{ idn.hw_fingerprint.substring(0, 18) }}...
|
|
</code>
|
|
<button onclick="copyText('{{ idn.hw_fingerprint }}')" class="btn-icon" title="Copy hardware ID">
|
|
<i class="fa-regular fa-copy"></i>
|
|
</button>
|
|
@if(idn.is_hw_banned)
|
|
<span class="badge" style="background: #e74c3c; color: #fff; padding: 2px 5px; border-radius: 3px; font-size: 0.65rem;">HW BANNED</span>
|
|
@else
|
|
<button onclick="openBanModal('hardware', '{{ idn.hw_fingerprint }}', {{ idn.user_id }})" class="btn-icon" title="Ban Hardware ID" style="color: #e74c3c; margin-left: 2px;">
|
|
<i class="fa-solid fa-ban"></i>
|
|
</button>
|
|
@endif
|
|
</div>
|
|
@else
|
|
<span style="color: #666; font-size: 0.8rem;">—</span>
|
|
@endif
|
|
</td>
|
|
<td data-label="Created / IP">
|
|
<div>{{ idn.created_at ? new Date(idn.created_at).toLocaleDateString() : '—' }}</div>
|
|
@if(idn.created_ip)
|
|
<div style="font-size: 0.75rem; color: #888; font-family: monospace;">{{ idn.created_ip.substring(0, 16) }}...</div>
|
|
@endif
|
|
</td>
|
|
<td data-label="Last Seen / IP">
|
|
<div>{{ idn.last_seen ? new Date(idn.last_seen).toLocaleDateString() : '—' }}</div>
|
|
@if(idn.last_ip)
|
|
<div style="font-size: 0.75rem; color: #888; font-family: monospace;">{{ idn.last_ip.substring(0, 16) }}...</div>
|
|
@endif
|
|
</td>
|
|
<td data-label="Status">
|
|
@if(idn.is_banned || idn.is_fp_banned || idn.is_hw_banned)
|
|
<span class="badge" style="background: #e74c3c; color: #fff; padding: 3px 8px; border-radius: 3px; font-size: 0.75rem;">BANNED</span>
|
|
@else
|
|
<span class="badge" style="background: #27ae60; color: #fff; padding: 3px 8px; border-radius: 3px; font-size: 0.75rem;">ACTIVE</span>
|
|
@endif
|
|
</td>
|
|
<td data-label="Actions">
|
|
@if(idn.is_banned || idn.is_fp_banned)
|
|
<button onclick="unbanFingerprint('{{ idn.fingerprint }}')" class="btn-remove" style="padding: 4px 10px; font-size: 0.8rem; background: #27ae60; border-color: #2ecc71; color: #fff; cursor: pointer; border-radius: 4px;">
|
|
<i class="fa-solid fa-unlock"></i> Unban Key
|
|
</button>
|
|
@else
|
|
<button onclick="promptBanIdentity({{ idn.user_id }}, '{{ idn.fingerprint }}')" class="btn-remove" style="padding: 4px 10px; font-size: 0.8rem; background: #e74c3c; border-color: #c0392b; color: #fff; cursor: pointer; border-radius: 4px;">
|
|
<i class="fa-solid fa-ban"></i> Ban Key
|
|
</button>
|
|
@endif
|
|
</td>
|
|
</tr>
|
|
@endeach
|
|
@else
|
|
<tr>
|
|
<td colspan="7" style="text-align: center; padding: 30px; color: #888;">
|
|
No anonymous identities found in database.
|
|
</td>
|
|
</tr>
|
|
@endif
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- TAB 4: ACTIVITY AUDIT LOG -->
|
|
<div id="tab-pane-activity" class="tab-pane" style="display: none;">
|
|
<div class="upload-form" style="background: rgba(0,0,0,0.3); padding: 15px; border-radius: 6px;">
|
|
<table class="responsive-table clean-table" style="width: 100%;">
|
|
<thead>
|
|
<tr>
|
|
<th>Time</th>
|
|
<th>User</th>
|
|
<th>Action</th>
|
|
<th>Target ID</th>
|
|
<th>Details</th>
|
|
<th>IP</th>
|
|
<th>Hardware ID</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@if(recentActivity && recentActivity.length > 0)
|
|
@each(recentActivity as act)
|
|
<tr>
|
|
<td data-label="Time" style="white-space: nowrap; font-size: 0.85rem;">
|
|
{{ act.created_at ? new Date(act.created_at).toLocaleString() : '—' }}
|
|
</td>
|
|
<td data-label="User">
|
|
@if(act.login)
|
|
<a href="/user/{{ act.login }}" target="_blank" style="color: var(--accent); text-decoration: none;">
|
|
{!! act.user || act.login !!}
|
|
</a>
|
|
@else
|
|
<span style="color: #888;">UID: {{ act.user_id }}</span>
|
|
@endif
|
|
</td>
|
|
<td data-label="Action">
|
|
<span class="action-badge action-{{ act.action }}">
|
|
{{ act.action }}
|
|
</span>
|
|
</td>
|
|
<td data-label="Target ID">
|
|
@if(act.target_id)
|
|
@if(act.action === 'comment' || act.action === 'upload')
|
|
<a href="/{{ act.target_id }}" target="_blank" style="color: var(--accent); text-decoration: underline;">
|
|
#{{ act.target_id }}
|
|
</a>
|
|
@else
|
|
#{{ act.target_id }}
|
|
@endif
|
|
@else
|
|
<span style="color: #777;">—</span>
|
|
@endif
|
|
</td>
|
|
<td data-label="Details" style="font-size: 0.8rem; font-family: monospace; max-width: 300px; word-break: break-all;">
|
|
@if(act.details)
|
|
{{ JSON.stringify(act.details) }}
|
|
@else
|
|
<span style="color: #777;">—</span>
|
|
@endif
|
|
</td>
|
|
<td data-label="IP" style="font-family: monospace; font-size: 0.8rem;">
|
|
{{ act.ip }}
|
|
</td>
|
|
<td data-label="Hardware ID" style="font-family: monospace; font-size: 0.8rem;">
|
|
@if(act.hw_fingerprint)
|
|
<div style="display: flex; align-items: center; gap: 6px;">
|
|
<code class="clickable-code" onclick="copyText('{{ act.hw_fingerprint }}')" title="Click to copy Hardware ID">
|
|
{{ act.hw_fingerprint.substring(0, 16) }}...
|
|
</code>
|
|
<button onclick="copyText('{{ act.hw_fingerprint }}')" class="btn-icon" title="Copy Hardware ID">
|
|
<i class="fa-regular fa-copy"></i>
|
|
</button>
|
|
</div>
|
|
@else
|
|
<span style="color: #777;">—</span>
|
|
@endif
|
|
</td>
|
|
</tr>
|
|
@endeach
|
|
@else
|
|
<tr>
|
|
<td colspan="7" style="text-align: center; padding: 30px; color: #888;">
|
|
No anonymous activity recorded yet.
|
|
</td>
|
|
</tr>
|
|
@endif
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal: Ban Fingerprint or IP -->
|
|
<div id="ban-modal" class="modal-overlay" style="display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.75); z-index: 10000; align-items: center; justify-content: center;">
|
|
<div style="background: #1e1e1e; border: 1px solid rgba(255,255,255,0.15); border-radius: 8px; width: 92%; max-width: 500px; padding: 25px; box-shadow: 0 10px 30px rgba(0,0,0,0.8);">
|
|
<h3 id="modal-title" style="margin-top: 0; margin-bottom: 15px; color: #fff; display: flex; align-items: center; gap: 8px;">
|
|
<i class="fa-solid fa-ban" style="color: #e74c3c;"></i> Ban Target
|
|
</h3>
|
|
|
|
<form id="ban-form" onsubmit="submitBan(event)">
|
|
<input type="hidden" id="ban-target-type" value="fingerprint">
|
|
<input type="hidden" id="ban-target-user-id" value="">
|
|
|
|
<div style="margin-bottom: 15px;">
|
|
<label id="target-value-label" style="display: block; font-size: 0.85rem; color: #aaa; margin-bottom: 5px;">OpenSSH Fingerprint</label>
|
|
<input type="text" id="ban-target-value" required style="width: 100%; background: #2a2a2a; border: 1px solid #444; border-radius: 4px; padding: 8px 10px; color: #fff; font-family: monospace; font-size: 0.9rem; box-sizing: border-box;" placeholder="SHA256:...">
|
|
</div>
|
|
|
|
<div style="margin-bottom: 15px;">
|
|
<label style="display: block; font-size: 0.85rem; color: #aaa; margin-bottom: 5px;">Ban Reason (Displayed to User)</label>
|
|
<input type="text" id="ban-reason" required value="Violation of community guidelines" style="width: 100%; background: #2a2a2a; border: 1px solid #444; border-radius: 4px; padding: 8px 10px; color: #fff; font-size: 0.9rem; box-sizing: border-box;">
|
|
</div>
|
|
|
|
<div style="margin-bottom: 15px;">
|
|
<label style="display: block; font-size: 0.85rem; color: #aaa; margin-bottom: 5px;">Duration</label>
|
|
<select id="ban-duration" style="width: 100%; background: #2a2a2a; border: 1px solid #444; border-radius: 4px; padding: 8px 10px; color: #fff; font-size: 0.9rem; box-sizing: border-box;">
|
|
<option value="1">1 Hour</option>
|
|
<option value="24">24 Hours</option>
|
|
<option value="168">7 Days</option>
|
|
<option value="720">30 Days</option>
|
|
<option value="permanent" selected>Permanent</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div id="cascade-ips-group" style="margin-bottom: 20px; display: flex; align-items: center; gap: 8px;">
|
|
<input type="checkbox" id="ban-cascade-ips" checked style="cursor: pointer;">
|
|
<label for="ban-cascade-ips" style="color: #ddd; font-size: 0.85rem; cursor: pointer;">
|
|
Also ban all associated IP addresses used by this identity
|
|
</label>
|
|
</div>
|
|
|
|
<div style="display: flex; justify-content: flex-end; gap: 10px;">
|
|
<button type="button" onclick="closeBanModal()" style="background: transparent; border: 1px solid #555; color: #ccc; padding: 8px 16px; border-radius: 4px; cursor: pointer;">Cancel</button>
|
|
<button type="submit" style="background: #e74c3c; border: 1px solid #c0392b; color: #fff; padding: 8px 18px; border-radius: 4px; font-weight: bold; cursor: pointer;">Enforce Ban</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.admin-tab-btn {
|
|
background: transparent;
|
|
border: none;
|
|
border-bottom: 2px solid transparent;
|
|
color: #888;
|
|
padding: 10px 16px;
|
|
font-size: 0.95rem;
|
|
cursor: pointer;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
transition: all 0.15s ease;
|
|
}
|
|
.admin-tab-btn:hover {
|
|
color: #fff;
|
|
}
|
|
.admin-tab-btn.active-tab {
|
|
color: var(--accent);
|
|
border-bottom-color: var(--accent);
|
|
font-weight: bold;
|
|
}
|
|
.clickable-code {
|
|
background: rgba(255,255,255,0.06);
|
|
border: 1px solid rgba(255,255,255,0.1);
|
|
padding: 2px 6px;
|
|
border-radius: 3px;
|
|
color: #a5d8ff;
|
|
font-size: 0.85rem;
|
|
cursor: pointer;
|
|
transition: background 0.15s;
|
|
}
|
|
.clickable-code:hover {
|
|
background: rgba(255,255,255,0.12);
|
|
color: #d0ebff;
|
|
}
|
|
.btn-icon {
|
|
background: none;
|
|
border: none;
|
|
color: #888;
|
|
cursor: pointer;
|
|
font-size: 0.85rem;
|
|
padding: 2px;
|
|
}
|
|
.btn-icon:hover {
|
|
color: var(--accent);
|
|
}
|
|
.action-badge {
|
|
display: inline-block;
|
|
padding: 2px 7px;
|
|
border-radius: 3px;
|
|
font-size: 0.75rem;
|
|
font-weight: bold;
|
|
text-transform: uppercase;
|
|
}
|
|
.action-comment { background: rgba(51, 154, 240, 0.2); color: #74c0fc; border: 1px solid #339af0; }
|
|
.action-upload { background: rgba(81, 207, 102, 0.2); color: #51cf66; border: 1px solid #51cf66; }
|
|
.action-tag, .action-cycle_rating { background: rgba(252, 196, 25, 0.2); color: #fcc419; border: 1px solid #fcc419; }
|
|
.action-favorite, .action-favorites_import { background: rgba(255, 107, 107, 0.2); color: #ff6b6b; border: 1px solid #ff6b6b; }
|
|
.action-comment_delete, .action-delete_tag { background: rgba(224, 49, 49, 0.2); color: #ff8787; border: 1px solid #e03131; }
|
|
.action-handshake { background: rgba(132, 94, 247, 0.2); color: #b197fc; border: 1px solid #845ef7; }
|
|
</style>
|
|
|
|
<script>
|
|
function switchTab(tabId) {
|
|
document.querySelectorAll('.admin-tab-btn').forEach(btn => btn.classList.remove('active-tab'));
|
|
document.querySelectorAll('.tab-pane').forEach(pane => {
|
|
pane.style.display = 'none';
|
|
pane.classList.remove('active-pane');
|
|
});
|
|
|
|
const btn = document.getElementById('tab-btn-' + tabId);
|
|
const pane = document.getElementById('tab-pane-' + tabId);
|
|
if (btn) btn.classList.add('active-tab');
|
|
if (pane) {
|
|
pane.style.display = 'block';
|
|
pane.classList.add('active-pane');
|
|
}
|
|
}
|
|
|
|
function copyText(text) {
|
|
if (navigator.clipboard) {
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
alert('Copied to clipboard: ' + text);
|
|
}).catch(() => {
|
|
prompt('Copy text:', text);
|
|
});
|
|
} else {
|
|
prompt('Copy text:', text);
|
|
}
|
|
}
|
|
|
|
function openBanModal(type, targetValue = '', userId = null) {
|
|
document.getElementById('ban-target-type').value = type;
|
|
document.getElementById('ban-target-user-id').value = userId || '';
|
|
document.getElementById('ban-target-value').value = targetValue;
|
|
|
|
const modalTitle = document.getElementById('modal-title');
|
|
const valueLabel = document.getElementById('target-value-label');
|
|
const cascadeGroup = document.getElementById('cascade-ips-group');
|
|
const targetInput = document.getElementById('ban-target-value');
|
|
|
|
if (type === 'fingerprint') {
|
|
modalTitle.innerHTML = '<i class="fa-solid fa-key" style="color: #e74c3c;"></i> Ban Key Fingerprint';
|
|
valueLabel.innerText = 'OpenSSH Key Fingerprint (e.g. SHA256:...)';
|
|
targetInput.placeholder = 'SHA256:...';
|
|
cascadeGroup.style.display = 'flex';
|
|
} else if (type === 'hardware') {
|
|
modalTitle.innerHTML = '<i class="fa-solid fa-microchip" style="color: #cc5de8;"></i> Ban Hardware ID';
|
|
valueLabel.innerText = 'Hardware Fingerprint (e.g. HW:...)';
|
|
targetInput.placeholder = 'HW:...';
|
|
cascadeGroup.style.display = 'flex';
|
|
} else {
|
|
modalTitle.innerHTML = '<i class="fa-solid fa-network-wired" style="color: #d35400;"></i> Ban IP Address';
|
|
valueLabel.innerText = 'IP Address (IPv4 or IPv6 or Hash)';
|
|
targetInput.placeholder = '198.51.100.42';
|
|
cascadeGroup.style.display = 'none';
|
|
}
|
|
|
|
document.getElementById('ban-modal').style.display = 'flex';
|
|
}
|
|
|
|
function closeBanModal() {
|
|
document.getElementById('ban-modal').style.display = 'none';
|
|
}
|
|
|
|
function promptBanIdentity(userId, fingerprint) {
|
|
openBanModal('fingerprint', fingerprint, userId);
|
|
}
|
|
|
|
function getCsrfToken() {
|
|
return (window.f0ckSession && window.f0ckSession.csrf_token) ||
|
|
document.querySelector('meta[name="csrf-token"]')?.content ||
|
|
'{{ csrf_token }}' ||
|
|
'{{ session ? session.csrf_token : "" }}' ||
|
|
'';
|
|
}
|
|
|
|
async function submitBan(e) {
|
|
e.preventDefault();
|
|
const type = document.getElementById('ban-target-type').value;
|
|
const value = document.getElementById('ban-target-value').value.trim();
|
|
const reason = document.getElementById('ban-reason').value.trim();
|
|
const duration = document.getElementById('ban-duration').value;
|
|
const cascadeIps = document.getElementById('ban-cascade-ips').checked;
|
|
const userId = document.getElementById('ban-target-user-id').value;
|
|
|
|
if (!value) return alert('Target value is required');
|
|
|
|
try {
|
|
const csrfToken = getCsrfToken();
|
|
let endpoint = type === 'fingerprint'
|
|
? '/api/v2/admin/bans/fingerprint/ban'
|
|
: (type === 'hardware' ? '/api/v2/admin/bans/hardware/ban' : '/api/v2/admin/bans/ip/ban');
|
|
|
|
const payload = type === 'fingerprint'
|
|
? { fingerprint: value, reason, duration, ban_ips: cascadeIps, user_id: userId || undefined, csrf_token: csrfToken }
|
|
: (type === 'hardware'
|
|
? { hw_fingerprint: value, reason, duration, ban_ips: cascadeIps, user_id: userId || undefined, csrf_token: csrfToken }
|
|
: { ip: value, reason, duration, csrf_token: csrfToken });
|
|
|
|
const res = await fetch(endpoint, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
alert('Ban successfully applied!');
|
|
window.location.reload();
|
|
} else {
|
|
alert(data.msg || 'Failed to apply ban');
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('Network error while enforcing ban');
|
|
}
|
|
}
|
|
|
|
async function unbanFingerprint(fingerprint) {
|
|
if (!confirm('Are you sure you want to unban this key fingerprint: ' + fingerprint + '?')) return;
|
|
|
|
try {
|
|
const csrfToken = getCsrfToken();
|
|
const res = await fetch('/api/v2/admin/bans/fingerprint/unban', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: JSON.stringify({ fingerprint, csrf_token: csrfToken })
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
alert('Fingerprint successfully unbanned!');
|
|
window.location.reload();
|
|
} else {
|
|
alert(data.msg || 'Failed to unban fingerprint');
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('Network error while unbanning');
|
|
}
|
|
}
|
|
|
|
async function unbanHardware(hw_fingerprint) {
|
|
if (!confirm('Are you sure you want to unban this Hardware ID: ' + hw_fingerprint + '?')) return;
|
|
|
|
try {
|
|
const csrfToken = getCsrfToken();
|
|
const res = await fetch('/api/v2/admin/bans/hardware/unban', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: JSON.stringify({ hw_fingerprint, csrf_token: csrfToken })
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
alert('Hardware ID successfully unbanned!');
|
|
window.location.reload();
|
|
} else {
|
|
alert(data.msg || 'Failed to unban Hardware ID');
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('Network error while unbanning Hardware ID');
|
|
}
|
|
}
|
|
|
|
async function unbanIp(ip) {
|
|
if (!confirm('Are you sure you want to unban this IP: ' + ip + '?')) return;
|
|
|
|
try {
|
|
const csrfToken = getCsrfToken();
|
|
const res = await fetch('/api/v2/admin/bans/ip/unban', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: JSON.stringify({ ip, csrf_token: csrfToken })
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
alert('IP successfully unbanned!');
|
|
window.location.reload();
|
|
} else {
|
|
alert(data.msg || 'Failed to unban IP');
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('Network error while unbanning IP');
|
|
}
|
|
}
|
|
</script>
|
|
|
|
@include(snippets/footer)
|