auth-yes/ui/components/AAGUIDPage.tsx

168 lines
5.8 KiB
TypeScript

import { AdminLayout } from "./AdminLayout.tsx";
export const AAGUIDPage = ({ allowlist }: { allowlist: any[] }) => {
return (
<AdminLayout title="AAGUID Allow-List" currentPath="/admin/aaguid">
<div
id="status-banner"
style="display: none; margin-bottom: 1rem; padding: 0.75rem 1rem; border-radius: 4px; font-size: 0.9rem;"
/>
<div class="card">
<h2>AAGUID Allow-List Management</h2>
<p>
Manage the enterprise allow-list of approved hardware Authenticator
Attestation GUIDs (AAGUIDs). If this list is populated, only passkeys
matching these AAGUIDs will be permitted to register. If empty, all
certified hardware passkeys are allowed (software passkeys are always
rejected).
</p>
<form
id="add-aaguid-form"
style="margin-bottom: 2rem; display: flex; gap: 1rem; align-items: flex-end;"
>
<div style="flex: 1;">
<label style="display: block; margin-bottom: 0.5rem; font-weight: 500;">
AAGUID (UUID format)
</label>
<input
type="text"
name="aaguid"
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
required
style="width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;"
/>
</div>
<div style="flex: 2;">
<label style="display: block; margin-bottom: 0.5rem; font-weight: 500;">
Description (e.g. YubiKey 5 NFC)
</label>
<input
type="text"
name="description"
placeholder="Hardware Key Model"
style="width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;"
/>
</div>
<div>
<button
type="submit"
class="btn-action btn-success"
style="padding: 0.6rem 1rem;"
>
Add to Allow-List
</button>
</div>
</form>
<div class="table-container">
<table>
<thead>
<tr>
<th>AAGUID</th>
<th>Description</th>
<th>Added On</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{allowlist.length === 0
? (
<tr>
<td colspan={4} style="text-align: center; color: #6c757d;">
The allow-list is empty. All hardware passkeys are
currently accepted.
</td>
</tr>
)
: (
allowlist.map((item: any) => (
<tr key={item.id}>
<td>
<code style="background: #f8f9fa; padding: 0.2rem 0.4rem; border-radius: 3px;">
{item.aaguid}
</code>
</td>
<td>{item.description || "-"}</td>
<td>{new Date(item.created_at).toLocaleString()}</td>
<td>
<button
type="button"
class="btn-action btn-warning"
onclick={`removeAaguid('${item.id}')`}
>
Remove
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
<script
dangerouslySetInnerHTML={{
__html: `
function showNotice(msg, isError) {
const banner = document.getElementById('status-banner');
banner.textContent = msg;
banner.style.display = 'block';
banner.style.background = isError ? '#f8d7da' : '#d1e7dd';
banner.style.color = isError ? '#842029' : '#0f5132';
banner.style.border = isError ? '1px solid #f5c2c7' : '1px solid #badbcc';
setTimeout(() => { banner.style.display = 'none'; }, 6000);
}
document.getElementById('add-aaguid-form').addEventListener('submit', async (e) => {
e.preventDefault();
const aaguid = e.target.aaguid.value;
const description = e.target.description.value;
try {
const res = await fetch('/api/admin/aaguid', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ aaguid, description })
});
if (res.ok) {
showNotice('AAGUID added to allow-list', false);
setTimeout(() => window.location.reload(), 800);
} else {
const data = await res.json();
showNotice(data.error || 'Failed to add AAGUID', true);
}
} catch (err) {
showNotice('Network error', true);
}
});
async function removeAaguid(id) {
if (!confirm('Are you sure you want to remove this AAGUID from the allow-list?')) {
return;
}
try {
const res = await fetch('/api/admin/aaguid/' + id, {
method: 'DELETE'
});
if (res.ok) {
showNotice('AAGUID removed', false);
setTimeout(() => window.location.reload(), 800);
} else {
const data = await res.json();
showNotice(data.error || 'Failed to remove AAGUID', true);
}
} catch (err) {
showNotice('Network error', true);
}
}
`,
}}
>
</script>
</AdminLayout>
);
};