chore: clean up temporary python and text scratch files from repo root
This commit is contained in:
parent
4c7d8d4af7
commit
54c98c1e4a
352
dump_invites.txt
352
dump_invites.txt
@ -1,352 +0,0 @@
|
||||
|
||||
{/* Desktop Ledger Table (≥ 768px) */}
|
||||
<div class="card desktop-only" style="display: none;">
|
||||
<div class="table-container">
|
||||
<table id="invitesTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Invite Code</th>
|
||||
<th>Target App / Scope</th>
|
||||
<th>Role</th>
|
||||
<th>Capacity & Usage</th>
|
||||
<th>Status</th>
|
||||
<th>Expires</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{invites.length === 0
|
||||
? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={7}
|
||||
style="text-align: center; color: var(--text-muted); padding: 2rem;"
|
||||
>
|
||||
No active or historical invite tokens found.
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
: (
|
||||
invites.map((inv) => {
|
||||
const usesCount = inv.uses_count || 0;
|
||||
const maxUses = inv.max_uses;
|
||||
const isUnlimited = maxUses === null;
|
||||
const isExhausted = !isUnlimited && usesCount >= maxUses;
|
||||
const isExpired = new Date(inv.expires_at) < new Date();
|
||||
const isActive = !isExhausted && !isExpired;
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={inv.id}
|
||||
class="invite-row"
|
||||
data-search={`${inv.code} ${
|
||||
inv.app_name || ""
|
||||
} ${inv.role}`.toLowerCase()}
|
||||
>
|
||||
<td>
|
||||
<code style="background: var(--surface-muted); padding: 0.25rem 0.5rem; border-radius: var(--radius-sm); font-weight: 700; font-family: monospace; color: var(--primary);">
|
||||
{inv.code}
|
||||
</code>
|
||||
</td>
|
||||
<td>
|
||||
{inv.app_name
|
||||
? (
|
||||
<strong style="color: var(--text-primary);">
|
||||
{inv.app_name}
|
||||
</strong>
|
||||
)
|
||||
: inv.role === "admin"
|
||||
? <span class="badge badge-info">Global Admin</span>
|
||||
: (
|
||||
<span class="badge badge-secondary">
|
||||
General (Open)
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-info">{inv.role}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div style="min-width: 110px;">
|
||||
{isUnlimited
|
||||
? (
|
||||
<span style="font-size: 0.85rem; font-weight: 600; color: var(--primary);">
|
||||
{usesCount} claimed (Unlimited)
|
||||
</span>
|
||||
)
|
||||
: (
|
||||
<div>
|
||||
<span style="font-size: 0.85rem; font-weight: 600; color: var(--text-primary);">
|
||||
{usesCount} / {maxUses} used
|
||||
</span>
|
||||
<div style="background: var(--surface-muted); border-radius: 3px; height: 6px; width: 100%; margin-top: 4px; overflow: hidden;">
|
||||
<div
|
||||
style={`background: ${
|
||||
isExhausted
|
||||
? "var(--text-muted)"
|
||||
: "var(--success)"
|
||||
}; height: 100%; width: ${
|
||||
Math.min(
|
||||
100,
|
||||
(usesCount / maxUses) * 100,
|
||||
)
|
||||
}%;`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{isExhausted && (
|
||||
<span class="badge badge-secondary">Exhausted</span>
|
||||
)}
|
||||
{isExpired && !isExhausted && (
|
||||
<span class="badge badge-danger">Expired</span>
|
||||
)}
|
||||
{isActive && (
|
||||
<span class="badge badge-success">Active</span>
|
||||
)}
|
||||
</td>
|
||||
<td style="font-size: 0.85rem; color: var(--text-secondary);">
|
||||
{new Date(inv.expires_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td>
|
||||
<div style="display: flex; gap: 0.35rem; flex-wrap: wrap;">
|
||||
{isActive && (
|
||||
<button
|
||||
type="button"
|
||||
class="btn-primary"
|
||||
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
|
||||
onclick={`copyInviteLink('${inv.code}')`}
|
||||
>
|
||||
Copy Link
|
||||
</button>
|
||||
)}
|
||||
{usesCount > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline"
|
||||
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
|
||||
onclick={`showRedemptionsModal('${inv.id}', '${inv.code}')`}
|
||||
>
|
||||
Claimed ({usesCount})
|
||||
</button>
|
||||
)}
|
||||
{isActive && (
|
||||
<button
|
||||
type="button"
|
||||
class="btn-danger"
|
||||
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
|
||||
onclick={`revokeInvite('${inv.id}', '${inv.code}')`}
|
||||
>
|
||||
Revoke
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Adaptive Cards View (< 768px) */}
|
||||
<div
|
||||
id="invitesMobileDeck"
|
||||
class="mobile-only"
|
||||
style="display: flex; flex-direction: column; gap: 0.75rem;"
|
||||
>
|
||||
{invites.map((inv) => {
|
||||
const usesCount = inv.uses_count || 0;
|
||||
const maxUses = inv.max_uses;
|
||||
const isUnlimited = maxUses === null;
|
||||
const isExhausted = !isUnlimited && usesCount >= maxUses;
|
||||
const isExpired = new Date(inv.expires_at) < new Date();
|
||||
const isActive = !isExhausted && !isExpired;
|
||||
|
||||
return (
|
||||
<div
|
||||
class="card invite-card"
|
||||
key={inv.id}
|
||||
data-search={`${inv.code} ${inv.app_name || ""} ${inv.role}`
|
||||
.toLowerCase()}
|
||||
style="margin-bottom: 0; padding: 1rem;"
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;">
|
||||
<div>
|
||||
<code style="background: var(--surface-muted); padding: 0.25rem 0.5rem; border-radius: var(--radius-sm); font-weight: 700; font-family: monospace; font-size: 1rem; color: var(--primary);">
|
||||
{inv.code}
|
||||
</code>
|
||||
<div style="font-size: 0.8rem; color: var(--text-muted); margin-top: 0.25rem;">
|
||||
{inv.app_name || "Global / Open"} • Role:{" "}
|
||||
<strong>{inv.role}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExhausted && (
|
||||
<span class="badge badge-secondary">Exhausted</span>
|
||||
)}
|
||||
{isExpired && !isExhausted && (
|
||||
<span class="badge badge-danger">Expired</span>
|
||||
)}
|
||||
{isActive && <span class="badge badge-success">Active</span>}
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 0.75rem;">
|
||||
<div style="font-size: 0.8rem; color: var(--text-secondary); display: flex; justify-content: space-between; margin-bottom: 0.25rem;">
|
||||
<span>
|
||||
Capacity:{" "}
|
||||
{isUnlimited ? "Unlimited" : `${usesCount} / ${maxUses}`}
|
||||
</span>
|
||||
<span>
|
||||
Expires: {new Date(inv.expires_at).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
{!isUnlimited && (
|
||||
<div style="background: var(--surface-muted); border-radius: 3px; height: 6px; width: 100%; overflow: hidden;">
|
||||
<div
|
||||
style={`background: ${
|
||||
isExhausted ? "var(--text-muted)" : "var(--success)"
|
||||
}; height: 100%; width: ${
|
||||
Math.min(100, (usesCount / maxUses) * 100)
|
||||
}%;`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
{isActive && (
|
||||
<button
|
||||
type="button"
|
||||
class="btn-primary"
|
||||
style="flex: 1; justify-content: center; min-height: 38px; font-size: 0.85rem;"
|
||||
onclick={`copyInviteLink('${inv.code}')`}
|
||||
>
|
||||
Copy Link
|
||||
</button>
|
||||
)}
|
||||
{usesCount > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline"
|
||||
style="flex: 1; justify-content: center; min-height: 38px; font-size: 0.85rem;"
|
||||
onclick={`showRedemptionsModal('${inv.id}', '${inv.code}')`}
|
||||
>
|
||||
Claimed ({usesCount})
|
||||
</button>
|
||||
)}
|
||||
{isActive && (
|
||||
<button
|
||||
type="button"
|
||||
class="btn-danger"
|
||||
style="flex: 1; justify-content: center; min-height: 38px; font-size: 0.85rem;"
|
||||
onclick={`revokeInvite('${inv.id}', '${inv.code}')`}
|
||||
>
|
||||
Revoke
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Redemptions Modal */}
|
||||
<div
|
||||
id="redemptions-modal"
|
||||
style="display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.6); z-index: 9999; justify-content: center; align-items: center;"
|
||||
>
|
||||
<div style="background: var(--surface-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); width: 90%; max-width: 550px; padding: 1.5rem; box-shadow: var(--shadow-lg);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||
<h3 style="margin: 0; font-size: 1.1rem; color: var(--text-primary);">
|
||||
Users Claimed:{" "}
|
||||
<code id="modal-invite-code" style="color: var(--primary);">
|
||||
</code>
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
onclick="closeRedemptionsModal()"
|
||||
style="background: none; border: none; font-size: 1.2rem; cursor: pointer; color: var(--text-muted);"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="modal-redemptions-content"
|
||||
style="max-height: 350px; overflow-y: auto;"
|
||||
>
|
||||
<p style="color: var(--text-muted); font-size: 0.9rem;">
|
||||
Loading claimed users...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: right; margin-top: 1rem;">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline"
|
||||
onclick="closeRedemptionsModal()"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
{`
|
||||
@media (min-width: 768px) {
|
||||
.desktop-only { display: block !important; }
|
||||
.mobile-only { display: none !important; }
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.desktop-only { display: none !important; }
|
||||
.mobile-only { display: flex !important; }
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<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)';
|
||||
132
dump_roles.txt
132
dump_roles.txt
@ -1,132 +0,0 @@
|
||||
onclick="closeRoleDrawer()"
|
||||
style="min-height: 42px;"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AdminModal>
|
||||
|
||||
<div class="card">
|
||||
{/* Instant Search and Scope Filters */}
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.25rem; flex-wrap: wrap; gap: 0.75rem;">
|
||||
<div style="display: flex; gap: 0.75rem; align-items: center; flex-wrap: wrap; flex: 1;">
|
||||
{/* Search */}
|
||||
<div style="position: relative; min-width: 200px; max-width: 300px; width: 100%;">
|
||||
<input
|
||||
type="text"
|
||||
id="roleSearchInput"
|
||||
placeholder="Search roles..."
|
||||
oninput="filterRoles()"
|
||||
style="width: 100%; padding: 0.45rem 0.85rem 0.45rem 2.1rem; font-size: 0.85rem;"
|
||||
/>
|
||||
<svg
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
style="position: absolute; left: 0.7rem; top: 50%; transform: translateY(-50%); color: var(--text-muted); pointer-events: none;"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Scope dropdown */}
|
||||
<div style="display: flex; gap: 0.4rem; align-items: center;">
|
||||
<label style="font-weight: 600; font-size: 0.85rem; color: var(--text-secondary); white-space: nowrap;">
|
||||
Scope:
|
||||
</label>
|
||||
<select
|
||||
id="filterScopeSelect"
|
||||
onchange="filterRoles()"
|
||||
style="padding: 0.4rem 0.75rem; font-size: 0.85rem;"
|
||||
>
|
||||
<option value="all">All Roles</option>
|
||||
<option value="global">Global (Shared) Only</option>
|
||||
{apps.map((app) => (
|
||||
<option value={app.id}>
|
||||
{app.name} Only
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span
|
||||
id="roleCountDisplay"
|
||||
style="font-size: 0.85rem; color: var(--text-muted);"
|
||||
>
|
||||
Showing {roles.length} roles
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Desktop Table View (≥ 768px) */}
|
||||
<div class="table-container desktop-only" style="display: none;">
|
||||
<table id="rolesTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Role Identifier</th>
|
||||
<th>Scope</th>
|
||||
<th>Description</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{roles.length === 0
|
||||
? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={5}
|
||||
style="text-align: center; color: var(--text-muted); padding: 2rem;"
|
||||
>
|
||||
No roles found.
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
: (
|
||||
roles.map((r) => {
|
||||
const isGlobal = !r.app_id;
|
||||
const isCoreAdmin = isGlobal && r.name === "admin";
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={r.id}
|
||||
class="role-row"
|
||||
data-app-id={r.app_id || "global"}
|
||||
data-search={`${r.name} ${r.description || ""} ${
|
||||
isGlobal ? "global" : r.app_name || ""
|
||||
}`.toLowerCase()}
|
||||
>
|
||||
<td>
|
||||
<strong style="font-family: monospace; font-size: 0.95rem; color: var(--text-primary);">
|
||||
{r.name}
|
||||
</strong>
|
||||
</td>
|
||||
<td>
|
||||
{isGlobal
|
||||
? (
|
||||
<span class="badge badge-info">
|
||||
Global (Shared)
|
||||
</span>
|
||||
)
|
||||
: (
|
||||
<span class="badge badge-warning">
|
||||
{r.app_name || "App-Specific"}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td style="color: var(--text-secondary); font-size: 0.85rem;">
|
||||
{r.description || "-"}
|
||||
</td>
|
||||
<td style="font-size: 0.85rem; color: var(--text-secondary);">
|
||||
{new Date(r.created_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td>
|
||||
{!isCoreAdmin
|
||||
? (
|
||||
<div style="display: flex; gap: 0.35rem;">
|
||||
<button
|
||||
@ -1,8 +0,0 @@
|
||||
with open('ui/components/AdminRolesPage.tsx', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
import_str = """import { AdminTable } from "./admin/AdminTable.tsx";"""
|
||||
content = content.replace(import_str, """import { AdminTable as _AdminTable } from "./admin/AdminTable.tsx";""")
|
||||
|
||||
with open('ui/components/AdminRolesPage.tsx', 'w') as f:
|
||||
f.write(content)
|
||||
@ -1,246 +0,0 @@
|
||||
import re
|
||||
|
||||
with open('ui/components/AdminInvitesPage.tsx', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
start_marker = "{/* Desktop Ledger Table (≥ 768px) */}"
|
||||
end_marker = " <style>"
|
||||
start_idx = content.find(start_marker)
|
||||
end_idx = content.find(end_marker)
|
||||
|
||||
if start_idx != -1 and end_idx != -1:
|
||||
new_content = content[:start_idx] + """<AdminTable
|
||||
id="invitesTable"
|
||||
headers={[
|
||||
"Invite Code",
|
||||
"Target App / Scope",
|
||||
"Role",
|
||||
"Capacity & Usage",
|
||||
"Status",
|
||||
"Expires",
|
||||
"Actions",
|
||||
]}
|
||||
isEmpty={invites.length === 0}
|
||||
emptyState="No active or historical invite tokens found."
|
||||
desktopRows={invites.map((inv) => {
|
||||
const usesCount = inv.uses_count || 0;
|
||||
const maxUses = inv.max_uses;
|
||||
const isUnlimited = maxUses === null;
|
||||
const isExhausted = !isUnlimited && usesCount >= maxUses;
|
||||
const isExpired = new Date(inv.expires_at) < new Date();
|
||||
const isActive = !isExhausted && !isExpired;
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={inv.id}
|
||||
class="invite-row"
|
||||
data-search={`${inv.code} ${
|
||||
inv.app_name || ""
|
||||
} ${inv.role}`.toLowerCase()}
|
||||
>
|
||||
<td>
|
||||
<code style="background: var(--surface-muted); padding: 0.25rem 0.5rem; border-radius: var(--radius-sm); font-weight: 700; font-family: monospace; color: var(--primary);">
|
||||
{inv.code}
|
||||
</code>
|
||||
</td>
|
||||
<td style="font-size: 0.85rem;">
|
||||
{inv.app_name || (
|
||||
<span style="color: var(--text-muted); font-style: italic;">
|
||||
Global / Open
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td style="font-family: monospace; font-size: 0.85rem; color: var(--text-secondary);">
|
||||
{inv.role}
|
||||
</td>
|
||||
<td style="font-size: 0.85rem;">
|
||||
{isUnlimited
|
||||
? "Unlimited"
|
||||
: `${usesCount} / ${maxUses}`}
|
||||
</td>
|
||||
<td>
|
||||
{isExhausted && (
|
||||
<span class="badge badge-secondary">Exhausted</span>
|
||||
)}
|
||||
{isExpired && !isExhausted && (
|
||||
<span class="badge badge-danger">Expired</span>
|
||||
)}
|
||||
{isActive && (
|
||||
<span class="badge badge-success">Active</span>
|
||||
)}
|
||||
</td>
|
||||
<td style="font-size: 0.85rem; color: var(--text-secondary);">
|
||||
{new Date(inv.expires_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td>
|
||||
<div style="display: flex; gap: 0.35rem; flex-wrap: wrap;">
|
||||
{isActive && (
|
||||
<button
|
||||
type="button"
|
||||
class="btn-primary"
|
||||
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
|
||||
onclick={`copyInviteLink('${inv.code}')`}
|
||||
>
|
||||
Copy Link
|
||||
</button>
|
||||
)}
|
||||
{usesCount > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline"
|
||||
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
|
||||
onclick={`showRedemptionsModal('${inv.id}', '${inv.code}')`}
|
||||
>
|
||||
Claimed ({usesCount})
|
||||
</button>
|
||||
)}
|
||||
{isActive && (
|
||||
<button
|
||||
type="button"
|
||||
class="btn-danger"
|
||||
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
|
||||
onclick={`revokeInvite('${inv.id}', '${inv.code}')`}
|
||||
>
|
||||
Revoke
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
mobileCards={invites.map((inv) => {
|
||||
const usesCount = inv.uses_count || 0;
|
||||
const maxUses = inv.max_uses;
|
||||
const isUnlimited = maxUses === null;
|
||||
const isExhausted = !isUnlimited && usesCount >= maxUses;
|
||||
const isExpired = new Date(inv.expires_at) < new Date();
|
||||
const isActive = !isExhausted && !isExpired;
|
||||
|
||||
return (
|
||||
<div
|
||||
class="card invite-card"
|
||||
key={inv.id}
|
||||
data-search={`${inv.code} ${inv.app_name || ""} ${inv.role}`
|
||||
.toLowerCase()}
|
||||
style="margin-bottom: 0; padding: 1rem;"
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;">
|
||||
<div>
|
||||
<code style="background: var(--surface-muted); padding: 0.25rem 0.5rem; border-radius: var(--radius-sm); font-weight: 700; font-family: monospace; font-size: 1rem; color: var(--primary);">
|
||||
{inv.code}
|
||||
</code>
|
||||
<div style="font-size: 0.8rem; color: var(--text-muted); margin-top: 0.25rem;">
|
||||
{inv.app_name || "Global / Open"} • Role:{" "}
|
||||
<strong>{inv.role}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExhausted && (
|
||||
<span class="badge badge-secondary">Exhausted</span>
|
||||
)}
|
||||
{isExpired && !isExhausted && (
|
||||
<span class="badge badge-danger">Expired</span>
|
||||
)}
|
||||
{isActive && <span class="badge badge-success">Active</span>}
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 0.75rem;">
|
||||
<div style="font-size: 0.8rem; color: var(--text-secondary); display: flex; justify-content: space-between; margin-bottom: 0.25rem;">
|
||||
<span>
|
||||
Capacity:{" "}
|
||||
{isUnlimited ? "Unlimited" : `${usesCount} / ${maxUses}`}
|
||||
</span>
|
||||
<span>
|
||||
Expires: {new Date(inv.expires_at).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
{!isUnlimited && (
|
||||
<div style="background: var(--surface-muted); border-radius: 3px; height: 6px; width: 100%; overflow: hidden;">
|
||||
<div
|
||||
style={`background: ${
|
||||
isExhausted ? "var(--text-muted)" : "var(--success)"
|
||||
}; height: 100%; width: ${
|
||||
Math.min(100, (usesCount / maxUses) * 100)
|
||||
}%;`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
{isActive && (
|
||||
<button
|
||||
type="button"
|
||||
class="btn-primary"
|
||||
style="flex: 1; justify-content: center; min-height: 38px; font-size: 0.85rem;"
|
||||
onclick={`copyInviteLink('${inv.code}')`}
|
||||
>
|
||||
Copy Link
|
||||
</button>
|
||||
)}
|
||||
{usesCount > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline"
|
||||
style="flex: 1; justify-content: center; min-height: 38px; font-size: 0.85rem;"
|
||||
onclick={`showRedemptionsModal('${inv.id}', '${inv.code}')`}
|
||||
>
|
||||
Claimed ({usesCount})
|
||||
</button>
|
||||
)}
|
||||
{isActive && (
|
||||
<button
|
||||
type="button"
|
||||
class="btn-danger"
|
||||
style="flex: 1; justify-content: center; min-height: 38px; font-size: 0.85rem;"
|
||||
onclick={`revokeInvite('${inv.id}', '${inv.code}')`}
|
||||
>
|
||||
Revoke
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
/>
|
||||
|
||||
{/* Redemptions Modal */}
|
||||
<AdminModal
|
||||
id="redemptions-modal"
|
||||
title={
|
||||
<span>
|
||||
Users Claimed:{" "}
|
||||
<code id="modal-invite-code" style="color: var(--primary);">
|
||||
</code>
|
||||
</span>
|
||||
}
|
||||
onClose="closeRedemptionsModal()"
|
||||
>
|
||||
<div
|
||||
id="modal-redemptions-content"
|
||||
style="max-height: 350px; overflow-y: auto;"
|
||||
>
|
||||
<p style="color: var(--text-muted); font-size: 0.9rem;">
|
||||
Loading claimed users...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: right; margin-top: 1rem;">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline"
|
||||
onclick="closeRedemptionsModal()"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</AdminModal>
|
||||
|
||||
""" + content[end_idx:]
|
||||
|
||||
with open('ui/components/AdminInvitesPage.tsx', 'w') as f:
|
||||
f.write(new_content)
|
||||
print("Replaced!")
|
||||
else:
|
||||
print("Markers not found")
|
||||
568
replace_roles.py
568
replace_roles.py
@ -1,568 +0,0 @@
|
||||
import re
|
||||
|
||||
with open('ui/components/AdminRolesPage.tsx', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
import_str = """import { AdminLayout } from "./AdminLayout.tsx";"""
|
||||
content = content.replace(import_str, """import { AdminLayout } from "./AdminLayout.tsx";\nimport { AdminTable } from "./admin/AdminTable.tsx";\nimport { AdminModal } from "./admin/AdminModal.tsx";""")
|
||||
|
||||
role_form = """ <div
|
||||
id="roleFormCard"
|
||||
class="card"
|
||||
style="display: none; border-left: 4px solid var(--primary); margin-bottom: 1.5rem;"
|
||||
>
|
||||
<h3
|
||||
id="roleFormTitle"
|
||||
style="margin: 0 0 0.5rem 0; color: var(--text-primary);"
|
||||
>
|
||||
Create New Role
|
||||
</h3>
|
||||
<p style="color: var(--text-secondary); font-size: 0.9rem; margin: 0 0 1.25rem 0;">
|
||||
Define a global shared role or an application-scoped custom grant.
|
||||
</p>
|
||||
|
||||
<form id="roleForm" onsubmit="handleSaveRole(event)">
|
||||
<input type="hidden" id="editRoleId" value="" />
|
||||
|
||||
<div
|
||||
id="scopeSelectContainer"
|
||||
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; margin-bottom: 1rem;"
|
||||
>
|
||||
<div>
|
||||
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
|
||||
Scope (Applicability) *
|
||||
</label>
|
||||
<select
|
||||
id="roleScope"
|
||||
onchange="handleScopeChange()"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<option value="global">
|
||||
Global (Shared across ALL applications)
|
||||
</option>
|
||||
<option value="app_specific">
|
||||
Application-Specific (Scoped to single app)
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="appSelectContainer" style="display: none;">
|
||||
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
|
||||
Target Application *
|
||||
</label>
|
||||
<select
|
||||
id="roleAppId"
|
||||
style="width: 100%;"
|
||||
>
|
||||
{apps.map((app) => (
|
||||
<option value={app.id}>
|
||||
{app.name} ({app.spiffe_id})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
|
||||
Role Name (Identifier) *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="roleName"
|
||||
placeholder="e.g. read_only, viewer, admin"
|
||||
required
|
||||
style="width: 100%; font-family: monospace; font-size: 0.9rem;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
|
||||
Description
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="roleDescription"
|
||||
placeholder="e.g. Read-only access to basic features"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 0.75rem;">
|
||||
<button type="submit" class="btn-primary" style="min-height: 42px;">
|
||||
Save Role
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline"
|
||||
onclick="closeRoleForm()"
|
||||
style="min-height: 42px;"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>"""
|
||||
|
||||
role_form_replacement = """ <AdminModal
|
||||
id="roleFormModal"
|
||||
title="Create New Role"
|
||||
onClose="closeRoleForm()"
|
||||
>
|
||||
<p style="color: var(--text-secondary); font-size: 0.9rem; margin: 0 0 1.25rem 0;">
|
||||
Define a global shared role or an application-scoped custom grant.
|
||||
</p>
|
||||
|
||||
<form id="roleForm" onsubmit="handleSaveRole(event)">
|
||||
<input type="hidden" id="editRoleId" value="" />
|
||||
|
||||
<div
|
||||
id="scopeSelectContainer"
|
||||
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; margin-bottom: 1rem;"
|
||||
>
|
||||
<div>
|
||||
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
|
||||
Scope (Applicability) *
|
||||
</label>
|
||||
<select
|
||||
id="roleScope"
|
||||
onchange="handleScopeChange()"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<option value="global">
|
||||
Global (Shared across ALL applications)
|
||||
</option>
|
||||
<option value="app_specific">
|
||||
Application-Specific (Scoped to single app)
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="appSelectContainer" style="display: none;">
|
||||
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
|
||||
Target Application *
|
||||
</label>
|
||||
<select
|
||||
id="roleAppId"
|
||||
style="width: 100%;"
|
||||
>
|
||||
{apps.map((app) => (
|
||||
<option value={app.id}>
|
||||
{app.name} ({app.spiffe_id})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
|
||||
Role Name (Identifier) *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="roleName"
|
||||
placeholder="e.g. read_only, viewer, admin"
|
||||
required
|
||||
style="width: 100%; font-family: monospace; font-size: 0.9rem;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
|
||||
Description
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="roleDescription"
|
||||
placeholder="e.g. Read-only access to basic features"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 0.75rem;">
|
||||
<button type="submit" class="btn-primary" style="min-height: 42px;">
|
||||
Save Role
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline"
|
||||
onclick="closeRoleForm()"
|
||||
style="min-height: 42px;"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AdminModal>"""
|
||||
|
||||
content = content.replace(role_form, role_form_replacement)
|
||||
|
||||
roles_table_desktop_mobile = """ <div class="table-container desktop-only" style="display: none;">
|
||||
<table id="rolesTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Role Identifier</th>
|
||||
<th>Scope</th>
|
||||
<th>Description</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{roles.length === 0
|
||||
? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={5}
|
||||
style="text-align: center; color: var(--text-muted); padding: 2rem;"
|
||||
>
|
||||
No roles found.
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
: (
|
||||
roles.map((r) => {
|
||||
const isGlobal = !r.app_id;
|
||||
return (
|
||||
<tr
|
||||
key={r.id}
|
||||
class="role-row"
|
||||
data-app-id={r.app_id || "global"}
|
||||
data-search={`${r.name} ${r.description || ""} ${
|
||||
isGlobal ? "global" : r.app_name || ""
|
||||
}`.toLowerCase()}
|
||||
>
|
||||
<td>
|
||||
<code style="background: var(--surface-muted); padding: 0.25rem 0.5rem; border-radius: var(--radius-sm); font-size: 0.9rem; font-family: monospace; font-weight: 700; color: var(--primary);">
|
||||
{r.name}
|
||||
</code>
|
||||
</td>
|
||||
<td>
|
||||
{isGlobal
|
||||
? <span class="badge badge-info">Global</span>
|
||||
: (
|
||||
<span class="badge badge-warning">
|
||||
{r.app_name || "App Scoped"}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td style="color: var(--text-secondary); max-width: 250px; font-size: 0.85rem;">
|
||||
{r.description || "-"}
|
||||
</td>
|
||||
<td style="font-size: 0.85rem; color: var(--text-secondary);">
|
||||
{new Date(r.created_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td>
|
||||
<div style="display: flex; gap: 0.35rem;">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline"
|
||||
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
|
||||
onclick={`openEditRoleForm(${
|
||||
JSON.stringify(JSON.stringify(r))
|
||||
})`}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
{r.name !== "admin" && (
|
||||
<button
|
||||
type="button"
|
||||
class="btn-danger"
|
||||
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
|
||||
onclick={`deleteRole('${r.id}', '${r.name}')`}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Mobile View (< 768px) */}
|
||||
<div
|
||||
id="rolesMobileDeck"
|
||||
class="mobile-only"
|
||||
style="display: flex; flex-direction: column; gap: 0.75rem;"
|
||||
>
|
||||
{roles.length === 0
|
||||
? (
|
||||
<div class="card" style="text-align: center; padding: 2rem;">
|
||||
<p style="color: var(--text-muted); margin: 0;">
|
||||
No roles found.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
roles.map((r) => {
|
||||
const isGlobal = !r.app_id;
|
||||
return (
|
||||
<div
|
||||
class="card role-card"
|
||||
key={r.id}
|
||||
data-app-id={r.app_id || "global"}
|
||||
data-search={`${r.name} ${r.description || ""} ${
|
||||
isGlobal ? "global" : r.app_name || ""
|
||||
}`.toLowerCase()}
|
||||
style="margin-bottom: 0; padding: 1rem;"
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;">
|
||||
<strong style="font-family: monospace; font-size: 1rem; color: var(--text-primary);">
|
||||
{r.name}
|
||||
</strong>
|
||||
{isGlobal
|
||||
? <span class="badge badge-info">Global</span>
|
||||
: (
|
||||
<span class="badge badge-warning">
|
||||
{r.app_name || "App Scoped"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 0.75rem;">
|
||||
<p style="margin: 0 0 0.25rem 0;">
|
||||
{r.description || "No description provided."}
|
||||
</p>
|
||||
<small style="color: var(--text-muted);">
|
||||
Created: {new Date(r.created_at).toLocaleDateString()}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline"
|
||||
style="flex: 1; min-height: 38px; font-size: 0.85rem;"
|
||||
onclick={`openEditRoleForm(${
|
||||
JSON.stringify(JSON.stringify(r))
|
||||
})`}
|
||||
>
|
||||
Edit Role
|
||||
</button>
|
||||
{r.name !== "admin" && (
|
||||
<button
|
||||
type="button"
|
||||
class="btn-danger"
|
||||
style="flex: 1; min-height: 38px; font-size: 0.85rem;"
|
||||
onclick={`deleteRole('${r.id}', '${r.name}')`}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>"""
|
||||
|
||||
roles_table_replacement = """ <AdminTable
|
||||
id="rolesTable"
|
||||
headers={[
|
||||
"Role Identifier",
|
||||
"Scope",
|
||||
"Description",
|
||||
"Created",
|
||||
"Actions",
|
||||
]}
|
||||
isEmpty={roles.length === 0}
|
||||
emptyState="No roles found."
|
||||
desktopRows={roles.map((r) => {
|
||||
const isGlobal = !r.app_id;
|
||||
return (
|
||||
<tr
|
||||
key={r.id}
|
||||
class="role-row"
|
||||
data-app-id={r.app_id || "global"}
|
||||
data-search={`${r.name} ${r.description || ""} ${
|
||||
isGlobal ? "global" : r.app_name || ""
|
||||
}`.toLowerCase()}
|
||||
>
|
||||
<td>
|
||||
<code style="background: var(--surface-muted); padding: 0.25rem 0.5rem; border-radius: var(--radius-sm); font-size: 0.9rem; font-family: monospace; font-weight: 700; color: var(--primary);">
|
||||
{r.name}
|
||||
</code>
|
||||
</td>
|
||||
<td>
|
||||
{isGlobal
|
||||
? <span class="badge badge-info">Global</span>
|
||||
: (
|
||||
<span class="badge badge-warning">
|
||||
{r.app_name || "App Scoped"}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td style="color: var(--text-secondary); max-width: 250px; font-size: 0.85rem;">
|
||||
{r.description || "-"}
|
||||
</td>
|
||||
<td style="font-size: 0.85rem; color: var(--text-secondary);">
|
||||
{new Date(r.created_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td>
|
||||
<div style="display: flex; gap: 0.35rem;">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline"
|
||||
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
|
||||
onclick={`openEditRoleForm(${
|
||||
JSON.stringify(JSON.stringify(r))
|
||||
})`}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
{r.name !== "admin" && (
|
||||
<button
|
||||
type="button"
|
||||
class="btn-danger"
|
||||
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
|
||||
onclick={`deleteRole('${r.id}', '${r.name}')`}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
mobileCards={roles.map((r) => {
|
||||
const isGlobal = !r.app_id;
|
||||
return (
|
||||
<div
|
||||
class="card role-card"
|
||||
key={r.id}
|
||||
data-app-id={r.app_id || "global"}
|
||||
data-search={`${r.name} ${r.description || ""} ${
|
||||
isGlobal ? "global" : r.app_name || ""
|
||||
}`.toLowerCase()}
|
||||
style="margin-bottom: 0; padding: 1rem;"
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;">
|
||||
<strong style="font-family: monospace; font-size: 1rem; color: var(--text-primary);">
|
||||
{r.name}
|
||||
</strong>
|
||||
{isGlobal
|
||||
? <span class="badge badge-info">Global</span>
|
||||
: (
|
||||
<span class="badge badge-warning">
|
||||
{r.app_name || "App Scoped"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 0.75rem;">
|
||||
<p style="margin: 0 0 0.25rem 0;">
|
||||
{r.description || "No description provided."}
|
||||
</p>
|
||||
<small style="color: var(--text-muted);">
|
||||
Created: {new Date(r.created_at).toLocaleDateString()}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline"
|
||||
style="flex: 1; min-height: 38px; font-size: 0.85rem;"
|
||||
onclick={`openEditRoleForm(${
|
||||
JSON.stringify(JSON.stringify(r))
|
||||
})`}
|
||||
>
|
||||
Edit Role
|
||||
</button>
|
||||
{r.name !== "admin" && (
|
||||
<button
|
||||
type="button"
|
||||
class="btn-danger"
|
||||
style="flex: 1; min-height: 38px; font-size: 0.85rem;"
|
||||
onclick={`deleteRole('${r.id}', '${r.name}')`}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
/>"""
|
||||
|
||||
content = content.replace(roles_table_desktop_mobile, roles_table_replacement)
|
||||
|
||||
|
||||
js_repl1 = """ function openCreateRoleForm() {
|
||||
document.getElementById('editRoleId').value = '';
|
||||
document.getElementById('roleFormTitle').textContent = 'Create New Role';
|
||||
document.getElementById('roleScope').value = 'global';
|
||||
document.getElementById('roleScope').disabled = false;
|
||||
document.getElementById('roleAppId').value = document.getElementById('roleAppId').options[0]?.value || '';
|
||||
document.getElementById('roleName').value = '';
|
||||
document.getElementById('roleName').readOnly = false;
|
||||
document.getElementById('roleDescription').value = '';
|
||||
handleScopeChange();
|
||||
document.getElementById('roleFormCard').style.display = 'block';
|
||||
document.getElementById('roleFormCard').scrollIntoView({ behavior: 'smooth' });
|
||||
}"""
|
||||
|
||||
js_repl1_new = """ function openCreateRoleForm() {
|
||||
document.getElementById('editRoleId').value = '';
|
||||
document.querySelector('#roleFormModal h3').textContent = 'Create New Role';
|
||||
document.getElementById('roleScope').value = 'global';
|
||||
document.getElementById('roleScope').disabled = false;
|
||||
document.getElementById('roleAppId').value = document.getElementById('roleAppId').options[0]?.value || '';
|
||||
document.getElementById('roleName').value = '';
|
||||
document.getElementById('roleName').readOnly = false;
|
||||
document.getElementById('roleDescription').value = '';
|
||||
handleScopeChange();
|
||||
document.getElementById('roleFormModal').style.display = 'flex';
|
||||
}"""
|
||||
content = content.replace(js_repl1, js_repl1_new)
|
||||
|
||||
js_repl2 = """ function openEditRoleForm(roleJson) {
|
||||
const r = JSON.parse(roleJson);
|
||||
document.getElementById('editRoleId').value = r.id;
|
||||
document.getElementById('roleFormTitle').textContent = 'Edit Role: ' + r.name;
|
||||
document.getElementById('roleScope').value = r.app_id ? 'app_specific' : 'global';
|
||||
document.getElementById('roleScope').disabled = true; // Cannot change scope after creation
|
||||
document.getElementById('roleAppId').value = r.app_id || document.getElementById('roleAppId').options[0]?.value || '';
|
||||
document.getElementById('roleName').value = r.name;
|
||||
document.getElementById('roleName').readOnly = (r.name === 'admin'); // cannot rename admin
|
||||
document.getElementById('roleDescription').value = r.description || '';
|
||||
handleScopeChange();
|
||||
document.getElementById('roleFormCard').style.display = 'block';
|
||||
document.getElementById('roleFormCard').scrollIntoView({ behavior: 'smooth' });
|
||||
}"""
|
||||
|
||||
js_repl2_new = """ function openEditRoleForm(roleJson) {
|
||||
const r = JSON.parse(roleJson);
|
||||
document.getElementById('editRoleId').value = r.id;
|
||||
document.querySelector('#roleFormModal h3').textContent = 'Edit Role: ' + r.name;
|
||||
document.getElementById('roleScope').value = r.app_id ? 'app_specific' : 'global';
|
||||
document.getElementById('roleScope').disabled = true; // Cannot change scope after creation
|
||||
document.getElementById('roleAppId').value = r.app_id || document.getElementById('roleAppId').options[0]?.value || '';
|
||||
document.getElementById('roleName').value = r.name;
|
||||
document.getElementById('roleName').readOnly = (r.name === 'admin'); // cannot rename admin
|
||||
document.getElementById('roleDescription').value = r.description || '';
|
||||
handleScopeChange();
|
||||
document.getElementById('roleFormModal').style.display = 'flex';
|
||||
}"""
|
||||
content = content.replace(js_repl2, js_repl2_new)
|
||||
|
||||
|
||||
js_repl3 = """ function closeRoleForm() {
|
||||
document.getElementById('roleFormCard').style.display = 'none';
|
||||
}"""
|
||||
|
||||
js_repl3_new = """ function closeRoleForm() {
|
||||
document.getElementById('roleFormModal').style.display = 'none';
|
||||
}"""
|
||||
content = content.replace(js_repl3, js_repl3_new)
|
||||
|
||||
with open('ui/components/AdminRolesPage.tsx', 'w') as f:
|
||||
f.write(content)
|
||||
print("Replaced Roles table!")
|
||||
@ -1,10 +0,0 @@
|
||||
import re
|
||||
|
||||
with open('ui/components/AdminRolesPage.tsx', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
import_str = """import { AdminLayout } from "./AdminLayout.tsx";\nimport { AdminTable } from "./admin/AdminTable.tsx";\nimport { AdminModal } from "./admin/AdminModal.tsx";"""
|
||||
content = content.replace(import_str, """import { AdminLayout } from "./AdminLayout.tsx";\nimport { AdminTable } from "./admin/AdminTable.tsx";\nimport { AdminModal } from "./admin/AdminModal.tsx";\n\n// We need to use these imports, they are falsely flagged by lint because they are only used in JSX.\n// They are used, but we'll import them anyway to appease the linter if Deno 2 has a bug.\n// In Hono JSX, imports might not be strictly recognized.""")
|
||||
|
||||
with open('ui/components/AdminRolesPage.tsx', 'w') as f:
|
||||
f.write(content)
|
||||
@ -1,204 +0,0 @@
|
||||
import re
|
||||
|
||||
with open('ui/components/AdminRolesPage.tsx', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
role_form = """ <div
|
||||
id="roleFormCard"
|
||||
class="card"
|
||||
style="display: none; border-left: 4px solid var(--primary); margin-bottom: 1.5rem;"
|
||||
>
|
||||
<h3
|
||||
id="roleFormTitle"
|
||||
style="margin: 0 0 0.5rem 0; color: var(--text-primary);"
|
||||
>
|
||||
Create New Role
|
||||
</h3>
|
||||
<p style="color: var(--text-secondary); font-size: 0.9rem; margin: 0 0 1.25rem 0;">
|
||||
Define a global shared role or an application-scoped custom grant.
|
||||
</p>
|
||||
|
||||
<form id="roleForm" onsubmit="handleSaveRole(event)">
|
||||
<input type="hidden" id="editRoleId" value="" />
|
||||
|
||||
<div
|
||||
id="scopeSelectContainer"
|
||||
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; margin-bottom: 1rem;"
|
||||
>
|
||||
<div>
|
||||
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
|
||||
Scope (Applicability) *
|
||||
</label>
|
||||
<select
|
||||
id="roleScope"
|
||||
onchange="handleScopeChange()"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<option value="global">
|
||||
Global (Shared across ALL applications)
|
||||
</option>
|
||||
<option value="app_specific">
|
||||
Application-Specific (Scoped to single app)
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="appSelectContainer" style="display: none;">
|
||||
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
|
||||
Target Application *
|
||||
</label>
|
||||
<select
|
||||
id="roleAppId"
|
||||
style="width: 100%;"
|
||||
>
|
||||
{apps.map((app) => (
|
||||
<option value={app.id}>
|
||||
{app.name} ({app.spiffe_id})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; margin-bottom: 1.25rem;">
|
||||
<div>
|
||||
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
|
||||
Role Identifier *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="roleName"
|
||||
placeholder="e.g. navigator, copilot, auditor"
|
||||
required
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
|
||||
Description / Purpose
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="roleDescription"
|
||||
placeholder="e.g. Flight routing and navigational telemetry access"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 0.75rem;">
|
||||
<button type="submit" class="btn-primary" style="min-height: 42px;">
|
||||
Save Role
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline"
|
||||
onclick="closeRoleDrawer()"
|
||||
style="min-height: 42px;"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>"""
|
||||
|
||||
role_form_replacement = """ <AdminModal
|
||||
id="roleFormModal"
|
||||
title="Create New Role"
|
||||
onClose="closeRoleDrawer()"
|
||||
>
|
||||
<p style="color: var(--text-secondary); font-size: 0.9rem; margin: 0 0 1.25rem 0;">
|
||||
Define a global shared role or an application-scoped custom grant.
|
||||
</p>
|
||||
|
||||
<form id="roleForm" onsubmit="handleSaveRole(event)">
|
||||
<input type="hidden" id="editRoleId" value="" />
|
||||
|
||||
<div
|
||||
id="scopeSelectContainer"
|
||||
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; margin-bottom: 1rem;"
|
||||
>
|
||||
<div>
|
||||
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
|
||||
Scope (Applicability) *
|
||||
</label>
|
||||
<select
|
||||
id="roleScope"
|
||||
onchange="handleScopeChange()"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<option value="global">
|
||||
Global (Shared across ALL applications)
|
||||
</option>
|
||||
<option value="app_specific">
|
||||
Application-Specific (Scoped to single app)
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="appSelectContainer" style="display: none;">
|
||||
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
|
||||
Target Application *
|
||||
</label>
|
||||
<select
|
||||
id="roleAppId"
|
||||
style="width: 100%;"
|
||||
>
|
||||
{apps.map((app) => (
|
||||
<option value={app.id}>
|
||||
{app.name} ({app.spiffe_id})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; margin-bottom: 1.25rem;">
|
||||
<div>
|
||||
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
|
||||
Role Identifier *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="roleName"
|
||||
placeholder="e.g. navigator, copilot, auditor"
|
||||
required
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
|
||||
Description / Purpose
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="roleDescription"
|
||||
placeholder="e.g. Flight routing and navigational telemetry access"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 0.75rem;">
|
||||
<button type="submit" class="btn-primary" style="min-height: 42px;">
|
||||
Save Role
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline"
|
||||
onclick="closeRoleDrawer()"
|
||||
style="min-height: 42px;"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AdminModal>"""
|
||||
|
||||
content = content.replace(role_form, role_form_replacement)
|
||||
|
||||
with open('ui/components/AdminRolesPage.tsx', 'w') as f:
|
||||
f.write(content)
|
||||
print("Replaced Roles Form!")
|
||||
@ -1,355 +0,0 @@
|
||||
import re
|
||||
|
||||
with open('ui/components/AdminRolesPage.tsx', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
|
||||
roles_table_desktop_mobile = """ {/* Desktop Table View (≥ 768px) */}
|
||||
<div class="table-container desktop-only" style="display: none;">
|
||||
<table id="rolesTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Role Identifier</th>
|
||||
<th>Scope</th>
|
||||
<th>Description</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{roles.length === 0
|
||||
? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={5}
|
||||
style="text-align: center; color: var(--text-muted); padding: 2rem;"
|
||||
>
|
||||
No roles found.
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
: (
|
||||
roles.map((r) => {
|
||||
const isGlobal = !r.app_id;
|
||||
const isCoreAdmin = isGlobal && r.name === "admin";
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={r.id}
|
||||
class="role-row"
|
||||
data-app-id={r.app_id || "global"}
|
||||
data-search={`${r.name} ${r.description || ""} ${
|
||||
isGlobal ? "global" : r.app_name || ""
|
||||
}`.toLowerCase()}
|
||||
>
|
||||
<td>
|
||||
<strong style="font-family: monospace; font-size: 0.95rem; color: var(--text-primary);">
|
||||
{r.name}
|
||||
</strong>
|
||||
</td>
|
||||
<td>
|
||||
{isGlobal
|
||||
? (
|
||||
<span class="badge badge-info">
|
||||
Global (Shared)
|
||||
</span>
|
||||
)
|
||||
: (
|
||||
<span class="badge badge-warning">
|
||||
{r.app_name || "App-Specific"}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td style="color: var(--text-secondary); font-size: 0.85rem;">
|
||||
{r.description || "-"}
|
||||
</td>
|
||||
<td style="font-size: 0.85rem; color: var(--text-secondary);">
|
||||
{new Date(r.created_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td>
|
||||
{!isCoreAdmin
|
||||
? (
|
||||
<div style="display: flex; gap: 0.35rem;">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline"
|
||||
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
|
||||
onclick={`openEditRoleDrawer(${
|
||||
JSON.stringify(JSON.stringify(r))
|
||||
})`}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-danger"
|
||||
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
|
||||
onclick={`deleteRole('${r.id}', '${r.name}')`}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<span style="font-size: 0.75rem; color: var(--text-muted); font-style: italic;">
|
||||
Immutable System Role
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Mobile View (< 768px) */}
|
||||
<div
|
||||
id="rolesMobileDeck"
|
||||
class="mobile-only"
|
||||
style="display: flex; flex-direction: column; gap: 0.75rem;"
|
||||
>
|
||||
{roles.length === 0
|
||||
? (
|
||||
<div class="card" style="text-align: center; padding: 2rem;">
|
||||
<p style="color: var(--text-muted); margin: 0;">
|
||||
No roles found.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
roles.map((r) => {
|
||||
const isGlobal = !r.app_id;
|
||||
const isCoreAdmin = isGlobal && r.name === "admin";
|
||||
|
||||
return (
|
||||
<div
|
||||
class="card role-card"
|
||||
key={r.id}
|
||||
data-app-id={r.app_id || "global"}
|
||||
data-search={`${r.name} ${r.description || ""} ${
|
||||
isGlobal ? "global" : r.app_name || ""
|
||||
}`.toLowerCase()}
|
||||
style="margin-bottom: 0; padding: 1rem;"
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;">
|
||||
<strong style="font-family: monospace; font-size: 1.05rem; color: var(--text-primary);">
|
||||
{r.name}
|
||||
</strong>
|
||||
{isGlobal
|
||||
? (
|
||||
<span class="badge badge-info">
|
||||
Global (Shared)
|
||||
</span>
|
||||
)
|
||||
: (
|
||||
<span class="badge badge-warning">
|
||||
{r.app_name || "App-Specific"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 1rem; line-height: 1.5;">
|
||||
<p style="margin: 0 0 0.5rem 0;">
|
||||
{r.description || "No description provided."}
|
||||
</p>
|
||||
<div style="font-size: 0.8rem;">
|
||||
Created: {new Date(r.created_at).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isCoreAdmin
|
||||
? (
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline"
|
||||
style="flex: 1; min-height: 38px; font-size: 0.85rem;"
|
||||
onclick={`openEditRoleDrawer(${
|
||||
JSON.stringify(JSON.stringify(r))
|
||||
})`}
|
||||
>
|
||||
Edit Role
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-danger"
|
||||
style="flex: 1; min-height: 38px; font-size: 0.85rem;"
|
||||
onclick={`deleteRole('${r.id}', '${r.name}')`}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div style="text-align: center; padding: 0.5rem; background: var(--surface-muted); border-radius: var(--radius-sm); font-size: 0.8rem; color: var(--text-muted); font-style: italic;">
|
||||
Immutable System Role
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>"""
|
||||
|
||||
roles_table_replacement = """ <AdminTable
|
||||
id="rolesTable"
|
||||
headers={[
|
||||
"Role Identifier",
|
||||
"Scope",
|
||||
"Description",
|
||||
"Created",
|
||||
"Actions",
|
||||
]}
|
||||
isEmpty={roles.length === 0}
|
||||
emptyState="No roles found."
|
||||
desktopRows={roles.map((r) => {
|
||||
const isGlobal = !r.app_id;
|
||||
const isCoreAdmin = isGlobal && r.name === "admin";
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={r.id}
|
||||
class="role-row"
|
||||
data-app-id={r.app_id || "global"}
|
||||
data-search={`${r.name} ${r.description || ""} ${
|
||||
isGlobal ? "global" : r.app_name || ""
|
||||
}`.toLowerCase()}
|
||||
>
|
||||
<td>
|
||||
<strong style="font-family: monospace; font-size: 0.95rem; color: var(--text-primary);">
|
||||
{r.name}
|
||||
</strong>
|
||||
</td>
|
||||
<td>
|
||||
{isGlobal
|
||||
? (
|
||||
<span class="badge badge-info">
|
||||
Global (Shared)
|
||||
</span>
|
||||
)
|
||||
: (
|
||||
<span class="badge badge-warning">
|
||||
{r.app_name || "App-Specific"}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td style="color: var(--text-secondary); font-size: 0.85rem;">
|
||||
{r.description || "-"}
|
||||
</td>
|
||||
<td style="font-size: 0.85rem; color: var(--text-secondary);">
|
||||
{new Date(r.created_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td>
|
||||
{!isCoreAdmin
|
||||
? (
|
||||
<div style="display: flex; gap: 0.35rem;">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline"
|
||||
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
|
||||
onclick={`openEditRoleDrawer(${
|
||||
JSON.stringify(JSON.stringify(r))
|
||||
})`}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-danger"
|
||||
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
|
||||
onclick={`deleteRole('${r.id}', '${r.name}')`}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<span style="font-size: 0.75rem; color: var(--text-muted); font-style: italic;">
|
||||
Immutable System Role
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
mobileCards={roles.map((r) => {
|
||||
const isGlobal = !r.app_id;
|
||||
const isCoreAdmin = isGlobal && r.name === "admin";
|
||||
|
||||
return (
|
||||
<div
|
||||
class="card role-card"
|
||||
key={r.id}
|
||||
data-app-id={r.app_id || "global"}
|
||||
data-search={`${r.name} ${r.description || ""} ${
|
||||
isGlobal ? "global" : r.app_name || ""
|
||||
}`.toLowerCase()}
|
||||
style="margin-bottom: 0; padding: 1rem;"
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;">
|
||||
<strong style="font-family: monospace; font-size: 1.05rem; color: var(--text-primary);">
|
||||
{r.name}
|
||||
</strong>
|
||||
{isGlobal
|
||||
? (
|
||||
<span class="badge badge-info">
|
||||
Global (Shared)
|
||||
</span>
|
||||
)
|
||||
: (
|
||||
<span class="badge badge-warning">
|
||||
{r.app_name || "App-Specific"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 1rem; line-height: 1.5;">
|
||||
<p style="margin: 0 0 0.5rem 0;">
|
||||
{r.description || "No description provided."}
|
||||
</p>
|
||||
<div style="font-size: 0.8rem;">
|
||||
Created: {new Date(r.created_at).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isCoreAdmin
|
||||
? (
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline"
|
||||
style="flex: 1; min-height: 38px; font-size: 0.85rem;"
|
||||
onclick={`openEditRoleDrawer(${
|
||||
JSON.stringify(JSON.stringify(r))
|
||||
})`}
|
||||
>
|
||||
Edit Role
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-danger"
|
||||
style="flex: 1; min-height: 38px; font-size: 0.85rem;"
|
||||
onclick={`deleteRole('${r.id}', '${r.name}')`}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div style="text-align: center; padding: 0.5rem; background: var(--surface-muted); border-radius: var(--radius-sm); font-size: 0.8rem; color: var(--text-muted); font-style: italic;">
|
||||
Immutable System Role
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
/>"""
|
||||
|
||||
content = content.replace(roles_table_desktop_mobile, roles_table_replacement)
|
||||
|
||||
|
||||
with open('ui/components/AdminRolesPage.tsx', 'w') as f:
|
||||
f.write(content)
|
||||
print("Replaced Roles table!")
|
||||
@ -1,385 +0,0 @@
|
||||
import re
|
||||
|
||||
with open('ui/components/AdminUserDetailsPage.tsx', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
import_str = """import { AdminLayout } from "./AdminLayout.tsx";"""
|
||||
content = content.replace(import_str, """import { AdminLayout } from "./AdminLayout.tsx";\nimport { AdminTable } from "./admin/AdminTable.tsx";""")
|
||||
|
||||
grants_table = """ <div class="table-container" style="margin-top: 1.25rem;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Application Name</th>
|
||||
<th>SPIFFE Workload ID</th>
|
||||
<th>Assigned Role</th>
|
||||
<th>Granted At</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{grants.length === 0
|
||||
? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={5}
|
||||
style="text-align: center; color: var(--danger); padding: 1.5rem;"
|
||||
>
|
||||
No application permissions granted (User is blocked from
|
||||
all subsidiary apps).
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
: (
|
||||
grants.map((grant) => (
|
||||
<tr key={grant.id}>
|
||||
<td>
|
||||
<strong style="color: var(--text-primary);">
|
||||
{grant.app_name}
|
||||
</strong>
|
||||
</td>
|
||||
<td>
|
||||
<code style="background: var(--surface-muted); padding: 0.2rem 0.4rem; border-radius: var(--radius-sm); font-size: 0.8rem; font-family: monospace;">
|
||||
{grant.spiffe_id}
|
||||
</code>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-info">
|
||||
{grant.role}
|
||||
</span>
|
||||
</td>
|
||||
<td style="font-size: 0.85rem; color: var(--text-secondary);">
|
||||
{new Date(grant.created_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-danger"
|
||||
style="padding: 0.35rem 0.75rem; font-size: 0.8rem; min-height: 32px;"
|
||||
onclick={`revokeGrant('${user.id}', '${grant.app_id}', '${grant.app_name}')`}
|
||||
>
|
||||
Revoke Access
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>"""
|
||||
|
||||
grants_replacement = """ <div style="margin-top: 1.25rem;">
|
||||
<AdminTable
|
||||
id="grantsTable"
|
||||
headers={[
|
||||
"Application Name",
|
||||
"SPIFFE Workload ID",
|
||||
"Assigned Role",
|
||||
"Granted At",
|
||||
"Actions",
|
||||
]}
|
||||
isEmpty={grants.length === 0}
|
||||
emptyState="No application permissions granted (User is blocked from all subsidiary apps)."
|
||||
desktopRows={grants.map((grant) => (
|
||||
<tr key={grant.id}>
|
||||
<td>
|
||||
<strong style="color: var(--text-primary);">
|
||||
{grant.app_name}
|
||||
</strong>
|
||||
</td>
|
||||
<td>
|
||||
<code style="background: var(--surface-muted); padding: 0.2rem 0.4rem; border-radius: var(--radius-sm); font-size: 0.8rem; font-family: monospace;">
|
||||
{grant.spiffe_id}
|
||||
</code>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-info">
|
||||
{grant.role}
|
||||
</span>
|
||||
</td>
|
||||
<td style="font-size: 0.85rem; color: var(--text-secondary);">
|
||||
{new Date(grant.created_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-danger"
|
||||
style="padding: 0.35rem 0.75rem; font-size: 0.8rem; min-height: 32px;"
|
||||
onclick={`revokeGrant('${user.id}', '${grant.app_id}', '${grant.app_name}')`}
|
||||
>
|
||||
Revoke Access
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
mobileCards={grants.map((grant) => (
|
||||
<div class="card" key={grant.id} style="margin-bottom: 0;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;">
|
||||
<strong style="color: var(--text-primary);">{grant.app_name}</strong>
|
||||
<span class="badge badge-info">{grant.role}</span>
|
||||
</div>
|
||||
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 0.75rem; font-family: monospace;">
|
||||
{grant.spiffe_id}
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span style="font-size: 0.8rem; color: var(--text-secondary);">
|
||||
{new Date(grant.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-danger"
|
||||
style="padding: 0.25rem 0.5rem; font-size: 0.75rem;"
|
||||
onclick={`revokeGrant('${user.id}', '${grant.app_id}', '${grant.app_name}')`}
|
||||
>
|
||||
Revoke Access
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
/>
|
||||
</div>"""
|
||||
|
||||
content = content.replace(grants_table, grants_replacement)
|
||||
|
||||
|
||||
sessions_table = """ <div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Session ID</th>
|
||||
<th>Created</th>
|
||||
<th>Expires</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sessions.length === 0
|
||||
? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={4}
|
||||
style="text-align: center; color: var(--text-muted); padding: 1.5rem;"
|
||||
>
|
||||
No active sessions.
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
: (
|
||||
sessions.map((s) => (
|
||||
<tr key={s.id}>
|
||||
<td style="font-family: monospace; font-size: 0.85rem;">
|
||||
<code style="background: var(--surface-muted); padding: 0.2rem 0.4rem; border-radius: var(--radius-sm); color: var(--text-primary);">
|
||||
{s.id.substring(0, 16)}...
|
||||
</code>
|
||||
{s.is_agent
|
||||
? (
|
||||
<span
|
||||
class="badge badge-warning"
|
||||
style="margin-left: 0.75rem;"
|
||||
>
|
||||
Agent Session
|
||||
</span>
|
||||
)
|
||||
: null}
|
||||
</td>
|
||||
<td style="font-size: 0.85rem; color: var(--text-secondary);">
|
||||
{new Date(s.created_at).toLocaleString()}
|
||||
</td>
|
||||
<td style="font-size: 0.85rem; color: var(--text-secondary);">
|
||||
{new Date(s.expires_at).toLocaleString()}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline"
|
||||
style="padding: 0.35rem 0.75rem; font-size: 0.8rem; min-height: 32px;"
|
||||
onclick={`revokeSession('${user.id}', '${s.id}')`}
|
||||
>
|
||||
Revoke
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>"""
|
||||
|
||||
sessions_replacement = """ <AdminTable
|
||||
id="sessionsTable"
|
||||
headers={["Session ID", "Created", "Expires", "Actions"]}
|
||||
isEmpty={sessions.length === 0}
|
||||
emptyState="No active sessions."
|
||||
desktopRows={sessions.map((s) => (
|
||||
<tr key={s.id}>
|
||||
<td style="font-family: monospace; font-size: 0.85rem;">
|
||||
<code style="background: var(--surface-muted); padding: 0.2rem 0.4rem; border-radius: var(--radius-sm); color: var(--text-primary);">
|
||||
{s.id.substring(0, 16)}...
|
||||
</code>
|
||||
{s.is_agent
|
||||
? (
|
||||
<span
|
||||
class="badge badge-warning"
|
||||
style="margin-left: 0.75rem;"
|
||||
>
|
||||
Agent Session
|
||||
</span>
|
||||
)
|
||||
: null}
|
||||
</td>
|
||||
<td style="font-size: 0.85rem; color: var(--text-secondary);">
|
||||
{new Date(s.created_at).toLocaleString()}
|
||||
</td>
|
||||
<td style="font-size: 0.85rem; color: var(--text-secondary);">
|
||||
{new Date(s.expires_at).toLocaleString()}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline"
|
||||
style="padding: 0.35rem 0.75rem; font-size: 0.8rem; min-height: 32px;"
|
||||
onclick={`revokeSession('${user.id}', '${s.id}')`}
|
||||
>
|
||||
Revoke
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
mobileCards={sessions.map((s) => (
|
||||
<div class="card" key={s.id} style="margin-bottom: 0;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;">
|
||||
<code style="font-family: monospace; font-size: 0.85rem; background: var(--surface-muted); padding: 0.2rem 0.4rem; border-radius: var(--radius-sm);">
|
||||
{s.id.substring(0, 16)}...
|
||||
</code>
|
||||
{s.is_agent
|
||||
? (
|
||||
<span class="badge badge-warning">Agent Session</span>
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 0.25rem;">
|
||||
Created: {new Date(s.created_at).toLocaleString()}
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span style="font-size: 0.8rem; color: var(--text-secondary);">
|
||||
Expires: {new Date(s.expires_at).toLocaleString()}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline"
|
||||
style="padding: 0.25rem 0.5rem; font-size: 0.75rem;"
|
||||
onclick={`revokeSession('${user.id}', '${s.id}')`}
|
||||
>
|
||||
Revoke
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
/>"""
|
||||
|
||||
content = content.replace(sessions_table, sessions_replacement)
|
||||
|
||||
|
||||
passkeys_table = """ <div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Credential ID</th>
|
||||
<th>Counter</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{passkeys.length === 0
|
||||
? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={3}
|
||||
style="text-align: center; color: var(--text-muted); padding: 1.5rem;"
|
||||
>
|
||||
No registered passkeys.
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
: (
|
||||
passkeys.map((pk) => (
|
||||
<tr key={pk.credential_id}>
|
||||
<td style="font-family: monospace; font-size: 0.85rem; word-break: break-all; max-width: 250px;">
|
||||
<code style="background: var(--surface-muted); padding: 0.2rem 0.4rem; border-radius: var(--radius-sm); color: var(--text-primary);">
|
||||
{pk.credential_id.substring(0, 32)}...
|
||||
</code>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-info">{pk.sign_count}</span>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-danger"
|
||||
style="padding: 0.35rem 0.75rem; font-size: 0.8rem; min-height: 32px;"
|
||||
onclick={`deletePasskey('${user.id}', '${pk.credential_id}')`}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>"""
|
||||
|
||||
passkeys_replacement = """ <AdminTable
|
||||
id="passkeysTable"
|
||||
headers={["Credential ID", "Counter", "Actions"]}
|
||||
isEmpty={passkeys.length === 0}
|
||||
emptyState="No registered passkeys."
|
||||
desktopRows={passkeys.map((pk) => (
|
||||
<tr key={pk.credential_id}>
|
||||
<td style="font-family: monospace; font-size: 0.85rem; word-break: break-all; max-width: 250px;">
|
||||
<code style="background: var(--surface-muted); padding: 0.2rem 0.4rem; border-radius: var(--radius-sm); color: var(--text-primary);">
|
||||
{pk.credential_id.substring(0, 32)}...
|
||||
</code>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-info">{pk.sign_count}</span>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-danger"
|
||||
style="padding: 0.35rem 0.75rem; font-size: 0.8rem; min-height: 32px;"
|
||||
onclick={`deletePasskey('${user.id}', '${pk.credential_id}')`}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
mobileCards={passkeys.map((pk) => (
|
||||
<div class="card" key={pk.credential_id} style="margin-bottom: 0;">
|
||||
<div style="font-family: monospace; font-size: 0.85rem; word-break: break-all; margin-bottom: 0.75rem; color: var(--text-primary);">
|
||||
<code style="background: var(--surface-muted); padding: 0.2rem 0.4rem; border-radius: var(--radius-sm); color: var(--text-primary);">
|
||||
{pk.credential_id.substring(0, 32)}...
|
||||
</code>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span class="badge badge-info">Count: {pk.sign_count}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-danger"
|
||||
style="padding: 0.25rem 0.5rem; font-size: 0.75rem;"
|
||||
onclick={`deletePasskey('${user.id}', '${pk.credential_id}')`}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
/>"""
|
||||
|
||||
content = content.replace(passkeys_table, passkeys_replacement)
|
||||
|
||||
with open('ui/components/AdminUserDetailsPage.tsx', 'w') as f:
|
||||
f.write(content)
|
||||
print("Replaced User Details tables!")
|
||||
Loading…
x
Reference in New Issue
Block a user