This commit is contained in:
2026-09-12 21:55:40 +02:00
parent 53055ea5c6
commit 90860b9279
24 changed files with 1958 additions and 45 deletions
+1
View File
@@ -16,6 +16,7 @@
<li><a href="/admin/sessions">Sessions</a></li>
<li><a href="/admin/tokens">Invite Tokens</a></li>
<li><a href="/admin/users">User Manager</a></li>
<li><a href="/admin/bans">Ban & Fingerprint Manager</a></li>
<li><a href="/admin/emojis">Emoji Manager</a></li>
<li><a href="/admin/memes">Meme Manager</a></li>
<li><a href="/admin/koepfe">Köpfe Manager</a></li>
+788
View File
@@ -0,0 +1,788 @@
@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)
+1
View File
@@ -74,6 +74,7 @@
<tr><td>{{ t('ranking.stat_total') }}</td><td>{{ stats.total }}</td></tr>
<tr><td>{{ t('ranking.stat_comments') }}</td><td>{{ totalComments }}</td></tr>
<tr><td>{{ t('ranking.stat_users') }}</td><td>{{ totalUsers }}</td></tr>
<tr><td>{{ t('ranking.stat_anon_users') }}</td><td>{{ totalAnonUsers }}</td></tr>
<tr><td>{{ t('ranking.stat_favs') }}</td><td>{{ totalFavs }}</td></tr>
<tr><td>{{ t('ranking.stat_tagged') }}</td><td>{{ stats.tagged }}</td></tr>
<tr><td>{{ t('ranking.stat_untagged') }}</td><td>{{ stats.untagged }}</td></tr>
+8 -8
View File
@@ -1,6 +1,6 @@
<div class="profile-page-wrapper" style="max-width: 800px; margin: 0 auto; width: 100%; padding: 20px 10px; box-sizing: border-box;">
<div class="profile_head" id="live-profile-preview-box" data-banner-user="{{ user.user || '' }}" style="position: relative; @if(user.banner_file && user_banner_enabled) --author-banner: url('/a/{{ user.banner_file }}'); --author-banner-position: {{ user.banner_position === 'center' ? 'center top' : (user.banner_position || 'center top') }}; --author-banner-size: {{ user.banner_size || 'cover' }}; @endif">
@if(session && session.id === user.user_id && user_banner_enabled)
@if(session && session.id === user.user_id && !session.is_anon && user_banner_enabled)
<div id="inline-banner-edit-wrapper" style="display: none; position:absolute; top: 5px; right: 5px; z-index: 10;">
<button id="inline-banner-edit-btn" onclick="const menu = document.getElementById('banner-edit-menu'); menu.style.display = menu.style.display === 'none' ? 'block' : 'none';" style="background: rgba(0,0,0,0.6); border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; padding: 4px 8px; color: #fff; cursor: pointer; font-size: 0.8em;" title="{{ t('profile.edit_banner', 'Edit Banner') }}">
<i class="fa-solid fa-panorama"></i>
@@ -33,14 +33,14 @@
@else
<img src="/a/default.png" class="profile-avatar-img" />
@endif
@if(session && session.id === user.user_id)
@if(session && session.id === user.user_id && !session.is_anon)
<button id="inline-avatar-btn" class="avatar-edit-btn" title="{{ t('profile.edit_avatar', 'Edit Avatar') }}">
<i class="fa-solid fa-pen" style="font-size: 0.8em;"></i>
</button>
<input type="file" id="inline-avatar-file" accept="image/gif,image/jpeg,image/png,image/webp" style="display: none;">
@endif
</div>
@if(session && session.id === user.user_id)
@if(session && session.id === user.user_id && !session.is_anon)
<button id="edit-profile-btn" class="btn btn-sm btn-outline-primary" onclick="const box = document.getElementById('live-profile-preview-box'); box.classList.toggle('edit-mode-active'); if(box.classList.contains('edit-mode-active')){ this.innerHTML='<i class=&quot;fa-solid fa-check&quot;></i>'; this.style.color='#51cf66'; this.style.borderColor='#51cf66'; } else { this.innerHTML='<i class=&quot;fa-solid fa-pen&quot;></i>'; this.style.color='var(--accent)'; this.style.borderColor='var(--accent)'; }" style="position: absolute; bottom: 5px; left: 5px; padding: 3px 8px; font-size: 0.85em; border: 1px solid var(--accent); color: var(--accent); background: rgba(0,0,0,0.8); cursor: pointer; border-radius: 4px; z-index: 20; box-shadow: 0 0 5px rgba(0,0,0,0.5);" title="{{ t('profile.edit_btn', 'Edit Profile') }}"><i class="fa-solid fa-pen"></i></button>
@endif
</div>
@@ -49,13 +49,13 @@
<div style="display: flex; justify-content: space-between; align-items: flex-start; gap: 10px;">
<div style="flex: 1; min-width: 0; word-break: break-word; line-height: 1.2; align-self: flex-start; margin-top: 0; display: flex; align-items: center; gap: 5px;" id="profile-name-container">
<span id="profile-display-name" @if(user.username_color) style="color: {{ user.username_color }}" @endif>@if(user.admin)&#9889;&nbsp;@elseif(user.is_moderator)&#128737;&nbsp;@endif{!! user.display_name || user.user !!}@if(user.is_ghost) <span class="badge badge-secondary" style="font-size: 0.5em; vertical-align: middle; background-color: #5bc0de; color: #fff; padding: 2px 5px; border-radius: 3px; margin-left: 5px;">LEGACY</span>@endif @if(session && user.banned) <span class="badge badge-danger" tooltip="{{ user.ban_duration }}" style="font-size: 0.5em; vertical-align: middle; background-color: #d9534f; color: #fff; padding: 2px 5px; border-radius: 3px; margin-left: 5px;">BANNED</span>@endif</span>@if(user.display_name) <span style="font-size: 0.65em; color: #666; font-weight: 400; margin-left: 5px; letter-spacing: 0.5px;" id="username-bracket">({!! user.user !!})</span>@endif
@if(session && session.id === user.user_id)
@if(session && session.id === user.user_id && !session.is_anon)
<button id="inline-name-edit-btn" style="background: transparent; border:none; color: var(--text-muted); cursor: pointer; font-size: 0.7em; padding: 0;" title="Edit Name & Color">
<i class="fa-solid fa-pen"></i>
</button>
@endif
</div>
@if(session && session.id === user.user_id)
@if(session && session.id === user.user_id && !session.is_anon)
<div id="profile-name-edit-container" style="display: none; align-items: center; gap: 5px; background: rgba(0,0,0,0.5); padding: 5px; border-radius: 4px; width: max-content;">
<input type="text" id="inline-display-name" value="@if(user.display_name){!! user.display_name !!}@endif" maxlength="32" style="background: #222; border: 1px solid #444; color: #fff; padding: 4px; border-radius: 3px; width: 120px; font-size: 0.9em;">
<input type="color" id="inline-username-color" value="{{ user.username_color || '#ffffff' }}" style="width: 25px; height: 25px; padding: 0; border: 1px solid #444; cursor: pointer; background: none;">
@@ -87,7 +87,7 @@
<div class="profile_description" style="position: relative;">
<div id="profile-desc-display" style="display: block; margin-bottom: 5px; margin-top: 5px;">
<span class="desc-text" style="word-break: break-word;">@if(user.description){!! user.description !!}@else<em style="color:var(--text-muted);">No description</em>@endif</span>
@if(session && session.id === user.user_id)
@if(session && session.id === user.user_id && !session.is_anon)
<button id="inline-desc-edit-btn" style="background: transparent; border:none; color: var(--text-muted); cursor: pointer; font-size: 0.7em; padding: 0; margin-left: 5px; display: inline-block; vertical-align: baseline;" title="Edit Description">
<i class="fa-solid fa-pen"></i>
</button>
@@ -379,7 +379,7 @@
@endif
@endif
@if(session && session.id === user.user_id)
@if(session && session.id === user.user_id && !session.is_anon)
<div id="profile-desc-edit-container" class="modal-overlay" style="display: none;">
<div class="modal-content" style="max-width: 600px; width: 90%; text-align: left;">
<h3 style="margin-top: 0; margin-bottom: 15px;">Edit Description</h3>
@@ -392,7 +392,7 @@
</div>
@endif
@if(session && session.id === user.user_id)
@if(session && session.id === user.user_id && !session.is_anon)
<style>
#live-profile-preview-box:not(.edit-mode-active) .avatar-edit-btn,
#live-profile-preview-box:not(.edit-mode-active) #inline-banner-edit-wrapper,