mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
Merge branch 'V2' into feature/v2/saved-signatures
This commit is contained in:
commit
234095f905
74
.dockerignore
Normal file
74
.dockerignore
Normal file
@ -0,0 +1,74 @@
|
||||
# Node modules and build artifacts
|
||||
node_modules
|
||||
frontend/node_modules
|
||||
frontend/dist
|
||||
frontend/build
|
||||
frontend/.vite
|
||||
frontend/.tauri
|
||||
|
||||
# Gradle build artifacts
|
||||
.gradle
|
||||
build
|
||||
bin
|
||||
target
|
||||
out
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# IDE
|
||||
.vscode
|
||||
.idea
|
||||
*.iml
|
||||
*.iws
|
||||
*.ipr
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Java compiled files
|
||||
*.class
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
|
||||
# Test reports
|
||||
test-results
|
||||
coverage
|
||||
|
||||
# Docker
|
||||
docker-compose.override.yml
|
||||
.dockerignore
|
||||
|
||||
# Temporary files
|
||||
tmp
|
||||
temp
|
||||
*.tmp
|
||||
*.swp
|
||||
*~
|
||||
|
||||
# Runtime database and config files (locked by running app)
|
||||
app/core/configs/**
|
||||
stirling/**
|
||||
stirling-pdf-DB*.mv.db
|
||||
stirling-pdf-DB*.trace.db
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
!README.md
|
||||
docs
|
||||
|
||||
# CI/CD
|
||||
.github
|
||||
.gitlab-ci.yml
|
||||
13
.github/workflows/tauri-build.yml
vendored
13
.github/workflows/tauri-build.yml
vendored
@ -17,18 +17,11 @@ on:
|
||||
branches: [main, V2]
|
||||
paths:
|
||||
- 'frontend/src-tauri/**'
|
||||
- 'frontend/src/**'
|
||||
- 'frontend/package.json'
|
||||
- 'frontend/package-lock.json'
|
||||
- 'frontend/src/desktop/**'
|
||||
- 'frontend/tsconfig.desktop.json'
|
||||
- '.github/workflows/tauri-build.yml'
|
||||
push:
|
||||
branches: [main, V2]
|
||||
paths:
|
||||
- 'frontend/src-tauri/**'
|
||||
- 'frontend/src/**'
|
||||
- 'frontend/package.json'
|
||||
- 'frontend/package-lock.json'
|
||||
- '.github/workflows/tauri-build.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@ -344,4 +337,4 @@ jobs:
|
||||
echo "❌ Some Tauri builds failed."
|
||||
echo "Please check the logs and fix any issues."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
@ -505,10 +505,19 @@ public class ApplicationProperties {
|
||||
public static class Ui {
|
||||
private String appNameNavbar;
|
||||
private List<String> languages;
|
||||
private String logoStyle = "classic"; // Options: "classic" (default) or "modern"
|
||||
|
||||
public String getAppNameNavbar() {
|
||||
return appNameNavbar != null && !appNameNavbar.trim().isEmpty() ? appNameNavbar : null;
|
||||
}
|
||||
|
||||
public String getLogoStyle() {
|
||||
// Validate and return either "modern" or "classic"
|
||||
if ("modern".equalsIgnoreCase(logoStyle)) {
|
||||
return "modern";
|
||||
}
|
||||
return "classic"; // default
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
|
||||
@ -75,6 +75,9 @@ public class RequestUriUtils {
|
||||
|| trimmedUri.startsWith("/api/v1/auth/login")
|
||||
|| trimmedUri.startsWith("/api/v1/auth/refresh")
|
||||
|| trimmedUri.startsWith("/api/v1/auth/logout")
|
||||
|| trimmedUri.startsWith(
|
||||
"/api/v1/proprietary/ui-data/login") // Login page config (SSO providers +
|
||||
// enableLogin)
|
||||
|| trimmedUri.startsWith("/v1/api-docs")
|
||||
|| trimmedUri.startsWith("/api/v1/invite/validate")
|
||||
|| trimmedUri.startsWith("/api/v1/invite/accept")
|
||||
|
||||
@ -10,6 +10,8 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.SPDF.config.InitialSetup;
|
||||
import stirling.software.common.annotations.api.ConfigApi;
|
||||
@ -20,6 +22,7 @@ import stirling.software.common.service.UserServiceInterface;
|
||||
|
||||
@ConfigApi
|
||||
@Hidden
|
||||
@Slf4j
|
||||
public class ConfigController {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
@ -59,9 +62,15 @@ public class ConfigController {
|
||||
// Extract values from ApplicationProperties
|
||||
configData.put("appNameNavbar", applicationProperties.getUi().getAppNameNavbar());
|
||||
configData.put("languages", applicationProperties.getUi().getLanguages());
|
||||
configData.put("logoStyle", applicationProperties.getUi().getLogoStyle());
|
||||
|
||||
// Security settings
|
||||
configData.put("enableLogin", applicationProperties.getSecurity().getEnableLogin());
|
||||
// enableLogin requires both the config flag AND proprietary features to be loaded
|
||||
// If userService is null, proprietary module isn't loaded
|
||||
// (DISABLE_ADDITIONAL_FEATURES=true or DOCKER_ENABLE_SECURITY=false)
|
||||
boolean enableLogin =
|
||||
applicationProperties.getSecurity().getEnableLogin() && userService != null;
|
||||
configData.put("enableLogin", enableLogin);
|
||||
|
||||
// Mail settings - check both SMTP enabled AND invites enabled
|
||||
boolean smtpEnabled = applicationProperties.getMail().isEnabled();
|
||||
|
||||
@ -176,6 +176,7 @@ system:
|
||||
|
||||
ui:
|
||||
appNameNavbar: '' # name displayed on the navigation bar
|
||||
logoStyle: classic # Options: 'classic' (default - classic S icon) or 'modern' (minimalist logo)
|
||||
languages: [] # If empty, all languages are enabled. To display only German and Polish ["de_DE", "pl_PL"]. British English is always enabled.
|
||||
|
||||
endpoints:
|
||||
|
||||
@ -116,6 +116,10 @@ public class ProprietaryUIDataController {
|
||||
LoginData data = new LoginData();
|
||||
Map<String, String> providerList = new HashMap<>();
|
||||
Security securityProps = applicationProperties.getSecurity();
|
||||
|
||||
// Add enableLogin flag so frontend doesn't need to call /app-config
|
||||
data.setEnableLogin(securityProps.getEnableLogin());
|
||||
|
||||
OAUTH2 oauth = securityProps.getOauth2();
|
||||
|
||||
if (oauth != null && oauth.getEnabled()) {
|
||||
@ -448,6 +452,7 @@ public class ProprietaryUIDataController {
|
||||
|
||||
@Data
|
||||
public static class LoginData {
|
||||
private Boolean enableLogin;
|
||||
private Map<String, String> providerList;
|
||||
private String loginMethod;
|
||||
private boolean altLogin;
|
||||
|
||||
@ -223,7 +223,8 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
|| trimmedUri.startsWith("/saml2")
|
||||
|| trimmedUri.startsWith("/api/v1/auth/login")
|
||||
|| trimmedUri.startsWith("/api/v1/auth/refresh")
|
||||
|| trimmedUri.startsWith("/api/v1/auth/logout");
|
||||
|| trimmedUri.startsWith("/api/v1/auth/logout")
|
||||
|| trimmedUri.startsWith("/api/v1/proprietary/ui-data/login");
|
||||
}
|
||||
|
||||
private enum UserLoginType {
|
||||
|
||||
@ -57,7 +57,7 @@ repositories {
|
||||
|
||||
allprojects {
|
||||
group = 'stirling.software'
|
||||
version = '1.4.0'
|
||||
version = '2.0.0'
|
||||
|
||||
configurations.configureEach {
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
# Run nginx as non-root user
|
||||
pid /tmp/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
BIN
frontend/public/branding/old/favicon.ico
Normal file
BIN
frontend/public/branding/old/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
frontend/public/branding/old/favicon.png
Normal file
BIN
frontend/public/branding/old/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.7 KiB |
1
frontend/public/branding/old/favicon.svg
Normal file
1
frontend/public/branding/old/favicon.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" id="Layer_1" x="0" y="0" version="1.1" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve"><defs id="defs173"><linearGradient id="XMLID_5_" x1="304.496" x2="316.036" y1="422.91" y2="326.263" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#dcf1f3" id="stop156"/><stop offset="1" style="stop-color:#c2c2c9" id="stop158"/></linearGradient></defs><style id="style150" type="text/css">.st1{fill:#c02223}.st2{fill:#882425}.st3{fill:url(#XMLID_5_)}.st4{fill:url(#XMLID_7_)}</style><g id="XMLID_4_"><path id="XMLID_131_" d="M 347.01402,14.355825 98.978019,69.02261 C 73.825483,74.547445 55.942464,96.792175 55.942464,122.52628 v 315.06096 c 0,22.39012 16.719895,41.14548 38.819234,43.76251 L 224.8861,498.36042 339.48636,384.26465 455.76603,265.15425 453.73057,84.870162 C 453.43979,62.916214 433.08513,46.632491 411.71274,51.284984 l -28.78729,6.251786 0.14539,-13.666697 C 383.36162,24.678542 365.62399,10.284894 347.01402,14.355825 Z" class="st1" style="stroke-width:1.45391"/><path id="XMLID_117_" d="m 383.21622,57.53677 v 285.8375 L 456.05681,265.00885 454.02135,78.763767 C 453.87595,59.863016 436.28372,45.905539 417.81914,49.97647 Z" class="st2" style="stroke-width:1.45391"/><polygon id="XMLID_18_" points="234.7 422.6 368.5 387.7 393.5 262.2" class="st3" style="fill:url(#XMLID_5_)" transform="matrix(1.4556308,0,0,1.4548265,-116.73161,-116.45231)"/><linearGradient id="XMLID_7_" x1="223.084" x2="241.417" y1="372.756" y2="114.557" gradientTransform="matrix(1.4539039,0,0,1.4539039,-116.19976,-116.20474)" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#dcf1f3" id="stop163"/><stop offset="1" style="stop-color:#c2c2c9" id="stop165"/></linearGradient><path id="XMLID_6_" d="m 282.89686,214.84917 c 0,0 -22.24473,-28.93269 -38.67384,-36.78377 -10.46811,-4.94327 -26.02489,-6.83335 -38.23768,-0.72695 -18.02841,9.0142 -19.91848,34.31213 -3.34397,44.34406 3.92553,2.47165 9.15959,4.50711 15.99294,6.10641 36.63838,8.43264 97.12077,25.87949 89.70587,96.10304 0,0 -4.21633,65.86185 -73.56753,73.42215 -12.2128,1.30851 -24.57098,0.43617 -36.493,-2.32625 -16.42911,-3.63476 -45.50719,-11.04967 -59.75545,-19.91849 l -2.61703,-75.16682 h 6.97875 c 0,0 13.81208,33.43978 53.06749,49.57812 7.26952,2.90781 15.26599,4.07093 22.97168,2.90781 9.74116,-1.45391 21.22699,-6.68796 25.87949,-22.53551 0,0 7.85108,-23.11707 -32.85823,-35.76604 -32.56744,-10.17733 -63.24481,-20.64543 -75.89378,-54.95757 -5.961,-16.28371 -6.97874,-34.31212 -2.90781,-51.61358 5.37944,-22.53551 20.79082,-54.23062 64.40794,-67.89732 0,0 57.28381,-15.55677 96.53922,5.52484 l -1.74468,89.70587 z" class="st4" style="fill:url(#XMLID_7_);stroke-width:1.45391"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@ -3230,6 +3230,7 @@
|
||||
"rememberme": "Remember me",
|
||||
"invalid": "Invalid username or password.",
|
||||
"locked": "Your account has been locked.",
|
||||
"sessionExpired": "Your session has expired. Please sign in again.",
|
||||
"signinTitle": "Please sign in",
|
||||
"ssoSignIn": "Login via Single Sign-on",
|
||||
"oAuth2AutoCreateDisabled": "OAUTH2 Auto-Create User Disabled",
|
||||
@ -3734,6 +3735,12 @@
|
||||
"saveSuccess": "Settings saved successfully",
|
||||
"save": "Save Changes",
|
||||
"restartRequired": "Restart Required",
|
||||
"loginRequired": "Login mode must be enabled to modify admin settings",
|
||||
"loginDisabled": {
|
||||
"title": "Login Mode Required",
|
||||
"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.",
|
||||
"readOnly": "The settings below show example values for reference. Enable login mode to view and edit actual configuration."
|
||||
},
|
||||
"restart": {
|
||||
"title": "Restart Required",
|
||||
"message": "Settings have been saved successfully. A server restart is required for the changes to take effect.",
|
||||
@ -3804,6 +3811,12 @@
|
||||
"description": "Default producer for PDF metadata"
|
||||
}
|
||||
},
|
||||
"logoStyle": {
|
||||
"label": "Logo Style",
|
||||
"description": "Choose between the modern minimalist logo or the classic S icon",
|
||||
"classic": "Classic",
|
||||
"modern": "Modern"
|
||||
},
|
||||
"customPaths": {
|
||||
"label": "Custom Paths",
|
||||
"description": "Configure custom file system paths for pipeline processing and external tools",
|
||||
|
||||
@ -143,16 +143,15 @@ const AppConfigModal: React.FC<AppConfigModalProps> = ({ opened, onClose }) => {
|
||||
<div
|
||||
key={item.key}
|
||||
onClick={() => {
|
||||
if (!isDisabled) {
|
||||
setActive(item.key);
|
||||
navigate(`/settings/${item.key}`);
|
||||
}
|
||||
// Allow navigation even when disabled - the content inside will be disabled
|
||||
setActive(item.key);
|
||||
navigate(`/settings/${item.key}`);
|
||||
}}
|
||||
className={`modal-nav-item ${isMobile ? 'mobile' : ''}`}
|
||||
style={{
|
||||
background: isActive ? colors.navItemActiveBg : 'transparent',
|
||||
opacity: isDisabled ? 0.5 : 1,
|
||||
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
||||
opacity: isDisabled ? 0.6 : 1,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
data-tour={`admin-${item.key}-nav`}
|
||||
>
|
||||
|
||||
@ -52,7 +52,7 @@ export default function FirstLoginModal({ opened, onPasswordChanged, username }:
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
await accountService.changePassword(currentPassword, newPassword);
|
||||
await accountService.changePasswordOnLogin(currentPassword, newPassword);
|
||||
|
||||
alert({
|
||||
alertType: 'success',
|
||||
|
||||
@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useFileHandler } from '@app/hooks/useFileHandler';
|
||||
import { useFilesModalContext } from '@app/contexts/FilesModalContext';
|
||||
import { BASE_PATH } from '@app/constants/app';
|
||||
import { useLogoPath } from '@app/hooks/useLogoPath';
|
||||
|
||||
const LandingPage = () => {
|
||||
const { addFiles } = useFileHandler();
|
||||
@ -14,6 +15,7 @@ const LandingPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { openFilesModal } = useFilesModalContext();
|
||||
const [isUploadHover, setIsUploadHover] = React.useState(false);
|
||||
const logoPath = useLogoPath();
|
||||
|
||||
const handleFileDrop = async (files: File[]) => {
|
||||
await addFiles(files);
|
||||
@ -72,7 +74,7 @@ const LandingPage = () => {
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={colorScheme === 'dark' ? `${BASE_PATH}/branding/StirlingPDFLogoNoTextDark.svg` : `${BASE_PATH}/branding/StirlingPDFLogoNoTextLight.svg`}
|
||||
src={logoPath}
|
||||
alt="Stirling PDF Logo"
|
||||
style={{
|
||||
height: 'auto',
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -2,18 +2,6 @@ import React from 'react';
|
||||
import { NavKey } from '@app/components/shared/config/types';
|
||||
import HotkeysSection from '@app/components/shared/config/configSections/HotkeysSection';
|
||||
import GeneralSection from '@app/components/shared/config/configSections/GeneralSection';
|
||||
import AdminGeneralSection from '@app/components/shared/config/configSections/AdminGeneralSection';
|
||||
import AdminSecuritySection from '@app/components/shared/config/configSections/AdminSecuritySection';
|
||||
import AdminConnectionsSection from '@app/components/shared/config/configSections/AdminConnectionsSection';
|
||||
import AdminPrivacySection from '@app/components/shared/config/configSections/AdminPrivacySection';
|
||||
import AdminDatabaseSection from '@app/components/shared/config/configSections/AdminDatabaseSection';
|
||||
import AdminAdvancedSection from '@app/components/shared/config/configSections/AdminAdvancedSection';
|
||||
import AdminLegalSection from '@app/components/shared/config/configSections/AdminLegalSection';
|
||||
import AdminPremiumSection from '@app/components/shared/config/configSections/AdminPremiumSection';
|
||||
import AdminFeaturesSection from '@app/components/shared/config/configSections/AdminFeaturesSection';
|
||||
import AdminEndpointsSection from '@app/components/shared/config/configSections/AdminEndpointsSection';
|
||||
import AdminAuditSection from '@app/components/shared/config/configSections/AdminAuditSection';
|
||||
import AdminUsageSection from '@app/components/shared/config/configSections/AdminUsageSection';
|
||||
|
||||
export interface ConfigNavItem {
|
||||
key: NavKey;
|
||||
@ -40,8 +28,8 @@ export interface ConfigColors {
|
||||
}
|
||||
|
||||
export const createConfigNavSections = (
|
||||
isAdmin: boolean = false,
|
||||
runningEE: boolean = false,
|
||||
_isAdmin: boolean = false,
|
||||
_runningEE: boolean = false,
|
||||
_loginEnabled: boolean = false
|
||||
): ConfigNavSection[] => {
|
||||
const sections: ConfigNavSection[] = [
|
||||
@ -64,112 +52,5 @@ export const createConfigNavSections = (
|
||||
},
|
||||
];
|
||||
|
||||
// Add Admin sections if user is admin
|
||||
if (isAdmin) {
|
||||
// Configuration
|
||||
sections.push({
|
||||
title: 'Configuration',
|
||||
items: [
|
||||
{
|
||||
key: 'adminGeneral',
|
||||
label: 'System Settings',
|
||||
icon: 'settings-rounded',
|
||||
component: <AdminGeneralSection />
|
||||
},
|
||||
{
|
||||
key: 'adminFeatures',
|
||||
label: 'Features',
|
||||
icon: 'extension-rounded',
|
||||
component: <AdminFeaturesSection />
|
||||
},
|
||||
{
|
||||
key: 'adminEndpoints',
|
||||
label: 'Endpoints',
|
||||
icon: 'api-rounded',
|
||||
component: <AdminEndpointsSection />
|
||||
},
|
||||
{
|
||||
key: 'adminDatabase',
|
||||
label: 'Database',
|
||||
icon: 'storage-rounded',
|
||||
component: <AdminDatabaseSection />
|
||||
},
|
||||
{
|
||||
key: 'adminAdvanced',
|
||||
label: 'Advanced',
|
||||
icon: 'tune-rounded',
|
||||
component: <AdminAdvancedSection />
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Security & Authentication
|
||||
sections.push({
|
||||
title: 'Security & Authentication',
|
||||
items: [
|
||||
{
|
||||
key: 'adminSecurity',
|
||||
label: 'Security',
|
||||
icon: 'shield-rounded',
|
||||
component: <AdminSecuritySection />
|
||||
},
|
||||
{
|
||||
key: 'adminConnections',
|
||||
label: 'Connections',
|
||||
icon: 'link-rounded',
|
||||
component: <AdminConnectionsSection />
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Licensing & Analytics
|
||||
sections.push({
|
||||
title: 'Licensing & Analytics',
|
||||
items: [
|
||||
{
|
||||
key: 'adminPremium',
|
||||
label: 'Premium',
|
||||
icon: 'star-rounded',
|
||||
component: <AdminPremiumSection />
|
||||
},
|
||||
{
|
||||
key: 'adminAudit',
|
||||
label: 'Audit',
|
||||
icon: 'fact-check-rounded',
|
||||
component: <AdminAuditSection />,
|
||||
disabled: !runningEE,
|
||||
disabledTooltip: 'Requires Enterprise license'
|
||||
},
|
||||
{
|
||||
key: 'adminUsage',
|
||||
label: 'Usage Analytics',
|
||||
icon: 'analytics-rounded',
|
||||
component: <AdminUsageSection />,
|
||||
disabled: !runningEE,
|
||||
disabledTooltip: 'Requires Enterprise license'
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Policies & Privacy
|
||||
sections.push({
|
||||
title: 'Policies & Privacy',
|
||||
items: [
|
||||
{
|
||||
key: 'adminLegal',
|
||||
label: 'Legal',
|
||||
icon: 'gavel-rounded',
|
||||
component: <AdminLegalSection />
|
||||
},
|
||||
{
|
||||
key: 'adminPrivacy',
|
||||
label: 'Privacy',
|
||||
icon: 'visibility-rounded',
|
||||
component: <AdminPrivacySection />
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return sections;
|
||||
};
|
||||
|
||||
@ -10,6 +10,7 @@ interface ProviderCardProps {
|
||||
settings?: Record<string, any>;
|
||||
onSave?: (settings: Record<string, any>) => void;
|
||||
onDisconnect?: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function ProviderCard({
|
||||
@ -18,6 +19,7 @@ export default function ProviderCard({
|
||||
settings = {},
|
||||
onSave,
|
||||
onDisconnect,
|
||||
disabled = false,
|
||||
}: ProviderCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
@ -39,6 +41,7 @@ export default function ProviderCard({
|
||||
};
|
||||
|
||||
const handleFieldChange = (key: string, value: any) => {
|
||||
if (disabled) return; // Block changes when disabled
|
||||
setLocalSettings((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
@ -63,6 +66,7 @@ export default function ProviderCard({
|
||||
<Switch
|
||||
checked={value || false}
|
||||
onChange={(e) => handleFieldChange(field.key, e.target.checked)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@ -76,6 +80,7 @@ export default function ProviderCard({
|
||||
placeholder={field.placeholder}
|
||||
value={value}
|
||||
onChange={(e) => handleFieldChange(field.key, e.target.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -88,6 +93,7 @@ export default function ProviderCard({
|
||||
placeholder={field.placeholder}
|
||||
value={value}
|
||||
onChange={(e) => handleFieldChange(field.key, e.target.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -100,6 +106,7 @@ export default function ProviderCard({
|
||||
placeholder={field.placeholder}
|
||||
value={value}
|
||||
onChange={(e) => handleFieldChange(field.key, e.target.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -174,11 +181,12 @@ export default function ProviderCard({
|
||||
color="red"
|
||||
size="sm"
|
||||
onClick={onDisconnect}
|
||||
disabled={disabled}
|
||||
>
|
||||
{t('admin.settings.connections.disconnect', 'Disconnect')}
|
||||
</Button>
|
||||
)}
|
||||
<Button size="sm" onClick={handleSave}>
|
||||
<Button size="sm" onClick={handleSave} disabled={disabled}>
|
||||
{t('admin.settings.save', 'Save Changes')}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
@ -8,6 +8,7 @@ import { ToolRegistryEntry } from '@app/data/toolsTaxonomy';
|
||||
import { ToolId } from '@app/types/toolId';
|
||||
import { useFocusTrap } from '@app/hooks/useFocusTrap';
|
||||
import { BASE_PATH } from '@app/constants/app';
|
||||
import { useLogoPath } from '@app/hooks/useLogoPath';
|
||||
import { Tooltip } from '@app/components/shared/Tooltip';
|
||||
import '@app/components/tools/ToolPanel.css';
|
||||
import { ToolPanelGeometry } from '@app/hooks/tools/useToolPanelGeometry';
|
||||
@ -51,9 +52,7 @@ const FullscreenToolSurface = ({
|
||||
useFocusTrap(surfaceRef, !isExiting);
|
||||
|
||||
const brandAltText = t("home.mobile.brandAlt", "Stirling PDF logo");
|
||||
const brandIconSrc = `${BASE_PATH}/branding/StirlingPDFLogoNoText${
|
||||
colorScheme === "dark" ? "Dark" : "Light"
|
||||
}.svg`;
|
||||
const brandIconSrc = useLogoPath();
|
||||
const brandTextSrc = `${BASE_PATH}/branding/StirlingPDFLogo${
|
||||
colorScheme === "dark" ? "White" : "Black"
|
||||
}Text.svg`;
|
||||
|
||||
@ -19,6 +19,7 @@ export interface AppConfig {
|
||||
serverPort?: number;
|
||||
appNameNavbar?: string;
|
||||
languages?: string[];
|
||||
logoStyle?: 'modern' | 'classic';
|
||||
enableLogin?: boolean;
|
||||
enableEmailInvites?: boolean;
|
||||
isAdmin?: boolean;
|
||||
|
||||
89
frontend/src/core/hooks/useLoginRequired.ts
Normal file
89
frontend/src/core/hooks/useLoginRequired.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
31
frontend/src/core/hooks/useLogoPath.ts
Normal file
31
frontend/src/core/hooks/useLogoPath.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useAppConfig } from '@app/contexts/AppConfigContext';
|
||||
import { useMantineColorScheme } from '@mantine/core';
|
||||
import { BASE_PATH } from '@app/constants/app';
|
||||
|
||||
/**
|
||||
* Hook to get the correct logo path based on app config (logo style) and theme (light/dark)
|
||||
*
|
||||
* Logo styles:
|
||||
* - classic: branding/old/favicon.svg (classic S logo - default)
|
||||
* - modern: StirlingPDFLogoNoText{Light|Dark}.svg (minimalist modern design)
|
||||
*
|
||||
* @returns The path to the appropriate logo SVG file
|
||||
*/
|
||||
export function useLogoPath(): string {
|
||||
const { config } = useAppConfig();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
|
||||
return useMemo(() => {
|
||||
const logoStyle = config?.logoStyle || 'classic';
|
||||
|
||||
if (logoStyle === 'classic') {
|
||||
// Classic logo (old favicon) - same for both light and dark modes
|
||||
return `${BASE_PATH}/branding/old/favicon.svg`;
|
||||
}
|
||||
|
||||
// Modern logo - different for light and dark modes
|
||||
const themeSuffix = colorScheme === 'dark' ? 'Dark' : 'Light';
|
||||
return `${BASE_PATH}/branding/StirlingPDFLogoNoText${themeSuffix}.svg`;
|
||||
}, [config?.logoStyle, colorScheme]);
|
||||
}
|
||||
@ -8,6 +8,7 @@ import { BASE_PATH } from "@app/constants/app";
|
||||
import { useBaseUrl } from "@app/hooks/useBaseUrl";
|
||||
import { useIsMobile } from "@app/hooks/useIsMobile";
|
||||
import { useAppConfig } from "@app/contexts/AppConfigContext";
|
||||
import { useLogoPath } from "@app/hooks/useLogoPath";
|
||||
import AppsIcon from '@mui/icons-material/AppsRounded';
|
||||
|
||||
import ToolPanel from "@app/components/tools/ToolPanel";
|
||||
@ -60,9 +61,7 @@ export default function HomePage() {
|
||||
}, [config]);
|
||||
|
||||
const brandAltText = t("home.mobile.brandAlt", "Stirling PDF logo");
|
||||
const brandIconSrc = `${BASE_PATH}/branding/StirlingPDFLogoNoText${
|
||||
colorScheme === "dark" ? "Dark" : "Light"
|
||||
}.svg`;
|
||||
const brandIconSrc = useLogoPath();
|
||||
const brandTextSrc = `${BASE_PATH}/branding/StirlingPDFLogo${
|
||||
colorScheme === "dark" ? "White" : "Black"
|
||||
}Text.svg`;
|
||||
|
||||
@ -31,4 +31,14 @@ export const accountService = {
|
||||
formData.append('newPassword', newPassword);
|
||||
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);
|
||||
},
|
||||
};
|
||||
|
||||
@ -92,6 +92,32 @@ export async function handleHttpError(error: any): Promise<boolean> {
|
||||
if (error?.config?.suppressErrorToast === true) {
|
||||
return false; // Don't show global toast, but continue rejection
|
||||
}
|
||||
|
||||
// Handle 401 authentication errors
|
||||
const status: number | undefined = error?.response?.status;
|
||||
if (status === 401) {
|
||||
const pathname = window.location.pathname;
|
||||
|
||||
// Check if we're already on an auth page
|
||||
const isAuthPage = pathname.includes('/login') ||
|
||||
pathname.includes('/signup') ||
|
||||
pathname.includes('/auth/') ||
|
||||
pathname.includes('/invite/');
|
||||
|
||||
// If not on auth page, redirect to login with expired session message
|
||||
if (!isAuthPage) {
|
||||
console.debug('[httpErrorHandler] 401 detected, redirecting to login');
|
||||
// Store the current location so we can redirect back after login
|
||||
const currentLocation = window.location.pathname + window.location.search;
|
||||
// Redirect to login with state
|
||||
window.location.href = `/login?expired=true&from=${encodeURIComponent(currentLocation)}`;
|
||||
return true; // Suppress toast since we're redirecting
|
||||
}
|
||||
|
||||
// On auth pages, suppress the toast (user is already trying to authenticate)
|
||||
console.debug('[httpErrorHandler] Suppressing 401 on auth page:', pathname);
|
||||
return true;
|
||||
}
|
||||
// Compute title/body (friendly) from the error object
|
||||
const { title, body } = extractAxiosErrorMessage(error);
|
||||
|
||||
@ -112,7 +138,6 @@ export async function handleHttpError(error: any): Promise<boolean> {
|
||||
|
||||
// 2) Generic-vs-special dedupe by endpoint
|
||||
const url: string | undefined = error?.config?.url;
|
||||
const status: number | undefined = error?.response?.status;
|
||||
const now = Date.now();
|
||||
const isSpecial =
|
||||
status === 422 ||
|
||||
|
||||
@ -251,7 +251,7 @@ class SpringAuthClient {
|
||||
* This redirects to the Spring OAuth2 authorization endpoint
|
||||
*/
|
||||
async signInWithOAuth(params: {
|
||||
provider: 'github' | 'google' | 'apple' | 'azure';
|
||||
provider: 'github' | 'google' | 'apple' | 'azure' | 'keycloak' | 'oidc';
|
||||
options?: { redirectTo?: string; queryParams?: Record<string, any> };
|
||||
}): Promise<{ error: AuthError | null }> {
|
||||
try {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { memo, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { BASE_PATH } from '@app/constants/app';
|
||||
|
||||
type ImageSlide = { src: string; alt?: string; cornerModelUrl?: string; title?: string; subtitle?: string; followMouseTilt?: boolean; tiltMaxDeg?: number }
|
||||
|
||||
export default function LoginRightCarousel({
|
||||
function LoginRightCarousel({
|
||||
imageSlides = [],
|
||||
showBackground = true,
|
||||
initialSeconds = 5,
|
||||
@ -157,3 +157,5 @@ export default function LoginRightCarousel({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(LoginRightCarousel);
|
||||
|
||||
@ -2,41 +2,181 @@ import React from 'react';
|
||||
import { createConfigNavSections as createCoreConfigNavSections, ConfigNavSection } from '@core/components/shared/config/configNavSections';
|
||||
import PeopleSection from '@app/components/shared/config/configSections/PeopleSection';
|
||||
import TeamsSection from '@app/components/shared/config/configSections/TeamsSection';
|
||||
import AdminGeneralSection from '@app/components/shared/config/configSections/AdminGeneralSection';
|
||||
import AdminSecuritySection from '@app/components/shared/config/configSections/AdminSecuritySection';
|
||||
import AdminConnectionsSection from '@app/components/shared/config/configSections/AdminConnectionsSection';
|
||||
import AdminPrivacySection from '@app/components/shared/config/configSections/AdminPrivacySection';
|
||||
import AdminDatabaseSection from '@app/components/shared/config/configSections/AdminDatabaseSection';
|
||||
import AdminAdvancedSection from '@app/components/shared/config/configSections/AdminAdvancedSection';
|
||||
import AdminLegalSection from '@app/components/shared/config/configSections/AdminLegalSection';
|
||||
import AdminPremiumSection from '@app/components/shared/config/configSections/AdminPremiumSection';
|
||||
import AdminFeaturesSection from '@app/components/shared/config/configSections/AdminFeaturesSection';
|
||||
import AdminEndpointsSection from '@app/components/shared/config/configSections/AdminEndpointsSection';
|
||||
import AdminAuditSection from '@app/components/shared/config/configSections/AdminAuditSection';
|
||||
import AdminUsageSection from '@app/components/shared/config/configSections/AdminUsageSection';
|
||||
import ApiKeys from '@app/components/shared/config/configSections/ApiKeys';
|
||||
|
||||
/**
|
||||
* Proprietary extension of createConfigNavSections that adds workspace sections
|
||||
* Proprietary extension of createConfigNavSections that adds all admin and workspace sections
|
||||
*/
|
||||
export const createConfigNavSections = (
|
||||
isAdmin: boolean = false,
|
||||
runningEE: boolean = false,
|
||||
loginEnabled: boolean = false
|
||||
): ConfigNavSection[] => {
|
||||
// Get the core sections
|
||||
const sections = createCoreConfigNavSections(isAdmin, runningEE);
|
||||
// Get the core sections (just Preferences)
|
||||
const sections = createCoreConfigNavSections(isAdmin, runningEE, loginEnabled);
|
||||
|
||||
// Add Workspace section if user is admin
|
||||
if (isAdmin) {
|
||||
const workspaceSection: ConfigNavSection = {
|
||||
// Add Admin sections if user is admin OR if login is disabled (but mark as disabled)
|
||||
if (isAdmin || !loginEnabled) {
|
||||
const requiresLogin = !loginEnabled;
|
||||
|
||||
// Workspace
|
||||
sections.push({
|
||||
title: 'Workspace',
|
||||
items: [
|
||||
{
|
||||
key: 'people',
|
||||
label: 'People',
|
||||
icon: 'group-rounded',
|
||||
component: <PeopleSection />
|
||||
component: <PeopleSection />,
|
||||
disabled: requiresLogin,
|
||||
disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined
|
||||
},
|
||||
{
|
||||
key: 'teams',
|
||||
label: 'Teams',
|
||||
icon: 'groups-rounded',
|
||||
component: <TeamsSection />
|
||||
component: <TeamsSection />,
|
||||
disabled: requiresLogin,
|
||||
disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
// Insert workspace section after Preferences (at index 1)
|
||||
sections.splice(1, 0, workspaceSection);
|
||||
// Configuration
|
||||
sections.push({
|
||||
title: 'Configuration',
|
||||
items: [
|
||||
{
|
||||
key: 'adminGeneral',
|
||||
label: 'System Settings',
|
||||
icon: 'settings-rounded',
|
||||
component: <AdminGeneralSection />,
|
||||
disabled: requiresLogin,
|
||||
disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined
|
||||
},
|
||||
{
|
||||
key: 'adminFeatures',
|
||||
label: 'Features',
|
||||
icon: 'extension-rounded',
|
||||
component: <AdminFeaturesSection />,
|
||||
disabled: requiresLogin,
|
||||
disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined
|
||||
},
|
||||
{
|
||||
key: 'adminEndpoints',
|
||||
label: 'Endpoints',
|
||||
icon: 'api-rounded',
|
||||
component: <AdminEndpointsSection />,
|
||||
disabled: requiresLogin,
|
||||
disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined
|
||||
},
|
||||
{
|
||||
key: 'adminDatabase',
|
||||
label: 'Database',
|
||||
icon: 'storage-rounded',
|
||||
component: <AdminDatabaseSection />,
|
||||
disabled: requiresLogin,
|
||||
disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined
|
||||
},
|
||||
{
|
||||
key: 'adminAdvanced',
|
||||
label: 'Advanced',
|
||||
icon: 'tune-rounded',
|
||||
component: <AdminAdvancedSection />,
|
||||
disabled: requiresLogin,
|
||||
disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Security & Authentication
|
||||
sections.push({
|
||||
title: 'Security & Authentication',
|
||||
items: [
|
||||
{
|
||||
key: 'adminSecurity',
|
||||
label: 'Security',
|
||||
icon: 'shield-rounded',
|
||||
component: <AdminSecuritySection />,
|
||||
disabled: requiresLogin,
|
||||
disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined
|
||||
},
|
||||
{
|
||||
key: 'adminConnections',
|
||||
label: 'Connections',
|
||||
icon: 'link-rounded',
|
||||
component: <AdminConnectionsSection />,
|
||||
disabled: requiresLogin,
|
||||
disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Licensing & Analytics
|
||||
sections.push({
|
||||
title: 'Licensing & Analytics',
|
||||
items: [
|
||||
{
|
||||
key: 'adminPremium',
|
||||
label: 'Premium',
|
||||
icon: 'star-rounded',
|
||||
component: <AdminPremiumSection />,
|
||||
disabled: requiresLogin,
|
||||
disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined
|
||||
},
|
||||
{
|
||||
key: 'adminAudit',
|
||||
label: 'Audit',
|
||||
icon: 'fact-check-rounded',
|
||||
component: <AdminAuditSection />,
|
||||
disabled: !runningEE || requiresLogin,
|
||||
disabledTooltip: requiresLogin ? 'Enable login mode first' : 'Requires Enterprise license'
|
||||
},
|
||||
{
|
||||
key: 'adminUsage',
|
||||
label: 'Usage Analytics',
|
||||
icon: 'analytics-rounded',
|
||||
component: <AdminUsageSection />,
|
||||
disabled: !runningEE || requiresLogin,
|
||||
disabledTooltip: requiresLogin ? 'Enable login mode first' : 'Requires Enterprise license'
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Policies & Privacy
|
||||
sections.push({
|
||||
title: 'Policies & Privacy',
|
||||
items: [
|
||||
{
|
||||
key: 'adminLegal',
|
||||
label: 'Legal',
|
||||
icon: 'gavel-rounded',
|
||||
component: <AdminLegalSection />,
|
||||
disabled: requiresLogin,
|
||||
disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined
|
||||
},
|
||||
{
|
||||
key: 'adminPrivacy',
|
||||
label: 'Privacy',
|
||||
icon: 'visibility-rounded',
|
||||
component: <AdminPrivacySection />,
|
||||
disabled: requiresLogin,
|
||||
disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// Add Developer section if login is enabled
|
||||
|
||||
@ -7,6 +7,8 @@ import { useRestartServer } from '@app/components/shared/config/useRestartServer
|
||||
import { useAdminSettings } from '@app/hooks/useAdminSettings';
|
||||
import PendingBadge from '@app/components/shared/config/PendingBadge';
|
||||
import apiClient from '@app/services/apiClient';
|
||||
import { useLoginRequired } from '@app/hooks/useLoginRequired';
|
||||
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
|
||||
|
||||
interface AdvancedSettingsData {
|
||||
enableAlphaFunctionality?: boolean;
|
||||
@ -55,6 +57,7 @@ interface AdvancedSettingsData {
|
||||
export default function AdminAdvancedSection() {
|
||||
const { t } = useTranslation();
|
||||
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
|
||||
const { loginEnabled, validateLoginEnabled, getDisabledStyles } = useLoginRequired();
|
||||
|
||||
const {
|
||||
settings,
|
||||
@ -165,10 +168,15 @@ export default function AdminAdvancedSection() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
if (loginEnabled) {
|
||||
fetchSettings();
|
||||
}
|
||||
}, [loginEnabled]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!validateLoginEnabled()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await saveSettings();
|
||||
showRestartModal();
|
||||
@ -181,7 +189,9 @@ export default function AdminAdvancedSection() {
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
const actualLoading = loginEnabled ? loading : false;
|
||||
|
||||
if (actualLoading) {
|
||||
return (
|
||||
<Stack align="center" justify="center" h={200}>
|
||||
<Loader size="lg" />
|
||||
@ -191,6 +201,7 @@ export default function AdminAdvancedSection() {
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<LoginRequiredBanner show={!loginEnabled} />
|
||||
<div>
|
||||
<Text fw={600} size="lg">{t('admin.settings.advanced.title', 'Advanced')}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
@ -213,7 +224,12 @@ export default function AdminAdvancedSection() {
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
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')} />
|
||||
</Group>
|
||||
@ -229,7 +245,12 @@ export default function AdminAdvancedSection() {
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
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')} />
|
||||
</Group>
|
||||
@ -245,7 +266,12 @@ export default function AdminAdvancedSection() {
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
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')} />
|
||||
</Group>
|
||||
@ -271,6 +297,7 @@ export default function AdminAdvancedSection() {
|
||||
onChange={(value) => setSettings({ ...settings, maxDPI: Number(value) })}
|
||||
min={0}
|
||||
max={3000}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -286,6 +313,7 @@ export default function AdminAdvancedSection() {
|
||||
value={settings.tessdataDir || ''}
|
||||
onChange={(e) => setSettings({ ...settings, tessdataDir: e.target.value })}
|
||||
placeholder="/usr/share/tessdata"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
@ -311,6 +339,7 @@ export default function AdminAdvancedSection() {
|
||||
tempFileManagement: { ...settings.tempFileManagement, baseTmpDir: e.target.value }
|
||||
})}
|
||||
placeholder="Default: java.io.tmpdir/stirling-pdf"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -324,6 +353,7 @@ export default function AdminAdvancedSection() {
|
||||
tempFileManagement: { ...settings.tempFileManagement, libreofficeDir: e.target.value }
|
||||
})}
|
||||
placeholder="Default: baseTmpDir/libreoffice"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -337,6 +367,7 @@ export default function AdminAdvancedSection() {
|
||||
tempFileManagement: { ...settings.tempFileManagement, systemTempDir: e.target.value }
|
||||
})}
|
||||
placeholder="System temp directory path"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -350,6 +381,7 @@ export default function AdminAdvancedSection() {
|
||||
tempFileManagement: { ...settings.tempFileManagement, prefix: e.target.value }
|
||||
})}
|
||||
placeholder="stirling-pdf-"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -364,6 +396,7 @@ export default function AdminAdvancedSection() {
|
||||
})}
|
||||
min={1}
|
||||
max={720}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -378,6 +411,7 @@ export default function AdminAdvancedSection() {
|
||||
})}
|
||||
min={1}
|
||||
max={1440}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -391,10 +425,15 @@ export default function AdminAdvancedSection() {
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
checked={settings.tempFileManagement?.startupCleanup ?? true}
|
||||
onChange={(e) => setSettings({
|
||||
...settings,
|
||||
tempFileManagement: { ...settings.tempFileManagement, startupCleanup: e.target.checked }
|
||||
})}
|
||||
onChange={(e) => {
|
||||
if (!loginEnabled) return;
|
||||
setSettings({
|
||||
...settings,
|
||||
tempFileManagement: { ...settings.tempFileManagement, startupCleanup: e.target.checked }
|
||||
});
|
||||
}}
|
||||
disabled={!loginEnabled}
|
||||
styles={getDisabledStyles()}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('tempFileManagement.startupCleanup')} />
|
||||
</Group>
|
||||
@ -410,10 +449,15 @@ export default function AdminAdvancedSection() {
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
checked={settings.tempFileManagement?.cleanupSystemTemp ?? false}
|
||||
onChange={(e) => setSettings({
|
||||
...settings,
|
||||
tempFileManagement: { ...settings.tempFileManagement, cleanupSystemTemp: e.target.checked }
|
||||
})}
|
||||
onChange={(e) => {
|
||||
if (!loginEnabled) return;
|
||||
setSettings({
|
||||
...settings,
|
||||
tempFileManagement: { ...settings.tempFileManagement, cleanupSystemTemp: e.target.checked }
|
||||
});
|
||||
}}
|
||||
disabled={!loginEnabled}
|
||||
styles={getDisabledStyles()}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('tempFileManagement.cleanupSystemTemp')} />
|
||||
</Group>
|
||||
@ -448,6 +492,7 @@ export default function AdminAdvancedSection() {
|
||||
})}
|
||||
min={1}
|
||||
max={100}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
|
||||
@ -462,6 +507,7 @@ export default function AdminAdvancedSection() {
|
||||
})}
|
||||
min={1}
|
||||
max={240}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</Stack>
|
||||
</Accordion.Panel>
|
||||
@ -485,6 +531,7 @@ export default function AdminAdvancedSection() {
|
||||
})}
|
||||
min={1}
|
||||
max={100}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
|
||||
@ -499,6 +546,7 @@ export default function AdminAdvancedSection() {
|
||||
})}
|
||||
min={1}
|
||||
max={240}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</Stack>
|
||||
</Accordion.Panel>
|
||||
@ -522,6 +570,7 @@ export default function AdminAdvancedSection() {
|
||||
})}
|
||||
min={1}
|
||||
max={100}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
|
||||
@ -536,6 +585,7 @@ export default function AdminAdvancedSection() {
|
||||
})}
|
||||
min={1}
|
||||
max={240}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</Stack>
|
||||
</Accordion.Panel>
|
||||
@ -559,6 +609,7 @@ export default function AdminAdvancedSection() {
|
||||
})}
|
||||
min={1}
|
||||
max={100}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
|
||||
@ -573,6 +624,7 @@ export default function AdminAdvancedSection() {
|
||||
})}
|
||||
min={1}
|
||||
max={240}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</Stack>
|
||||
</Accordion.Panel>
|
||||
@ -596,6 +648,7 @@ export default function AdminAdvancedSection() {
|
||||
})}
|
||||
min={1}
|
||||
max={100}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
|
||||
@ -610,6 +663,7 @@ export default function AdminAdvancedSection() {
|
||||
})}
|
||||
min={1}
|
||||
max={240}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</Stack>
|
||||
</Accordion.Panel>
|
||||
@ -633,6 +687,7 @@ export default function AdminAdvancedSection() {
|
||||
})}
|
||||
min={1}
|
||||
max={100}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
|
||||
@ -647,6 +702,7 @@ export default function AdminAdvancedSection() {
|
||||
})}
|
||||
min={1}
|
||||
max={240}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</Stack>
|
||||
</Accordion.Panel>
|
||||
@ -670,6 +726,7 @@ export default function AdminAdvancedSection() {
|
||||
})}
|
||||
min={1}
|
||||
max={100}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
|
||||
@ -684,6 +741,7 @@ export default function AdminAdvancedSection() {
|
||||
})}
|
||||
min={1}
|
||||
max={240}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</Stack>
|
||||
</Accordion.Panel>
|
||||
@ -707,6 +765,7 @@ export default function AdminAdvancedSection() {
|
||||
})}
|
||||
min={1}
|
||||
max={100}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
|
||||
@ -721,6 +780,7 @@ export default function AdminAdvancedSection() {
|
||||
})}
|
||||
min={1}
|
||||
max={240}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</Stack>
|
||||
</Accordion.Panel>
|
||||
@ -744,6 +804,7 @@ export default function AdminAdvancedSection() {
|
||||
})}
|
||||
min={1}
|
||||
max={100}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
|
||||
@ -758,6 +819,7 @@ export default function AdminAdvancedSection() {
|
||||
})}
|
||||
min={1}
|
||||
max={240}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</Stack>
|
||||
</Accordion.Panel>
|
||||
@ -781,6 +843,7 @@ export default function AdminAdvancedSection() {
|
||||
})}
|
||||
min={1}
|
||||
max={100}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
|
||||
@ -795,6 +858,7 @@ export default function AdminAdvancedSection() {
|
||||
})}
|
||||
min={1}
|
||||
max={240}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</Stack>
|
||||
</Accordion.Panel>
|
||||
@ -805,7 +869,7 @@ export default function AdminAdvancedSection() {
|
||||
|
||||
{/* Save Button */}
|
||||
<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')}
|
||||
</Button>
|
||||
</Group>
|
||||
@ -6,9 +6,12 @@ import AuditSystemStatus from '@app/components/shared/config/configSections/audi
|
||||
import AuditChartsSection from '@app/components/shared/config/configSections/audit/AuditChartsSection';
|
||||
import AuditEventsTable from '@app/components/shared/config/configSections/audit/AuditEventsTable';
|
||||
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 { t } = useTranslation();
|
||||
const { loginEnabled } = useLoginRequired();
|
||||
const [systemStatus, setSystemStatus] = useState<AuditStatus | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@ -27,10 +30,24 @@ const AdminAuditSection: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
fetchSystemStatus();
|
||||
}, []);
|
||||
if (loginEnabled) {
|
||||
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 (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', padding: '2rem 0' }}>
|
||||
<Loader size="lg" />
|
||||
@ -56,32 +73,33 @@ const AdminAuditSection: React.FC = () => {
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<LoginRequiredBanner show={!loginEnabled} />
|
||||
<AuditSystemStatus status={systemStatus} />
|
||||
|
||||
{systemStatus.enabled ? (
|
||||
<Tabs defaultValue="dashboard">
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="dashboard">
|
||||
<Tabs.Tab value="dashboard" disabled={!loginEnabled}>
|
||||
{t('audit.tabs.dashboard', 'Dashboard')}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="events">
|
||||
<Tabs.Tab value="events" disabled={!loginEnabled}>
|
||||
{t('audit.tabs.events', 'Audit Events')}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="export">
|
||||
<Tabs.Tab value="export" disabled={!loginEnabled}>
|
||||
{t('audit.tabs.export', 'Export')}
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Panel value="dashboard" pt="md">
|
||||
<AuditChartsSection />
|
||||
<AuditChartsSection loginEnabled={loginEnabled} />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="events" pt="md">
|
||||
<AuditEventsTable />
|
||||
<AuditEventsTable loginEnabled={loginEnabled} />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="export" pt="md">
|
||||
<AuditExportSection />
|
||||
<AuditExportSection loginEnabled={loginEnabled} />
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
) : (
|
||||
@ -12,6 +12,8 @@ import {
|
||||
Provider,
|
||||
} from '@app/components/shared/config/configSections/providerDefinitions';
|
||||
import apiClient from '@app/services/apiClient';
|
||||
import { useLoginRequired } from '@app/hooks/useLoginRequired';
|
||||
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
|
||||
|
||||
interface ConnectionsSettingsData {
|
||||
oauth2?: {
|
||||
@ -45,15 +47,10 @@ interface ConnectionsSettingsData {
|
||||
|
||||
export default function AdminConnectionsSection() {
|
||||
const { t } = useTranslation();
|
||||
const { loginEnabled, validateLoginEnabled, getDisabledStyles } = useLoginRequired();
|
||||
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
|
||||
|
||||
const {
|
||||
settings,
|
||||
setSettings,
|
||||
loading,
|
||||
fetchSettings,
|
||||
isFieldPending,
|
||||
} = useAdminSettings<ConnectionsSettingsData>({
|
||||
const adminSettings = useAdminSettings<ConnectionsSettingsData>({
|
||||
sectionName: 'connections',
|
||||
fetchTransformer: async () => {
|
||||
// Fetch security settings (oauth2, saml2)
|
||||
@ -106,57 +103,75 @@ export default function AdminConnectionsSection() {
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
settings,
|
||||
setSettings,
|
||||
loading,
|
||||
fetchSettings,
|
||||
isFieldPending,
|
||||
} = adminSettings;
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
if (loginEnabled) {
|
||||
fetchSettings();
|
||||
}
|
||||
}, [loginEnabled, fetchSettings]);
|
||||
|
||||
// Override loading state when login is disabled
|
||||
const actualLoading = loginEnabled ? loading : false;
|
||||
|
||||
const isProviderConfigured = (provider: Provider): boolean => {
|
||||
if (provider.id === 'saml2') {
|
||||
return settings.saml2?.enabled === true;
|
||||
return settings?.saml2?.enabled === true;
|
||||
}
|
||||
|
||||
if (provider.id === 'smtp') {
|
||||
return settings.mail?.enabled === true;
|
||||
return settings?.mail?.enabled === true;
|
||||
}
|
||||
|
||||
if (provider.id === 'oauth2-generic') {
|
||||
return settings.oauth2?.enabled === true;
|
||||
return settings?.oauth2?.enabled === true;
|
||||
}
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
const getProviderSettings = (provider: Provider): Record<string, any> => {
|
||||
if (provider.id === 'saml2') {
|
||||
return settings.saml2 || {};
|
||||
return settings?.saml2 || {};
|
||||
}
|
||||
|
||||
if (provider.id === 'smtp') {
|
||||
return settings.mail || {};
|
||||
return settings?.mail || {};
|
||||
}
|
||||
|
||||
if (provider.id === 'oauth2-generic') {
|
||||
// Generic OAuth2 settings are at the root oauth2 level
|
||||
return {
|
||||
enabled: settings.oauth2?.enabled,
|
||||
provider: settings.oauth2?.provider,
|
||||
issuer: settings.oauth2?.issuer,
|
||||
clientId: settings.oauth2?.clientId,
|
||||
clientSecret: settings.oauth2?.clientSecret,
|
||||
scopes: settings.oauth2?.scopes,
|
||||
useAsUsername: settings.oauth2?.useAsUsername,
|
||||
autoCreateUser: settings.oauth2?.autoCreateUser,
|
||||
blockRegistration: settings.oauth2?.blockRegistration,
|
||||
enabled: settings?.oauth2?.enabled,
|
||||
provider: settings?.oauth2?.provider,
|
||||
issuer: settings?.oauth2?.issuer,
|
||||
clientId: settings?.oauth2?.clientId,
|
||||
clientSecret: settings?.oauth2?.clientSecret,
|
||||
scopes: settings?.oauth2?.scopes,
|
||||
useAsUsername: settings?.oauth2?.useAsUsername,
|
||||
autoCreateUser: settings?.oauth2?.autoCreateUser,
|
||||
blockRegistration: settings?.oauth2?.blockRegistration,
|
||||
};
|
||||
}
|
||||
|
||||
// Specific OAuth2 provider settings
|
||||
return settings.oauth2?.client?.[provider.id] || {};
|
||||
return settings?.oauth2?.client?.[provider.id] || {};
|
||||
};
|
||||
|
||||
const handleProviderSave = async (provider: Provider, providerSettings: Record<string, any>) => {
|
||||
// Block save if login is disabled
|
||||
if (!validateLoginEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (provider.id === 'smtp') {
|
||||
// Mail settings use a different endpoint
|
||||
@ -218,7 +233,12 @@ export default function AdminConnectionsSection() {
|
||||
};
|
||||
|
||||
const handleProviderDisconnect = async (provider: Provider) => {
|
||||
try {
|
||||
// Block disconnect if login is disabled
|
||||
if (!validateLoginEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try{
|
||||
if (provider.id === 'smtp') {
|
||||
// Mail settings use a different endpoint
|
||||
const response = await apiClient.put('/api/v1/admin/settings/section/mail', { enabled: false });
|
||||
@ -271,7 +291,7 @@ export default function AdminConnectionsSection() {
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
if (actualLoading) {
|
||||
return (
|
||||
<Stack align="center" justify="center" h={200}>
|
||||
<Loader size="lg" />
|
||||
@ -280,9 +300,14 @@ export default function AdminConnectionsSection() {
|
||||
}
|
||||
|
||||
const handleSSOAutoLoginSave = async () => {
|
||||
// Block save if login is disabled
|
||||
if (!validateLoginEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const deltaSettings = {
|
||||
'premium.proFeatures.ssoAutoLogin': settings.ssoAutoLogin
|
||||
'premium.proFeatures.ssoAutoLogin': settings?.ssoAutoLogin
|
||||
};
|
||||
|
||||
const response = await apiClient.put('/api/v1/admin/settings', { settings: deltaSettings });
|
||||
@ -311,6 +336,8 @@ export default function AdminConnectionsSection() {
|
||||
|
||||
return (
|
||||
<Stack gap="xl">
|
||||
<LoginRequiredBanner show={!loginEnabled} />
|
||||
|
||||
{/* Header */}
|
||||
<div>
|
||||
<Text fw={600} size="lg">
|
||||
@ -341,11 +368,14 @@ export default function AdminConnectionsSection() {
|
||||
</div>
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
checked={settings.ssoAutoLogin || false}
|
||||
checked={settings?.ssoAutoLogin || false}
|
||||
onChange={(e) => {
|
||||
if (!loginEnabled) return; // Block change when login disabled
|
||||
setSettings({ ...settings, ssoAutoLogin: e.target.checked });
|
||||
handleSSOAutoLoginSave();
|
||||
}}
|
||||
disabled={!loginEnabled}
|
||||
styles={getDisabledStyles()}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('ssoAutoLogin')} />
|
||||
</Group>
|
||||
@ -369,6 +399,7 @@ export default function AdminConnectionsSection() {
|
||||
settings={getProviderSettings(provider)}
|
||||
onSave={(providerSettings) => handleProviderSave(provider, providerSettings)}
|
||||
onDisconnect={() => handleProviderDisconnect(provider)}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
@ -392,6 +423,7 @@ export default function AdminConnectionsSection() {
|
||||
provider={provider}
|
||||
isConfigured={false}
|
||||
onSave={(providerSettings) => handleProviderSave(provider, providerSettings)}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
@ -6,6 +6,8 @@ import RestartConfirmationModal from '@app/components/shared/config/RestartConfi
|
||||
import { useRestartServer } from '@app/components/shared/config/useRestartServer';
|
||||
import { useAdminSettings } from '@app/hooks/useAdminSettings';
|
||||
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';
|
||||
|
||||
interface DatabaseSettingsData {
|
||||
@ -21,6 +23,7 @@ interface DatabaseSettingsData {
|
||||
|
||||
export default function AdminDatabaseSection() {
|
||||
const { t } = useTranslation();
|
||||
const { loginEnabled, validateLoginEnabled, getDisabledStyles } = useLoginRequired();
|
||||
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
|
||||
|
||||
const {
|
||||
@ -78,10 +81,16 @@ export default function AdminDatabaseSection() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
if (loginEnabled) {
|
||||
fetchSettings();
|
||||
}
|
||||
}, [loginEnabled, fetchSettings]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!validateLoginEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await saveSettings();
|
||||
showRestartModal();
|
||||
@ -94,7 +103,10 @@ export default function AdminDatabaseSection() {
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
// Override loading state when login is disabled
|
||||
const actualLoading = loginEnabled ? loading : false;
|
||||
|
||||
if (actualLoading) {
|
||||
return (
|
||||
<Stack align="center" justify="center" h={200}>
|
||||
<Loader size="lg" />
|
||||
@ -104,6 +116,8 @@ export default function AdminDatabaseSection() {
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<LoginRequiredBanner show={!loginEnabled} />
|
||||
|
||||
<div>
|
||||
<Group justify="space-between" align="center">
|
||||
<div>
|
||||
@ -130,14 +144,19 @@ export default function AdminDatabaseSection() {
|
||||
</div>
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
checked={settings.enableCustomDatabase || false}
|
||||
onChange={(e) => setSettings({ ...settings, enableCustomDatabase: e.target.checked })}
|
||||
checked={settings?.enableCustomDatabase || false}
|
||||
onChange={(e) => {
|
||||
if (!loginEnabled) return;
|
||||
setSettings({ ...settings, enableCustomDatabase: e.target.checked });
|
||||
}}
|
||||
disabled={!loginEnabled}
|
||||
styles={getDisabledStyles()}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('enableCustomDatabase')} />
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
{settings.enableCustomDatabase && (
|
||||
{settings?.enableCustomDatabase && (
|
||||
<>
|
||||
<div>
|
||||
<TextInput
|
||||
@ -148,9 +167,10 @@ export default function AdminDatabaseSection() {
|
||||
</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.')}
|
||||
value={settings.customDatabaseUrl || ''}
|
||||
value={settings?.customDatabaseUrl || ''}
|
||||
onChange={(e) => setSettings({ ...settings, customDatabaseUrl: e.target.value })}
|
||||
placeholder="jdbc:postgresql://localhost:5432/postgres"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -163,7 +183,7 @@ export default function AdminDatabaseSection() {
|
||||
</Group>
|
||||
}
|
||||
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' })}
|
||||
data={[
|
||||
{ value: 'postgresql', label: 'PostgreSQL' },
|
||||
@ -171,6 +191,7 @@ export default function AdminDatabaseSection() {
|
||||
{ value: 'mysql', label: 'MySQL' },
|
||||
{ value: 'mariadb', label: 'MariaDB' }
|
||||
]}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -183,9 +204,10 @@ export default function AdminDatabaseSection() {
|
||||
</Group>
|
||||
}
|
||||
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 })}
|
||||
placeholder="localhost"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -198,10 +220,11 @@ export default function AdminDatabaseSection() {
|
||||
</Group>
|
||||
}
|
||||
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) })}
|
||||
min={1}
|
||||
max={65535}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -214,9 +237,10 @@ export default function AdminDatabaseSection() {
|
||||
</Group>
|
||||
}
|
||||
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 })}
|
||||
placeholder="postgres"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -229,9 +253,10 @@ export default function AdminDatabaseSection() {
|
||||
</Group>
|
||||
}
|
||||
description={t('admin.settings.database.username.description', 'Database authentication username')}
|
||||
value={settings.username || ''}
|
||||
value={settings?.username || ''}
|
||||
onChange={(e) => setSettings({ ...settings, username: e.target.value })}
|
||||
placeholder="postgres"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -244,9 +269,10 @@ export default function AdminDatabaseSection() {
|
||||
</Group>
|
||||
}
|
||||
description={t('admin.settings.database.password.description', 'Database authentication password')}
|
||||
value={settings.password || ''}
|
||||
value={settings?.password || ''}
|
||||
onChange={(e) => setSettings({ ...settings, password: e.target.value })}
|
||||
placeholder="••••••••"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@ -256,7 +282,7 @@ export default function AdminDatabaseSection() {
|
||||
|
||||
{/* Save Button */}
|
||||
<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')}
|
||||
</Button>
|
||||
</Group>
|
||||
@ -6,6 +6,8 @@ import RestartConfirmationModal from '@app/components/shared/config/RestartConfi
|
||||
import { useRestartServer } from '@app/components/shared/config/useRestartServer';
|
||||
import { useAdminSettings } from '@app/hooks/useAdminSettings';
|
||||
import PendingBadge from '@app/components/shared/config/PendingBadge';
|
||||
import { useLoginRequired } from '@app/hooks/useLoginRequired';
|
||||
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
|
||||
|
||||
interface EndpointsSettingsData {
|
||||
toRemove?: string[];
|
||||
@ -14,6 +16,7 @@ interface EndpointsSettingsData {
|
||||
|
||||
export default function AdminEndpointsSection() {
|
||||
const { t } = useTranslation();
|
||||
const { loginEnabled, validateLoginEnabled } = useLoginRequired();
|
||||
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
|
||||
|
||||
const {
|
||||
@ -29,10 +32,16 @@ export default function AdminEndpointsSection() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
if (loginEnabled) {
|
||||
fetchSettings();
|
||||
}
|
||||
}, [loginEnabled, fetchSettings]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!validateLoginEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await saveSettings();
|
||||
showRestartModal();
|
||||
@ -45,7 +54,10 @@ export default function AdminEndpointsSection() {
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
// Override loading state when login is disabled
|
||||
const actualLoading = loginEnabled ? loading : false;
|
||||
|
||||
if (actualLoading) {
|
||||
return (
|
||||
<Stack align="center" justify="center" h={200}>
|
||||
<Loader size="lg" />
|
||||
@ -102,6 +114,8 @@ export default function AdminEndpointsSection() {
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<LoginRequiredBanner show={!loginEnabled} />
|
||||
|
||||
<div>
|
||||
<Text fw={600} size="lg">{t('admin.settings.endpoints.title', 'API Endpoints')}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
@ -123,12 +137,16 @@ export default function AdminEndpointsSection() {
|
||||
}
|
||||
description={t('admin.settings.endpoints.toRemove.description', 'Select individual endpoints to disable')}
|
||||
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 }))}
|
||||
searchable
|
||||
clearable
|
||||
placeholder="Select endpoints to disable"
|
||||
comboboxProps={{ zIndex: 1400 }}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -142,12 +160,16 @@ export default function AdminEndpointsSection() {
|
||||
}
|
||||
description={t('admin.settings.endpoints.groupsToRemove.description', 'Select endpoint groups to disable')}
|
||||
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 }))}
|
||||
searchable
|
||||
clearable
|
||||
placeholder="Select groups to disable"
|
||||
comboboxProps={{ zIndex: 1400 }}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -160,7 +182,7 @@ export default function AdminEndpointsSection() {
|
||||
</Paper>
|
||||
|
||||
<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')}
|
||||
</Button>
|
||||
</Group>
|
||||
@ -7,6 +7,8 @@ import { useRestartServer } from '@app/components/shared/config/useRestartServer
|
||||
import { useAdminSettings } from '@app/hooks/useAdminSettings';
|
||||
import PendingBadge from '@app/components/shared/config/PendingBadge';
|
||||
import apiClient from '@app/services/apiClient';
|
||||
import { useLoginRequired } from '@app/hooks/useLoginRequired';
|
||||
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
|
||||
|
||||
interface FeaturesSettingsData {
|
||||
serverCertificate?: {
|
||||
@ -19,6 +21,7 @@ interface FeaturesSettingsData {
|
||||
|
||||
export default function AdminFeaturesSection() {
|
||||
const { t } = useTranslation();
|
||||
const { loginEnabled, validateLoginEnabled, getDisabledStyles } = useLoginRequired();
|
||||
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
|
||||
|
||||
const {
|
||||
@ -69,10 +72,15 @@ export default function AdminFeaturesSection() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
if (loginEnabled) {
|
||||
fetchSettings();
|
||||
}
|
||||
}, [loginEnabled]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!validateLoginEnabled()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await saveSettings();
|
||||
showRestartModal();
|
||||
@ -85,7 +93,9 @@ export default function AdminFeaturesSection() {
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
const actualLoading = loginEnabled ? loading : false;
|
||||
|
||||
if (actualLoading) {
|
||||
return (
|
||||
<Stack align="center" justify="center" h={200}>
|
||||
<Loader size="lg" />
|
||||
@ -95,6 +105,7 @@ export default function AdminFeaturesSection() {
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<LoginRequiredBanner show={!loginEnabled} />
|
||||
<div>
|
||||
<Text fw={600} size="lg">{t('admin.settings.features.title', 'Features')}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
@ -124,10 +135,15 @@ export default function AdminFeaturesSection() {
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
checked={settings.serverCertificate?.enabled ?? true}
|
||||
onChange={(e) => setSettings({
|
||||
...settings,
|
||||
serverCertificate: { ...settings.serverCertificate, enabled: e.target.checked }
|
||||
})}
|
||||
onChange={(e) => {
|
||||
if (!loginEnabled) return;
|
||||
setSettings({
|
||||
...settings,
|
||||
serverCertificate: { ...settings.serverCertificate, enabled: e.target.checked }
|
||||
});
|
||||
}}
|
||||
disabled={!loginEnabled}
|
||||
styles={getDisabledStyles()}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('serverCertificate.enabled')} />
|
||||
</Group>
|
||||
@ -148,6 +164,7 @@ export default function AdminFeaturesSection() {
|
||||
serverCertificate: { ...settings.serverCertificate, organizationName: e.target.value }
|
||||
})}
|
||||
placeholder="Stirling-PDF"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -167,6 +184,7 @@ export default function AdminFeaturesSection() {
|
||||
})}
|
||||
min={1}
|
||||
max={3650}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -180,10 +198,15 @@ export default function AdminFeaturesSection() {
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
checked={settings.serverCertificate?.regenerateOnStartup ?? false}
|
||||
onChange={(e) => setSettings({
|
||||
...settings,
|
||||
serverCertificate: { ...settings.serverCertificate, regenerateOnStartup: e.target.checked }
|
||||
})}
|
||||
onChange={(e) => {
|
||||
if (!loginEnabled) return;
|
||||
setSettings({
|
||||
...settings,
|
||||
serverCertificate: { ...settings.serverCertificate, regenerateOnStartup: e.target.checked }
|
||||
});
|
||||
}}
|
||||
disabled={!loginEnabled}
|
||||
styles={getDisabledStyles()}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('serverCertificate.regenerateOnStartup')} />
|
||||
</Group>
|
||||
@ -193,7 +216,7 @@ export default function AdminFeaturesSection() {
|
||||
|
||||
{/* Save Button */}
|
||||
<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')}
|
||||
</Button>
|
||||
</Group>
|
||||
@ -1,17 +1,20 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TextInput, Switch, Button, Stack, Paper, Text, Loader, Group, MultiSelect, Badge } from '@mantine/core';
|
||||
import { TextInput, Switch, Button, Stack, Paper, Text, Loader, Group, MultiSelect, Badge, SegmentedControl } from '@mantine/core';
|
||||
import { alert } from '@app/components/toast';
|
||||
import RestartConfirmationModal from '@app/components/shared/config/RestartConfirmationModal';
|
||||
import { useRestartServer } from '@app/components/shared/config/useRestartServer';
|
||||
import { useAdminSettings } from '@app/hooks/useAdminSettings';
|
||||
import PendingBadge from '@app/components/shared/config/PendingBadge';
|
||||
import apiClient from '@app/services/apiClient';
|
||||
import { useLoginRequired } from '@app/hooks/useLoginRequired';
|
||||
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
|
||||
|
||||
interface GeneralSettingsData {
|
||||
ui: {
|
||||
appNameNavbar?: string;
|
||||
languages?: string[];
|
||||
logoStyle?: 'modern' | 'classic';
|
||||
};
|
||||
system: {
|
||||
defaultLocale?: string;
|
||||
@ -40,6 +43,7 @@ interface GeneralSettingsData {
|
||||
|
||||
export default function AdminGeneralSection() {
|
||||
const { t } = useTranslation();
|
||||
const { loginEnabled, validateLoginEnabled } = useLoginRequired();
|
||||
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
|
||||
|
||||
const {
|
||||
@ -108,14 +112,15 @@ export default function AdminGeneralSection() {
|
||||
saveTransformer: (settings) => {
|
||||
const deltaSettings: Record<string, any> = {
|
||||
// UI settings
|
||||
'ui.appNameNavbar': settings.ui.appNameNavbar,
|
||||
'ui.languages': settings.ui.languages,
|
||||
'ui.appNameNavbar': settings.ui?.appNameNavbar,
|
||||
'ui.languages': settings.ui?.languages,
|
||||
'ui.logoStyle': settings.ui?.logoStyle,
|
||||
// System settings
|
||||
'system.defaultLocale': settings.system.defaultLocale,
|
||||
'system.showUpdate': settings.system.showUpdate,
|
||||
'system.showUpdateOnlyAdmin': settings.system.showUpdateOnlyAdmin,
|
||||
'system.customHTMLFiles': settings.system.customHTMLFiles,
|
||||
'system.fileUploadLimit': settings.system.fileUploadLimit,
|
||||
'system.defaultLocale': settings.system?.defaultLocale,
|
||||
'system.showUpdate': settings.system?.showUpdate,
|
||||
'system.showUpdateOnlyAdmin': settings.system?.showUpdateOnlyAdmin,
|
||||
'system.customHTMLFiles': settings.system?.customHTMLFiles,
|
||||
'system.fileUploadLimit': settings.system?.fileUploadLimit,
|
||||
// Premium custom metadata
|
||||
'premium.proFeatures.customMetadata.autoUpdateMetadata': settings.customMetadata?.autoUpdateMetadata,
|
||||
'premium.proFeatures.customMetadata.author': settings.customMetadata?.author,
|
||||
@ -124,10 +129,10 @@ export default function AdminGeneralSection() {
|
||||
};
|
||||
|
||||
if (settings.customPaths) {
|
||||
deltaSettings['system.customPaths.pipeline.watchedFoldersDir'] = settings.customPaths.pipeline?.watchedFoldersDir;
|
||||
deltaSettings['system.customPaths.pipeline.finishedFoldersDir'] = settings.customPaths.pipeline?.finishedFoldersDir;
|
||||
deltaSettings['system.customPaths.operations.weasyprint'] = settings.customPaths.operations?.weasyprint;
|
||||
deltaSettings['system.customPaths.operations.unoconvert'] = settings.customPaths.operations?.unoconvert;
|
||||
deltaSettings['system.customPaths.pipeline.watchedFoldersDir'] = settings.customPaths?.pipeline?.watchedFoldersDir;
|
||||
deltaSettings['system.customPaths.pipeline.finishedFoldersDir'] = settings.customPaths?.pipeline?.finishedFoldersDir;
|
||||
deltaSettings['system.customPaths.operations.weasyprint'] = settings.customPaths?.operations?.weasyprint;
|
||||
deltaSettings['system.customPaths.operations.unoconvert'] = settings.customPaths?.operations?.unoconvert;
|
||||
}
|
||||
|
||||
return {
|
||||
@ -138,10 +143,21 @@ export default function AdminGeneralSection() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
// Only fetch real settings if login is enabled
|
||||
if (loginEnabled) {
|
||||
fetchSettings();
|
||||
}
|
||||
}, [loginEnabled, fetchSettings]);
|
||||
|
||||
// Override loading state when login is disabled
|
||||
const actualLoading = loginEnabled ? loading : false;
|
||||
|
||||
const handleSave = async () => {
|
||||
// Block save if login is disabled
|
||||
if (!validateLoginEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await saveSettings();
|
||||
showRestartModal();
|
||||
@ -154,7 +170,7 @@ export default function AdminGeneralSection() {
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
if (actualLoading) {
|
||||
return (
|
||||
<Stack align="center" justify="center" h={200}>
|
||||
<Loader size="lg" />
|
||||
@ -164,6 +180,8 @@ export default function AdminGeneralSection() {
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<LoginRequiredBanner show={!loginEnabled} />
|
||||
|
||||
<div>
|
||||
<Text fw={600} size="lg">{t('admin.settings.general.title', 'System Settings')}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
@ -185,9 +203,55 @@ export default function AdminGeneralSection() {
|
||||
</Group>
|
||||
}
|
||||
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 } })}
|
||||
placeholder="Stirling PDF"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text size="sm" fw={500} mb={4}>
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.general.logoStyle.label', 'Logo Style')}</span>
|
||||
<PendingBadge show={isFieldPending('ui.logoStyle')} />
|
||||
</Group>
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mb="xs">
|
||||
{t('admin.settings.general.logoStyle.description', 'Choose between the modern minimalist logo or the classic S icon')}
|
||||
</Text>
|
||||
<SegmentedControl
|
||||
value={settings.ui?.logoStyle || 'classic'}
|
||||
onChange={(value) => setSettings({ ...settings, ui: { ...settings.ui, logoStyle: value as 'modern' | 'classic' } })}
|
||||
data={[
|
||||
{
|
||||
value: 'classic',
|
||||
label: (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.25rem 0' }}>
|
||||
<img
|
||||
src="/branding/old/favicon.svg"
|
||||
alt="Classic logo"
|
||||
style={{ width: '24px', height: '24px' }}
|
||||
/>
|
||||
<span>{t('admin.settings.general.logoStyle.classic', 'Classic')}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'modern',
|
||||
label: (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.25rem 0' }}>
|
||||
<img
|
||||
src="/branding/StirlingPDFLogoNoTextLight.svg"
|
||||
alt="Modern logo"
|
||||
style={{ width: '24px', height: '24px' }}
|
||||
/>
|
||||
<span>{t('admin.settings.general.logoStyle.modern', 'Modern')}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
]}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -200,7 +264,7 @@ export default function AdminGeneralSection() {
|
||||
</Group>
|
||||
}
|
||||
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 } })}
|
||||
data={[
|
||||
{ value: 'de_DE', label: 'Deutsch' },
|
||||
@ -218,6 +282,7 @@ export default function AdminGeneralSection() {
|
||||
clearable
|
||||
placeholder="Select languages"
|
||||
comboboxProps={{ zIndex: 1400 }}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -230,9 +295,10 @@ export default function AdminGeneralSection() {
|
||||
</Group>
|
||||
}
|
||||
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 } })}
|
||||
placeholder="en_US"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -245,9 +311,10 @@ export default function AdminGeneralSection() {
|
||||
</Group>
|
||||
}
|
||||
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 } })}
|
||||
placeholder="100MB"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -260,8 +327,9 @@ export default function AdminGeneralSection() {
|
||||
</div>
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
checked={settings.system.showUpdate || false}
|
||||
checked={ settings.system?.showUpdate || false}
|
||||
onChange={(e) => setSettings({ ...settings, system: { ...settings.system, showUpdate: e.target.checked } })}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('system.showUpdate')} />
|
||||
</Group>
|
||||
@ -276,8 +344,9 @@ export default function AdminGeneralSection() {
|
||||
</div>
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
checked={settings.system.showUpdateOnlyAdmin || false}
|
||||
checked={ settings.system?.showUpdateOnlyAdmin || false}
|
||||
onChange={(e) => setSettings({ ...settings, system: { ...settings.system, showUpdateOnlyAdmin: e.target.checked } })}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('system.showUpdateOnlyAdmin')} />
|
||||
</Group>
|
||||
@ -292,8 +361,9 @@ export default function AdminGeneralSection() {
|
||||
</div>
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
checked={settings.system.customHTMLFiles || false}
|
||||
checked={settings.system?.customHTMLFiles || false}
|
||||
onChange={(e) => setSettings({ ...settings, system: { ...settings.system, customHTMLFiles: e.target.checked } })}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('system.customHTMLFiles')} />
|
||||
</Group>
|
||||
@ -326,6 +396,7 @@ export default function AdminGeneralSection() {
|
||||
autoUpdateMetadata: e.target.checked
|
||||
}
|
||||
})}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('customMetadata.autoUpdateMetadata')} />
|
||||
</Group>
|
||||
@ -349,6 +420,7 @@ export default function AdminGeneralSection() {
|
||||
}
|
||||
})}
|
||||
placeholder="username"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -370,6 +442,7 @@ export default function AdminGeneralSection() {
|
||||
}
|
||||
})}
|
||||
placeholder="Stirling-PDF"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -391,6 +464,7 @@ export default function AdminGeneralSection() {
|
||||
}
|
||||
})}
|
||||
placeholder="Stirling-PDF"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
@ -429,6 +503,7 @@ export default function AdminGeneralSection() {
|
||||
}
|
||||
})}
|
||||
placeholder="/pipeline/watchedFolders"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -453,6 +528,7 @@ export default function AdminGeneralSection() {
|
||||
}
|
||||
})}
|
||||
placeholder="/pipeline/finishedFolders"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -479,6 +555,7 @@ export default function AdminGeneralSection() {
|
||||
}
|
||||
})}
|
||||
placeholder="/opt/venv/bin/weasyprint"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -503,6 +580,7 @@ export default function AdminGeneralSection() {
|
||||
}
|
||||
})}
|
||||
placeholder="/opt/venv/bin/unoconvert"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
@ -510,7 +588,7 @@ export default function AdminGeneralSection() {
|
||||
|
||||
{/* Save Button */}
|
||||
<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')}
|
||||
</Button>
|
||||
</Group>
|
||||
@ -7,6 +7,8 @@ import RestartConfirmationModal from '@app/components/shared/config/RestartConfi
|
||||
import { useRestartServer } from '@app/components/shared/config/useRestartServer';
|
||||
import { useAdminSettings } from '@app/hooks/useAdminSettings';
|
||||
import PendingBadge from '@app/components/shared/config/PendingBadge';
|
||||
import { useLoginRequired } from '@app/hooks/useLoginRequired';
|
||||
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
|
||||
|
||||
interface LegalSettingsData {
|
||||
termsAndConditions?: string;
|
||||
@ -18,6 +20,7 @@ interface LegalSettingsData {
|
||||
|
||||
export default function AdminLegalSection() {
|
||||
const { t } = useTranslation();
|
||||
const { loginEnabled, validateLoginEnabled } = useLoginRequired();
|
||||
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
|
||||
|
||||
const {
|
||||
@ -33,10 +36,15 @@ export default function AdminLegalSection() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
if (loginEnabled) {
|
||||
fetchSettings();
|
||||
}
|
||||
}, [loginEnabled]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!validateLoginEnabled()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await saveSettings();
|
||||
showRestartModal();
|
||||
@ -49,7 +57,9 @@ export default function AdminLegalSection() {
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
const actualLoading = loginEnabled ? loading : false;
|
||||
|
||||
if (actualLoading) {
|
||||
return (
|
||||
<Stack align="center" justify="center" h={200}>
|
||||
<Loader size="lg" />
|
||||
@ -59,6 +69,7 @@ export default function AdminLegalSection() {
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<LoginRequiredBanner show={!loginEnabled} />
|
||||
<div>
|
||||
<Text fw={600} size="lg">{t('admin.settings.legal.title', 'Legal Documents')}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
@ -95,6 +106,7 @@ export default function AdminLegalSection() {
|
||||
value={settings.termsAndConditions || ''}
|
||||
onChange={(e) => setSettings({ ...settings, termsAndConditions: e.target.value })}
|
||||
placeholder="https://example.com/terms"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -110,6 +122,7 @@ export default function AdminLegalSection() {
|
||||
value={settings.privacyPolicy || ''}
|
||||
onChange={(e) => setSettings({ ...settings, privacyPolicy: e.target.value })}
|
||||
placeholder="https://example.com/privacy"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -125,6 +138,7 @@ export default function AdminLegalSection() {
|
||||
value={settings.accessibilityStatement || ''}
|
||||
onChange={(e) => setSettings({ ...settings, accessibilityStatement: e.target.value })}
|
||||
placeholder="https://example.com/accessibility"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -140,6 +154,7 @@ export default function AdminLegalSection() {
|
||||
value={settings.cookiePolicy || ''}
|
||||
onChange={(e) => setSettings({ ...settings, cookiePolicy: e.target.value })}
|
||||
placeholder="https://example.com/cookies"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -155,13 +170,14 @@ export default function AdminLegalSection() {
|
||||
value={settings.impressum || ''}
|
||||
onChange={(e) => setSettings({ ...settings, impressum: e.target.value })}
|
||||
placeholder="https://example.com/impressum"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<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')}
|
||||
</Button>
|
||||
</Group>
|
||||
@ -7,6 +7,8 @@ import RestartConfirmationModal from '@app/components/shared/config/RestartConfi
|
||||
import { useRestartServer } from '@app/components/shared/config/useRestartServer';
|
||||
import { useAdminSettings } from '@app/hooks/useAdminSettings';
|
||||
import PendingBadge from '@app/components/shared/config/PendingBadge';
|
||||
import { useLoginRequired } from '@app/hooks/useLoginRequired';
|
||||
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
|
||||
|
||||
interface PremiumSettingsData {
|
||||
key?: string;
|
||||
@ -15,6 +17,7 @@ interface PremiumSettingsData {
|
||||
|
||||
export default function AdminPremiumSection() {
|
||||
const { t } = useTranslation();
|
||||
const { loginEnabled, validateLoginEnabled, getDisabledStyles } = useLoginRequired();
|
||||
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
|
||||
|
||||
const {
|
||||
@ -30,10 +33,15 @@ export default function AdminPremiumSection() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
if (loginEnabled) {
|
||||
fetchSettings();
|
||||
}
|
||||
}, [loginEnabled]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!validateLoginEnabled()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await saveSettings();
|
||||
showRestartModal();
|
||||
@ -46,7 +54,9 @@ export default function AdminPremiumSection() {
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
const actualLoading = loginEnabled ? loading : false;
|
||||
|
||||
if (actualLoading) {
|
||||
return (
|
||||
<Stack align="center" justify="center" h={200}>
|
||||
<Loader size="lg" />
|
||||
@ -56,6 +66,7 @@ export default function AdminPremiumSection() {
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<LoginRequiredBanner show={!loginEnabled} />
|
||||
<div>
|
||||
<Text fw={600} size="lg">{t('admin.settings.premium.title', 'Premium & Enterprise')}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
@ -98,6 +109,7 @@ export default function AdminPremiumSection() {
|
||||
value={settings.key || ''}
|
||||
onChange={(e) => setSettings({ ...settings, key: e.target.value })}
|
||||
placeholder="00000000-0000-0000-0000-000000000000"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -111,7 +123,12 @@ export default function AdminPremiumSection() {
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
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')} />
|
||||
</Group>
|
||||
@ -120,7 +137,7 @@ export default function AdminPremiumSection() {
|
||||
</Paper>
|
||||
|
||||
<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')}
|
||||
</Button>
|
||||
</Group>
|
||||
@ -6,6 +6,8 @@ import RestartConfirmationModal from '@app/components/shared/config/RestartConfi
|
||||
import { useRestartServer } from '@app/components/shared/config/useRestartServer';
|
||||
import { useAdminSettings } from '@app/hooks/useAdminSettings';
|
||||
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';
|
||||
|
||||
interface PrivacySettingsData {
|
||||
@ -16,6 +18,7 @@ interface PrivacySettingsData {
|
||||
|
||||
export default function AdminPrivacySection() {
|
||||
const { t } = useTranslation();
|
||||
const { loginEnabled, validateLoginEnabled, getDisabledStyles } = useLoginRequired();
|
||||
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
|
||||
|
||||
const {
|
||||
@ -76,10 +79,16 @@ export default function AdminPrivacySection() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
if (loginEnabled) {
|
||||
fetchSettings();
|
||||
}
|
||||
}, [loginEnabled, fetchSettings]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!validateLoginEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await saveSettings();
|
||||
showRestartModal();
|
||||
@ -92,7 +101,10 @@ export default function AdminPrivacySection() {
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
// Override loading state when login is disabled
|
||||
const actualLoading = loginEnabled ? loading : false;
|
||||
|
||||
if (actualLoading) {
|
||||
return (
|
||||
<Stack align="center" justify="center" h={200}>
|
||||
<Loader size="lg" />
|
||||
@ -102,6 +114,8 @@ export default function AdminPrivacySection() {
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<LoginRequiredBanner show={!loginEnabled} />
|
||||
|
||||
<div>
|
||||
<Text fw={600} size="lg">{t('admin.settings.privacy.title', 'Privacy')}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
@ -123,8 +137,13 @@ export default function AdminPrivacySection() {
|
||||
</div>
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
checked={settings.enableAnalytics || false}
|
||||
onChange={(e) => setSettings({ ...settings, enableAnalytics: e.target.checked })}
|
||||
checked={settings?.enableAnalytics || false}
|
||||
onChange={(e) => {
|
||||
if (!loginEnabled) return;
|
||||
setSettings({ ...settings, enableAnalytics: e.target.checked });
|
||||
}}
|
||||
disabled={!loginEnabled}
|
||||
styles={getDisabledStyles()}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('enableAnalytics')} />
|
||||
</Group>
|
||||
@ -139,8 +158,13 @@ export default function AdminPrivacySection() {
|
||||
</div>
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
checked={settings.metricsEnabled || false}
|
||||
onChange={(e) => setSettings({ ...settings, metricsEnabled: e.target.checked })}
|
||||
checked={settings?.metricsEnabled || false}
|
||||
onChange={(e) => {
|
||||
if (!loginEnabled) return;
|
||||
setSettings({ ...settings, metricsEnabled: e.target.checked });
|
||||
}}
|
||||
disabled={!loginEnabled}
|
||||
styles={getDisabledStyles()}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('metricsEnabled')} />
|
||||
</Group>
|
||||
@ -162,8 +186,13 @@ export default function AdminPrivacySection() {
|
||||
</div>
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
checked={settings.googleVisibility || false}
|
||||
onChange={(e) => setSettings({ ...settings, googleVisibility: e.target.checked })}
|
||||
checked={settings?.googleVisibility || false}
|
||||
onChange={(e) => {
|
||||
if (!loginEnabled) return;
|
||||
setSettings({ ...settings, googleVisibility: e.target.checked });
|
||||
}}
|
||||
disabled={!loginEnabled}
|
||||
styles={getDisabledStyles()}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('googleVisibility')} />
|
||||
</Group>
|
||||
@ -173,7 +202,7 @@ export default function AdminPrivacySection() {
|
||||
|
||||
{/* Save Button */}
|
||||
<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')}
|
||||
</Button>
|
||||
</Group>
|
||||
@ -8,6 +8,8 @@ import { useRestartServer } from '@app/components/shared/config/useRestartServer
|
||||
import { useAdminSettings } from '@app/hooks/useAdminSettings';
|
||||
import PendingBadge from '@app/components/shared/config/PendingBadge';
|
||||
import apiClient from '@app/services/apiClient';
|
||||
import { useLoginRequired } from '@app/hooks/useLoginRequired';
|
||||
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
|
||||
|
||||
interface SecuritySettingsData {
|
||||
enableLogin?: boolean;
|
||||
@ -44,6 +46,7 @@ interface SecuritySettingsData {
|
||||
|
||||
export default function AdminSecuritySection() {
|
||||
const { t } = useTranslation();
|
||||
const { loginEnabled, validateLoginEnabled } = useLoginRequired();
|
||||
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
|
||||
|
||||
const {
|
||||
@ -157,10 +160,20 @@ export default function AdminSecuritySection() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
if (loginEnabled) {
|
||||
fetchSettings();
|
||||
}
|
||||
}, [loginEnabled, fetchSettings]);
|
||||
|
||||
// Override loading state when login is disabled
|
||||
const actualLoading = loginEnabled ? loading : false;
|
||||
|
||||
const handleSave = async () => {
|
||||
// Block save if login is disabled
|
||||
if (!validateLoginEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await saveSettings();
|
||||
showRestartModal();
|
||||
@ -173,7 +186,7 @@ export default function AdminSecuritySection() {
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
if (actualLoading) {
|
||||
return (
|
||||
<Stack align="center" justify="center" h={200}>
|
||||
<Loader size="lg" />
|
||||
@ -183,6 +196,8 @@ export default function AdminSecuritySection() {
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<LoginRequiredBanner show={!loginEnabled} />
|
||||
|
||||
<div>
|
||||
<Text fw={600} size="lg">{t('admin.settings.security.title', 'Security')}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
@ -204,8 +219,9 @@ export default function AdminSecuritySection() {
|
||||
</div>
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
checked={settings.enableLogin || false}
|
||||
checked={settings?.enableLogin || false}
|
||||
onChange={(e) => setSettings({ ...settings, enableLogin: e.target.checked })}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('enableLogin')} />
|
||||
</Group>
|
||||
@ -215,7 +231,7 @@ export default function AdminSecuritySection() {
|
||||
<Select
|
||||
label={t('admin.settings.security.loginMethod.label', 'Login Method')}
|
||||
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' })}
|
||||
data={[
|
||||
{ value: 'all', label: t('admin.settings.security.loginMethod.all', 'All Methods') },
|
||||
@ -224,6 +240,7 @@ export default function AdminSecuritySection() {
|
||||
{ value: 'saml2', label: t('admin.settings.security.loginMethod.saml2', 'SAML2 Only') },
|
||||
]}
|
||||
comboboxProps={{ zIndex: 1400 }}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
{isFieldPending('loginMethod') && (
|
||||
<Group mt="xs">
|
||||
@ -241,10 +258,11 @@ export default function AdminSecuritySection() {
|
||||
</Group>
|
||||
}
|
||||
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) })}
|
||||
min={0}
|
||||
max={100}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -257,10 +275,11 @@ export default function AdminSecuritySection() {
|
||||
</Group>
|
||||
}
|
||||
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) })}
|
||||
min={0}
|
||||
max={1440}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -273,8 +292,9 @@ export default function AdminSecuritySection() {
|
||||
</div>
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
checked={settings.csrfDisabled || false}
|
||||
checked={settings?.csrfDisabled || false}
|
||||
onChange={(e) => setSettings({ ...settings, csrfDisabled: e.target.checked })}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('csrfDisabled')} />
|
||||
</Group>
|
||||
@ -308,8 +328,9 @@ export default function AdminSecuritySection() {
|
||||
</div>
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
checked={settings.jwt?.persistence || false}
|
||||
onChange={(e) => setSettings({ ...settings, jwt: { ...settings.jwt, persistence: e.target.checked } })}
|
||||
checked={settings?.jwt?.persistence || false}
|
||||
onChange={(e) => setSettings({ ...settings, jwt: { ...settings?.jwt, persistence: e.target.checked } })}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('jwt.persistence')} />
|
||||
</Group>
|
||||
@ -324,8 +345,9 @@ export default function AdminSecuritySection() {
|
||||
</div>
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
checked={settings.jwt?.enableKeyRotation || false}
|
||||
onChange={(e) => setSettings({ ...settings, jwt: { ...settings.jwt, enableKeyRotation: e.target.checked } })}
|
||||
checked={settings?.jwt?.enableKeyRotation || false}
|
||||
onChange={(e) => setSettings({ ...settings, jwt: { ...settings?.jwt, enableKeyRotation: e.target.checked } })}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('jwt.enableKeyRotation')} />
|
||||
</Group>
|
||||
@ -340,8 +362,9 @@ export default function AdminSecuritySection() {
|
||||
</div>
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
checked={settings.jwt?.enableKeyCleanup || false}
|
||||
onChange={(e) => setSettings({ ...settings, jwt: { ...settings.jwt, enableKeyCleanup: e.target.checked } })}
|
||||
checked={settings?.jwt?.enableKeyCleanup || false}
|
||||
onChange={(e) => setSettings({ ...settings, jwt: { ...settings?.jwt, enableKeyCleanup: e.target.checked } })}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('jwt.enableKeyCleanup')} />
|
||||
</Group>
|
||||
@ -356,10 +379,11 @@ export default function AdminSecuritySection() {
|
||||
</Group>
|
||||
}
|
||||
description={t('admin.settings.security.jwt.keyRetentionDays.description', 'Number of days to retain old JWT keys for verification')}
|
||||
value={settings.jwt?.keyRetentionDays || 7}
|
||||
onChange={(value) => setSettings({ ...settings, jwt: { ...settings.jwt, keyRetentionDays: Number(value) } })}
|
||||
value={settings?.jwt?.keyRetentionDays || 7}
|
||||
onChange={(value) => setSettings({ ...settings, jwt: { ...settings?.jwt, keyRetentionDays: Number(value) } })}
|
||||
min={1}
|
||||
max={365}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -372,8 +396,9 @@ export default function AdminSecuritySection() {
|
||||
</div>
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
checked={settings.jwt?.secureCookie || false}
|
||||
onChange={(e) => setSettings({ ...settings, jwt: { ...settings.jwt, secureCookie: e.target.checked } })}
|
||||
checked={settings?.jwt?.secureCookie || false}
|
||||
onChange={(e) => setSettings({ ...settings, jwt: { ...settings?.jwt, secureCookie: e.target.checked } })}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('jwt.secureCookie')} />
|
||||
</Group>
|
||||
@ -398,8 +423,9 @@ export default function AdminSecuritySection() {
|
||||
</div>
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
checked={settings.audit?.enabled || false}
|
||||
onChange={(e) => setSettings({ ...settings, audit: { ...settings.audit, enabled: e.target.checked } })}
|
||||
checked={settings?.audit?.enabled || false}
|
||||
onChange={(e) => setSettings({ ...settings, audit: { ...settings?.audit, enabled: e.target.checked } })}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('audit.enabled')} />
|
||||
</Group>
|
||||
@ -414,10 +440,11 @@ export default function AdminSecuritySection() {
|
||||
</Group>
|
||||
}
|
||||
description={t('admin.settings.security.audit.level.description', '0=OFF, 1=BASIC, 2=STANDARD, 3=VERBOSE')}
|
||||
value={settings.audit?.level || 2}
|
||||
onChange={(value) => setSettings({ ...settings, audit: { ...settings.audit, level: Number(value) } })}
|
||||
value={settings?.audit?.level || 2}
|
||||
onChange={(value) => setSettings({ ...settings, audit: { ...settings?.audit, level: Number(value) } })}
|
||||
min={0}
|
||||
max={3}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -430,10 +457,11 @@ export default function AdminSecuritySection() {
|
||||
</Group>
|
||||
}
|
||||
description={t('admin.settings.security.audit.retentionDays.description', 'Number of days to retain audit logs')}
|
||||
value={settings.audit?.retentionDays || 90}
|
||||
onChange={(value) => setSettings({ ...settings, audit: { ...settings.audit, retentionDays: Number(value) } })}
|
||||
value={settings?.audit?.retentionDays || 90}
|
||||
onChange={(value) => setSettings({ ...settings, audit: { ...settings?.audit, retentionDays: Number(value) } })}
|
||||
min={1}
|
||||
max={3650}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
@ -458,14 +486,15 @@ export default function AdminSecuritySection() {
|
||||
</div>
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
checked={settings.html?.urlSecurity?.enabled || false}
|
||||
checked={settings?.html?.urlSecurity?.enabled || false}
|
||||
onChange={(e) => setSettings({
|
||||
...settings,
|
||||
html: {
|
||||
...settings.html,
|
||||
urlSecurity: { ...settings.html?.urlSecurity, enabled: e.target.checked }
|
||||
...settings?.html,
|
||||
urlSecurity: { ...settings?.html?.urlSecurity, enabled: e.target.checked }
|
||||
}
|
||||
})}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('html.urlSecurity.enabled')} />
|
||||
</Group>
|
||||
@ -480,12 +509,12 @@ export default function AdminSecuritySection() {
|
||||
</Group>
|
||||
}
|
||||
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({
|
||||
...settings,
|
||||
html: {
|
||||
...settings.html,
|
||||
urlSecurity: { ...settings.html?.urlSecurity, level: value || 'MEDIUM' }
|
||||
...settings?.html,
|
||||
urlSecurity: { ...settings?.html?.urlSecurity, level: value || 'MEDIUM' }
|
||||
}
|
||||
})}
|
||||
data={[
|
||||
@ -494,6 +523,7 @@ export default function AdminSecuritySection() {
|
||||
{ value: 'OFF', label: t('admin.settings.security.htmlUrlSecurity.level.off', 'Off (No Restrictions)') },
|
||||
]}
|
||||
comboboxProps={{ zIndex: 1400 }}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -512,13 +542,13 @@ export default function AdminSecuritySection() {
|
||||
</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')}
|
||||
value={settings.html?.urlSecurity?.allowedDomains?.join('\n') || ''}
|
||||
value={settings?.html?.urlSecurity?.allowedDomains?.join('\n') || ''}
|
||||
onChange={(e) => setSettings({
|
||||
...settings,
|
||||
html: {
|
||||
...settings.html,
|
||||
...settings?.html,
|
||||
urlSecurity: {
|
||||
...settings.html?.urlSecurity,
|
||||
...settings?.html?.urlSecurity,
|
||||
allowedDomains: e.target.value ? e.target.value.split('\n').filter(d => d.trim()) : []
|
||||
}
|
||||
}
|
||||
@ -526,6 +556,7 @@ export default function AdminSecuritySection() {
|
||||
placeholder="cdn.example.com images.google.com"
|
||||
minRows={3}
|
||||
autosize
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -539,13 +570,13 @@ export default function AdminSecuritySection() {
|
||||
</Group>
|
||||
}
|
||||
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({
|
||||
...settings,
|
||||
html: {
|
||||
...settings.html,
|
||||
...settings?.html,
|
||||
urlSecurity: {
|
||||
...settings.html?.urlSecurity,
|
||||
...settings?.html?.urlSecurity,
|
||||
blockedDomains: e.target.value ? e.target.value.split('\n').filter(d => d.trim()) : []
|
||||
}
|
||||
}
|
||||
@ -553,6 +584,7 @@ export default function AdminSecuritySection() {
|
||||
placeholder="malicious.com evil.org"
|
||||
minRows={3}
|
||||
autosize
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -566,13 +598,13 @@ export default function AdminSecuritySection() {
|
||||
</Group>
|
||||
}
|
||||
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({
|
||||
...settings,
|
||||
html: {
|
||||
...settings.html,
|
||||
...settings?.html,
|
||||
urlSecurity: {
|
||||
...settings.html?.urlSecurity,
|
||||
...settings?.html?.urlSecurity,
|
||||
internalTlds: e.target.value ? e.target.value.split('\n').filter(d => d.trim()) : []
|
||||
}
|
||||
}
|
||||
@ -580,6 +612,7 @@ export default function AdminSecuritySection() {
|
||||
placeholder=".local .internal .corp .home"
|
||||
minRows={3}
|
||||
autosize
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -595,14 +628,15 @@ export default function AdminSecuritySection() {
|
||||
</div>
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
checked={settings.html?.urlSecurity?.blockPrivateNetworks || false}
|
||||
checked={settings?.html?.urlSecurity?.blockPrivateNetworks || false}
|
||||
onChange={(e) => setSettings({
|
||||
...settings,
|
||||
html: {
|
||||
...settings.html,
|
||||
urlSecurity: { ...settings.html?.urlSecurity, blockPrivateNetworks: e.target.checked }
|
||||
...settings?.html,
|
||||
urlSecurity: { ...settings?.html?.urlSecurity, blockPrivateNetworks: e.target.checked }
|
||||
}
|
||||
})}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('html.urlSecurity.blockPrivateNetworks')} />
|
||||
</Group>
|
||||
@ -617,14 +651,15 @@ export default function AdminSecuritySection() {
|
||||
</div>
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
checked={settings.html?.urlSecurity?.blockLocalhost || false}
|
||||
checked={settings?.html?.urlSecurity?.blockLocalhost || false}
|
||||
onChange={(e) => setSettings({
|
||||
...settings,
|
||||
html: {
|
||||
...settings.html,
|
||||
urlSecurity: { ...settings.html?.urlSecurity, blockLocalhost: e.target.checked }
|
||||
...settings?.html,
|
||||
urlSecurity: { ...settings?.html?.urlSecurity, blockLocalhost: e.target.checked }
|
||||
}
|
||||
})}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('html.urlSecurity.blockLocalhost')} />
|
||||
</Group>
|
||||
@ -639,14 +674,15 @@ export default function AdminSecuritySection() {
|
||||
</div>
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
checked={settings.html?.urlSecurity?.blockLinkLocal || false}
|
||||
checked={settings?.html?.urlSecurity?.blockLinkLocal || false}
|
||||
onChange={(e) => setSettings({
|
||||
...settings,
|
||||
html: {
|
||||
...settings.html,
|
||||
urlSecurity: { ...settings.html?.urlSecurity, blockLinkLocal: e.target.checked }
|
||||
...settings?.html,
|
||||
urlSecurity: { ...settings?.html?.urlSecurity, blockLinkLocal: e.target.checked }
|
||||
}
|
||||
})}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('html.urlSecurity.blockLinkLocal')} />
|
||||
</Group>
|
||||
@ -661,14 +697,15 @@ export default function AdminSecuritySection() {
|
||||
</div>
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
checked={settings.html?.urlSecurity?.blockCloudMetadata || false}
|
||||
checked={settings?.html?.urlSecurity?.blockCloudMetadata || false}
|
||||
onChange={(e) => setSettings({
|
||||
...settings,
|
||||
html: {
|
||||
...settings.html,
|
||||
urlSecurity: { ...settings.html?.urlSecurity, blockCloudMetadata: e.target.checked }
|
||||
...settings?.html,
|
||||
urlSecurity: { ...settings?.html?.urlSecurity, blockCloudMetadata: e.target.checked }
|
||||
}
|
||||
})}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('html.urlSecurity.blockCloudMetadata')} />
|
||||
</Group>
|
||||
@ -682,7 +719,7 @@ export default function AdminSecuritySection() {
|
||||
|
||||
{/* Save Button */}
|
||||
<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')}
|
||||
</Button>
|
||||
</Group>
|
||||
@ -14,9 +14,12 @@ import usageAnalyticsService, { EndpointStatisticsResponse } from '@app/services
|
||||
import UsageAnalyticsChart from '@app/components/shared/config/configSections/usage/UsageAnalyticsChart';
|
||||
import UsageAnalyticsTable from '@app/components/shared/config/configSections/usage/UsageAnalyticsTable';
|
||||
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 { t } = useTranslation();
|
||||
const { loginEnabled, validateLoginEnabled } = useLoginRequired();
|
||||
const [data, setData] = useState<EndpointStatisticsResponse | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@ -24,6 +27,10 @@ const AdminUsageSection: React.FC = () => {
|
||||
const [dataType, setDataType] = useState<'all' | 'api' | 'ui'>('all');
|
||||
|
||||
const fetchData = async () => {
|
||||
if (!validateLoginEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
@ -40,10 +47,55 @@ const AdminUsageSection: React.FC = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [displayMode, dataType]);
|
||||
if (loginEnabled) {
|
||||
fetchData();
|
||||
} 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,
|
||||
totalEndpoints: filteredEndpoints.length,
|
||||
endpoints: filteredEndpoints,
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
}, [displayMode, dataType, loginEnabled]);
|
||||
|
||||
const handleRefresh = () => {
|
||||
if (!validateLoginEnabled()) {
|
||||
return;
|
||||
}
|
||||
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
|
||||
if (loading) {
|
||||
if (actualLoading) {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: '2rem' }}>
|
||||
<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
|
||||
? ((displayedVisits / data.totalVisits) * 100).toFixed(1)
|
||||
const displayedPercentage = (data?.totalVisits || 0) > 0
|
||||
? ((displayedVisits / (data?.totalVisits || 1)) * 100).toFixed(1)
|
||||
: '0';
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<LoginRequiredBanner show={!loginEnabled} />
|
||||
|
||||
{/* Controls */}
|
||||
<Card padding="lg" radius="md" withBorder>
|
||||
<Stack gap="md">
|
||||
@ -103,6 +160,7 @@ const AdminUsageSection: React.FC = () => {
|
||||
<SegmentedControl
|
||||
value={displayMode}
|
||||
onChange={(value) => setDisplayMode(value as 'top10' | 'top20' | 'all')}
|
||||
disabled={!loginEnabled}
|
||||
data={[
|
||||
{
|
||||
value: 'top10',
|
||||
@ -123,6 +181,7 @@ const AdminUsageSection: React.FC = () => {
|
||||
leftSection={<LocalIcon icon="refresh" width="1rem" height="1rem" />}
|
||||
onClick={handleRefresh}
|
||||
loading={loading}
|
||||
disabled={!loginEnabled}
|
||||
>
|
||||
{t('usage.controls.refresh', 'Refresh')}
|
||||
</Button>
|
||||
@ -136,6 +195,7 @@ const AdminUsageSection: React.FC = () => {
|
||||
<SegmentedControl
|
||||
value={dataType}
|
||||
onChange={(value) => setDataType(value as 'all' | 'api' | 'ui')}
|
||||
disabled={!loginEnabled}
|
||||
data={[
|
||||
{
|
||||
value: 'all',
|
||||
@ -28,10 +28,13 @@ import { userManagementService, User } from '@app/services/userManagementService
|
||||
import { teamService, Team } from '@app/services/teamService';
|
||||
import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex';
|
||||
import { useAppConfig } from '@app/contexts/AppConfigContext';
|
||||
import { useLoginRequired } from '@app/hooks/useLoginRequired';
|
||||
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
|
||||
|
||||
export default function PeopleSection() {
|
||||
const { t } = useTranslation();
|
||||
const { config } = useAppConfig();
|
||||
const { loginEnabled } = useLoginRequired();
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [teams, setTeams] = useState<Team[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@ -97,30 +100,103 @@ export default function PeopleSection() {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [adminData, teamsData] = await Promise.all([
|
||||
userManagementService.getUsers(),
|
||||
teamService.getTeams(),
|
||||
]);
|
||||
|
||||
// Enrich users with session data
|
||||
const enrichedUsers = adminData.users.map(user => ({
|
||||
...user,
|
||||
isActive: adminData.userSessions[user.username] || false,
|
||||
lastRequest: adminData.userLastRequest[user.username] || undefined,
|
||||
}));
|
||||
if (loginEnabled) {
|
||||
const [adminData, teamsData] = await Promise.all([
|
||||
userManagementService.getUsers(),
|
||||
teamService.getTeams(),
|
||||
]);
|
||||
|
||||
setUsers(enrichedUsers);
|
||||
setTeams(teamsData);
|
||||
// Enrich users with session data
|
||||
const enrichedUsers = adminData.users.map(user => ({
|
||||
...user,
|
||||
isActive: adminData.userSessions[user.username] || false,
|
||||
lastRequest: adminData.userLastRequest[user.username] || undefined,
|
||||
}));
|
||||
|
||||
// Store license information
|
||||
setLicenseInfo({
|
||||
maxAllowedUsers: adminData.maxAllowedUsers,
|
||||
availableSlots: adminData.availableSlots,
|
||||
grandfatheredUserCount: adminData.grandfatheredUserCount,
|
||||
licenseMaxUsers: adminData.licenseMaxUsers,
|
||||
premiumEnabled: adminData.premiumEnabled,
|
||||
totalUsers: adminData.totalUsers,
|
||||
});
|
||||
setUsers(enrichedUsers);
|
||||
setTeams(teamsData);
|
||||
|
||||
// Store license information
|
||||
setLicenseInfo({
|
||||
maxAllowedUsers: adminData.maxAllowedUsers,
|
||||
availableSlots: adminData.availableSlots,
|
||||
grandfatheredUserCount: adminData.grandfatheredUserCount,
|
||||
licenseMaxUsers: adminData.licenseMaxUsers,
|
||||
premiumEnabled: adminData.premiumEnabled,
|
||||
totalUsers: adminData.totalUsers,
|
||||
});
|
||||
} else {
|
||||
// Provide example data when login is disabled
|
||||
const exampleUsers: User[] = [
|
||||
{
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
email: 'admin@example.com',
|
||||
enabled: true,
|
||||
roleName: 'ROLE_ADMIN',
|
||||
rolesAsString: 'ROLE_ADMIN',
|
||||
authenticationType: 'password',
|
||||
isActive: true,
|
||||
lastRequest: Date.now(),
|
||||
team: { id: 1, name: 'Engineering' }
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: 'john.doe',
|
||||
email: 'john.doe@example.com',
|
||||
enabled: true,
|
||||
roleName: 'ROLE_USER',
|
||||
rolesAsString: 'ROLE_USER',
|
||||
authenticationType: 'password',
|
||||
isActive: false,
|
||||
lastRequest: Date.now() - 86400000,
|
||||
team: { id: 1, name: 'Engineering' }
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
username: 'jane.smith',
|
||||
email: 'jane.smith@example.com',
|
||||
enabled: true,
|
||||
roleName: 'ROLE_USER',
|
||||
rolesAsString: 'ROLE_USER',
|
||||
authenticationType: 'oauth',
|
||||
isActive: true,
|
||||
lastRequest: Date.now(),
|
||||
team: { id: 2, name: 'Marketing' }
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
username: 'bob.wilson',
|
||||
email: 'bob.wilson@example.com',
|
||||
enabled: false,
|
||||
roleName: 'ROLE_USER',
|
||||
rolesAsString: 'ROLE_USER',
|
||||
authenticationType: 'password',
|
||||
isActive: false,
|
||||
lastRequest: Date.now() - 604800000,
|
||||
team: undefined
|
||||
}
|
||||
];
|
||||
|
||||
const exampleTeams: Team[] = [
|
||||
{ id: 1, name: 'Engineering', userCount: 3 },
|
||||
{ id: 2, name: 'Marketing', userCount: 2 }
|
||||
];
|
||||
|
||||
setUsers(exampleUsers);
|
||||
setTeams(exampleTeams);
|
||||
|
||||
// Example license information
|
||||
setLicenseInfo({
|
||||
maxAllowedUsers: 10,
|
||||
availableSlots: 6,
|
||||
grandfatheredUserCount: 0,
|
||||
licenseMaxUsers: 5,
|
||||
premiumEnabled: true,
|
||||
totalUsers: 4,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch people data:', error);
|
||||
alert({ alertType: 'error', title: 'Failed to load people data' });
|
||||
@ -405,6 +481,7 @@ export default function PeopleSection() {
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<LoginRequiredBanner show={!loginEnabled} />
|
||||
<div>
|
||||
<Text fw={600} size="lg">
|
||||
{t('workspace.people.title')}
|
||||
@ -457,15 +534,15 @@ export default function PeopleSection() {
|
||||
style={{ maxWidth: 300 }}
|
||||
/>
|
||||
<Tooltip
|
||||
label={t('workspace.people.license.noSlotsAvailable', 'No user slots available')}
|
||||
disabled={!licenseInfo || licenseInfo.availableSlots > 0}
|
||||
label={!loginEnabled ? 'Enable login mode first' : t('workspace.people.license.noSlotsAvailable', 'No user slots available')}
|
||||
disabled={loginEnabled && (!licenseInfo || licenseInfo.availableSlots > 0)}
|
||||
position="bottom"
|
||||
withArrow
|
||||
>
|
||||
<Button
|
||||
leftSection={<LocalIcon icon="person-add" width="1rem" height="1rem" />}
|
||||
onClick={() => setInviteModalOpened(true)}
|
||||
disabled={licenseInfo ? licenseInfo.availableSlots === 0 : false}
|
||||
disabled={!loginEnabled || (licenseInfo ? licenseInfo.availableSlots === 0 : false)}
|
||||
>
|
||||
{t('workspace.people.addMembers')}
|
||||
</Button>
|
||||
@ -616,20 +693,21 @@ export default function PeopleSection() {
|
||||
{/* Actions menu */}
|
||||
<Menu position="bottom-end" withinPortal>
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="subtle" color="gray">
|
||||
<ActionIcon variant="subtle" color="gray" disabled={!loginEnabled}>
|
||||
<LocalIcon icon="more-vert" width="1rem" height="1rem" />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown style={{ zIndex: Z_INDEX_OVER_CONFIG_MODAL }}>
|
||||
<Menu.Item onClick={() => openEditModal(user)}>{t('workspace.people.editRole')}</Menu.Item>
|
||||
<Menu.Item onClick={() => openEditModal(user)} disabled={!loginEnabled}>{t('workspace.people.editRole')}</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={user.enabled ? <LocalIcon icon="person-off" width="1rem" height="1rem" /> : <LocalIcon icon="person-check" width="1rem" height="1rem" />}
|
||||
onClick={() => handleToggleEnabled(user)}
|
||||
disabled={!loginEnabled}
|
||||
>
|
||||
{user.enabled ? t('workspace.people.disable') : t('workspace.people.enable')}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item color="red" leftSection={<LocalIcon icon="delete" width="1rem" height="1rem" />} onClick={() => handleDeleteUser(user)}>
|
||||
<Menu.Item color="red" leftSection={<LocalIcon icon="delete" width="1rem" height="1rem" />} onClick={() => handleDeleteUser(user)} disabled={!loginEnabled}>
|
||||
{t('workspace.people.deleteUser')}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
|
||||
@ -22,9 +22,12 @@ import { teamService, Team } from '@app/services/teamService';
|
||||
import { userManagementService, User } from '@app/services/userManagementService';
|
||||
import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex';
|
||||
import TeamDetailsSection from '@app/components/shared/config/configSections/TeamDetailsSection';
|
||||
import { useLoginRequired } from '@app/hooks/useLoginRequired';
|
||||
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
|
||||
|
||||
export default function TeamsSection() {
|
||||
const { t } = useTranslation();
|
||||
const { loginEnabled } = useLoginRequired();
|
||||
const [teams, setTeams] = useState<Team[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [createModalOpened, setCreateModalOpened] = useState(false);
|
||||
@ -47,8 +50,18 @@ export default function TeamsSection() {
|
||||
const fetchTeams = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const teamsData = await teamService.getTeams();
|
||||
setTeams(teamsData);
|
||||
if (loginEnabled) {
|
||||
const teamsData = await teamService.getTeams();
|
||||
setTeams(teamsData);
|
||||
} else {
|
||||
// Provide example data when login is disabled
|
||||
const exampleTeams: Team[] = [
|
||||
{ id: 1, name: 'Engineering', userCount: 3 },
|
||||
{ id: 2, name: 'Marketing', userCount: 2 },
|
||||
{ id: 3, name: 'Internal', userCount: 1 },
|
||||
];
|
||||
setTeams(exampleTeams);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch teams:', error);
|
||||
alert({ alertType: 'error', title: 'Failed to load teams' });
|
||||
@ -207,6 +220,7 @@ export default function TeamsSection() {
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<LoginRequiredBanner show={!loginEnabled} />
|
||||
<div>
|
||||
<Text fw={600} size="lg">
|
||||
{t('workspace.teams.title')}
|
||||
@ -218,7 +232,7 @@ export default function TeamsSection() {
|
||||
|
||||
{/* Header Actions */}
|
||||
<Group justify="flex-end">
|
||||
<Button leftSection={<LocalIcon icon="add" width="1rem" height="1rem" />} onClick={() => setCreateModalOpened(true)}>
|
||||
<Button leftSection={<LocalIcon icon="add" width="1rem" height="1rem" />} onClick={() => setCreateModalOpened(true)} disabled={!loginEnabled}>
|
||||
{t('workspace.teams.createNewTeam')}
|
||||
</Button>
|
||||
</Group>
|
||||
@ -257,8 +271,8 @@ export default function TeamsSection() {
|
||||
teams.map((team) => (
|
||||
<Table.Tr
|
||||
key={team.id}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => setViewingTeamId(team.id)}
|
||||
style={{ cursor: loginEnabled ? 'pointer' : 'default' }}
|
||||
onClick={() => loginEnabled && setViewingTeamId(team.id)}
|
||||
>
|
||||
<Table.Td>
|
||||
<Group gap="xs">
|
||||
@ -290,18 +304,18 @@ export default function TeamsSection() {
|
||||
<Table.Td onClick={(e) => e.stopPropagation()}>
|
||||
<Menu position="bottom-end" withinPortal>
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="subtle" color="gray">
|
||||
<ActionIcon variant="subtle" color="gray" disabled={!loginEnabled}>
|
||||
<LocalIcon icon="more-vert" width="1rem" height="1rem" />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown style={{ zIndex: Z_INDEX_OVER_CONFIG_MODAL }}>
|
||||
<Menu.Item leftSection={<LocalIcon icon="visibility" width="1rem" height="1rem" />} onClick={() => setViewingTeamId(team.id)}>
|
||||
<Menu.Item leftSection={<LocalIcon icon="visibility" width="1rem" height="1rem" />} onClick={() => setViewingTeamId(team.id)} disabled={!loginEnabled}>
|
||||
{t('workspace.teams.viewTeam', 'View Team')}
|
||||
</Menu.Item>
|
||||
<Menu.Item leftSection={<LocalIcon icon="group" width="1rem" height="1rem" />} onClick={() => openAddMemberModal(team)}>
|
||||
<Menu.Item leftSection={<LocalIcon icon="group" width="1rem" height="1rem" />} onClick={() => openAddMemberModal(team)} disabled={!loginEnabled}>
|
||||
{t('workspace.teams.addMember')}
|
||||
</Menu.Item>
|
||||
<Menu.Item leftSection={<LocalIcon icon="edit" width="1rem" height="1rem" />} onClick={() => openRenameModal(team)}>
|
||||
<Menu.Item leftSection={<LocalIcon icon="edit" width="1rem" height="1rem" />} onClick={() => openRenameModal(team)} disabled={!loginEnabled}>
|
||||
{t('workspace.teams.renameTeamLabel')}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
@ -309,7 +323,7 @@ export default function TeamsSection() {
|
||||
color="red"
|
||||
leftSection={<LocalIcon icon="delete" width="1rem" height="1rem" />}
|
||||
onClick={() => handleDeleteTeam(team)}
|
||||
disabled={team.name === 'Internal'}
|
||||
disabled={!loginEnabled || team.name === 'Internal'}
|
||||
>
|
||||
{t('workspace.teams.deleteTeamLabel')}
|
||||
</Menu.Item>
|
||||
|
||||
@ -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 [timePeriod, setTimePeriod] = useState<'day' | 'week' | 'month'>('week');
|
||||
const [chartsData, setChartsData] = useState<AuditChartsData | null>(null);
|
||||
@ -74,8 +78,27 @@ const AuditChartsSection: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
fetchChartsData();
|
||||
}, [timePeriod]);
|
||||
if (loginEnabled) {
|
||||
fetchChartsData();
|
||||
} 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) {
|
||||
return (
|
||||
@ -123,7 +146,11 @@ const AuditChartsSection: React.FC = () => {
|
||||
</Text>
|
||||
<SegmentedControl
|
||||
value={timePeriod}
|
||||
onChange={(value) => setTimePeriod(value as 'day' | 'week' | 'month')}
|
||||
onChange={(value) => {
|
||||
if (!loginEnabled) return;
|
||||
setTimePeriod(value as 'day' | 'week' | 'month');
|
||||
}}
|
||||
disabled={!loginEnabled}
|
||||
data={[
|
||||
{ label: t('audit.charts.day', 'Day'), value: 'day' },
|
||||
{ label: t('audit.charts.week', 'Week'), value: 'week' },
|
||||
@ -18,7 +18,11 @@ import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex';
|
||||
import { useAuditFilters } from '@app/hooks/useAuditFilters';
|
||||
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 [events, setEvents] = useState<AuditEvent[]>([]);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
@ -51,8 +55,57 @@ const AuditEventsTable: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
fetchEvents();
|
||||
}, [filters, currentPage]);
|
||||
if (loginEnabled) {
|
||||
fetchEvents();
|
||||
} 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: { message: '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: { message: '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: { message: '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: { message: 'Downloaded report.pdf' },
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
timestamp: new Date(now.getTime() - 1000 * 60 * 90).toISOString(),
|
||||
eventType: 'LOGOUT',
|
||||
username: 'user1',
|
||||
ipAddress: '192.168.1.101',
|
||||
details: { message: 'User logged out' },
|
||||
},
|
||||
]);
|
||||
setTotalPages(1);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [filters, currentPage, loginEnabled]);
|
||||
|
||||
// Wrap filter handlers to reset pagination
|
||||
const handleFilterChangeWithReset = (key: keyof typeof filters, value: any) => {
|
||||
@ -83,6 +136,7 @@ const AuditEventsTable: React.FC = () => {
|
||||
users={users}
|
||||
onFilterChange={handleFilterChangeWithReset}
|
||||
onClearFilters={handleClearFiltersWithReset}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
|
||||
{/* Table */}
|
||||
@ -153,6 +207,7 @@ const AuditEventsTable: React.FC = () => {
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
onClick={() => setSelectedEvent(event)}
|
||||
disabled={!loginEnabled}
|
||||
>
|
||||
{t('audit.events.viewDetails', 'View Details')}
|
||||
</Button>
|
||||
@ -13,7 +13,11 @@ import LocalIcon from '@app/components/shared/LocalIcon';
|
||||
import { useAuditFilters } from '@app/hooks/useAuditFilters';
|
||||
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 [exportFormat, setExportFormat] = useState<'csv' | 'json'>('csv');
|
||||
const [exporting, setExporting] = useState(false);
|
||||
@ -22,6 +26,8 @@ const AuditExportSection: React.FC = () => {
|
||||
const { filters, eventTypes, users, handleFilterChange, handleClearFilters } = useAuditFilters();
|
||||
|
||||
const handleExport = async () => {
|
||||
if (!loginEnabled) return;
|
||||
|
||||
try {
|
||||
setExporting(true);
|
||||
|
||||
@ -65,7 +71,11 @@ const AuditExportSection: React.FC = () => {
|
||||
</Text>
|
||||
<SegmentedControl
|
||||
value={exportFormat}
|
||||
onChange={(value) => setExportFormat(value as 'csv' | 'json')}
|
||||
onChange={(value) => {
|
||||
if (!loginEnabled) return;
|
||||
setExportFormat(value as 'csv' | 'json');
|
||||
}}
|
||||
disabled={!loginEnabled}
|
||||
data={[
|
||||
{ label: 'CSV', value: 'csv' },
|
||||
{ label: 'JSON', value: 'json' },
|
||||
@ -84,6 +94,7 @@ const AuditExportSection: React.FC = () => {
|
||||
users={users}
|
||||
onFilterChange={handleFilterChange}
|
||||
onClearFilters={handleClearFilters}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -93,7 +104,7 @@ const AuditExportSection: React.FC = () => {
|
||||
leftSection={<LocalIcon icon="download" width="1rem" height="1rem" />}
|
||||
onClick={handleExport}
|
||||
loading={exporting}
|
||||
disabled={exporting}
|
||||
disabled={!loginEnabled || exporting}
|
||||
>
|
||||
{t('audit.export.exportButton', 'Export Data')}
|
||||
</Button>
|
||||
@ -11,6 +11,7 @@ interface AuditFiltersFormProps {
|
||||
users: string[];
|
||||
onFilterChange: (key: keyof AuditFilters, value: any) => void;
|
||||
onClearFilters: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -22,6 +23,7 @@ const AuditFiltersForm: React.FC<AuditFiltersFormProps> = ({
|
||||
users,
|
||||
onFilterChange,
|
||||
onClearFilters,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -33,6 +35,7 @@ const AuditFiltersForm: React.FC<AuditFiltersFormProps> = ({
|
||||
value={filters.eventType}
|
||||
onChange={(value) => onFilterChange('eventType', value || undefined)}
|
||||
clearable
|
||||
disabled={disabled}
|
||||
style={{ flex: 1, minWidth: 200 }}
|
||||
comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }}
|
||||
/>
|
||||
@ -43,6 +46,7 @@ const AuditFiltersForm: React.FC<AuditFiltersFormProps> = ({
|
||||
onChange={(value) => onFilterChange('username', value || undefined)}
|
||||
clearable
|
||||
searchable
|
||||
disabled={disabled}
|
||||
style={{ flex: 1, minWidth: 200 }}
|
||||
comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }}
|
||||
/>
|
||||
@ -53,6 +57,7 @@ const AuditFiltersForm: React.FC<AuditFiltersFormProps> = ({
|
||||
onFilterChange('startDate', value ?? undefined)
|
||||
}
|
||||
clearable
|
||||
disabled={disabled}
|
||||
style={{ flex: 1, minWidth: 150 }}
|
||||
popoverProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }}
|
||||
/>
|
||||
@ -63,10 +68,11 @@ const AuditFiltersForm: React.FC<AuditFiltersFormProps> = ({
|
||||
onFilterChange('endDate', value ?? undefined)
|
||||
}
|
||||
clearable
|
||||
disabled={disabled}
|
||||
style={{ flex: 1, minWidth: 150 }}
|
||||
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')}
|
||||
</Button>
|
||||
</Group>
|
||||
@ -3,7 +3,7 @@ import { Navigate, useLocation } from 'react-router-dom'
|
||||
import { useAuth } from '@app/auth/UseSession'
|
||||
import { useAppConfig } from '@app/contexts/AppConfigContext'
|
||||
import HomePage from '@app/pages/HomePage'
|
||||
import Login from '@app/routes/Login'
|
||||
// Login component is used via routing, not directly imported
|
||||
import FirstLoginModal from '@app/components/shared/FirstLoginModal'
|
||||
import { accountService } from '@app/services/accountService'
|
||||
|
||||
@ -95,13 +95,7 @@ export default function Landing() {
|
||||
);
|
||||
}
|
||||
|
||||
// If we're at home route ("/"), show login directly (marketing/landing page)
|
||||
// Otherwise navigate to login (fixes URL mismatch for tool routes)
|
||||
const isHome = location.pathname === '/' || location.pathname === '';
|
||||
if (isHome) {
|
||||
return <Login />;
|
||||
}
|
||||
|
||||
// For non-home routes without auth, navigate to login (preserves from location)
|
||||
// No session - redirect to login page
|
||||
// This ensures the URL always shows /login when not authenticated
|
||||
return <Navigate to="/login" replace state={{ from: location }} />;
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ import AuthLayout from '@app/routes/authShared/AuthLayout';
|
||||
import LoginHeader from '@app/routes/login/LoginHeader';
|
||||
import ErrorMessage from '@app/routes/login/ErrorMessage';
|
||||
import EmailPasswordForm from '@app/routes/login/EmailPasswordForm';
|
||||
import OAuthButtons from '@app/routes/login/OAuthButtons';
|
||||
import OAuthButtons, { DEBUG_SHOW_ALL_PROVIDERS, oauthProviderConfig } from '@app/routes/login/OAuthButtons';
|
||||
import DividerWithText from '@app/components/shared/DividerWithText';
|
||||
import LoggedInState from '@app/routes/login/LoggedInState';
|
||||
import { BASE_PATH } from '@app/constants/app';
|
||||
@ -26,8 +26,55 @@ export default function Login() {
|
||||
const [showEmailForm, setShowEmailForm] = useState(false);
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [enabledProviders, setEnabledProviders] = useState<string[]>([]);
|
||||
const [hasSSOProviders, setHasSSOProviders] = useState(false);
|
||||
const [_enableLogin, setEnableLogin] = useState<boolean | null>(null);
|
||||
|
||||
// Handle query params (email prefill and success messages)
|
||||
// Fetch enabled SSO providers and login config from backend
|
||||
useEffect(() => {
|
||||
const fetchProviders = async () => {
|
||||
try {
|
||||
const response = await fetch(`${BASE_PATH}/api/v1/proprietary/ui-data/login`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
|
||||
// Check if login is disabled - if so, redirect to home
|
||||
if (data.enableLogin === false) {
|
||||
console.debug('[Login] Login disabled, redirecting to home');
|
||||
navigate('/');
|
||||
return;
|
||||
}
|
||||
|
||||
setEnableLogin(data.enableLogin ?? true);
|
||||
|
||||
// Extract provider IDs from the providerList map
|
||||
// The keys are like "/oauth2/authorization/google" - extract the last part
|
||||
const providerIds = Object.keys(data.providerList || {})
|
||||
.map(key => key.split('/').pop())
|
||||
.filter((id): id is string => id !== undefined);
|
||||
setEnabledProviders(providerIds);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Login] Failed to fetch enabled providers:', err);
|
||||
}
|
||||
};
|
||||
fetchProviders();
|
||||
}, [navigate]);
|
||||
|
||||
// Update hasSSOProviders and showEmailForm when enabledProviders changes
|
||||
useEffect(() => {
|
||||
// In debug mode, check if any providers exist in the config
|
||||
const hasProviders = DEBUG_SHOW_ALL_PROVIDERS
|
||||
? Object.keys(oauthProviderConfig).length > 0
|
||||
: enabledProviders.length > 0;
|
||||
setHasSSOProviders(hasProviders);
|
||||
// If no SSO providers, show email form by default
|
||||
if (!hasProviders) {
|
||||
setShowEmailForm(true);
|
||||
}
|
||||
}, [enabledProviders]);
|
||||
|
||||
// Handle query params (email prefill, success messages, and session expiry)
|
||||
useEffect(() => {
|
||||
try {
|
||||
const emailFromQuery = searchParams.get('email');
|
||||
@ -35,6 +82,12 @@ export default function Login() {
|
||||
setEmail(emailFromQuery);
|
||||
}
|
||||
|
||||
// Check if session expired (401 redirect)
|
||||
const expired = searchParams.get('expired');
|
||||
if (expired === 'true') {
|
||||
setError(t('login.sessionExpired', 'Your session has expired. Please sign in again.'));
|
||||
}
|
||||
|
||||
const messageType = searchParams.get('messageType')
|
||||
if (messageType) {
|
||||
switch (messageType) {
|
||||
@ -71,7 +124,7 @@ export default function Login() {
|
||||
return <LoggedInState />;
|
||||
}
|
||||
|
||||
const signInWithProvider = async (provider: 'github' | 'google' | 'apple' | 'azure') => {
|
||||
const signInWithProvider = async (provider: 'github' | 'google' | 'apple' | 'azure' | 'keycloak' | 'oidc') => {
|
||||
try {
|
||||
setIsSigningIn(true);
|
||||
setError(null);
|
||||
@ -129,9 +182,10 @@ export default function Login() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleForgotPassword = () => {
|
||||
navigate('/auth/reset');
|
||||
};
|
||||
// Forgot password handler (currently unused, reserved for future implementation)
|
||||
// const handleForgotPassword = () => {
|
||||
// navigate('/auth/reset');
|
||||
// };
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
@ -160,25 +214,31 @@ export default function Login() {
|
||||
onProviderClick={signInWithProvider}
|
||||
isSubmitting={isSigningIn}
|
||||
layout="vertical"
|
||||
enabledProviders={enabledProviders}
|
||||
/>
|
||||
|
||||
{/* Divider between OAuth and Email */}
|
||||
<DividerWithText text={t('signup.or', 'or')} respondsToDarkMode={false} opacity={0.4} />
|
||||
{/* Divider between OAuth and Email - only show if SSO is available */}
|
||||
{hasSSOProviders && (
|
||||
<DividerWithText text={t('signup.or', 'or')} respondsToDarkMode={false} opacity={0.4} />
|
||||
)}
|
||||
|
||||
{/* Sign in with email button (primary color to match signup CTA) */}
|
||||
<div className="auth-section">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowEmailForm(true)}
|
||||
disabled={isSigningIn}
|
||||
className="w-full px-4 py-[0.75rem] rounded-[0.625rem] text-base font-semibold mb-2 cursor-pointer border-0 disabled:opacity-50 disabled:cursor-not-allowed auth-cta-button"
|
||||
>
|
||||
{t('login.useEmailInstead', 'Login with email')}
|
||||
</button>
|
||||
</div>
|
||||
{/* Sign in with email button - only show if SSO providers exist */}
|
||||
{hasSSOProviders && !showEmailForm && (
|
||||
<div className="auth-section">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowEmailForm(true)}
|
||||
disabled={isSigningIn}
|
||||
className="w-full px-4 py-[0.75rem] rounded-[0.625rem] text-base font-semibold mb-2 cursor-pointer border-0 disabled:opacity-50 disabled:cursor-not-allowed auth-cta-button"
|
||||
>
|
||||
{t('login.useEmailInstead', 'Login with email')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Email form - show by default if no SSO, or when button clicked */}
|
||||
{showEmailForm && (
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<div style={{ marginTop: hasSSOProviders ? '1rem' : '0' }}>
|
||||
<EmailPasswordForm
|
||||
email={email}
|
||||
password={password}
|
||||
@ -191,31 +251,6 @@ export default function Login() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showEmailForm && (
|
||||
<div className="auth-section-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleForgotPassword}
|
||||
className="auth-link-black"
|
||||
>
|
||||
{t('login.forgotPassword', 'Forgot your password?')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Divider then signup link */}
|
||||
<DividerWithText text={t('signup.or', 'or')} respondsToDarkMode={false} opacity={0.4} />
|
||||
|
||||
<div style={{ textAlign: 'center', margin: '0.5rem 0 0.25rem' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate('/signup')}
|
||||
className="auth-link-black"
|
||||
>
|
||||
{t('signup.signUp', 'Sign up')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@ -239,7 +239,7 @@
|
||||
}
|
||||
|
||||
.login-logo-text {
|
||||
height: 1.5rem; /* 24px */
|
||||
height: 2rem; /* 32px - increased from 24px */
|
||||
}
|
||||
|
||||
.login-title {
|
||||
|
||||
@ -2,11 +2,13 @@ import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '@app/auth/UseSession';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLogoPath } from '@app/hooks/useLogoPath';
|
||||
|
||||
export default function LoggedInState() {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
const { t } = useTranslation();
|
||||
const logoPath = useLogoPath();
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
@ -34,7 +36,13 @@ export default function LoggedInState() {
|
||||
padding: '32px'
|
||||
}}>
|
||||
<div style={{ textAlign: 'center', marginBottom: '24px' }}>
|
||||
<div style={{ fontSize: '48px', marginBottom: '16px' }}>✅</div>
|
||||
<div style={{ marginBottom: '16px', display: 'flex', justifyContent: 'center' }}>
|
||||
<img
|
||||
src={logoPath}
|
||||
alt="Stirling PDF Logo"
|
||||
style={{ width: '64px', height: '64px', objectFit: 'contain' }}
|
||||
/>
|
||||
</div>
|
||||
<h1 style={{ fontSize: '24px', fontWeight: 'bold', color: '#059669', marginBottom: '8px' }}>
|
||||
{t('login.youAreLoggedIn')}
|
||||
</h1>
|
||||
|
||||
@ -10,7 +10,6 @@ export default function LoginHeader({ title, subtitle }: LoginHeaderProps) {
|
||||
return (
|
||||
<div className="login-header">
|
||||
<div className="login-header-logos">
|
||||
<img src={`${BASE_PATH}/logo192.png`} alt="Logo" className="login-logo-icon" />
|
||||
<img src={`${BASE_PATH}/branding/StirlingPDFLogoBlackText.svg`} alt="Stirling PDF" className="login-logo-text" />
|
||||
</div>
|
||||
<h1 className="login-title">{title}</h1>
|
||||
|
||||
@ -1,30 +1,54 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BASE_PATH } from '@app/constants/app';
|
||||
|
||||
// OAuth provider configuration
|
||||
const oauthProviders = [
|
||||
{ id: 'google', label: 'Google', file: 'google.svg', isDisabled: false },
|
||||
{ id: 'github', label: 'GitHub', file: 'github.svg', isDisabled: false },
|
||||
{ id: 'apple', label: 'Apple', file: 'apple.svg', isDisabled: true },
|
||||
{ id: 'azure', label: 'Microsoft', file: 'microsoft.svg', isDisabled: true }
|
||||
];
|
||||
// Debug flag to show all providers for UI testing
|
||||
// Set to true to see all SSO options regardless of backend configuration
|
||||
export const DEBUG_SHOW_ALL_PROVIDERS = false;
|
||||
|
||||
// OAuth provider configuration - maps provider ID to display info
|
||||
export const oauthProviderConfig = {
|
||||
google: { label: 'Google', file: 'google.svg' },
|
||||
github: { label: 'GitHub', file: 'github.svg' },
|
||||
apple: { label: 'Apple', file: 'apple.svg' },
|
||||
azure: { label: 'Microsoft', file: 'microsoft.svg' },
|
||||
// microsoft and azure are the same, keycloak and oidc need their own icons
|
||||
// These are commented out from debug view since they need proper icons or backend doesn't use them
|
||||
// keycloak: { label: 'Keycloak', file: 'keycloak.svg' },
|
||||
// oidc: { label: 'OIDC', file: 'oidc.svg' }
|
||||
};
|
||||
|
||||
interface OAuthButtonsProps {
|
||||
onProviderClick: (provider: 'github' | 'google' | 'apple' | 'azure') => void
|
||||
onProviderClick: (provider: 'github' | 'google' | 'apple' | 'azure' | 'keycloak' | 'oidc') => void
|
||||
isSubmitting: boolean
|
||||
layout?: 'vertical' | 'grid' | 'icons'
|
||||
enabledProviders?: string[] // List of enabled provider IDs from backend
|
||||
}
|
||||
|
||||
export default function OAuthButtons({ onProviderClick, isSubmitting, layout = 'vertical' }: OAuthButtonsProps) {
|
||||
export default function OAuthButtons({ onProviderClick, isSubmitting, layout = 'vertical', enabledProviders = [] }: OAuthButtonsProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Filter out disabled providers - don't show them at all
|
||||
const enabledProviders = oauthProviders.filter(p => !p.isDisabled);
|
||||
// Debug mode: show all providers for UI testing
|
||||
const providersToShow = DEBUG_SHOW_ALL_PROVIDERS
|
||||
? Object.keys(oauthProviderConfig)
|
||||
: enabledProviders;
|
||||
|
||||
// Filter to only show enabled providers from backend
|
||||
const providers = providersToShow
|
||||
.filter(id => id in oauthProviderConfig)
|
||||
.map(id => ({
|
||||
id,
|
||||
...oauthProviderConfig[id as keyof typeof oauthProviderConfig]
|
||||
}));
|
||||
|
||||
// If no providers are enabled, don't render anything
|
||||
if (providers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (layout === 'icons') {
|
||||
return (
|
||||
<div className="oauth-container-icons">
|
||||
{enabledProviders.map((p) => (
|
||||
{providers.map((p) => (
|
||||
<div key={p.id} title={`${t('login.signInWith', 'Sign in with')} ${p.label}`}>
|
||||
<button
|
||||
onClick={() => onProviderClick(p.id as any)}
|
||||
@ -43,7 +67,7 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = '
|
||||
if (layout === 'grid') {
|
||||
return (
|
||||
<div className="oauth-container-grid">
|
||||
{enabledProviders.map((p) => (
|
||||
{providers.map((p) => (
|
||||
<div key={p.id} title={`${t('login.signInWith', 'Sign in with')} ${p.label}`}>
|
||||
<button
|
||||
onClick={() => onProviderClick(p.id as any)}
|
||||
@ -61,7 +85,7 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = '
|
||||
|
||||
return (
|
||||
<div className="oauth-container-vertical">
|
||||
{enabledProviders.map((p) => (
|
||||
{providers.map((p) => (
|
||||
<button
|
||||
key={p.id}
|
||||
onClick={() => onProviderClick(p.id as any)}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user