mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-24 22:46:39 +02:00
feat: add SSO configuration page in admin section
New admin page at /admin/sso with: - Live SSO status indicator (active/not configured) - Step-by-step Azure App Registration guide - Required values table (Tenant ID, Client ID, Client Secret) - API permissions list (openid, profile, email, User.Read) - Redirect URI with copy button - Environment variables template - Technical reference (endpoints, security info) Accessible via sidebar: Administration → SSO-Konfiguration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
45cf644f81
commit
eba738fdc5
3 changed files with 482 additions and 0 deletions
472
packages/frontend/src/admin/AdminSsoPage.tsx
Normal file
472
packages/frontend/src/admin/AdminSsoPage.tsx
Normal file
|
|
@ -0,0 +1,472 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import api from '../api/client';
|
||||
|
||||
interface SsoStatus {
|
||||
microsoft: boolean;
|
||||
}
|
||||
|
||||
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 codeBlockStyle: React.CSSProperties = {
|
||||
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',
|
||||
whiteSpace: 'pre',
|
||||
marginTop: '0.75rem',
|
||||
};
|
||||
|
||||
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',
|
||||
};
|
||||
|
||||
export function AdminSsoPage() {
|
||||
const { data: ssoStatus, isLoading } = useQuery<SsoStatus>({
|
||||
queryKey: ['sso', 'status'],
|
||||
queryFn: async () => {
|
||||
const res = await api.get<SsoStatus>('/auth/sso/status');
|
||||
return res.data;
|
||||
},
|
||||
});
|
||||
|
||||
const redirectUri = `${window.location.origin}/api/v1/auth/sso/microsoft/callback`;
|
||||
|
||||
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}>{redirectUri}</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 */}
|
||||
<div style={stepStyle}>
|
||||
<div style={stepNumberStyle}>4</div>
|
||||
<div style={stepContentStyle}>
|
||||
<h3 style={h3Style}>Werte aus Azure Portal kopieren</h3>
|
||||
<p style={pStyle}>
|
||||
In der App Registration → <strong>Overview</strong> findest du die benötigten Werte:
|
||||
</p>
|
||||
<table style={tableStyle}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={thStyle}>Azure Portal Feld</th>
|
||||
<th style={thStyle}>Umgebungsvariable</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={tdStyle}><strong>Directory (tenant) ID</strong></td>
|
||||
<td style={tdStyle}><code style={inlineCodeStyle}>AZURE_TENANT_ID</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={tdStyle}><strong>Application (client) ID</strong></td>
|
||||
<td style={tdStyle}><code style={inlineCodeStyle}>AZURE_CLIENT_ID</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={tdStyle}><strong>Client secret value</strong> (aus Schritt 2)</td>
|
||||
<td style={tdStyle}><code style={inlineCodeStyle}>AZURE_CLIENT_SECRET</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Schritt 5 */}
|
||||
<div style={stepStyle}>
|
||||
<div style={stepNumberStyle}>5</div>
|
||||
<div style={stepContentStyle}>
|
||||
<h3 style={h3Style}>Umgebungsvariablen auf dem Server setzen</h3>
|
||||
<p style={pStyle}>
|
||||
Trage die Werte in die <code style={inlineCodeStyle}>.env</code>-Datei auf dem Server ein:
|
||||
</p>
|
||||
<div style={codeBlockStyle}>
|
||||
{`AZURE_TENANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
AZURE_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
AZURE_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
AZURE_REDIRECT_URI=${redirectUri}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Schritt 6 */}
|
||||
<div style={stepStyle}>
|
||||
<div style={stepNumberStyle}>6</div>
|
||||
<div style={stepContentStyle}>
|
||||
<h3 style={h3Style}>Core-Service neu starten</h3>
|
||||
<p style={pStyle}>
|
||||
Nach dem Setzen der Umgebungsvariablen den Backend-Service neu starten:
|
||||
</p>
|
||||
<div style={codeBlockStyle}>
|
||||
{`docker compose restart core`}
|
||||
</div>
|
||||
<p style={{ ...pStyle, marginTop: '0.75rem' }}>
|
||||
Nach dem Neustart sollte der Status oben auf{' '}
|
||||
<strong style={{ color: '#166534' }}>aktiv</strong> wechseln und
|
||||
der „Mit Microsoft anmelden"-Button auf der Login-Seite erscheinen.
|
||||
</p>
|
||||
</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={{
|
||||
...codeBlockStyle,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: '1rem',
|
||||
}}>
|
||||
<span>{redirectUri}</span>
|
||||
<button
|
||||
onClick={() => navigator.clipboard.writeText(redirectUri)}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import { AppLayout } from './AppLayout';
|
|||
import { DashboardPage } from './DashboardPage';
|
||||
import { AdminUsersPage } from '../admin/AdminUsersPage';
|
||||
import { AdminTenantsPage } from '../admin/AdminTenantsPage';
|
||||
import { AdminSsoPage } from '../admin/AdminSsoPage';
|
||||
import { ProfilePage } from '../profile/ProfilePage';
|
||||
|
||||
function PrivateRoute({ children }: { children: React.ReactNode }) {
|
||||
|
|
@ -46,6 +47,7 @@ export function App() {
|
|||
<Route path="profile" element={<ProfilePage />} />
|
||||
<Route path="admin/users" element={<AdminUsersPage />} />
|
||||
<Route path="admin/tenants" element={<AdminTenantsPage />} />
|
||||
<Route path="admin/sso" element={<AdminSsoPage />} />
|
||||
</Route>
|
||||
|
||||
{/* Fallback */}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,14 @@ export function AppLayout() {
|
|||
>
|
||||
Mandanten
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/admin/sso"
|
||||
className={({ isActive }) =>
|
||||
`${styles.navLink} ${isActive ? styles.active : ''}`
|
||||
}
|
||||
>
|
||||
SSO-Konfiguration
|
||||
</NavLink>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue