import { useState, useEffect, useRef } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Settings, Mail, Webhook, Bell, User, Plus, Trash2, TestTube, Save, Eye, EyeOff, CheckCircle, XCircle, AlertCircle, Loader2, Edit2, X, Link2, KeyRound, Copy, Building2, Server, Database, HardDrive, Clock, PackageOpen, Download, Upload, ShieldCheck, } from "lucide-react"; import { useAuth } from "../context/AuthContext"; import client from "../api/client"; import { getSsoConfig, updateSsoConfig, type SsoConfig, type SsoConfigUpdate, } from "../api/sso"; import { getEmailConfig, updateEmailConfig, sendTestEmail, getWebhooks, createWebhook, updateWebhook, deleteWebhook, testWebhook, getMe, updateMyPreferences, getJiraConfig, updateJiraConfig, testJiraConnection, testTempoConnection, type EmailConfigUpdate, type WebhookCreate, type WebhookOut, type NotificationPreferences, type JiraConfigUpdate, } from "../api/settings"; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function Section({ title, icon: Icon, children, }: { title: string; icon: React.FC<{ className?: string }>; children: React.ReactNode; }) { return (

{title}

{children}
); } function ToggleRow({ label, description, checked, onChange, }: { label: string; description?: string; checked: boolean; onChange: (v: boolean) => void; }) { return (

{label}

{description &&

{description}

}
); } function Toast({ message, type, onClose, }: { message: string; type: "success" | "error"; onClose: () => void; }) { return (
{type === "success" ? ( ) : ( )} {message}
); } // --------------------------------------------------------------------------- // SMTP Email Section (admin only) // --------------------------------------------------------------------------- const AVAILABLE_EVENTS = [ "test.validated", "test.rejected", "campaign.completed", "campaign.started", "mitre.synced", "webhook.test", ]; function EmailSection() { const qc = useQueryClient(); const [showPw, setShowPw] = useState(false); const [toast, setToast] = useState<{ msg: string; type: "success" | "error" } | null>(null); const [testEmail, setTestEmail] = useState(""); const { data: cfg, isLoading } = useQuery({ queryKey: ["email-config"], queryFn: getEmailConfig, }); const [form, setForm] = useState({}); const effective = { ...cfg, ...form }; const saveMutation = useMutation({ mutationFn: updateEmailConfig, onSuccess: () => { qc.invalidateQueries({ queryKey: ["email-config"] }); setForm({}); setToast({ msg: "Email configuration saved", type: "success" }); }, onError: () => setToast({ msg: "Failed to save email configuration", type: "error" }), }); const testMutation = useMutation({ mutationFn: () => sendTestEmail(testEmail), onSuccess: () => setToast({ msg: `Test email sent to ${testEmail}`, type: "success" }), onError: () => setToast({ msg: "Failed to send test email. Check SMTP settings and logs.", type: "error" }), }); if (isLoading) { return (
); } const field = ( key: keyof EmailConfigUpdate, label: string, type = "text", placeholder = "" ) => (
setForm((prev) => ({ ...prev, [key]: e.target.value })) } placeholder={placeholder} className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 placeholder-gray-600 focus:border-cyan-500 focus:outline-none" />
); return ( <> {toast && ( setToast(null)} /> )}
{/* Enable toggle */} setForm((prev) => ({ ...prev, enabled: v }))} />
{field("host", "SMTP Host", "text", "smtp.gmail.com")}
setForm((prev) => ({ ...prev, port: parseInt(e.target.value) || 587 })) } className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 focus:border-cyan-500 focus:outline-none" />
{field("username", "Username / Email", "text", "you@company.com")}
setForm((prev) => ({ ...prev, password: e.target.value }))} placeholder="Leave blank to keep current" className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 pr-10 text-sm text-gray-200 placeholder-gray-600 focus:border-cyan-500 focus:outline-none" />
{field("from_email", "From Address", "email", "aegis@company.com")}
setForm((prev) => ({ ...prev, use_tls: v }))} />
{/* Test email */}

Send Test Email

setTestEmail(e.target.value)} placeholder="recipient@example.com" className="flex-1 rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 placeholder-gray-600 focus:border-cyan-500 focus:outline-none" />
); } // --------------------------------------------------------------------------- // Webhooks Section (admin + leads) // --------------------------------------------------------------------------- function WebhookForm({ initial, onSave, onCancel, isSaving, }: { initial?: Partial; onSave: (data: WebhookCreate) => void; onCancel: () => void; isSaving: boolean; }) { const [form, setForm] = useState({ name: initial?.name ?? "", url: initial?.url ?? "", secret: initial?.secret ?? "", events: initial?.events ?? [], is_active: initial?.is_active ?? true, }); const toggleEvent = (ev: string) => { setForm((prev) => ({ ...prev, events: prev.events.includes(ev) ? prev.events.filter((e) => e !== ev) : [...prev.events, ev], })); }; return (
setForm((p) => ({ ...p, name: e.target.value }))} placeholder="My Webhook" className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 placeholder-gray-600 focus:border-cyan-500 focus:outline-none" />
setForm((p) => ({ ...p, url: e.target.value }))} placeholder="https://hooks.example.com/..." className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 placeholder-gray-600 focus:border-cyan-500 focus:outline-none" />
setForm((p) => ({ ...p, secret: e.target.value }))} placeholder="supersecretkey" className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 placeholder-gray-600 focus:border-cyan-500 focus:outline-none" />
{AVAILABLE_EVENTS.map((ev) => ( ))}
setForm((p) => ({ ...p, is_active: v }))} />
); } function WebhooksSection() { const qc = useQueryClient(); const [toast, setToast] = useState<{ msg: string; type: "success" | "error" } | null>(null); const [creating, setCreating] = useState(false); const [editingId, setEditingId] = useState(null); const { data: webhooks = [], isLoading } = useQuery({ queryKey: ["webhooks"], queryFn: getWebhooks, }); const createMut = useMutation({ mutationFn: createWebhook, onSuccess: () => { qc.invalidateQueries({ queryKey: ["webhooks"] }); setCreating(false); setToast({ msg: "Webhook created", type: "success" }); }, onError: () => setToast({ msg: "Failed to create webhook", type: "error" }), }); const updateMut = useMutation({ mutationFn: ({ id, data }: { id: string; data: Partial }) => updateWebhook(id, data), onSuccess: () => { qc.invalidateQueries({ queryKey: ["webhooks"] }); setEditingId(null); setToast({ msg: "Webhook updated", type: "success" }); }, onError: () => setToast({ msg: "Failed to update webhook", type: "error" }), }); const deleteMut = useMutation({ mutationFn: deleteWebhook, onSuccess: () => { qc.invalidateQueries({ queryKey: ["webhooks"] }); setToast({ msg: "Webhook deleted", type: "success" }); }, onError: () => setToast({ msg: "Failed to delete webhook", type: "error" }), }); const testMut = useMutation({ mutationFn: testWebhook, onSuccess: () => setToast({ msg: "Test ping dispatched", type: "success" }), onError: () => setToast({ msg: "Ping failed", type: "error" }), }); if (isLoading) { return (
); } return ( <> {toast && ( setToast(null)} /> )}
{webhooks.length === 0 && !creating && (

No webhooks configured yet.

)} {webhooks.map((wh) => editingId === wh.id ? ( updateMut.mutate({ id: wh.id, data })} onCancel={() => setEditingId(null)} isSaving={updateMut.isPending} /> ) : ( setEditingId(wh.id)} onDelete={() => deleteMut.mutate(wh.id)} onTest={() => testMut.mutate(wh.id)} isDeleting={deleteMut.isPending} isTesting={testMut.isPending} /> ) )} {creating && ( createMut.mutate(data)} onCancel={() => setCreating(false)} isSaving={createMut.isPending} /> )} {!creating && ( )}
); } function WebhookRow({ wh, onEdit, onDelete, onTest, isDeleting, isTesting, }: { wh: WebhookOut; onEdit: () => void; onDelete: () => void; onTest: () => void; isDeleting: boolean; isTesting: boolean; }) { return (
{wh.name} {wh.is_active ? "active" : "inactive"} {wh.failure_count > 0 && ( {wh.failure_count} failures )}

{wh.url}

{wh.events.map((ev) => ( {ev} ))}
); } // --------------------------------------------------------------------------- // Notification Preferences Section (all users) // --------------------------------------------------------------------------- const PREF_DEFS: { key: keyof NotificationPreferences; label: string; description?: string; roles: string[]; // roles that see this pref }[] = [ { key: "email_on_test_validated", label: "Email on test validated", description: "Receive an email when a test you're involved with gets validated", roles: ["admin", "red_lead", "blue_lead", "red_tech", "blue_tech", "viewer"], }, { key: "email_on_test_rejected", label: "Email on test rejected", description: "Receive an email when a test you submitted gets rejected", roles: ["admin", "red_lead", "blue_lead", "red_tech", "blue_tech"], }, { key: "email_on_campaign_completed", label: "Email on campaign completed", roles: ["admin", "red_lead", "blue_lead", "red_tech", "blue_tech"], }, { key: "email_on_new_mitre_techniques", label: "Email on new MITRE techniques", description: "Get notified when the MITRE ATT&CK sync adds new techniques", roles: ["admin", "red_lead", "blue_lead"], }, { key: "email_on_stale_coverage", label: "Email on stale coverage alert", description: "Notified when techniques haven't been tested in a long time", roles: ["admin", "red_lead", "blue_lead"], }, { key: "email_on_assigned_to_campaign", label: "Email when assigned to a campaign", roles: ["admin", "red_lead", "blue_lead", "red_tech", "blue_tech"], }, { key: "email_on_test_state_change", label: "Email on test state change", description: "Notified when tests move through the validation workflow", roles: ["admin", "red_lead", "blue_lead", "red_tech", "blue_tech"], }, { key: "email_on_all_team_validations", label: "Email on all team validations", description: "Lead-level: receive email for every validation in your team", roles: ["admin", "red_lead", "blue_lead"], }, { key: "email_on_webhook_failures", label: "Email on webhook delivery failures", roles: ["admin", "red_lead", "blue_lead"], }, { key: "email_on_new_users", label: "Email on new user registration", roles: ["admin"], }, { key: "email_on_system_errors", label: "Email on system errors", description: "Critical errors in background jobs and integrations", roles: ["admin"], }, { key: "in_app_all", label: "In-app notifications", description: "Show notification bell alerts inside Aegis", roles: ["admin", "red_lead", "blue_lead", "red_tech", "blue_tech", "viewer"], }, ]; const DEFAULT_PREFS: NotificationPreferences = { email_on_test_validated: true, email_on_test_rejected: true, email_on_campaign_completed: true, email_on_new_mitre_techniques: false, email_on_stale_coverage: false, email_on_assigned_to_campaign: true, email_on_test_state_change: true, email_on_all_team_validations: false, email_on_webhook_failures: true, email_on_new_users: false, email_on_system_errors: true, in_app_all: true, }; function NotificationSection() { const qc = useQueryClient(); const { user } = useAuth(); const role = user?.role ?? "viewer"; const [toast, setToast] = useState<{ msg: string; type: "success" | "error" } | null>(null); const { data: me, isLoading } = useQuery({ queryKey: ["me-prefs"], queryFn: getMe, }); const [localPrefs, setLocalPrefs] = useState>({}); const [jiraAccountId, setJiraAccountId] = useState(""); const [jiraIdDirty, setJiraIdDirty] = useState(false); const effectivePrefs: NotificationPreferences = { ...DEFAULT_PREFS, ...(me?.notification_preferences ?? {}), ...localPrefs, }; const saveMut = useMutation({ mutationFn: updateMyPreferences, onSuccess: () => { qc.invalidateQueries({ queryKey: ["me-prefs"] }); setLocalPrefs({}); setJiraIdDirty(false); setToast({ msg: "Preferences saved", type: "success" }); }, onError: () => setToast({ msg: "Failed to save preferences", type: "error" }), }); const handleSave = () => { const payload: Parameters[0] = {}; if (Object.keys(localPrefs).length > 0) { payload.notification_preferences = { ...(me?.notification_preferences ?? {}), ...localPrefs, }; } if (jiraIdDirty) { payload.jira_account_id = jiraAccountId || null; } if (Object.keys(payload).length > 0) { saveMut.mutate(payload); } }; const isDirty = Object.keys(localPrefs).length > 0 || jiraIdDirty; if (isLoading) { return (
); } const visiblePrefs = PREF_DEFS.filter((d) => d.roles.includes(role)); return ( <> {toast && ( setToast(null)} /> )}
{visiblePrefs.map((def) => ( setLocalPrefs((prev) => ({ ...prev, [def.key]: v }))} /> ))}
); } // --------------------------------------------------------------------------- // Profile / Jira Section (all users) // --------------------------------------------------------------------------- function ProfileSection() { const { data: me, isLoading } = useQuery({ queryKey: ["me-prefs"], queryFn: getMe, }); if (isLoading) { return (
); } return (

Username

{me?.username}

Role

{me?.role?.replace("_", " ")}

Email

{me?.email ?? "—"}

Last Login

{me?.last_login ? new Date(me.last_login).toLocaleString("en-US", { timeZone: "UTC", year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }) + " UTC" : "—"}

Jira / Tempo

Your Atlassian account ID is detected automatically on login using the admin Jira account. No personal tokens required.

Account ID: {me?.jira_account_id ? ( {me.jira_account_id} ) : ( Not detected yet — log out and back in after admin configures Jira )}
); } // --------------------------------------------------------------------------- // Jira Admin Config Section (admin only) // --------------------------------------------------------------------------- function JiraConfigSection() { const qc = useQueryClient(); const [toast, setToast] = useState<{ msg: string; type: "success" | "error" } | null>(null); const [showAdminToken, setShowAdminToken] = useState(false); const [showTempoToken, setShowTempoToken] = useState(false); const [jiraTestResult, setJiraTestResult] = useState<{ connectedAs: string; url: string } | null>(null); const [jiraTestError, setJiraTestError] = useState(null); const [tempoTestResult, setTempoTestResult] = useState(null); const [tempoTestError, setTempoTestError] = useState(null); const { data: cfg, isLoading } = useQuery({ queryKey: ["jira-config"], queryFn: getJiraConfig, }); const [form, setForm] = useState({}); const saveMut = useMutation({ mutationFn: updateJiraConfig, onSuccess: () => { qc.invalidateQueries({ queryKey: ["jira-config"] }); setForm({}); setToast({ msg: "Jira configuration saved", type: "success" }); }, onError: () => setToast({ msg: "Failed to save Jira configuration", type: "error" }), }); const jiraTestMut = useMutation({ mutationFn: testJiraConnection, onSuccess: (data) => { if (data.status === "ok") { setJiraTestResult({ connectedAs: data.connected_as ?? "", url: data.jira_url ?? "" }); setJiraTestError(null); } else { setJiraTestError((data as { message?: string }).message ?? "Connection failed"); setJiraTestResult(null); } }, onError: (err: Error) => { setJiraTestError(err.message || "Connection failed"); setJiraTestResult(null); }, }); const tempoTestMut = useMutation({ mutationFn: testTempoConnection, onSuccess: (data) => { if (data.status === "ok") { setTempoTestResult(data.message ?? "Connected"); setTempoTestError(null); } else { setTempoTestError(data.message ?? "Tempo test failed"); setTempoTestResult(null); } }, onError: (err: Error) => { setTempoTestError(err.message || "Tempo test failed"); setTempoTestResult(null); }, }); if (isLoading) { return (
); } return ( <> {toast && ( setToast(null)} /> )}
setForm((prev) => ({ ...prev, enabled: v }))} />
setForm((prev) => ({ ...prev, url: e.target.value }))} placeholder="https://yourcompany.atlassian.net" className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 placeholder-gray-600 focus:border-cyan-500 focus:outline-none" />

Base URL of your Jira instance

setForm((prev) => ({ ...prev, project_key: e.target.value }))} placeholder="SEC" className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 placeholder-gray-600 focus:border-cyan-500 focus:outline-none" />

Jira project where test tickets will be created

setForm((prev) => ({ ...prev, parent_ticket: e.target.value }))} placeholder="OFS-20795" className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 placeholder-gray-600 focus:border-cyan-500 focus:outline-none" />

Campaigns are created directly under this issue

setForm((prev) => ({ ...prev, parent_ticket_standalone: e.target.value }))} placeholder="OFS-20798" className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 placeholder-gray-600 focus:border-cyan-500 focus:outline-none" />

Standalone tests are nested here. Falls back to Campaign Parent if not set.

{/* ── Admin account (replaces per-user tokens) ───────────── */}

Admin Jira Account

One admin account handles all Jira and Tempo operations. Regular users need zero configuration — their Atlassian account ID is auto-detected on login.

setForm((prev) => ({ ...prev, admin_email: e.target.value }))} placeholder="jira-admin@company.com" className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 placeholder-gray-600 focus:border-cyan-500 focus:outline-none" />
{cfg?.admin_email_set ? ( Configured ) : ( Not configured )} Leave blank to keep current
setForm((prev) => ({ ...prev, admin_api_token: e.target.value }))} placeholder={cfg?.admin_email_set ? "Leave blank to keep current token" : "Paste Atlassian API token here"} className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 pr-10 text-sm text-gray-200 placeholder-gray-600 focus:border-cyan-500 focus:outline-none" />

Create at{" "} id.atlassian.com →

setForm((prev) => ({ ...prev, tempo_admin_token: e.target.value }))} placeholder={cfg?.tempo_admin_token_set ? "Leave blank to keep current token" : "Paste Tempo API token here"} className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 pr-10 text-sm text-gray-200 placeholder-gray-600 focus:border-purple-500 focus:outline-none" />
{cfg?.tempo_admin_token_set ? ( Configured ) : ( Not configured )} Jira → Apps → Tempo → Settings → API Integration
{/* ── Test connections ───────────────────────────────────── */}

Test Jira connection

{jiraTestResult && (
Connected as: {jiraTestResult.connectedAs}
)} {jiraTestError && (
{jiraTestError}
)}

Test Tempo connection

{tempoTestResult && (
{tempoTestResult}
)} {tempoTestError && (
{tempoTestError}
)}
); } // --------------------------------------------------------------------------- // SSO / Azure AD Config Section (admin only) // --------------------------------------------------------------------------- const AZURE_ROLES = [ { value: "admin", label: "Aegis Admin", desc: "Full platform access including system settings" }, { value: "red_lead", label: "Aegis Red Lead", desc: "Red team lead — manage tests, campaigns and templates" }, { value: "blue_lead", label: "Aegis Blue Lead", desc: "Blue team lead — validate tests and manage coverage" }, { value: "manager", label: "Aegis Manager", desc: "Approves campaigns and campaign changes" }, { value: "red_tech", label: "Aegis Red Tech", desc: "Red team technician — execute tests" }, { value: "blue_tech", label: "Aegis Blue Tech", desc: "Blue team technician — review detections" }, { value: "viewer", label: "Aegis Viewer", desc: "Read-only access to dashboards and reports" }, ]; function CopyFieldSSO({ value, label }: { value: string; label: string }) { const [copied, setCopied] = useState(false); const copy = () => { navigator.clipboard.writeText(value).then(() => { setCopied(true); setTimeout(() => setCopied(false), 2000); }); }; return (
{value || not set}
); } function SsoConfigSection() { const qc = useQueryClient(); const origin = typeof window !== "undefined" ? window.location.origin : ""; const defaultSpEntityId = `${origin}/api/v1/sso/metadata`; const defaultSpAcsUrl = `${origin}/api/v1/sso/callback`; const { data: existingConfig, isLoading: configLoading } = useQuery({ queryKey: ["sso-config"], queryFn: async () => { try { return await getSsoConfig(); } catch { return null; } }, }); const [tenantId, setTenantId] = useState(""); const [form, setForm] = useState({ is_enabled: false, provider_name: "Azure AD / Entra ID", sp_entity_id: defaultSpEntityId, sp_acs_url: defaultSpAcsUrl, idp_entity_id: "", idp_sso_url: "", idp_certificate: "", attr_email: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", attr_username: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", attr_role: "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", default_role: "viewer", auto_provision: true, }); const [formLoaded, setFormLoaded] = useState(false); const [saveSuccess, setSaveSuccess] = useState(false); if (existingConfig && !formLoaded) { setForm({ is_enabled: existingConfig.is_enabled, provider_name: existingConfig.provider_name ?? "Azure AD / Entra ID", sp_entity_id: existingConfig.sp_entity_id ?? defaultSpEntityId, sp_acs_url: existingConfig.sp_acs_url ?? defaultSpAcsUrl, idp_entity_id: existingConfig.idp_entity_id ?? "", idp_sso_url: existingConfig.idp_sso_url ?? "", idp_certificate: existingConfig.idp_certificate ?? "", attr_email: existingConfig.attr_email ?? "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", attr_username: existingConfig.attr_username ?? "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", attr_role: existingConfig.attr_role ?? "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", default_role: existingConfig.default_role ?? "viewer", auto_provision: existingConfig.auto_provision ?? true, }); if (existingConfig.idp_sso_url) { const m = existingConfig.idp_sso_url.match(/microsoftonline\.com\/([^/]+)\//); if (m) setTenantId(m[1]); } setFormLoaded(true); } const handleTenantChange = (val: string) => { setTenantId(val); if (val.trim()) { setForm((f) => ({ ...f, idp_entity_id: `https://sts.windows.net/${val.trim()}/`, idp_sso_url: `https://login.microsoftonline.com/${val.trim()}/saml2`, })); } }; const saveMutation = useMutation({ mutationFn: (payload: SsoConfigUpdate) => updateSsoConfig(payload), onSuccess: () => { setSaveSuccess(true); setTimeout(() => setSaveSuccess(false), 4000); qc.invalidateQueries({ queryKey: ["sso-config"] }); qc.invalidateQueries({ queryKey: ["sso-status"] }); }, }); const handleSave = (e: React.FormEvent) => { e.preventDefault(); saveMutation.mutate(form); }; const isConfigured = !!(form.idp_entity_id && form.idp_sso_url && form.idp_certificate); return (

Azure AD / Entra ID SSO

Delegate authentication to Azure Active Directory via SAML 2.0. Users sign in with corporate credentials; roles assigned automatically via Azure App Roles.

{form.is_enabled && isConfigured ? <> Active : <> {isConfigured ? "Disabled" : "Not configured"}}
{configLoading ? (
) : (
{/* Step 1 */}
1

Give these values to your IT team

{/* Step 2 */}
2

Create App Roles in Azure

{AZURE_ROLES.map((r) => ( ))}
Display Name Value (exact) Description
{r.label} {r.value} {r.desc}
{/* Step 3 */}
3

Enter Azure tenant details

handleTenantChange(e.target.value)} placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 placeholder-gray-500 focus:border-indigo-500 focus:outline-none font-mono" />

Azure AD → Overview → Tenant ID

setForm({ ...form, idp_entity_id: e.target.value })} placeholder="https://sts.windows.net/{tenant-id}/" className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 placeholder-gray-500 focus:border-indigo-500 focus:outline-none font-mono" />
setForm({ ...form, idp_sso_url: e.target.value })} placeholder="https://login.microsoftonline.com/{tenant-id}/saml2" className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 placeholder-gray-500 focus:border-indigo-500 focus:outline-none font-mono" />
setForm({ ...form, provider_name: e.target.value })} placeholder="Azure AD / Entra ID" className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 placeholder-gray-500 focus:border-indigo-500 focus:outline-none" />
{/* Step 4 */}
4

Paste IdP certificate