This commit is contained in:
2026-09-29 04:35:35 +02:00
parent 73c0659d4a
commit 6c813143eb
51 changed files with 3708 additions and 743 deletions
+166 -9
View File
@@ -8,6 +8,34 @@
<button id="generate-token" class="btn-upload" style="width: auto; padding: 10px 20px;">Generate New Token</button>
</div>
<!-- ═══════════════════ Invite Requests ═══════════════════ -->
<div id="invite-requests" style="margin-bottom: 30px; scroll-margin-top: calc(var(--navbar-h, 50px) + 16px);">
<h3 style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px;">
<i class="fa-solid fa-inbox" style="color: var(--accent);"></i> Invite Requests
<span id="invite-req-count" style="display: none; background: #ff6b6b; color: white; font-size: 0.7em; padding: 2px 8px; border-radius: 10px; font-weight: 600;"></span>
</h3>
<div class="upload-form">
<table class="responsive-table">
<thead>
<tr>
<th>User</th>
<th>Reason</th>
<th>Status</th>
<th>Submitted</th>
<th>Reviewed By</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="invite-req-list">
<tr><td colspan="6" style="text-align: center; color: var(--text-muted);">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
<h3 style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px;">
<i class="fa-solid fa-ticket" style="color: var(--accent);"></i> Token List
</h3>
<div class="upload-form">
<table class="responsive-table">
<thead>
@@ -29,14 +57,137 @@
<script>
window.f0ckDebug = window.f0ckDebug || (() => {});
const loadTokens = async () => {
// ═══════════════════ Invite Requests ═══════════════════
window.loadInviteRequests = async () => {
const tbody = document.getElementById('invite-req-list');
try {
const res = await fetch('/api/v2/admin/invite-requests');
const data = await res.json();
if (data.success) {
const countBadge = document.getElementById('invite-req-count');
if (!tbody) return;
const pendingCount = data.requests.filter(r => r.status === 'pending').length;
if (countBadge) {
if (pendingCount > 0) {
countBadge.textContent = pendingCount;
countBadge.style.display = '';
} else {
countBadge.style.display = 'none';
}
}
if (data.requests.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; color: var(--text-muted);">No invite requests yet.</td></tr>';
return;
}
tbody.innerHTML = data.requests.map(r => {
const reason = r.reason ? '<span title="' + r.reason.replace(/"/g, '&quot;') + '">' + (r.reason.length > 60 ? r.reason.slice(0, 60) + '…' : r.reason) + '</span>' : '<span style="color: var(--text-muted); font-style: italic;">No reason given</span>';
let statusBadge;
if (r.status === 'pending') {
statusBadge = '<span style="color: #ffc107; font-weight: 600;"><i class="fa-solid fa-clock"></i> Pending</span>';
} else if (r.status === 'approved') {
statusBadge = '<span style="color: #51cf66; font-weight: 600;"><i class="fa-solid fa-circle-check"></i> Approved</span>';
} else if (r.status === 'revoked') {
statusBadge = '<span style="color: #e67700; font-weight: 600;"><i class="fa-solid fa-ban"></i> Revoked</span>';
} else {
statusBadge = '<span style="color: #ff6b6b; font-weight: 600;"><i class="fa-solid fa-circle-xmark"></i> Denied</span>';
}
let actions = '';
if (r.status === 'pending') {
actions =
'<div style="display: flex; gap: 6px; flex-wrap: wrap;">' +
'<button onclick="approveInviteReq(' + r.id + ')" class="btn-upload" style="padding: 5px 12px; font-size: 0.8em; background: #51cf66; border-color: #51cf66;"><i class="fa-solid fa-check"></i> Approve</button>' +
'<button onclick="denyInviteReq(' + r.id + ')" class="btn-remove" style="padding: 5px 12px; font-size: 0.8em;"><i class="fa-solid fa-xmark"></i> Deny</button>' +
'</div>';
} else {
actions = '<span style="color: var(--text-muted); font-size: 0.85em;">' + (r.reviewed_at ? new Date(r.reviewed_at).toLocaleString() : '') + '</span>';
}
const username = r.requester_login || r.requester_name || (r.fingerprint ? r.fingerprint.slice(0, 12) + '…' : '—');
const userCell = r.requester_login
? '<a href="/user/' + r.requester_login + '" style="color: #5bc0be; text-decoration: none; font-weight: 600;">' + username + '</a>'
: '<span style="color: var(--text-muted);">' + username + '</span>';
return '<tr' + (r.status === 'pending' ? ' style="background: rgba(255,193,7,0.04);"' : '') + '>' +
'<td data-label="User">' + userCell + '</td>' +
'<td data-label="Reason" style="max-width: 250px;">' + reason + '</td>' +
'<td data-label="Status">' + statusBadge + '</td>' +
'<td data-label="Submitted">' + new Date(r.created_at).toLocaleString() + '</td>' +
'<td data-label="Reviewed By">' + (r.reviewed_by_name || '—') + '</td>' +
'<td data-label="Actions">' + actions + '</td>' +
'</tr>';
}).join('');
} else {
if (tbody) tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; color: var(--text-muted);">Failed to load invite requests.</td></tr>';
}
} catch (e) {
console.error('Failed to load invite requests:', e);
if (tbody) tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; color: var(--text-muted);">Failed to load invite requests.</td></tr>';
}
};
window.approveInviteReq = async (id) => {
if (!confirm('Approve this invite request? A token will be generated and sent to the user.')) return;
try {
const res = await fetch('/api/v2/admin/invite-requests/approve', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': window.f0ckSession?.csrf_token
},
body: JSON.stringify({ id })
});
const data = await res.json();
if (data.success) {
window.loadInviteRequests();
window.loadTokens();
} else {
alert('Failed: ' + (data.msg || 'Unknown error'));
}
} catch (e) {
console.error(e);
alert('Error: ' + e.message);
}
};
window.denyInviteReq = async (id) => {
if (!confirm('Deny this invite request?')) return;
try {
const res = await fetch('/api/v2/admin/invite-requests/deny', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': window.f0ckSession?.csrf_token
},
body: JSON.stringify({ id })
});
const data = await res.json();
if (data.success) {
window.loadInviteRequests();
} else {
alert('Failed: ' + (data.msg || 'Unknown error'));
}
} catch (e) {
console.error(e);
alert('Error: ' + e.message);
}
};
// ═══════════════════ Tokens ═══════════════════
window.loadTokens = async () => {
const tbody = document.getElementById('token-list');
try {
window.f0ckDebug('Loading tokens...');
const res = await fetch('/api/v2/admin/tokens');
const data = await res.json();
window.f0ckDebug('Tokens data:', data);
if (data.success) {
const tbody = document.getElementById('token-list');
if (!tbody) return;
tbody.innerHTML = data.tokens.map(t =>
'<tr>' +
@@ -57,11 +208,16 @@
'</td>' +
'</tr>'
).join('');
} else {
if (tbody) tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; color: var(--text-muted);">Failed to load tokens.</td></tr>';
}
} catch (e) { console.error(e); }
} catch (e) {
console.error(e);
if (tbody) tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; color: var(--text-muted);">Failed to load tokens.</td></tr>';
}
};
const generateToken = async () => {
window.generateToken = async () => {
window.f0ckDebug('Generating...');
try {
const res = await fetch('/api/v2/admin/tokens/create', {
@@ -71,7 +227,7 @@
const data = await res.json();
window.f0ckDebug('Gen result:', data);
if (data.success) {
loadTokens();
window.loadTokens();
} else {
alert('Failed: ' + data.msg);
}
@@ -81,7 +237,7 @@
}
};
const deleteToken = async (id) => {
window.deleteToken = async (id) => {
if (!confirm('Delete this token?')) return;
const res = await fetch('/api/v2/admin/tokens/delete', {
method: 'POST',
@@ -93,13 +249,14 @@
});
const data = await res.json();
if (data.success) {
loadTokens();
window.loadTokens();
}
};
const btnGenToken = document.getElementById('generate-token');
if (btnGenToken) btnGenToken.addEventListener('click', generateToken);
loadTokens();
if (btnGenToken) btnGenToken.addEventListener('click', window.generateToken);
window.loadTokens();
window.loadInviteRequests();
</script>
</div>
</div>
+3 -1
View File
@@ -43,6 +43,7 @@
.status-active { background: linear-gradient(135deg, #28a745, #20c997); color: #fff; }
.status-pending { background: linear-gradient(135deg, #f08c00, #ffc107); color: #000; }
.status-banned { background: linear-gradient(135deg, #e03131, #f03e3e); color: #fff; }
.status-system { background: #5c0a0a; color: #ffb3b3; border: 1px solid #8b1a1a; }
.method-tag {
font-size: 0.7rem;
@@ -329,7 +330,8 @@
</div>
</div>
<div style="padding: 0 15px;">
<!-- .container: the admin skin's 1200px column, same width and gutters as the back bar above -->
<div class="container">
<div style="display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 25px; gap: 20px; flex-wrap: wrap;">
<div>
<h2 style="margin: 0; font-weight: 800; letter-spacing: -0.5px;">User Management</h2>
+3
View File
@@ -34,6 +34,9 @@
<td data-label="Status" id="status-cell-{{ u.id || 'ghost-' + u.login }}">
@if(!u.id)
<span class="status-badge" style="background: rgba(255,255,255,0.05); color: #888; border: 1px dashed rgba(255,255,255,0.1);">Legacy</span>
@elseif(u.login === 'deleted_user')
{{-- System ghost that deleted users' content is reassigned to; it is banned only so it can't log in --}}
<span class="status-badge status-system" title="System account: holds content of deleted users">System</span>
@else
@if(u.banned)
<span class="status-badge status-banned">Banned</span>