'use client' import React, { useState, useEffect, useCallback } from 'react' import { Shield, Plus, Pencil, Trash2, ExternalLink, Copy, CheckCircle2, XCircle, Loader2, Users, Key, Globe, Info, ChevronDown, ChevronUp, TestTube, RefreshCw, X, Eye, EyeOff, AlertTriangle } from 'lucide-react' // ============================================================================= // TYPES // ============================================================================= interface SSOConfig { id: string tenant_id: string provider_type: 'oidc' | 'saml' name: string enabled: boolean oidc_issuer_url: string oidc_client_id: string oidc_client_secret: string oidc_redirect_uri: string oidc_scopes: string[] role_mapping: Record default_role_id: string | null auto_provision: boolean created_at: string updated_at: string } interface RoleMapping { sso_group: string internal_role: string } interface SSOUser { id: string tenant_id: string sso_config_id: string external_id: string email: string display_name: string groups: string[] last_login: string | null is_active: boolean created_at: string updated_at: string } interface SSOFormData { name: string provider_type: 'oidc' issuer_url: string client_id: string client_secret: string redirect_uri: string scopes: string auto_provision: boolean default_role: string role_mappings: RoleMapping[] } interface ConnectionTestResult { success: boolean message: string details?: { issuer_reachable: boolean jwks_available: boolean authorization_endpoint: string token_endpoint: string } } type TabId = 'configs' | 'users' | 'info' // ============================================================================= // CONSTANTS // ============================================================================= const FALLBACK_TENANT_ID = '00000000-0000-0000-0000-000000000001' const FALLBACK_USER_ID = '00000000-0000-0000-0000-000000000001' const API_BASE = '/api/sdk/v1/sso' const DEFAULT_SCOPES = 'openid,email,profile' const AVAILABLE_ROLES = [ { value: 'user', label: 'Benutzer' }, { value: 'admin', label: 'Administrator' }, { value: 'data_protection_officer', label: 'Datenschutzbeauftragter' }, { value: 'auditor', label: 'Auditor' }, { value: 'viewer', label: 'Nur Lesen' }, ] const EMPTY_FORM: SSOFormData = { name: '', provider_type: 'oidc', issuer_url: '', client_id: '', client_secret: '', redirect_uri: '', scopes: DEFAULT_SCOPES, auto_provision: true, default_role: 'user', role_mappings: [], } // ============================================================================= // HELPERS // ============================================================================= function getTenantId(): string { if (typeof window !== 'undefined') { return localStorage.getItem('sdk-tenant-id') || FALLBACK_TENANT_ID } return FALLBACK_TENANT_ID } function getUserId(): string { if (typeof window !== 'undefined') { return localStorage.getItem('sdk-user-id') || FALLBACK_USER_ID } return FALLBACK_USER_ID } function getDefaultRedirectUri(): string { if (typeof window !== 'undefined') { return `${window.location.origin}/api/sdk/v1/sso/oidc/callback` } return '' } function formatDate(dateStr: string | null): string { if (!dateStr) return '-' return new Date(dateStr).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }) } async function apiFetch( endpoint: string, options: RequestInit = {} ): Promise { const res = await fetch(`${API_BASE}${endpoint}`, { ...options, headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': getTenantId(), 'X-User-ID': getUserId(), ...options.headers, }, }) if (!res.ok) { const body = await res.json().catch(() => ({})) throw new Error(body.error || body.message || `HTTP ${res.status}`) } return res.json() } // ============================================================================= // SUB-COMPONENTS // ============================================================================= function LoadingSkeleton({ rows = 3 }: { rows?: number }) { return (
{Array.from({ length: rows }).map((_, i) => (
))}
) } function EmptyState({ icon, title, description, action, }: { icon: React.ReactNode title: string description: string action?: React.ReactNode }) { return (
{icon}

{title}

{description}

{action &&
{action}
}
) } function StatusBadge({ enabled }: { enabled: boolean }) { return enabled ? ( Aktiv ) : ( Deaktiviert ) } function CopyButton({ value }: { value: string }) { const [copied, setCopied] = useState(false) const handleCopy = () => { navigator.clipboard.writeText(value) setCopied(true) setTimeout(() => setCopied(false), 2000) } return ( ) } // ============================================================================= // SSO CONFIG FORM MODAL // ============================================================================= function SSOConfigFormModal({ isOpen, editConfig, onClose, onSave, }: { isOpen: boolean editConfig: SSOConfig | null onClose: () => void onSave: (data: SSOFormData) => Promise }) { const [formData, setFormData] = useState(EMPTY_FORM) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const [showSecret, setShowSecret] = useState(false) useEffect(() => { if (editConfig) { setFormData({ name: editConfig.name, provider_type: 'oidc', issuer_url: editConfig.oidc_issuer_url || '', client_id: editConfig.oidc_client_id || '', client_secret: '', redirect_uri: editConfig.oidc_redirect_uri || '', scopes: (editConfig.oidc_scopes || []).join(','), auto_provision: editConfig.auto_provision, default_role: editConfig.default_role_id || 'user', role_mappings: Object.entries(editConfig.role_mapping || {}).map(([k, v]) => ({ sso_group: k, internal_role: v })), }) } else { setFormData({ ...EMPTY_FORM, redirect_uri: getDefaultRedirectUri(), }) } setError(null) setShowSecret(false) }, [editConfig, isOpen]) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setError(null) if (!formData.name.trim()) { setError('Name ist erforderlich') return } if (!formData.issuer_url.trim()) { setError('Issuer URL ist erforderlich') return } if (!formData.client_id.trim()) { setError('Client ID ist erforderlich') return } if (!editConfig && !formData.client_secret.trim()) { setError('Client Secret ist erforderlich') return } setSaving(true) try { await onSave(formData) onClose() } catch (err: unknown) { setError(err instanceof Error ? err.message : 'Fehler beim Speichern') } finally { setSaving(false) } } const addRoleMapping = () => { setFormData(prev => ({ ...prev, role_mappings: [...prev.role_mappings, { sso_group: '', internal_role: 'user' }], })) } const updateRoleMapping = (index: number, field: keyof RoleMapping, value: string) => { setFormData(prev => ({ ...prev, role_mappings: prev.role_mappings.map((m, i) => i === index ? { ...m, [field]: value } : m ), })) } const removeRoleMapping = (index: number) => { setFormData(prev => ({ ...prev, role_mappings: prev.role_mappings.filter((_, i) => i !== index), })) } if (!isOpen) return null return (
{/* Header */}

{editConfig ? 'SSO-Konfiguration bearbeiten' : 'Neue SSO-Konfiguration'}

{/* Form */}
{error && (
{error}
)} {/* Provider Type */}
{/* Name */}
setFormData(prev => ({ ...prev, name: e.target.value }))} placeholder="z.B. Azure AD, Okta, Keycloak" className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500" />
{/* Issuer URL */}
setFormData(prev => ({ ...prev, issuer_url: e.target.value }))} placeholder="https://login.microsoftonline.com/{tenant-id}/v2.0" className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm font-mono focus:ring-2 focus:ring-purple-500 focus:border-purple-500" />

Die Discovery-URL wird automatisch unter /.well-known/openid-configuration abgefragt

{/* Client ID */}
setFormData(prev => ({ ...prev, client_id: e.target.value }))} placeholder="Application (client) ID" className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm font-mono focus:ring-2 focus:ring-purple-500 focus:border-purple-500" />
{/* Client Secret */}
setFormData(prev => ({ ...prev, client_secret: e.target.value }))} placeholder={editConfig ? '********' : 'Client Secret eingeben'} className="w-full px-3 py-2 pr-10 border border-slate-300 rounded-lg text-sm font-mono focus:ring-2 focus:ring-purple-500 focus:border-purple-500" />
{/* Redirect URI */}
setFormData(prev => ({ ...prev, redirect_uri: e.target.value }))} className="flex-1 px-3 py-2 border border-slate-300 rounded-lg text-sm font-mono bg-slate-50 focus:ring-2 focus:ring-purple-500 focus:border-purple-500" />

Diese URI muss im Identity Provider als erlaubte Redirect-URI eingetragen werden

{/* Scopes */}
setFormData(prev => ({ ...prev, scopes: e.target.value }))} placeholder="openid,email,profile" className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500" />

Komma-getrennte Liste von OIDC Scopes

{/* Auto-Provision Toggle */}

Benutzer automatisch beim ersten Login anlegen

{/* Default Role */}

Rolle fuer neu provisionierte Benutzer ohne Rollen-Mapping

{/* Role Mappings */}
{formData.role_mappings.length === 0 ? (

Keine Rollen-Mappings konfiguriert. Alle Benutzer erhalten die Standard-Rolle.

) : (
{formData.role_mappings.map((mapping, idx) => (
updateRoleMapping(idx, 'sso_group', e.target.value)} placeholder="SSO-Gruppe (z.B. admins)" className="flex-1 px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500" />
))}
)}
{/* Footer */}
) } // ============================================================================= // DELETE CONFIRMATION MODAL // ============================================================================= function DeleteConfirmModal({ config, onClose, onConfirm, }: { config: SSOConfig | null onClose: () => void onConfirm: () => Promise }) { const [deleting, setDeleting] = useState(false) if (!config) return null const handleDelete = async () => { setDeleting(true) try { await onConfirm() onClose() } finally { setDeleting(false) } } return (

Konfiguration loeschen?

Diese Aktion kann nicht rueckgaengig gemacht werden.

Die SSO-Konfiguration "{config.name}" wird unwiderruflich geloescht. Alle ueber diese Konfiguration provisionierten Benutzer verlieren den SSO-Zugang.

) } // ============================================================================= // CONNECTION TEST PANEL // ============================================================================= function ConnectionTestPanel({ config }: { config: SSOConfig }) { const [testing, setTesting] = useState(false) const [result, setResult] = useState(null) const runTest = async () => { setTesting(true) setResult(null) try { const data = await apiFetch(`/configs/${config.id}/test`, { method: 'POST', }) setResult(data) } catch (err: unknown) { setResult({ success: false, message: err instanceof Error ? err.message : 'Verbindungstest fehlgeschlagen', }) } finally { setTesting(false) } } const openLoginFlow = () => { const loginUrl = `${API_BASE}/configs/${config.id}/login?tenant_id=${getTenantId()}` window.open(loginUrl, '_blank', 'width=600,height=700') } return (

Verbindungstest

{result && (
{result.success ? ( ) : ( )} {result.message}
{result.details && (
{result.details.issuer_reachable ? ( ) : ( )} Issuer erreichbar
{result.details.jwks_available ? ( ) : ( )} JWKS verfuegbar
{result.details.authorization_endpoint && (
Authorization: {result.details.authorization_endpoint}
)} {result.details.token_endpoint && (
Token: {result.details.token_endpoint}
)}
)}
)}
) } // ============================================================================= // SSO CONFIG CARD // ============================================================================= function SSOConfigCard({ config, onEdit, onDelete, onToggle, }: { config: SSOConfig onEdit: () => void onDelete: () => void onToggle: () => void }) { const [expanded, setExpanded] = useState(false) return (
{/* Main Row */}

{config.name}

{config.provider_type}

{config.oidc_issuer_url}

Client ID: {(config.oidc_client_id || '').substring(0, 12)}... Scopes: {(config.oidc_scopes || []).join(', ')} Erstellt: {formatDate(config.created_at)}
{/* Actions */}
{/* Expanded Details */} {expanded && (
{/* Config Details Grid */}
Redirect URI:
{config.oidc_redirect_uri}
Auto-Provisioning:
{config.auto_provision ? 'Aktiviert' : 'Deaktiviert'}
Standard-Rolle:
{config.default_role_id || 'Standard'}
Scopes:
{(config.oidc_scopes || []).map(scope => ( {scope} ))}
{/* Role Mappings */} {config.role_mapping && Object.keys(config.role_mapping).length > 0 && (

Rollen-Mapping

{Object.entries(config.role_mapping).map(([group, role]) => (
{group} {AVAILABLE_ROLES.find(r => r.value === role)?.label || role}
))}
)} {/* Connection Test */}
)}
) } // ============================================================================= // SSO USERS TABLE // ============================================================================= function SSOUsersTable({ users, loading, }: { users: SSOUser[] loading: boolean }) { if (loading) { return } if (users.length === 0) { return ( } title="Keine SSO-Benutzer" description="Es wurden noch keine Benutzer ueber SSO provisioniert. Benutzer erscheinen hier nach dem ersten Login." /> ) } return (
{users.map(user => ( ))}
Name E-Mail Externe ID Gruppen Letzter Login Status
{user.display_name}
{user.email} {user.external_id}
{user.groups.length > 0 ? ( user.groups.map(group => ( {group} )) ) : ( - )}
{formatDate(user.last_login)} {user.is_active ? ( Aktiv ) : ( Inaktiv )}
) } // ============================================================================= // INFO SECTION // ============================================================================= function SSOInfoSection() { const [expandedProvider, setExpandedProvider] = useState(null) const providers = [ { id: 'azure', name: 'Microsoft Entra ID (Azure AD)', steps: [ 'Melden Sie sich im Azure Portal (portal.azure.com) an', 'Navigieren Sie zu "Microsoft Entra ID" > "App-Registrierungen" > "Neue Registrierung"', 'Geben Sie einen Namen ein (z.B. "BreakPilot Comply SSO")', 'Waehlen Sie den Kontotyp (Einzelmandant oder Mehrere Mandanten)', 'Tragen Sie die Redirect URI ein (Typ: Web)', 'Notieren Sie die Application (Client) ID und die Directory (Tenant) ID', 'Erstellen Sie unter "Zertifikate & Geheimnisse" ein neues Client Secret', 'Issuer URL: https://login.microsoftonline.com/{Tenant-ID}/v2.0', 'Optional: Konfigurieren Sie Gruppen-Claims unter "Token-Konfiguration"', ], }, { id: 'okta', name: 'Okta', steps: [ 'Melden Sie sich in der Okta Admin-Konsole an', 'Navigieren Sie zu "Applications" > "Create App Integration"', 'Waehlen Sie "OIDC - OpenID Connect" und "Web Application"', 'Geben Sie einen Namen und die Redirect URI ein', 'Notieren Sie die Client ID und das Client Secret', 'Issuer URL: https://{your-domain}.okta.com', 'Weisen Sie Benutzer/Gruppen der Applikation zu', 'Optional: Konfigurieren Sie Gruppen-Claims im Authorization Server', ], }, { id: 'keycloak', name: 'Keycloak', steps: [ 'Melden Sie sich in der Keycloak Admin-Konsole an', 'Waehlen Sie den gewuenschten Realm oder erstellen Sie einen neuen', 'Navigieren Sie zu "Clients" > "Create client"', 'Client Type: "OpenID Connect", geben Sie eine Client ID ein', 'Aktivieren Sie "Client authentication" und "Standard flow"', 'Tragen Sie die Redirect URI unter "Valid redirect URIs" ein', 'Kopieren Sie das Client Secret aus dem Tab "Credentials"', 'Issuer URL: https://{host}/realms/{realm-name}', 'Optional: Konfigurieren Sie Gruppen-Mapper unter "Client scopes"', ], }, ] return (
{/* How SSO Works */}

Wie funktioniert SSO?

Single Sign-On (SSO) ermoeglicht es Ihren Benutzern, sich mit den bestehenden Unternehmens-Zugangsdaten anzumelden, ohne ein separates Passwort erstellen zu muessen. BreakPilot Comply unterstuetzt den OpenID Connect (OIDC) Standard, der mit allen gaengigen Identity Providern kompatibel ist.

1

Konfiguration

Verbinden Sie Ihren Identity Provider (Azure AD, Okta, Keycloak, etc.) ueber OIDC.

2

Authentifizierung

Benutzer werden zum Identity Provider weitergeleitet und melden sich dort an.

3

Provisionierung

Benutzerkonten werden automatisch angelegt und Rollen ueber Gruppen-Mappings zugewiesen.

{/* Provider Setup Guides */}

Einrichtungsanleitungen

{providers.map(provider => (
{expandedProvider === provider.id && (
    {provider.steps.map((step, idx) => (
  1. {idx + 1} {step}
  2. ))}
)}
))}
{/* Security Note */}

Sicherheitshinweis

Client Secrets werden verschluesselt gespeichert und sind nach der Eingabe nicht mehr einsehbar. Aktivieren Sie Auto-Provisioning nur, wenn Sie sicherstellen koennen, dass nur autorisierte Benutzer Zugang zum Identity Provider haben. Verwenden Sie Rollen-Mappings, um die Berechtigungen Ihrer Benutzer granular zu steuern.

) } // ============================================================================= // MAIN PAGE // ============================================================================= export default function SSOPage() { // State const [activeTab, setActiveTab] = useState('configs') const [configs, setConfigs] = useState([]) const [users, setUsers] = useState([]) const [loading, setLoading] = useState(true) const [usersLoading, setUsersLoading] = useState(true) const [error, setError] = useState(null) // Modal state const [showForm, setShowForm] = useState(false) const [editingConfig, setEditingConfig] = useState(null) const [deletingConfig, setDeletingConfig] = useState(null) // --------------------------------------------------------------------------- // Data Loading // --------------------------------------------------------------------------- const loadConfigs = useCallback(async () => { setLoading(true) setError(null) try { const data = await apiFetch<{ configs: SSOConfig[] }>('/configs') setConfigs(data.configs || []) } catch (err: unknown) { setError(err instanceof Error ? err.message : 'Fehler beim Laden der SSO-Konfigurationen') } finally { setLoading(false) } }, []) const loadUsers = useCallback(async () => { setUsersLoading(true) try { const data = await apiFetch<{ users: SSOUser[] }>('/users') setUsers(data.users || []) } catch { // Users endpoint might not be available yet - fail silently setUsers([]) } finally { setUsersLoading(false) } }, []) useEffect(() => { loadConfigs() loadUsers() }, [loadConfigs, loadUsers]) // --------------------------------------------------------------------------- // Handlers // --------------------------------------------------------------------------- const handleSaveConfig = async (formData: SSOFormData) => { const roleMapping: Record = {} for (const m of formData.role_mappings) { if (m.sso_group.trim()) { roleMapping[m.sso_group.trim()] = m.internal_role } } const payload: Record = { provider_type: formData.provider_type, name: formData.name, oidc_issuer_url: formData.issuer_url, oidc_client_id: formData.client_id, oidc_redirect_uri: formData.redirect_uri, oidc_scopes: formData.scopes.split(',').map(s => s.trim()).filter(Boolean), role_mapping: roleMapping, auto_provision: formData.auto_provision, } if (formData.client_secret) { payload.oidc_client_secret = formData.client_secret } if (editingConfig) { await apiFetch(`/configs/${editingConfig.id}`, { method: 'PUT', body: JSON.stringify(payload), }) } else { payload.enabled = false payload.oidc_client_secret = formData.client_secret await apiFetch('/configs', { method: 'POST', body: JSON.stringify(payload), }) } await loadConfigs() } const handleDeleteConfig = async () => { if (!deletingConfig) return await apiFetch(`/configs/${deletingConfig.id}`, { method: 'DELETE' }) setDeletingConfig(null) await loadConfigs() await loadUsers() } const handleToggleConfig = async (config: SSOConfig) => { try { await apiFetch(`/configs/${config.id}`, { method: 'PUT', body: JSON.stringify({ enabled: !config.enabled }), }) await loadConfigs() } catch (err: unknown) { setError(err instanceof Error ? err.message : 'Fehler beim Aendern des Status') } } const openCreateForm = () => { setEditingConfig(null) setShowForm(true) } const openEditForm = (config: SSOConfig) => { setEditingConfig(config) setShowForm(true) } // --------------------------------------------------------------------------- // Tab Configuration // --------------------------------------------------------------------------- const tabs: { id: TabId; label: string; icon: React.ReactNode; count?: number }[] = [ { id: 'configs', label: 'Konfigurationen', icon: , count: configs.length }, { id: 'users', label: 'SSO-Benutzer', icon: , count: users.length }, { id: 'info', label: 'Anleitung', icon: }, ] // --------------------------------------------------------------------------- // Render // --------------------------------------------------------------------------- return (
{/* Header */}

Single Sign-On (SSO)

Konfigurieren Sie OIDC-Authentifizierung fuer Ihr Unternehmen

{/* Stats Cards */}
Konfigurationen
{configs.length}
Aktive Provider
{configs.filter(c => c.enabled).length}
SSO-Benutzer
{users.length}
Aktive Benutzer
{users.filter(u => u.is_active).length}
{/* Tab Navigation */}
{/* Error Banner */} {error && (
{error}
)} {/* Tab Content */} {activeTab === 'configs' && (
{loading ? ( ) : configs.length === 0 ? (
} title="Keine SSO-Konfigurationen" description="Erstellen Sie Ihre erste SSO-Konfiguration, um Ihren Benutzern die Anmeldung ueber Ihren Identity Provider zu ermoeglichen." action={ } />
) : (
{configs.map(config => ( openEditForm(config)} onDelete={() => setDeletingConfig(config)} onToggle={() => handleToggleConfig(config)} /> ))}
)}
)} {activeTab === 'users' && (

SSO-Benutzer

Uebersicht aller ueber SSO provisionierten Benutzer

)} {activeTab === 'info' && ( )} {/* Modals */} { setShowForm(false) setEditingConfig(null) }} onSave={handleSaveConfig} /> setDeletingConfig(null)} onConfirm={handleDeleteConfig} />
) }