Implement authentication system with access control UI
This commit is contained in:
342
frontend/src/pages/AdminOIDCPage.tsx
Normal file
342
frontend/src/pages/AdminOIDCPage.tsx
Normal file
@@ -0,0 +1,342 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { getOIDCConfig, updateOIDCConfig } from '../api';
|
||||
import { OIDCConfig } from '../types';
|
||||
import './AdminOIDCPage.css';
|
||||
|
||||
function AdminOIDCPage() {
|
||||
const { user, loading: authLoading } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [config, setConfig] = useState<OIDCConfig | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
|
||||
// Form state
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [issuerUrl, setIssuerUrl] = useState('');
|
||||
const [clientId, setClientId] = useState('');
|
||||
const [clientSecret, setClientSecret] = useState('');
|
||||
const [scopes, setScopes] = useState('openid profile email');
|
||||
const [autoCreateUsers, setAutoCreateUsers] = useState(true);
|
||||
const [adminGroup, setAdminGroup] = useState('');
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading && !user) {
|
||||
navigate('/login', { state: { from: '/admin/oidc' } });
|
||||
}
|
||||
}, [user, authLoading, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user && user.is_admin) {
|
||||
loadConfig();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (successMessage) {
|
||||
const timer = setTimeout(() => setSuccessMessage(null), 3000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [successMessage]);
|
||||
|
||||
async function loadConfig() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await getOIDCConfig();
|
||||
setConfig(data);
|
||||
setEnabled(data.enabled);
|
||||
setIssuerUrl(data.issuer_url);
|
||||
setClientId(data.client_id);
|
||||
setScopes(data.scopes.join(' '));
|
||||
setAutoCreateUsers(data.auto_create_users);
|
||||
setAdminGroup(data.admin_group);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load OIDC configuration');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
if (enabled && !issuerUrl.trim()) {
|
||||
setError('Issuer URL is required when OIDC is enabled');
|
||||
return;
|
||||
}
|
||||
if (enabled && !clientId.trim()) {
|
||||
setError('Client ID is required when OIDC is enabled');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const scopesList = scopes.split(/\s+/).filter(s => s.length > 0);
|
||||
const updateData: Record<string, unknown> = {
|
||||
enabled,
|
||||
issuer_url: issuerUrl.trim(),
|
||||
client_id: clientId.trim(),
|
||||
scopes: scopesList,
|
||||
auto_create_users: autoCreateUsers,
|
||||
admin_group: adminGroup.trim(),
|
||||
};
|
||||
|
||||
if (clientSecret) {
|
||||
updateData.client_secret = clientSecret;
|
||||
}
|
||||
|
||||
await updateOIDCConfig(updateData);
|
||||
setSuccessMessage('OIDC configuration saved successfully');
|
||||
setClientSecret('');
|
||||
await loadConfig();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to save OIDC configuration');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (authLoading) {
|
||||
return (
|
||||
<div className="admin-oidc-page">
|
||||
<div className="admin-oidc-loading">
|
||||
<div className="admin-oidc-spinner"></div>
|
||||
<span>Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!user.is_admin) {
|
||||
return (
|
||||
<div className="admin-oidc-page">
|
||||
<div className="admin-oidc-access-denied">
|
||||
<div className="admin-oidc-access-denied-icon">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2>Access Denied</h2>
|
||||
<p>You do not have permission to access this page. Admin privileges are required.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="admin-oidc-page">
|
||||
<div className="admin-oidc-header">
|
||||
<div className="admin-oidc-header-content">
|
||||
<h1>Single Sign-On (OIDC)</h1>
|
||||
<p className="admin-oidc-subtitle">
|
||||
Configure OpenID Connect for SSO authentication
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{successMessage && (
|
||||
<div className="admin-oidc-success">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
|
||||
<polyline points="22 4 12 14.01 9 11.01"/>
|
||||
</svg>
|
||||
<span>{successMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="admin-oidc-error">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="12" y1="8" x2="12" y2="12"/>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"/>
|
||||
</svg>
|
||||
<span>{error}</span>
|
||||
<button onClick={() => setError(null)} className="admin-oidc-error-dismiss">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="admin-oidc-card">
|
||||
<div className="admin-oidc-loading">
|
||||
<div className="admin-oidc-spinner"></div>
|
||||
<span>Loading configuration...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSave} className="admin-oidc-card">
|
||||
<div className="admin-oidc-section">
|
||||
<h2>Status</h2>
|
||||
<div className="admin-oidc-toggle-group">
|
||||
<label className="admin-oidc-toggle-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enabled}
|
||||
onChange={(e) => setEnabled(e.target.checked)}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
<span className="admin-oidc-toggle-custom"></span>
|
||||
Enable OIDC Authentication
|
||||
</label>
|
||||
<p className="admin-oidc-field-help">
|
||||
When enabled, users can sign in using your organization's identity provider.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-oidc-section">
|
||||
<h2>Provider Configuration</h2>
|
||||
|
||||
<div className="admin-oidc-form-group">
|
||||
<label htmlFor="issuer-url">Issuer URL</label>
|
||||
<input
|
||||
id="issuer-url"
|
||||
type="url"
|
||||
value={issuerUrl}
|
||||
onChange={(e) => setIssuerUrl(e.target.value)}
|
||||
placeholder="https://your-provider.com"
|
||||
disabled={isSaving}
|
||||
/>
|
||||
<p className="admin-oidc-field-help">
|
||||
The base URL of your OIDC provider. Discovery document will be fetched from <code>/.well-known/openid-configuration</code>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="admin-oidc-form-row">
|
||||
<div className="admin-oidc-form-group">
|
||||
<label htmlFor="client-id">Client ID</label>
|
||||
<input
|
||||
id="client-id"
|
||||
type="text"
|
||||
value={clientId}
|
||||
onChange={(e) => setClientId(e.target.value)}
|
||||
placeholder="your-client-id"
|
||||
disabled={isSaving}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="admin-oidc-form-group">
|
||||
<label htmlFor="client-secret">
|
||||
Client Secret
|
||||
{config?.has_client_secret && (
|
||||
<span className="admin-oidc-secret-status"> (configured)</span>
|
||||
)}
|
||||
</label>
|
||||
<input
|
||||
id="client-secret"
|
||||
type="password"
|
||||
value={clientSecret}
|
||||
onChange={(e) => setClientSecret(e.target.value)}
|
||||
placeholder={config?.has_client_secret ? 'Leave blank to keep current' : 'Enter client secret'}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-oidc-form-group">
|
||||
<label htmlFor="scopes">Scopes</label>
|
||||
<input
|
||||
id="scopes"
|
||||
type="text"
|
||||
value={scopes}
|
||||
onChange={(e) => setScopes(e.target.value)}
|
||||
placeholder="openid profile email"
|
||||
disabled={isSaving}
|
||||
/>
|
||||
<p className="admin-oidc-field-help">
|
||||
Space-separated list of OIDC scopes to request. Common scopes: openid, profile, email, groups.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-oidc-section">
|
||||
<h2>User Provisioning</h2>
|
||||
|
||||
<div className="admin-oidc-toggle-group">
|
||||
<label className="admin-oidc-toggle-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoCreateUsers}
|
||||
onChange={(e) => setAutoCreateUsers(e.target.checked)}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
<span className="admin-oidc-toggle-custom"></span>
|
||||
Auto-create users on first login
|
||||
</label>
|
||||
<p className="admin-oidc-field-help">
|
||||
When enabled, new users will be created automatically when they sign in via OIDC for the first time.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="admin-oidc-form-group">
|
||||
<label htmlFor="admin-group">Admin Group (optional)</label>
|
||||
<input
|
||||
id="admin-group"
|
||||
type="text"
|
||||
value={adminGroup}
|
||||
onChange={(e) => setAdminGroup(e.target.value)}
|
||||
placeholder="admin, orchard-admins"
|
||||
disabled={isSaving}
|
||||
/>
|
||||
<p className="admin-oidc-field-help">
|
||||
Users in this group (from the groups claim) will be granted admin privileges. Leave blank to disable automatic admin assignment.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-oidc-form-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="admin-oidc-cancel-button"
|
||||
onClick={loadConfig}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="admin-oidc-submit-button"
|
||||
disabled={isSaving}
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<span className="admin-oidc-button-spinner"></span>
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
'Save Configuration'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
<div className="admin-oidc-info-card">
|
||||
<h3>Callback URL</h3>
|
||||
<p>Configure your identity provider with the following callback URL:</p>
|
||||
<code className="admin-oidc-callback-url">
|
||||
{window.location.origin}/api/v1/auth/oidc/callback
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AdminOIDCPage;
|
||||
Reference in New Issue
Block a user