This commit is contained in:
Anthony Stirling 2025-11-12 15:05:27 +00:00
parent ebf4bab80b
commit 0da1ae06d9
25 changed files with 878 additions and 228 deletions

View File

@ -107,7 +107,8 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
} }
} }
// If we still don't have any authentication, check if it's a public endpoint. If not, deny the request // If we still don't have any authentication, check if it's a public endpoint. If not, deny
// the request
if (authentication == null || !authentication.isAuthenticated()) { if (authentication == null || !authentication.isAuthenticated()) {
String method = request.getMethod(); String method = request.getMethod();
String contextPath = request.getContextPath(); String contextPath = request.getContextPath();

View File

@ -64,19 +64,21 @@ const AppConfigModal: React.FC<AppConfigModalProps> = ({ opened, onClose }) => {
headerBorder: 'var(--modal-header-border)', headerBorder: 'var(--modal-header-border)',
}), []); }), []);
// Get isAdmin and runningEE from app config // Get isAdmin, runningEE, and enableLogin from app config
const isAdmin = config?.isAdmin ?? false; const isAdmin = config?.isAdmin ?? false;
const runningEE = config?.runningEE ?? false; const runningEE = config?.runningEE ?? false;
const loginEnabled = config?.enableLogin ?? true;
console.log('[AppConfigModal] Config:', { isAdmin, runningEE, fullConfig: config }); console.log('[AppConfigModal] Config:', { isAdmin, runningEE, loginEnabled, fullConfig: config });
// Left navigation structure and icons // Left navigation structure and icons
const configNavSections = useMemo(() => const configNavSections = useMemo(() =>
createConfigNavSections( createConfigNavSections(
isAdmin, isAdmin,
runningEE runningEE,
loginEnabled
), ),
[isAdmin, runningEE] [isAdmin, runningEE, loginEnabled]
); );
const activeLabel = useMemo(() => { const activeLabel = useMemo(() => {
@ -143,16 +145,15 @@ const AppConfigModal: React.FC<AppConfigModalProps> = ({ opened, onClose }) => {
<div <div
key={item.key} key={item.key}
onClick={() => { onClick={() => {
if (!isDisabled) { // Allow navigation even when disabled - the content inside will be disabled
setActive(item.key); setActive(item.key);
navigate(`/settings/${item.key}`); navigate(`/settings/${item.key}`);
}
}} }}
className={`modal-nav-item ${isMobile ? 'mobile' : ''}`} className={`modal-nav-item ${isMobile ? 'mobile' : ''}`}
style={{ style={{
background: isActive ? colors.navItemActiveBg : 'transparent', background: isActive ? colors.navItemActiveBg : 'transparent',
opacity: isDisabled ? 0.5 : 1, opacity: isDisabled ? 0.6 : 1,
cursor: isDisabled ? 'not-allowed' : 'pointer', cursor: 'pointer',
}} }}
> >
<LocalIcon icon={item.icon} width={iconSize} height={iconSize} style={{ color }} /> <LocalIcon icon={item.icon} width={iconSize} height={iconSize} style={{ color }} />

View File

@ -52,7 +52,7 @@ export default function FirstLoginModal({ opened, onPasswordChanged, username }:
setLoading(true); setLoading(true);
setError(''); setError('');
await accountService.changePassword(currentPassword, newPassword); await accountService.changePasswordOnLogin(currentPassword, newPassword);
alert({ alert({
alertType: 'success', alertType: 'success',

View File

@ -0,0 +1,38 @@
import { Alert, Text } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import LocalIcon from '@app/components/shared/LocalIcon';
interface LoginRequiredBannerProps {
show: boolean;
}
/**
* Banner component that displays when login mode is required but not enabled
* Shows prominent warning that settings are read-only
*/
export default function LoginRequiredBanner({ show }: LoginRequiredBannerProps) {
const { t } = useTranslation();
if (!show) return null;
return (
<Alert
icon={<LocalIcon icon="lock-rounded" width={20} height={20} />}
title={t('admin.settings.loginDisabled.title', 'Login Mode Required')}
color="blue"
variant="light"
styles={{
root: {
borderLeft: '4px solid var(--mantine-color-blue-6)'
}
}}
>
<Text size="sm">
{t('admin.settings.loginDisabled.message', 'Login mode must be enabled to modify admin settings. Please set SECURITY_ENABLELOGIN=true in your environment or security.enableLogin: true in settings.yml, then restart the server.')}
</Text>
<Text size="sm" fw={600} mt="xs" c="dimmed">
{t('admin.settings.loginDisabled.readOnly', 'The settings below show example values for reference. Enable login mode to view and edit actual configuration.')}
</Text>
</Alert>
);
}

View File

@ -41,7 +41,8 @@ export interface ConfigColors {
export const createConfigNavSections = ( export const createConfigNavSections = (
isAdmin: boolean = false, isAdmin: boolean = false,
runningEE: boolean = false runningEE: boolean = false,
loginEnabled: boolean = true
): ConfigNavSection[] => { ): ConfigNavSection[] => {
const sections: ConfigNavSection[] = [ const sections: ConfigNavSection[] = [
{ {
@ -63,8 +64,9 @@ export const createConfigNavSections = (
}, },
]; ];
// Add Admin sections if user is admin // Add Admin sections if user is admin OR if login is disabled (but mark as disabled)
if (isAdmin) { if (isAdmin || !loginEnabled) {
const requiresLogin = !loginEnabled;
// Configuration // Configuration
sections.push({ sections.push({
title: 'Configuration', title: 'Configuration',
@ -73,31 +75,41 @@ export const createConfigNavSections = (
key: 'adminGeneral', key: 'adminGeneral',
label: 'System Settings', label: 'System Settings',
icon: 'settings-rounded', icon: 'settings-rounded',
component: <AdminGeneralSection /> component: <AdminGeneralSection />,
disabled: requiresLogin,
disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined
}, },
{ {
key: 'adminFeatures', key: 'adminFeatures',
label: 'Features', label: 'Features',
icon: 'extension-rounded', icon: 'extension-rounded',
component: <AdminFeaturesSection /> component: <AdminFeaturesSection />,
disabled: requiresLogin,
disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined
}, },
{ {
key: 'adminEndpoints', key: 'adminEndpoints',
label: 'Endpoints', label: 'Endpoints',
icon: 'api-rounded', icon: 'api-rounded',
component: <AdminEndpointsSection /> component: <AdminEndpointsSection />,
disabled: requiresLogin,
disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined
}, },
{ {
key: 'adminDatabase', key: 'adminDatabase',
label: 'Database', label: 'Database',
icon: 'storage-rounded', icon: 'storage-rounded',
component: <AdminDatabaseSection /> component: <AdminDatabaseSection />,
disabled: requiresLogin,
disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined
}, },
{ {
key: 'adminAdvanced', key: 'adminAdvanced',
label: 'Advanced', label: 'Advanced',
icon: 'tune-rounded', icon: 'tune-rounded',
component: <AdminAdvancedSection /> component: <AdminAdvancedSection />,
disabled: requiresLogin,
disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined
}, },
], ],
}); });
@ -110,13 +122,17 @@ export const createConfigNavSections = (
key: 'adminSecurity', key: 'adminSecurity',
label: 'Security', label: 'Security',
icon: 'shield-rounded', icon: 'shield-rounded',
component: <AdminSecuritySection /> component: <AdminSecuritySection />,
disabled: requiresLogin,
disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined
}, },
{ {
key: 'adminConnections', key: 'adminConnections',
label: 'Connections', label: 'Connections',
icon: 'link-rounded', icon: 'link-rounded',
component: <AdminConnectionsSection /> component: <AdminConnectionsSection />,
disabled: requiresLogin,
disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined
}, },
], ],
}); });
@ -129,23 +145,25 @@ export const createConfigNavSections = (
key: 'adminPremium', key: 'adminPremium',
label: 'Premium', label: 'Premium',
icon: 'star-rounded', icon: 'star-rounded',
component: <AdminPremiumSection /> component: <AdminPremiumSection />,
disabled: requiresLogin,
disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined
}, },
{ {
key: 'adminAudit', key: 'adminAudit',
label: 'Audit', label: 'Audit',
icon: 'fact-check-rounded', icon: 'fact-check-rounded',
component: <AdminAuditSection />, component: <AdminAuditSection />,
disabled: !runningEE, disabled: !runningEE || requiresLogin,
disabledTooltip: 'Requires Enterprise license' disabledTooltip: requiresLogin ? 'Enable login mode first' : 'Requires Enterprise license'
}, },
{ {
key: 'adminUsage', key: 'adminUsage',
label: 'Usage Analytics', label: 'Usage Analytics',
icon: 'analytics-rounded', icon: 'analytics-rounded',
component: <AdminUsageSection />, component: <AdminUsageSection />,
disabled: !runningEE, disabled: !runningEE || requiresLogin,
disabledTooltip: 'Requires Enterprise license' disabledTooltip: requiresLogin ? 'Enable login mode first' : 'Requires Enterprise license'
}, },
], ],
}); });
@ -158,13 +176,17 @@ export const createConfigNavSections = (
key: 'adminLegal', key: 'adminLegal',
label: 'Legal', label: 'Legal',
icon: 'gavel-rounded', icon: 'gavel-rounded',
component: <AdminLegalSection /> component: <AdminLegalSection />,
disabled: requiresLogin,
disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined
}, },
{ {
key: 'adminPrivacy', key: 'adminPrivacy',
label: 'Privacy', label: 'Privacy',
icon: 'visibility-rounded', icon: 'visibility-rounded',
component: <AdminPrivacySection /> component: <AdminPrivacySection />,
disabled: requiresLogin,
disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined
}, },
], ],
}); });

View File

@ -7,6 +7,8 @@ import { useRestartServer } from '@app/components/shared/config/useRestartServer
import { useAdminSettings } from '@app/hooks/useAdminSettings'; import { useAdminSettings } from '@app/hooks/useAdminSettings';
import PendingBadge from '@app/components/shared/config/PendingBadge'; import PendingBadge from '@app/components/shared/config/PendingBadge';
import apiClient from '@app/services/apiClient'; import apiClient from '@app/services/apiClient';
import { useLoginRequired } from '@app/hooks/useLoginRequired';
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
interface AdvancedSettingsData { interface AdvancedSettingsData {
enableAlphaFunctionality?: boolean; enableAlphaFunctionality?: boolean;
@ -55,6 +57,7 @@ interface AdvancedSettingsData {
export default function AdminAdvancedSection() { export default function AdminAdvancedSection() {
const { t } = useTranslation(); const { t } = useTranslation();
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer(); const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
const { loginEnabled, validateLoginEnabled, getDisabledStyles } = useLoginRequired();
const { const {
settings, settings,
@ -165,10 +168,15 @@ export default function AdminAdvancedSection() {
}); });
useEffect(() => { useEffect(() => {
if (loginEnabled) {
fetchSettings(); fetchSettings();
}, []); }
}, [loginEnabled]);
const handleSave = async () => { const handleSave = async () => {
if (!validateLoginEnabled()) {
return;
}
try { try {
await saveSettings(); await saveSettings();
showRestartModal(); showRestartModal();
@ -181,7 +189,9 @@ export default function AdminAdvancedSection() {
} }
}; };
if (loading) { const actualLoading = loginEnabled ? loading : false;
if (actualLoading) {
return ( return (
<Stack align="center" justify="center" h={200}> <Stack align="center" justify="center" h={200}>
<Loader size="lg" /> <Loader size="lg" />
@ -191,6 +201,7 @@ export default function AdminAdvancedSection() {
return ( return (
<Stack gap="lg"> <Stack gap="lg">
<LoginRequiredBanner show={!loginEnabled} />
<div> <div>
<Text fw={600} size="lg">{t('admin.settings.advanced.title', 'Advanced')}</Text> <Text fw={600} size="lg">{t('admin.settings.advanced.title', 'Advanced')}</Text>
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
@ -213,7 +224,12 @@ export default function AdminAdvancedSection() {
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.enableAlphaFunctionality || false} checked={settings.enableAlphaFunctionality || false}
onChange={(e) => setSettings({ ...settings, enableAlphaFunctionality: e.target.checked })} onChange={(e) => {
if (!loginEnabled) return;
setSettings({ ...settings, enableAlphaFunctionality: e.target.checked });
}}
disabled={!loginEnabled}
styles={getDisabledStyles()}
/> />
<PendingBadge show={isFieldPending('enableAlphaFunctionality')} /> <PendingBadge show={isFieldPending('enableAlphaFunctionality')} />
</Group> </Group>
@ -229,7 +245,12 @@ export default function AdminAdvancedSection() {
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.enableUrlToPDF || false} checked={settings.enableUrlToPDF || false}
onChange={(e) => setSettings({ ...settings, enableUrlToPDF: e.target.checked })} onChange={(e) => {
if (!loginEnabled) return;
setSettings({ ...settings, enableUrlToPDF: e.target.checked });
}}
disabled={!loginEnabled}
styles={getDisabledStyles()}
/> />
<PendingBadge show={isFieldPending('enableUrlToPDF')} /> <PendingBadge show={isFieldPending('enableUrlToPDF')} />
</Group> </Group>
@ -245,7 +266,12 @@ export default function AdminAdvancedSection() {
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.disableSanitize || false} checked={settings.disableSanitize || false}
onChange={(e) => setSettings({ ...settings, disableSanitize: e.target.checked })} onChange={(e) => {
if (!loginEnabled) return;
setSettings({ ...settings, disableSanitize: e.target.checked });
}}
disabled={!loginEnabled}
styles={getDisabledStyles()}
/> />
<PendingBadge show={isFieldPending('disableSanitize')} /> <PendingBadge show={isFieldPending('disableSanitize')} />
</Group> </Group>
@ -271,6 +297,7 @@ export default function AdminAdvancedSection() {
onChange={(value) => setSettings({ ...settings, maxDPI: Number(value) })} onChange={(value) => setSettings({ ...settings, maxDPI: Number(value) })}
min={0} min={0}
max={3000} max={3000}
disabled={!loginEnabled}
/> />
</div> </div>
@ -286,6 +313,7 @@ export default function AdminAdvancedSection() {
value={settings.tessdataDir || ''} value={settings.tessdataDir || ''}
onChange={(e) => setSettings({ ...settings, tessdataDir: e.target.value })} onChange={(e) => setSettings({ ...settings, tessdataDir: e.target.value })}
placeholder="/usr/share/tessdata" placeholder="/usr/share/tessdata"
disabled={!loginEnabled}
/> />
</div> </div>
</Stack> </Stack>
@ -311,6 +339,7 @@ export default function AdminAdvancedSection() {
tempFileManagement: { ...settings.tempFileManagement, baseTmpDir: e.target.value } tempFileManagement: { ...settings.tempFileManagement, baseTmpDir: e.target.value }
})} })}
placeholder="Default: java.io.tmpdir/stirling-pdf" placeholder="Default: java.io.tmpdir/stirling-pdf"
disabled={!loginEnabled}
/> />
</div> </div>
@ -324,6 +353,7 @@ export default function AdminAdvancedSection() {
tempFileManagement: { ...settings.tempFileManagement, libreofficeDir: e.target.value } tempFileManagement: { ...settings.tempFileManagement, libreofficeDir: e.target.value }
})} })}
placeholder="Default: baseTmpDir/libreoffice" placeholder="Default: baseTmpDir/libreoffice"
disabled={!loginEnabled}
/> />
</div> </div>
@ -337,6 +367,7 @@ export default function AdminAdvancedSection() {
tempFileManagement: { ...settings.tempFileManagement, systemTempDir: e.target.value } tempFileManagement: { ...settings.tempFileManagement, systemTempDir: e.target.value }
})} })}
placeholder="System temp directory path" placeholder="System temp directory path"
disabled={!loginEnabled}
/> />
</div> </div>
@ -350,6 +381,7 @@ export default function AdminAdvancedSection() {
tempFileManagement: { ...settings.tempFileManagement, prefix: e.target.value } tempFileManagement: { ...settings.tempFileManagement, prefix: e.target.value }
})} })}
placeholder="stirling-pdf-" placeholder="stirling-pdf-"
disabled={!loginEnabled}
/> />
</div> </div>
@ -364,6 +396,7 @@ export default function AdminAdvancedSection() {
})} })}
min={1} min={1}
max={720} max={720}
disabled={!loginEnabled}
/> />
</div> </div>
@ -378,6 +411,7 @@ export default function AdminAdvancedSection() {
})} })}
min={1} min={1}
max={1440} max={1440}
disabled={!loginEnabled}
/> />
</div> </div>
@ -391,10 +425,15 @@ export default function AdminAdvancedSection() {
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.tempFileManagement?.startupCleanup ?? true} checked={settings.tempFileManagement?.startupCleanup ?? true}
onChange={(e) => setSettings({ onChange={(e) => {
if (!loginEnabled) return;
setSettings({
...settings, ...settings,
tempFileManagement: { ...settings.tempFileManagement, startupCleanup: e.target.checked } tempFileManagement: { ...settings.tempFileManagement, startupCleanup: e.target.checked }
})} });
}}
disabled={!loginEnabled}
styles={getDisabledStyles()}
/> />
<PendingBadge show={isFieldPending('tempFileManagement.startupCleanup')} /> <PendingBadge show={isFieldPending('tempFileManagement.startupCleanup')} />
</Group> </Group>
@ -410,10 +449,15 @@ export default function AdminAdvancedSection() {
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.tempFileManagement?.cleanupSystemTemp ?? false} checked={settings.tempFileManagement?.cleanupSystemTemp ?? false}
onChange={(e) => setSettings({ onChange={(e) => {
if (!loginEnabled) return;
setSettings({
...settings, ...settings,
tempFileManagement: { ...settings.tempFileManagement, cleanupSystemTemp: e.target.checked } tempFileManagement: { ...settings.tempFileManagement, cleanupSystemTemp: e.target.checked }
})} });
}}
disabled={!loginEnabled}
styles={getDisabledStyles()}
/> />
<PendingBadge show={isFieldPending('tempFileManagement.cleanupSystemTemp')} /> <PendingBadge show={isFieldPending('tempFileManagement.cleanupSystemTemp')} />
</Group> </Group>
@ -448,6 +492,7 @@ export default function AdminAdvancedSection() {
})} })}
min={1} min={1}
max={100} max={100}
disabled={!loginEnabled}
/> />
<NumberInput <NumberInput
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')} label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
@ -462,6 +507,7 @@ export default function AdminAdvancedSection() {
})} })}
min={1} min={1}
max={240} max={240}
disabled={!loginEnabled}
/> />
</Stack> </Stack>
</Accordion.Panel> </Accordion.Panel>
@ -485,6 +531,7 @@ export default function AdminAdvancedSection() {
})} })}
min={1} min={1}
max={100} max={100}
disabled={!loginEnabled}
/> />
<NumberInput <NumberInput
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')} label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
@ -499,6 +546,7 @@ export default function AdminAdvancedSection() {
})} })}
min={1} min={1}
max={240} max={240}
disabled={!loginEnabled}
/> />
</Stack> </Stack>
</Accordion.Panel> </Accordion.Panel>
@ -522,6 +570,7 @@ export default function AdminAdvancedSection() {
})} })}
min={1} min={1}
max={100} max={100}
disabled={!loginEnabled}
/> />
<NumberInput <NumberInput
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')} label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
@ -536,6 +585,7 @@ export default function AdminAdvancedSection() {
})} })}
min={1} min={1}
max={240} max={240}
disabled={!loginEnabled}
/> />
</Stack> </Stack>
</Accordion.Panel> </Accordion.Panel>
@ -559,6 +609,7 @@ export default function AdminAdvancedSection() {
})} })}
min={1} min={1}
max={100} max={100}
disabled={!loginEnabled}
/> />
<NumberInput <NumberInput
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')} label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
@ -573,6 +624,7 @@ export default function AdminAdvancedSection() {
})} })}
min={1} min={1}
max={240} max={240}
disabled={!loginEnabled}
/> />
</Stack> </Stack>
</Accordion.Panel> </Accordion.Panel>
@ -596,6 +648,7 @@ export default function AdminAdvancedSection() {
})} })}
min={1} min={1}
max={100} max={100}
disabled={!loginEnabled}
/> />
<NumberInput <NumberInput
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')} label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
@ -610,6 +663,7 @@ export default function AdminAdvancedSection() {
})} })}
min={1} min={1}
max={240} max={240}
disabled={!loginEnabled}
/> />
</Stack> </Stack>
</Accordion.Panel> </Accordion.Panel>
@ -633,6 +687,7 @@ export default function AdminAdvancedSection() {
})} })}
min={1} min={1}
max={100} max={100}
disabled={!loginEnabled}
/> />
<NumberInput <NumberInput
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')} label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
@ -647,6 +702,7 @@ export default function AdminAdvancedSection() {
})} })}
min={1} min={1}
max={240} max={240}
disabled={!loginEnabled}
/> />
</Stack> </Stack>
</Accordion.Panel> </Accordion.Panel>
@ -670,6 +726,7 @@ export default function AdminAdvancedSection() {
})} })}
min={1} min={1}
max={100} max={100}
disabled={!loginEnabled}
/> />
<NumberInput <NumberInput
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')} label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
@ -684,6 +741,7 @@ export default function AdminAdvancedSection() {
})} })}
min={1} min={1}
max={240} max={240}
disabled={!loginEnabled}
/> />
</Stack> </Stack>
</Accordion.Panel> </Accordion.Panel>
@ -707,6 +765,7 @@ export default function AdminAdvancedSection() {
})} })}
min={1} min={1}
max={100} max={100}
disabled={!loginEnabled}
/> />
<NumberInput <NumberInput
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')} label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
@ -721,6 +780,7 @@ export default function AdminAdvancedSection() {
})} })}
min={1} min={1}
max={240} max={240}
disabled={!loginEnabled}
/> />
</Stack> </Stack>
</Accordion.Panel> </Accordion.Panel>
@ -744,6 +804,7 @@ export default function AdminAdvancedSection() {
})} })}
min={1} min={1}
max={100} max={100}
disabled={!loginEnabled}
/> />
<NumberInput <NumberInput
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')} label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
@ -758,6 +819,7 @@ export default function AdminAdvancedSection() {
})} })}
min={1} min={1}
max={240} max={240}
disabled={!loginEnabled}
/> />
</Stack> </Stack>
</Accordion.Panel> </Accordion.Panel>
@ -781,6 +843,7 @@ export default function AdminAdvancedSection() {
})} })}
min={1} min={1}
max={100} max={100}
disabled={!loginEnabled}
/> />
<NumberInput <NumberInput
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')} label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
@ -795,6 +858,7 @@ export default function AdminAdvancedSection() {
})} })}
min={1} min={1}
max={240} max={240}
disabled={!loginEnabled}
/> />
</Stack> </Stack>
</Accordion.Panel> </Accordion.Panel>
@ -805,7 +869,7 @@ export default function AdminAdvancedSection() {
{/* Save Button */} {/* Save Button */}
<Group justify="flex-end"> <Group justify="flex-end">
<Button onClick={handleSave} loading={saving} size="sm"> <Button onClick={handleSave} loading={saving} size="sm" disabled={!loginEnabled}>
{t('admin.settings.save', 'Save Changes')} {t('admin.settings.save', 'Save Changes')}
</Button> </Button>
</Group> </Group>

View File

@ -6,9 +6,12 @@ import AuditSystemStatus from '@app/components/shared/config/configSections/audi
import AuditChartsSection from '@app/components/shared/config/configSections/audit/AuditChartsSection'; import AuditChartsSection from '@app/components/shared/config/configSections/audit/AuditChartsSection';
import AuditEventsTable from '@app/components/shared/config/configSections/audit/AuditEventsTable'; import AuditEventsTable from '@app/components/shared/config/configSections/audit/AuditEventsTable';
import AuditExportSection from '@app/components/shared/config/configSections/audit/AuditExportSection'; import AuditExportSection from '@app/components/shared/config/configSections/audit/AuditExportSection';
import { useLoginRequired } from '@app/hooks/useLoginRequired';
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
const AdminAuditSection: React.FC = () => { const AdminAuditSection: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { loginEnabled, validateLoginEnabled, getDisabledStyles } = useLoginRequired();
const [systemStatus, setSystemStatus] = useState<AuditStatus | null>(null); const [systemStatus, setSystemStatus] = useState<AuditStatus | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -27,10 +30,24 @@ const AdminAuditSection: React.FC = () => {
} }
}; };
if (loginEnabled) {
fetchSystemStatus(); fetchSystemStatus();
}, []); } else {
// Provide example audit system status when login is disabled
setSystemStatus({
enabled: true,
level: 'INFO',
retentionDays: 90,
totalEvents: 1234,
});
setLoading(false);
}
}, [loginEnabled]);
if (loading) { // Override loading state when login is disabled
const actualLoading = loginEnabled ? loading : false;
if (actualLoading) {
return ( return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', padding: '2rem 0' }}> <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', padding: '2rem 0' }}>
<Loader size="lg" /> <Loader size="lg" />
@ -56,32 +73,33 @@ const AdminAuditSection: React.FC = () => {
return ( return (
<Stack gap="lg"> <Stack gap="lg">
<LoginRequiredBanner show={!loginEnabled} />
<AuditSystemStatus status={systemStatus} /> <AuditSystemStatus status={systemStatus} />
{systemStatus.enabled ? ( {systemStatus.enabled ? (
<Tabs defaultValue="dashboard"> <Tabs defaultValue="dashboard">
<Tabs.List> <Tabs.List>
<Tabs.Tab value="dashboard"> <Tabs.Tab value="dashboard" disabled={!loginEnabled}>
{t('audit.tabs.dashboard', 'Dashboard')} {t('audit.tabs.dashboard', 'Dashboard')}
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab value="events"> <Tabs.Tab value="events" disabled={!loginEnabled}>
{t('audit.tabs.events', 'Audit Events')} {t('audit.tabs.events', 'Audit Events')}
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab value="export"> <Tabs.Tab value="export" disabled={!loginEnabled}>
{t('audit.tabs.export', 'Export')} {t('audit.tabs.export', 'Export')}
</Tabs.Tab> </Tabs.Tab>
</Tabs.List> </Tabs.List>
<Tabs.Panel value="dashboard" pt="md"> <Tabs.Panel value="dashboard" pt="md">
<AuditChartsSection /> <AuditChartsSection loginEnabled={loginEnabled} />
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value="events" pt="md"> <Tabs.Panel value="events" pt="md">
<AuditEventsTable /> <AuditEventsTable loginEnabled={loginEnabled} />
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value="export" pt="md"> <Tabs.Panel value="export" pt="md">
<AuditExportSection /> <AuditExportSection loginEnabled={loginEnabled} />
</Tabs.Panel> </Tabs.Panel>
</Tabs> </Tabs>
) : ( ) : (

View File

@ -12,6 +12,8 @@ import {
Provider, Provider,
} from '@app/components/shared/config/configSections/providerDefinitions'; } from '@app/components/shared/config/configSections/providerDefinitions';
import apiClient from '@app/services/apiClient'; import apiClient from '@app/services/apiClient';
import { useLoginRequired } from '@app/hooks/useLoginRequired';
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
interface ConnectionsSettingsData { interface ConnectionsSettingsData {
oauth2?: { oauth2?: {
@ -45,15 +47,10 @@ interface ConnectionsSettingsData {
export default function AdminConnectionsSection() { export default function AdminConnectionsSection() {
const { t } = useTranslation(); const { t } = useTranslation();
const { loginEnabled, validateLoginEnabled, getDisabledStyles } = useLoginRequired();
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer(); const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
const { const adminSettings = useAdminSettings<ConnectionsSettingsData>({
settings,
setSettings,
loading,
fetchSettings,
isFieldPending,
} = useAdminSettings<ConnectionsSettingsData>({
sectionName: 'connections', sectionName: 'connections',
fetchTransformer: async () => { fetchTransformer: async () => {
// Fetch security settings (oauth2, saml2) // Fetch security settings (oauth2, saml2)
@ -106,57 +103,76 @@ export default function AdminConnectionsSection() {
} }
}); });
const {
settings,
setSettings,
loading,
fetchSettings,
isFieldPending,
} = adminSettings;
useEffect(() => { useEffect(() => {
if (loginEnabled) {
fetchSettings(); fetchSettings();
}, []); }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loginEnabled]);
// Override loading state when login is disabled
const actualLoading = loginEnabled ? loading : false;
const isProviderConfigured = (provider: Provider): boolean => { const isProviderConfigured = (provider: Provider): boolean => {
if (provider.id === 'saml2') { if (provider.id === 'saml2') {
return settings.saml2?.enabled === true; return settings?.saml2?.enabled === true;
} }
if (provider.id === 'smtp') { if (provider.id === 'smtp') {
return settings.mail?.enabled === true; return settings?.mail?.enabled === true;
} }
if (provider.id === 'oauth2-generic') { if (provider.id === 'oauth2-generic') {
return settings.oauth2?.enabled === true; return settings?.oauth2?.enabled === true;
} }
// Check if specific OAuth2 provider is configured (has clientId) // Check if specific OAuth2 provider is configured (has clientId)
const providerSettings = settings.oauth2?.client?.[provider.id]; const providerSettings = settings?.oauth2?.client?.[provider.id];
return !!(providerSettings?.clientId); return !!(providerSettings?.clientId);
}; };
const getProviderSettings = (provider: Provider): Record<string, any> => { const getProviderSettings = (provider: Provider): Record<string, any> => {
if (provider.id === 'saml2') { if (provider.id === 'saml2') {
return settings.saml2 || {}; return settings?.saml2 || {};
} }
if (provider.id === 'smtp') { if (provider.id === 'smtp') {
return settings.mail || {}; return settings?.mail || {};
} }
if (provider.id === 'oauth2-generic') { if (provider.id === 'oauth2-generic') {
// Generic OAuth2 settings are at the root oauth2 level // Generic OAuth2 settings are at the root oauth2 level
return { return {
enabled: settings.oauth2?.enabled, enabled: settings?.oauth2?.enabled,
provider: settings.oauth2?.provider, provider: settings?.oauth2?.provider,
issuer: settings.oauth2?.issuer, issuer: settings?.oauth2?.issuer,
clientId: settings.oauth2?.clientId, clientId: settings?.oauth2?.clientId,
clientSecret: settings.oauth2?.clientSecret, clientSecret: settings?.oauth2?.clientSecret,
scopes: settings.oauth2?.scopes, scopes: settings?.oauth2?.scopes,
useAsUsername: settings.oauth2?.useAsUsername, useAsUsername: settings?.oauth2?.useAsUsername,
autoCreateUser: settings.oauth2?.autoCreateUser, autoCreateUser: settings?.oauth2?.autoCreateUser,
blockRegistration: settings.oauth2?.blockRegistration, blockRegistration: settings?.oauth2?.blockRegistration,
}; };
} }
// Specific OAuth2 provider settings // Specific OAuth2 provider settings
return settings.oauth2?.client?.[provider.id] || {}; return settings?.oauth2?.client?.[provider.id] || {};
}; };
const handleProviderSave = async (provider: Provider, providerSettings: Record<string, any>) => { const handleProviderSave = async (provider: Provider, providerSettings: Record<string, any>) => {
// Block save if login is disabled
if (!validateLoginEnabled()) {
return;
}
try { try {
if (provider.id === 'smtp') { if (provider.id === 'smtp') {
// Mail settings use a different endpoint // Mail settings use a different endpoint
@ -218,6 +234,11 @@ export default function AdminConnectionsSection() {
}; };
const handleProviderDisconnect = async (provider: Provider) => { const handleProviderDisconnect = async (provider: Provider) => {
// Block disconnect if login is disabled
if (!validateLoginEnabled()) {
return;
}
try{ try{
if (provider.id === 'smtp') { if (provider.id === 'smtp') {
// Mail settings use a different endpoint // Mail settings use a different endpoint
@ -271,7 +292,7 @@ export default function AdminConnectionsSection() {
} }
}; };
if (loading) { if (actualLoading) {
return ( return (
<Stack align="center" justify="center" h={200}> <Stack align="center" justify="center" h={200}>
<Loader size="lg" /> <Loader size="lg" />
@ -280,9 +301,14 @@ export default function AdminConnectionsSection() {
} }
const handleSSOAutoLoginSave = async () => { const handleSSOAutoLoginSave = async () => {
// Block save if login is disabled
if (!validateLoginEnabled()) {
return;
}
try { try {
const deltaSettings = { const deltaSettings = {
'premium.proFeatures.ssoAutoLogin': settings.ssoAutoLogin 'premium.proFeatures.ssoAutoLogin': settings?.ssoAutoLogin
}; };
const response = await apiClient.put('/api/v1/admin/settings', { settings: deltaSettings }); const response = await apiClient.put('/api/v1/admin/settings', { settings: deltaSettings });
@ -311,6 +337,8 @@ export default function AdminConnectionsSection() {
return ( return (
<Stack gap="xl"> <Stack gap="xl">
<LoginRequiredBanner show={!loginEnabled} />
{/* Header */} {/* Header */}
<div> <div>
<Text fw={600} size="lg"> <Text fw={600} size="lg">
@ -341,11 +369,14 @@ export default function AdminConnectionsSection() {
</div> </div>
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.ssoAutoLogin || false} checked={settings?.ssoAutoLogin || false}
onChange={(e) => { onChange={(e) => {
if (!loginEnabled) return; // Block change when login disabled
setSettings({ ...settings, ssoAutoLogin: e.target.checked }); setSettings({ ...settings, ssoAutoLogin: e.target.checked });
handleSSOAutoLoginSave(); handleSSOAutoLoginSave();
}} }}
disabled={!loginEnabled}
styles={getDisabledStyles()}
/> />
<PendingBadge show={isFieldPending('ssoAutoLogin')} /> <PendingBadge show={isFieldPending('ssoAutoLogin')} />
</Group> </Group>
@ -369,6 +400,7 @@ export default function AdminConnectionsSection() {
settings={getProviderSettings(provider)} settings={getProviderSettings(provider)}
onSave={(providerSettings) => handleProviderSave(provider, providerSettings)} onSave={(providerSettings) => handleProviderSave(provider, providerSettings)}
onDisconnect={() => handleProviderDisconnect(provider)} onDisconnect={() => handleProviderDisconnect(provider)}
disabled={!loginEnabled}
/> />
))} ))}
</Stack> </Stack>
@ -392,6 +424,7 @@ export default function AdminConnectionsSection() {
provider={provider} provider={provider}
isConfigured={false} isConfigured={false}
onSave={(providerSettings) => handleProviderSave(provider, providerSettings)} onSave={(providerSettings) => handleProviderSave(provider, providerSettings)}
disabled={!loginEnabled}
/> />
))} ))}
</Stack> </Stack>

View File

@ -6,6 +6,8 @@ import RestartConfirmationModal from '@app/components/shared/config/RestartConfi
import { useRestartServer } from '@app/components/shared/config/useRestartServer'; import { useRestartServer } from '@app/components/shared/config/useRestartServer';
import { useAdminSettings } from '@app/hooks/useAdminSettings'; import { useAdminSettings } from '@app/hooks/useAdminSettings';
import PendingBadge from '@app/components/shared/config/PendingBadge'; import PendingBadge from '@app/components/shared/config/PendingBadge';
import { useLoginRequired } from '@app/hooks/useLoginRequired';
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
import apiClient from '@app/services/apiClient'; import apiClient from '@app/services/apiClient';
interface DatabaseSettingsData { interface DatabaseSettingsData {
@ -21,6 +23,7 @@ interface DatabaseSettingsData {
export default function AdminDatabaseSection() { export default function AdminDatabaseSection() {
const { t } = useTranslation(); const { t } = useTranslation();
const { loginEnabled, validateLoginEnabled, getDisabledStyles } = useLoginRequired();
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer(); const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
const { const {
@ -78,10 +81,17 @@ export default function AdminDatabaseSection() {
}); });
useEffect(() => { useEffect(() => {
if (loginEnabled) {
fetchSettings(); fetchSettings();
}, []); }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loginEnabled]);
const handleSave = async () => { const handleSave = async () => {
if (!validateLoginEnabled()) {
return;
}
try { try {
await saveSettings(); await saveSettings();
showRestartModal(); showRestartModal();
@ -94,7 +104,10 @@ export default function AdminDatabaseSection() {
} }
}; };
if (loading) { // Override loading state when login is disabled
const actualLoading = loginEnabled ? loading : false;
if (actualLoading) {
return ( return (
<Stack align="center" justify="center" h={200}> <Stack align="center" justify="center" h={200}>
<Loader size="lg" /> <Loader size="lg" />
@ -104,6 +117,8 @@ export default function AdminDatabaseSection() {
return ( return (
<Stack gap="lg"> <Stack gap="lg">
<LoginRequiredBanner show={!loginEnabled} />
<div> <div>
<Group justify="space-between" align="center"> <Group justify="space-between" align="center">
<div> <div>
@ -130,14 +145,19 @@ export default function AdminDatabaseSection() {
</div> </div>
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.enableCustomDatabase || false} checked={settings?.enableCustomDatabase || false}
onChange={(e) => setSettings({ ...settings, enableCustomDatabase: e.target.checked })} onChange={(e) => {
if (!loginEnabled) return;
setSettings({ ...settings, enableCustomDatabase: e.target.checked });
}}
disabled={!loginEnabled}
styles={getDisabledStyles()}
/> />
<PendingBadge show={isFieldPending('enableCustomDatabase')} /> <PendingBadge show={isFieldPending('enableCustomDatabase')} />
</Group> </Group>
</div> </div>
{settings.enableCustomDatabase && ( {settings?.enableCustomDatabase && (
<> <>
<div> <div>
<TextInput <TextInput
@ -148,9 +168,10 @@ export default function AdminDatabaseSection() {
</Group> </Group>
} }
description={t('admin.settings.database.customUrl.description', 'Full JDBC connection string (e.g., jdbc:postgresql://localhost:5432/postgres). If provided, individual connection settings below are not used.')} description={t('admin.settings.database.customUrl.description', 'Full JDBC connection string (e.g., jdbc:postgresql://localhost:5432/postgres). If provided, individual connection settings below are not used.')}
value={settings.customDatabaseUrl || ''} value={settings?.customDatabaseUrl || ''}
onChange={(e) => setSettings({ ...settings, customDatabaseUrl: e.target.value })} onChange={(e) => setSettings({ ...settings, customDatabaseUrl: e.target.value })}
placeholder="jdbc:postgresql://localhost:5432/postgres" placeholder="jdbc:postgresql://localhost:5432/postgres"
disabled={!loginEnabled}
/> />
</div> </div>
@ -163,7 +184,7 @@ export default function AdminDatabaseSection() {
</Group> </Group>
} }
description={t('admin.settings.database.type.description', 'Type of database (not used if custom URL is provided)')} description={t('admin.settings.database.type.description', 'Type of database (not used if custom URL is provided)')}
value={settings.type || 'postgresql'} value={settings?.type || 'postgresql'}
onChange={(value) => setSettings({ ...settings, type: value || 'postgresql' })} onChange={(value) => setSettings({ ...settings, type: value || 'postgresql' })}
data={[ data={[
{ value: 'postgresql', label: 'PostgreSQL' }, { value: 'postgresql', label: 'PostgreSQL' },
@ -171,6 +192,7 @@ export default function AdminDatabaseSection() {
{ value: 'mysql', label: 'MySQL' }, { value: 'mysql', label: 'MySQL' },
{ value: 'mariadb', label: 'MariaDB' } { value: 'mariadb', label: 'MariaDB' }
]} ]}
disabled={!loginEnabled}
/> />
</div> </div>
@ -183,9 +205,10 @@ export default function AdminDatabaseSection() {
</Group> </Group>
} }
description={t('admin.settings.database.hostName.description', 'Database server hostname (not used if custom URL is provided)')} description={t('admin.settings.database.hostName.description', 'Database server hostname (not used if custom URL is provided)')}
value={settings.hostName || ''} value={settings?.hostName || ''}
onChange={(e) => setSettings({ ...settings, hostName: e.target.value })} onChange={(e) => setSettings({ ...settings, hostName: e.target.value })}
placeholder="localhost" placeholder="localhost"
disabled={!loginEnabled}
/> />
</div> </div>
@ -198,10 +221,11 @@ export default function AdminDatabaseSection() {
</Group> </Group>
} }
description={t('admin.settings.database.port.description', 'Database server port (not used if custom URL is provided)')} description={t('admin.settings.database.port.description', 'Database server port (not used if custom URL is provided)')}
value={settings.port || 5432} value={settings?.port || 5432}
onChange={(value) => setSettings({ ...settings, port: Number(value) })} onChange={(value) => setSettings({ ...settings, port: Number(value) })}
min={1} min={1}
max={65535} max={65535}
disabled={!loginEnabled}
/> />
</div> </div>
@ -214,9 +238,10 @@ export default function AdminDatabaseSection() {
</Group> </Group>
} }
description={t('admin.settings.database.name.description', 'Name of the database (not used if custom URL is provided)')} description={t('admin.settings.database.name.description', 'Name of the database (not used if custom URL is provided)')}
value={settings.name || ''} value={settings?.name || ''}
onChange={(e) => setSettings({ ...settings, name: e.target.value })} onChange={(e) => setSettings({ ...settings, name: e.target.value })}
placeholder="postgres" placeholder="postgres"
disabled={!loginEnabled}
/> />
</div> </div>
@ -229,9 +254,10 @@ export default function AdminDatabaseSection() {
</Group> </Group>
} }
description={t('admin.settings.database.username.description', 'Database authentication username')} description={t('admin.settings.database.username.description', 'Database authentication username')}
value={settings.username || ''} value={settings?.username || ''}
onChange={(e) => setSettings({ ...settings, username: e.target.value })} onChange={(e) => setSettings({ ...settings, username: e.target.value })}
placeholder="postgres" placeholder="postgres"
disabled={!loginEnabled}
/> />
</div> </div>
@ -244,9 +270,10 @@ export default function AdminDatabaseSection() {
</Group> </Group>
} }
description={t('admin.settings.database.password.description', 'Database authentication password')} description={t('admin.settings.database.password.description', 'Database authentication password')}
value={settings.password || ''} value={settings?.password || ''}
onChange={(e) => setSettings({ ...settings, password: e.target.value })} onChange={(e) => setSettings({ ...settings, password: e.target.value })}
placeholder="••••••••" placeholder="••••••••"
disabled={!loginEnabled}
/> />
</div> </div>
</> </>
@ -256,7 +283,7 @@ export default function AdminDatabaseSection() {
{/* Save Button */} {/* Save Button */}
<Group justify="flex-end"> <Group justify="flex-end">
<Button onClick={handleSave} loading={saving} size="sm"> <Button onClick={handleSave} loading={saving} size="sm" disabled={!loginEnabled}>
{t('admin.settings.save', 'Save Changes')} {t('admin.settings.save', 'Save Changes')}
</Button> </Button>
</Group> </Group>

View File

@ -6,6 +6,8 @@ import RestartConfirmationModal from '@app/components/shared/config/RestartConfi
import { useRestartServer } from '@app/components/shared/config/useRestartServer'; import { useRestartServer } from '@app/components/shared/config/useRestartServer';
import { useAdminSettings } from '@app/hooks/useAdminSettings'; import { useAdminSettings } from '@app/hooks/useAdminSettings';
import PendingBadge from '@app/components/shared/config/PendingBadge'; import PendingBadge from '@app/components/shared/config/PendingBadge';
import { useLoginRequired } from '@app/hooks/useLoginRequired';
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
interface EndpointsSettingsData { interface EndpointsSettingsData {
toRemove?: string[]; toRemove?: string[];
@ -14,6 +16,7 @@ interface EndpointsSettingsData {
export default function AdminEndpointsSection() { export default function AdminEndpointsSection() {
const { t } = useTranslation(); const { t } = useTranslation();
const { loginEnabled, validateLoginEnabled, getDisabledStyles } = useLoginRequired();
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer(); const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
const { const {
@ -29,10 +32,17 @@ export default function AdminEndpointsSection() {
}); });
useEffect(() => { useEffect(() => {
if (loginEnabled) {
fetchSettings(); fetchSettings();
}, []); }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loginEnabled]);
const handleSave = async () => { const handleSave = async () => {
if (!validateLoginEnabled()) {
return;
}
try { try {
await saveSettings(); await saveSettings();
showRestartModal(); showRestartModal();
@ -45,7 +55,10 @@ export default function AdminEndpointsSection() {
} }
}; };
if (loading) { // Override loading state when login is disabled
const actualLoading = loginEnabled ? loading : false;
if (actualLoading) {
return ( return (
<Stack align="center" justify="center" h={200}> <Stack align="center" justify="center" h={200}>
<Loader size="lg" /> <Loader size="lg" />
@ -102,6 +115,8 @@ export default function AdminEndpointsSection() {
return ( return (
<Stack gap="lg"> <Stack gap="lg">
<LoginRequiredBanner show={!loginEnabled} />
<div> <div>
<Text fw={600} size="lg">{t('admin.settings.endpoints.title', 'API Endpoints')}</Text> <Text fw={600} size="lg">{t('admin.settings.endpoints.title', 'API Endpoints')}</Text>
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
@ -123,12 +138,16 @@ export default function AdminEndpointsSection() {
} }
description={t('admin.settings.endpoints.toRemove.description', 'Select individual endpoints to disable')} description={t('admin.settings.endpoints.toRemove.description', 'Select individual endpoints to disable')}
value={settings.toRemove || []} value={settings.toRemove || []}
onChange={(value) => setSettings({ ...settings, toRemove: value })} onChange={(value) => {
if (!loginEnabled) return;
setSettings({ ...settings, toRemove: value });
}}
data={commonEndpoints.map(endpoint => ({ value: endpoint, label: endpoint }))} data={commonEndpoints.map(endpoint => ({ value: endpoint, label: endpoint }))}
searchable searchable
clearable clearable
placeholder="Select endpoints to disable" placeholder="Select endpoints to disable"
comboboxProps={{ zIndex: 1400 }} comboboxProps={{ zIndex: 1400 }}
disabled={!loginEnabled}
/> />
</div> </div>
@ -142,12 +161,16 @@ export default function AdminEndpointsSection() {
} }
description={t('admin.settings.endpoints.groupsToRemove.description', 'Select endpoint groups to disable')} description={t('admin.settings.endpoints.groupsToRemove.description', 'Select endpoint groups to disable')}
value={settings.groupsToRemove || []} value={settings.groupsToRemove || []}
onChange={(value) => setSettings({ ...settings, groupsToRemove: value })} onChange={(value) => {
if (!loginEnabled) return;
setSettings({ ...settings, groupsToRemove: value });
}}
data={commonGroups.map(group => ({ value: group, label: group }))} data={commonGroups.map(group => ({ value: group, label: group }))}
searchable searchable
clearable clearable
placeholder="Select groups to disable" placeholder="Select groups to disable"
comboboxProps={{ zIndex: 1400 }} comboboxProps={{ zIndex: 1400 }}
disabled={!loginEnabled}
/> />
</div> </div>
@ -160,7 +183,7 @@ export default function AdminEndpointsSection() {
</Paper> </Paper>
<Group justify="flex-end"> <Group justify="flex-end">
<Button onClick={handleSave} loading={saving} size="sm"> <Button onClick={handleSave} loading={saving} size="sm" disabled={!loginEnabled}>
{t('admin.settings.save', 'Save Changes')} {t('admin.settings.save', 'Save Changes')}
</Button> </Button>
</Group> </Group>

View File

@ -7,6 +7,8 @@ import { useRestartServer } from '@app/components/shared/config/useRestartServer
import { useAdminSettings } from '@app/hooks/useAdminSettings'; import { useAdminSettings } from '@app/hooks/useAdminSettings';
import PendingBadge from '@app/components/shared/config/PendingBadge'; import PendingBadge from '@app/components/shared/config/PendingBadge';
import apiClient from '@app/services/apiClient'; import apiClient from '@app/services/apiClient';
import { useLoginRequired } from '@app/hooks/useLoginRequired';
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
interface FeaturesSettingsData { interface FeaturesSettingsData {
serverCertificate?: { serverCertificate?: {
@ -19,6 +21,7 @@ interface FeaturesSettingsData {
export default function AdminFeaturesSection() { export default function AdminFeaturesSection() {
const { t } = useTranslation(); const { t } = useTranslation();
const { loginEnabled, validateLoginEnabled, getDisabledStyles } = useLoginRequired();
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer(); const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
const { const {
@ -69,10 +72,15 @@ export default function AdminFeaturesSection() {
}); });
useEffect(() => { useEffect(() => {
if (loginEnabled) {
fetchSettings(); fetchSettings();
}, []); }
}, [loginEnabled]);
const handleSave = async () => { const handleSave = async () => {
if (!validateLoginEnabled()) {
return;
}
try { try {
await saveSettings(); await saveSettings();
showRestartModal(); showRestartModal();
@ -85,7 +93,9 @@ export default function AdminFeaturesSection() {
} }
}; };
if (loading) { const actualLoading = loginEnabled ? loading : false;
if (actualLoading) {
return ( return (
<Stack align="center" justify="center" h={200}> <Stack align="center" justify="center" h={200}>
<Loader size="lg" /> <Loader size="lg" />
@ -95,6 +105,7 @@ export default function AdminFeaturesSection() {
return ( return (
<Stack gap="lg"> <Stack gap="lg">
<LoginRequiredBanner show={!loginEnabled} />
<div> <div>
<Text fw={600} size="lg">{t('admin.settings.features.title', 'Features')}</Text> <Text fw={600} size="lg">{t('admin.settings.features.title', 'Features')}</Text>
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
@ -124,10 +135,15 @@ export default function AdminFeaturesSection() {
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.serverCertificate?.enabled ?? true} checked={settings.serverCertificate?.enabled ?? true}
onChange={(e) => setSettings({ onChange={(e) => {
if (!loginEnabled) return;
setSettings({
...settings, ...settings,
serverCertificate: { ...settings.serverCertificate, enabled: e.target.checked } serverCertificate: { ...settings.serverCertificate, enabled: e.target.checked }
})} });
}}
disabled={!loginEnabled}
styles={getDisabledStyles()}
/> />
<PendingBadge show={isFieldPending('serverCertificate.enabled')} /> <PendingBadge show={isFieldPending('serverCertificate.enabled')} />
</Group> </Group>
@ -148,6 +164,7 @@ export default function AdminFeaturesSection() {
serverCertificate: { ...settings.serverCertificate, organizationName: e.target.value } serverCertificate: { ...settings.serverCertificate, organizationName: e.target.value }
})} })}
placeholder="Stirling-PDF" placeholder="Stirling-PDF"
disabled={!loginEnabled}
/> />
</div> </div>
@ -167,6 +184,7 @@ export default function AdminFeaturesSection() {
})} })}
min={1} min={1}
max={3650} max={3650}
disabled={!loginEnabled}
/> />
</div> </div>
@ -180,10 +198,15 @@ export default function AdminFeaturesSection() {
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.serverCertificate?.regenerateOnStartup ?? false} checked={settings.serverCertificate?.regenerateOnStartup ?? false}
onChange={(e) => setSettings({ onChange={(e) => {
if (!loginEnabled) return;
setSettings({
...settings, ...settings,
serverCertificate: { ...settings.serverCertificate, regenerateOnStartup: e.target.checked } serverCertificate: { ...settings.serverCertificate, regenerateOnStartup: e.target.checked }
})} });
}}
disabled={!loginEnabled}
styles={getDisabledStyles()}
/> />
<PendingBadge show={isFieldPending('serverCertificate.regenerateOnStartup')} /> <PendingBadge show={isFieldPending('serverCertificate.regenerateOnStartup')} />
</Group> </Group>
@ -193,7 +216,7 @@ export default function AdminFeaturesSection() {
{/* Save Button */} {/* Save Button */}
<Group justify="flex-end"> <Group justify="flex-end">
<Button onClick={handleSave} loading={saving} size="sm"> <Button onClick={handleSave} loading={saving} size="sm" disabled={!loginEnabled}>
{t('admin.settings.save', 'Save Changes')} {t('admin.settings.save', 'Save Changes')}
</Button> </Button>
</Group> </Group>

View File

@ -7,6 +7,8 @@ import { useRestartServer } from '@app/components/shared/config/useRestartServer
import { useAdminSettings } from '@app/hooks/useAdminSettings'; import { useAdminSettings } from '@app/hooks/useAdminSettings';
import PendingBadge from '@app/components/shared/config/PendingBadge'; import PendingBadge from '@app/components/shared/config/PendingBadge';
import apiClient from '@app/services/apiClient'; import apiClient from '@app/services/apiClient';
import { useLoginRequired } from '@app/hooks/useLoginRequired';
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
interface GeneralSettingsData { interface GeneralSettingsData {
ui: { ui: {
@ -40,6 +42,7 @@ interface GeneralSettingsData {
export default function AdminGeneralSection() { export default function AdminGeneralSection() {
const { t } = useTranslation(); const { t } = useTranslation();
const { loginEnabled, validateLoginEnabled } = useLoginRequired();
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer(); const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
const { const {
@ -108,14 +111,14 @@ export default function AdminGeneralSection() {
saveTransformer: (settings) => { saveTransformer: (settings) => {
const deltaSettings: Record<string, any> = { const deltaSettings: Record<string, any> = {
// UI settings // UI settings
'ui.appNameNavbar': settings.ui.appNameNavbar, 'ui.appNameNavbar': settings.ui?.appNameNavbar,
'ui.languages': settings.ui.languages, 'ui.languages': settings.ui?.languages,
// System settings // System settings
'system.defaultLocale': settings.system.defaultLocale, 'system.defaultLocale': settings.system?.defaultLocale,
'system.showUpdate': settings.system.showUpdate, 'system.showUpdate': settings.system?.showUpdate,
'system.showUpdateOnlyAdmin': settings.system.showUpdateOnlyAdmin, 'system.showUpdateOnlyAdmin': settings.system?.showUpdateOnlyAdmin,
'system.customHTMLFiles': settings.system.customHTMLFiles, 'system.customHTMLFiles': settings.system?.customHTMLFiles,
'system.fileUploadLimit': settings.system.fileUploadLimit, 'system.fileUploadLimit': settings.system?.fileUploadLimit,
// Premium custom metadata // Premium custom metadata
'premium.proFeatures.customMetadata.autoUpdateMetadata': settings.customMetadata?.autoUpdateMetadata, 'premium.proFeatures.customMetadata.autoUpdateMetadata': settings.customMetadata?.autoUpdateMetadata,
'premium.proFeatures.customMetadata.author': settings.customMetadata?.author, 'premium.proFeatures.customMetadata.author': settings.customMetadata?.author,
@ -124,10 +127,10 @@ export default function AdminGeneralSection() {
}; };
if (settings.customPaths) { if (settings.customPaths) {
deltaSettings['system.customPaths.pipeline.watchedFoldersDir'] = settings.customPaths.pipeline?.watchedFoldersDir; deltaSettings['system.customPaths.pipeline.watchedFoldersDir'] = settings.customPaths?.pipeline?.watchedFoldersDir;
deltaSettings['system.customPaths.pipeline.finishedFoldersDir'] = settings.customPaths.pipeline?.finishedFoldersDir; deltaSettings['system.customPaths.pipeline.finishedFoldersDir'] = settings.customPaths?.pipeline?.finishedFoldersDir;
deltaSettings['system.customPaths.operations.weasyprint'] = settings.customPaths.operations?.weasyprint; deltaSettings['system.customPaths.operations.weasyprint'] = settings.customPaths?.operations?.weasyprint;
deltaSettings['system.customPaths.operations.unoconvert'] = settings.customPaths.operations?.unoconvert; deltaSettings['system.customPaths.operations.unoconvert'] = settings.customPaths?.operations?.unoconvert;
} }
return { return {
@ -138,10 +141,22 @@ export default function AdminGeneralSection() {
}); });
useEffect(() => { useEffect(() => {
// Only fetch real settings if login is enabled
if (loginEnabled) {
fetchSettings(); fetchSettings();
}, []); }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loginEnabled]);
// Override loading state when login is disabled
const actualLoading = loginEnabled ? loading : false;
const handleSave = async () => { const handleSave = async () => {
// Block save if login is disabled
if (!validateLoginEnabled()) {
return;
}
try { try {
await saveSettings(); await saveSettings();
showRestartModal(); showRestartModal();
@ -154,7 +169,7 @@ export default function AdminGeneralSection() {
} }
}; };
if (loading) { if (actualLoading) {
return ( return (
<Stack align="center" justify="center" h={200}> <Stack align="center" justify="center" h={200}>
<Loader size="lg" /> <Loader size="lg" />
@ -164,6 +179,8 @@ export default function AdminGeneralSection() {
return ( return (
<Stack gap="lg"> <Stack gap="lg">
<LoginRequiredBanner show={!loginEnabled} />
<div> <div>
<Text fw={600} size="lg">{t('admin.settings.general.title', 'System Settings')}</Text> <Text fw={600} size="lg">{t('admin.settings.general.title', 'System Settings')}</Text>
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
@ -185,9 +202,10 @@ export default function AdminGeneralSection() {
</Group> </Group>
} }
description={t('admin.settings.general.appNameNavbar.description', 'The name displayed in the navigation bar')} description={t('admin.settings.general.appNameNavbar.description', 'The name displayed in the navigation bar')}
value={settings.ui.appNameNavbar || ''} value={settings.ui?.appNameNavbar || ''}
onChange={(e) => setSettings({ ...settings, ui: { ...settings.ui, appNameNavbar: e.target.value } })} onChange={(e) => setSettings({ ...settings, ui: { ...settings.ui, appNameNavbar: e.target.value } })}
placeholder="Stirling PDF" placeholder="Stirling PDF"
disabled={!loginEnabled}
/> />
</div> </div>
@ -200,7 +218,7 @@ export default function AdminGeneralSection() {
</Group> </Group>
} }
description={t('admin.settings.general.languages.description', 'Limit which languages are available (empty = all languages)')} description={t('admin.settings.general.languages.description', 'Limit which languages are available (empty = all languages)')}
value={settings.ui.languages || []} value={settings.ui?.languages || []}
onChange={(value) => setSettings({ ...settings, ui: { ...settings.ui, languages: value } })} onChange={(value) => setSettings({ ...settings, ui: { ...settings.ui, languages: value } })}
data={[ data={[
{ value: 'de_DE', label: 'Deutsch' }, { value: 'de_DE', label: 'Deutsch' },
@ -218,6 +236,7 @@ export default function AdminGeneralSection() {
clearable clearable
placeholder="Select languages" placeholder="Select languages"
comboboxProps={{ zIndex: 1400 }} comboboxProps={{ zIndex: 1400 }}
disabled={!loginEnabled}
/> />
</div> </div>
@ -230,9 +249,10 @@ export default function AdminGeneralSection() {
</Group> </Group>
} }
description={t('admin.settings.general.defaultLocale.description', 'The default language for new users (e.g., en_US, es_ES)')} description={t('admin.settings.general.defaultLocale.description', 'The default language for new users (e.g., en_US, es_ES)')}
value={settings.system.defaultLocale || ''} value={ settings.system?.defaultLocale || ''}
onChange={(e) => setSettings({ ...settings, system: { ...settings.system, defaultLocale: e.target.value } })} onChange={(e) => setSettings({ ...settings, system: { ...settings.system, defaultLocale: e.target.value } })}
placeholder="en_US" placeholder="en_US"
disabled={!loginEnabled}
/> />
</div> </div>
@ -245,9 +265,10 @@ export default function AdminGeneralSection() {
</Group> </Group>
} }
description={t('admin.settings.general.fileUploadLimit.description', 'Maximum file upload size (e.g., 100MB, 1GB)')} description={t('admin.settings.general.fileUploadLimit.description', 'Maximum file upload size (e.g., 100MB, 1GB)')}
value={settings.system.fileUploadLimit || ''} value={ settings.system?.fileUploadLimit || ''}
onChange={(e) => setSettings({ ...settings, system: { ...settings.system, fileUploadLimit: e.target.value } })} onChange={(e) => setSettings({ ...settings, system: { ...settings.system, fileUploadLimit: e.target.value } })}
placeholder="100MB" placeholder="100MB"
disabled={!loginEnabled}
/> />
</div> </div>
@ -260,8 +281,9 @@ export default function AdminGeneralSection() {
</div> </div>
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.system.showUpdate || false} checked={ settings.system?.showUpdate || false}
onChange={(e) => setSettings({ ...settings, system: { ...settings.system, showUpdate: e.target.checked } })} onChange={(e) => setSettings({ ...settings, system: { ...settings.system, showUpdate: e.target.checked } })}
disabled={!loginEnabled}
/> />
<PendingBadge show={isFieldPending('system.showUpdate')} /> <PendingBadge show={isFieldPending('system.showUpdate')} />
</Group> </Group>
@ -276,8 +298,9 @@ export default function AdminGeneralSection() {
</div> </div>
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.system.showUpdateOnlyAdmin || false} checked={ settings.system?.showUpdateOnlyAdmin || false}
onChange={(e) => setSettings({ ...settings, system: { ...settings.system, showUpdateOnlyAdmin: e.target.checked } })} onChange={(e) => setSettings({ ...settings, system: { ...settings.system, showUpdateOnlyAdmin: e.target.checked } })}
disabled={!loginEnabled}
/> />
<PendingBadge show={isFieldPending('system.showUpdateOnlyAdmin')} /> <PendingBadge show={isFieldPending('system.showUpdateOnlyAdmin')} />
</Group> </Group>
@ -292,8 +315,9 @@ export default function AdminGeneralSection() {
</div> </div>
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.system.customHTMLFiles || false} checked={settings.system?.customHTMLFiles || false}
onChange={(e) => setSettings({ ...settings, system: { ...settings.system, customHTMLFiles: e.target.checked } })} onChange={(e) => setSettings({ ...settings, system: { ...settings.system, customHTMLFiles: e.target.checked } })}
disabled={!loginEnabled}
/> />
<PendingBadge show={isFieldPending('system.customHTMLFiles')} /> <PendingBadge show={isFieldPending('system.customHTMLFiles')} />
</Group> </Group>
@ -326,6 +350,7 @@ export default function AdminGeneralSection() {
autoUpdateMetadata: e.target.checked autoUpdateMetadata: e.target.checked
} }
})} })}
disabled={!loginEnabled}
/> />
<PendingBadge show={isFieldPending('customMetadata.autoUpdateMetadata')} /> <PendingBadge show={isFieldPending('customMetadata.autoUpdateMetadata')} />
</Group> </Group>
@ -349,6 +374,7 @@ export default function AdminGeneralSection() {
} }
})} })}
placeholder="username" placeholder="username"
disabled={!loginEnabled}
/> />
</div> </div>
@ -370,6 +396,7 @@ export default function AdminGeneralSection() {
} }
})} })}
placeholder="Stirling-PDF" placeholder="Stirling-PDF"
disabled={!loginEnabled}
/> />
</div> </div>
@ -391,6 +418,7 @@ export default function AdminGeneralSection() {
} }
})} })}
placeholder="Stirling-PDF" placeholder="Stirling-PDF"
disabled={!loginEnabled}
/> />
</div> </div>
</Stack> </Stack>
@ -429,6 +457,7 @@ export default function AdminGeneralSection() {
} }
})} })}
placeholder="/pipeline/watchedFolders" placeholder="/pipeline/watchedFolders"
disabled={!loginEnabled}
/> />
</div> </div>
@ -453,6 +482,7 @@ export default function AdminGeneralSection() {
} }
})} })}
placeholder="/pipeline/finishedFolders" placeholder="/pipeline/finishedFolders"
disabled={!loginEnabled}
/> />
</div> </div>
@ -479,6 +509,7 @@ export default function AdminGeneralSection() {
} }
})} })}
placeholder="/opt/venv/bin/weasyprint" placeholder="/opt/venv/bin/weasyprint"
disabled={!loginEnabled}
/> />
</div> </div>
@ -503,6 +534,7 @@ export default function AdminGeneralSection() {
} }
})} })}
placeholder="/opt/venv/bin/unoconvert" placeholder="/opt/venv/bin/unoconvert"
disabled={!loginEnabled}
/> />
</div> </div>
</Stack> </Stack>
@ -510,7 +542,7 @@ export default function AdminGeneralSection() {
{/* Save Button */} {/* Save Button */}
<Group justify="flex-end"> <Group justify="flex-end">
<Button onClick={handleSave} loading={saving} size="sm"> <Button onClick={handleSave} loading={saving} size="sm" disabled={!loginEnabled}>
{t('admin.settings.save', 'Save Changes')} {t('admin.settings.save', 'Save Changes')}
</Button> </Button>
</Group> </Group>

View File

@ -7,6 +7,8 @@ import RestartConfirmationModal from '@app/components/shared/config/RestartConfi
import { useRestartServer } from '@app/components/shared/config/useRestartServer'; import { useRestartServer } from '@app/components/shared/config/useRestartServer';
import { useAdminSettings } from '@app/hooks/useAdminSettings'; import { useAdminSettings } from '@app/hooks/useAdminSettings';
import PendingBadge from '@app/components/shared/config/PendingBadge'; import PendingBadge from '@app/components/shared/config/PendingBadge';
import { useLoginRequired } from '@app/hooks/useLoginRequired';
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
interface LegalSettingsData { interface LegalSettingsData {
termsAndConditions?: string; termsAndConditions?: string;
@ -18,6 +20,7 @@ interface LegalSettingsData {
export default function AdminLegalSection() { export default function AdminLegalSection() {
const { t } = useTranslation(); const { t } = useTranslation();
const { loginEnabled, validateLoginEnabled, getDisabledStyles } = useLoginRequired();
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer(); const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
const { const {
@ -33,10 +36,15 @@ export default function AdminLegalSection() {
}); });
useEffect(() => { useEffect(() => {
if (loginEnabled) {
fetchSettings(); fetchSettings();
}, []); }
}, [loginEnabled]);
const handleSave = async () => { const handleSave = async () => {
if (!validateLoginEnabled()) {
return;
}
try { try {
await saveSettings(); await saveSettings();
showRestartModal(); showRestartModal();
@ -49,7 +57,9 @@ export default function AdminLegalSection() {
} }
}; };
if (loading) { const actualLoading = loginEnabled ? loading : false;
if (actualLoading) {
return ( return (
<Stack align="center" justify="center" h={200}> <Stack align="center" justify="center" h={200}>
<Loader size="lg" /> <Loader size="lg" />
@ -59,6 +69,7 @@ export default function AdminLegalSection() {
return ( return (
<Stack gap="lg"> <Stack gap="lg">
<LoginRequiredBanner show={!loginEnabled} />
<div> <div>
<Text fw={600} size="lg">{t('admin.settings.legal.title', 'Legal Documents')}</Text> <Text fw={600} size="lg">{t('admin.settings.legal.title', 'Legal Documents')}</Text>
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
@ -95,6 +106,7 @@ export default function AdminLegalSection() {
value={settings.termsAndConditions || ''} value={settings.termsAndConditions || ''}
onChange={(e) => setSettings({ ...settings, termsAndConditions: e.target.value })} onChange={(e) => setSettings({ ...settings, termsAndConditions: e.target.value })}
placeholder="https://example.com/terms" placeholder="https://example.com/terms"
disabled={!loginEnabled}
/> />
</div> </div>
@ -110,6 +122,7 @@ export default function AdminLegalSection() {
value={settings.privacyPolicy || ''} value={settings.privacyPolicy || ''}
onChange={(e) => setSettings({ ...settings, privacyPolicy: e.target.value })} onChange={(e) => setSettings({ ...settings, privacyPolicy: e.target.value })}
placeholder="https://example.com/privacy" placeholder="https://example.com/privacy"
disabled={!loginEnabled}
/> />
</div> </div>
@ -125,6 +138,7 @@ export default function AdminLegalSection() {
value={settings.accessibilityStatement || ''} value={settings.accessibilityStatement || ''}
onChange={(e) => setSettings({ ...settings, accessibilityStatement: e.target.value })} onChange={(e) => setSettings({ ...settings, accessibilityStatement: e.target.value })}
placeholder="https://example.com/accessibility" placeholder="https://example.com/accessibility"
disabled={!loginEnabled}
/> />
</div> </div>
@ -140,6 +154,7 @@ export default function AdminLegalSection() {
value={settings.cookiePolicy || ''} value={settings.cookiePolicy || ''}
onChange={(e) => setSettings({ ...settings, cookiePolicy: e.target.value })} onChange={(e) => setSettings({ ...settings, cookiePolicy: e.target.value })}
placeholder="https://example.com/cookies" placeholder="https://example.com/cookies"
disabled={!loginEnabled}
/> />
</div> </div>
@ -155,13 +170,14 @@ export default function AdminLegalSection() {
value={settings.impressum || ''} value={settings.impressum || ''}
onChange={(e) => setSettings({ ...settings, impressum: e.target.value })} onChange={(e) => setSettings({ ...settings, impressum: e.target.value })}
placeholder="https://example.com/impressum" placeholder="https://example.com/impressum"
disabled={!loginEnabled}
/> />
</div> </div>
</Stack> </Stack>
</Paper> </Paper>
<Group justify="flex-end"> <Group justify="flex-end">
<Button onClick={handleSave} loading={saving} size="sm"> <Button onClick={handleSave} loading={saving} size="sm" disabled={!loginEnabled}>
{t('admin.settings.save', 'Save Changes')} {t('admin.settings.save', 'Save Changes')}
</Button> </Button>
</Group> </Group>

View File

@ -7,6 +7,8 @@ import RestartConfirmationModal from '@app/components/shared/config/RestartConfi
import { useRestartServer } from '@app/components/shared/config/useRestartServer'; import { useRestartServer } from '@app/components/shared/config/useRestartServer';
import { useAdminSettings } from '@app/hooks/useAdminSettings'; import { useAdminSettings } from '@app/hooks/useAdminSettings';
import PendingBadge from '@app/components/shared/config/PendingBadge'; import PendingBadge from '@app/components/shared/config/PendingBadge';
import { useLoginRequired } from '@app/hooks/useLoginRequired';
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
interface PremiumSettingsData { interface PremiumSettingsData {
key?: string; key?: string;
@ -15,6 +17,7 @@ interface PremiumSettingsData {
export default function AdminPremiumSection() { export default function AdminPremiumSection() {
const { t } = useTranslation(); const { t } = useTranslation();
const { loginEnabled, validateLoginEnabled, getDisabledStyles } = useLoginRequired();
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer(); const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
const { const {
@ -30,10 +33,15 @@ export default function AdminPremiumSection() {
}); });
useEffect(() => { useEffect(() => {
if (loginEnabled) {
fetchSettings(); fetchSettings();
}, []); }
}, [loginEnabled]);
const handleSave = async () => { const handleSave = async () => {
if (!validateLoginEnabled()) {
return;
}
try { try {
await saveSettings(); await saveSettings();
showRestartModal(); showRestartModal();
@ -46,7 +54,9 @@ export default function AdminPremiumSection() {
} }
}; };
if (loading) { const actualLoading = loginEnabled ? loading : false;
if (actualLoading) {
return ( return (
<Stack align="center" justify="center" h={200}> <Stack align="center" justify="center" h={200}>
<Loader size="lg" /> <Loader size="lg" />
@ -56,6 +66,7 @@ export default function AdminPremiumSection() {
return ( return (
<Stack gap="lg"> <Stack gap="lg">
<LoginRequiredBanner show={!loginEnabled} />
<div> <div>
<Text fw={600} size="lg">{t('admin.settings.premium.title', 'Premium & Enterprise')}</Text> <Text fw={600} size="lg">{t('admin.settings.premium.title', 'Premium & Enterprise')}</Text>
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
@ -98,6 +109,7 @@ export default function AdminPremiumSection() {
value={settings.key || ''} value={settings.key || ''}
onChange={(e) => setSettings({ ...settings, key: e.target.value })} onChange={(e) => setSettings({ ...settings, key: e.target.value })}
placeholder="00000000-0000-0000-0000-000000000000" placeholder="00000000-0000-0000-0000-000000000000"
disabled={!loginEnabled}
/> />
</div> </div>
@ -111,7 +123,12 @@ export default function AdminPremiumSection() {
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.enabled || false} checked={settings.enabled || false}
onChange={(e) => setSettings({ ...settings, enabled: e.target.checked })} onChange={(e) => {
if (!loginEnabled) return;
setSettings({ ...settings, enabled: e.target.checked });
}}
disabled={!loginEnabled}
styles={getDisabledStyles()}
/> />
<PendingBadge show={isFieldPending('enabled')} /> <PendingBadge show={isFieldPending('enabled')} />
</Group> </Group>
@ -120,7 +137,7 @@ export default function AdminPremiumSection() {
</Paper> </Paper>
<Group justify="flex-end"> <Group justify="flex-end">
<Button onClick={handleSave} loading={saving} size="sm"> <Button onClick={handleSave} loading={saving} size="sm" disabled={!loginEnabled}>
{t('admin.settings.save', 'Save Changes')} {t('admin.settings.save', 'Save Changes')}
</Button> </Button>
</Group> </Group>

View File

@ -6,6 +6,8 @@ import RestartConfirmationModal from '@app/components/shared/config/RestartConfi
import { useRestartServer } from '@app/components/shared/config/useRestartServer'; import { useRestartServer } from '@app/components/shared/config/useRestartServer';
import { useAdminSettings } from '@app/hooks/useAdminSettings'; import { useAdminSettings } from '@app/hooks/useAdminSettings';
import PendingBadge from '@app/components/shared/config/PendingBadge'; import PendingBadge from '@app/components/shared/config/PendingBadge';
import { useLoginRequired } from '@app/hooks/useLoginRequired';
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
import apiClient from '@app/services/apiClient'; import apiClient from '@app/services/apiClient';
interface PrivacySettingsData { interface PrivacySettingsData {
@ -16,6 +18,7 @@ interface PrivacySettingsData {
export default function AdminPrivacySection() { export default function AdminPrivacySection() {
const { t } = useTranslation(); const { t } = useTranslation();
const { loginEnabled, validateLoginEnabled, getDisabledStyles } = useLoginRequired();
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer(); const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
const { const {
@ -76,10 +79,17 @@ export default function AdminPrivacySection() {
}); });
useEffect(() => { useEffect(() => {
if (loginEnabled) {
fetchSettings(); fetchSettings();
}, []); }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loginEnabled]);
const handleSave = async () => { const handleSave = async () => {
if (!validateLoginEnabled()) {
return;
}
try { try {
await saveSettings(); await saveSettings();
showRestartModal(); showRestartModal();
@ -92,7 +102,10 @@ export default function AdminPrivacySection() {
} }
}; };
if (loading) { // Override loading state when login is disabled
const actualLoading = loginEnabled ? loading : false;
if (actualLoading) {
return ( return (
<Stack align="center" justify="center" h={200}> <Stack align="center" justify="center" h={200}>
<Loader size="lg" /> <Loader size="lg" />
@ -102,6 +115,8 @@ export default function AdminPrivacySection() {
return ( return (
<Stack gap="lg"> <Stack gap="lg">
<LoginRequiredBanner show={!loginEnabled} />
<div> <div>
<Text fw={600} size="lg">{t('admin.settings.privacy.title', 'Privacy')}</Text> <Text fw={600} size="lg">{t('admin.settings.privacy.title', 'Privacy')}</Text>
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
@ -123,8 +138,13 @@ export default function AdminPrivacySection() {
</div> </div>
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.enableAnalytics || false} checked={settings?.enableAnalytics || false}
onChange={(e) => setSettings({ ...settings, enableAnalytics: e.target.checked })} onChange={(e) => {
if (!loginEnabled) return;
setSettings({ ...settings, enableAnalytics: e.target.checked });
}}
disabled={!loginEnabled}
styles={getDisabledStyles()}
/> />
<PendingBadge show={isFieldPending('enableAnalytics')} /> <PendingBadge show={isFieldPending('enableAnalytics')} />
</Group> </Group>
@ -139,8 +159,13 @@ export default function AdminPrivacySection() {
</div> </div>
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.metricsEnabled || false} checked={settings?.metricsEnabled || false}
onChange={(e) => setSettings({ ...settings, metricsEnabled: e.target.checked })} onChange={(e) => {
if (!loginEnabled) return;
setSettings({ ...settings, metricsEnabled: e.target.checked });
}}
disabled={!loginEnabled}
styles={getDisabledStyles()}
/> />
<PendingBadge show={isFieldPending('metricsEnabled')} /> <PendingBadge show={isFieldPending('metricsEnabled')} />
</Group> </Group>
@ -162,8 +187,13 @@ export default function AdminPrivacySection() {
</div> </div>
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.googleVisibility || false} checked={settings?.googleVisibility || false}
onChange={(e) => setSettings({ ...settings, googleVisibility: e.target.checked })} onChange={(e) => {
if (!loginEnabled) return;
setSettings({ ...settings, googleVisibility: e.target.checked });
}}
disabled={!loginEnabled}
styles={getDisabledStyles()}
/> />
<PendingBadge show={isFieldPending('googleVisibility')} /> <PendingBadge show={isFieldPending('googleVisibility')} />
</Group> </Group>
@ -173,7 +203,7 @@ export default function AdminPrivacySection() {
{/* Save Button */} {/* Save Button */}
<Group justify="flex-end"> <Group justify="flex-end">
<Button onClick={handleSave} loading={saving} size="sm"> <Button onClick={handleSave} loading={saving} size="sm" disabled={!loginEnabled}>
{t('admin.settings.save', 'Save Changes')} {t('admin.settings.save', 'Save Changes')}
</Button> </Button>
</Group> </Group>

View File

@ -8,6 +8,8 @@ import { useRestartServer } from '@app/components/shared/config/useRestartServer
import { useAdminSettings } from '@app/hooks/useAdminSettings'; import { useAdminSettings } from '@app/hooks/useAdminSettings';
import PendingBadge from '@app/components/shared/config/PendingBadge'; import PendingBadge from '@app/components/shared/config/PendingBadge';
import apiClient from '@app/services/apiClient'; import apiClient from '@app/services/apiClient';
import { useLoginRequired } from '@app/hooks/useLoginRequired';
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
interface SecuritySettingsData { interface SecuritySettingsData {
enableLogin?: boolean; enableLogin?: boolean;
@ -44,6 +46,7 @@ interface SecuritySettingsData {
export default function AdminSecuritySection() { export default function AdminSecuritySection() {
const { t } = useTranslation(); const { t } = useTranslation();
const { loginEnabled, validateLoginEnabled } = useLoginRequired();
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer(); const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
const { const {
@ -157,10 +160,21 @@ export default function AdminSecuritySection() {
}); });
useEffect(() => { useEffect(() => {
if (loginEnabled) {
fetchSettings(); fetchSettings();
}, []); }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loginEnabled]);
// Override loading state when login is disabled
const actualLoading = loginEnabled ? loading : false;
const handleSave = async () => { const handleSave = async () => {
// Block save if login is disabled
if (!validateLoginEnabled()) {
return;
}
try { try {
await saveSettings(); await saveSettings();
showRestartModal(); showRestartModal();
@ -173,7 +187,7 @@ export default function AdminSecuritySection() {
} }
}; };
if (loading) { if (actualLoading) {
return ( return (
<Stack align="center" justify="center" h={200}> <Stack align="center" justify="center" h={200}>
<Loader size="lg" /> <Loader size="lg" />
@ -183,6 +197,8 @@ export default function AdminSecuritySection() {
return ( return (
<Stack gap="lg"> <Stack gap="lg">
<LoginRequiredBanner show={!loginEnabled} />
<div> <div>
<Text fw={600} size="lg">{t('admin.settings.security.title', 'Security')}</Text> <Text fw={600} size="lg">{t('admin.settings.security.title', 'Security')}</Text>
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
@ -204,8 +220,9 @@ export default function AdminSecuritySection() {
</div> </div>
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.enableLogin || false} checked={settings?.enableLogin || false}
onChange={(e) => setSettings({ ...settings, enableLogin: e.target.checked })} onChange={(e) => setSettings({ ...settings, enableLogin: e.target.checked })}
disabled={!loginEnabled}
/> />
<PendingBadge show={isFieldPending('enableLogin')} /> <PendingBadge show={isFieldPending('enableLogin')} />
</Group> </Group>
@ -215,7 +232,7 @@ export default function AdminSecuritySection() {
<Select <Select
label={t('admin.settings.security.loginMethod.label', 'Login Method')} label={t('admin.settings.security.loginMethod.label', 'Login Method')}
description={t('admin.settings.security.loginMethod.description', 'The authentication method to use for user login')} description={t('admin.settings.security.loginMethod.description', 'The authentication method to use for user login')}
value={settings.loginMethod || 'all'} value={settings?.loginMethod || 'all'}
onChange={(value) => setSettings({ ...settings, loginMethod: value || 'all' })} onChange={(value) => setSettings({ ...settings, loginMethod: value || 'all' })}
data={[ data={[
{ value: 'all', label: t('admin.settings.security.loginMethod.all', 'All Methods') }, { value: 'all', label: t('admin.settings.security.loginMethod.all', 'All Methods') },
@ -224,6 +241,7 @@ export default function AdminSecuritySection() {
{ value: 'saml2', label: t('admin.settings.security.loginMethod.saml2', 'SAML2 Only') }, { value: 'saml2', label: t('admin.settings.security.loginMethod.saml2', 'SAML2 Only') },
]} ]}
comboboxProps={{ zIndex: 1400 }} comboboxProps={{ zIndex: 1400 }}
disabled={!loginEnabled}
/> />
{isFieldPending('loginMethod') && ( {isFieldPending('loginMethod') && (
<Group mt="xs"> <Group mt="xs">
@ -241,10 +259,11 @@ export default function AdminSecuritySection() {
</Group> </Group>
} }
description={t('admin.settings.security.loginAttemptCount.description', 'Maximum number of failed login attempts before account lockout')} description={t('admin.settings.security.loginAttemptCount.description', 'Maximum number of failed login attempts before account lockout')}
value={settings.loginAttemptCount || 0} value={settings?.loginAttemptCount || 0}
onChange={(value) => setSettings({ ...settings, loginAttemptCount: Number(value) })} onChange={(value) => setSettings({ ...settings, loginAttemptCount: Number(value) })}
min={0} min={0}
max={100} max={100}
disabled={!loginEnabled}
/> />
</div> </div>
@ -257,10 +276,11 @@ export default function AdminSecuritySection() {
</Group> </Group>
} }
description={t('admin.settings.security.loginResetTimeMinutes.description', 'Time before failed login attempts are reset')} description={t('admin.settings.security.loginResetTimeMinutes.description', 'Time before failed login attempts are reset')}
value={settings.loginResetTimeMinutes || 0} value={settings?.loginResetTimeMinutes || 0}
onChange={(value) => setSettings({ ...settings, loginResetTimeMinutes: Number(value) })} onChange={(value) => setSettings({ ...settings, loginResetTimeMinutes: Number(value) })}
min={0} min={0}
max={1440} max={1440}
disabled={!loginEnabled}
/> />
</div> </div>
@ -273,8 +293,9 @@ export default function AdminSecuritySection() {
</div> </div>
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.csrfDisabled || false} checked={settings?.csrfDisabled || false}
onChange={(e) => setSettings({ ...settings, csrfDisabled: e.target.checked })} onChange={(e) => setSettings({ ...settings, csrfDisabled: e.target.checked })}
disabled={!loginEnabled}
/> />
<PendingBadge show={isFieldPending('csrfDisabled')} /> <PendingBadge show={isFieldPending('csrfDisabled')} />
</Group> </Group>
@ -308,8 +329,9 @@ export default function AdminSecuritySection() {
</div> </div>
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.jwt?.persistence || false} checked={settings?.jwt?.persistence || false}
onChange={(e) => setSettings({ ...settings, jwt: { ...settings.jwt, persistence: e.target.checked } })} onChange={(e) => setSettings({ ...settings, jwt: { ...settings?.jwt, persistence: e.target.checked } })}
disabled={!loginEnabled}
/> />
<PendingBadge show={isFieldPending('jwt.persistence')} /> <PendingBadge show={isFieldPending('jwt.persistence')} />
</Group> </Group>
@ -324,8 +346,9 @@ export default function AdminSecuritySection() {
</div> </div>
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.jwt?.enableKeyRotation || false} checked={settings?.jwt?.enableKeyRotation || false}
onChange={(e) => setSettings({ ...settings, jwt: { ...settings.jwt, enableKeyRotation: e.target.checked } })} onChange={(e) => setSettings({ ...settings, jwt: { ...settings?.jwt, enableKeyRotation: e.target.checked } })}
disabled={!loginEnabled}
/> />
<PendingBadge show={isFieldPending('jwt.enableKeyRotation')} /> <PendingBadge show={isFieldPending('jwt.enableKeyRotation')} />
</Group> </Group>
@ -340,8 +363,9 @@ export default function AdminSecuritySection() {
</div> </div>
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.jwt?.enableKeyCleanup || false} checked={settings?.jwt?.enableKeyCleanup || false}
onChange={(e) => setSettings({ ...settings, jwt: { ...settings.jwt, enableKeyCleanup: e.target.checked } })} onChange={(e) => setSettings({ ...settings, jwt: { ...settings?.jwt, enableKeyCleanup: e.target.checked } })}
disabled={!loginEnabled}
/> />
<PendingBadge show={isFieldPending('jwt.enableKeyCleanup')} /> <PendingBadge show={isFieldPending('jwt.enableKeyCleanup')} />
</Group> </Group>
@ -356,10 +380,11 @@ export default function AdminSecuritySection() {
</Group> </Group>
} }
description={t('admin.settings.security.jwt.keyRetentionDays.description', 'Number of days to retain old JWT keys for verification')} description={t('admin.settings.security.jwt.keyRetentionDays.description', 'Number of days to retain old JWT keys for verification')}
value={settings.jwt?.keyRetentionDays || 7} value={settings?.jwt?.keyRetentionDays || 7}
onChange={(value) => setSettings({ ...settings, jwt: { ...settings.jwt, keyRetentionDays: Number(value) } })} onChange={(value) => setSettings({ ...settings, jwt: { ...settings?.jwt, keyRetentionDays: Number(value) } })}
min={1} min={1}
max={365} max={365}
disabled={!loginEnabled}
/> />
</div> </div>
@ -372,8 +397,9 @@ export default function AdminSecuritySection() {
</div> </div>
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.jwt?.secureCookie || false} checked={settings?.jwt?.secureCookie || false}
onChange={(e) => setSettings({ ...settings, jwt: { ...settings.jwt, secureCookie: e.target.checked } })} onChange={(e) => setSettings({ ...settings, jwt: { ...settings?.jwt, secureCookie: e.target.checked } })}
disabled={!loginEnabled}
/> />
<PendingBadge show={isFieldPending('jwt.secureCookie')} /> <PendingBadge show={isFieldPending('jwt.secureCookie')} />
</Group> </Group>
@ -398,8 +424,9 @@ export default function AdminSecuritySection() {
</div> </div>
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.audit?.enabled || false} checked={settings?.audit?.enabled || false}
onChange={(e) => setSettings({ ...settings, audit: { ...settings.audit, enabled: e.target.checked } })} onChange={(e) => setSettings({ ...settings, audit: { ...settings?.audit, enabled: e.target.checked } })}
disabled={!loginEnabled}
/> />
<PendingBadge show={isFieldPending('audit.enabled')} /> <PendingBadge show={isFieldPending('audit.enabled')} />
</Group> </Group>
@ -414,10 +441,11 @@ export default function AdminSecuritySection() {
</Group> </Group>
} }
description={t('admin.settings.security.audit.level.description', '0=OFF, 1=BASIC, 2=STANDARD, 3=VERBOSE')} description={t('admin.settings.security.audit.level.description', '0=OFF, 1=BASIC, 2=STANDARD, 3=VERBOSE')}
value={settings.audit?.level || 2} value={settings?.audit?.level || 2}
onChange={(value) => setSettings({ ...settings, audit: { ...settings.audit, level: Number(value) } })} onChange={(value) => setSettings({ ...settings, audit: { ...settings?.audit, level: Number(value) } })}
min={0} min={0}
max={3} max={3}
disabled={!loginEnabled}
/> />
</div> </div>
@ -430,10 +458,11 @@ export default function AdminSecuritySection() {
</Group> </Group>
} }
description={t('admin.settings.security.audit.retentionDays.description', 'Number of days to retain audit logs')} description={t('admin.settings.security.audit.retentionDays.description', 'Number of days to retain audit logs')}
value={settings.audit?.retentionDays || 90} value={settings?.audit?.retentionDays || 90}
onChange={(value) => setSettings({ ...settings, audit: { ...settings.audit, retentionDays: Number(value) } })} onChange={(value) => setSettings({ ...settings, audit: { ...settings?.audit, retentionDays: Number(value) } })}
min={1} min={1}
max={3650} max={3650}
disabled={!loginEnabled}
/> />
</div> </div>
</Stack> </Stack>
@ -458,14 +487,15 @@ export default function AdminSecuritySection() {
</div> </div>
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.html?.urlSecurity?.enabled || false} checked={settings?.html?.urlSecurity?.enabled || false}
onChange={(e) => setSettings({ onChange={(e) => setSettings({
...settings, ...settings,
html: { html: {
...settings.html, ...settings?.html,
urlSecurity: { ...settings.html?.urlSecurity, enabled: e.target.checked } urlSecurity: { ...settings?.html?.urlSecurity, enabled: e.target.checked }
} }
})} })}
disabled={!loginEnabled}
/> />
<PendingBadge show={isFieldPending('html.urlSecurity.enabled')} /> <PendingBadge show={isFieldPending('html.urlSecurity.enabled')} />
</Group> </Group>
@ -480,12 +510,12 @@ export default function AdminSecuritySection() {
</Group> </Group>
} }
description={t('admin.settings.security.htmlUrlSecurity.level.description', 'MAX: whitelist only, MEDIUM: block internal networks, OFF: no restrictions')} description={t('admin.settings.security.htmlUrlSecurity.level.description', 'MAX: whitelist only, MEDIUM: block internal networks, OFF: no restrictions')}
value={settings.html?.urlSecurity?.level || 'MEDIUM'} value={settings?.html?.urlSecurity?.level || 'MEDIUM'}
onChange={(value) => setSettings({ onChange={(value) => setSettings({
...settings, ...settings,
html: { html: {
...settings.html, ...settings?.html,
urlSecurity: { ...settings.html?.urlSecurity, level: value || 'MEDIUM' } urlSecurity: { ...settings?.html?.urlSecurity, level: value || 'MEDIUM' }
} }
})} })}
data={[ data={[
@ -494,6 +524,7 @@ export default function AdminSecuritySection() {
{ value: 'OFF', label: t('admin.settings.security.htmlUrlSecurity.level.off', 'Off (No Restrictions)') }, { value: 'OFF', label: t('admin.settings.security.htmlUrlSecurity.level.off', 'Off (No Restrictions)') },
]} ]}
comboboxProps={{ zIndex: 1400 }} comboboxProps={{ zIndex: 1400 }}
disabled={!loginEnabled}
/> />
</div> </div>
@ -512,13 +543,13 @@ export default function AdminSecuritySection() {
</Group> </Group>
} }
description={t('admin.settings.security.htmlUrlSecurity.allowedDomains.description', 'One domain per line (e.g., cdn.example.com). Only these domains allowed when level is MAX')} description={t('admin.settings.security.htmlUrlSecurity.allowedDomains.description', 'One domain per line (e.g., cdn.example.com). Only these domains allowed when level is MAX')}
value={settings.html?.urlSecurity?.allowedDomains?.join('\n') || ''} value={settings?.html?.urlSecurity?.allowedDomains?.join('\n') || ''}
onChange={(e) => setSettings({ onChange={(e) => setSettings({
...settings, ...settings,
html: { html: {
...settings.html, ...settings?.html,
urlSecurity: { urlSecurity: {
...settings.html?.urlSecurity, ...settings?.html?.urlSecurity,
allowedDomains: e.target.value ? e.target.value.split('\n').filter(d => d.trim()) : [] allowedDomains: e.target.value ? e.target.value.split('\n').filter(d => d.trim()) : []
} }
} }
@ -526,6 +557,7 @@ export default function AdminSecuritySection() {
placeholder="cdn.example.com&#10;images.google.com" placeholder="cdn.example.com&#10;images.google.com"
minRows={3} minRows={3}
autosize autosize
disabled={!loginEnabled}
/> />
</div> </div>
@ -539,13 +571,13 @@ export default function AdminSecuritySection() {
</Group> </Group>
} }
description={t('admin.settings.security.htmlUrlSecurity.blockedDomains.description', 'One domain per line (e.g., malicious.com). Additional domains to block')} description={t('admin.settings.security.htmlUrlSecurity.blockedDomains.description', 'One domain per line (e.g., malicious.com). Additional domains to block')}
value={settings.html?.urlSecurity?.blockedDomains?.join('\n') || ''} value={settings?.html?.urlSecurity?.blockedDomains?.join('\n') || ''}
onChange={(e) => setSettings({ onChange={(e) => setSettings({
...settings, ...settings,
html: { html: {
...settings.html, ...settings?.html,
urlSecurity: { urlSecurity: {
...settings.html?.urlSecurity, ...settings?.html?.urlSecurity,
blockedDomains: e.target.value ? e.target.value.split('\n').filter(d => d.trim()) : [] blockedDomains: e.target.value ? e.target.value.split('\n').filter(d => d.trim()) : []
} }
} }
@ -553,6 +585,7 @@ export default function AdminSecuritySection() {
placeholder="malicious.com&#10;evil.org" placeholder="malicious.com&#10;evil.org"
minRows={3} minRows={3}
autosize autosize
disabled={!loginEnabled}
/> />
</div> </div>
@ -566,13 +599,13 @@ export default function AdminSecuritySection() {
</Group> </Group>
} }
description={t('admin.settings.security.htmlUrlSecurity.internalTlds.description', 'One TLD per line (e.g., .local, .internal). Block domains with these TLD patterns')} description={t('admin.settings.security.htmlUrlSecurity.internalTlds.description', 'One TLD per line (e.g., .local, .internal). Block domains with these TLD patterns')}
value={settings.html?.urlSecurity?.internalTlds?.join('\n') || ''} value={settings?.html?.urlSecurity?.internalTlds?.join('\n') || ''}
onChange={(e) => setSettings({ onChange={(e) => setSettings({
...settings, ...settings,
html: { html: {
...settings.html, ...settings?.html,
urlSecurity: { urlSecurity: {
...settings.html?.urlSecurity, ...settings?.html?.urlSecurity,
internalTlds: e.target.value ? e.target.value.split('\n').filter(d => d.trim()) : [] internalTlds: e.target.value ? e.target.value.split('\n').filter(d => d.trim()) : []
} }
} }
@ -580,6 +613,7 @@ export default function AdminSecuritySection() {
placeholder=".local&#10;.internal&#10;.corp&#10;.home" placeholder=".local&#10;.internal&#10;.corp&#10;.home"
minRows={3} minRows={3}
autosize autosize
disabled={!loginEnabled}
/> />
</div> </div>
@ -595,14 +629,15 @@ export default function AdminSecuritySection() {
</div> </div>
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.html?.urlSecurity?.blockPrivateNetworks || false} checked={settings?.html?.urlSecurity?.blockPrivateNetworks || false}
onChange={(e) => setSettings({ onChange={(e) => setSettings({
...settings, ...settings,
html: { html: {
...settings.html, ...settings?.html,
urlSecurity: { ...settings.html?.urlSecurity, blockPrivateNetworks: e.target.checked } urlSecurity: { ...settings?.html?.urlSecurity, blockPrivateNetworks: e.target.checked }
} }
})} })}
disabled={!loginEnabled}
/> />
<PendingBadge show={isFieldPending('html.urlSecurity.blockPrivateNetworks')} /> <PendingBadge show={isFieldPending('html.urlSecurity.blockPrivateNetworks')} />
</Group> </Group>
@ -617,14 +652,15 @@ export default function AdminSecuritySection() {
</div> </div>
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.html?.urlSecurity?.blockLocalhost || false} checked={settings?.html?.urlSecurity?.blockLocalhost || false}
onChange={(e) => setSettings({ onChange={(e) => setSettings({
...settings, ...settings,
html: { html: {
...settings.html, ...settings?.html,
urlSecurity: { ...settings.html?.urlSecurity, blockLocalhost: e.target.checked } urlSecurity: { ...settings?.html?.urlSecurity, blockLocalhost: e.target.checked }
} }
})} })}
disabled={!loginEnabled}
/> />
<PendingBadge show={isFieldPending('html.urlSecurity.blockLocalhost')} /> <PendingBadge show={isFieldPending('html.urlSecurity.blockLocalhost')} />
</Group> </Group>
@ -639,14 +675,15 @@ export default function AdminSecuritySection() {
</div> </div>
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.html?.urlSecurity?.blockLinkLocal || false} checked={settings?.html?.urlSecurity?.blockLinkLocal || false}
onChange={(e) => setSettings({ onChange={(e) => setSettings({
...settings, ...settings,
html: { html: {
...settings.html, ...settings?.html,
urlSecurity: { ...settings.html?.urlSecurity, blockLinkLocal: e.target.checked } urlSecurity: { ...settings?.html?.urlSecurity, blockLinkLocal: e.target.checked }
} }
})} })}
disabled={!loginEnabled}
/> />
<PendingBadge show={isFieldPending('html.urlSecurity.blockLinkLocal')} /> <PendingBadge show={isFieldPending('html.urlSecurity.blockLinkLocal')} />
</Group> </Group>
@ -661,14 +698,15 @@ export default function AdminSecuritySection() {
</div> </div>
<Group gap="xs"> <Group gap="xs">
<Switch <Switch
checked={settings.html?.urlSecurity?.blockCloudMetadata || false} checked={settings?.html?.urlSecurity?.blockCloudMetadata || false}
onChange={(e) => setSettings({ onChange={(e) => setSettings({
...settings, ...settings,
html: { html: {
...settings.html, ...settings?.html,
urlSecurity: { ...settings.html?.urlSecurity, blockCloudMetadata: e.target.checked } urlSecurity: { ...settings?.html?.urlSecurity, blockCloudMetadata: e.target.checked }
} }
})} })}
disabled={!loginEnabled}
/> />
<PendingBadge show={isFieldPending('html.urlSecurity.blockCloudMetadata')} /> <PendingBadge show={isFieldPending('html.urlSecurity.blockCloudMetadata')} />
</Group> </Group>
@ -682,7 +720,7 @@ export default function AdminSecuritySection() {
{/* Save Button */} {/* Save Button */}
<Group justify="flex-end"> <Group justify="flex-end">
<Button onClick={handleSave} loading={saving} size="sm"> <Button onClick={handleSave} loading={saving} size="sm" disabled={!loginEnabled}>
{t('admin.settings.save', 'Save Changes')} {t('admin.settings.save', 'Save Changes')}
</Button> </Button>
</Group> </Group>

View File

@ -14,9 +14,12 @@ import usageAnalyticsService, { EndpointStatisticsResponse } from '@app/services
import UsageAnalyticsChart from '@app/components/shared/config/configSections/usage/UsageAnalyticsChart'; import UsageAnalyticsChart from '@app/components/shared/config/configSections/usage/UsageAnalyticsChart';
import UsageAnalyticsTable from '@app/components/shared/config/configSections/usage/UsageAnalyticsTable'; import UsageAnalyticsTable from '@app/components/shared/config/configSections/usage/UsageAnalyticsTable';
import LocalIcon from '@app/components/shared/LocalIcon'; import LocalIcon from '@app/components/shared/LocalIcon';
import { useLoginRequired } from '@app/hooks/useLoginRequired';
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
const AdminUsageSection: React.FC = () => { const AdminUsageSection: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { loginEnabled, validateLoginEnabled } = useLoginRequired();
const [data, setData] = useState<EndpointStatisticsResponse | null>(null); const [data, setData] = useState<EndpointStatisticsResponse | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -24,6 +27,10 @@ const AdminUsageSection: React.FC = () => {
const [dataType, setDataType] = useState<'all' | 'api' | 'ui'>('all'); const [dataType, setDataType] = useState<'all' | 'api' | 'ui'>('all');
const fetchData = async () => { const fetchData = async () => {
if (!validateLoginEnabled()) {
return;
}
try { try {
setLoading(true); setLoading(true);
setError(null); setError(null);
@ -40,10 +47,55 @@ const AdminUsageSection: React.FC = () => {
}; };
useEffect(() => { useEffect(() => {
if (loginEnabled) {
fetchData(); fetchData();
}, [displayMode, dataType]); } else {
// Provide example usage analytics data when login is disabled
const totalVisits = 15847;
const allEndpoints = [
{ endpoint: 'merge-pdfs', visits: 3245, percentage: (3245 / totalVisits) * 100 },
{ endpoint: 'compress-pdf', visits: 2891, percentage: (2891 / totalVisits) * 100 },
{ endpoint: 'pdf-to-img', visits: 2156, percentage: (2156 / totalVisits) * 100 },
{ endpoint: 'split-pdf', visits: 1834, percentage: (1834 / totalVisits) * 100 },
{ endpoint: 'rotate-pdf', visits: 1523, percentage: (1523 / totalVisits) * 100 },
{ endpoint: 'ocr-pdf', visits: 1287, percentage: (1287 / totalVisits) * 100 },
{ endpoint: 'add-watermark', visits: 945, percentage: (945 / totalVisits) * 100 },
{ endpoint: 'extract-images', visits: 782, percentage: (782 / totalVisits) * 100 },
{ endpoint: 'add-password', visits: 621, percentage: (621 / totalVisits) * 100 },
{ endpoint: 'html-to-pdf', visits: 563, percentage: (563 / totalVisits) * 100 },
{ endpoint: 'remove-password', visits: 487, percentage: (487 / totalVisits) * 100 },
{ endpoint: 'pdf-to-pdfa', visits: 423, percentage: (423 / totalVisits) * 100 },
{ endpoint: 'extract-pdf-metadata', visits: 356, percentage: (356 / totalVisits) * 100 },
{ endpoint: 'add-page-numbers', visits: 298, percentage: (298 / totalVisits) * 100 },
{ endpoint: 'crop', visits: 245, percentage: (245 / totalVisits) * 100 },
{ endpoint: 'flatten', visits: 187, percentage: (187 / totalVisits) * 100 },
{ endpoint: 'sanitize-pdf', visits: 134, percentage: (134 / totalVisits) * 100 },
{ endpoint: 'auto-split-pdf', visits: 98, percentage: (98 / totalVisits) * 100 },
{ endpoint: 'scale-pages', visits: 76, percentage: (76 / totalVisits) * 100 },
{ endpoint: 'compare-pdfs', visits: 42, percentage: (42 / totalVisits) * 100 },
];
// Filter based on display mode
let filteredEndpoints = allEndpoints;
if (displayMode === 'top10') {
filteredEndpoints = allEndpoints.slice(0, 10);
} else if (displayMode === 'top20') {
filteredEndpoints = allEndpoints.slice(0, 20);
}
setData({
totalVisits: totalVisits,
endpoints: filteredEndpoints,
});
setLoading(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [displayMode, dataType, loginEnabled]);
const handleRefresh = () => { const handleRefresh = () => {
if (!validateLoginEnabled()) {
return;
}
fetchData(); fetchData();
}; };
@ -60,8 +112,11 @@ const AdminUsageSection: React.FC = () => {
} }
}; };
// Override loading state when login is disabled
const actualLoading = loginEnabled ? loading : false;
// Early returns for loading/error states // Early returns for loading/error states
if (loading) { if (actualLoading) {
return ( return (
<div style={{ display: 'flex', justifyContent: 'center', padding: '2rem' }}> <div style={{ display: 'flex', justifyContent: 'center', padding: '2rem' }}>
<Loader size="lg" /> <Loader size="lg" />
@ -85,16 +140,18 @@ const AdminUsageSection: React.FC = () => {
); );
} }
const chartData = data.endpoints.map((e) => ({ label: e.endpoint, value: e.visits })); const chartData = data?.endpoints?.map((e) => ({ label: e.endpoint, value: e.visits })) || [];
const displayedVisits = data.endpoints.reduce((sum, e) => sum + e.visits, 0); const displayedVisits = data?.endpoints?.reduce((sum, e) => sum + e.visits, 0) || 0;
const displayedPercentage = data.totalVisits > 0 const displayedPercentage = (data?.totalVisits || 0) > 0
? ((displayedVisits / data.totalVisits) * 100).toFixed(1) ? ((displayedVisits / (data?.totalVisits || 1)) * 100).toFixed(1)
: '0'; : '0';
return ( return (
<Stack gap="lg"> <Stack gap="lg">
<LoginRequiredBanner show={!loginEnabled} />
{/* Controls */} {/* Controls */}
<Card padding="lg" radius="md" withBorder> <Card padding="lg" radius="md" withBorder>
<Stack gap="md"> <Stack gap="md">
@ -103,6 +160,7 @@ const AdminUsageSection: React.FC = () => {
<SegmentedControl <SegmentedControl
value={displayMode} value={displayMode}
onChange={(value) => setDisplayMode(value as 'top10' | 'top20' | 'all')} onChange={(value) => setDisplayMode(value as 'top10' | 'top20' | 'all')}
disabled={!loginEnabled}
data={[ data={[
{ {
value: 'top10', value: 'top10',
@ -123,6 +181,7 @@ const AdminUsageSection: React.FC = () => {
leftSection={<LocalIcon icon="refresh" width="1rem" height="1rem" />} leftSection={<LocalIcon icon="refresh" width="1rem" height="1rem" />}
onClick={handleRefresh} onClick={handleRefresh}
loading={loading} loading={loading}
disabled={!loginEnabled}
> >
{t('usage.controls.refresh', 'Refresh')} {t('usage.controls.refresh', 'Refresh')}
</Button> </Button>
@ -136,6 +195,7 @@ const AdminUsageSection: React.FC = () => {
<SegmentedControl <SegmentedControl
value={dataType} value={dataType}
onChange={(value) => setDataType(value as 'all' | 'api' | 'ui')} onChange={(value) => setDataType(value as 'all' | 'api' | 'ui')}
disabled={!loginEnabled}
data={[ data={[
{ {
value: 'all', value: 'all',

View File

@ -10,6 +10,7 @@ interface ProviderCardProps {
settings?: Record<string, any>; settings?: Record<string, any>;
onSave?: (settings: Record<string, any>) => void; onSave?: (settings: Record<string, any>) => void;
onDisconnect?: () => void; onDisconnect?: () => void;
disabled?: boolean;
} }
export default function ProviderCard({ export default function ProviderCard({
@ -18,6 +19,7 @@ export default function ProviderCard({
settings = {}, settings = {},
onSave, onSave,
onDisconnect, onDisconnect,
disabled = false,
}: ProviderCardProps) { }: ProviderCardProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
@ -39,6 +41,7 @@ export default function ProviderCard({
}; };
const handleFieldChange = (key: string, value: any) => { const handleFieldChange = (key: string, value: any) => {
if (disabled) return; // Block changes when disabled
setLocalSettings((prev) => ({ ...prev, [key]: value })); setLocalSettings((prev) => ({ ...prev, [key]: value }));
}; };
@ -63,6 +66,7 @@ export default function ProviderCard({
<Switch <Switch
checked={value || false} checked={value || false}
onChange={(e) => handleFieldChange(field.key, e.target.checked)} onChange={(e) => handleFieldChange(field.key, e.target.checked)}
disabled={disabled}
/> />
</div> </div>
); );
@ -76,6 +80,7 @@ export default function ProviderCard({
placeholder={field.placeholder} placeholder={field.placeholder}
value={value} value={value}
onChange={(e) => handleFieldChange(field.key, e.target.value)} onChange={(e) => handleFieldChange(field.key, e.target.value)}
disabled={disabled}
/> />
); );
@ -88,6 +93,7 @@ export default function ProviderCard({
placeholder={field.placeholder} placeholder={field.placeholder}
value={value} value={value}
onChange={(e) => handleFieldChange(field.key, e.target.value)} onChange={(e) => handleFieldChange(field.key, e.target.value)}
disabled={disabled}
/> />
); );
@ -100,6 +106,7 @@ export default function ProviderCard({
placeholder={field.placeholder} placeholder={field.placeholder}
value={value} value={value}
onChange={(e) => handleFieldChange(field.key, e.target.value)} onChange={(e) => handleFieldChange(field.key, e.target.value)}
disabled={disabled}
/> />
); );
} }
@ -174,11 +181,12 @@ export default function ProviderCard({
color="red" color="red"
size="sm" size="sm"
onClick={onDisconnect} onClick={onDisconnect}
disabled={disabled}
> >
{t('admin.settings.connections.disconnect', 'Disconnect')} {t('admin.settings.connections.disconnect', 'Disconnect')}
</Button> </Button>
)} )}
<Button size="sm" onClick={handleSave}> <Button size="sm" onClick={handleSave} disabled={disabled}>
{t('admin.settings.save', 'Save Changes')} {t('admin.settings.save', 'Save Changes')}
</Button> </Button>
</Group> </Group>

View File

@ -53,7 +53,11 @@ const SimpleBarChart: React.FC<SimpleBarChartProps> = ({ data, title, color = 'b
); );
}; };
const AuditChartsSection: React.FC = () => { interface AuditChartsSectionProps {
loginEnabled?: boolean;
}
const AuditChartsSection: React.FC<AuditChartsSectionProps> = ({ loginEnabled = true }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [timePeriod, setTimePeriod] = useState<'day' | 'week' | 'month'>('week'); const [timePeriod, setTimePeriod] = useState<'day' | 'week' | 'month'>('week');
const [chartsData, setChartsData] = useState<AuditChartsData | null>(null); const [chartsData, setChartsData] = useState<AuditChartsData | null>(null);
@ -74,8 +78,27 @@ const AuditChartsSection: React.FC = () => {
} }
}; };
if (loginEnabled) {
fetchChartsData(); fetchChartsData();
}, [timePeriod]); } else {
// Provide example charts data when login is disabled
setChartsData({
eventsByType: {
labels: ['LOGIN', 'LOGOUT', 'SETTINGS_CHANGE', 'FILE_UPLOAD', 'FILE_DOWNLOAD'],
values: [342, 289, 145, 678, 523],
},
eventsByUser: {
labels: ['admin', 'user1', 'user2', 'user3', 'user4'],
values: [456, 321, 287, 198, 165],
},
eventsOverTime: {
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
values: [123, 145, 167, 189, 201, 87, 65],
},
});
setLoading(false);
}
}, [timePeriod, loginEnabled]);
if (loading) { if (loading) {
return ( return (
@ -123,7 +146,11 @@ const AuditChartsSection: React.FC = () => {
</Text> </Text>
<SegmentedControl <SegmentedControl
value={timePeriod} value={timePeriod}
onChange={(value) => setTimePeriod(value as 'day' | 'week' | 'month')} onChange={(value) => {
if (!loginEnabled) return;
setTimePeriod(value as 'day' | 'week' | 'month');
}}
disabled={!loginEnabled}
data={[ data={[
{ label: t('audit.charts.day', 'Day'), value: 'day' }, { label: t('audit.charts.day', 'Day'), value: 'day' },
{ label: t('audit.charts.week', 'Week'), value: 'week' }, { label: t('audit.charts.week', 'Week'), value: 'week' },

View File

@ -18,7 +18,11 @@ import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex';
import { useAuditFilters } from '@app/hooks/useAuditFilters'; import { useAuditFilters } from '@app/hooks/useAuditFilters';
import AuditFiltersForm from '@app/components/shared/config/configSections/audit/AuditFiltersForm'; import AuditFiltersForm from '@app/components/shared/config/configSections/audit/AuditFiltersForm';
const AuditEventsTable: React.FC = () => { interface AuditEventsTableProps {
loginEnabled?: boolean;
}
const AuditEventsTable: React.FC<AuditEventsTableProps> = ({ loginEnabled = true }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [events, setEvents] = useState<AuditEvent[]>([]); const [events, setEvents] = useState<AuditEvent[]>([]);
const [totalPages, setTotalPages] = useState(0); const [totalPages, setTotalPages] = useState(0);
@ -51,8 +55,57 @@ const AuditEventsTable: React.FC = () => {
} }
}; };
if (loginEnabled) {
fetchEvents(); fetchEvents();
}, [filters, currentPage]); } else {
// Provide example audit events when login is disabled
const now = new Date();
setEvents([
{
id: '1',
timestamp: new Date(now.getTime() - 1000 * 60 * 15).toISOString(),
eventType: 'LOGIN',
username: 'admin',
ipAddress: '192.168.1.100',
details: 'User logged in successfully',
},
{
id: '2',
timestamp: new Date(now.getTime() - 1000 * 60 * 30).toISOString(),
eventType: 'FILE_UPLOAD',
username: 'user1',
ipAddress: '192.168.1.101',
details: 'Uploaded document.pdf',
},
{
id: '3',
timestamp: new Date(now.getTime() - 1000 * 60 * 45).toISOString(),
eventType: 'SETTINGS_CHANGE',
username: 'admin',
ipAddress: '192.168.1.100',
details: 'Modified system settings',
},
{
id: '4',
timestamp: new Date(now.getTime() - 1000 * 60 * 60).toISOString(),
eventType: 'FILE_DOWNLOAD',
username: 'user2',
ipAddress: '192.168.1.102',
details: 'Downloaded report.pdf',
},
{
id: '5',
timestamp: new Date(now.getTime() - 1000 * 60 * 90).toISOString(),
eventType: 'LOGOUT',
username: 'user1',
ipAddress: '192.168.1.101',
details: 'User logged out',
},
]);
setTotalPages(1);
setLoading(false);
}
}, [filters, currentPage, loginEnabled]);
// Wrap filter handlers to reset pagination // Wrap filter handlers to reset pagination
const handleFilterChangeWithReset = (key: keyof typeof filters, value: any) => { const handleFilterChangeWithReset = (key: keyof typeof filters, value: any) => {
@ -83,6 +136,7 @@ const AuditEventsTable: React.FC = () => {
users={users} users={users}
onFilterChange={handleFilterChangeWithReset} onFilterChange={handleFilterChangeWithReset}
onClearFilters={handleClearFiltersWithReset} onClearFilters={handleClearFiltersWithReset}
disabled={!loginEnabled}
/> />
{/* Table */} {/* Table */}
@ -153,6 +207,7 @@ const AuditEventsTable: React.FC = () => {
variant="subtle" variant="subtle"
size="xs" size="xs"
onClick={() => setSelectedEvent(event)} onClick={() => setSelectedEvent(event)}
disabled={!loginEnabled}
> >
{t('audit.events.viewDetails', 'View Details')} {t('audit.events.viewDetails', 'View Details')}
</Button> </Button>

View File

@ -13,7 +13,11 @@ import LocalIcon from '@app/components/shared/LocalIcon';
import { useAuditFilters } from '@app/hooks/useAuditFilters'; import { useAuditFilters } from '@app/hooks/useAuditFilters';
import AuditFiltersForm from '@app/components/shared/config/configSections/audit/AuditFiltersForm'; import AuditFiltersForm from '@app/components/shared/config/configSections/audit/AuditFiltersForm';
const AuditExportSection: React.FC = () => { interface AuditExportSectionProps {
loginEnabled?: boolean;
}
const AuditExportSection: React.FC<AuditExportSectionProps> = ({ loginEnabled = true }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [exportFormat, setExportFormat] = useState<'csv' | 'json'>('csv'); const [exportFormat, setExportFormat] = useState<'csv' | 'json'>('csv');
const [exporting, setExporting] = useState(false); const [exporting, setExporting] = useState(false);
@ -22,6 +26,8 @@ const AuditExportSection: React.FC = () => {
const { filters, eventTypes, users, handleFilterChange, handleClearFilters } = useAuditFilters(); const { filters, eventTypes, users, handleFilterChange, handleClearFilters } = useAuditFilters();
const handleExport = async () => { const handleExport = async () => {
if (!loginEnabled) return;
try { try {
setExporting(true); setExporting(true);
@ -65,7 +71,11 @@ const AuditExportSection: React.FC = () => {
</Text> </Text>
<SegmentedControl <SegmentedControl
value={exportFormat} value={exportFormat}
onChange={(value) => setExportFormat(value as 'csv' | 'json')} onChange={(value) => {
if (!loginEnabled) return;
setExportFormat(value as 'csv' | 'json');
}}
disabled={!loginEnabled}
data={[ data={[
{ label: 'CSV', value: 'csv' }, { label: 'CSV', value: 'csv' },
{ label: 'JSON', value: 'json' }, { label: 'JSON', value: 'json' },
@ -84,6 +94,7 @@ const AuditExportSection: React.FC = () => {
users={users} users={users}
onFilterChange={handleFilterChange} onFilterChange={handleFilterChange}
onClearFilters={handleClearFilters} onClearFilters={handleClearFilters}
disabled={!loginEnabled}
/> />
</div> </div>
@ -93,7 +104,7 @@ const AuditExportSection: React.FC = () => {
leftSection={<LocalIcon icon="download" width="1rem" height="1rem" />} leftSection={<LocalIcon icon="download" width="1rem" height="1rem" />}
onClick={handleExport} onClick={handleExport}
loading={exporting} loading={exporting}
disabled={exporting} disabled={!loginEnabled || exporting}
> >
{t('audit.export.exportButton', 'Export Data')} {t('audit.export.exportButton', 'Export Data')}
</Button> </Button>

View File

@ -11,6 +11,7 @@ interface AuditFiltersFormProps {
users: string[]; users: string[];
onFilterChange: (key: keyof AuditFilters, value: any) => void; onFilterChange: (key: keyof AuditFilters, value: any) => void;
onClearFilters: () => void; onClearFilters: () => void;
disabled?: boolean;
} }
/** /**
@ -22,6 +23,7 @@ const AuditFiltersForm: React.FC<AuditFiltersFormProps> = ({
users, users,
onFilterChange, onFilterChange,
onClearFilters, onClearFilters,
disabled = false,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -33,6 +35,7 @@ const AuditFiltersForm: React.FC<AuditFiltersFormProps> = ({
value={filters.eventType} value={filters.eventType}
onChange={(value) => onFilterChange('eventType', value || undefined)} onChange={(value) => onFilterChange('eventType', value || undefined)}
clearable clearable
disabled={disabled}
style={{ flex: 1, minWidth: 200 }} style={{ flex: 1, minWidth: 200 }}
comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }} comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }}
/> />
@ -43,6 +46,7 @@ const AuditFiltersForm: React.FC<AuditFiltersFormProps> = ({
onChange={(value) => onFilterChange('username', value || undefined)} onChange={(value) => onFilterChange('username', value || undefined)}
clearable clearable
searchable searchable
disabled={disabled}
style={{ flex: 1, minWidth: 200 }} style={{ flex: 1, minWidth: 200 }}
comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }} comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }}
/> />
@ -53,6 +57,7 @@ const AuditFiltersForm: React.FC<AuditFiltersFormProps> = ({
onFilterChange('startDate', value ?? undefined) onFilterChange('startDate', value ?? undefined)
} }
clearable clearable
disabled={disabled}
style={{ flex: 1, minWidth: 150 }} style={{ flex: 1, minWidth: 150 }}
popoverProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }} popoverProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }}
/> />
@ -63,10 +68,11 @@ const AuditFiltersForm: React.FC<AuditFiltersFormProps> = ({
onFilterChange('endDate', value ?? undefined) onFilterChange('endDate', value ?? undefined)
} }
clearable clearable
disabled={disabled}
style={{ flex: 1, minWidth: 150 }} style={{ flex: 1, minWidth: 150 }}
popoverProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }} popoverProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }}
/> />
<Button variant="outline" onClick={onClearFilters}> <Button variant="outline" onClick={onClearFilters} disabled={disabled}>
{t('audit.events.clearFilters', 'Clear')} {t('audit.events.clearFilters', 'Clear')}
</Button> </Button>
</Group> </Group>

View File

@ -0,0 +1,89 @@
import { useAppConfig } from '@app/contexts/AppConfigContext';
import { alert } from '@app/components/toast';
import { useTranslation } from 'react-i18next';
/**
* Hook to manage login-required functionality in admin sections
* Provides login state, validation, and alert functionality
*/
export function useLoginRequired() {
const { config } = useAppConfig();
const { t } = useTranslation();
const loginEnabled = config?.enableLogin ?? true;
/**
* Show alert when user tries to modify settings with login disabled
*/
const showLoginRequiredAlert = () => {
alert({
alertType: 'warning',
title: t('admin.error', 'Error'),
body: t('admin.settings.loginRequired', 'Login mode must be enabled to modify admin settings'),
});
};
/**
* Validate that login is enabled before allowing action
* Returns true if login is enabled, false otherwise (and shows alert)
*/
const validateLoginEnabled = (): boolean => {
if (!loginEnabled) {
showLoginRequiredAlert();
return false;
}
return true;
};
/**
* Wrap an async handler to check login state before executing
*/
const withLoginCheck = <T extends (...args: any[]) => Promise<any>>(
handler: T
): T => {
return (async (...args: any[]) => {
if (!validateLoginEnabled()) {
return;
}
return handler(...args);
}) as T;
};
/**
* Get styles for disabled inputs (cursor not-allowed)
*/
const getDisabledStyles = () => {
if (!loginEnabled) {
return {
input: { cursor: 'not-allowed' },
track: { cursor: 'not-allowed' },
thumb: { cursor: 'not-allowed' }
};
}
return undefined;
};
/**
* Wrap fetch function to skip API call when login disabled
*/
const withLoginCheckForFetch = <T extends (...args: any[]) => Promise<any>>(
fetchHandler: T,
skipWhenDisabled: boolean = true
): T => {
return (async (...args: any[]) => {
if (!loginEnabled && skipWhenDisabled) {
// Skip fetch when login disabled - component will use default/empty values
return;
}
return fetchHandler(...args);
}) as T;
};
return {
loginEnabled,
showLoginRequiredAlert,
validateLoginEnabled,
withLoginCheck,
withLoginCheckForFetch,
getDisabledStyles,
};
}

View File

@ -31,4 +31,14 @@ export const accountService = {
formData.append('newPassword', newPassword); formData.append('newPassword', newPassword);
await apiClient.post('/api/v1/user/change-password', formData); await apiClient.post('/api/v1/user/change-password', formData);
}, },
/**
* Change user password on first login (resets firstLogin flag)
*/
async changePasswordOnLogin(currentPassword: string, newPassword: string): Promise<void> {
const formData = new FormData();
formData.append('currentPassword', currentPassword);
formData.append('newPassword', newPassword);
await apiClient.post('/api/v1/user/change-password-on-login', formData);
},
}; };

View File

@ -8,10 +8,11 @@ import TeamsSection from '@app/components/shared/config/configSections/TeamsSect
*/ */
export const createConfigNavSections = ( export const createConfigNavSections = (
isAdmin: boolean = false, isAdmin: boolean = false,
runningEE: boolean = false runningEE: boolean = false,
loginEnabled: boolean = true
): ConfigNavSection[] => { ): ConfigNavSection[] => {
// Get the core sections // Get the core sections
const sections = createCoreConfigNavSections(isAdmin, runningEE); const sections = createCoreConfigNavSections(isAdmin, runningEE, loginEnabled);
// Add Workspace section if user is admin // Add Workspace section if user is admin
if (isAdmin) { if (isAdmin) {