gfds
This commit is contained in:
+166
-9
@@ -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, '"') + '">' + (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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user