diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx index 44156f9..a9235df 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 } from 'lucide-react'; +import { LayoutDashboard, ShieldCheck, Users, Settings, Shield, Building, ScrollText, Mail, BarChart3, Server, ExternalLink, Key, Lock } from 'lucide-react'; import { useQuery } from '@tanstack/react-query'; import { useAuth } from '../auth/useAuth'; import { useScopes } from '../auth/useScopes'; @@ -148,6 +148,15 @@ export function Layout() { License Tools +
navigate('/vendor/auth-policy')} + > + + Auth Policy +
window.open(`${window.location.protocol}//${window.location.hostname}:3002`, '_blank', 'noopener')} diff --git a/ui/src/pages/vendor/AuthPolicyPage.module.css b/ui/src/pages/vendor/AuthPolicyPage.module.css new file mode 100644 index 0000000..15a04f3 --- /dev/null +++ b/ui/src/pages/vendor/AuthPolicyPage.module.css @@ -0,0 +1,25 @@ +.subtitle { + color: var(--text-muted); + margin-bottom: 0; + margin-top: 8px; +} + +.description { + color: var(--text-muted); + font-size: 0.875rem; + margin-top: 0; + margin-bottom: 16px; +} + +.controlRow { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; + font-size: 0.875rem; +} + +.buttonGroup { + display: flex; + gap: 8px; +} diff --git a/ui/src/pages/vendor/AuthPolicyPage.tsx b/ui/src/pages/vendor/AuthPolicyPage.tsx new file mode 100644 index 0000000..58ff11c --- /dev/null +++ b/ui/src/pages/vendor/AuthPolicyPage.tsx @@ -0,0 +1,164 @@ +import { useState } from 'react'; +import { Card, Button, Badge, Alert, useToast } from '@cameleer/design-system'; +import { useVendorAuthPolicy, useUpdateVendorAuthPolicy } from '../../api/vendor-hooks'; +import { errorMessage } from '../../api/client'; +import styles from './AuthPolicyPage.module.css'; + +export function AuthPolicyPage() { + const { data: policy, isLoading } = useVendorAuthPolicy(); + const updatePolicy = useUpdateVendorAuthPolicy(); + const { toast } = useToast(); + const [confirmRequired, setConfirmRequired] = useState(false); + + if (isLoading || !policy) return null; + + async function handleMfaModeChange(mode: string) { + if (mode === 'required' && policy?.mfaMode !== 'required') { + setConfirmRequired(true); + return; + } + try { + await updatePolicy.mutateAsync({ mfaMode: mode }); + toast({ title: `MFA mode set to ${mode}`, variant: 'success' }); + } catch (err) { + toast({ title: 'Failed to update policy', description: errorMessage(err), variant: 'error' }); + } + } + + async function handleConfirmRequired() { + try { + await updatePolicy.mutateAsync({ mfaMode: 'required' }); + setConfirmRequired(false); + toast({ title: 'MFA is now required for all tenant admins', variant: 'success' }); + } catch (err) { + toast({ title: 'Failed to update policy', description: errorMessage(err), variant: 'error' }); + } + } + + async function handlePasskeyToggle() { + try { + await updatePolicy.mutateAsync({ passkeyEnabled: !policy?.passkeyEnabled }); + toast({ + title: policy?.passkeyEnabled ? 'Passkeys disabled' : 'Passkeys enabled', + variant: 'success', + }); + } catch (err) { + toast({ title: 'Failed to update policy', description: errorMessage(err), variant: 'error' }); + } + } + + async function handlePasskeyModeChange(mode: string) { + try { + await updatePolicy.mutateAsync({ passkeyMode: mode }); + toast({ title: `Passkey mode set to ${mode}`, variant: 'success' }); + } catch (err) { + toast({ title: 'Failed to update policy', description: errorMessage(err), variant: 'error' }); + } + } + + return ( +
+
+

Authentication Policy

+

+ Controls how tenant admins authenticate to the SaaS platform. This does not affect how + tenant users access their dashboards — tenants set their own policy. +

+
+ + +
+

+ Require tenant admins to use MFA when accessing the management platform. +

+
+ MFA Mode + +
+
+ {['off', 'optional', 'required'].map((mode) => ( + + ))} +
+ {confirmRequired && ( +
+ + All tenant admins who have not enrolled in MFA will be blocked from the platform + until they enroll. + +
+ + +
+
+ )} +
+
+ + +
+

+ Allow tenant admins to use passkeys (fingerprint, face, or security key) for + authentication. +

+
+ Passkeys + +
+ + + {policy.passkeyEnabled && ( +
+
+ Passkey Mode + +
+
+ {['optional', 'preferred', 'required'].map((mode) => ( + + ))} +
+
+ )} +
+
+
+ ); +} diff --git a/ui/src/router.tsx b/ui/src/router.tsx index 24b3721..0dd6359 100644 --- a/ui/src/router.tsx +++ b/ui/src/router.tsx @@ -18,6 +18,7 @@ import { InfrastructurePage } from './pages/vendor/InfrastructurePage'; 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 { TenantDashboardPage } from './pages/tenant/TenantDashboardPage'; import { TenantLicensePage } from './pages/tenant/TenantLicensePage'; import { SsoPage } from './pages/tenant/SsoPage'; @@ -114,6 +115,11 @@ export function AppRouter() { } /> + }> + + + } /> {/* Tenant portal */} } />