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