Files
Stirling-PDF/frontend/src/components/shared/config/configSections/AdminConnectionsSection.tsx
T

338 lines
11 KiB
TypeScript

import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Stack, Text, Loader, Group, Divider } from '@mantine/core';
import { alert } from '../../../toast';
import RestartConfirmationModal from '../RestartConfirmationModal';
import { useRestartServer } from '../useRestartServer';
import ProviderCard from './ProviderCard';
import {
ALL_PROVIDERS,
OAUTH2_PROVIDERS,
GENERIC_OAUTH2_PROVIDER,
SAML2_PROVIDER,
Provider,
} from './providerDefinitions';
interface ConnectionsSettingsData {
oauth2?: {
enabled?: boolean;
issuer?: string;
clientId?: string;
clientSecret?: string;
provider?: string;
autoCreateUser?: boolean;
blockRegistration?: boolean;
useAsUsername?: string;
scopes?: string;
client?: {
[key: string]: any;
};
};
saml2?: {
[key: string]: any;
};
mail?: {
enabled?: boolean;
host?: string;
port?: number;
username?: string;
password?: string;
from?: string;
};
}
export default function AdminConnectionsSection() {
const { t } = useTranslation();
const [loading, setLoading] = useState(true);
const [settings, setSettings] = useState<ConnectionsSettingsData>({});
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
useEffect(() => {
fetchSettings();
}, []);
const fetchSettings = async () => {
try {
// Fetch security settings (oauth2, saml2)
const securityResponse = await fetch('/api/v1/admin/settings/section/security');
const securityData = securityResponse.ok ? await securityResponse.json() : {};
// Fetch mail settings
const mailResponse = await fetch('/api/v1/admin/settings/section/mail');
const mailData = mailResponse.ok ? await mailResponse.json() : {};
setSettings({
oauth2: securityData.oauth2 || {},
saml2: securityData.saml2 || {},
mail: mailData || {}
});
} catch (error) {
console.error('Failed to fetch connections settings:', error);
alert({
alertType: 'error',
title: t('admin.error', 'Error'),
body: t('admin.settings.fetchError', 'Failed to load settings'),
});
} finally {
setLoading(false);
}
};
const isProviderConfigured = (provider: Provider): boolean => {
if (provider.id === 'saml2') {
return settings.saml2?.enabled === true;
}
if (provider.id === 'smtp') {
return settings.mail?.enabled === true;
}
if (provider.id === 'oauth2-generic') {
return settings.oauth2?.enabled === true;
}
// Check if specific OAuth2 provider is configured (has clientId)
const providerSettings = settings.oauth2?.client?.[provider.id];
return !!(providerSettings?.clientId);
};
const getProviderSettings = (provider: Provider): Record<string, any> => {
if (provider.id === 'saml2') {
return settings.saml2 || {};
}
if (provider.id === 'smtp') {
return settings.mail || {};
}
if (provider.id === 'oauth2-generic') {
// Generic OAuth2 settings are at the root oauth2 level
return {
enabled: settings.oauth2?.enabled,
provider: settings.oauth2?.provider,
issuer: settings.oauth2?.issuer,
clientId: settings.oauth2?.clientId,
clientSecret: settings.oauth2?.clientSecret,
scopes: settings.oauth2?.scopes,
useAsUsername: settings.oauth2?.useAsUsername,
autoCreateUser: settings.oauth2?.autoCreateUser,
blockRegistration: settings.oauth2?.blockRegistration,
};
}
// Specific OAuth2 provider settings
return settings.oauth2?.client?.[provider.id] || {};
};
const handleProviderSave = async (provider: Provider, providerSettings: Record<string, any>) => {
try {
if (provider.id === 'smtp') {
// Mail settings use a different endpoint
const response = await fetch('/api/v1/admin/settings/section/mail', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(providerSettings),
});
if (response.ok) {
await fetchSettings(); // Refresh settings
alert({
alertType: 'success',
title: t('admin.success', 'Success'),
body: t('admin.settings.saveSuccess', 'Settings saved successfully'),
});
showRestartModal();
} else {
throw new Error('Failed to save');
}
} else {
// OAuth2/SAML2 use delta settings
const deltaSettings: Record<string, any> = {};
if (provider.id === 'saml2') {
// SAML2 settings
Object.keys(providerSettings).forEach((key) => {
deltaSettings[`security.saml2.${key}`] = providerSettings[key];
});
} else if (provider.id === 'oauth2-generic') {
// Generic OAuth2 settings at root level
Object.keys(providerSettings).forEach((key) => {
deltaSettings[`security.oauth2.${key}`] = providerSettings[key];
});
} else {
// Specific OAuth2 provider (google, github, keycloak)
Object.keys(providerSettings).forEach((key) => {
deltaSettings[`security.oauth2.client.${provider.id}.${key}`] = providerSettings[key];
});
}
const response = await fetch('/api/v1/admin/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ settings: deltaSettings }),
});
if (response.ok) {
await fetchSettings(); // Refresh settings
alert({
alertType: 'success',
title: t('admin.success', 'Success'),
body: t('admin.settings.saveSuccess', 'Settings saved successfully'),
});
showRestartModal();
} else {
throw new Error('Failed to save');
}
}
} catch (error) {
alert({
alertType: 'error',
title: t('admin.error', 'Error'),
body: t('admin.settings.saveError', 'Failed to save settings'),
});
}
};
const handleProviderDisconnect = async (provider: Provider) => {
try {
if (provider.id === 'smtp') {
// Mail settings use a different endpoint
const response = await fetch('/api/v1/admin/settings/section/mail', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: false }),
});
if (response.ok) {
await fetchSettings();
alert({
alertType: 'success',
title: t('admin.success', 'Success'),
body: t('admin.settings.connections.disconnected', 'Provider disconnected successfully'),
});
showRestartModal();
} else {
throw new Error('Failed to disconnect');
}
} else {
const deltaSettings: Record<string, any> = {};
if (provider.id === 'saml2') {
deltaSettings['security.saml2.enabled'] = false;
} else if (provider.id === 'oauth2-generic') {
deltaSettings['security.oauth2.enabled'] = false;
} else {
// Clear all fields for specific OAuth2 provider
provider.fields.forEach((field) => {
deltaSettings[`security.oauth2.client.${provider.id}.${field.key}`] = '';
});
}
const response = await fetch('/api/v1/admin/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ settings: deltaSettings }),
});
if (response.ok) {
await fetchSettings();
alert({
alertType: 'success',
title: t('admin.success', 'Success'),
body: t('admin.settings.connections.disconnected', 'Provider disconnected successfully'),
});
showRestartModal();
} else {
throw new Error('Failed to disconnect');
}
}
} catch (error) {
alert({
alertType: 'error',
title: t('admin.error', 'Error'),
body: t('admin.settings.connections.disconnectError', 'Failed to disconnect provider'),
});
}
};
if (loading) {
return (
<Stack align="center" justify="center" h={200}>
<Loader size="lg" />
</Stack>
);
}
const linkedProviders = ALL_PROVIDERS.filter((p) => isProviderConfigured(p));
const availableProviders = ALL_PROVIDERS.filter((p) => !isProviderConfigured(p));
return (
<Stack gap="xl">
{/* Header */}
<div>
<Text fw={600} size="lg">
{t('admin.settings.connections.title', 'Connections')}
</Text>
<Text size="sm" c="dimmed">
{t(
'admin.settings.connections.description',
'Configure external authentication providers like OAuth2 and SAML.'
)}
</Text>
</div>
{/* Linked Services Section - Only show if there are linked providers */}
{linkedProviders.length > 0 && (
<>
<div>
<Text fw={600} size="md" mb="md">
{t('admin.settings.connections.linkedServices', 'Linked Services')}
</Text>
<Stack gap="sm">
{linkedProviders.map((provider) => (
<ProviderCard
key={provider.id}
provider={provider}
isConfigured={true}
settings={getProviderSettings(provider)}
onSave={(providerSettings) => handleProviderSave(provider, providerSettings)}
onDisconnect={() => handleProviderDisconnect(provider)}
/>
))}
</Stack>
</div>
{/* Divider between sections */}
{availableProviders.length > 0 && <Divider />}
</>
)}
{/* Unlinked Services Section */}
{availableProviders.length > 0 && (
<div>
<Text fw={600} size="md" mb="md">
{t('admin.settings.connections.unlinkedServices', 'Unlinked Services')}
</Text>
<Stack gap="sm">
{availableProviders.map((provider) => (
<ProviderCard
key={provider.id}
provider={provider}
isConfigured={false}
onSave={(providerSettings) => handleProviderSave(provider, providerSettings)}
/>
))}
</Stack>
</div>
)}
{/* Restart Confirmation Modal */}
<RestartConfirmationModal
opened={restartModalOpened}
onClose={closeRestartModal}
onRestart={restartServer}
/>
</Stack>
);
}