Phase 2 Admin Drawers & Scripts execution: - Extract `AppDrawer`, `InviteDrawer`, `RoleEditorDrawer`, and `GrantDrawer`. - Extract `AdminAppsScript`, `AdminInvitesScript`, `AdminRolesScript`, and `AdminUserDetailsScript`. - Hook extracted components into their respective pages. - Format `AdminRolesPage.tsx` and all modified files using `deno fmt`. Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
212 lines
9.3 KiB
TypeScript
212 lines
9.3 KiB
TypeScript
export const AdminInvitesScript = ({ allRoles }: { allRoles: any[] }) => {
|
|
return (
|
|
<script
|
|
dangerouslySetInnerHTML={{
|
|
__html: `
|
|
const ROLES_CATALOG = ${JSON.stringify(allRoles)};
|
|
|
|
function updateInviteRoleOptions() {
|
|
const appSelect = document.getElementById('inviteAppId');
|
|
const roleSelect = document.getElementById('inviteRole');
|
|
if (!appSelect || !roleSelect) return;
|
|
|
|
const appId = appSelect.value;
|
|
roleSelect.innerHTML = '';
|
|
|
|
const available = ROLES_CATALOG.filter(r => !r.app_id || r.app_id === appId);
|
|
if (available.length === 0) {
|
|
const opt = document.createElement('option');
|
|
opt.value = 'user';
|
|
opt.textContent = 'user';
|
|
roleSelect.appendChild(opt);
|
|
return;
|
|
}
|
|
|
|
available.forEach(r => {
|
|
const opt = document.createElement('option');
|
|
opt.value = r.name;
|
|
opt.textContent = r.name + (r.app_id ? ' (App Custom)' : ' (Global)');
|
|
roleSelect.appendChild(opt);
|
|
});
|
|
}
|
|
|
|
if (document.getElementById('inviteAppId')) {
|
|
updateInviteRoleOptions();
|
|
}
|
|
|
|
function showNotice(msg, isError) {
|
|
const banner = document.getElementById('status-banner');
|
|
banner.textContent = msg;
|
|
banner.style.display = 'block';
|
|
banner.style.background = isError ? 'var(--danger-bg)' : 'var(--success-bg)';
|
|
banner.style.color = isError ? 'var(--danger-text)' : 'var(--success-text)';
|
|
banner.style.border = isError ? '1px solid var(--danger-border)' : '1px solid var(--success-border)';
|
|
setTimeout(() => { banner.style.display = 'none'; }, 5000);
|
|
}
|
|
|
|
function toggleCreateInviteForm() {
|
|
const el = document.getElementById('createInviteModal');
|
|
el.style.display = el.style.display === 'none' ? 'flex' : 'none';
|
|
if (el.style.display === 'flex') {
|
|
updateInviteRoleOptions();
|
|
}
|
|
}
|
|
|
|
function handleInviteTypeChange() {
|
|
const type = document.getElementById('inviteType').value;
|
|
const appContainer = document.getElementById('appSelectContainer');
|
|
const roleContainer = document.getElementById('roleSelectContainer');
|
|
|
|
if (type === 'global_admin' || type === 'open_pending') {
|
|
appContainer.style.display = 'none';
|
|
roleContainer.style.display = 'none';
|
|
} else {
|
|
appContainer.style.display = 'block';
|
|
roleContainer.style.display = 'block';
|
|
updateInviteRoleOptions();
|
|
}
|
|
}
|
|
|
|
function handleUsageTypeChange() {
|
|
const usage = document.getElementById('inviteUsageType').value;
|
|
const maxUsesContainer = document.getElementById('maxUsesContainer');
|
|
maxUsesContainer.style.display = usage === 'limited' ? 'block' : 'none';
|
|
}
|
|
|
|
function filterInvitesList() {
|
|
const query = (document.getElementById('inviteSearchInput')?.value || '').toLowerCase().trim();
|
|
document.querySelectorAll('.invite-row').forEach(r => {
|
|
const text = r.getAttribute('data-search') || '';
|
|
r.style.display = text.includes(query) ? '' : 'none';
|
|
});
|
|
document.querySelectorAll('.invite-card').forEach(c => {
|
|
const text = c.getAttribute('data-search') || '';
|
|
c.style.display = text.includes(query) ? '' : 'none';
|
|
});
|
|
}
|
|
|
|
async function handleCreateInvite(e) {
|
|
e.preventDefault();
|
|
const type = document.getElementById('inviteType').value;
|
|
let appId = null;
|
|
let role = 'user';
|
|
|
|
if (type === 'global_admin') {
|
|
role = 'admin';
|
|
} else if (type === 'open_pending') {
|
|
role = 'user';
|
|
} else {
|
|
appId = document.getElementById('inviteAppId').value;
|
|
role = document.getElementById('inviteRole').value;
|
|
}
|
|
|
|
const usageLimitType = document.getElementById('inviteUsageType').value;
|
|
const maxUses = usageLimitType === 'limited' ? parseInt(document.getElementById('inviteMaxUses').value) || 5 : null;
|
|
const autoActivate = document.getElementById('inviteAutoActivate').checked;
|
|
const expiresInDays = parseInt(document.getElementById('inviteExpiresInDays').value) || 7;
|
|
const customCode = document.getElementById('inviteCustomCode').value.trim();
|
|
|
|
try {
|
|
const res = await fetch('/api/admin/invites/create', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
appId,
|
|
role,
|
|
usageLimitType,
|
|
maxUses,
|
|
autoActivate,
|
|
expiresInDays,
|
|
customCode: customCode || undefined,
|
|
}),
|
|
});
|
|
const data = await res.json();
|
|
if (res.ok) {
|
|
const regUrl = window.location.origin + '/register?code=' + data.inviteCode;
|
|
document.getElementById('generatedTokenUrl').textContent = regUrl;
|
|
document.getElementById('generated-token-banner').style.display = 'block';
|
|
showNotice('Invite token created successfully!', false);
|
|
setTimeout(() => { window.location.reload(); }, 2500);
|
|
} else {
|
|
showNotice(data.error || 'Failed to create invite token', true);
|
|
}
|
|
} catch (err) {
|
|
showNotice('Network error creating invite', true);
|
|
}
|
|
}
|
|
|
|
function copyGeneratedTokenUrl() {
|
|
const text = document.getElementById('generatedTokenUrl').textContent;
|
|
navigator.clipboard.writeText(text);
|
|
showNotice('Registration URL copied to clipboard!', false);
|
|
}
|
|
|
|
function copyInviteLink(code) {
|
|
const url = window.location.origin + '/register?code=' + code;
|
|
navigator.clipboard.writeText(url);
|
|
showNotice('Registration link copied: ' + url, false);
|
|
}
|
|
|
|
async function revokeInvite(inviteId, code) {
|
|
if (!confirm('Revoke invite code "' + code + '"?')) return;
|
|
try {
|
|
const res = await fetch('/api/admin/invites/' + inviteId, {
|
|
method: 'DELETE',
|
|
});
|
|
if (res.ok) {
|
|
showNotice('Invite token revoked', false);
|
|
setTimeout(() => window.location.reload(), 600);
|
|
} else {
|
|
const data = await res.json();
|
|
showNotice(data.error || 'Failed to revoke invite', true);
|
|
}
|
|
} catch (err) {
|
|
showNotice('Network error', true);
|
|
}
|
|
}
|
|
|
|
async function showRedemptionsModal(inviteId, code) {
|
|
const modal = document.getElementById('redemptions-modal');
|
|
const codeEl = document.getElementById('modal-invite-code');
|
|
const contentEl = document.getElementById('modal-redemptions-content');
|
|
|
|
codeEl.textContent = code;
|
|
contentEl.innerHTML = '<p style="color: var(--text-muted);">Loading...</p>';
|
|
modal.style.display = 'flex';
|
|
|
|
try {
|
|
const res = await fetch('/api/admin/invites/' + inviteId + '/redemptions');
|
|
const data = await res.json();
|
|
if (res.ok && data.redemptions && data.redemptions.length > 0) {
|
|
let html = '<table style="width: 100%; border-collapse: collapse; font-size: 0.85rem;">';
|
|
html += '<thead><tr>';
|
|
html += '<th style="padding: 0.5rem;">Username</th>';
|
|
html += '<th style="padding: 0.5rem;">Status</th>';
|
|
html += '<th style="padding: 0.5rem;">Redeemed At</th>';
|
|
html += '</tr></thead><tbody>';
|
|
data.redemptions.forEach(r => {
|
|
html += '<tr>';
|
|
html += '<td style="padding: 0.5rem;"><strong style="color: var(--text-primary); font-family: monospace;">@' + r.username + '</strong></td>';
|
|
html += '<td style="padding: 0.5rem;"><span class="badge badge-' + (r.account_status === 'active' ? 'success' : 'warning') + '">' + r.account_status + '</span></td>';
|
|
html += '<td style="padding: 0.5rem; color: var(--text-secondary);">' + new Date(r.redeemed_at).toLocaleString() + '</td>';
|
|
html += '</tr>';
|
|
});
|
|
html += '</tbody></table>';
|
|
contentEl.innerHTML = html;
|
|
} else {
|
|
contentEl.innerHTML = '<p style="color: var(--text-muted); text-align: center; padding: 1rem;">No users have redeemed this token yet.</p>';
|
|
}
|
|
} catch (err) {
|
|
contentEl.innerHTML = '<p style="color: var(--danger);">Failed to load redemption details.</p>';
|
|
}
|
|
}
|
|
|
|
function closeRedemptionsModal() {
|
|
document.getElementById('redemptions-modal').style.display = 'none';
|
|
}
|
|
`,
|
|
}}
|
|
/>
|
|
);
|
|
};
|