diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx
index 80cc8d8..b9f4b71 100644
--- a/frontend/src/pages/SettingsPage.tsx
+++ b/frontend/src/pages/SettingsPage.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from "react";
+import { useState, useEffect, useRef } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
Settings,
@@ -19,8 +19,26 @@ import {
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,
@@ -1308,11 +1326,484 @@ function JiraConfigSection() {
);
}
+// ---------------------------------------------------------------------------
+// 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: "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 (
+
+
{label}
+
+
+ {value || not set }
+
+
+ {copied ? : }
+
+
+
+ );
+}
+
+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 ? (
+
+ ) : (
+
+ )}
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Export / Import Configuration (admin only)
+// ---------------------------------------------------------------------------
+
+function ExportImportSection() {
+ const fileRef = useRef(null);
+ const [importing, setImporting] = useState(false);
+ const [importResult, setImportResult] = useState<{
+ status: string;
+ summary: Record;
+ warnings: string[];
+ } | null>(null);
+ const [importError, setImportError] = useState(null);
+
+ const handleExport = async () => {
+ try {
+ const resp = await client.get("/admin/export-config", { responseType: "blob" });
+ const url = URL.createObjectURL(resp.data);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `aegis-config-${new Date().toISOString().slice(0, 10)}.json`;
+ a.click();
+ URL.revokeObjectURL(url);
+ } catch (e) {
+ alert("Export failed: " + (e as Error).message);
+ }
+ };
+
+ const handleImportFile = async (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+ e.target.value = "";
+ setImportError(null);
+ setImportResult(null);
+ setImporting(true);
+ try {
+ const json = JSON.parse(await file.text());
+ const resp = await client.post("/admin/import-config", json);
+ setImportResult(resp.data);
+ } catch (err: unknown) {
+ const msg =
+ (err as { response?: { data?: { detail?: string } } })?.response?.data?.detail ??
+ (err as Error)?.message ?? "Import failed";
+ setImportError(String(msg));
+ } finally {
+ setImporting(false);
+ }
+ };
+
+ return (
+
+
+
+
Configuration Export / Import
+
+
+ Export platform configuration to JSON for backup or migration. Import restores settings,
+ webhooks, templates and users on a fresh instance.
+
+
+
What is included
+
+ {[
+ ["✅", "Email & Jira settings"],
+ ["✅", "SSO / SAML configuration"],
+ ["✅", "Webhooks"],
+ ["✅", "Scoring weights"],
+ ["✅", "Custom test templates"],
+ ["✅", "Users (roles & status)"],
+ ["🔒", "Passwords / API tokens → REDACTED"],
+ ["❌", "Tests, campaigns, techniques data"],
+ ].map(([icon, text]) => (
+
{icon} {text}
+ ))}
+
+
+
+
+ Export Configuration
+
+
+ fileRef.current?.click()} disabled={importing}
+ className="flex items-center gap-2 rounded-lg border border-gray-700 bg-gray-800 px-4 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 disabled:opacity-50 transition-colors">
+ {importing ? : }
+ {importing ? "Importing…" : "Import Configuration"}
+
+
+ {importResult && (
+
+
+ Import completed successfully
+
+
+ {Object.entries(importResult.summary).map(([key, val]) => (
+
+ {key.replace(/_/g, " ")}
+ {val}
+
+ ))}
+
+ {importResult.warnings.length > 0 && (
+
+ {importResult.warnings.map((w) => {w} )}
+
+ )}
+
+ )}
+ {importError && (
+
⚠ {importError}
+ )}
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// System Information + Version (admin only)
+// ---------------------------------------------------------------------------
+
+function SystemInfoSection() {
+ return (
+
+
+
+ System Status
+
+
+ {[
+ { icon: Server, label: "Backend", value: "Online", color: "text-green-400" },
+ { icon: Database, label: "PostgreSQL", value: "Connected", color: "text-green-400" },
+ { icon: HardDrive, label: "MinIO Storage", value: "Available", color: "text-green-400" },
+ { icon: Clock, label: "Scheduler", value: "Running", color: "text-green-400" },
+ ].map(({ icon: Icon, label, value, color }) => (
+
+ ))}
+
+
+
+
+
Version Information
+
+
Platform Aegis v0.1.0
+
Backend FastAPI + Python 3.11
+
Frontend React 19 + TypeScript
+
Database PostgreSQL 15
+
+
+
+ );
+}
+
// ---------------------------------------------------------------------------
// Main SettingsPage
// ---------------------------------------------------------------------------
-type Tab = "profile" | "notifications" | "webhooks" | "email" | "jira";
+type Tab = "profile" | "notifications" | "webhooks" | "email" | "jira" | "sso" | "system";
export default function SettingsPage() {
const { user } = useAuth();
@@ -1334,6 +1825,8 @@ export default function SettingsPage() {
},
{ id: "email", label: "Email / SMTP", icon: Mail, show: isAdmin },
{ id: "jira", label: "Jira", icon: Link2, show: isAdmin },
+ { id: "sso", label: "SSO / Azure AD", icon: KeyRound, show: isAdmin },
+ { id: "system", label: "System", icon: Server, show: isAdmin },
];
const visibleTabs = tabs.filter((t) => t.show);
@@ -1398,6 +1891,15 @@ export default function SettingsPage() {
)}
+ {activeTab === "sso" && isAdmin && (
+
+ )}
+ {activeTab === "system" && isAdmin && (
+
+
+
+
+ )}
diff --git a/frontend/src/pages/SystemPage.tsx b/frontend/src/pages/SystemPage.tsx
index 353043d..95d63a9 100644
--- a/frontend/src/pages/SystemPage.tsx
+++ b/frontend/src/pages/SystemPage.tsx
@@ -1,12 +1,9 @@
-import { useState, useRef } from "react";
+import { useState, useRef } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
Loader2,
AlertCircle,
RefreshCw,
- Server,
- Database,
- HardDrive,
Clock,
CheckCircle,
XCircle,
@@ -19,25 +16,13 @@ import {
BarChart3,
X,
Download,
- Upload,
- PackageOpen,
Swords,
Sparkles,
AlertTriangle,
ExternalLink,
CalendarCheck,
ShieldCheck,
- KeyRound,
- Copy,
- Building2,
} from "lucide-react";
-import client from "../api/client";
-import {
- getSsoConfig,
- updateSsoConfig,
- type SsoConfig,
- type SsoConfigUpdate,
-} from "../api/sso";
import {
triggerMitreSync,
triggerIntelScan,
@@ -1084,66 +1069,6 @@ export default function SystemPage() {
)}
- {/* SSO / Azure AD Configuration */}
-
-
- {/* System Information */}
-
-
System Information
-
-
-
-
-
-
-
PostgreSQL
-
Connected
-
-
-
-
-
-
-
-
MinIO Storage
-
Available
-
-
-
-
-
-
-
-
Scheduler
-
- {statusLoading
- ? "Checking..."
- : schedulerStatus?.running
- ? "Running"
- : "Stopped"}
-
-
-
-
-
-
-
{/* Scheduled Jobs */}
Scheduled Jobs
@@ -1210,32 +1135,6 @@ export default function SystemPage() {
)}
- {/* Export / Import Configuration */}
-
-
- {/* Version Info */}
-
-
Version Information
-
-
-
Platform
- Aegis v0.1.0
-
-
-
Backend
- FastAPI + Python 3.11
-
-
-
Frontend
- React 19 + TypeScript
-
-
-
Database
- PostgreSQL 15
-
-
-
-
{/* Template Detail Modal */}
{selectedTemplateId && (
selectedTemplateLoading ? (
@@ -1257,143 +1156,6 @@ export default function SystemPage() {
);
}
-/* ── Export / Import Configuration ───────────────────────────────── */
-
-function ExportImportSection() {
- const fileRef = useRef(null);
- const [importing, setImporting] = useState(false);
- const [importResult, setImportResult] = useState<{
- status: string;
- summary: Record;
- warnings: string[];
- } | null>(null);
- const [importError, setImportError] = useState(null);
-
- const handleExport = async () => {
- try {
- const resp = await client.get("/admin/export-config", {
- responseType: "blob",
- });
- const url = URL.createObjectURL(resp.data);
- const a = document.createElement("a");
- a.href = url;
- const date = new Date().toISOString().slice(0, 10);
- a.download = `aegis-config-${date}.json`;
- a.click();
- URL.revokeObjectURL(url);
- } catch (e) {
- alert("Export failed: " + (e as Error).message);
- }
- };
-
- const handleImportFile = async (e: React.ChangeEvent) => {
- const file = e.target.files?.[0];
- if (!file) return;
- e.target.value = "";
- setImportError(null);
- setImportResult(null);
- setImporting(true);
- try {
- const text = await file.text();
- const json = JSON.parse(text);
- const resp = await client.post("/admin/import-config", json);
- setImportResult(resp.data);
- } catch (err: unknown) {
- const msg =
- (err as { response?: { data?: { detail?: string } } })?.response?.data?.detail ??
- (err as Error)?.message ??
- "Import failed";
- setImportError(String(msg));
- } finally {
- setImporting(false);
- }
- };
-
- return (
-
-
-
-
Configuration Export / Import
-
-
- Export all platform configuration to a JSON file for backup or migration.
- Import restores settings, webhooks, custom templates and users on a fresh instance.
-
-
- {/* What is exported */}
-
-
What is included
-
- {[
- ["✅", "Email & Jira settings"],
- ["✅", "SSO / SAML configuration"],
- ["✅", "Webhooks"],
- ["✅", "Scoring weights"],
- ["✅", "Custom test templates"],
- ["✅", "Users (roles & status)"],
- ["🔒", "Passwords / API tokens → REDACTED"],
- ["❌", "Tests, campaigns, techniques data"],
- ].map(([icon, text]) => (
-
- {icon}
- {text}
-
- ))}
-
-
-
- {/* Actions */}
-
-
-
- Export Configuration
-
-
-
- fileRef.current?.click()}
- disabled={importing}
- className="flex items-center gap-2 rounded-lg border border-gray-700 bg-gray-800 px-4 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 disabled:opacity-50 transition-colors"
- >
- {importing ? : }
- {importing ? "Importing…" : "Import Configuration"}
-
-
-
- {/* Import result */}
- {importResult && (
-
-
- Import completed successfully
-
-
- {Object.entries(importResult.summary).map(([key, val]) => (
-
- {key.replace(/_/g, " ")}
- {val}
-
- ))}
-
- {importResult.warnings.length > 0 && (
-
- {importResult.warnings.map((w) => {w} )}
-
- )}
-
- )}
-
- {importError && (
-
- ⚠ {importError}
-
- )}
-
- );
-}
-
/* ── Bulk Approve Evaluation Tests Modal ─────────────────────────── */
function BulkApproveModal({
@@ -1780,451 +1542,6 @@ function CreateTemplateForm({
);
}
-/* ── SSO / Azure AD Configuration Section ───────────────────────── */
-
-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: "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 CopyField({ 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 (
-
-
{label}
-
-
- {value || not set }
-
-
- {copied ? : }
-
-
-
- );
-}
-
-function SsoConfigSection() {
- const queryClient = useQueryClient();
-
- // Derive default SP values from the current browser origin
- const origin = 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; // 404 = not configured yet
- }
- },
- });
-
- // Form state — pre-seeded from existing config when loaded
- 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);
-
- // Load existing config into form (once)
- 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,
- });
- // Try to extract tenant ID from existing idp_sso_url
- if (existingConfig.idp_sso_url) {
- const m = existingConfig.idp_sso_url.match(/microsoftonline\.com\/([^/]+)\//);
- if (m) setTenantId(m[1]);
- }
- setFormLoaded(true);
- }
-
- // Auto-fill IdP URLs when tenant ID is entered
- 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);
- queryClient.invalidateQueries({ queryKey: ["sso-config"] });
- queryClient.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 (
-
- {/* Header */}
-
-
-
-
-
-
-
Azure AD / Entra ID SSO
-
- Delegate authentication to Azure Active Directory via SAML 2.0.
- Users sign in with their corporate credentials; roles are assigned automatically via Azure App Roles.
-
-
-
- {/* Enabled badge */}
-
- {form.is_enabled && isConfigured ? (
- <> Active>
- ) : (
- <> {isConfigured ? "Disabled" : "Not configured"}>
- )}
-
-
-
- {configLoading ? (
-
-
-
- ) : (
-
-
- {/* ── Step 1: SP values for IT ─────────────────────────── */}
-
-
- 1
-
Give these values to your IT team
-
-
- Your IT team needs to register Aegis as an Enterprise Application in Azure AD.
- Provide them with the following SP (Service Provider) values:
-
-
-
-
- {/* ── Step 2: Azure App Roles to create ───────────────── */}
-
-
- 2
-
Create App Roles in Azure
-
-
- In your Azure Enterprise App → App Roles, create the following roles. The{" "}
- Value field must match exactly (case-sensitive):
-
-
-
-
-
- Display Name
- Value (exact)
- Description
-
-
-
- {AZURE_ROLES.map((r) => (
-
- {r.label}
-
-
- {r.value}
-
-
- {r.desc}
-
- ))}
-
-
-
-
- After creating roles, assign them to users or groups in Azure AD → Enterprise Applications → Users and groups.
-
-
-
- {/* ── Step 3: IdP configuration ───────────────────────── */}
-
-
- 3
-
Enter Azure tenant details
-
-
- {/* Tenant ID shortcut */}
-
-
- Tenant ID (auto-fills the fields below)
-
-
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"
- />
-
- Found in Azure AD → Overview → Tenant ID
-
-
-
- {/* IdP Entity ID */}
-
-
- IdP Entity 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"
- />
-
-
- {/* IdP SSO URL */}
-
-
- SSO URL (Login URL) *
-
- 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"
- />
-
-
- {/* Provider display name */}
-
-
- Display name (shown on login page)
-
- 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: X.509 Certificate ───────────────────────── */}
-
-
- 4
-
Paste IdP certificate
-
-
- In Azure AD → Enterprise App → Single sign-on → SAML Signing Certificate, download the{" "}
- Base64 certificate and paste its contents below.
-
-
setForm({ ...form, idp_certificate: e.target.value })}
- placeholder={"-----BEGIN CERTIFICATE-----\nMIIC...base64...\n-----END CERTIFICATE-----"}
- rows={6}
- className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-xs text-gray-300 placeholder-gray-500 focus:border-indigo-500 focus:outline-none font-mono"
- />
-
- Include the header and footer lines, or paste just the base64 block — both are accepted.
-
-
-
- {/* ── Step 5: Attribute mapping ────────────────────────── */}
-
-
- 5
-
Attribute mapping
- (pre-filled with Azure AD defaults)
-
-
-
- Email attribute
- setForm({ ...form, attr_email: e.target.value })}
- className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-xs text-gray-300 focus:border-indigo-500 focus:outline-none font-mono"
- />
-
-
- Username attribute
- setForm({ ...form, attr_username: e.target.value })}
- className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-xs text-gray-300 focus:border-indigo-500 focus:outline-none font-mono"
- />
-
-
- Role attribute
- setForm({ ...form, attr_role: e.target.value })}
- className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-xs text-gray-300 focus:border-indigo-500 focus:outline-none font-mono"
- />
-
-
-
- Default role
- setForm({ ...form, default_role: e.target.value })}
- className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 focus:border-indigo-500 focus:outline-none"
- >
- {AZURE_ROLES.map((r) => (
- {r.label}
- ))}
-
-
-
-
- setForm({ ...form, auto_provision: e.target.checked })}
- className="h-4 w-4 rounded accent-indigo-500"
- />
- Auto-provision new users
-
-
-
-
-
-
- {/* ── Enable toggle + Save ─────────────────────────────── */}
-
-
- setForm((f) => ({ ...f, is_enabled: !f.is_enabled }))}
- className={`relative h-6 w-11 rounded-full transition-colors ${
- form.is_enabled ? "bg-indigo-600" : "bg-gray-600"
- }`}
- >
-
-
-
- {form.is_enabled ? "SSO enabled — Azure AD is the primary login" : "SSO disabled — local login only"}
-
-
-
-
- {saveSuccess && (
-
- Saved
-
- )}
- {saveMutation.isError && (
-
- {(saveMutation.error as Error)?.message ?? "Save failed"}
-
- )}
-
- {saveMutation.isPending ? (
-
- ) : (
-
- )}
- {saveMutation.isPending ? "Saving…" : "Save configuration"}
-
-
-
-
- )}
-
- );
-}
/* ── Template Detail / Edit Modal ────────────────────────────────── */