mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-04-16 23:08:38 +02:00
option to hide google drive and add settings (#5863)
Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
This commit is contained in:
@@ -713,6 +713,7 @@ public class ApplicationProperties {
|
||||
private String logoStyle = "classic"; // Options: "classic" (default) or "modern"
|
||||
private boolean defaultHideUnavailableTools = false;
|
||||
private boolean defaultHideUnavailableConversions = false;
|
||||
private HideDisabledTools hideDisabledTools = new HideDisabledTools();
|
||||
|
||||
public String getAppNameNavbar() {
|
||||
return appNameNavbar != null && !appNameNavbar.trim().isEmpty() ? appNameNavbar : null;
|
||||
@@ -725,6 +726,12 @@ public class ApplicationProperties {
|
||||
}
|
||||
return "classic"; // default
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class HideDisabledTools {
|
||||
private boolean googleDrive = false;
|
||||
private boolean mobileQRScanner = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@@ -912,6 +919,15 @@ public class ApplicationProperties {
|
||||
private boolean ssoAutoLogin;
|
||||
private boolean database;
|
||||
private CustomMetadata customMetadata = new CustomMetadata();
|
||||
private GoogleDrive googleDrive = new GoogleDrive();
|
||||
|
||||
@Data
|
||||
public static class GoogleDrive {
|
||||
private boolean enabled = false;
|
||||
private String clientId = "";
|
||||
private String apiKey = "";
|
||||
private String appId = "";
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class CustomMetadata {
|
||||
|
||||
@@ -159,6 +159,24 @@ public class ConfigController {
|
||||
"defaultHideUnavailableConversions",
|
||||
applicationProperties.getUi().isDefaultHideUnavailableConversions());
|
||||
|
||||
// Hide disabled tools settings
|
||||
configData.put(
|
||||
"hideDisabledToolsGoogleDrive",
|
||||
applicationProperties.getUi().getHideDisabledTools().isGoogleDrive());
|
||||
configData.put(
|
||||
"hideDisabledToolsMobileQRScanner",
|
||||
applicationProperties.getUi().getHideDisabledTools().isMobileQRScanner());
|
||||
|
||||
// Google Drive backend settings (only if enabled)
|
||||
ApplicationProperties.Premium.ProFeatures.GoogleDrive googleDrive =
|
||||
applicationProperties.getPremium().getProFeatures().getGoogleDrive();
|
||||
if (googleDrive.isEnabled()) {
|
||||
configData.put("googleDriveEnabled", true);
|
||||
configData.put("googleDriveClientId", googleDrive.getClientId());
|
||||
configData.put("googleDriveApiKey", googleDrive.getApiKey());
|
||||
configData.put("googleDriveAppId", googleDrive.getAppId());
|
||||
}
|
||||
|
||||
// Security settings
|
||||
// enableLogin requires both the config flag AND proprietary features to be loaded
|
||||
// If userService is null, proprietary module isn't loaded
|
||||
|
||||
@@ -96,6 +96,11 @@ premium:
|
||||
author: username
|
||||
creator: Stirling-PDF
|
||||
producer: Stirling-PDF
|
||||
googleDrive:
|
||||
enabled: false # Enable Google Drive file picker integration
|
||||
clientId: "" # Google OAuth 2.0 client ID (obtain from Google Cloud Console)
|
||||
apiKey: "" # Google API key for Google Picker API (obtain from Google Cloud Console)
|
||||
appId: "" # Google Drive app ID
|
||||
enterpriseFeatures:
|
||||
audit:
|
||||
enabled: true # Enable audit logging for security and compliance tracking
|
||||
@@ -238,6 +243,9 @@ ui:
|
||||
languages: [] # If empty, all languages are enabled. To display only German and Polish ["de_DE", "pl_PL"]. British English is always enabled.
|
||||
defaultHideUnavailableTools: false # Default user preference: hide disabled tools instead of greying them out
|
||||
defaultHideUnavailableConversions: false # Default user preference: hide disabled conversion options instead of greying them out
|
||||
hideDisabledTools:
|
||||
googleDrive: false # Hide Google Drive button when not enabled
|
||||
mobileQRScanner: false # Hide mobile QR scanner button when not enabled
|
||||
|
||||
endpoints:
|
||||
toRemove: [] # list endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
|
||||
|
||||
@@ -7323,3 +7323,204 @@ title = "Team Members"
|
||||
button = "Upgrade to Team"
|
||||
description = "Unlock team collaboration features"
|
||||
title = "Upgrade to Team Plan"
|
||||
|
||||
[provider.oauth2.google]
|
||||
scope = "Sign-in authentication"
|
||||
|
||||
[provider.oauth2.google.clientId]
|
||||
label = "Client ID"
|
||||
description = "The OAuth2 client ID from Google Cloud Console"
|
||||
|
||||
[provider.oauth2.google.clientSecret]
|
||||
label = "Client Secret"
|
||||
description = "The OAuth2 client secret from Google Cloud Console"
|
||||
|
||||
[provider.oauth2.google.scopes]
|
||||
label = "Scopes"
|
||||
description = "Comma-separated OAuth2 scopes"
|
||||
|
||||
[provider.oauth2.google.useAsUsername]
|
||||
label = "Use as Username"
|
||||
description = "Field to use as username (email, name, given_name, family_name)"
|
||||
|
||||
[provider.oauth2.github]
|
||||
scope = "Sign-in authentication"
|
||||
|
||||
[provider.oauth2.github.clientId]
|
||||
label = "Client ID"
|
||||
description = "The OAuth2 client ID from GitHub Developer Settings"
|
||||
|
||||
[provider.oauth2.github.clientSecret]
|
||||
label = "Client Secret"
|
||||
description = "The OAuth2 client secret from GitHub Developer Settings"
|
||||
|
||||
[provider.oauth2.github.scopes]
|
||||
label = "Scopes"
|
||||
description = "Comma-separated OAuth2 scopes"
|
||||
|
||||
[provider.oauth2.github.useAsUsername]
|
||||
label = "Use as Username"
|
||||
description = "Field to use as username (email, login, name)"
|
||||
|
||||
[provider.oauth2.keycloak]
|
||||
scope = "SSO"
|
||||
|
||||
[provider.oauth2.keycloak.issuer]
|
||||
label = "Issuer URL"
|
||||
description = "URL of the Keycloak realm's OpenID Connect Discovery endpoint"
|
||||
|
||||
[provider.oauth2.keycloak.clientId]
|
||||
label = "Client ID"
|
||||
description = "The OAuth2 client ID from Keycloak"
|
||||
|
||||
[provider.oauth2.keycloak.clientSecret]
|
||||
label = "Client Secret"
|
||||
description = "The OAuth2 client secret from Keycloak"
|
||||
|
||||
[provider.oauth2.keycloak.scopes]
|
||||
label = "Scopes"
|
||||
description = "Comma-separated OAuth2 scopes"
|
||||
|
||||
[provider.oauth2.keycloak.useAsUsername]
|
||||
label = "Use as Username"
|
||||
description = "Field to use as username (email, name, given_name, family_name, preferred_username)"
|
||||
|
||||
[provider.oauth2.generic]
|
||||
name = "Generic OAuth2"
|
||||
scope = "SSO"
|
||||
|
||||
[provider.oauth2.generic.enabled]
|
||||
label = "Enable Generic OAuth2"
|
||||
description = "Enable authentication using a custom OAuth2 provider"
|
||||
|
||||
[provider.oauth2.generic.provider]
|
||||
label = "Provider Name"
|
||||
description = "The name of your OAuth2 provider (e.g., Azure AD, Okta)"
|
||||
|
||||
[provider.oauth2.generic.issuer]
|
||||
label = "Issuer URL"
|
||||
description = "Provider that supports OpenID Connect Discovery (/.well-known/openid-configuration)"
|
||||
|
||||
[provider.oauth2.generic.clientId]
|
||||
label = "Client ID"
|
||||
description = "The OAuth2 client ID from your provider"
|
||||
|
||||
[provider.oauth2.generic.clientSecret]
|
||||
label = "Client Secret"
|
||||
description = "The OAuth2 client secret from your provider"
|
||||
|
||||
[provider.oauth2.generic.scopes]
|
||||
label = "Scopes"
|
||||
description = "Comma-separated OAuth2 scopes"
|
||||
|
||||
[provider.oauth2.generic.useAsUsername]
|
||||
label = "Use as Username"
|
||||
description = "Field to use as username"
|
||||
|
||||
[provider.oauth2.generic.autoCreateUser]
|
||||
label = "Auto Create Users"
|
||||
description = "Automatically create user accounts on first OAuth2 login"
|
||||
|
||||
[provider.oauth2.generic.blockRegistration]
|
||||
label = "Block Registration"
|
||||
description = "Prevent new user registration via OAuth2"
|
||||
|
||||
[provider.smtp]
|
||||
name = "SMTP Mail"
|
||||
scope = "Email Notifications"
|
||||
|
||||
[provider.smtp.enabled]
|
||||
label = "Enable Mail"
|
||||
description = "Enable email notifications and SMTP functionality"
|
||||
|
||||
[provider.smtp.host]
|
||||
label = "SMTP Host"
|
||||
description = "The hostname or IP address of your SMTP server"
|
||||
|
||||
[provider.smtp.port]
|
||||
label = "SMTP Port"
|
||||
description = "The port number for SMTP connection (typically 25, 465, or 587)"
|
||||
|
||||
[provider.smtp.username]
|
||||
label = "SMTP Username"
|
||||
description = "Username for SMTP authentication"
|
||||
|
||||
[provider.smtp.password]
|
||||
label = "SMTP Password"
|
||||
description = "Password for SMTP authentication"
|
||||
|
||||
[provider.smtp.from]
|
||||
label = "From Address"
|
||||
description = "The email address to use as the sender"
|
||||
|
||||
[provider.saml2]
|
||||
name = "SAML2"
|
||||
scope = "SSO (SAML)"
|
||||
|
||||
[provider.saml2.enabled]
|
||||
label = "Enable SAML2"
|
||||
description = "Enable SAML2 authentication (Enterprise only)"
|
||||
|
||||
[provider.saml2.provider]
|
||||
label = "Provider Name"
|
||||
description = "The name of your SAML2 provider"
|
||||
|
||||
[provider.saml2.registrationId]
|
||||
label = "Registration ID"
|
||||
description = "The name of your Service Provider (SP) app name"
|
||||
|
||||
[provider.saml2.idpMetadataUri]
|
||||
label = "IDP Metadata URI"
|
||||
description = "The URI for your provider's metadata"
|
||||
|
||||
[provider.saml2.idpSingleLoginUrl]
|
||||
label = "IDP Single Login URL"
|
||||
description = "The URL for initiating SSO"
|
||||
|
||||
[provider.saml2.idpSingleLogoutUrl]
|
||||
label = "IDP Single Logout URL"
|
||||
description = "The URL for initiating SLO"
|
||||
|
||||
[provider.saml2.idpIssuer]
|
||||
label = "IDP Issuer"
|
||||
description = "The ID of your provider"
|
||||
|
||||
[provider.saml2.idpCert]
|
||||
label = "IDP Certificate"
|
||||
description = "The certificate path (e.g., classpath:okta.cert)"
|
||||
|
||||
[provider.saml2.privateKey]
|
||||
label = "Private Key"
|
||||
description = "Your private key path"
|
||||
|
||||
[provider.saml2.spCert]
|
||||
label = "SP Certificate"
|
||||
description = "Your signing certificate path"
|
||||
|
||||
[provider.saml2.autoCreateUser]
|
||||
label = "Auto Create Users"
|
||||
description = "Automatically create user accounts on first SAML2 login"
|
||||
|
||||
[provider.saml2.blockRegistration]
|
||||
label = "Block Registration"
|
||||
description = "Prevent new user registration via SAML2"
|
||||
|
||||
[provider.googledrive]
|
||||
name = "Google Drive"
|
||||
scope = "File Import"
|
||||
|
||||
[provider.googledrive.enabled]
|
||||
label = "Enable Google Drive File Picker"
|
||||
description = "Allow users to import files directly from Google Drive"
|
||||
|
||||
[provider.googledrive.clientId]
|
||||
label = "Client ID"
|
||||
description = "Google OAuth 2.0 Client ID from Google Cloud Console"
|
||||
|
||||
[provider.googledrive.apiKey]
|
||||
label = "API Key"
|
||||
description = "Google API Key for Google Picker API from Google Cloud Console"
|
||||
|
||||
[provider.googledrive.appId]
|
||||
label = "App ID"
|
||||
description = "Google Drive App ID from Google Cloud Console"
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import { Modal } from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { StirlingFileStub } from '@app/types/fileContext';
|
||||
import { useFileManager } from '@app/hooks/useFileManager';
|
||||
import { useFilesModalContext } from '@app/contexts/FilesModalContext';
|
||||
import { useAppConfig } from '@app/contexts/AppConfigContext';
|
||||
import { Tool } from '@app/types/tool';
|
||||
import MobileLayout from '@app/components/fileManager/MobileLayout';
|
||||
import DesktopLayout from '@app/components/fileManager/DesktopLayout';
|
||||
import DragOverlay from '@app/components/fileManager/DragOverlay';
|
||||
import { FileManagerProvider } from '@app/contexts/FileManagerContext';
|
||||
import { Z_INDEX_FILE_MANAGER_MODAL } from '@app/styles/zIndex';
|
||||
import { isGoogleDriveConfigured } from '@app/services/googleDrivePickerService';
|
||||
import { isGoogleDriveConfigured, extractGoogleDriveBackendConfig } from '@app/services/googleDrivePickerService';
|
||||
import { loadScript } from '@app/utils/scriptLoader';
|
||||
import { useAllFiles } from '@app/contexts/FileContext';
|
||||
|
||||
@@ -20,6 +21,7 @@ interface FileManagerProps {
|
||||
|
||||
const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
||||
const { isFilesModalOpen, closeFilesModal, onFileUpload, onRecentFileSelect } = useFilesModalContext();
|
||||
const { config } = useAppConfig();
|
||||
const [recentFiles, setRecentFiles] = useState<StirlingFileStub[]>([]);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
@@ -92,9 +94,14 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
||||
}, []);
|
||||
|
||||
// Preload Google Drive scripts if configured
|
||||
// Use useMemo to only track Google Drive config changes, not all config updates
|
||||
const googleDriveBackendConfig = useMemo(
|
||||
() => extractGoogleDriveBackendConfig(config),
|
||||
[config?.googleDriveEnabled, config?.googleDriveClientId, config?.googleDriveApiKey, config?.googleDriveAppId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isGoogleDriveConfigured()) {
|
||||
if (isGoogleDriveConfigured(googleDriveBackendConfig)) {
|
||||
// Load scripts in parallel without blocking
|
||||
Promise.all([
|
||||
loadScript({
|
||||
@@ -113,7 +120,7 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
||||
console.warn('Failed to preload Google Drive scripts:', error);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
}, [googleDriveBackendConfig]);
|
||||
|
||||
// Modal size constants for consistent scaling
|
||||
const modalHeight = '80vh';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Stack, Text, Button, Group } from '@mantine/core';
|
||||
import HistoryIcon from '@mui/icons-material/History';
|
||||
import CloudIcon from '@mui/icons-material/Cloud';
|
||||
import PhonelinkIcon from '@mui/icons-material/Phonelink';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useFileManagerContext } from '@app/contexts/FileManagerContext';
|
||||
@@ -16,6 +15,23 @@ interface FileSourceButtonsProps {
|
||||
horizontal?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Drive icon component - displays the branded SVG icon
|
||||
* Shows grayscale when disabled
|
||||
*/
|
||||
const GoogleDriveIcon: React.FC<{ disabled?: boolean }> = ({ disabled }) => (
|
||||
<img
|
||||
src="/images/google-drive.svg"
|
||||
alt="Google Drive"
|
||||
style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
filter: disabled ? 'grayscale(100%)' : 'none',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
|
||||
horizontal = false
|
||||
}) => {
|
||||
@@ -51,6 +67,12 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Determine visibility of Google Drive button
|
||||
const shouldHideGoogleDrive = !isGoogleDriveEnabled && config?.hideDisabledToolsGoogleDrive;
|
||||
|
||||
// Determine visibility of Mobile QR Scanner button
|
||||
const shouldHideMobileQR = !isMobileUploadEnabled && config?.hideDisabledToolsMobileQRScanner;
|
||||
|
||||
const buttonProps = {
|
||||
variant: (source: string) => activeSource === source ? 'filled' : 'subtle',
|
||||
getColor: (source: string) => activeSource === source ? 'var(--mantine-color-gray-2)' : undefined,
|
||||
@@ -101,51 +123,55 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
|
||||
{horizontal ? terminology.upload : terminology.uploadFiles}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="subtle"
|
||||
color='var(--mantine-color-gray-6)'
|
||||
leftSection={<CloudIcon />}
|
||||
justify={horizontal ? "center" : "flex-start"}
|
||||
onClick={handleGoogleDriveClick}
|
||||
fullWidth={!horizontal}
|
||||
size={horizontal ? "xs" : "sm"}
|
||||
disabled={!isGoogleDriveEnabled}
|
||||
styles={{
|
||||
root: {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
'&:hover': {
|
||||
backgroundColor: isGoogleDriveEnabled ? 'var(--mantine-color-gray-0)' : 'transparent'
|
||||
{!shouldHideGoogleDrive && (
|
||||
<Button
|
||||
variant="subtle"
|
||||
color='var(--mantine-color-gray-6)'
|
||||
leftSection={<GoogleDriveIcon disabled={!isGoogleDriveEnabled} />}
|
||||
justify={horizontal ? "center" : "flex-start"}
|
||||
onClick={handleGoogleDriveClick}
|
||||
fullWidth={!horizontal}
|
||||
size={horizontal ? "xs" : "sm"}
|
||||
disabled={!isGoogleDriveEnabled}
|
||||
styles={{
|
||||
root: {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
'&:hover': {
|
||||
backgroundColor: isGoogleDriveEnabled ? 'var(--mantine-color-gray-0)' : 'transparent'
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
title={!isGoogleDriveEnabled ? t('fileManager.googleDriveNotAvailable', 'Google Drive integration not available') : undefined}
|
||||
>
|
||||
{horizontal ? t('fileManager.googleDriveShort', 'Drive') : t('fileManager.googleDrive', 'Google Drive')}
|
||||
</Button>
|
||||
}}
|
||||
title={!isGoogleDriveEnabled ? t('fileManager.googleDriveNotAvailable', 'Google Drive integration not available') : undefined}
|
||||
>
|
||||
{horizontal ? t('fileManager.googleDriveShort', 'Drive') : t('fileManager.googleDrive', 'Google Drive')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="subtle"
|
||||
color='var(--mantine-color-gray-6)'
|
||||
leftSection={<PhonelinkIcon />}
|
||||
justify={horizontal ? "center" : "flex-start"}
|
||||
onClick={handleMobileUploadClick}
|
||||
fullWidth={!horizontal}
|
||||
size={horizontal ? "xs" : "sm"}
|
||||
disabled={!isMobileUploadEnabled}
|
||||
styles={{
|
||||
root: {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
'&:hover': {
|
||||
backgroundColor: isMobileUploadEnabled ? 'var(--mantine-color-gray-0)' : 'transparent'
|
||||
{!shouldHideMobileQR && (
|
||||
<Button
|
||||
variant="subtle"
|
||||
color='var(--mantine-color-gray-6)'
|
||||
leftSection={<PhonelinkIcon />}
|
||||
justify={horizontal ? "center" : "flex-start"}
|
||||
onClick={handleMobileUploadClick}
|
||||
fullWidth={!horizontal}
|
||||
size={horizontal ? "xs" : "sm"}
|
||||
disabled={!isMobileUploadEnabled}
|
||||
styles={{
|
||||
root: {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
'&:hover': {
|
||||
backgroundColor: isMobileUploadEnabled ? 'var(--mantine-color-gray-0)' : 'transparent'
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
title={!isMobileUploadEnabled ? t('fileManager.mobileUploadNotAvailable', 'Mobile upload not available') : undefined}
|
||||
>
|
||||
{horizontal ? t('fileManager.mobileShort', 'Mobile') : t('fileManager.mobileUpload', 'Mobile Upload')}
|
||||
</Button>
|
||||
}}
|
||||
title={!isMobileUploadEnabled ? t('fileManager.mobileUploadNotAvailable', 'Mobile upload not available') : undefined}
|
||||
>
|
||||
{horizontal ? t('fileManager.mobileShort', 'Mobile') : t('fileManager.mobileUpload', 'Mobile Upload')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Switch,
|
||||
NumberInput,
|
||||
TagsInput,
|
||||
Anchor,
|
||||
} from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||
@@ -231,6 +232,18 @@ export default function ProviderCard({
|
||||
{/* Expandable Settings */}
|
||||
<Collapse in={expanded}>
|
||||
<Stack gap="md" mt="xs">
|
||||
{/* Documentation Link */}
|
||||
{provider.documentationUrl && (
|
||||
<Anchor
|
||||
href={provider.documentationUrl}
|
||||
target="_blank"
|
||||
size="xs"
|
||||
c="blue"
|
||||
>
|
||||
{t('admin.settings.connections.documentation', 'View documentation')} ↗
|
||||
</Anchor>
|
||||
)}
|
||||
|
||||
{provider.fields.map((field) => renderField(field))}
|
||||
|
||||
{!readOnly && (onSave || onDisconnect) && (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export type ProviderType = 'oauth2' | 'saml2' | 'telegram';
|
||||
export type ProviderType = 'oauth2' | 'saml2' | 'telegram' | 'googledrive';
|
||||
|
||||
export interface ProviderField {
|
||||
key: string;
|
||||
@@ -18,247 +18,273 @@ export interface Provider {
|
||||
type: ProviderType;
|
||||
scope: string; // Summary of what this provider does
|
||||
businessTier?: boolean; // Enterprise only
|
||||
documentationUrl?: string; // Optional link to documentation
|
||||
fields: ProviderField[];
|
||||
}
|
||||
|
||||
export const OAUTH2_PROVIDERS: Provider[] = [
|
||||
{
|
||||
const useGoogleProvider = (): Provider => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
id: 'google',
|
||||
name: 'Google',
|
||||
icon: '/Login/google.svg',
|
||||
type: 'oauth2',
|
||||
scope: 'Sign-in authentication',
|
||||
scope: t('provider.oauth2.google.scope', 'Sign-in authentication'),
|
||||
documentationUrl: 'https://docs.stirlingpdf.com/Configuration/OAuth%20SSO%20Configuration',
|
||||
fields: [
|
||||
{
|
||||
key: 'clientId',
|
||||
type: 'text',
|
||||
label: 'Client ID',
|
||||
description: 'The OAuth2 client ID from Google Cloud Console',
|
||||
label: t('provider.oauth2.google.clientId.label', 'Client ID'),
|
||||
description: t('provider.oauth2.google.clientId.description', 'The OAuth2 client ID from Google Cloud Console'),
|
||||
placeholder: 'your-client-id.apps.googleusercontent.com',
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
type: 'password',
|
||||
label: 'Client Secret',
|
||||
description: 'The OAuth2 client secret from Google Cloud Console',
|
||||
label: t('provider.oauth2.google.clientSecret.label', 'Client Secret'),
|
||||
description: t('provider.oauth2.google.clientSecret.description', 'The OAuth2 client secret from Google Cloud Console'),
|
||||
},
|
||||
{
|
||||
key: 'scopes',
|
||||
type: 'text',
|
||||
label: 'Scopes',
|
||||
description: 'Comma-separated OAuth2 scopes',
|
||||
label: t('provider.oauth2.google.scopes.label', 'Scopes'),
|
||||
description: t('provider.oauth2.google.scopes.description', 'Comma-separated OAuth2 scopes'),
|
||||
defaultValue: 'email, profile',
|
||||
},
|
||||
{
|
||||
key: 'useAsUsername',
|
||||
type: 'text',
|
||||
label: 'Use as Username',
|
||||
description: 'Field to use as username (email, name, given_name, family_name)',
|
||||
label: t('provider.oauth2.google.useAsUsername.label', 'Use as Username'),
|
||||
description: t('provider.oauth2.google.useAsUsername.description', 'Field to use as username (email, name, given_name, family_name)'),
|
||||
defaultValue: 'email',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
};
|
||||
};
|
||||
|
||||
const useGitHubProvider = (): Provider => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
id: 'github',
|
||||
name: 'GitHub',
|
||||
icon: '/Login/github.svg',
|
||||
type: 'oauth2',
|
||||
scope: 'Sign-in authentication',
|
||||
scope: t('provider.oauth2.github.scope', 'Sign-in authentication'),
|
||||
documentationUrl: 'https://docs.stirlingpdf.com/Configuration/OAuth%20SSO%20Configuration',
|
||||
fields: [
|
||||
{
|
||||
key: 'clientId',
|
||||
type: 'text',
|
||||
label: 'Client ID',
|
||||
description: 'The OAuth2 client ID from GitHub Developer Settings',
|
||||
label: t('provider.oauth2.github.clientId.label', 'Client ID'),
|
||||
description: t('provider.oauth2.github.clientId.description', 'The OAuth2 client ID from GitHub Developer Settings'),
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
type: 'password',
|
||||
label: 'Client Secret',
|
||||
description: 'The OAuth2 client secret from GitHub Developer Settings',
|
||||
label: t('provider.oauth2.github.clientSecret.label', 'Client Secret'),
|
||||
description: t('provider.oauth2.github.clientSecret.description', 'The OAuth2 client secret from GitHub Developer Settings'),
|
||||
},
|
||||
{
|
||||
key: 'scopes',
|
||||
type: 'text',
|
||||
label: 'Scopes',
|
||||
description: 'Comma-separated OAuth2 scopes',
|
||||
label: t('provider.oauth2.github.scopes.label', 'Scopes'),
|
||||
description: t('provider.oauth2.github.scopes.description', 'Comma-separated OAuth2 scopes'),
|
||||
defaultValue: 'read:user',
|
||||
},
|
||||
{
|
||||
key: 'useAsUsername',
|
||||
type: 'text',
|
||||
label: 'Use as Username',
|
||||
description: 'Field to use as username (email, login, name)',
|
||||
label: t('provider.oauth2.github.useAsUsername.label', 'Use as Username'),
|
||||
description: t('provider.oauth2.github.useAsUsername.description', 'Field to use as username (email, login, name)'),
|
||||
defaultValue: 'login',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
};
|
||||
};
|
||||
|
||||
const useKeycloakProvider = (): Provider => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
id: 'keycloak',
|
||||
name: 'Keycloak',
|
||||
icon: 'key-rounded',
|
||||
type: 'oauth2',
|
||||
scope: 'SSO',
|
||||
businessTier: false, // Server tier - OAuth2/OIDC SSO
|
||||
scope: t('provider.oauth2.keycloak.scope', 'SSO'),
|
||||
businessTier: false,
|
||||
documentationUrl: 'https://docs.stirlingpdf.com/Configuration/OAuth%20SSO%20Configuration',
|
||||
fields: [
|
||||
{
|
||||
key: 'issuer',
|
||||
type: 'text',
|
||||
label: 'Issuer URL',
|
||||
description: "URL of the Keycloak realm's OpenID Connect Discovery endpoint",
|
||||
label: t('provider.oauth2.keycloak.issuer.label', 'Issuer URL'),
|
||||
description: t('provider.oauth2.keycloak.issuer.description', "URL of the Keycloak realm's OpenID Connect Discovery endpoint"),
|
||||
placeholder: 'https://keycloak.example.com/realms/myrealm',
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
type: 'text',
|
||||
label: 'Client ID',
|
||||
description: 'The OAuth2 client ID from Keycloak',
|
||||
label: t('provider.oauth2.keycloak.clientId.label', 'Client ID'),
|
||||
description: t('provider.oauth2.keycloak.clientId.description', 'The OAuth2 client ID from Keycloak'),
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
type: 'password',
|
||||
label: 'Client Secret',
|
||||
description: 'The OAuth2 client secret from Keycloak',
|
||||
label: t('provider.oauth2.keycloak.clientSecret.label', 'Client Secret'),
|
||||
description: t('provider.oauth2.keycloak.clientSecret.description', 'The OAuth2 client secret from Keycloak'),
|
||||
},
|
||||
{
|
||||
key: 'scopes',
|
||||
type: 'text',
|
||||
label: 'Scopes',
|
||||
description: 'Comma-separated OAuth2 scopes',
|
||||
label: t('provider.oauth2.keycloak.scopes.label', 'Scopes'),
|
||||
description: t('provider.oauth2.keycloak.scopes.description', 'Comma-separated OAuth2 scopes'),
|
||||
defaultValue: 'openid, profile, email',
|
||||
},
|
||||
{
|
||||
key: 'useAsUsername',
|
||||
type: 'text',
|
||||
label: 'Use as Username',
|
||||
description: 'Field to use as username (email, name, given_name, family_name, preferred_username)',
|
||||
label: t('provider.oauth2.keycloak.useAsUsername.label', 'Use as Username'),
|
||||
description: t('provider.oauth2.keycloak.useAsUsername.description', 'Field to use as username (email, name, given_name, family_name, preferred_username)'),
|
||||
defaultValue: 'preferred_username',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const GENERIC_OAUTH2_PROVIDER: Provider = {
|
||||
id: 'oauth2-generic',
|
||||
name: 'Generic OAuth2',
|
||||
icon: 'link-rounded',
|
||||
type: 'oauth2',
|
||||
scope: 'SSO',
|
||||
businessTier: false, // Server tier - OAuth2/OIDC SSO
|
||||
fields: [
|
||||
{
|
||||
key: 'enabled',
|
||||
type: 'switch',
|
||||
label: 'Enable Generic OAuth2',
|
||||
description: 'Enable authentication using a custom OAuth2 provider',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: 'provider',
|
||||
type: 'text',
|
||||
label: 'Provider Name',
|
||||
description: 'The name of your OAuth2 provider (e.g., Azure AD, Okta)',
|
||||
placeholder: 'azure-ad',
|
||||
},
|
||||
{
|
||||
key: 'issuer',
|
||||
type: 'text',
|
||||
label: 'Issuer URL',
|
||||
description: 'Provider that supports OpenID Connect Discovery (/.well-known/openid-configuration)',
|
||||
placeholder: 'https://login.microsoftonline.com/{tenant-id}/v2.0',
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
type: 'text',
|
||||
label: 'Client ID',
|
||||
description: 'The OAuth2 client ID from your provider',
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
type: 'password',
|
||||
label: 'Client Secret',
|
||||
description: 'The OAuth2 client secret from your provider',
|
||||
},
|
||||
{
|
||||
key: 'scopes',
|
||||
type: 'text',
|
||||
label: 'Scopes',
|
||||
description: 'Comma-separated OAuth2 scopes',
|
||||
defaultValue: 'openid, profile, email',
|
||||
},
|
||||
{
|
||||
key: 'useAsUsername',
|
||||
type: 'text',
|
||||
label: 'Use as Username',
|
||||
description: 'Field to use as username',
|
||||
defaultValue: 'email',
|
||||
},
|
||||
{
|
||||
key: 'autoCreateUser',
|
||||
type: 'switch',
|
||||
label: 'Auto Create Users',
|
||||
description: 'Automatically create user accounts on first OAuth2 login',
|
||||
defaultValue: true,
|
||||
},
|
||||
{
|
||||
key: 'blockRegistration',
|
||||
type: 'switch',
|
||||
label: 'Block Registration',
|
||||
description: 'Prevent new user registration via OAuth2',
|
||||
defaultValue: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export const SMTP_PROVIDER: Provider = {
|
||||
id: 'smtp',
|
||||
name: 'SMTP Mail',
|
||||
icon: 'mail-rounded',
|
||||
type: 'oauth2', // Using oauth2 as the base type, but it's really just a generic provider
|
||||
scope: 'Email Notifications',
|
||||
fields: [
|
||||
{
|
||||
key: 'enabled',
|
||||
type: 'switch',
|
||||
label: 'Enable Mail',
|
||||
description: 'Enable email notifications and SMTP functionality',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: 'host',
|
||||
type: 'text',
|
||||
label: 'SMTP Host',
|
||||
description: 'The hostname or IP address of your SMTP server',
|
||||
placeholder: 'smtp.example.com',
|
||||
},
|
||||
{
|
||||
key: 'port',
|
||||
type: 'number',
|
||||
label: 'SMTP Port',
|
||||
description: 'The port number for SMTP connection (typically 25, 465, or 587)',
|
||||
placeholder: '587',
|
||||
defaultValue: '587',
|
||||
},
|
||||
{
|
||||
key: 'username',
|
||||
type: 'text',
|
||||
label: 'SMTP Username',
|
||||
description: 'Username for SMTP authentication',
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
type: 'password',
|
||||
label: 'SMTP Password',
|
||||
description: 'Password for SMTP authentication',
|
||||
},
|
||||
{
|
||||
key: 'from',
|
||||
type: 'text',
|
||||
label: 'From Address',
|
||||
description: 'The email address to use as the sender',
|
||||
placeholder: 'noreply@example.com',
|
||||
},
|
||||
],
|
||||
const useGenericOAuth2Provider = (): Provider => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
id: 'oauth2-generic',
|
||||
name: t('provider.oauth2.generic.name', 'Generic OAuth2'),
|
||||
icon: 'link-rounded',
|
||||
type: 'oauth2',
|
||||
scope: t('provider.oauth2.generic.scope', 'SSO'),
|
||||
businessTier: false,
|
||||
documentationUrl: 'https://docs.stirlingpdf.com/Configuration/OAuth%20SSO%20Configuration',
|
||||
fields: [
|
||||
{
|
||||
key: 'enabled',
|
||||
type: 'switch',
|
||||
label: t('provider.oauth2.generic.enabled.label', 'Enable Generic OAuth2'),
|
||||
description: t('provider.oauth2.generic.enabled.description', 'Enable authentication using a custom OAuth2 provider'),
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: 'provider',
|
||||
type: 'text',
|
||||
label: t('provider.oauth2.generic.provider.label', 'Provider Name'),
|
||||
description: t('provider.oauth2.generic.provider.description', 'The name of your OAuth2 provider (e.g., Azure AD, Okta)'),
|
||||
placeholder: 'azure-ad',
|
||||
},
|
||||
{
|
||||
key: 'issuer',
|
||||
type: 'text',
|
||||
label: t('provider.oauth2.generic.issuer.label', 'Issuer URL'),
|
||||
description: t('provider.oauth2.generic.issuer.description', 'Provider that supports OpenID Connect Discovery (/.well-known/openid-configuration)'),
|
||||
placeholder: 'https://login.microsoftonline.com/{tenant-id}/v2.0',
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
type: 'text',
|
||||
label: t('provider.oauth2.generic.clientId.label', 'Client ID'),
|
||||
description: t('provider.oauth2.generic.clientId.description', 'The OAuth2 client ID from your provider'),
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
type: 'password',
|
||||
label: t('provider.oauth2.generic.clientSecret.label', 'Client Secret'),
|
||||
description: t('provider.oauth2.generic.clientSecret.description', 'The OAuth2 client secret from your provider'),
|
||||
},
|
||||
{
|
||||
key: 'scopes',
|
||||
type: 'text',
|
||||
label: t('provider.oauth2.generic.scopes.label', 'Scopes'),
|
||||
description: t('provider.oauth2.generic.scopes.description', 'Comma-separated OAuth2 scopes'),
|
||||
defaultValue: 'openid, profile, email',
|
||||
},
|
||||
{
|
||||
key: 'useAsUsername',
|
||||
type: 'text',
|
||||
label: t('provider.oauth2.generic.useAsUsername.label', 'Use as Username'),
|
||||
description: t('provider.oauth2.generic.useAsUsername.description', 'Field to use as username'),
|
||||
defaultValue: 'email',
|
||||
},
|
||||
{
|
||||
key: 'autoCreateUser',
|
||||
type: 'switch',
|
||||
label: t('provider.oauth2.generic.autoCreateUser.label', 'Auto Create Users'),
|
||||
description: t('provider.oauth2.generic.autoCreateUser.description', 'Automatically create user accounts on first OAuth2 login'),
|
||||
defaultValue: true,
|
||||
},
|
||||
{
|
||||
key: 'blockRegistration',
|
||||
type: 'switch',
|
||||
label: t('provider.oauth2.generic.blockRegistration.label', 'Block Registration'),
|
||||
description: t('provider.oauth2.generic.blockRegistration.description', 'Prevent new user registration via OAuth2'),
|
||||
defaultValue: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const useSMTPProvider = (): Provider => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
id: 'smtp',
|
||||
name: t('provider.smtp.name', 'SMTP Mail'),
|
||||
icon: 'mail-rounded',
|
||||
type: 'oauth2',
|
||||
scope: t('provider.smtp.scope', 'Email Notifications'),
|
||||
documentationUrl: 'https://docs.stirlingpdf.com/Configuration/System%20and%20Security/#email-configuration',
|
||||
fields: [
|
||||
{
|
||||
key: 'enabled',
|
||||
type: 'switch',
|
||||
label: t('provider.smtp.enabled.label', 'Enable Mail'),
|
||||
description: t('provider.smtp.enabled.description', 'Enable email notifications and SMTP functionality'),
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: 'host',
|
||||
type: 'text',
|
||||
label: t('provider.smtp.host.label', 'SMTP Host'),
|
||||
description: t('provider.smtp.host.description', 'The hostname or IP address of your SMTP server'),
|
||||
placeholder: 'smtp.example.com',
|
||||
},
|
||||
{
|
||||
key: 'port',
|
||||
type: 'number',
|
||||
label: t('provider.smtp.port.label', 'SMTP Port'),
|
||||
description: t('provider.smtp.port.description', 'The port number for SMTP connection (typically 25, 465, or 587)'),
|
||||
placeholder: '587',
|
||||
defaultValue: '587',
|
||||
},
|
||||
{
|
||||
key: 'username',
|
||||
type: 'text',
|
||||
label: t('provider.smtp.username.label', 'SMTP Username'),
|
||||
description: t('provider.smtp.username.description', 'Username for SMTP authentication'),
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
type: 'password',
|
||||
label: t('provider.smtp.password.label', 'SMTP Password'),
|
||||
description: t('provider.smtp.password.description', 'Password for SMTP authentication'),
|
||||
},
|
||||
{
|
||||
key: 'from',
|
||||
type: 'text',
|
||||
label: t('provider.smtp.from.label', 'From Address'),
|
||||
description: t('provider.smtp.from.description', 'The email address to use as the sender'),
|
||||
placeholder: 'noreply@example.com',
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
const useTelegramProvider = (): Provider => {
|
||||
const { t } = useTranslation();
|
||||
@@ -471,107 +497,165 @@ const useTelegramProvider = (): Provider => {
|
||||
};
|
||||
};
|
||||
|
||||
export const SAML2_PROVIDER: Provider = {
|
||||
id: 'saml2',
|
||||
name: 'SAML2',
|
||||
icon: 'verified-user-rounded',
|
||||
type: 'saml2',
|
||||
scope: 'SSO (SAML)',
|
||||
businessTier: true, // Enterprise tier - SAML only
|
||||
fields: [
|
||||
{
|
||||
key: 'enabled',
|
||||
type: 'switch',
|
||||
label: 'Enable SAML2',
|
||||
description: 'Enable SAML2 authentication (Enterprise only)',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: 'provider',
|
||||
type: 'text',
|
||||
label: 'Provider Name',
|
||||
description: 'The name of your SAML2 provider',
|
||||
},
|
||||
{
|
||||
key: 'registrationId',
|
||||
type: 'text',
|
||||
label: 'Registration ID',
|
||||
description: 'The name of your Service Provider (SP) app name',
|
||||
defaultValue: 'stirling',
|
||||
},
|
||||
{
|
||||
key: 'idpMetadataUri',
|
||||
type: 'text',
|
||||
label: 'IDP Metadata URI',
|
||||
description: 'The URI for your provider\'s metadata',
|
||||
placeholder: 'https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata',
|
||||
},
|
||||
{
|
||||
key: 'idpSingleLoginUrl',
|
||||
type: 'text',
|
||||
label: 'IDP Single Login URL',
|
||||
description: 'The URL for initiating SSO',
|
||||
placeholder: 'https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/sso/saml',
|
||||
},
|
||||
{
|
||||
key: 'idpSingleLogoutUrl',
|
||||
type: 'text',
|
||||
label: 'IDP Single Logout URL',
|
||||
description: 'The URL for initiating SLO',
|
||||
placeholder: 'https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/slo/saml',
|
||||
},
|
||||
{
|
||||
key: 'idpIssuer',
|
||||
type: 'text',
|
||||
label: 'IDP Issuer',
|
||||
description: 'The ID of your provider',
|
||||
},
|
||||
{
|
||||
key: 'idpCert',
|
||||
type: 'text',
|
||||
label: 'IDP Certificate',
|
||||
description: 'The certificate path (e.g., classpath:okta.cert)',
|
||||
placeholder: 'classpath:okta.cert',
|
||||
},
|
||||
{
|
||||
key: 'privateKey',
|
||||
type: 'text',
|
||||
label: 'Private Key',
|
||||
description: 'Your private key path',
|
||||
placeholder: 'classpath:saml-private-key.key',
|
||||
},
|
||||
{
|
||||
key: 'spCert',
|
||||
type: 'text',
|
||||
label: 'SP Certificate',
|
||||
description: 'Your signing certificate path',
|
||||
placeholder: 'classpath:saml-public-cert.crt',
|
||||
},
|
||||
{
|
||||
key: 'autoCreateUser',
|
||||
type: 'switch',
|
||||
label: 'Auto Create Users',
|
||||
description: 'Automatically create user accounts on first SAML2 login',
|
||||
defaultValue: true,
|
||||
},
|
||||
{
|
||||
key: 'blockRegistration',
|
||||
type: 'switch',
|
||||
label: 'Block Registration',
|
||||
description: 'Prevent new user registration via SAML2',
|
||||
defaultValue: false,
|
||||
},
|
||||
],
|
||||
const useSAML2Provider = (): Provider => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
id: 'saml2',
|
||||
name: t('provider.saml2.name', 'SAML2'),
|
||||
icon: 'verified-user-rounded',
|
||||
type: 'saml2',
|
||||
scope: t('provider.saml2.scope', 'SSO (SAML)'),
|
||||
businessTier: true,
|
||||
documentationUrl: 'https://docs.stirlingpdf.com/Configuration/SAML%20SSO%20Configuration/',
|
||||
fields: [
|
||||
{
|
||||
key: 'enabled',
|
||||
type: 'switch',
|
||||
label: t('provider.saml2.enabled.label', 'Enable SAML2'),
|
||||
description: t('provider.saml2.enabled.description', 'Enable SAML2 authentication (Enterprise only)'),
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: 'provider',
|
||||
type: 'text',
|
||||
label: t('provider.saml2.provider.label', 'Provider Name'),
|
||||
description: t('provider.saml2.provider.description', 'The name of your SAML2 provider'),
|
||||
},
|
||||
{
|
||||
key: 'registrationId',
|
||||
type: 'text',
|
||||
label: t('provider.saml2.registrationId.label', 'Registration ID'),
|
||||
description: t('provider.saml2.registrationId.description', 'The name of your Service Provider (SP) app name'),
|
||||
defaultValue: 'stirling',
|
||||
},
|
||||
{
|
||||
key: 'idpMetadataUri',
|
||||
type: 'text',
|
||||
label: t('provider.saml2.idpMetadataUri.label', 'IDP Metadata URI'),
|
||||
description: t('provider.saml2.idpMetadataUri.description', 'The URI for your provider\'s metadata'),
|
||||
placeholder: 'https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata',
|
||||
},
|
||||
{
|
||||
key: 'idpSingleLoginUrl',
|
||||
type: 'text',
|
||||
label: t('provider.saml2.idpSingleLoginUrl.label', 'IDP Single Login URL'),
|
||||
description: t('provider.saml2.idpSingleLoginUrl.description', 'The URL for initiating SSO'),
|
||||
placeholder: 'https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/sso/saml',
|
||||
},
|
||||
{
|
||||
key: 'idpSingleLogoutUrl',
|
||||
type: 'text',
|
||||
label: t('provider.saml2.idpSingleLogoutUrl.label', 'IDP Single Logout URL'),
|
||||
description: t('provider.saml2.idpSingleLogoutUrl.description', 'The URL for initiating SLO'),
|
||||
placeholder: 'https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/slo/saml',
|
||||
},
|
||||
{
|
||||
key: 'idpIssuer',
|
||||
type: 'text',
|
||||
label: t('provider.saml2.idpIssuer.label', 'IDP Issuer'),
|
||||
description: t('provider.saml2.idpIssuer.description', 'The ID of your provider'),
|
||||
},
|
||||
{
|
||||
key: 'idpCert',
|
||||
type: 'text',
|
||||
label: t('provider.saml2.idpCert.label', 'IDP Certificate'),
|
||||
description: t('provider.saml2.idpCert.description', 'The certificate path (e.g., classpath:okta.cert)'),
|
||||
placeholder: 'classpath:okta.cert',
|
||||
},
|
||||
{
|
||||
key: 'privateKey',
|
||||
type: 'text',
|
||||
label: t('provider.saml2.privateKey.label', 'Private Key'),
|
||||
description: t('provider.saml2.privateKey.description', 'Your private key path'),
|
||||
placeholder: 'classpath:saml-private-key.key',
|
||||
},
|
||||
{
|
||||
key: 'spCert',
|
||||
type: 'text',
|
||||
label: t('provider.saml2.spCert.label', 'SP Certificate'),
|
||||
description: t('provider.saml2.spCert.description', 'Your signing certificate path'),
|
||||
placeholder: 'classpath:saml-public-cert.crt',
|
||||
},
|
||||
{
|
||||
key: 'autoCreateUser',
|
||||
type: 'switch',
|
||||
label: t('provider.saml2.autoCreateUser.label', 'Auto Create Users'),
|
||||
description: t('provider.saml2.autoCreateUser.description', 'Automatically create user accounts on first SAML2 login'),
|
||||
defaultValue: true,
|
||||
},
|
||||
{
|
||||
key: 'blockRegistration',
|
||||
type: 'switch',
|
||||
label: t('provider.saml2.blockRegistration.label', 'Block Registration'),
|
||||
description: t('provider.saml2.blockRegistration.description', 'Prevent new user registration via SAML2'),
|
||||
defaultValue: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const useGoogleDriveProvider = (): Provider => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
id: 'googledrive',
|
||||
name: t('provider.googledrive.name', 'Google Drive'),
|
||||
icon: '/images/google-drive.svg',
|
||||
type: 'googledrive',
|
||||
scope: t('provider.googledrive.scope', 'File Import'),
|
||||
documentationUrl: 'https://docs.stirlingpdf.com/Configuration/Google%20Drive%20File%20Picker/',
|
||||
fields: [
|
||||
{
|
||||
key: 'enabled',
|
||||
type: 'switch',
|
||||
label: t('provider.googledrive.enabled.label', 'Enable Google Drive File Picker'),
|
||||
description: t('provider.googledrive.enabled.description', 'Allow users to import files directly from Google Drive'),
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
type: 'text',
|
||||
label: t('provider.googledrive.clientId.label', 'Client ID'),
|
||||
description: t('provider.googledrive.clientId.description', 'Google OAuth 2.0 Client ID from Google Cloud Console'),
|
||||
placeholder: 'xxx.apps.googleusercontent.com',
|
||||
},
|
||||
{
|
||||
key: 'apiKey',
|
||||
type: 'text',
|
||||
label: t('provider.googledrive.apiKey.label', 'API Key'),
|
||||
description: t('provider.googledrive.apiKey.description', 'Google API Key for Google Picker API from Google Cloud Console'),
|
||||
placeholder: 'AIza...',
|
||||
},
|
||||
{
|
||||
key: 'appId',
|
||||
type: 'text',
|
||||
label: t('provider.googledrive.appId.label', 'App ID'),
|
||||
description: t('provider.googledrive.appId.description', 'Google Drive App ID from Google Cloud Console'),
|
||||
placeholder: 'xxxxxxxxxxxxx',
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export const useAllProviders = (): Provider[] => {
|
||||
const googleProvider = useGoogleProvider();
|
||||
const gitHubProvider = useGitHubProvider();
|
||||
const keycloakProvider = useKeycloakProvider();
|
||||
const genericOAuth2Provider = useGenericOAuth2Provider();
|
||||
const smtpProvider = useSMTPProvider();
|
||||
const telegramProvider = useTelegramProvider();
|
||||
const saml2Provider = useSAML2Provider();
|
||||
const googleDriveProvider = useGoogleDriveProvider();
|
||||
|
||||
return [
|
||||
...OAUTH2_PROVIDERS,
|
||||
GENERIC_OAUTH2_PROVIDER,
|
||||
SAML2_PROVIDER,
|
||||
SMTP_PROVIDER,
|
||||
googleProvider,
|
||||
gitHubProvider,
|
||||
keycloakProvider,
|
||||
genericOAuth2Provider,
|
||||
saml2Provider,
|
||||
smtpProvider,
|
||||
telegramProvider,
|
||||
googleDriveProvider,
|
||||
];
|
||||
};
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
* React hook for Google Drive file picker
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useAppConfig } from '@app/contexts/AppConfigContext';
|
||||
import {
|
||||
getGoogleDrivePickerService,
|
||||
isGoogleDriveConfigured,
|
||||
getGoogleDriveConfig,
|
||||
extractGoogleDriveBackendConfig,
|
||||
} from '@app/services/googleDrivePickerService';
|
||||
|
||||
interface UseGoogleDrivePickerOptions {
|
||||
@@ -25,16 +27,27 @@ interface UseGoogleDrivePickerReturn {
|
||||
* Hook to use Google Drive file picker
|
||||
*/
|
||||
export function useGoogleDrivePicker(): UseGoogleDrivePickerReturn {
|
||||
const { config } = useAppConfig();
|
||||
const [isEnabled, setIsEnabled] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
// Check if Google Drive is configured on mount
|
||||
// Memoize backend config to only track Google Drive specific properties
|
||||
const googleDriveBackendConfig = useMemo(
|
||||
() => extractGoogleDriveBackendConfig(config),
|
||||
[config?.googleDriveEnabled, config?.googleDriveClientId, config?.googleDriveApiKey, config?.googleDriveAppId]
|
||||
);
|
||||
|
||||
// Check if Google Drive is configured and reset initialization if disabled
|
||||
useEffect(() => {
|
||||
const configured = isGoogleDriveConfigured();
|
||||
const configured = isGoogleDriveConfigured(googleDriveBackendConfig);
|
||||
setIsEnabled(configured);
|
||||
}, []);
|
||||
// Reset initialization state if Google Drive becomes disabled
|
||||
if (!configured) {
|
||||
setIsInitialized(false);
|
||||
}
|
||||
}, [googleDriveBackendConfig]);
|
||||
|
||||
/**
|
||||
* Initialize the Google Drive service (lazy initialization)
|
||||
@@ -42,15 +55,15 @@ export function useGoogleDrivePicker(): UseGoogleDrivePickerReturn {
|
||||
const initializeService = useCallback(async () => {
|
||||
if (isInitialized) return;
|
||||
|
||||
const config = getGoogleDriveConfig();
|
||||
if (!config) {
|
||||
const googleDriveConfig = getGoogleDriveConfig(googleDriveBackendConfig);
|
||||
if (!googleDriveConfig) {
|
||||
throw new Error('Google Drive is not configured');
|
||||
}
|
||||
|
||||
const service = getGoogleDrivePickerService();
|
||||
await service.initialize(config);
|
||||
await service.initialize(googleDriveConfig);
|
||||
setIsInitialized(true);
|
||||
}, [isInitialized]);
|
||||
}, [isInitialized, googleDriveBackendConfig]);
|
||||
|
||||
/**
|
||||
* Open the Google Drive picker
|
||||
|
||||
@@ -14,6 +14,13 @@ interface GoogleDriveConfig {
|
||||
appId: string;
|
||||
}
|
||||
|
||||
interface BackendGoogleDriveConfig {
|
||||
enabled?: boolean;
|
||||
clientId?: string;
|
||||
apiKey?: string;
|
||||
appId?: string;
|
||||
}
|
||||
|
||||
interface PickerOptions {
|
||||
multiple?: boolean;
|
||||
mimeTypes?: string | null;
|
||||
@@ -270,8 +277,21 @@ export function getGoogleDrivePickerService(): GoogleDrivePickerService {
|
||||
|
||||
/**
|
||||
* Check if Google Drive credentials are configured
|
||||
* Supports both backend settings and environment variables
|
||||
*/
|
||||
export function isGoogleDriveConfigured(): boolean {
|
||||
export function isGoogleDriveConfigured(backendConfig?: BackendGoogleDriveConfig): boolean {
|
||||
// If backend config is provided, use it exclusively (don't fall back to env vars)
|
||||
if (backendConfig) {
|
||||
if (backendConfig.enabled === false) {
|
||||
return false; // Admin explicitly disabled it
|
||||
}
|
||||
if (backendConfig.enabled) {
|
||||
return !!(backendConfig.clientId && backendConfig.apiKey && backendConfig.appId);
|
||||
}
|
||||
return false; // No backend config provided
|
||||
}
|
||||
|
||||
// Fall back to environment variables only when backend config is not available
|
||||
const clientId = import.meta.env.VITE_GOOGLE_DRIVE_CLIENT_ID;
|
||||
const apiKey = import.meta.env.VITE_GOOGLE_DRIVE_API_KEY;
|
||||
const appId = import.meta.env.VITE_GOOGLE_DRIVE_APP_ID;
|
||||
@@ -280,16 +300,50 @@ export function isGoogleDriveConfigured(): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Google Drive configuration from environment variables
|
||||
* Get Google Drive configuration from backend settings or environment variables
|
||||
* Backend settings take priority over environment variables
|
||||
*/
|
||||
export function getGoogleDriveConfig(): GoogleDriveConfig | null {
|
||||
if (!isGoogleDriveConfigured()) {
|
||||
return null;
|
||||
export function getGoogleDriveConfig(backendConfig?: BackendGoogleDriveConfig): GoogleDriveConfig | null {
|
||||
// If backend config is provided, use it exclusively (don't fall back to env vars)
|
||||
if (backendConfig) {
|
||||
if (backendConfig.enabled === false) {
|
||||
return null; // Admin explicitly disabled it
|
||||
}
|
||||
if (backendConfig.enabled && backendConfig.clientId && backendConfig.apiKey && backendConfig.appId) {
|
||||
return {
|
||||
clientId: backendConfig.clientId,
|
||||
apiKey: backendConfig.apiKey,
|
||||
appId: backendConfig.appId,
|
||||
};
|
||||
}
|
||||
return null; // Backend config provided but incomplete
|
||||
}
|
||||
|
||||
// Fall back to environment variables only when backend config is not available
|
||||
const envClientId = import.meta.env.VITE_GOOGLE_DRIVE_CLIENT_ID;
|
||||
const envApiKey = import.meta.env.VITE_GOOGLE_DRIVE_API_KEY;
|
||||
const envAppId = import.meta.env.VITE_GOOGLE_DRIVE_APP_ID;
|
||||
|
||||
if (envClientId && envApiKey && envAppId) {
|
||||
return {
|
||||
clientId: envClientId,
|
||||
apiKey: envApiKey,
|
||||
appId: envAppId,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract Google Drive backend config from AppConfig object
|
||||
* Eliminates duplicated config construction pattern
|
||||
*/
|
||||
export function extractGoogleDriveBackendConfig(appConfig: any): BackendGoogleDriveConfig {
|
||||
return {
|
||||
clientId: import.meta.env.VITE_GOOGLE_DRIVE_CLIENT_ID,
|
||||
apiKey: import.meta.env.VITE_GOOGLE_DRIVE_API_KEY,
|
||||
appId: import.meta.env.VITE_GOOGLE_DRIVE_APP_ID,
|
||||
enabled: appConfig?.googleDriveEnabled,
|
||||
clientId: appConfig?.googleDriveClientId,
|
||||
apiKey: appConfig?.googleDriveApiKey,
|
||||
appId: appConfig?.googleDriveAppId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,6 +44,12 @@ export interface AppConfig {
|
||||
isNewUser?: boolean;
|
||||
defaultHideUnavailableTools?: boolean;
|
||||
defaultHideUnavailableConversions?: boolean;
|
||||
hideDisabledToolsGoogleDrive?: boolean;
|
||||
hideDisabledToolsMobileQRScanner?: boolean;
|
||||
googleDriveEnabled?: boolean;
|
||||
googleDriveClientId?: string;
|
||||
googleDriveApiKey?: string;
|
||||
googleDriveAppId?: string;
|
||||
}
|
||||
|
||||
export type AppConfigBootstrapMode = 'blocking' | 'non-blocking';
|
||||
|
||||
@@ -78,6 +78,10 @@ interface ConnectionsSettingsData {
|
||||
mobileScannerImageResolution?: string;
|
||||
mobileScannerPageFormat?: string;
|
||||
mobileScannerStretchToFit?: boolean;
|
||||
googleDriveEnabled?: boolean;
|
||||
googleDriveClientId?: string;
|
||||
googleDriveApiKey?: string;
|
||||
googleDriveAppId?: string;
|
||||
}
|
||||
|
||||
export default function AdminConnectionsSection() {
|
||||
@@ -120,24 +124,61 @@ export default function AdminConnectionsSection() {
|
||||
mobileScannerConvertToPdf: systemData.mobileScannerSettings?.convertToPdf !== false,
|
||||
mobileScannerImageResolution: systemData.mobileScannerSettings?.imageResolution || 'full',
|
||||
mobileScannerPageFormat: systemData.mobileScannerSettings?.pageFormat || 'A4',
|
||||
mobileScannerStretchToFit: systemData.mobileScannerSettings?.stretchToFit || false
|
||||
mobileScannerStretchToFit: systemData.mobileScannerSettings?.stretchToFit || false,
|
||||
googleDriveEnabled: premiumData.proFeatures?.googleDrive?.enabled || false,
|
||||
googleDriveClientId: premiumData.proFeatures?.googleDrive?.clientId || '',
|
||||
googleDriveApiKey: premiumData.proFeatures?.googleDrive?.apiKey || '',
|
||||
googleDriveAppId: premiumData.proFeatures?.googleDrive?.appId || ''
|
||||
};
|
||||
|
||||
// Merge pending blocks from all endpoints - initialize with defaults to avoid warnings
|
||||
const pendingBlock: Record<string, any> = {
|
||||
oauth2: securityData._pending?.oauth2,
|
||||
saml2: securityData._pending?.saml2,
|
||||
mail: mailData._pending,
|
||||
telegram: telegramData._pending,
|
||||
ssoAutoLogin: premiumData._pending?.proFeatures?.ssoAutoLogin,
|
||||
enableMobileScanner: systemData._pending?.enableMobileScanner,
|
||||
mobileScannerConvertToPdf: systemData._pending?.mobileScannerSettings?.convertToPdf,
|
||||
mobileScannerImageResolution: systemData._pending?.mobileScannerSettings?.imageResolution,
|
||||
mobileScannerPageFormat: systemData._pending?.mobileScannerSettings?.pageFormat,
|
||||
mobileScannerStretchToFit: systemData._pending?.mobileScannerSettings?.stretchToFit,
|
||||
};
|
||||
// Merge pending blocks from all endpoints
|
||||
const pendingBlock: Record<string, any> = {};
|
||||
if (securityData._pending?.oauth2) {
|
||||
pendingBlock.oauth2 = securityData._pending.oauth2;
|
||||
}
|
||||
if (securityData._pending?.saml2) {
|
||||
pendingBlock.saml2 = securityData._pending.saml2;
|
||||
}
|
||||
if (mailData._pending) {
|
||||
pendingBlock.mail = mailData._pending;
|
||||
}
|
||||
if (telegramData._pending) {
|
||||
pendingBlock.telegram = telegramData._pending;
|
||||
}
|
||||
if (premiumData._pending?.proFeatures?.ssoAutoLogin !== undefined) {
|
||||
pendingBlock.ssoAutoLogin = premiumData._pending.proFeatures.ssoAutoLogin;
|
||||
}
|
||||
if (systemData._pending?.enableMobileScanner !== undefined) {
|
||||
pendingBlock.enableMobileScanner = systemData._pending.enableMobileScanner;
|
||||
}
|
||||
if (systemData._pending?.mobileScannerSettings?.convertToPdf !== undefined) {
|
||||
pendingBlock.mobileScannerConvertToPdf = systemData._pending.mobileScannerSettings.convertToPdf;
|
||||
}
|
||||
if (systemData._pending?.mobileScannerSettings?.imageResolution !== undefined) {
|
||||
pendingBlock.mobileScannerImageResolution = systemData._pending.mobileScannerSettings.imageResolution;
|
||||
}
|
||||
if (systemData._pending?.mobileScannerSettings?.pageFormat !== undefined) {
|
||||
pendingBlock.mobileScannerPageFormat = systemData._pending.mobileScannerSettings.pageFormat;
|
||||
}
|
||||
if (systemData._pending?.mobileScannerSettings?.stretchToFit !== undefined) {
|
||||
pendingBlock.mobileScannerStretchToFit = systemData._pending.mobileScannerSettings.stretchToFit;
|
||||
}
|
||||
if (premiumData._pending?.proFeatures?.googleDrive?.enabled !== undefined) {
|
||||
pendingBlock.googleDriveEnabled = premiumData._pending.proFeatures.googleDrive.enabled;
|
||||
}
|
||||
if (premiumData._pending?.proFeatures?.googleDrive?.clientId !== undefined) {
|
||||
pendingBlock.googleDriveClientId = premiumData._pending.proFeatures.googleDrive.clientId;
|
||||
}
|
||||
if (premiumData._pending?.proFeatures?.googleDrive?.apiKey !== undefined) {
|
||||
pendingBlock.googleDriveApiKey = premiumData._pending.proFeatures.googleDrive.apiKey;
|
||||
}
|
||||
if (premiumData._pending?.proFeatures?.googleDrive?.appId !== undefined) {
|
||||
pendingBlock.googleDriveAppId = premiumData._pending.proFeatures.googleDrive.appId;
|
||||
}
|
||||
|
||||
result._pending = pendingBlock;
|
||||
if (Object.keys(pendingBlock).length > 0) {
|
||||
result._pending = pendingBlock;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
@@ -207,6 +248,20 @@ export default function AdminConnectionsSection() {
|
||||
deltaSettings['system.mobileScannerSettings.stretchToFit'] = currentSettings.mobileScannerStretchToFit;
|
||||
}
|
||||
|
||||
// Google Drive settings
|
||||
if (currentSettings?.googleDriveEnabled !== undefined) {
|
||||
deltaSettings['premium.proFeatures.googleDrive.enabled'] = currentSettings.googleDriveEnabled;
|
||||
}
|
||||
if (currentSettings?.googleDriveClientId !== undefined) {
|
||||
deltaSettings['premium.proFeatures.googleDrive.clientId'] = currentSettings.googleDriveClientId;
|
||||
}
|
||||
if (currentSettings?.googleDriveApiKey !== undefined) {
|
||||
deltaSettings['premium.proFeatures.googleDrive.apiKey'] = currentSettings.googleDriveApiKey;
|
||||
}
|
||||
if (currentSettings?.googleDriveAppId !== undefined) {
|
||||
deltaSettings['premium.proFeatures.googleDrive.appId'] = currentSettings.googleDriveAppId;
|
||||
}
|
||||
|
||||
return {
|
||||
sectionData: {},
|
||||
deltaSettings
|
||||
@@ -264,6 +319,10 @@ export default function AdminConnectionsSection() {
|
||||
return settings?.telegram?.enabled === true;
|
||||
}
|
||||
|
||||
if (provider.id === 'googledrive') {
|
||||
return settings?.googleDriveEnabled === true;
|
||||
}
|
||||
|
||||
if (provider.id === 'oauth2-generic') {
|
||||
return settings?.oauth2?.enabled === true;
|
||||
}
|
||||
@@ -286,6 +345,15 @@ export default function AdminConnectionsSection() {
|
||||
return settings?.telegram || {};
|
||||
}
|
||||
|
||||
if (provider.id === 'googledrive') {
|
||||
return {
|
||||
enabled: settings?.googleDriveEnabled,
|
||||
clientId: settings?.googleDriveClientId,
|
||||
apiKey: settings?.googleDriveApiKey,
|
||||
appId: settings?.googleDriveAppId,
|
||||
};
|
||||
}
|
||||
|
||||
if (provider.id === 'oauth2-generic') {
|
||||
// Generic OAuth2 settings are at the root oauth2 level
|
||||
return {
|
||||
@@ -323,6 +391,14 @@ export default function AdminConnectionsSection() {
|
||||
setSettings({ ...settings, mail: updatedSettings });
|
||||
} else if (provider.id === 'telegram') {
|
||||
setSettings({ ...settings, telegram: updatedSettings });
|
||||
} else if (provider.id === 'googledrive') {
|
||||
setSettings({
|
||||
...settings,
|
||||
googleDriveEnabled: updatedSettings.enabled,
|
||||
googleDriveClientId: updatedSettings.clientId,
|
||||
googleDriveApiKey: updatedSettings.apiKey,
|
||||
googleDriveAppId: updatedSettings.appId,
|
||||
});
|
||||
} else if (provider.id === 'saml2') {
|
||||
setSettings({ ...settings, saml2: updatedSettings });
|
||||
} else if (provider.id === 'oauth2-generic') {
|
||||
@@ -407,6 +483,16 @@ export default function AdminConnectionsSection() {
|
||||
<Text fw={600} size="sm">{t('admin.settings.connections.mobileScanner.label', 'Mobile Phone Upload')}</Text>
|
||||
</Group>
|
||||
|
||||
{/* Documentation Link */}
|
||||
<Anchor
|
||||
href="https://docs.stirlingpdf.com/Functionality/Mobile-Scanner"
|
||||
target="_blank"
|
||||
size="xs"
|
||||
c="blue"
|
||||
>
|
||||
{t('admin.settings.connections.documentation', 'View documentation')} ↗
|
||||
</Anchor>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.connections.mobileScanner.enable', 'Enable QR Code Upload')}</Text>
|
||||
|
||||
@@ -22,6 +22,10 @@ interface GeneralSettingsData {
|
||||
appNameNavbar?: string;
|
||||
languages?: string[];
|
||||
logoStyle?: 'modern' | 'classic';
|
||||
hideDisabledTools?: {
|
||||
googleDrive?: boolean;
|
||||
mobileQRScanner?: boolean;
|
||||
};
|
||||
};
|
||||
system: {
|
||||
defaultLocale?: string;
|
||||
@@ -162,6 +166,8 @@ export default function AdminGeneralSection() {
|
||||
'ui.appNameNavbar': settings.ui?.appNameNavbar,
|
||||
'ui.languages': settings.ui?.languages,
|
||||
'ui.logoStyle': settings.ui?.logoStyle,
|
||||
'ui.hideDisabledTools.googleDrive': settings.ui?.hideDisabledTools?.googleDrive,
|
||||
'ui.hideDisabledTools.mobileQRScanner': settings.ui?.hideDisabledTools?.mobileQRScanner,
|
||||
// System settings
|
||||
'system.defaultLocale': settings.system?.defaultLocale,
|
||||
'system.showUpdate': settings.system?.showUpdate,
|
||||
@@ -503,6 +509,41 @@ export default function AdminGeneralSection() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Hide Disabled Tools Settings */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '1rem' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.general.hideDisabledTools.googleDrive.label', 'Hide Google Drive')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.general.hideDisabledTools.googleDrive.description', 'Hide Google Drive button when not enabled')}
|
||||
</Text>
|
||||
</div>
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
checked={settings.ui?.hideDisabledTools?.googleDrive || false}
|
||||
onChange={(e) => setSettings({ ...settings, ui: { ...settings.ui, hideDisabledTools: { ...settings.ui?.hideDisabledTools, googleDrive: e.target.checked } } })}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('ui.hideDisabledTools.googleDrive')} />
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.general.hideDisabledTools.mobileScanner.label', 'Hide Mobile Scanner')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.general.hideDisabledTools.mobileScanner.description', 'Hide mobile QR scanner button when not enabled')}
|
||||
</Text>
|
||||
</div>
|
||||
<Group gap="xs">
|
||||
<Switch
|
||||
checked={settings.ui?.hideDisabledTools?.mobileQRScanner || false}
|
||||
onChange={(e) => setSettings({ ...settings, ui: { ...settings.ui, hideDisabledTools: { ...settings.ui?.hideDisabledTools, mobileQRScanner: e.target.checked } } })}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
<PendingBadge show={isFieldPending('ui.hideDisabledTools.mobileQRScanner')} />
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.general.showUpdate.label', 'Show Update Notifications')}</Text>
|
||||
|
||||
Reference in New Issue
Block a user