import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import { Shield, Lock, Fingerprint, Smartphone, UserPlus, AlertTriangle, Loader2, Users, ExternalLink } from "lucide-react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Slider } from "@/components/ui/slider"; import { Badge } from "@/components/ui/badge"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { api, OrgPolicyResponse, UpdateOrgPolicyDto, create403Handler } from "@/lib/api"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useToast } from "@/hooks/use-toast"; import { useOrganizations } from "@/hooks/useOrganizations"; const MFA_MODE_LABELS: Record = { disabled: { label: "Disabled", description: "No MFA required for members", }, optional: { label: "Optional", description: "Members may opt-in to MFA", }, require_totp: { label: "Require TOTP", description: "All members must set up an authenticator app", }, require_webauthn: { label: "Require Passkey", description: "All members must register a passkey", }, require_totp_or_webauthn: { label: "Require TOTP or Passkey", description: "Members must set up at least one MFA method", }, }; export default function PoliciesPage() { const navigate = useNavigate(); const { toast } = useToast(); const queryClient = useQueryClient(); const [currentOrgId, setCurrentOrgId] = useState(null); // Local form state for unsaved changes const [formData, setFormData] = useState({ mfa_policy_mode: '', mfa_grace_period_days: 14, notify_days_before: 7, }); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); // Fetch organizations to get current org const { data: organizations, isLoading: orgsLoading } = useOrganizations(); useEffect(() => { if (organizations && organizations.length > 0) { setCurrentOrgId(organizations[0].id); } }, [organizations]); // Fetch org policy const { data: policy, isLoading: policyLoading } = useQuery({ queryKey: ['org-policy', currentOrgId], queryFn: () => currentOrgId ? api.policies.getOrgPolicy(currentOrgId, { on403: create403Handler(toast), }) : null, enabled: !!currentOrgId, }); useEffect(() => { if (policy?.security_policy) { setFormData({ mfa_policy_mode: policy.security_policy.mfa_policy_mode, mfa_grace_period_days: policy.security_policy.mfa_grace_period_days, notify_days_before: policy.security_policy.notify_days_before, }); setHasUnsavedChanges(false); } }, [policy]); // Fetch org compliance summary const { data: complianceData, isLoading: complianceLoading } = useQuery({ queryKey: ['org-compliance', currentOrgId], queryFn: () => currentOrgId ? api.policies.listOrgCompliance(currentOrgId, {}, { on403: create403Handler(toast), }) : null, enabled: !!currentOrgId, }); // Update policy mutation const updatePolicyMutation = useMutation({ mutationFn: async (data: UpdateOrgPolicyDto) => { if (!currentOrgId) throw new Error('No organization selected'); return api.policies.updateOrgPolicy(currentOrgId, data, { on403: create403Handler(toast), }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['org-policy', currentOrgId] }); setHasUnsavedChanges(false); toast({ title: "Policy updated", description: "Security policy has been updated successfully.", }); }, onError: (error: Error) => { toast({ variant: "destructive", title: "Failed to update policy", description: error.message, }); }, }); // Calculate compliance stats const complianceStats = { compliant: 0, inGrace: 0, pastDue: 0, suspended: 0, pending: 0, total: complianceData?.count || 0, }; if (complianceData?.members) { for (const member of complianceData.members) { switch (member.status) { case 'compliant': complianceStats.compliant++; break; case 'in_grace': complianceStats.inGrace++; break; case 'past_due': complianceStats.pastDue++; break; case 'suspended': complianceStats.suspended++; break; case 'pending': complianceStats.pending++; break; } } } const handleMfaModeChange = (mode: string) => { setFormData(prev => ({ ...prev, mfa_policy_mode: mode })); setHasUnsavedChanges(true); }; const handleGracePeriodChange = (days: number[]) => { setFormData(prev => ({ ...prev, mfa_grace_period_days: days[0] })); setHasUnsavedChanges(true); }; const handleNotifyDaysChange = (days: number[]) => { setFormData(prev => ({ ...prev, notify_days_before: days[0] })); setHasUnsavedChanges(true); }; const handleSavePolicy = () => { updatePolicyMutation.mutate({ mfa_policy_mode: formData.mfa_policy_mode, mfa_grace_period_days: formData.mfa_grace_period_days, notify_days_before: formData.notify_days_before, }); }; const handleDiscardChanges = () => { if (policy?.security_policy) { setFormData({ mfa_policy_mode: policy.security_policy.mfa_policy_mode, mfa_grace_period_days: policy.security_policy.mfa_grace_period_days, notify_days_before: policy.security_policy.notify_days_before, }); setHasUnsavedChanges(false); } }; if (orgsLoading || policyLoading) { return (
); } if (!currentOrgId || !policy) { return (
Unable to load organization policy. Please try again later.
); } return (

Security Policies

Configure security requirements for organization members

{/* Compliance Overview */} Compliance Overview Current MFA compliance status for organization members

{complianceStats.compliant}

Compliant

{complianceStats.inGrace}

In Grace

{complianceStats.pastDue}

Past Due

{complianceStats.suspended}

Suspended

{complianceStats.total}

Total Members

{/* MFA Policy */} Multi-Factor Authentication Require additional authentication methods for all members {hasUnsavedChanges && ( You have unsaved changes. Click "Save Changes" to apply them or "Discard" to revert. )}

{MFA_MODE_LABELS[formData.mfa_policy_mode]?.description}

{formData.mfa_policy_mode !== 'disabled' && formData.mfa_policy_mode !== 'optional' && ( <>
{formData.mfa_grace_period_days} days

Members will have this many days to configure MFA after policy is applied.

{formData.notify_days_before} days

Send reminder notifications this many days before the deadline.

)} {hasUnsavedChanges && (
)}
{/* Password Policy */} Password Policy Set minimum password requirements for all members
12 chars

At least one A-Z

At least one 0-9

At least one !@#$%^&*

{/* Passkey Requirements */} Passkeys (WebAuthn) Require passwordless authentication capability

Members must register a passkey for backup authentication

); }