mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-17 13:52:14 +01:00
settingsPage Init selfhost
This commit is contained in:
parent
f6a7b983a0
commit
b3c1b4791c
@ -1,11 +1,13 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
@ -46,4 +48,301 @@ public class SettingsController {
|
||||
public ResponseEntity<Map<String, Boolean>> getDisabledEndpoints() {
|
||||
return ResponseEntity.ok(endpointConfiguration.getEndpointStatuses());
|
||||
}
|
||||
|
||||
// ========== GENERAL SETTINGS ==========
|
||||
|
||||
@GetMapping("/admin/settings/general")
|
||||
@Hidden
|
||||
public ResponseEntity<Map<String, Object>> getGeneralSettings() {
|
||||
Map<String, Object> settings = new HashMap<>();
|
||||
settings.put("ui", applicationProperties.getUi());
|
||||
settings.put("system", Map.of(
|
||||
"defaultLocale", applicationProperties.getSystem().getDefaultLocale(),
|
||||
"showUpdate", applicationProperties.getSystem().isShowUpdate(),
|
||||
"showUpdateOnlyAdmin", applicationProperties.getSystem().getShowUpdateOnlyAdmin(),
|
||||
"customHTMLFiles", applicationProperties.getSystem().isCustomHTMLFiles(),
|
||||
"fileUploadLimit", applicationProperties.getSystem().getFileUploadLimit()
|
||||
));
|
||||
return ResponseEntity.ok(settings);
|
||||
}
|
||||
|
||||
@PostMapping("/admin/settings/general")
|
||||
@Hidden
|
||||
public ResponseEntity<String> updateGeneralSettings(@RequestBody Map<String, Object> settings) throws IOException {
|
||||
// Update UI settings
|
||||
if (settings.containsKey("ui")) {
|
||||
Map<String, String> ui = (Map<String, String>) settings.get("ui");
|
||||
if (ui.containsKey("appName")) {
|
||||
GeneralUtils.saveKeyToSettings("ui.appName", ui.get("appName"));
|
||||
applicationProperties.getUi().setAppName(ui.get("appName"));
|
||||
}
|
||||
if (ui.containsKey("homeDescription")) {
|
||||
GeneralUtils.saveKeyToSettings("ui.homeDescription", ui.get("homeDescription"));
|
||||
applicationProperties.getUi().setHomeDescription(ui.get("homeDescription"));
|
||||
}
|
||||
if (ui.containsKey("appNameNavbar")) {
|
||||
GeneralUtils.saveKeyToSettings("ui.appNameNavbar", ui.get("appNameNavbar"));
|
||||
applicationProperties.getUi().setAppNameNavbar(ui.get("appNameNavbar"));
|
||||
}
|
||||
}
|
||||
|
||||
// Update System settings
|
||||
if (settings.containsKey("system")) {
|
||||
Map<String, Object> system = (Map<String, Object>) settings.get("system");
|
||||
if (system.containsKey("defaultLocale")) {
|
||||
GeneralUtils.saveKeyToSettings("system.defaultLocale", system.get("defaultLocale"));
|
||||
applicationProperties.getSystem().setDefaultLocale((String) system.get("defaultLocale"));
|
||||
}
|
||||
if (system.containsKey("showUpdate")) {
|
||||
GeneralUtils.saveKeyToSettings("system.showUpdate", system.get("showUpdate"));
|
||||
applicationProperties.getSystem().setShowUpdate((Boolean) system.get("showUpdate"));
|
||||
}
|
||||
if (system.containsKey("showUpdateOnlyAdmin")) {
|
||||
GeneralUtils.saveKeyToSettings("system.showUpdateOnlyAdmin", system.get("showUpdateOnlyAdmin"));
|
||||
applicationProperties.getSystem().setShowUpdateOnlyAdmin((Boolean) system.get("showUpdateOnlyAdmin"));
|
||||
}
|
||||
if (system.containsKey("fileUploadLimit")) {
|
||||
GeneralUtils.saveKeyToSettings("system.fileUploadLimit", system.get("fileUploadLimit"));
|
||||
applicationProperties.getSystem().setFileUploadLimit((String) system.get("fileUploadLimit"));
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.ok("General settings updated. Restart required for changes to take effect.");
|
||||
}
|
||||
|
||||
// ========== SECURITY SETTINGS ==========
|
||||
|
||||
@GetMapping("/admin/settings/security")
|
||||
@Hidden
|
||||
public ResponseEntity<Map<String, Object>> getSecuritySettings() {
|
||||
Map<String, Object> settings = new HashMap<>();
|
||||
ApplicationProperties.Security security = applicationProperties.getSecurity();
|
||||
|
||||
settings.put("enableLogin", security.getEnableLogin());
|
||||
settings.put("csrfDisabled", security.getCsrfDisabled());
|
||||
settings.put("loginMethod", security.getLoginMethod());
|
||||
settings.put("loginAttemptCount", security.getLoginAttemptCount());
|
||||
settings.put("loginResetTimeMinutes", security.getLoginResetTimeMinutes());
|
||||
settings.put("initialLogin", Map.of(
|
||||
"username", security.getInitialLogin().getUsername() != null ? security.getInitialLogin().getUsername() : ""
|
||||
));
|
||||
|
||||
// JWT settings
|
||||
ApplicationProperties.Security.Jwt jwt = security.getJwt();
|
||||
settings.put("jwt", Map.of(
|
||||
"enableKeystore", jwt.isEnableKeystore(),
|
||||
"enableKeyRotation", jwt.isEnableKeyRotation(),
|
||||
"enableKeyCleanup", jwt.isEnableKeyCleanup(),
|
||||
"keyRetentionDays", jwt.getKeyRetentionDays(),
|
||||
"secureCookie", jwt.isSecureCookie()
|
||||
));
|
||||
|
||||
return ResponseEntity.ok(settings);
|
||||
}
|
||||
|
||||
@PostMapping("/admin/settings/security")
|
||||
@Hidden
|
||||
public ResponseEntity<String> updateSecuritySettings(@RequestBody Map<String, Object> settings) throws IOException {
|
||||
if (settings.containsKey("enableLogin")) {
|
||||
GeneralUtils.saveKeyToSettings("security.enableLogin", settings.get("enableLogin"));
|
||||
applicationProperties.getSecurity().setEnableLogin((Boolean) settings.get("enableLogin"));
|
||||
}
|
||||
if (settings.containsKey("csrfDisabled")) {
|
||||
GeneralUtils.saveKeyToSettings("security.csrfDisabled", settings.get("csrfDisabled"));
|
||||
applicationProperties.getSecurity().setCsrfDisabled((Boolean) settings.get("csrfDisabled"));
|
||||
}
|
||||
if (settings.containsKey("loginMethod")) {
|
||||
GeneralUtils.saveKeyToSettings("security.loginMethod", settings.get("loginMethod"));
|
||||
applicationProperties.getSecurity().setLoginMethod((String) settings.get("loginMethod"));
|
||||
}
|
||||
if (settings.containsKey("loginAttemptCount")) {
|
||||
GeneralUtils.saveKeyToSettings("security.loginAttemptCount", settings.get("loginAttemptCount"));
|
||||
applicationProperties.getSecurity().setLoginAttemptCount((Integer) settings.get("loginAttemptCount"));
|
||||
}
|
||||
if (settings.containsKey("loginResetTimeMinutes")) {
|
||||
GeneralUtils.saveKeyToSettings("security.loginResetTimeMinutes", settings.get("loginResetTimeMinutes"));
|
||||
applicationProperties.getSecurity().setLoginResetTimeMinutes(((Number) settings.get("loginResetTimeMinutes")).longValue());
|
||||
}
|
||||
|
||||
// JWT settings
|
||||
if (settings.containsKey("jwt")) {
|
||||
Map<String, Object> jwt = (Map<String, Object>) settings.get("jwt");
|
||||
if (jwt.containsKey("secureCookie")) {
|
||||
GeneralUtils.saveKeyToSettings("security.jwt.secureCookie", jwt.get("secureCookie"));
|
||||
applicationProperties.getSecurity().getJwt().setSecureCookie((Boolean) jwt.get("secureCookie"));
|
||||
}
|
||||
if (jwt.containsKey("keyRetentionDays")) {
|
||||
GeneralUtils.saveKeyToSettings("security.jwt.keyRetentionDays", jwt.get("keyRetentionDays"));
|
||||
applicationProperties.getSecurity().getJwt().setKeyRetentionDays((Integer) jwt.get("keyRetentionDays"));
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.ok("Security settings updated. Restart required for changes to take effect.");
|
||||
}
|
||||
|
||||
// ========== CONNECTIONS SETTINGS (OAuth/SAML) ==========
|
||||
|
||||
@GetMapping("/admin/settings/connections")
|
||||
@Hidden
|
||||
public ResponseEntity<Map<String, Object>> getConnectionsSettings() {
|
||||
Map<String, Object> settings = new HashMap<>();
|
||||
ApplicationProperties.Security security = applicationProperties.getSecurity();
|
||||
|
||||
// OAuth2 settings
|
||||
ApplicationProperties.Security.OAUTH2 oauth2 = security.getOauth2();
|
||||
settings.put("oauth2", Map.of(
|
||||
"enabled", oauth2.getEnabled(),
|
||||
"issuer", oauth2.getIssuer() != null ? oauth2.getIssuer() : "",
|
||||
"clientId", oauth2.getClientId() != null ? oauth2.getClientId() : "",
|
||||
"provider", oauth2.getProvider() != null ? oauth2.getProvider() : "",
|
||||
"autoCreateUser", oauth2.getAutoCreateUser(),
|
||||
"blockRegistration", oauth2.getBlockRegistration(),
|
||||
"useAsUsername", oauth2.getUseAsUsername() != null ? oauth2.getUseAsUsername() : ""
|
||||
));
|
||||
|
||||
// SAML2 settings
|
||||
ApplicationProperties.Security.SAML2 saml2 = security.getSaml2();
|
||||
settings.put("saml2", Map.of(
|
||||
"enabled", saml2.getEnabled(),
|
||||
"provider", saml2.getProvider() != null ? saml2.getProvider() : "",
|
||||
"autoCreateUser", saml2.getAutoCreateUser(),
|
||||
"blockRegistration", saml2.getBlockRegistration(),
|
||||
"registrationId", saml2.getRegistrationId()
|
||||
));
|
||||
|
||||
return ResponseEntity.ok(settings);
|
||||
}
|
||||
|
||||
@PostMapping("/admin/settings/connections")
|
||||
@Hidden
|
||||
public ResponseEntity<String> updateConnectionsSettings(@RequestBody Map<String, Object> settings) throws IOException {
|
||||
// OAuth2 settings
|
||||
if (settings.containsKey("oauth2")) {
|
||||
Map<String, Object> oauth2 = (Map<String, Object>) settings.get("oauth2");
|
||||
if (oauth2.containsKey("enabled")) {
|
||||
GeneralUtils.saveKeyToSettings("security.oauth2.enabled", oauth2.get("enabled"));
|
||||
applicationProperties.getSecurity().getOauth2().setEnabled((Boolean) oauth2.get("enabled"));
|
||||
}
|
||||
if (oauth2.containsKey("issuer")) {
|
||||
GeneralUtils.saveKeyToSettings("security.oauth2.issuer", oauth2.get("issuer"));
|
||||
applicationProperties.getSecurity().getOauth2().setIssuer((String) oauth2.get("issuer"));
|
||||
}
|
||||
if (oauth2.containsKey("clientId")) {
|
||||
GeneralUtils.saveKeyToSettings("security.oauth2.clientId", oauth2.get("clientId"));
|
||||
applicationProperties.getSecurity().getOauth2().setClientId((String) oauth2.get("clientId"));
|
||||
}
|
||||
if (oauth2.containsKey("clientSecret")) {
|
||||
GeneralUtils.saveKeyToSettings("security.oauth2.clientSecret", oauth2.get("clientSecret"));
|
||||
applicationProperties.getSecurity().getOauth2().setClientSecret((String) oauth2.get("clientSecret"));
|
||||
}
|
||||
if (oauth2.containsKey("provider")) {
|
||||
GeneralUtils.saveKeyToSettings("security.oauth2.provider", oauth2.get("provider"));
|
||||
applicationProperties.getSecurity().getOauth2().setProvider((String) oauth2.get("provider"));
|
||||
}
|
||||
if (oauth2.containsKey("autoCreateUser")) {
|
||||
GeneralUtils.saveKeyToSettings("security.oauth2.autoCreateUser", oauth2.get("autoCreateUser"));
|
||||
applicationProperties.getSecurity().getOauth2().setAutoCreateUser((Boolean) oauth2.get("autoCreateUser"));
|
||||
}
|
||||
if (oauth2.containsKey("blockRegistration")) {
|
||||
GeneralUtils.saveKeyToSettings("security.oauth2.blockRegistration", oauth2.get("blockRegistration"));
|
||||
applicationProperties.getSecurity().getOauth2().setBlockRegistration((Boolean) oauth2.get("blockRegistration"));
|
||||
}
|
||||
if (oauth2.containsKey("useAsUsername")) {
|
||||
GeneralUtils.saveKeyToSettings("security.oauth2.useAsUsername", oauth2.get("useAsUsername"));
|
||||
applicationProperties.getSecurity().getOauth2().setUseAsUsername((String) oauth2.get("useAsUsername"));
|
||||
}
|
||||
}
|
||||
|
||||
// SAML2 settings
|
||||
if (settings.containsKey("saml2")) {
|
||||
Map<String, Object> saml2 = (Map<String, Object>) settings.get("saml2");
|
||||
if (saml2.containsKey("enabled")) {
|
||||
GeneralUtils.saveKeyToSettings("security.saml2.enabled", saml2.get("enabled"));
|
||||
applicationProperties.getSecurity().getSaml2().setEnabled((Boolean) saml2.get("enabled"));
|
||||
}
|
||||
if (saml2.containsKey("provider")) {
|
||||
GeneralUtils.saveKeyToSettings("security.saml2.provider", saml2.get("provider"));
|
||||
applicationProperties.getSecurity().getSaml2().setProvider((String) saml2.get("provider"));
|
||||
}
|
||||
if (saml2.containsKey("autoCreateUser")) {
|
||||
GeneralUtils.saveKeyToSettings("security.saml2.autoCreateUser", saml2.get("autoCreateUser"));
|
||||
applicationProperties.getSecurity().getSaml2().setAutoCreateUser((Boolean) saml2.get("autoCreateUser"));
|
||||
}
|
||||
if (saml2.containsKey("blockRegistration")) {
|
||||
GeneralUtils.saveKeyToSettings("security.saml2.blockRegistration", saml2.get("blockRegistration"));
|
||||
applicationProperties.getSecurity().getSaml2().setBlockRegistration((Boolean) saml2.get("blockRegistration"));
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.ok("Connection settings updated. Restart required for changes to take effect.");
|
||||
}
|
||||
|
||||
// ========== PRIVACY SETTINGS ==========
|
||||
|
||||
@GetMapping("/admin/settings/privacy")
|
||||
@Hidden
|
||||
public ResponseEntity<Map<String, Object>> getPrivacySettings() {
|
||||
Map<String, Object> settings = new HashMap<>();
|
||||
|
||||
settings.put("enableAnalytics", applicationProperties.getSystem().getEnableAnalytics());
|
||||
settings.put("googleVisibility", applicationProperties.getSystem().getGooglevisibility());
|
||||
settings.put("metricsEnabled", applicationProperties.getMetrics().getEnabled());
|
||||
|
||||
return ResponseEntity.ok(settings);
|
||||
}
|
||||
|
||||
@PostMapping("/admin/settings/privacy")
|
||||
@Hidden
|
||||
public ResponseEntity<String> updatePrivacySettings(@RequestBody Map<String, Object> settings) throws IOException {
|
||||
if (settings.containsKey("enableAnalytics")) {
|
||||
GeneralUtils.saveKeyToSettings("system.enableAnalytics", settings.get("enableAnalytics"));
|
||||
applicationProperties.getSystem().setEnableAnalytics((Boolean) settings.get("enableAnalytics"));
|
||||
}
|
||||
if (settings.containsKey("googleVisibility")) {
|
||||
GeneralUtils.saveKeyToSettings("system.googlevisibility", settings.get("googleVisibility"));
|
||||
applicationProperties.getSystem().setGooglevisibility((Boolean) settings.get("googleVisibility"));
|
||||
}
|
||||
if (settings.containsKey("metricsEnabled")) {
|
||||
GeneralUtils.saveKeyToSettings("metrics.enabled", settings.get("metricsEnabled"));
|
||||
applicationProperties.getMetrics().setEnabled((Boolean) settings.get("metricsEnabled"));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok("Privacy settings updated. Restart required for changes to take effect.");
|
||||
}
|
||||
|
||||
// ========== ADVANCED SETTINGS ==========
|
||||
|
||||
@GetMapping("/admin/settings/advanced")
|
||||
@Hidden
|
||||
public ResponseEntity<Map<String, Object>> getAdvancedSettings() {
|
||||
Map<String, Object> settings = new HashMap<>();
|
||||
|
||||
settings.put("endpoints", applicationProperties.getEndpoints());
|
||||
settings.put("enableAlphaFunctionality", applicationProperties.getSystem().getEnableAlphaFunctionality());
|
||||
settings.put("maxDPI", applicationProperties.getSystem().getMaxDPI());
|
||||
settings.put("enableUrlToPDF", applicationProperties.getSystem().getEnableUrlToPDF());
|
||||
settings.put("customPaths", applicationProperties.getSystem().getCustomPaths());
|
||||
settings.put("tempFileManagement", applicationProperties.getSystem().getTempFileManagement());
|
||||
|
||||
return ResponseEntity.ok(settings);
|
||||
}
|
||||
|
||||
@PostMapping("/admin/settings/advanced")
|
||||
@Hidden
|
||||
public ResponseEntity<String> updateAdvancedSettings(@RequestBody Map<String, Object> settings) throws IOException {
|
||||
if (settings.containsKey("enableAlphaFunctionality")) {
|
||||
GeneralUtils.saveKeyToSettings("system.enableAlphaFunctionality", settings.get("enableAlphaFunctionality"));
|
||||
applicationProperties.getSystem().setEnableAlphaFunctionality((Boolean) settings.get("enableAlphaFunctionality"));
|
||||
}
|
||||
if (settings.containsKey("maxDPI")) {
|
||||
GeneralUtils.saveKeyToSettings("system.maxDPI", settings.get("maxDPI"));
|
||||
applicationProperties.getSystem().setMaxDPI((Integer) settings.get("maxDPI"));
|
||||
}
|
||||
if (settings.containsKey("enableUrlToPDF")) {
|
||||
GeneralUtils.saveKeyToSettings("system.enableUrlToPDF", settings.get("enableUrlToPDF"));
|
||||
applicationProperties.getSystem().setEnableUrlToPDF((Boolean) settings.get("enableUrlToPDF"));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok("Advanced settings updated. Restart required for changes to take effect.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -3207,8 +3207,218 @@
|
||||
"activity": "Activity",
|
||||
"account": "Account",
|
||||
"config": "Config",
|
||||
"adminSettings": "Admin Settings",
|
||||
"allTools": "All Tools"
|
||||
},
|
||||
"admin": {
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"status": {
|
||||
"active": "Active",
|
||||
"inactive": "Inactive"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Admin Settings",
|
||||
"workspace": "Workspace",
|
||||
"fetchError": "Failed to load settings",
|
||||
"saveError": "Failed to save settings",
|
||||
"saved": "Settings saved successfully",
|
||||
"save": "Save Changes",
|
||||
"restartRequired": "Settings changes require a server restart to take effect.",
|
||||
"general": {
|
||||
"title": "General",
|
||||
"description": "Configure general application settings including branding and default behaviour.",
|
||||
"ui": "User Interface",
|
||||
"system": "System",
|
||||
"appName": "Application Name",
|
||||
"appName.description": "The name displayed in the browser tab and home page",
|
||||
"appNameNavbar": "Navbar Brand",
|
||||
"appNameNavbar.description": "The name displayed in the navigation bar",
|
||||
"homeDescription": "Home Description",
|
||||
"homeDescription.description": "The description text shown on the home page",
|
||||
"defaultLocale": "Default Locale",
|
||||
"defaultLocale.description": "The default language for new users (e.g., en_US, es_ES)",
|
||||
"fileUploadLimit": "File Upload Limit",
|
||||
"fileUploadLimit.description": "Maximum file upload size (e.g., 100MB, 1GB)",
|
||||
"showUpdate": "Show Update Notifications",
|
||||
"showUpdate.description": "Display notifications when a new version is available",
|
||||
"showUpdateOnlyAdmin": "Show Updates to Admins Only",
|
||||
"showUpdateOnlyAdmin.description": "Restrict update notifications to admin users only",
|
||||
"customHTMLFiles": "Custom HTML Files",
|
||||
"customHTMLFiles.description": "Allow serving custom HTML files from the customFiles directory",
|
||||
"languages": "Available Languages",
|
||||
"languages.description": "Languages that users can select from (leave empty to enable all languages)"
|
||||
},
|
||||
"security": {
|
||||
"title": "Security",
|
||||
"description": "Configure authentication, login behaviour, and security policies.",
|
||||
"authentication": "Authentication",
|
||||
"enableLogin": "Enable Login",
|
||||
"enableLogin.description": "Require users to log in before accessing the application",
|
||||
"loginMethod": "Login Method",
|
||||
"loginMethod.description": "The authentication method to use for user login",
|
||||
"loginMethod.all": "All Methods",
|
||||
"loginMethod.normal": "Username/Password Only",
|
||||
"loginMethod.oauth2": "OAuth2 Only",
|
||||
"loginMethod.saml2": "SAML2 Only",
|
||||
"loginAttemptCount": "Login Attempt Limit",
|
||||
"loginAttemptCount.description": "Maximum number of failed login attempts before account lockout",
|
||||
"loginResetTimeMinutes": "Login Reset Time (minutes)",
|
||||
"loginResetTimeMinutes.description": "Time before failed login attempts are reset",
|
||||
"csrfDisabled": "Disable CSRF Protection",
|
||||
"csrfDisabled.description": "Disable Cross-Site Request Forgery protection (not recommended)",
|
||||
"initialLogin": "Initial Login",
|
||||
"initialLogin.username": "Initial Username",
|
||||
"initialLogin.username.description": "The username for the initial admin account",
|
||||
"initialLogin.password": "Initial Password",
|
||||
"initialLogin.password.description": "The password for the initial admin account",
|
||||
"jwt": "JWT Configuration",
|
||||
"jwt.secureCookie": "Secure Cookie",
|
||||
"jwt.secureCookie.description": "Require HTTPS for JWT cookies (recommended for production)",
|
||||
"jwt.keyRetentionDays": "Key Retention Days",
|
||||
"jwt.keyRetentionDays.description": "Number of days to retain old JWT keys for verification",
|
||||
"jwt.persistence": "Enable Key Persistence",
|
||||
"jwt.persistence.description": "Store JWT keys persistently to survive server restarts",
|
||||
"jwt.enableKeyRotation": "Enable Key Rotation",
|
||||
"jwt.enableKeyRotation.description": "Automatically rotate JWT signing keys periodically",
|
||||
"jwt.enableKeyCleanup": "Enable Key Cleanup",
|
||||
"jwt.enableKeyCleanup.description": "Automatically remove expired JWT keys"
|
||||
},
|
||||
"connections": {
|
||||
"title": "Connections",
|
||||
"description": "Configure external authentication providers like OAuth2 and SAML.",
|
||||
"oauth2": "OAuth2",
|
||||
"oauth2.enabled": "Enable OAuth2",
|
||||
"oauth2.enabled.description": "Allow users to authenticate using OAuth2 providers",
|
||||
"oauth2.provider": "Provider",
|
||||
"oauth2.provider.description": "The OAuth2 provider to use for authentication",
|
||||
"oauth2.issuer": "Issuer URL",
|
||||
"oauth2.issuer.description": "The OAuth2 provider issuer URL",
|
||||
"oauth2.clientId": "Client ID",
|
||||
"oauth2.clientId.description": "The OAuth2 client ID from your provider",
|
||||
"oauth2.clientSecret": "Client Secret",
|
||||
"oauth2.clientSecret.description": "The OAuth2 client secret from your provider",
|
||||
"oauth2.useAsUsername": "Use as Username",
|
||||
"oauth2.useAsUsername.description": "The OAuth2 claim to use as the username (e.g., email, sub)",
|
||||
"oauth2.autoCreateUser": "Auto Create Users",
|
||||
"oauth2.autoCreateUser.description": "Automatically create user accounts on first OAuth2 login",
|
||||
"oauth2.blockRegistration": "Block Registration",
|
||||
"oauth2.blockRegistration.description": "Prevent new user registration via OAuth2",
|
||||
"oauth2.scopes": "OAuth2 Scopes",
|
||||
"oauth2.scopes.description": "Comma-separated list of OAuth2 scopes to request (e.g., openid, profile, email)",
|
||||
"saml2": "SAML2",
|
||||
"saml2.enabled": "Enable SAML2",
|
||||
"saml2.enabled.description": "Allow users to authenticate using SAML2 providers",
|
||||
"saml2.provider": "Provider",
|
||||
"saml2.provider.description": "The SAML2 provider name",
|
||||
"saml2.registrationId": "Registration ID",
|
||||
"saml2.registrationId.description": "The SAML2 registration identifier",
|
||||
"saml2.autoCreateUser": "Auto Create Users",
|
||||
"saml2.autoCreateUser.description": "Automatically create user accounts on first SAML2 login",
|
||||
"saml2.blockRegistration": "Block Registration",
|
||||
"saml2.blockRegistration.description": "Prevent new user registration via SAML2"
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Privacy",
|
||||
"description": "Configure privacy and data collection settings.",
|
||||
"analytics": "Analytics & Tracking",
|
||||
"enableAnalytics": "Enable Analytics",
|
||||
"enableAnalytics.description": "Collect anonymous usage analytics to help improve the application",
|
||||
"metricsEnabled": "Enable Metrics",
|
||||
"metricsEnabled.description": "Enable collection of performance and usage metrics",
|
||||
"searchEngine": "Search Engine Visibility",
|
||||
"googleVisibility": "Google Visibility",
|
||||
"googleVisibility.description": "Allow search engines to index this application"
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Advanced",
|
||||
"description": "Configure advanced features and experimental functionality.",
|
||||
"features": "Feature Flags",
|
||||
"processing": "Processing",
|
||||
"endpoints": "Endpoints",
|
||||
"endpoints.manage": "Manage API Endpoints",
|
||||
"endpoints.description": "Endpoint management is configured via YAML. See documentation for details on enabling/disabling specific endpoints.",
|
||||
"enableAlphaFunctionality": "Enable Alpha Features",
|
||||
"enableAlphaFunctionality.description": "Enable experimental and alpha-stage features (may be unstable)",
|
||||
"enableUrlToPDF": "Enable URL to PDF",
|
||||
"enableUrlToPDF.description": "Allow conversion of web pages to PDF documents",
|
||||
"maxDPI": "Maximum DPI",
|
||||
"maxDPI.description": "Maximum DPI for image processing (0 = unlimited)",
|
||||
"tessdataDir": "Tessdata Directory",
|
||||
"tessdataDir.description": "Path to the tessdata directory for OCR language files",
|
||||
"disableSanitize": "Disable HTML Sanitization",
|
||||
"disableSanitize.description": "WARNING: Security risk - disabling HTML sanitization can lead to XSS vulnerabilities"
|
||||
},
|
||||
"mail": {
|
||||
"title": "Mail Server",
|
||||
"description": "Configure SMTP settings for sending email notifications.",
|
||||
"smtp": "SMTP Configuration",
|
||||
"enabled": "Enable Mail",
|
||||
"enabled.description": "Enable email notifications and SMTP functionality",
|
||||
"host": "SMTP Host",
|
||||
"host.description": "The hostname or IP address of your SMTP server",
|
||||
"port": "SMTP Port",
|
||||
"port.description": "The port number for SMTP connection (typically 25, 465, or 587)",
|
||||
"username": "SMTP Username",
|
||||
"username.description": "Username for SMTP authentication",
|
||||
"password": "SMTP Password",
|
||||
"password.description": "Password for SMTP authentication",
|
||||
"from": "From Address",
|
||||
"from.description": "The email address to use as the sender"
|
||||
},
|
||||
"legal": {
|
||||
"title": "Legal Documents",
|
||||
"description": "Configure links to legal documents and policies.",
|
||||
"termsAndConditions": "Terms and Conditions",
|
||||
"termsAndConditions.description": "URL or filename to terms and conditions",
|
||||
"privacyPolicy": "Privacy Policy",
|
||||
"privacyPolicy.description": "URL or filename to privacy policy",
|
||||
"accessibilityStatement": "Accessibility Statement",
|
||||
"accessibilityStatement.description": "URL or filename to accessibility statement",
|
||||
"cookiePolicy": "Cookie Policy",
|
||||
"cookiePolicy.description": "URL or filename to cookie policy",
|
||||
"impressum": "Impressum",
|
||||
"impressum.description": "URL or filename to impressum (required in some jurisdictions)"
|
||||
},
|
||||
"premium": {
|
||||
"title": "Premium & Enterprise",
|
||||
"description": "Configure premium and enterprise features.",
|
||||
"license": "License",
|
||||
"key": "License Key",
|
||||
"key.description": "Enter your premium or enterprise license key",
|
||||
"enabled": "Enable Premium Features",
|
||||
"enabled.description": "Enable license key checks for pro/enterprise features",
|
||||
"proFeatures": "Pro Features",
|
||||
"ssoAutoLogin": "SSO Auto Login",
|
||||
"ssoAutoLogin.description": "Automatically redirect to SSO login",
|
||||
"customMetadata.autoUpdate": "Auto Update Metadata",
|
||||
"customMetadata.autoUpdate.description": "Automatically update PDF metadata",
|
||||
"customMetadata.author": "Default Author",
|
||||
"customMetadata.author.description": "Default author for PDF metadata",
|
||||
"customMetadata.creator": "Default Creator",
|
||||
"customMetadata.creator.description": "Default creator for PDF metadata",
|
||||
"customMetadata.producer": "Default Producer",
|
||||
"customMetadata.producer.description": "Default producer for PDF metadata",
|
||||
"enterpriseFeatures": "Enterprise Features",
|
||||
"audit.enabled": "Enable Audit Logging",
|
||||
"audit.enabled.description": "Track user actions and system events",
|
||||
"audit.level": "Audit Level",
|
||||
"audit.level.description": "0=OFF, 1=BASIC, 2=STANDARD, 3=VERBOSE",
|
||||
"audit.retentionDays": "Audit Retention (days)",
|
||||
"audit.retentionDays.description": "Number of days to retain audit logs"
|
||||
},
|
||||
"endpoints": {
|
||||
"title": "API Endpoints",
|
||||
"description": "Control which API endpoints and endpoint groups are available.",
|
||||
"management": "Endpoint Management",
|
||||
"toRemove": "Disabled Endpoints",
|
||||
"toRemove.description": "Select individual endpoints to disable",
|
||||
"groupsToRemove": "Disabled Endpoint Groups",
|
||||
"groupsToRemove.description": "Select endpoint groups to disable",
|
||||
"note": "Note: Disabling endpoints restricts API access but does not remove UI components. Restart required for changes to take effect."
|
||||
}
|
||||
}
|
||||
},
|
||||
"fileUpload": {
|
||||
"selectFile": "Select a file",
|
||||
"selectFiles": "Select files",
|
||||
|
||||
@ -44,13 +44,17 @@ const AppConfigModal: React.FC<AppConfigModalProps> = ({ opened, onClose }) => {
|
||||
console.log('Logout placeholder for SaaS compatibility');
|
||||
};
|
||||
|
||||
// TODO: Replace with actual role check from JWT/auth context
|
||||
const isAdmin = true; // Emulated admin role for now
|
||||
|
||||
// Left navigation structure and icons
|
||||
const configNavSections = useMemo(() =>
|
||||
createConfigNavSections(
|
||||
Overview,
|
||||
handleLogout
|
||||
handleLogout,
|
||||
isAdmin
|
||||
),
|
||||
[]
|
||||
[isAdmin]
|
||||
);
|
||||
|
||||
const activeLabel = useMemo(() => {
|
||||
|
||||
@ -2,6 +2,15 @@ import React from 'react';
|
||||
import { NavKey } from './types';
|
||||
import HotkeysSection from './configSections/HotkeysSection';
|
||||
import GeneralSection from './configSections/GeneralSection';
|
||||
import AdminGeneralSection from './configSections/AdminGeneralSection';
|
||||
import AdminSecuritySection from './configSections/AdminSecuritySection';
|
||||
import AdminConnectionsSection from './configSections/AdminConnectionsSection';
|
||||
import AdminPrivacySection from './configSections/AdminPrivacySection';
|
||||
import AdminAdvancedSection from './configSections/AdminAdvancedSection';
|
||||
import AdminMailSection from './configSections/AdminMailSection';
|
||||
import AdminLegalSection from './configSections/AdminLegalSection';
|
||||
import AdminPremiumSection from './configSections/AdminPremiumSection';
|
||||
import AdminEndpointsSection from './configSections/AdminEndpointsSection';
|
||||
|
||||
export interface ConfigNavItem {
|
||||
key: NavKey;
|
||||
@ -27,7 +36,8 @@ export interface ConfigColors {
|
||||
|
||||
export const createConfigNavSections = (
|
||||
Overview: React.ComponentType<{ onLogoutClick: () => void }>,
|
||||
onLogoutClick: () => void
|
||||
onLogoutClick: () => void,
|
||||
isAdmin: boolean = false
|
||||
): ConfigNavSection[] => {
|
||||
const sections: ConfigNavSection[] = [
|
||||
{
|
||||
@ -60,5 +70,68 @@ export const createConfigNavSections = (
|
||||
},
|
||||
];
|
||||
|
||||
// Add Admin Settings section if user is admin
|
||||
if (isAdmin) {
|
||||
sections.push({
|
||||
title: 'Admin Settings',
|
||||
items: [
|
||||
{
|
||||
key: 'adminGeneral',
|
||||
label: 'General',
|
||||
icon: 'settings-rounded',
|
||||
component: <AdminGeneralSection />
|
||||
},
|
||||
{
|
||||
key: 'adminSecurity',
|
||||
label: 'Security',
|
||||
icon: 'shield-rounded',
|
||||
component: <AdminSecuritySection />
|
||||
},
|
||||
{
|
||||
key: 'adminConnections',
|
||||
label: 'Connections',
|
||||
icon: 'link-rounded',
|
||||
component: <AdminConnectionsSection />
|
||||
},
|
||||
{
|
||||
key: 'adminMail',
|
||||
label: 'Mail',
|
||||
icon: 'mail-rounded',
|
||||
component: <AdminMailSection />
|
||||
},
|
||||
{
|
||||
key: 'adminLegal',
|
||||
label: 'Legal',
|
||||
icon: 'gavel-rounded',
|
||||
component: <AdminLegalSection />
|
||||
},
|
||||
{
|
||||
key: 'adminPrivacy',
|
||||
label: 'Privacy',
|
||||
icon: 'visibility-rounded',
|
||||
component: <AdminPrivacySection />
|
||||
},
|
||||
{
|
||||
key: 'adminPremium',
|
||||
label: 'Premium',
|
||||
icon: 'star-rounded',
|
||||
component: <AdminPremiumSection />
|
||||
},
|
||||
{
|
||||
key: 'adminEndpoints',
|
||||
label: 'Endpoints',
|
||||
icon: 'api-rounded',
|
||||
component: <AdminEndpointsSection />
|
||||
},
|
||||
{
|
||||
key: 'adminAdvanced',
|
||||
label: 'Advanced',
|
||||
icon: 'tune-rounded',
|
||||
component: <AdminAdvancedSection />
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return sections;
|
||||
};
|
||||
@ -0,0 +1,202 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NumberInput, Switch, Button, Stack, Paper, Text, Loader, Group, Accordion, TextInput } from '@mantine/core';
|
||||
import { alert } from '../../../toast';
|
||||
|
||||
interface AdvancedSettingsData {
|
||||
enableAlphaFunctionality?: boolean;
|
||||
maxDPI?: number;
|
||||
enableUrlToPDF?: boolean;
|
||||
tessdataDir?: string;
|
||||
disableSanitize?: boolean;
|
||||
}
|
||||
|
||||
export default function AdminAdvancedSection() {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [settings, setSettings] = useState<AdvancedSettingsData>({});
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
|
||||
const fetchSettings = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/settings/section/system');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setSettings({
|
||||
enableAlphaFunctionality: data.enableAlphaFunctionality || false,
|
||||
maxDPI: data.maxDPI || 0,
|
||||
enableUrlToPDF: data.enableUrlToPDF || false,
|
||||
tessdataDir: data.tessdataDir || '',
|
||||
disableSanitize: data.disableSanitize || false
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch advanced settings:', error);
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('admin.error', 'Error'),
|
||||
body: t('admin.settings.fetchError', 'Failed to load settings'),
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
// Use delta update endpoint with dot notation
|
||||
const deltaSettings = {
|
||||
'system.enableAlphaFunctionality': settings.enableAlphaFunctionality,
|
||||
'system.maxDPI': settings.maxDPI,
|
||||
'system.enableUrlToPDF': settings.enableUrlToPDF,
|
||||
'system.tessdataDir': settings.tessdataDir,
|
||||
'system.disableSanitize': settings.disableSanitize
|
||||
};
|
||||
|
||||
const response = await fetch('/api/v1/admin/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ settings: deltaSettings }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert({
|
||||
alertType: 'success',
|
||||
title: t('admin.success', 'Success'),
|
||||
body: t('admin.settings.saved', 'Settings saved. Restart required for changes to take effect.'),
|
||||
});
|
||||
} else {
|
||||
throw new Error('Failed to save');
|
||||
}
|
||||
} catch (error) {
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('admin.error', 'Error'),
|
||||
body: t('admin.settings.saveError', 'Failed to save settings'),
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Stack align="center" justify="center" h={200}>
|
||||
<Loader size="lg" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Text fw={600} size="lg">{t('admin.settings.advanced.title', 'Advanced')}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('admin.settings.advanced.description', 'Configure advanced features and experimental functionality.')}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Feature Flags */}
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Text fw={600} size="sm" mb="xs">{t('admin.settings.advanced.features', 'Feature Flags')}</Text>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.advanced.enableAlphaFunctionality', 'Enable Alpha Features')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.advanced.enableAlphaFunctionality.description', 'Enable experimental and alpha-stage features (may be unstable)')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.enableAlphaFunctionality || false}
|
||||
onChange={(e) => setSettings({ ...settings, enableAlphaFunctionality: e.target.checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.advanced.enableUrlToPDF', 'Enable URL to PDF')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.advanced.enableUrlToPDF.description', 'Allow conversion of web pages to PDF documents (internal use only)')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.enableUrlToPDF || false}
|
||||
onChange={(e) => setSettings({ ...settings, enableUrlToPDF: e.target.checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.advanced.disableSanitize', 'Disable HTML Sanitization')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.advanced.disableSanitize.description', 'Disable HTML sanitization (WARNING: Security risk - can lead to XSS injections)')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.disableSanitize || false}
|
||||
onChange={(e) => setSettings({ ...settings, disableSanitize: e.target.checked })}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Processing Settings */}
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Text fw={600} size="sm" mb="xs">{t('admin.settings.advanced.processing', 'Processing')}</Text>
|
||||
|
||||
<div>
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.maxDPI', 'Maximum DPI')}
|
||||
description={t('admin.settings.advanced.maxDPI.description', 'Maximum DPI for image processing (0 = unlimited)')}
|
||||
value={settings.maxDPI || 0}
|
||||
onChange={(value) => setSettings({ ...settings, maxDPI: Number(value) })}
|
||||
min={0}
|
||||
max={3000}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.advanced.tessdataDir', 'Tessdata Directory')}
|
||||
description={t('admin.settings.advanced.tessdataDir.description', 'Path to the directory containing Tessdata files for OCR')}
|
||||
value={settings.tessdataDir || ''}
|
||||
onChange={(e) => setSettings({ ...settings, tessdataDir: e.target.value })}
|
||||
placeholder="/usr/share/tessdata"
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Endpoints Info */}
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Accordion variant="separated">
|
||||
<Accordion.Item value="endpoints">
|
||||
<Accordion.Control>
|
||||
{t('admin.settings.advanced.endpoints.manage', 'Manage API Endpoints')}
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('admin.settings.advanced.endpoints.description', 'Endpoint management is configured via YAML. See documentation for details on enabling/disabling specific endpoints.')}
|
||||
</Text>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
</Paper>
|
||||
|
||||
{/* Save Button */}
|
||||
<Group justify="flex-end">
|
||||
<Button onClick={handleSave} loading={saving} size="sm">
|
||||
{t('admin.settings.save', 'Save Changes')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,341 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TextInput, Switch, Button, Stack, Paper, Text, Loader, Group, Select, Badge, PasswordInput } from '@mantine/core';
|
||||
import { alert } from '../../../toast';
|
||||
import LocalIcon from '../../LocalIcon';
|
||||
|
||||
interface OAuth2Settings {
|
||||
enabled?: boolean;
|
||||
issuer?: string;
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
provider?: string;
|
||||
autoCreateUser?: boolean;
|
||||
blockRegistration?: boolean;
|
||||
useAsUsername?: string;
|
||||
scopes?: string;
|
||||
}
|
||||
|
||||
interface SAML2Settings {
|
||||
enabled?: boolean;
|
||||
provider?: string;
|
||||
autoCreateUser?: boolean;
|
||||
blockRegistration?: boolean;
|
||||
registrationId?: string;
|
||||
}
|
||||
|
||||
interface ConnectionsSettingsData {
|
||||
oauth2?: OAuth2Settings;
|
||||
saml2?: SAML2Settings;
|
||||
}
|
||||
|
||||
export default function AdminConnectionsSection() {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [settings, setSettings] = useState<ConnectionsSettingsData>({});
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
|
||||
const fetchSettings = async () => {
|
||||
try {
|
||||
// OAuth2 and SAML2 are nested under security section
|
||||
const response = await fetch('/api/v1/admin/settings/section/security');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
// Extract oauth2 and saml2 from security section
|
||||
setSettings({
|
||||
oauth2: data.oauth2 || {},
|
||||
saml2: data.saml2 || {}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch connections settings:', error);
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('admin.error', 'Error'),
|
||||
body: t('admin.settings.fetchError', 'Failed to load settings'),
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
// Use delta update endpoint with dot notation for nested oauth2/saml2 settings
|
||||
const deltaSettings: Record<string, any> = {};
|
||||
|
||||
// Convert oauth2 settings to dot notation
|
||||
if (settings.oauth2) {
|
||||
Object.keys(settings.oauth2).forEach(key => {
|
||||
deltaSettings[`security.oauth2.${key}`] = (settings.oauth2 as any)[key];
|
||||
});
|
||||
}
|
||||
|
||||
// Convert saml2 settings to dot notation
|
||||
if (settings.saml2) {
|
||||
Object.keys(settings.saml2).forEach(key => {
|
||||
deltaSettings[`security.saml2.${key}`] = (settings.saml2 as any)[key];
|
||||
});
|
||||
}
|
||||
|
||||
const response = await fetch('/api/v1/admin/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ settings: deltaSettings }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert({
|
||||
alertType: 'success',
|
||||
title: t('admin.success', 'Success'),
|
||||
body: t('admin.settings.saved', 'Settings saved. Restart required for changes to take effect.'),
|
||||
});
|
||||
} else {
|
||||
throw new Error('Failed to save');
|
||||
}
|
||||
} catch (error) {
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('admin.error', 'Error'),
|
||||
body: t('admin.settings.saveError', 'Failed to save settings'),
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Stack align="center" justify="center" h={200}>
|
||||
<Loader size="lg" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const getProviderIcon = (provider?: string) => {
|
||||
switch (provider?.toLowerCase()) {
|
||||
case 'google':
|
||||
return <LocalIcon icon="google-rounded" width="1rem" height="1rem" />;
|
||||
case 'github':
|
||||
return <LocalIcon icon="github-rounded" width="1rem" height="1rem" />;
|
||||
case 'keycloak':
|
||||
return <LocalIcon icon="key-rounded" width="1rem" height="1rem" />;
|
||||
default:
|
||||
return <LocalIcon icon="cloud-rounded" width="1rem" height="1rem" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Text fw={600} size="lg">{t('admin.settings.connections.title', 'Connections')}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('admin.settings.connections.description', 'Configure external authentication providers like OAuth2 and SAML.')}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* OAuth2 Settings */}
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between">
|
||||
<Group gap="xs">
|
||||
<LocalIcon icon="cloud-rounded" width="1.25rem" height="1.25rem" />
|
||||
<Text fw={600} size="sm">{t('admin.settings.connections.oauth2', 'OAuth2')}</Text>
|
||||
</Group>
|
||||
<Badge color={settings.oauth2?.enabled ? 'green' : 'gray'} size="sm">
|
||||
{settings.oauth2?.enabled ? t('admin.status.active', 'Active') : t('admin.status.inactive', 'Inactive')}
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.connections.oauth2.enabled', 'Enable OAuth2')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.connections.oauth2.enabled.description', 'Allow users to authenticate using OAuth2 providers')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.oauth2?.enabled || false}
|
||||
onChange={(e) => setSettings({ ...settings, oauth2: { ...settings.oauth2, enabled: e.target.checked } })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Select
|
||||
label={t('admin.settings.connections.oauth2.provider', 'Provider')}
|
||||
description={t('admin.settings.connections.oauth2.provider.description', 'The OAuth2 provider to use for authentication')}
|
||||
value={settings.oauth2?.provider || ''}
|
||||
onChange={(value) => setSettings({ ...settings, oauth2: { ...settings.oauth2, provider: value || '' } })}
|
||||
data={[
|
||||
{ value: 'google', label: 'Google' },
|
||||
{ value: 'github', label: 'GitHub' },
|
||||
{ value: 'keycloak', label: 'Keycloak' },
|
||||
]}
|
||||
leftSection={getProviderIcon(settings.oauth2?.provider)}
|
||||
comboboxProps={{ zIndex: 1400 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.connections.oauth2.issuer', 'Issuer URL')}
|
||||
description={t('admin.settings.connections.oauth2.issuer.description', 'The OAuth2 provider issuer URL')}
|
||||
value={settings.oauth2?.issuer || ''}
|
||||
onChange={(e) => setSettings({ ...settings, oauth2: { ...settings.oauth2, issuer: e.target.value } })}
|
||||
placeholder="https://accounts.google.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.connections.oauth2.clientId', 'Client ID')}
|
||||
description={t('admin.settings.connections.oauth2.clientId.description', 'The OAuth2 client ID from your provider')}
|
||||
value={settings.oauth2?.clientId || ''}
|
||||
onChange={(e) => setSettings({ ...settings, oauth2: { ...settings.oauth2, clientId: e.target.value } })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<PasswordInput
|
||||
label={t('admin.settings.connections.oauth2.clientSecret', 'Client Secret')}
|
||||
description={t('admin.settings.connections.oauth2.clientSecret.description', 'The OAuth2 client secret from your provider')}
|
||||
value={settings.oauth2?.clientSecret || ''}
|
||||
onChange={(e) => setSettings({ ...settings, oauth2: { ...settings.oauth2, clientSecret: e.target.value } })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.connections.oauth2.autoCreateUser', 'Auto Create Users')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.connections.oauth2.autoCreateUser.description', 'Automatically create user accounts on first OAuth2 login')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.oauth2?.autoCreateUser || false}
|
||||
onChange={(e) => setSettings({ ...settings, oauth2: { ...settings.oauth2, autoCreateUser: e.target.checked } })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.connections.oauth2.blockRegistration', 'Block Registration')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.connections.oauth2.blockRegistration.description', 'Prevent new user registration via OAuth2')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.oauth2?.blockRegistration || false}
|
||||
onChange={(e) => setSettings({ ...settings, oauth2: { ...settings.oauth2, blockRegistration: e.target.checked } })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.connections.oauth2.useAsUsername', 'Use as Username')}
|
||||
description={t('admin.settings.connections.oauth2.useAsUsername.description', 'The OAuth2 claim to use as the username (e.g., email, sub)')}
|
||||
value={settings.oauth2?.useAsUsername || ''}
|
||||
onChange={(e) => setSettings({ ...settings, oauth2: { ...settings.oauth2, useAsUsername: e.target.value } })}
|
||||
placeholder="email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.connections.oauth2.scopes', 'Scopes')}
|
||||
description={t('admin.settings.connections.oauth2.scopes.description', 'OAuth2 scopes (comma-separated, e.g., openid, profile, email)')}
|
||||
value={settings.oauth2?.scopes || ''}
|
||||
onChange={(e) => setSettings({ ...settings, oauth2: { ...settings.oauth2, scopes: e.target.value } })}
|
||||
placeholder="openid, profile, email"
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* SAML2 Settings */}
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between">
|
||||
<Group gap="xs">
|
||||
<LocalIcon icon="key-rounded" width="1.25rem" height="1.25rem" />
|
||||
<Text fw={600} size="sm">{t('admin.settings.connections.saml2', 'SAML2')}</Text>
|
||||
</Group>
|
||||
<Badge color={settings.saml2?.enabled ? 'green' : 'gray'} size="sm">
|
||||
{settings.saml2?.enabled ? t('admin.status.active', 'Active') : t('admin.status.inactive', 'Inactive')}
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.connections.saml2.enabled', 'Enable SAML2')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.connections.saml2.enabled.description', 'Allow users to authenticate using SAML2 providers')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.saml2?.enabled || false}
|
||||
onChange={(e) => setSettings({ ...settings, saml2: { ...settings.saml2, enabled: e.target.checked } })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.connections.saml2.provider', 'Provider')}
|
||||
description={t('admin.settings.connections.saml2.provider.description', 'The SAML2 provider name')}
|
||||
value={settings.saml2?.provider || ''}
|
||||
onChange={(e) => setSettings({ ...settings, saml2: { ...settings.saml2, provider: e.target.value } })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.connections.saml2.registrationId', 'Registration ID')}
|
||||
description={t('admin.settings.connections.saml2.registrationId.description', 'The SAML2 registration identifier')}
|
||||
value={settings.saml2?.registrationId || ''}
|
||||
onChange={(e) => setSettings({ ...settings, saml2: { ...settings.saml2, registrationId: e.target.value } })}
|
||||
placeholder="stirling"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.connections.saml2.autoCreateUser', 'Auto Create Users')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.connections.saml2.autoCreateUser.description', 'Automatically create user accounts on first SAML2 login')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.saml2?.autoCreateUser || false}
|
||||
onChange={(e) => setSettings({ ...settings, saml2: { ...settings.saml2, autoCreateUser: e.target.checked } })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.connections.saml2.blockRegistration', 'Block Registration')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.connections.saml2.blockRegistration.description', 'Prevent new user registration via SAML2')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.saml2?.blockRegistration || false}
|
||||
onChange={(e) => setSettings({ ...settings, saml2: { ...settings.saml2, blockRegistration: e.target.checked } })}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Save Button */}
|
||||
<Group justify="flex-end">
|
||||
<Button onClick={handleSave} loading={saving} size="sm">
|
||||
{t('admin.settings.save', 'Save Changes')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,180 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Stack, Paper, Text, Loader, Group, MultiSelect } from '@mantine/core';
|
||||
import { alert } from '../../../toast';
|
||||
|
||||
interface EndpointsSettingsData {
|
||||
toRemove?: string[];
|
||||
groupsToRemove?: string[];
|
||||
}
|
||||
|
||||
export default function AdminEndpointsSection() {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [settings, setSettings] = useState<EndpointsSettingsData>({});
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
|
||||
const fetchSettings = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/settings/section/endpoints');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setSettings(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch endpoints settings:', error);
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('admin.error', 'Error'),
|
||||
body: t('admin.settings.fetchError', 'Failed to load settings'),
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/settings/section/endpoints', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(settings),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert({
|
||||
alertType: 'success',
|
||||
title: t('admin.success', 'Success'),
|
||||
body: t('admin.settings.saved', 'Settings saved. Restart required for changes to take effect.'),
|
||||
});
|
||||
} else {
|
||||
throw new Error('Failed to save');
|
||||
}
|
||||
} catch (error) {
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('admin.error', 'Error'),
|
||||
body: t('admin.settings.saveError', 'Failed to save settings'),
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Stack align="center" justify="center" h={200}>
|
||||
<Loader size="lg" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
// Common endpoint examples
|
||||
const commonEndpoints = [
|
||||
'img-to-pdf',
|
||||
'pdf-to-img',
|
||||
'merge-pdfs',
|
||||
'split-pdf',
|
||||
'rotate-pdf',
|
||||
'compress-pdf',
|
||||
'extract-images',
|
||||
'extract-image-scans',
|
||||
'add-watermark',
|
||||
'remove-watermark',
|
||||
'add-password',
|
||||
'remove-password',
|
||||
'change-permissions',
|
||||
'ocr-pdf',
|
||||
'pdf-to-pdfa',
|
||||
'html-to-pdf',
|
||||
'url-to-pdf',
|
||||
'markdown-to-pdf',
|
||||
'get-info-on-pdf',
|
||||
'extract-pdf-metadata',
|
||||
'pdf-to-single-page',
|
||||
'crop',
|
||||
'auto-split-pdf',
|
||||
'sanitize-pdf',
|
||||
'add-page-numbers',
|
||||
'auto-rename',
|
||||
'scale-pages',
|
||||
'repair',
|
||||
'flatten',
|
||||
'remove-blanks',
|
||||
'compare-pdfs'
|
||||
];
|
||||
|
||||
// Common endpoint groups
|
||||
const commonGroups = [
|
||||
'Conversion',
|
||||
'Security',
|
||||
'Other',
|
||||
'Organize',
|
||||
'LibreOffice',
|
||||
'CLI',
|
||||
'Python',
|
||||
'OpenCV'
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Text fw={600} size="lg">{t('admin.settings.endpoints.title', 'API Endpoints')}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('admin.settings.endpoints.description', 'Control which API endpoints and endpoint groups are available.')}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Text fw={600} size="sm" mb="xs">{t('admin.settings.endpoints.management', 'Endpoint Management')}</Text>
|
||||
|
||||
<div>
|
||||
<MultiSelect
|
||||
label={t('admin.settings.endpoints.toRemove', 'Disabled Endpoints')}
|
||||
description={t('admin.settings.endpoints.toRemove.description', 'Select individual endpoints to disable')}
|
||||
value={settings.toRemove || []}
|
||||
onChange={(value) => setSettings({ ...settings, toRemove: value })}
|
||||
data={commonEndpoints.map(endpoint => ({ value: endpoint, label: endpoint }))}
|
||||
searchable
|
||||
clearable
|
||||
placeholder="Select endpoints to disable"
|
||||
comboboxProps={{ zIndex: 1400 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<MultiSelect
|
||||
label={t('admin.settings.endpoints.groupsToRemove', 'Disabled Endpoint Groups')}
|
||||
description={t('admin.settings.endpoints.groupsToRemove.description', 'Select endpoint groups to disable')}
|
||||
value={settings.groupsToRemove || []}
|
||||
onChange={(value) => setSettings({ ...settings, groupsToRemove: value })}
|
||||
data={commonGroups.map(group => ({ value: group, label: group }))}
|
||||
searchable
|
||||
clearable
|
||||
placeholder="Select groups to disable"
|
||||
comboboxProps={{ zIndex: 1400 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Paper bg="var(--mantine-color-blue-light)" p="sm" radius="sm">
|
||||
<Text size="xs" c="dimmed">
|
||||
{t('admin.settings.endpoints.note', 'Note: Disabling endpoints restricts API access but does not remove UI components. Restart required for changes to take effect.')}
|
||||
</Text>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Group justify="flex-end">
|
||||
<Button onClick={handleSave} loading={saving} size="sm">
|
||||
{t('admin.settings.save', 'Save Changes')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,250 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TextInput, Switch, Button, Stack, Paper, Text, Loader, Group, MultiSelect } from '@mantine/core';
|
||||
import { alert } from '../../../toast';
|
||||
|
||||
interface GeneralSettingsData {
|
||||
ui: {
|
||||
appName?: string;
|
||||
homeDescription?: string;
|
||||
appNameNavbar?: string;
|
||||
languages?: string[];
|
||||
};
|
||||
system: {
|
||||
defaultLocale?: string;
|
||||
showUpdate?: boolean;
|
||||
showUpdateOnlyAdmin?: boolean;
|
||||
customHTMLFiles?: boolean;
|
||||
fileUploadLimit?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function AdminGeneralSection() {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [settings, setSettings] = useState<GeneralSettingsData>({
|
||||
ui: {},
|
||||
system: {},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
|
||||
const fetchSettings = async () => {
|
||||
try {
|
||||
// Fetch both ui and system sections from proprietary admin API
|
||||
const [uiResponse, systemResponse] = await Promise.all([
|
||||
fetch('/api/v1/admin/settings/section/ui'),
|
||||
fetch('/api/v1/admin/settings/section/system')
|
||||
]);
|
||||
|
||||
if (uiResponse.ok && systemResponse.ok) {
|
||||
const ui = await uiResponse.json();
|
||||
const system = await systemResponse.json();
|
||||
setSettings({ ui, system });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch general settings:', error);
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('admin.error', 'Error'),
|
||||
body: t('admin.settings.fetchError', 'Failed to load settings'),
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
// Save both ui and system sections separately using proprietary admin API
|
||||
const [uiResponse, systemResponse] = await Promise.all([
|
||||
fetch('/api/v1/admin/settings/section/ui', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(settings.ui),
|
||||
}),
|
||||
fetch('/api/v1/admin/settings/section/system', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(settings.system),
|
||||
})
|
||||
]);
|
||||
|
||||
if (uiResponse.ok && systemResponse.ok) {
|
||||
alert({
|
||||
alertType: 'success',
|
||||
title: t('admin.success', 'Success'),
|
||||
body: t('admin.settings.saved', 'Settings saved. Restart required for changes to take effect.'),
|
||||
});
|
||||
} else {
|
||||
throw new Error('Failed to save');
|
||||
}
|
||||
} catch (error) {
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('admin.error', 'Error'),
|
||||
body: t('admin.settings.saveError', 'Failed to save settings'),
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Stack align="center" justify="center" h={200}>
|
||||
<Loader size="lg" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Text fw={600} size="lg">{t('admin.settings.general.title', 'General')}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('admin.settings.general.description', 'Configure general application settings including branding and default behaviour.')}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* UI Settings */}
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Text fw={600} size="sm" mb="xs">{t('admin.settings.general.ui', 'User Interface')}</Text>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.general.appName', 'Application Name')}
|
||||
description={t('admin.settings.general.appName.description', 'The name displayed in the browser tab and home page')}
|
||||
value={settings.ui.appName || ''}
|
||||
onChange={(e) => setSettings({ ...settings, ui: { ...settings.ui, appName: e.target.value } })}
|
||||
placeholder="Stirling PDF"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.general.appNameNavbar', 'Navbar Brand')}
|
||||
description={t('admin.settings.general.appNameNavbar.description', 'The name displayed in the navigation bar')}
|
||||
value={settings.ui.appNameNavbar || ''}
|
||||
onChange={(e) => setSettings({ ...settings, ui: { ...settings.ui, appNameNavbar: e.target.value } })}
|
||||
placeholder="Stirling PDF"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.general.homeDescription', 'Home Description')}
|
||||
description={t('admin.settings.general.homeDescription.description', 'The description text shown on the home page')}
|
||||
value={settings.ui.homeDescription || ''}
|
||||
onChange={(e) => setSettings({ ...settings, ui: { ...settings.ui, homeDescription: e.target.value } })}
|
||||
placeholder="Your locally hosted one-stop-shop for all your PDF needs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<MultiSelect
|
||||
label={t('admin.settings.general.languages', 'Available Languages')}
|
||||
description={t('admin.settings.general.languages.description', 'Limit which languages are available (empty = all languages)')}
|
||||
value={settings.ui.languages || []}
|
||||
onChange={(value) => setSettings({ ...settings, ui: { ...settings.ui, languages: value } })}
|
||||
data={[
|
||||
{ value: 'de_DE', label: 'Deutsch' },
|
||||
{ value: 'es_ES', label: 'Español' },
|
||||
{ value: 'fr_FR', label: 'Français' },
|
||||
{ value: 'it_IT', label: 'Italiano' },
|
||||
{ value: 'pl_PL', label: 'Polski' },
|
||||
{ value: 'pt_BR', label: 'Português (Brasil)' },
|
||||
{ value: 'ru_RU', label: 'Русский' },
|
||||
{ value: 'zh_CN', label: '简体中文' },
|
||||
{ value: 'ja_JP', label: '日本語' },
|
||||
{ value: 'ko_KR', label: '한국어' },
|
||||
]}
|
||||
searchable
|
||||
clearable
|
||||
placeholder="Select languages"
|
||||
comboboxProps={{ zIndex: 1400 }}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* System Settings */}
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Text fw={600} size="sm" mb="xs">{t('admin.settings.general.system', 'System')}</Text>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.general.defaultLocale', 'Default Locale')}
|
||||
description={t('admin.settings.general.defaultLocale.description', 'The default language for new users (e.g., en_US, es_ES)')}
|
||||
value={settings.system.defaultLocale || ''}
|
||||
onChange={(e) => setSettings({ ...settings, system: { ...settings.system, defaultLocale: e.target.value } })}
|
||||
placeholder="en_US"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.general.fileUploadLimit', 'File Upload Limit')}
|
||||
description={t('admin.settings.general.fileUploadLimit.description', 'Maximum file upload size (e.g., 100MB, 1GB)')}
|
||||
value={settings.system.fileUploadLimit || ''}
|
||||
onChange={(e) => setSettings({ ...settings, system: { ...settings.system, fileUploadLimit: e.target.value } })}
|
||||
placeholder="100MB"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.general.showUpdate', 'Show Update Notifications')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.general.showUpdate.description', 'Display notifications when a new version is available')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.system.showUpdate || false}
|
||||
onChange={(e) => setSettings({ ...settings, system: { ...settings.system, showUpdate: e.target.checked } })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.general.showUpdateOnlyAdmin', 'Show Updates to Admins Only')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.general.showUpdateOnlyAdmin.description', 'Restrict update notifications to admin users only')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.system.showUpdateOnlyAdmin || false}
|
||||
onChange={(e) => setSettings({ ...settings, system: { ...settings.system, showUpdateOnlyAdmin: e.target.checked } })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.general.customHTMLFiles', 'Custom HTML Files')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.general.customHTMLFiles.description', 'Allow serving custom HTML files from the customFiles directory')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.system.customHTMLFiles || false}
|
||||
onChange={(e) => setSettings({ ...settings, system: { ...settings.system, customHTMLFiles: e.target.checked } })}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Save Button */}
|
||||
<Group justify="flex-end">
|
||||
<Button onClick={handleSave} loading={saving} size="sm">
|
||||
{t('admin.settings.save', 'Save Changes')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,150 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TextInput, Button, Stack, Paper, Text, Loader, Group } from '@mantine/core';
|
||||
import { alert } from '../../../toast';
|
||||
|
||||
interface LegalSettingsData {
|
||||
termsAndConditions?: string;
|
||||
privacyPolicy?: string;
|
||||
accessibilityStatement?: string;
|
||||
cookiePolicy?: string;
|
||||
impressum?: string;
|
||||
}
|
||||
|
||||
export default function AdminLegalSection() {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [settings, setSettings] = useState<LegalSettingsData>({});
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
|
||||
const fetchSettings = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/settings/section/legal');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setSettings(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch legal settings:', error);
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('admin.error', 'Error'),
|
||||
body: t('admin.settings.fetchError', 'Failed to load settings'),
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/settings/section/legal', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(settings),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert({
|
||||
alertType: 'success',
|
||||
title: t('admin.success', 'Success'),
|
||||
body: t('admin.settings.saved', 'Settings saved. Restart required for changes to take effect.'),
|
||||
});
|
||||
} else {
|
||||
throw new Error('Failed to save');
|
||||
}
|
||||
} catch (error) {
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('admin.error', 'Error'),
|
||||
body: t('admin.settings.saveError', 'Failed to save settings'),
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Stack align="center" justify="center" h={200}>
|
||||
<Loader size="lg" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Text fw={600} size="lg">{t('admin.settings.legal.title', 'Legal Documents')}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('admin.settings.legal.description', 'Configure links to legal documents and policies.')}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.legal.termsAndConditions', 'Terms and Conditions')}
|
||||
description={t('admin.settings.legal.termsAndConditions.description', 'URL or filename to terms and conditions')}
|
||||
value={settings.termsAndConditions || ''}
|
||||
onChange={(e) => setSettings({ ...settings, termsAndConditions: e.target.value })}
|
||||
placeholder="https://example.com/terms"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.legal.privacyPolicy', 'Privacy Policy')}
|
||||
description={t('admin.settings.legal.privacyPolicy.description', 'URL or filename to privacy policy')}
|
||||
value={settings.privacyPolicy || ''}
|
||||
onChange={(e) => setSettings({ ...settings, privacyPolicy: e.target.value })}
|
||||
placeholder="https://example.com/privacy"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.legal.accessibilityStatement', 'Accessibility Statement')}
|
||||
description={t('admin.settings.legal.accessibilityStatement.description', 'URL or filename to accessibility statement')}
|
||||
value={settings.accessibilityStatement || ''}
|
||||
onChange={(e) => setSettings({ ...settings, accessibilityStatement: e.target.value })}
|
||||
placeholder="https://example.com/accessibility"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.legal.cookiePolicy', 'Cookie Policy')}
|
||||
description={t('admin.settings.legal.cookiePolicy.description', 'URL or filename to cookie policy')}
|
||||
value={settings.cookiePolicy || ''}
|
||||
onChange={(e) => setSettings({ ...settings, cookiePolicy: e.target.value })}
|
||||
placeholder="https://example.com/cookies"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.legal.impressum', 'Impressum')}
|
||||
description={t('admin.settings.legal.impressum.description', 'URL or filename to impressum (required in some jurisdictions)')}
|
||||
value={settings.impressum || ''}
|
||||
onChange={(e) => setSettings({ ...settings, impressum: e.target.value })}
|
||||
placeholder="https://example.com/impressum"
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Group justify="flex-end">
|
||||
<Button onClick={handleSave} loading={saving} size="sm">
|
||||
{t('admin.settings.save', 'Save Changes')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,163 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TextInput, NumberInput, Switch, Button, Stack, Paper, Text, Loader, Group, PasswordInput } from '@mantine/core';
|
||||
import { alert } from '../../../toast';
|
||||
|
||||
interface MailSettingsData {
|
||||
enabled?: boolean;
|
||||
host?: string;
|
||||
port?: number;
|
||||
username?: string;
|
||||
password?: string;
|
||||
from?: string;
|
||||
}
|
||||
|
||||
export default function AdminMailSection() {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [settings, setSettings] = useState<MailSettingsData>({});
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
|
||||
const fetchSettings = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/settings/section/mail');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setSettings(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch mail settings:', error);
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('admin.error', 'Error'),
|
||||
body: t('admin.settings.fetchError', 'Failed to load settings'),
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/settings/section/mail', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(settings),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert({
|
||||
alertType: 'success',
|
||||
title: t('admin.success', 'Success'),
|
||||
body: t('admin.settings.saved', 'Settings saved. Restart required for changes to take effect.'),
|
||||
});
|
||||
} else {
|
||||
throw new Error('Failed to save');
|
||||
}
|
||||
} catch (error) {
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('admin.error', 'Error'),
|
||||
body: t('admin.settings.saveError', 'Failed to save settings'),
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Stack align="center" justify="center" h={200}>
|
||||
<Loader size="lg" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Text fw={600} size="lg">{t('admin.settings.mail.title', 'Mail Configuration')}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('admin.settings.mail.description', 'Configure SMTP settings for email notifications.')}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.mail.enabled', 'Enable Mail')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.mail.enabled.description', 'Enable email notifications and SMTP functionality')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.enabled || false}
|
||||
onChange={(e) => setSettings({ ...settings, enabled: e.target.checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.mail.host', 'SMTP Host')}
|
||||
description={t('admin.settings.mail.host.description', 'SMTP server hostname')}
|
||||
value={settings.host || ''}
|
||||
onChange={(e) => setSettings({ ...settings, host: e.target.value })}
|
||||
placeholder="smtp.example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<NumberInput
|
||||
label={t('admin.settings.mail.port', 'SMTP Port')}
|
||||
description={t('admin.settings.mail.port.description', 'SMTP server port (typically 587 for TLS, 465 for SSL)')}
|
||||
value={settings.port || 587}
|
||||
onChange={(value) => setSettings({ ...settings, port: Number(value) })}
|
||||
min={1}
|
||||
max={65535}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.mail.username', 'SMTP Username')}
|
||||
description={t('admin.settings.mail.username.description', 'SMTP authentication username')}
|
||||
value={settings.username || ''}
|
||||
onChange={(e) => setSettings({ ...settings, username: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<PasswordInput
|
||||
label={t('admin.settings.mail.password', 'SMTP Password')}
|
||||
description={t('admin.settings.mail.password.description', 'SMTP authentication password')}
|
||||
value={settings.password || ''}
|
||||
onChange={(e) => setSettings({ ...settings, password: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.mail.from', 'From Address')}
|
||||
description={t('admin.settings.mail.from.description', 'Email address to use as sender')}
|
||||
value={settings.from || ''}
|
||||
onChange={(e) => setSettings({ ...settings, from: e.target.value })}
|
||||
placeholder="noreply@example.com"
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Group justify="flex-end">
|
||||
<Button onClick={handleSave} loading={saving} size="sm">
|
||||
{t('admin.settings.save', 'Save Changes')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,310 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TextInput, NumberInput, Switch, Button, Stack, Paper, Text, Loader, Group } from '@mantine/core';
|
||||
import { alert } from '../../../toast';
|
||||
|
||||
interface PremiumSettingsData {
|
||||
key?: string;
|
||||
enabled?: boolean;
|
||||
proFeatures?: {
|
||||
SSOAutoLogin?: boolean;
|
||||
CustomMetadata?: {
|
||||
autoUpdateMetadata?: boolean;
|
||||
author?: string;
|
||||
creator?: string;
|
||||
producer?: string;
|
||||
};
|
||||
};
|
||||
enterpriseFeatures?: {
|
||||
audit?: {
|
||||
enabled?: boolean;
|
||||
level?: number;
|
||||
retentionDays?: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default function AdminPremiumSection() {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [settings, setSettings] = useState<PremiumSettingsData>({});
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
|
||||
const fetchSettings = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/settings/section/premium');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setSettings(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch premium settings:', error);
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('admin.error', 'Error'),
|
||||
body: t('admin.settings.fetchError', 'Failed to load settings'),
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/settings/section/premium', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(settings),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert({
|
||||
alertType: 'success',
|
||||
title: t('admin.success', 'Success'),
|
||||
body: t('admin.settings.saved', 'Settings saved. Restart required for changes to take effect.'),
|
||||
});
|
||||
} else {
|
||||
throw new Error('Failed to save');
|
||||
}
|
||||
} catch (error) {
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('admin.error', 'Error'),
|
||||
body: t('admin.settings.saveError', 'Failed to save settings'),
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Stack align="center" justify="center" h={200}>
|
||||
<Loader size="lg" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Text fw={600} size="lg">{t('admin.settings.premium.title', 'Premium & Enterprise')}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('admin.settings.premium.description', 'Configure premium and enterprise features.')}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* License */}
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Text fw={600} size="sm" mb="xs">{t('admin.settings.premium.license', 'License')}</Text>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.premium.key', 'License Key')}
|
||||
description={t('admin.settings.premium.key.description', 'Enter your premium or enterprise license key')}
|
||||
value={settings.key || ''}
|
||||
onChange={(e) => setSettings({ ...settings, key: e.target.value })}
|
||||
placeholder="00000000-0000-0000-0000-000000000000"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.premium.enabled', 'Enable Premium Features')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.premium.enabled.description', 'Enable license key checks for pro/enterprise features')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.enabled || false}
|
||||
onChange={(e) => setSettings({ ...settings, enabled: e.target.checked })}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Pro Features */}
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Text fw={600} size="sm" mb="xs">{t('admin.settings.premium.proFeatures', 'Pro Features')}</Text>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.premium.ssoAutoLogin', 'SSO Auto Login')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.premium.ssoAutoLogin.description', 'Automatically redirect to SSO login')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.proFeatures?.SSOAutoLogin || false}
|
||||
onChange={(e) => setSettings({
|
||||
...settings,
|
||||
proFeatures: { ...settings.proFeatures, SSOAutoLogin: e.target.checked }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.premium.customMetadata.autoUpdate', 'Auto Update Metadata')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.premium.customMetadata.autoUpdate.description', 'Automatically update PDF metadata')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.proFeatures?.CustomMetadata?.autoUpdateMetadata || false}
|
||||
onChange={(e) => setSettings({
|
||||
...settings,
|
||||
proFeatures: {
|
||||
...settings.proFeatures,
|
||||
CustomMetadata: {
|
||||
...settings.proFeatures?.CustomMetadata,
|
||||
autoUpdateMetadata: e.target.checked
|
||||
}
|
||||
}
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.premium.customMetadata.author', 'Default Author')}
|
||||
description={t('admin.settings.premium.customMetadata.author.description', 'Default author for PDF metadata')}
|
||||
value={settings.proFeatures?.CustomMetadata?.author || ''}
|
||||
onChange={(e) => setSettings({
|
||||
...settings,
|
||||
proFeatures: {
|
||||
...settings.proFeatures,
|
||||
CustomMetadata: {
|
||||
...settings.proFeatures?.CustomMetadata,
|
||||
author: e.target.value
|
||||
}
|
||||
}
|
||||
})}
|
||||
placeholder="username"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.premium.customMetadata.creator', 'Default Creator')}
|
||||
description={t('admin.settings.premium.customMetadata.creator.description', 'Default creator for PDF metadata')}
|
||||
value={settings.proFeatures?.CustomMetadata?.creator || ''}
|
||||
onChange={(e) => setSettings({
|
||||
...settings,
|
||||
proFeatures: {
|
||||
...settings.proFeatures,
|
||||
CustomMetadata: {
|
||||
...settings.proFeatures?.CustomMetadata,
|
||||
creator: e.target.value
|
||||
}
|
||||
}
|
||||
})}
|
||||
placeholder="Stirling-PDF"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.premium.customMetadata.producer', 'Default Producer')}
|
||||
description={t('admin.settings.premium.customMetadata.producer.description', 'Default producer for PDF metadata')}
|
||||
value={settings.proFeatures?.CustomMetadata?.producer || ''}
|
||||
onChange={(e) => setSettings({
|
||||
...settings,
|
||||
proFeatures: {
|
||||
...settings.proFeatures,
|
||||
CustomMetadata: {
|
||||
...settings.proFeatures?.CustomMetadata,
|
||||
producer: e.target.value
|
||||
}
|
||||
}
|
||||
})}
|
||||
placeholder="Stirling-PDF"
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Enterprise Features */}
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Text fw={600} size="sm" mb="xs">{t('admin.settings.premium.enterpriseFeatures', 'Enterprise Features')}</Text>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.premium.audit.enabled', 'Enable Audit Logging')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.premium.audit.enabled.description', 'Track user actions and system events')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.enterpriseFeatures?.audit?.enabled || false}
|
||||
onChange={(e) => setSettings({
|
||||
...settings,
|
||||
enterpriseFeatures: {
|
||||
...settings.enterpriseFeatures,
|
||||
audit: {
|
||||
...settings.enterpriseFeatures?.audit,
|
||||
enabled: e.target.checked
|
||||
}
|
||||
}
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<NumberInput
|
||||
label={t('admin.settings.premium.audit.level', 'Audit Level')}
|
||||
description={t('admin.settings.premium.audit.level.description', '0=OFF, 1=BASIC, 2=STANDARD, 3=VERBOSE')}
|
||||
value={settings.enterpriseFeatures?.audit?.level || 2}
|
||||
onChange={(value) => setSettings({
|
||||
...settings,
|
||||
enterpriseFeatures: {
|
||||
...settings.enterpriseFeatures,
|
||||
audit: {
|
||||
...settings.enterpriseFeatures?.audit,
|
||||
level: Number(value)
|
||||
}
|
||||
}
|
||||
})}
|
||||
min={0}
|
||||
max={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<NumberInput
|
||||
label={t('admin.settings.premium.audit.retentionDays', 'Audit Retention (days)')}
|
||||
description={t('admin.settings.premium.audit.retentionDays.description', 'Number of days to retain audit logs')}
|
||||
value={settings.enterpriseFeatures?.audit?.retentionDays || 90}
|
||||
onChange={(value) => setSettings({
|
||||
...settings,
|
||||
enterpriseFeatures: {
|
||||
...settings.enterpriseFeatures,
|
||||
audit: {
|
||||
...settings.enterpriseFeatures?.audit,
|
||||
retentionDays: Number(value)
|
||||
}
|
||||
}
|
||||
})}
|
||||
min={1}
|
||||
max={3650}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Group justify="flex-end">
|
||||
<Button onClick={handleSave} loading={saving} size="sm">
|
||||
{t('admin.settings.save', 'Save Changes')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,166 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Switch, Button, Stack, Paper, Text, Loader, Group } from '@mantine/core';
|
||||
import { alert } from '../../../toast';
|
||||
|
||||
interface PrivacySettingsData {
|
||||
enableAnalytics?: boolean;
|
||||
googleVisibility?: boolean;
|
||||
metricsEnabled?: boolean;
|
||||
}
|
||||
|
||||
export default function AdminPrivacySection() {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [settings, setSettings] = useState<PrivacySettingsData>({});
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
|
||||
const fetchSettings = async () => {
|
||||
try {
|
||||
// Fetch metrics and system sections
|
||||
const [metricsResponse, systemResponse] = await Promise.all([
|
||||
fetch('/api/v1/admin/settings/section/metrics'),
|
||||
fetch('/api/v1/admin/settings/section/system')
|
||||
]);
|
||||
|
||||
if (metricsResponse.ok && systemResponse.ok) {
|
||||
const metrics = await metricsResponse.json();
|
||||
const system = await systemResponse.json();
|
||||
|
||||
setSettings({
|
||||
enableAnalytics: system.enableAnalytics || false,
|
||||
googleVisibility: system.googlevisibility || false,
|
||||
metricsEnabled: metrics.enabled || false
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch privacy settings:', error);
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('admin.error', 'Error'),
|
||||
body: t('admin.settings.fetchError', 'Failed to load settings'),
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
// Use delta update endpoint with dot notation for cross-section settings
|
||||
const deltaSettings = {
|
||||
'system.enableAnalytics': settings.enableAnalytics,
|
||||
'system.googlevisibility': settings.googleVisibility,
|
||||
'metrics.enabled': settings.metricsEnabled
|
||||
};
|
||||
|
||||
const response = await fetch('/api/v1/admin/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ settings: deltaSettings }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert({
|
||||
alertType: 'success',
|
||||
title: t('admin.success', 'Success'),
|
||||
body: t('admin.settings.saved', 'Settings saved. Restart required for changes to take effect.'),
|
||||
});
|
||||
} else {
|
||||
throw new Error('Failed to save');
|
||||
}
|
||||
} catch (error) {
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('admin.error', 'Error'),
|
||||
body: t('admin.settings.saveError', 'Failed to save settings'),
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Stack align="center" justify="center" h={200}>
|
||||
<Loader size="lg" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Text fw={600} size="lg">{t('admin.settings.privacy.title', 'Privacy')}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('admin.settings.privacy.description', 'Configure privacy and data collection settings.')}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Analytics & Tracking */}
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Text fw={600} size="sm" mb="xs">{t('admin.settings.privacy.analytics', 'Analytics & Tracking')}</Text>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.privacy.enableAnalytics', 'Enable Analytics')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.privacy.enableAnalytics.description', 'Collect anonymous usage analytics to help improve the application')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.enableAnalytics || false}
|
||||
onChange={(e) => setSettings({ ...settings, enableAnalytics: e.target.checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.privacy.metricsEnabled', 'Enable Metrics')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.privacy.metricsEnabled.description', 'Enable collection of performance and usage metrics')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.metricsEnabled || false}
|
||||
onChange={(e) => setSettings({ ...settings, metricsEnabled: e.target.checked })}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Search Engine Visibility */}
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Text fw={600} size="sm" mb="xs">{t('admin.settings.privacy.searchEngine', 'Search Engine Visibility')}</Text>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.privacy.googleVisibility', 'Google Visibility')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.privacy.googleVisibility.description', 'Allow search engines to index this application')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.googleVisibility || false}
|
||||
onChange={(e) => setSettings({ ...settings, googleVisibility: e.target.checked })}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Save Button */}
|
||||
<Group justify="flex-end">
|
||||
<Button onClick={handleSave} loading={saving} size="sm">
|
||||
{t('admin.settings.save', 'Save Changes')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,276 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TextInput, NumberInput, Switch, Button, Stack, Paper, Text, Loader, Group, Select, PasswordInput } from '@mantine/core';
|
||||
import { alert } from '../../../toast';
|
||||
|
||||
interface SecuritySettingsData {
|
||||
enableLogin?: boolean;
|
||||
csrfDisabled?: boolean;
|
||||
loginMethod?: string;
|
||||
loginAttemptCount?: number;
|
||||
loginResetTimeMinutes?: number;
|
||||
initialLogin?: {
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
jwt?: {
|
||||
persistence?: boolean;
|
||||
enableKeyRotation?: boolean;
|
||||
enableKeyCleanup?: boolean;
|
||||
keyRetentionDays?: number;
|
||||
secureCookie?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export default function AdminSecuritySection() {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [settings, setSettings] = useState<SecuritySettingsData>({});
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
|
||||
const fetchSettings = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/settings/section/security');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setSettings(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch security settings:', error);
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('admin.error', 'Error'),
|
||||
body: t('admin.settings.fetchError', 'Failed to load settings'),
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/settings/section/security', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(settings),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert({
|
||||
alertType: 'success',
|
||||
title: t('admin.success', 'Success'),
|
||||
body: t('admin.settings.saved', 'Settings saved. Restart required for changes to take effect.'),
|
||||
});
|
||||
} else {
|
||||
throw new Error('Failed to save');
|
||||
}
|
||||
} catch (error) {
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('admin.error', 'Error'),
|
||||
body: t('admin.settings.saveError', 'Failed to save settings'),
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Stack align="center" justify="center" h={200}>
|
||||
<Loader size="lg" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Text fw={600} size="lg">{t('admin.settings.security.title', 'Security')}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('admin.settings.security.description', 'Configure authentication, login behaviour, and security policies.')}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Authentication Settings */}
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Text fw={600} size="sm" mb="xs">{t('admin.settings.security.authentication', 'Authentication')}</Text>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.enableLogin', 'Enable Login')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.security.enableLogin.description', 'Require users to log in before accessing the application')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.enableLogin || false}
|
||||
onChange={(e) => setSettings({ ...settings, enableLogin: e.target.checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Select
|
||||
label={t('admin.settings.security.loginMethod', 'Login Method')}
|
||||
description={t('admin.settings.security.loginMethod.description', 'The authentication method to use for user login')}
|
||||
value={settings.loginMethod || 'all'}
|
||||
onChange={(value) => setSettings({ ...settings, loginMethod: value || 'all' })}
|
||||
data={[
|
||||
{ value: 'all', label: t('admin.settings.security.loginMethod.all', 'All Methods') },
|
||||
{ value: 'normal', label: t('admin.settings.security.loginMethod.normal', 'Username/Password Only') },
|
||||
{ value: 'oauth2', label: t('admin.settings.security.loginMethod.oauth2', 'OAuth2 Only') },
|
||||
{ value: 'saml2', label: t('admin.settings.security.loginMethod.saml2', 'SAML2 Only') },
|
||||
]}
|
||||
comboboxProps={{ zIndex: 1400 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<NumberInput
|
||||
label={t('admin.settings.security.loginAttemptCount', 'Login Attempt Limit')}
|
||||
description={t('admin.settings.security.loginAttemptCount.description', 'Maximum number of failed login attempts before account lockout')}
|
||||
value={settings.loginAttemptCount || 0}
|
||||
onChange={(value) => setSettings({ ...settings, loginAttemptCount: Number(value) })}
|
||||
min={0}
|
||||
max={100}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<NumberInput
|
||||
label={t('admin.settings.security.loginResetTimeMinutes', 'Login Reset Time (minutes)')}
|
||||
description={t('admin.settings.security.loginResetTimeMinutes.description', 'Time before failed login attempts are reset')}
|
||||
value={settings.loginResetTimeMinutes || 0}
|
||||
onChange={(value) => setSettings({ ...settings, loginResetTimeMinutes: Number(value) })}
|
||||
min={0}
|
||||
max={1440}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.csrfDisabled', 'Disable CSRF Protection')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.security.csrfDisabled.description', 'Disable Cross-Site Request Forgery protection (not recommended)')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.csrfDisabled || false}
|
||||
onChange={(e) => setSettings({ ...settings, csrfDisabled: e.target.checked })}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Initial Login Credentials */}
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Text fw={600} size="sm" mb="xs">{t('admin.settings.security.initialLogin', 'Initial Login')}</Text>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.security.initialLogin.username', 'Initial Username')}
|
||||
description={t('admin.settings.security.initialLogin.username.description', 'Default admin username for first-time setup')}
|
||||
value={settings.initialLogin?.username || ''}
|
||||
onChange={(e) => setSettings({ ...settings, initialLogin: { ...settings.initialLogin, username: e.target.value } })}
|
||||
placeholder="admin"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<PasswordInput
|
||||
label={t('admin.settings.security.initialLogin.password', 'Initial Password')}
|
||||
description={t('admin.settings.security.initialLogin.password.description', 'Default admin password for first-time setup')}
|
||||
value={settings.initialLogin?.password || ''}
|
||||
onChange={(e) => setSettings({ ...settings, initialLogin: { ...settings.initialLogin, password: e.target.value } })}
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* JWT Settings */}
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Text fw={600} size="sm" mb="xs">{t('admin.settings.security.jwt', 'JWT Configuration')}</Text>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.jwt.persistence', 'Enable Key Persistence')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.security.jwt.persistence.description', 'Store JWT keys persistently (required for multi-instance deployments)')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.jwt?.persistence || false}
|
||||
onChange={(e) => setSettings({ ...settings, jwt: { ...settings.jwt, persistence: e.target.checked } })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.jwt.enableKeyRotation', 'Enable Key Rotation')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.security.jwt.enableKeyRotation.description', 'Automatically rotate JWT signing keys for improved security')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.jwt?.enableKeyRotation || false}
|
||||
onChange={(e) => setSettings({ ...settings, jwt: { ...settings.jwt, enableKeyRotation: e.target.checked } })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.jwt.enableKeyCleanup', 'Enable Key Cleanup')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.security.jwt.enableKeyCleanup.description', 'Automatically remove old JWT keys after retention period')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.jwt?.enableKeyCleanup || false}
|
||||
onChange={(e) => setSettings({ ...settings, jwt: { ...settings.jwt, enableKeyCleanup: e.target.checked } })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<NumberInput
|
||||
label={t('admin.settings.security.jwt.keyRetentionDays', 'Key Retention Days')}
|
||||
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) } })}
|
||||
min={1}
|
||||
max={365}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.jwt.secureCookie', 'Secure Cookie')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.security.jwt.secureCookie.description', 'Require HTTPS for JWT cookies (recommended for production)')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.jwt?.secureCookie || false}
|
||||
onChange={(e) => setSettings({ ...settings, jwt: { ...settings.jwt, secureCookie: e.target.checked } })}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Save Button */}
|
||||
<Group justify="flex-end">
|
||||
<Button onClick={handleSave} loading={saving} size="sm">
|
||||
{t('admin.settings.save', 'Save Changes')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@ -13,7 +13,16 @@ export type NavKey =
|
||||
| 'requests'
|
||||
| 'developer'
|
||||
| 'api-keys'
|
||||
| 'hotkeys';
|
||||
| 'hotkeys'
|
||||
| 'adminGeneral'
|
||||
| 'adminSecurity'
|
||||
| 'adminConnections'
|
||||
| 'adminPrivacy'
|
||||
| 'adminAdvanced'
|
||||
| 'adminMail'
|
||||
| 'adminLegal'
|
||||
| 'adminPremium'
|
||||
| 'adminEndpoints';
|
||||
|
||||
|
||||
// some of these are not used yet, but appear in figma designs
|
||||
Loading…
Reference in New Issue
Block a user