auth-yes/ui/components/admin/AdminInvitesScript.tsx
google-labs-jules[bot] 65f59521c0 feat(ui): decompose Admin UI with separate Drawers and Scripts
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>
2026-08-26 18:37:15 +00:00

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';
}
`,
}}
/>
);
};