diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx
index a9235df..dde1b0e 100644
--- a/ui/src/components/Layout.tsx
+++ b/ui/src/components/Layout.tsx
@@ -4,7 +4,7 @@ import {
Sidebar,
TopBar,
} from '@cameleer/design-system';
-import { LayoutDashboard, ShieldCheck, Users, Settings, Shield, Building, ScrollText, Mail, BarChart3, Server, ExternalLink, Key, Lock } from 'lucide-react';
+import { LayoutDashboard, ShieldCheck, Users, Settings, Shield, Building, ScrollText, Mail, BarChart3, Server, ExternalLink, Key, Lock, UserCog } from 'lucide-react';
import { useQuery } from '@tanstack/react-query';
import { useAuth } from '../auth/useAuth';
import { useScopes } from '../auth/useScopes';
@@ -157,6 +157,15 @@ export function Layout() {
Auth Policy
+
navigate('/vendor/admins')}
+ >
+
+ Administrators
+
window.open(`${window.location.protocol}//${window.location.hostname}:3002`, '_blank', 'noopener')}
diff --git a/ui/src/pages/vendor/VendorAdminsPage.tsx b/ui/src/pages/vendor/VendorAdminsPage.tsx
new file mode 100644
index 0000000..291f699
--- /dev/null
+++ b/ui/src/pages/vendor/VendorAdminsPage.tsx
@@ -0,0 +1,399 @@
+import { useState } from 'react';
+import {
+ Alert,
+ AlertDialog,
+ Badge,
+ Button,
+ Card,
+ DataTable,
+ EmptyState,
+ FormField,
+ Input,
+ Spinner,
+ useToast,
+} from '@cameleer/design-system';
+import type { Column } from '@cameleer/design-system';
+import { Copy, Plus, ShieldCheck } from 'lucide-react';
+import { errorMessage } from '../../api/client';
+import {
+ useVendorAdminList,
+ useCreateVendorAdmin,
+ useRemoveVendorAdmin,
+ useResetVendorAdminPassword,
+ useResetVendorAdminMfa,
+} from '../../api/vendor-admin-hooks';
+import { useEmailConnector } from '../../api/email-connector-hooks';
+import { useMe } from '../../api/hooks';
+
+// DataTable requires T extends { id: string }
+interface AdminRow {
+ id: string;
+ name: string;
+ email: string;
+}
+
+function toRow(raw: { userId: string; name: string; email: string }): AdminRow {
+ return { id: raw.userId, name: raw.name || raw.email, email: raw.email };
+}
+
+export function VendorAdminsPage() {
+ const { data: rawAdmins, isLoading, isError } = useVendorAdminList();
+ const { data: me } = useMe();
+ const { data: emailConnector, isLoading: emailLoading } = useEmailConnector();
+ const createAdmin = useCreateVendorAdmin();
+ const removeAdmin = useRemoveVendorAdmin();
+ const resetPassword = useResetVendorAdminPassword();
+ const resetMfa = useResetVendorAdminMfa();
+ const { toast } = useToast();
+
+ const currentUserId = me?.userId ?? null;
+
+ // Add admin dialog state
+ const [showAdd, setShowAdd] = useState(false);
+ const [addEmail, setAddEmail] = useState('');
+ const [addTempPassword, setAddTempPassword] = useState('');
+ const [createdCredentials, setCreatedCredentials] = useState<{ email: string; tempPassword: string } | null>(null);
+
+ // Remove dialog state
+ const [removeTarget, setRemoveTarget] = useState
(null);
+
+ // Reset password state
+ const [pwTarget, setPwTarget] = useState(null);
+ const [pwValue, setPwValue] = useState('');
+
+ // Reset MFA state
+ const [mfaTarget, setMfaTarget] = useState(null);
+
+ const admins: AdminRow[] = (rawAdmins ?? []).map(toRow);
+ const emailConfigured = !emailLoading && emailConnector != null;
+
+ function openAdd() {
+ setAddEmail('');
+ setAddTempPassword('');
+ setCreatedCredentials(null);
+ setShowAdd(true);
+ }
+
+ function closeAdd() {
+ setShowAdd(false);
+ setAddEmail('');
+ setAddTempPassword('');
+ setCreatedCredentials(null);
+ }
+
+ async function handleAdd(e: React.FormEvent) {
+ e.preventDefault();
+ if (!addEmail.trim()) return;
+ try {
+ const result = await createAdmin.mutateAsync({
+ email: addEmail.trim(),
+ tempPassword: emailConfigured ? undefined : addTempPassword || undefined,
+ });
+ if (result.invited) {
+ toast({ title: `Invitation sent to ${addEmail}`, variant: 'success' });
+ closeAdd();
+ } else if (result.tempPassword) {
+ setCreatedCredentials({ email: addEmail.trim(), tempPassword: result.tempPassword });
+ } else {
+ toast({ title: `Administrator ${addEmail} created`, variant: 'success' });
+ closeAdd();
+ }
+ } catch (err) {
+ toast({ title: 'Failed to add administrator', description: errorMessage(err), variant: 'error' });
+ }
+ }
+
+ async function handleRemove() {
+ if (!removeTarget) return;
+ try {
+ await removeAdmin.mutateAsync(removeTarget.id);
+ toast({ title: `Removed ${removeTarget.name}`, variant: 'success' });
+ setRemoveTarget(null);
+ } catch (err) {
+ toast({ title: 'Remove failed', description: errorMessage(err), variant: 'error' });
+ setRemoveTarget(null);
+ }
+ }
+
+ const columns: Column[] = [
+ {
+ key: 'name',
+ header: 'Name',
+ render: (_v, row) => (
+
+ {row.name}
+ {row.id === currentUserId && (
+
+ )}
+
+ ),
+ },
+ {
+ key: 'email',
+ header: 'Email',
+ render: (_v, row) => (
+ {row.email}
+ ),
+ },
+ {
+ key: 'id',
+ header: 'Actions',
+ render: (_v, row) => (
+
+
+
+
+
+ ),
+ },
+ ];
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (isError) {
+ return (
+
+
+ Could not fetch administrator list. Please refresh.
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
Platform Administrators
+
+
+
+ {/* Add admin form */}
+ {showAdd && !createdCredentials && (
+
+ {emailLoading ? (
+
+
+
+ ) : (
+
+ )}
+
+ )}
+
+ {/* Credential display after creation (no email connector) */}
+ {showAdd && createdCredentials && (
+
+
+ Account created. Share these credentials securely — the temporary password will not be
+ shown again.
+
+
+
+ Email
+ {createdCredentials.email}
+
+
+
Temporary password
+
+
+ {createdCredentials.tempPassword}
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Admin table */}
+ {admins.length === 0 ? (
+
}
+ title="No platform administrators"
+ description="Add an administrator to grant platform:admin access."
+ action={
+
+ }
+ />
+ ) : (
+
+ )}
+
+ {/* Remove confirmation dialog */}
+
setRemoveTarget(null)}
+ onConfirm={handleRemove}
+ title="Remove Administrator"
+ description={`Remove "${removeTarget?.name ?? ''}" from platform administrators? They will lose platform:admin access immediately.`}
+ confirmLabel="Remove"
+ cancelLabel="Cancel"
+ variant="danger"
+ loading={removeAdmin.isPending}
+ />
+
+ {/* Reset MFA inline card */}
+ {mfaTarget && (
+
+
+ This will remove all MFA factors for this administrator. They will need to re-enroll if
+ MFA is required.
+
+
+
+
+
+
+ )}
+
+ {/* Reset password inline card */}
+ {pwTarget && (
+
+
+
+ )}
+
+ );
+}
diff --git a/ui/src/router.tsx b/ui/src/router.tsx
index 0dd6359..b585ffc 100644
--- a/ui/src/router.tsx
+++ b/ui/src/router.tsx
@@ -19,6 +19,7 @@ import { VendorMetricsPage } from './pages/vendor/VendorMetricsPage';
import { EmailConfigPage } from './pages/vendor/EmailConfigPage';
import { LicenseVerifyPage } from './pages/vendor/LicenseVerifyPage';
import { AuthPolicyPage } from './pages/vendor/AuthPolicyPage';
+import { VendorAdminsPage } from './pages/vendor/VendorAdminsPage';
import { TenantDashboardPage } from './pages/tenant/TenantDashboardPage';
import { TenantLicensePage } from './pages/tenant/TenantLicensePage';
import { SsoPage } from './pages/tenant/SsoPage';
@@ -120,6 +121,11 @@ export function AppRouter() {
} />
+ }>
+
+
+ } />
{/* Tenant portal */}
} />