mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-03-04 02:20:19 +01:00
remove unused settings and enhance others
This commit is contained in:
@@ -7,7 +7,6 @@ import AdminSecuritySection from './configSections/AdminSecuritySection';
|
||||
import AdminConnectionsSection from './configSections/AdminConnectionsSection';
|
||||
import AdminPrivacySection from './configSections/AdminPrivacySection';
|
||||
import AdminAdvancedSection from './configSections/AdminAdvancedSection';
|
||||
import AdminMailSection from './configSections/AdminMailSection';
|
||||
import AdminLegalSection from './configSections/AdminLegalSection';
|
||||
import AdminPremiumSection from './configSections/AdminPremiumSection';
|
||||
import AdminEndpointsSection from './configSections/AdminEndpointsSection';
|
||||
@@ -93,12 +92,6 @@ export const createConfigNavSections = (
|
||||
icon: 'link-rounded',
|
||||
component: <AdminConnectionsSection />
|
||||
},
|
||||
{
|
||||
key: 'adminMail',
|
||||
label: 'Mail',
|
||||
icon: 'mail-rounded',
|
||||
component: <AdminMailSection />
|
||||
},
|
||||
{
|
||||
key: 'adminLegal',
|
||||
label: 'Legal',
|
||||
|
||||
@@ -1,40 +1,49 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TextInput, Switch, Button, Stack, Paper, Text, Loader, Group, Select, Badge, PasswordInput } from '@mantine/core';
|
||||
import { Stack, Text, Loader, Group, Divider } from '@mantine/core';
|
||||
import { alert } from '../../../toast';
|
||||
import LocalIcon from '../../LocalIcon';
|
||||
import RestartConfirmationModal from '../RestartConfirmationModal';
|
||||
import { useRestartServer } from '../useRestartServer';
|
||||
|
||||
interface OAuth2Settings {
|
||||
enabled?: boolean;
|
||||
issuer?: string;
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
provider?: string;
|
||||
autoCreateUser?: boolean;
|
||||
blockRegistration?: boolean;
|
||||
useAsUsername?: string;
|
||||
scopes?: string;
|
||||
}
|
||||
|
||||
interface SAML2Settings {
|
||||
enabled?: boolean;
|
||||
provider?: string;
|
||||
autoCreateUser?: boolean;
|
||||
blockRegistration?: boolean;
|
||||
registrationId?: string;
|
||||
}
|
||||
import ProviderCard from './ProviderCard';
|
||||
import {
|
||||
ALL_PROVIDERS,
|
||||
OAUTH2_PROVIDERS,
|
||||
GENERIC_OAUTH2_PROVIDER,
|
||||
SAML2_PROVIDER,
|
||||
Provider,
|
||||
} from './providerDefinitions';
|
||||
|
||||
interface ConnectionsSettingsData {
|
||||
oauth2?: OAuth2Settings;
|
||||
saml2?: SAML2Settings;
|
||||
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 [saving, setSaving] = useState(false);
|
||||
const [settings, setSettings] = useState<ConnectionsSettingsData>({});
|
||||
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
|
||||
|
||||
@@ -44,16 +53,19 @@ export default function AdminConnectionsSection() {
|
||||
|
||||
const fetchSettings = async () => {
|
||||
try {
|
||||
// OAuth2 and SAML2 are nested under security section
|
||||
const response = await fetch('/api/v1/admin/settings/section/security');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
// Extract oauth2 and saml2 from security section
|
||||
setSettings({
|
||||
oauth2: data.oauth2 || {},
|
||||
saml2: data.saml2 || {}
|
||||
});
|
||||
}
|
||||
// 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({
|
||||
@@ -66,36 +78,111 @@ export default function AdminConnectionsSection() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
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 {
|
||||
// Use delta update endpoint with dot notation for nested oauth2/saml2 settings
|
||||
const deltaSettings: Record<string, any> = {};
|
||||
|
||||
// Convert oauth2 settings to dot notation
|
||||
if (settings.oauth2) {
|
||||
Object.keys(settings.oauth2).forEach(key => {
|
||||
deltaSettings[`security.oauth2.${key}`] = (settings.oauth2 as any)[key];
|
||||
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),
|
||||
});
|
||||
}
|
||||
|
||||
// Convert saml2 settings to dot notation
|
||||
if (settings.saml2) {
|
||||
Object.keys(settings.saml2).forEach(key => {
|
||||
deltaSettings[`security.saml2.${key}`] = (settings.saml2 as any)[key];
|
||||
});
|
||||
}
|
||||
|
||||
const response = await fetch('/api/v1/admin/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ settings: deltaSettings }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showRestartModal();
|
||||
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 {
|
||||
throw new Error('Failed to save');
|
||||
// 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({
|
||||
@@ -103,8 +190,68 @@ export default function AdminConnectionsSection() {
|
||||
title: t('admin.error', 'Error'),
|
||||
body: t('admin.settings.saveError', 'Failed to save settings'),
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
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'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -116,225 +263,68 @@ export default function AdminConnectionsSection() {
|
||||
);
|
||||
}
|
||||
|
||||
const getProviderIcon = (provider?: string) => {
|
||||
switch (provider?.toLowerCase()) {
|
||||
case 'google':
|
||||
return <LocalIcon icon="google-rounded" width="1rem" height="1rem" />;
|
||||
case 'github':
|
||||
return <LocalIcon icon="github-rounded" width="1rem" height="1rem" />;
|
||||
case 'keycloak':
|
||||
return <LocalIcon icon="key-rounded" width="1rem" height="1rem" />;
|
||||
default:
|
||||
return <LocalIcon icon="cloud-rounded" width="1rem" height="1rem" />;
|
||||
}
|
||||
};
|
||||
const linkedProviders = ALL_PROVIDERS.filter((p) => isProviderConfigured(p));
|
||||
const availableProviders = ALL_PROVIDERS.filter((p) => !isProviderConfigured(p));
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<Stack gap="xl">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<Text fw={600} size="lg">{t('admin.settings.connections.title', 'Connections')}</Text>
|
||||
<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.')}
|
||||
{t(
|
||||
'admin.settings.connections.description',
|
||||
'Configure external authentication providers like OAuth2 and SAML.'
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* OAuth2 Settings */}
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between">
|
||||
<Group gap="xs">
|
||||
<LocalIcon icon="cloud-rounded" width="1.25rem" height="1.25rem" />
|
||||
<Text fw={600} size="sm">{t('admin.settings.connections.oauth2', 'OAuth2')}</Text>
|
||||
</Group>
|
||||
<Badge color={settings.oauth2?.enabled ? 'green' : 'gray'} size="sm">
|
||||
{settings.oauth2?.enabled ? t('admin.status.active', 'Active') : t('admin.status.inactive', 'Inactive')}
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.connections.oauth2.enabled', 'Enable OAuth2')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.connections.oauth2.enabled.description', 'Allow users to authenticate using OAuth2 providers')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.oauth2?.enabled || false}
|
||||
onChange={(e) => setSettings({ ...settings, oauth2: { ...settings.oauth2, enabled: e.target.checked } })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Linked Services Section - Only show if there are linked providers */}
|
||||
{linkedProviders.length > 0 && (
|
||||
<>
|
||||
<div>
|
||||
<Select
|
||||
label={t('admin.settings.connections.oauth2.provider', 'Provider')}
|
||||
description={t('admin.settings.connections.oauth2.provider.description', 'The OAuth2 provider to use for authentication')}
|
||||
value={settings.oauth2?.provider || ''}
|
||||
onChange={(value) => setSettings({ ...settings, oauth2: { ...settings.oauth2, provider: value || '' } })}
|
||||
data={[
|
||||
{ value: 'google', label: 'Google' },
|
||||
{ value: 'github', label: 'GitHub' },
|
||||
{ value: 'keycloak', label: 'Keycloak' },
|
||||
]}
|
||||
leftSection={getProviderIcon(settings.oauth2?.provider)}
|
||||
comboboxProps={{ zIndex: 1400 }}
|
||||
/>
|
||||
<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>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.connections.oauth2.issuer', 'Issuer URL')}
|
||||
description={t('admin.settings.connections.oauth2.issuer.description', 'The OAuth2 provider issuer URL')}
|
||||
value={settings.oauth2?.issuer || ''}
|
||||
onChange={(e) => setSettings({ ...settings, oauth2: { ...settings.oauth2, issuer: e.target.value } })}
|
||||
placeholder="https://accounts.google.com"
|
||||
/>
|
||||
</div>
|
||||
{/* Divider between sections */}
|
||||
{availableProviders.length > 0 && <Divider />}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.connections.oauth2.clientId', 'Client ID')}
|
||||
description={t('admin.settings.connections.oauth2.clientId.description', 'The OAuth2 client ID from your provider')}
|
||||
value={settings.oauth2?.clientId || ''}
|
||||
onChange={(e) => setSettings({ ...settings, oauth2: { ...settings.oauth2, clientId: e.target.value } })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<PasswordInput
|
||||
label={t('admin.settings.connections.oauth2.clientSecret', 'Client Secret')}
|
||||
description={t('admin.settings.connections.oauth2.clientSecret.description', 'The OAuth2 client secret from your provider')}
|
||||
value={settings.oauth2?.clientSecret || ''}
|
||||
onChange={(e) => setSettings({ ...settings, oauth2: { ...settings.oauth2, clientSecret: e.target.value } })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.connections.oauth2.autoCreateUser', 'Auto Create Users')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.connections.oauth2.autoCreateUser.description', 'Automatically create user accounts on first OAuth2 login')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.oauth2?.autoCreateUser || false}
|
||||
onChange={(e) => setSettings({ ...settings, oauth2: { ...settings.oauth2, autoCreateUser: e.target.checked } })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.connections.oauth2.blockRegistration', 'Block Registration')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.connections.oauth2.blockRegistration.description', 'Prevent new user registration via OAuth2')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.oauth2?.blockRegistration || false}
|
||||
onChange={(e) => setSettings({ ...settings, oauth2: { ...settings.oauth2, blockRegistration: e.target.checked } })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.connections.oauth2.useAsUsername', 'Use as Username')}
|
||||
description={t('admin.settings.connections.oauth2.useAsUsername.description', 'The OAuth2 claim to use as the username (e.g., email, sub)')}
|
||||
value={settings.oauth2?.useAsUsername || ''}
|
||||
onChange={(e) => setSettings({ ...settings, oauth2: { ...settings.oauth2, useAsUsername: e.target.value } })}
|
||||
placeholder="email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.connections.oauth2.scopes', 'Scopes')}
|
||||
description={t('admin.settings.connections.oauth2.scopes.description', 'OAuth2 scopes (comma-separated, e.g., openid, profile, email)')}
|
||||
value={settings.oauth2?.scopes || ''}
|
||||
onChange={(e) => setSettings({ ...settings, oauth2: { ...settings.oauth2, scopes: e.target.value } })}
|
||||
placeholder="openid, profile, email"
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* SAML2 Settings */}
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between">
|
||||
<Group gap="xs">
|
||||
<LocalIcon icon="key-rounded" width="1.25rem" height="1.25rem" />
|
||||
<Text fw={600} size="sm">{t('admin.settings.connections.saml2', 'SAML2')}</Text>
|
||||
</Group>
|
||||
<Badge color={settings.saml2?.enabled ? 'green' : 'gray'} size="sm">
|
||||
{settings.saml2?.enabled ? t('admin.status.active', 'Active') : t('admin.status.inactive', 'Inactive')}
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.connections.saml2.enabled', 'Enable SAML2')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.connections.saml2.enabled.description', 'Allow users to authenticate using SAML2 providers')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.saml2?.enabled || false}
|
||||
onChange={(e) => setSettings({ ...settings, saml2: { ...settings.saml2, enabled: e.target.checked } })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.connections.saml2.provider', 'Provider')}
|
||||
description={t('admin.settings.connections.saml2.provider.description', 'The SAML2 provider name')}
|
||||
value={settings.saml2?.provider || ''}
|
||||
onChange={(e) => setSettings({ ...settings, saml2: { ...settings.saml2, provider: e.target.value } })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.connections.saml2.registrationId', 'Registration ID')}
|
||||
description={t('admin.settings.connections.saml2.registrationId.description', 'The SAML2 registration identifier')}
|
||||
value={settings.saml2?.registrationId || ''}
|
||||
onChange={(e) => setSettings({ ...settings, saml2: { ...settings.saml2, registrationId: e.target.value } })}
|
||||
placeholder="stirling"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.connections.saml2.autoCreateUser', 'Auto Create Users')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.connections.saml2.autoCreateUser.description', 'Automatically create user accounts on first SAML2 login')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.saml2?.autoCreateUser || false}
|
||||
onChange={(e) => setSettings({ ...settings, saml2: { ...settings.saml2, autoCreateUser: e.target.checked } })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.connections.saml2.blockRegistration', 'Block Registration')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.connections.saml2.blockRegistration.description', 'Prevent new user registration via SAML2')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.saml2?.blockRegistration || false}
|
||||
onChange={(e) => setSettings({ ...settings, saml2: { ...settings.saml2, blockRegistration: e.target.checked } })}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Save Button */}
|
||||
<Group justify="flex-end">
|
||||
<Button onClick={handleSave} loading={saving} size="sm">
|
||||
{t('admin.settings.save', 'Save Changes')}
|
||||
</Button>
|
||||
</Group>
|
||||
{/* 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
|
||||
|
||||
@@ -111,20 +111,10 @@ export default function AdminGeneralSection() {
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* UI Settings */}
|
||||
{/* System Settings */}
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Text fw={600} size="sm" mb="xs">{t('admin.settings.general.ui', 'User Interface')}</Text>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.general.appName', 'Application Name')}
|
||||
description={t('admin.settings.general.appName.description', 'The name displayed in the browser tab and home page')}
|
||||
value={settings.ui.appName || ''}
|
||||
onChange={(e) => setSettings({ ...settings, ui: { ...settings.ui, appName: e.target.value } })}
|
||||
placeholder="Stirling PDF"
|
||||
/>
|
||||
</div>
|
||||
<Text fw={600} size="sm" mb="xs">{t('admin.settings.general.system', 'System')}</Text>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
@@ -136,16 +126,6 @@ export default function AdminGeneralSection() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.general.homeDescription', 'Home Description')}
|
||||
description={t('admin.settings.general.homeDescription.description', 'The description text shown on the home page')}
|
||||
value={settings.ui.homeDescription || ''}
|
||||
onChange={(e) => setSettings({ ...settings, ui: { ...settings.ui, homeDescription: e.target.value } })}
|
||||
placeholder="Your locally hosted one-stop-shop for all your PDF needs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<MultiSelect
|
||||
label={t('admin.settings.general.languages', 'Available Languages')}
|
||||
@@ -170,13 +150,6 @@ export default function AdminGeneralSection() {
|
||||
comboboxProps={{ zIndex: 1400 }}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* System Settings */}
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Text fw={600} size="sm" mb="xs">{t('admin.settings.general.system', 'System')}</Text>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TextInput, NumberInput, Switch, Button, Stack, Paper, Text, Loader, Group, Select, PasswordInput } from '@mantine/core';
|
||||
import { TextInput, NumberInput, Switch, Button, Stack, Paper, Text, Loader, Group, Select, PasswordInput, Alert } from '@mantine/core';
|
||||
import { alert } from '../../../toast';
|
||||
import LocalIcon from '../../LocalIcon';
|
||||
import RestartConfirmationModal from '../RestartConfirmationModal';
|
||||
import { useRestartServer } from '../useRestartServer';
|
||||
|
||||
@@ -11,10 +12,6 @@ interface SecuritySettingsData {
|
||||
loginMethod?: string;
|
||||
loginAttemptCount?: number;
|
||||
loginResetTimeMinutes?: number;
|
||||
initialLogin?: {
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
jwt?: {
|
||||
persistence?: boolean;
|
||||
enableKeyRotation?: boolean;
|
||||
@@ -167,32 +164,17 @@ export default function AdminSecuritySection() {
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Initial Login Credentials */}
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Text fw={600} size="sm" mb="xs">{t('admin.settings.security.initialLogin', 'Initial Login')}</Text>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.security.initialLogin.username', 'Initial Username')}
|
||||
description={t('admin.settings.security.initialLogin.username.description', 'Default admin username for first-time setup')}
|
||||
value={settings.initialLogin?.username || ''}
|
||||
onChange={(e) => setSettings({ ...settings, initialLogin: { ...settings.initialLogin, username: e.target.value } })}
|
||||
placeholder="admin"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<PasswordInput
|
||||
label={t('admin.settings.security.initialLogin.password', 'Initial Password')}
|
||||
description={t('admin.settings.security.initialLogin.password.description', 'Default admin password for first-time setup')}
|
||||
value={settings.initialLogin?.password || ''}
|
||||
onChange={(e) => setSettings({ ...settings, initialLogin: { ...settings.initialLogin, password: e.target.value } })}
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Paper>
|
||||
{/* SSO/SAML Notice */}
|
||||
<Alert
|
||||
variant="light"
|
||||
color="blue"
|
||||
title={t('admin.settings.security.ssoNotice.title', 'Looking for SSO/SAML settings?')}
|
||||
icon={<LocalIcon icon="info-rounded" width="1rem" height="1rem" />}
|
||||
>
|
||||
<Text size="sm">
|
||||
{t('admin.settings.security.ssoNotice.message', 'OAuth2 and SAML2 authentication providers have been moved to the Connections menu for easier management.')}
|
||||
</Text>
|
||||
</Alert>
|
||||
|
||||
{/* JWT Settings */}
|
||||
<Paper withBorder p="md" radius="md">
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
import { useState } from 'react';
|
||||
import { Paper, Group, Text, Badge, Button, Collapse, Stack, TextInput, Switch, PasswordInput } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import LocalIcon from '../../LocalIcon';
|
||||
import { Provider, ProviderField } from './providerDefinitions';
|
||||
|
||||
interface ProviderCardProps {
|
||||
provider: Provider;
|
||||
isConfigured: boolean;
|
||||
settings?: Record<string, any>;
|
||||
onSave?: (settings: Record<string, any>) => void;
|
||||
onDisconnect?: () => void;
|
||||
}
|
||||
|
||||
export default function ProviderCard({
|
||||
provider,
|
||||
isConfigured,
|
||||
settings = {},
|
||||
onSave,
|
||||
onDisconnect,
|
||||
}: ProviderCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [localSettings, setLocalSettings] = useState<Record<string, any>>(settings);
|
||||
|
||||
// Initialize local settings with defaults when opening an unconfigured provider
|
||||
const handleConnectToggle = () => {
|
||||
if (!isConfigured && !expanded) {
|
||||
// First time opening an unconfigured provider - initialize with defaults
|
||||
const defaultSettings: Record<string, any> = {};
|
||||
provider.fields.forEach((field) => {
|
||||
if (field.defaultValue !== undefined) {
|
||||
defaultSettings[field.key] = field.defaultValue;
|
||||
}
|
||||
});
|
||||
setLocalSettings(defaultSettings);
|
||||
}
|
||||
setExpanded(!expanded);
|
||||
};
|
||||
|
||||
const handleFieldChange = (key: string, value: any) => {
|
||||
setLocalSettings((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (onSave) {
|
||||
onSave(localSettings);
|
||||
}
|
||||
setExpanded(false);
|
||||
};
|
||||
|
||||
const renderField = (field: ProviderField) => {
|
||||
const value = localSettings[field.key] ?? field.defaultValue ?? '';
|
||||
|
||||
switch (field.type) {
|
||||
case 'switch':
|
||||
return (
|
||||
<div key={field.key} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{field.label}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>{field.description}</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={value || false}
|
||||
onChange={(e) => handleFieldChange(field.key, e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'password':
|
||||
return (
|
||||
<PasswordInput
|
||||
key={field.key}
|
||||
label={field.label}
|
||||
description={field.description}
|
||||
placeholder={field.placeholder}
|
||||
value={value}
|
||||
onChange={(e) => handleFieldChange(field.key, e.target.value)}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'textarea':
|
||||
return (
|
||||
<TextInput
|
||||
key={field.key}
|
||||
label={field.label}
|
||||
description={field.description}
|
||||
placeholder={field.placeholder}
|
||||
value={value}
|
||||
onChange={(e) => handleFieldChange(field.key, e.target.value)}
|
||||
multiline
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<TextInput
|
||||
key={field.key}
|
||||
label={field.label}
|
||||
description={field.description}
|
||||
placeholder={field.placeholder}
|
||||
value={value}
|
||||
onChange={(e) => handleFieldChange(field.key, e.target.value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderProviderIcon = () => {
|
||||
// If icon starts with '/', it's a path to an SVG file
|
||||
if (provider.icon.startsWith('/')) {
|
||||
return (
|
||||
<img
|
||||
src={provider.icon}
|
||||
alt={provider.name}
|
||||
style={{ width: '1.5rem', height: '1.5rem' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
// Otherwise use LocalIcon for iconify icons
|
||||
return <LocalIcon icon={provider.icon} width="1.5rem" height="1.5rem" />;
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
{/* Provider Header */}
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Group gap="sm" style={{ flex: 1, minWidth: 0 }}>
|
||||
{renderProviderIcon()}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text fw={600} size="sm">{provider.name}</Text>
|
||||
<Text size="xs" c="dimmed" truncate>{provider.scope}</Text>
|
||||
</div>
|
||||
</Group>
|
||||
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Button
|
||||
variant={isConfigured ? "subtle" : "filled"}
|
||||
size="xs"
|
||||
onClick={isConfigured ? () => setExpanded(!expanded) : handleConnectToggle}
|
||||
rightSection={
|
||||
expanded ? (
|
||||
<LocalIcon
|
||||
icon="close-rounded"
|
||||
width="1rem"
|
||||
height="1rem"
|
||||
/>
|
||||
) : (isConfigured ? (
|
||||
<LocalIcon
|
||||
icon="expand-more-rounded"
|
||||
width="1rem"
|
||||
height="1rem"
|
||||
/>
|
||||
) : undefined)
|
||||
}
|
||||
>
|
||||
{isConfigured
|
||||
? (expanded ? t('admin.close', 'Close') : t('admin.expand', 'Expand'))
|
||||
: (expanded ? t('admin.close', 'Close') : t('admin.settings.connections.connect', 'Connect'))
|
||||
}
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{/* Expandable Settings */}
|
||||
<Collapse in={expanded}>
|
||||
<Stack gap="md" mt="xs">
|
||||
{provider.fields.map((field) => renderField(field))}
|
||||
|
||||
<Group justify="flex-end" mt="sm">
|
||||
{onDisconnect && (
|
||||
<Button
|
||||
variant="outline"
|
||||
color="red"
|
||||
size="sm"
|
||||
onClick={onDisconnect}
|
||||
>
|
||||
{t('admin.settings.connections.disconnect', 'Disconnect')}
|
||||
</Button>
|
||||
)}
|
||||
<Button size="sm" onClick={handleSave}>
|
||||
{t('admin.settings.save', 'Save Changes')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Collapse>
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
export type ProviderType = 'oauth2' | 'saml2';
|
||||
|
||||
export interface ProviderField {
|
||||
key: string;
|
||||
type: 'text' | 'password' | 'switch' | 'textarea';
|
||||
label: string;
|
||||
description: string;
|
||||
placeholder?: string;
|
||||
defaultValue?: any;
|
||||
}
|
||||
|
||||
export interface Provider {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
type: ProviderType;
|
||||
scope: string; // Summary of what this provider does
|
||||
businessTier?: boolean; // Enterprise only
|
||||
fields: ProviderField[];
|
||||
}
|
||||
|
||||
export const OAUTH2_PROVIDERS: Provider[] = [
|
||||
{
|
||||
id: 'google',
|
||||
name: 'Google',
|
||||
icon: '/Login/google.svg',
|
||||
type: 'oauth2',
|
||||
scope: 'Sign-in authentication',
|
||||
fields: [
|
||||
{
|
||||
key: 'clientId',
|
||||
type: 'text',
|
||||
label: 'Client ID',
|
||||
description: 'The OAuth2 client ID from Google Cloud Console',
|
||||
placeholder: 'your-client-id.apps.googleusercontent.com',
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
type: 'password',
|
||||
label: 'Client Secret',
|
||||
description: 'The OAuth2 client secret from Google Cloud Console',
|
||||
},
|
||||
{
|
||||
key: 'scopes',
|
||||
type: 'text',
|
||||
label: 'Scopes',
|
||||
description: 'Comma-separated OAuth2 scopes',
|
||||
defaultValue: 'email, profile',
|
||||
},
|
||||
{
|
||||
key: 'useAsUsername',
|
||||
type: 'text',
|
||||
label: 'Use as Username',
|
||||
description: 'Field to use as username (email, name, given_name, family_name)',
|
||||
defaultValue: 'email',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'github',
|
||||
name: 'GitHub',
|
||||
icon: '/Login/github.svg',
|
||||
type: 'oauth2',
|
||||
scope: 'Sign-in authentication',
|
||||
fields: [
|
||||
{
|
||||
key: 'clientId',
|
||||
type: 'text',
|
||||
label: 'Client ID',
|
||||
description: 'The OAuth2 client ID from GitHub Developer Settings',
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
type: 'password',
|
||||
label: 'Client Secret',
|
||||
description: 'The OAuth2 client secret from GitHub Developer Settings',
|
||||
},
|
||||
{
|
||||
key: 'scopes',
|
||||
type: 'text',
|
||||
label: 'Scopes',
|
||||
description: 'Comma-separated OAuth2 scopes',
|
||||
defaultValue: 'read:user',
|
||||
},
|
||||
{
|
||||
key: 'useAsUsername',
|
||||
type: 'text',
|
||||
label: 'Use as Username',
|
||||
description: 'Field to use as username (email, login, name)',
|
||||
defaultValue: 'login',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'keycloak',
|
||||
name: 'Keycloak',
|
||||
icon: 'key-rounded',
|
||||
type: 'oauth2',
|
||||
scope: 'SSO',
|
||||
fields: [
|
||||
{
|
||||
key: 'issuer',
|
||||
type: 'text',
|
||||
label: 'Issuer URL',
|
||||
description: "URL of the Keycloak realm's OpenID Connect Discovery endpoint",
|
||||
placeholder: 'https://keycloak.example.com/realms/myrealm',
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
type: 'text',
|
||||
label: 'Client ID',
|
||||
description: 'The OAuth2 client ID from Keycloak',
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
type: 'password',
|
||||
label: 'Client Secret',
|
||||
description: 'The OAuth2 client secret from Keycloak',
|
||||
},
|
||||
{
|
||||
key: 'scopes',
|
||||
type: 'text',
|
||||
label: 'Scopes',
|
||||
description: 'Comma-separated OAuth2 scopes',
|
||||
defaultValue: 'openid, profile, email',
|
||||
},
|
||||
{
|
||||
key: 'useAsUsername',
|
||||
type: 'text',
|
||||
label: 'Use as Username',
|
||||
description: 'Field to use as username (email, name, given_name, family_name, preferred_username)',
|
||||
defaultValue: 'preferred_username',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const GENERIC_OAUTH2_PROVIDER: Provider = {
|
||||
id: 'oauth2-generic',
|
||||
name: 'Generic OAuth2',
|
||||
icon: 'link-rounded',
|
||||
type: 'oauth2',
|
||||
scope: 'SSO',
|
||||
fields: [
|
||||
{
|
||||
key: 'enabled',
|
||||
type: 'switch',
|
||||
label: 'Enable Generic OAuth2',
|
||||
description: 'Enable authentication using a custom OAuth2 provider',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: 'provider',
|
||||
type: 'text',
|
||||
label: 'Provider Name',
|
||||
description: 'The name of your OAuth2 provider (e.g., Azure AD, Okta)',
|
||||
placeholder: 'azure-ad',
|
||||
},
|
||||
{
|
||||
key: 'issuer',
|
||||
type: 'text',
|
||||
label: 'Issuer URL',
|
||||
description: 'Provider that supports OpenID Connect Discovery (/.well-known/openid-configuration)',
|
||||
placeholder: 'https://login.microsoftonline.com/{tenant-id}/v2.0',
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
type: 'text',
|
||||
label: 'Client ID',
|
||||
description: 'The OAuth2 client ID from your provider',
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
type: 'password',
|
||||
label: 'Client Secret',
|
||||
description: 'The OAuth2 client secret from your provider',
|
||||
},
|
||||
{
|
||||
key: 'scopes',
|
||||
type: 'text',
|
||||
label: 'Scopes',
|
||||
description: 'Comma-separated OAuth2 scopes',
|
||||
defaultValue: 'openid, profile, email',
|
||||
},
|
||||
{
|
||||
key: 'useAsUsername',
|
||||
type: 'text',
|
||||
label: 'Use as Username',
|
||||
description: 'Field to use as username',
|
||||
defaultValue: 'email',
|
||||
},
|
||||
{
|
||||
key: 'autoCreateUser',
|
||||
type: 'switch',
|
||||
label: 'Auto Create Users',
|
||||
description: 'Automatically create user accounts on first OAuth2 login',
|
||||
defaultValue: true,
|
||||
},
|
||||
{
|
||||
key: 'blockRegistration',
|
||||
type: 'switch',
|
||||
label: 'Block Registration',
|
||||
description: 'Prevent new user registration via OAuth2',
|
||||
defaultValue: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const SMTP_PROVIDER: Provider = {
|
||||
id: 'smtp',
|
||||
name: 'SMTP Mail',
|
||||
icon: 'mail-rounded',
|
||||
type: 'oauth2', // Using oauth2 as the base type, but it's really just a generic provider
|
||||
scope: 'Email Notifications',
|
||||
fields: [
|
||||
{
|
||||
key: 'enabled',
|
||||
type: 'switch',
|
||||
label: 'Enable Mail',
|
||||
description: 'Enable email notifications and SMTP functionality',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: 'host',
|
||||
type: 'text',
|
||||
label: 'SMTP Host',
|
||||
description: 'The hostname or IP address of your SMTP server',
|
||||
placeholder: 'smtp.example.com',
|
||||
},
|
||||
{
|
||||
key: 'port',
|
||||
type: 'text',
|
||||
label: 'SMTP Port',
|
||||
description: 'The port number for SMTP connection (typically 25, 465, or 587)',
|
||||
placeholder: '587',
|
||||
defaultValue: '587',
|
||||
},
|
||||
{
|
||||
key: 'username',
|
||||
type: 'text',
|
||||
label: 'SMTP Username',
|
||||
description: 'Username for SMTP authentication',
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
type: 'password',
|
||||
label: 'SMTP Password',
|
||||
description: 'Password for SMTP authentication',
|
||||
},
|
||||
{
|
||||
key: 'from',
|
||||
type: 'text',
|
||||
label: 'From Address',
|
||||
description: 'The email address to use as the sender',
|
||||
placeholder: 'noreply@example.com',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const SAML2_PROVIDER: Provider = {
|
||||
id: 'saml2',
|
||||
name: 'SAML2',
|
||||
icon: 'verified-user-rounded',
|
||||
type: 'saml2',
|
||||
scope: 'SSO',
|
||||
businessTier: true,
|
||||
fields: [
|
||||
{
|
||||
key: 'enabled',
|
||||
type: 'switch',
|
||||
label: 'Enable SAML2',
|
||||
description: 'Enable SAML2 authentication (Enterprise only)',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: 'provider',
|
||||
type: 'text',
|
||||
label: 'Provider Name',
|
||||
description: 'The name of your SAML2 provider',
|
||||
},
|
||||
{
|
||||
key: 'registrationId',
|
||||
type: 'text',
|
||||
label: 'Registration ID',
|
||||
description: 'The name of your Service Provider (SP) app name',
|
||||
defaultValue: 'stirling',
|
||||
},
|
||||
{
|
||||
key: 'idpMetadataUri',
|
||||
type: 'text',
|
||||
label: 'IDP Metadata URI',
|
||||
description: 'The URI for your provider\'s metadata',
|
||||
placeholder: 'https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata',
|
||||
},
|
||||
{
|
||||
key: 'idpSingleLoginUrl',
|
||||
type: 'text',
|
||||
label: 'IDP Single Login URL',
|
||||
description: 'The URL for initiating SSO',
|
||||
placeholder: 'https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/sso/saml',
|
||||
},
|
||||
{
|
||||
key: 'idpSingleLogoutUrl',
|
||||
type: 'text',
|
||||
label: 'IDP Single Logout URL',
|
||||
description: 'The URL for initiating SLO',
|
||||
placeholder: 'https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/slo/saml',
|
||||
},
|
||||
{
|
||||
key: 'idpIssuer',
|
||||
type: 'text',
|
||||
label: 'IDP Issuer',
|
||||
description: 'The ID of your provider',
|
||||
},
|
||||
{
|
||||
key: 'idpCert',
|
||||
type: 'text',
|
||||
label: 'IDP Certificate',
|
||||
description: 'The certificate path (e.g., classpath:okta.cert)',
|
||||
placeholder: 'classpath:okta.cert',
|
||||
},
|
||||
{
|
||||
key: 'privateKey',
|
||||
type: 'text',
|
||||
label: 'Private Key',
|
||||
description: 'Your private key path',
|
||||
placeholder: 'classpath:saml-private-key.key',
|
||||
},
|
||||
{
|
||||
key: 'spCert',
|
||||
type: 'text',
|
||||
label: 'SP Certificate',
|
||||
description: 'Your signing certificate path',
|
||||
placeholder: 'classpath:saml-public-cert.crt',
|
||||
},
|
||||
{
|
||||
key: 'autoCreateUser',
|
||||
type: 'switch',
|
||||
label: 'Auto Create Users',
|
||||
description: 'Automatically create user accounts on first SAML2 login',
|
||||
defaultValue: true,
|
||||
},
|
||||
{
|
||||
key: 'blockRegistration',
|
||||
type: 'switch',
|
||||
label: 'Block Registration',
|
||||
description: 'Prevent new user registration via SAML2',
|
||||
defaultValue: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const ALL_PROVIDERS = [...OAUTH2_PROVIDERS, GENERIC_OAUTH2_PROVIDER, SAML2_PROVIDER, SMTP_PROVIDER];
|
||||
@@ -19,7 +19,6 @@ export type NavKey =
|
||||
| 'adminConnections'
|
||||
| 'adminPrivacy'
|
||||
| 'adminAdvanced'
|
||||
| 'adminMail'
|
||||
| 'adminLegal'
|
||||
| 'adminPremium'
|
||||
| 'adminEndpoints';
|
||||
|
||||
Reference in New Issue
Block a user