mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 08:46:39 +02:00
SSO config (Tenant ID, Client ID, Client Secret, Redirect URI) can now be managed entirely from the Admin SSO page. Config is stored in Redis (persistent) and the MSAL client is reinitialized on save — no server restart or console access required. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
817 lines
25 KiB
TypeScript
817 lines
25 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import api from '../api/client';
|
|
|
|
interface SsoStatus {
|
|
microsoft: boolean;
|
|
}
|
|
|
|
interface SsoConfigResponse {
|
|
configured: boolean;
|
|
config: {
|
|
tenantId: string;
|
|
clientId: string;
|
|
redirectUri: string;
|
|
clientSecretMasked: string;
|
|
} | null;
|
|
}
|
|
|
|
const cardStyle: React.CSSProperties = {
|
|
background: 'var(--color-bg-card)',
|
|
borderRadius: 'var(--radius-md)',
|
|
boxShadow: 'var(--shadow-sm)',
|
|
border: '1px solid var(--color-border)',
|
|
padding: '1.5rem',
|
|
marginBottom: '1.5rem',
|
|
};
|
|
|
|
const inlineCodeStyle: React.CSSProperties = {
|
|
background: 'var(--color-bg)',
|
|
padding: '0.125rem 0.375rem',
|
|
borderRadius: 'var(--radius-sm)',
|
|
fontSize: '0.8125rem',
|
|
fontFamily: "'JetBrains Mono', 'Fira Code', 'Consolas', monospace",
|
|
color: '#1e40af',
|
|
};
|
|
|
|
const stepStyle: React.CSSProperties = {
|
|
display: 'flex',
|
|
gap: '1rem',
|
|
marginBottom: '1.5rem',
|
|
};
|
|
|
|
const stepNumberStyle: React.CSSProperties = {
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
width: 32,
|
|
height: 32,
|
|
borderRadius: '50%',
|
|
background: 'var(--color-primary)',
|
|
color: 'white',
|
|
fontSize: '0.875rem',
|
|
fontWeight: 700,
|
|
flexShrink: 0,
|
|
};
|
|
|
|
const stepContentStyle: React.CSSProperties = {
|
|
flex: 1,
|
|
paddingTop: '0.25rem',
|
|
};
|
|
|
|
const h3Style: React.CSSProperties = {
|
|
fontSize: '1rem',
|
|
fontWeight: 600,
|
|
color: 'var(--color-text)',
|
|
marginBottom: '0.5rem',
|
|
};
|
|
|
|
const pStyle: React.CSSProperties = {
|
|
fontSize: '0.875rem',
|
|
color: 'var(--color-text-secondary)',
|
|
lineHeight: 1.6,
|
|
marginBottom: '0.5rem',
|
|
};
|
|
|
|
const tableStyle: React.CSSProperties = {
|
|
width: '100%',
|
|
borderCollapse: 'collapse',
|
|
marginTop: '0.75rem',
|
|
};
|
|
|
|
const thStyle: React.CSSProperties = {
|
|
padding: '0.625rem 0.75rem',
|
|
textAlign: 'left',
|
|
fontSize: '0.75rem',
|
|
textTransform: 'uppercase',
|
|
color: 'var(--color-text-muted)',
|
|
borderBottom: '1px solid var(--color-border)',
|
|
background: 'var(--color-bg)',
|
|
};
|
|
|
|
const tdStyle: React.CSSProperties = {
|
|
padding: '0.625rem 0.75rem',
|
|
fontSize: '0.8125rem',
|
|
borderBottom: '1px solid var(--color-border)',
|
|
verticalAlign: 'top',
|
|
};
|
|
|
|
const inputStyle: React.CSSProperties = {
|
|
width: '100%',
|
|
padding: '0.625rem 0.75rem',
|
|
fontSize: '0.875rem',
|
|
border: '1px solid var(--color-border)',
|
|
borderRadius: 'var(--radius-sm)',
|
|
background: 'var(--color-bg-card)',
|
|
color: 'var(--color-text)',
|
|
fontFamily: "'JetBrains Mono', 'Fira Code', 'Consolas', monospace",
|
|
outline: 'none',
|
|
boxSizing: 'border-box' as const,
|
|
};
|
|
|
|
const labelStyle: React.CSSProperties = {
|
|
display: 'block',
|
|
fontSize: '0.8125rem',
|
|
fontWeight: 600,
|
|
color: 'var(--color-text)',
|
|
marginBottom: '0.375rem',
|
|
};
|
|
|
|
const hintStyle: React.CSSProperties = {
|
|
fontSize: '0.75rem',
|
|
color: 'var(--color-text-muted)',
|
|
marginTop: '0.25rem',
|
|
};
|
|
|
|
export function AdminSsoPage() {
|
|
const queryClient = useQueryClient();
|
|
|
|
const { data: ssoStatus, isLoading: statusLoading } = useQuery<SsoStatus>({
|
|
queryKey: ['sso', 'status'],
|
|
queryFn: async () => {
|
|
const res = await api.get<SsoStatus>('/auth/sso/status');
|
|
return res.data;
|
|
},
|
|
});
|
|
|
|
const { data: ssoConfig, isLoading: configLoading } =
|
|
useQuery<SsoConfigResponse>({
|
|
queryKey: ['sso', 'config'],
|
|
queryFn: async () => {
|
|
const res = await api.get<SsoConfigResponse>('/auth/sso/config');
|
|
return res.data;
|
|
},
|
|
});
|
|
|
|
// Formular-State
|
|
const [tenantId, setTenantId] = useState('');
|
|
const [clientId, setClientId] = useState('');
|
|
const [clientSecret, setClientSecret] = useState('');
|
|
const [redirectUri, setRedirectUri] = useState('');
|
|
const [saveSuccess, setSaveSuccess] = useState(false);
|
|
|
|
// Default Redirect-URI
|
|
const defaultRedirectUri = `${window.location.origin}/api/v1/auth/sso/microsoft/callback`;
|
|
|
|
// Formular mit bestehenden Werten befuellen
|
|
useEffect(() => {
|
|
if (ssoConfig?.config) {
|
|
setTenantId(ssoConfig.config.tenantId);
|
|
setClientId(ssoConfig.config.clientId);
|
|
setRedirectUri(ssoConfig.config.redirectUri);
|
|
// Secret bleibt leer — wird nur bei Aenderung gesendet
|
|
} else {
|
|
// Default Redirect-URI setzen wenn noch keine Config da
|
|
setRedirectUri(defaultRedirectUri);
|
|
}
|
|
}, [ssoConfig, defaultRedirectUri]);
|
|
|
|
const saveMutation = useMutation({
|
|
mutationFn: async (data: {
|
|
tenantId: string;
|
|
clientId: string;
|
|
clientSecret?: string;
|
|
redirectUri: string;
|
|
}) => {
|
|
const res = await api.post('/auth/sso/config', data);
|
|
return res.data;
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['sso', 'status'] });
|
|
queryClient.invalidateQueries({ queryKey: ['sso', 'config'] });
|
|
setClientSecret(''); // Secret-Feld leeren
|
|
setSaveSuccess(true);
|
|
setTimeout(() => setSaveSuccess(false), 4000);
|
|
},
|
|
});
|
|
|
|
const handleSave = () => {
|
|
const data: {
|
|
tenantId: string;
|
|
clientId: string;
|
|
clientSecret?: string;
|
|
redirectUri: string;
|
|
} = {
|
|
tenantId,
|
|
clientId,
|
|
redirectUri,
|
|
};
|
|
|
|
// Secret nur mitsenden wenn es geaendert wurde
|
|
if (clientSecret) {
|
|
data.clientSecret = clientSecret;
|
|
}
|
|
|
|
saveMutation.mutate(data);
|
|
};
|
|
|
|
const isLoading = statusLoading || configLoading;
|
|
const hasExistingConfig = !!ssoConfig?.config;
|
|
const canSave =
|
|
tenantId.trim() &&
|
|
clientId.trim() &&
|
|
redirectUri.trim() &&
|
|
(clientSecret.trim() || hasExistingConfig); // Secret nur noetig bei Erstconfig
|
|
|
|
return (
|
|
<div>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
marginBottom: '1.5rem',
|
|
}}
|
|
>
|
|
<h1 style={{ fontSize: '1.5rem', fontWeight: 600 }}>
|
|
SSO-Konfiguration
|
|
</h1>
|
|
</div>
|
|
|
|
{/* Status-Anzeige */}
|
|
<div style={cardStyle}>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '0.75rem',
|
|
marginBottom: '0.5rem',
|
|
}}
|
|
>
|
|
<svg
|
|
width="24"
|
|
height="24"
|
|
viewBox="0 0 21 21"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<rect x="1" y="1" width="9" height="9" fill="#f25022" />
|
|
<rect x="11" y="1" width="9" height="9" fill="#7fba00" />
|
|
<rect x="1" y="11" width="9" height="9" fill="#00a4ef" />
|
|
<rect x="11" y="11" width="9" height="9" fill="#ffb900" />
|
|
</svg>
|
|
<h2 style={{ fontSize: '1.125rem', fontWeight: 600 }}>
|
|
Microsoft Entra ID (Azure AD)
|
|
</h2>
|
|
</div>
|
|
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '0.5rem',
|
|
padding: '0.75rem 1rem',
|
|
borderRadius: 'var(--radius-sm)',
|
|
background: ssoStatus?.microsoft ? '#f0fdf4' : '#fef2f2',
|
|
border: `1px solid ${ssoStatus?.microsoft ? '#bbf7d0' : '#fecaca'}`,
|
|
marginTop: '0.75rem',
|
|
}}
|
|
>
|
|
<span
|
|
style={{
|
|
display: 'inline-block',
|
|
width: 10,
|
|
height: 10,
|
|
borderRadius: '50%',
|
|
background: ssoStatus?.microsoft
|
|
? 'var(--color-success)'
|
|
: 'var(--color-error)',
|
|
}}
|
|
/>
|
|
<span
|
|
style={{
|
|
fontSize: '0.875rem',
|
|
fontWeight: 500,
|
|
color: ssoStatus?.microsoft ? '#166534' : '#991b1b',
|
|
}}
|
|
>
|
|
{isLoading
|
|
? 'Status wird geladen...'
|
|
: ssoStatus?.microsoft
|
|
? 'SSO ist aktiv — Benutzer können sich mit Microsoft anmelden'
|
|
: 'SSO ist nicht konfiguriert — folge der Anleitung unten'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Einrichtungs-Anleitung */}
|
|
<div style={cardStyle}>
|
|
<h2
|
|
style={{
|
|
fontSize: '1.125rem',
|
|
fontWeight: 600,
|
|
marginBottom: '1.25rem',
|
|
}}
|
|
>
|
|
Einrichtungsanleitung
|
|
</h2>
|
|
|
|
{/* Schritt 1 */}
|
|
<div style={stepStyle}>
|
|
<div style={stepNumberStyle}>1</div>
|
|
<div style={stepContentStyle}>
|
|
<h3 style={h3Style}>App Registration im Azure Portal anlegen</h3>
|
|
<p style={pStyle}>
|
|
Gehe zum{' '}
|
|
<a
|
|
href="https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
style={{
|
|
color: 'var(--color-primary)',
|
|
textDecoration: 'underline',
|
|
}}
|
|
>
|
|
Azure Portal → App registrations
|
|
</a>{' '}
|
|
und klicke auf <strong>New registration</strong>.
|
|
</p>
|
|
<table style={tableStyle}>
|
|
<thead>
|
|
<tr>
|
|
<th style={thStyle}>Feld</th>
|
|
<th style={thStyle}>Wert</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td style={tdStyle}>
|
|
<strong>Name</strong>
|
|
</td>
|
|
<td style={tdStyle}>INSIGHT Platform</td>
|
|
</tr>
|
|
<tr>
|
|
<td style={tdStyle}>
|
|
<strong>Supported account types</strong>
|
|
</td>
|
|
<td style={tdStyle}>
|
|
Accounts in this organizational directory only
|
|
<br />
|
|
<span
|
|
style={{
|
|
fontSize: '0.75rem',
|
|
color: 'var(--color-text-muted)',
|
|
}}
|
|
>
|
|
(Single tenant — nur euer Azure AD)
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style={tdStyle}>
|
|
<strong>Redirect URI</strong>
|
|
</td>
|
|
<td style={tdStyle}>
|
|
Platform: <strong>Web</strong>
|
|
<br />
|
|
URI:{' '}
|
|
<code style={inlineCodeStyle}>{defaultRedirectUri}</code>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Schritt 2 */}
|
|
<div style={stepStyle}>
|
|
<div style={stepNumberStyle}>2</div>
|
|
<div style={stepContentStyle}>
|
|
<h3 style={h3Style}>Client Secret erstellen</h3>
|
|
<p style={pStyle}>
|
|
In der App Registration →{' '}
|
|
<strong>Certificates & secrets</strong> →{' '}
|
|
<strong>New client secret</strong>.
|
|
</p>
|
|
<table style={tableStyle}>
|
|
<thead>
|
|
<tr>
|
|
<th style={thStyle}>Feld</th>
|
|
<th style={thStyle}>Wert</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td style={tdStyle}>
|
|
<strong>Description</strong>
|
|
</td>
|
|
<td style={tdStyle}>INSIGHT Platform SSO</td>
|
|
</tr>
|
|
<tr>
|
|
<td style={tdStyle}>
|
|
<strong>Expires</strong>
|
|
</td>
|
|
<td style={tdStyle}>
|
|
24 months (empfohlen)
|
|
<br />
|
|
<span
|
|
style={{
|
|
fontSize: '0.75rem',
|
|
color: 'var(--color-text-muted)',
|
|
}}
|
|
>
|
|
Danach muss das Secret erneuert werden
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div
|
|
style={{
|
|
marginTop: '0.75rem',
|
|
padding: '0.625rem 0.75rem',
|
|
background: '#fffbeb',
|
|
border: '1px solid #fde68a',
|
|
borderRadius: 'var(--radius-sm)',
|
|
fontSize: '0.8125rem',
|
|
color: '#92400e',
|
|
}}
|
|
>
|
|
<strong>Wichtig:</strong> Den <strong>Value</strong> des Secrets
|
|
sofort kopieren — er wird nur einmalig angezeigt!
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Schritt 3 */}
|
|
<div style={stepStyle}>
|
|
<div style={stepNumberStyle}>3</div>
|
|
<div style={stepContentStyle}>
|
|
<h3 style={h3Style}>API Permissions konfigurieren</h3>
|
|
<p style={pStyle}>
|
|
In der App Registration →{' '}
|
|
<strong>API permissions</strong> →{' '}
|
|
<strong>Add a permission</strong> →{' '}
|
|
<strong>Microsoft Graph</strong> →{' '}
|
|
<strong>Delegated permissions</strong>.
|
|
</p>
|
|
<p style={pStyle}>Folgende Berechtigungen hinzufügen:</p>
|
|
<table style={tableStyle}>
|
|
<thead>
|
|
<tr>
|
|
<th style={thStyle}>Permission</th>
|
|
<th style={thStyle}>Typ</th>
|
|
<th style={thStyle}>Beschreibung</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td style={tdStyle}>
|
|
<code style={inlineCodeStyle}>openid</code>
|
|
</td>
|
|
<td style={tdStyle}>Delegated</td>
|
|
<td style={tdStyle}>Sign users in</td>
|
|
</tr>
|
|
<tr>
|
|
<td style={tdStyle}>
|
|
<code style={inlineCodeStyle}>profile</code>
|
|
</td>
|
|
<td style={tdStyle}>Delegated</td>
|
|
<td style={tdStyle}>View users' basic profile</td>
|
|
</tr>
|
|
<tr>
|
|
<td style={tdStyle}>
|
|
<code style={inlineCodeStyle}>email</code>
|
|
</td>
|
|
<td style={tdStyle}>Delegated</td>
|
|
<td style={tdStyle}>View users' email address</td>
|
|
</tr>
|
|
<tr>
|
|
<td style={tdStyle}>
|
|
<code style={inlineCodeStyle}>User.Read</code>
|
|
</td>
|
|
<td style={tdStyle}>Delegated</td>
|
|
<td style={tdStyle}>Sign in and read user profile</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<p style={{ ...pStyle, marginTop: '0.5rem' }}>
|
|
Dann <strong>Grant admin consent</strong> klicken, um die
|
|
Berechtigungen für alle Benutzer freizugeben.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Schritt 4 — Konfiguration eingeben und speichern */}
|
|
<div style={stepStyle}>
|
|
<div style={stepNumberStyle}>4</div>
|
|
<div style={stepContentStyle}>
|
|
<h3 style={h3Style}>
|
|
Werte aus Azure Portal hier eintragen und speichern
|
|
</h3>
|
|
<p style={pStyle}>
|
|
Kopiere die Werte aus der App Registration →{' '}
|
|
<strong>Overview</strong> und trage sie in die Felder ein. Beim
|
|
Speichern wird die SSO-Verbindung automatisch aktiviert.
|
|
</p>
|
|
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '1rem',
|
|
marginTop: '1rem',
|
|
}}
|
|
>
|
|
{/* Tenant ID */}
|
|
<div>
|
|
<label style={labelStyle}>
|
|
Directory (Tenant) ID{' '}
|
|
<span style={{ color: 'var(--color-error)' }}>*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={tenantId}
|
|
onChange={(e) => setTenantId(e.target.value)}
|
|
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
style={inputStyle}
|
|
/>
|
|
<div style={hintStyle}>
|
|
Azure Portal → App Registration → Overview →
|
|
Directory (tenant) ID
|
|
</div>
|
|
</div>
|
|
|
|
{/* Client ID */}
|
|
<div>
|
|
<label style={labelStyle}>
|
|
Application (Client) ID{' '}
|
|
<span style={{ color: 'var(--color-error)' }}>*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={clientId}
|
|
onChange={(e) => setClientId(e.target.value)}
|
|
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
style={inputStyle}
|
|
/>
|
|
<div style={hintStyle}>
|
|
Azure Portal → App Registration → Overview →
|
|
Application (client) ID
|
|
</div>
|
|
</div>
|
|
|
|
{/* Client Secret */}
|
|
<div>
|
|
<label style={labelStyle}>
|
|
Client Secret{' '}
|
|
{!hasExistingConfig && (
|
|
<span style={{ color: 'var(--color-error)' }}>*</span>
|
|
)}
|
|
</label>
|
|
<input
|
|
type="password"
|
|
value={clientSecret}
|
|
onChange={(e) => setClientSecret(e.target.value)}
|
|
placeholder={
|
|
hasExistingConfig
|
|
? `Gespeichert (${ssoConfig?.config?.clientSecretMasked}) — leer lassen um beizubehalten`
|
|
: 'Client Secret Value eingeben'
|
|
}
|
|
style={inputStyle}
|
|
/>
|
|
<div style={hintStyle}>
|
|
Azure Portal → App Registration → Certificates &
|
|
secrets → Client secret Value
|
|
{hasExistingConfig && (
|
|
<span style={{ color: 'var(--color-text-secondary)' }}>
|
|
{' '}
|
|
— Leer lassen, um das bestehende Secret zu behalten
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Redirect URI */}
|
|
<div>
|
|
<label style={labelStyle}>
|
|
Redirect URI{' '}
|
|
<span style={{ color: 'var(--color-error)' }}>*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={redirectUri}
|
|
onChange={(e) => setRedirectUri(e.target.value)}
|
|
placeholder={defaultRedirectUri}
|
|
style={inputStyle}
|
|
/>
|
|
<div style={hintStyle}>
|
|
Muss exakt mit der Redirect URI in der Azure App Registration
|
|
übereinstimmen
|
|
</div>
|
|
</div>
|
|
|
|
{/* Speichern-Button + Status */}
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '1rem',
|
|
marginTop: '0.5rem',
|
|
}}
|
|
>
|
|
<button
|
|
onClick={handleSave}
|
|
disabled={!canSave || saveMutation.isPending}
|
|
style={{
|
|
padding: '0.625rem 1.5rem',
|
|
background: canSave ? 'var(--color-primary)' : '#94a3b8',
|
|
color: 'white',
|
|
border: 'none',
|
|
borderRadius: 'var(--radius-sm)',
|
|
fontSize: '0.875rem',
|
|
fontWeight: 600,
|
|
cursor: canSave ? 'pointer' : 'not-allowed',
|
|
opacity: saveMutation.isPending ? 0.7 : 1,
|
|
}}
|
|
>
|
|
{saveMutation.isPending
|
|
? 'Wird gespeichert...'
|
|
: 'Speichern und aktivieren'}
|
|
</button>
|
|
|
|
{saveSuccess && (
|
|
<span
|
|
style={{
|
|
fontSize: '0.875rem',
|
|
color: '#166534',
|
|
fontWeight: 500,
|
|
}}
|
|
>
|
|
SSO-Konfiguration gespeichert und aktiviert!
|
|
</span>
|
|
)}
|
|
|
|
{saveMutation.isError && (
|
|
<span
|
|
style={{
|
|
fontSize: '0.875rem',
|
|
color: 'var(--color-error)',
|
|
fontWeight: 500,
|
|
}}
|
|
>
|
|
Fehler:{' '}
|
|
{(saveMutation.error as Error)?.message ||
|
|
'Konfiguration konnte nicht gespeichert werden'}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Funktionsweise */}
|
|
<div style={cardStyle}>
|
|
<h2
|
|
style={{
|
|
fontSize: '1.125rem',
|
|
fontWeight: 600,
|
|
marginBottom: '1rem',
|
|
}}
|
|
>
|
|
Funktionsweise
|
|
</h2>
|
|
<div
|
|
style={{
|
|
display: 'grid',
|
|
gridTemplateColumns: '1fr 1fr',
|
|
gap: '1rem',
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
padding: '1rem',
|
|
background: 'var(--color-bg)',
|
|
borderRadius: 'var(--radius-sm)',
|
|
}}
|
|
>
|
|
<h3 style={{ ...h3Style, fontSize: '0.875rem' }}>Erstanmeldung</h3>
|
|
<p
|
|
style={{ ...pStyle, fontSize: '0.8125rem', marginBottom: 0 }}
|
|
>
|
|
Wenn ein Benutzer sich zum ersten Mal mit Microsoft anmeldet,
|
|
wird automatisch ein Konto angelegt (Rolle: Benutzer). Existiert
|
|
bereits ein Konto mit derselben E-Mail-Adresse, wird das
|
|
Microsoft-Konto automatisch verknüpft.
|
|
</p>
|
|
</div>
|
|
<div
|
|
style={{
|
|
padding: '1rem',
|
|
background: 'var(--color-bg)',
|
|
borderRadius: 'var(--radius-sm)',
|
|
}}
|
|
>
|
|
<h3 style={{ ...h3Style, fontSize: '0.875rem' }}>Sicherheit</h3>
|
|
<p
|
|
style={{ ...pStyle, fontSize: '0.8125rem', marginBottom: 0 }}
|
|
>
|
|
Der SSO-Flow nutzt den OAuth2 Authorization Code Flow. Das Client
|
|
Secret verlässt nie den Server. CSRF-Schutz via State-Parameter.
|
|
Die Multi-Faktor-Authentifizierung (MFA) von Microsoft wird
|
|
unterstützt.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Technische Details */}
|
|
<div style={cardStyle}>
|
|
<h2
|
|
style={{
|
|
fontSize: '1.125rem',
|
|
fontWeight: 600,
|
|
marginBottom: '1rem',
|
|
}}
|
|
>
|
|
Technische Referenz
|
|
</h2>
|
|
<table style={tableStyle}>
|
|
<thead>
|
|
<tr>
|
|
<th style={thStyle}>Endpoint</th>
|
|
<th style={thStyle}>Beschreibung</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td style={tdStyle}>
|
|
<code style={inlineCodeStyle}>
|
|
GET /api/v1/auth/sso/microsoft
|
|
</code>
|
|
</td>
|
|
<td style={tdStyle}>
|
|
Startet den SSO-Flow (Redirect zu Microsoft)
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style={tdStyle}>
|
|
<code style={inlineCodeStyle}>
|
|
GET /api/v1/auth/sso/microsoft/callback
|
|
</code>
|
|
</td>
|
|
<td style={tdStyle}>
|
|
Callback von Microsoft (Token-Exchange + User-Provisioning)
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style={tdStyle}>
|
|
<code style={inlineCodeStyle}>
|
|
GET /api/v1/auth/sso/status
|
|
</code>
|
|
</td>
|
|
<td style={tdStyle}>
|
|
Prüft ob SSO konfiguriert ist (für Login-Seite)
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<h3
|
|
style={{ ...h3Style, marginTop: '1.25rem', fontSize: '0.9375rem' }}
|
|
>
|
|
Redirect URI
|
|
</h3>
|
|
<p style={pStyle}>
|
|
Diese URI muss exakt so in der Azure App Registration eingetragen
|
|
sein:
|
|
</p>
|
|
<div
|
|
style={{
|
|
background: '#1e293b',
|
|
color: '#e2e8f0',
|
|
borderRadius: 'var(--radius-sm)',
|
|
padding: '1rem 1.25rem',
|
|
fontSize: '0.8125rem',
|
|
fontFamily:
|
|
"'JetBrains Mono', 'Fira Code', 'Consolas', monospace",
|
|
lineHeight: 1.7,
|
|
overflowX: 'auto',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
gap: '1rem',
|
|
marginTop: '0.75rem',
|
|
}}
|
|
>
|
|
<span>{defaultRedirectUri}</span>
|
|
<button
|
|
onClick={() => navigator.clipboard.writeText(defaultRedirectUri)}
|
|
style={{
|
|
padding: '0.25rem 0.5rem',
|
|
background: '#334155',
|
|
color: '#e2e8f0',
|
|
border: '1px solid #475569',
|
|
borderRadius: 'var(--radius-sm)',
|
|
fontSize: '0.75rem',
|
|
cursor: 'pointer',
|
|
whiteSpace: 'nowrap',
|
|
}}
|
|
>
|
|
Kopieren
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|