diff --git a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index cb30a085fc..8423c279e2 100644 --- a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -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 { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java index dabaab32f6..727f6c9e78 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java @@ -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 diff --git a/app/core/src/main/resources/settings.yml.template b/app/core/src/main/resources/settings.yml.template index 94ceb34613..363f399362 100644 --- a/app/core/src/main/resources/settings.yml.template +++ b/app/core/src/main/resources/settings.yml.template @@ -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']) diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index 08c21e0090..dda4834048 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -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" diff --git a/frontend/src/core/components/FileManager.tsx b/frontend/src/core/components/FileManager.tsx index 0afe91d086..e3c9a84661 100644 --- a/frontend/src/core/components/FileManager.tsx +++ b/frontend/src/core/components/FileManager.tsx @@ -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 = ({ selectedTool }) => { const { isFilesModalOpen, closeFilesModal, onFileUpload, onRecentFileSelect } = useFilesModalContext(); + const { config } = useAppConfig(); const [recentFiles, setRecentFiles] = useState([]); const [isDragging, setIsDragging] = useState(false); const [isMobile, setIsMobile] = useState(false); @@ -92,9 +94,14 @@ const FileManager: React.FC = ({ 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 = ({ selectedTool }) => { console.warn('Failed to preload Google Drive scripts:', error); }); } - }, []); + }, [googleDriveBackendConfig]); // Modal size constants for consistent scaling const modalHeight = '80vh'; diff --git a/frontend/src/core/components/fileManager/FileSourceButtons.tsx b/frontend/src/core/components/fileManager/FileSourceButtons.tsx index ba2ebcb92d..8695821efd 100644 --- a/frontend/src/core/components/fileManager/FileSourceButtons.tsx +++ b/frontend/src/core/components/fileManager/FileSourceButtons.tsx @@ -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 }) => ( + Google Drive +); + const FileSourceButtons: React.FC = ({ horizontal = false }) => { @@ -51,6 +67,12 @@ const FileSourceButtons: React.FC = ({ } }; + // 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 = ({ {horizontal ? terminology.upload : terminology.uploadFiles} - + }} + title={!isGoogleDriveEnabled ? t('fileManager.googleDriveNotAvailable', 'Google Drive integration not available') : undefined} + > + {horizontal ? t('fileManager.googleDriveShort', 'Drive') : t('fileManager.googleDrive', 'Google Drive')} + + )} - + }} + title={!isMobileUploadEnabled ? t('fileManager.mobileUploadNotAvailable', 'Mobile upload not available') : undefined} + > + {horizontal ? t('fileManager.mobileShort', 'Mobile') : t('fileManager.mobileUpload', 'Mobile Upload')} + + )} ); diff --git a/frontend/src/core/components/shared/config/configSections/ProviderCard.tsx b/frontend/src/core/components/shared/config/configSections/ProviderCard.tsx index 570f9c3da3..be3348b794 100644 --- a/frontend/src/core/components/shared/config/configSections/ProviderCard.tsx +++ b/frontend/src/core/components/shared/config/configSections/ProviderCard.tsx @@ -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 */} + {/* Documentation Link */} + {provider.documentationUrl && ( + + {t('admin.settings.connections.documentation', 'View documentation')} ↗ + + )} + {provider.fields.map((field) => renderField(field))} {!readOnly && (onSave || onDisconnect) && ( diff --git a/frontend/src/core/components/shared/config/configSections/providerDefinitions.ts b/frontend/src/core/components/shared/config/configSections/providerDefinitions.ts index 37b34e26c0..cb1f69e0d4 100644 --- a/frontend/src/core/components/shared/config/configSections/providerDefinitions.ts +++ b/frontend/src/core/components/shared/config/configSections/providerDefinitions.ts @@ -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, ]; }; diff --git a/frontend/src/core/hooks/useGoogleDrivePicker.ts b/frontend/src/core/hooks/useGoogleDrivePicker.ts index 9e60c2357e..1e9e0b4882 100644 --- a/frontend/src/core/hooks/useGoogleDrivePicker.ts +++ b/frontend/src/core/hooks/useGoogleDrivePicker.ts @@ -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(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 diff --git a/frontend/src/core/services/googleDrivePickerService.ts b/frontend/src/core/services/googleDrivePickerService.ts index d2956c38a6..1ef0bf8bba 100644 --- a/frontend/src/core/services/googleDrivePickerService.ts +++ b/frontend/src/core/services/googleDrivePickerService.ts @@ -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, }; } diff --git a/frontend/src/core/types/appConfig.ts b/frontend/src/core/types/appConfig.ts index 6b7f8c9d93..0b86070680 100644 --- a/frontend/src/core/types/appConfig.ts +++ b/frontend/src/core/types/appConfig.ts @@ -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'; diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminConnectionsSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminConnectionsSection.tsx index cdaa4e00b1..f3cec6a1c7 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminConnectionsSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminConnectionsSection.tsx @@ -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 = { - 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 = {}; + 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() { {t('admin.settings.connections.mobileScanner.label', 'Mobile Phone Upload')} + {/* Documentation Link */} + + {t('admin.settings.connections.documentation', 'View documentation')} ↗ + +
{t('admin.settings.connections.mobileScanner.enable', 'Enable QR Code Upload')} diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx index a251aec67c..9ca8a60303 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx @@ -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() { />
+ {/* Hide Disabled Tools Settings */} +
+
+ {t('admin.settings.general.hideDisabledTools.googleDrive.label', 'Hide Google Drive')} + + {t('admin.settings.general.hideDisabledTools.googleDrive.description', 'Hide Google Drive button when not enabled')} + +
+ + setSettings({ ...settings, ui: { ...settings.ui, hideDisabledTools: { ...settings.ui?.hideDisabledTools, googleDrive: e.target.checked } } })} + disabled={!loginEnabled} + /> + + +
+ +
+
+ {t('admin.settings.general.hideDisabledTools.mobileScanner.label', 'Hide Mobile Scanner')} + + {t('admin.settings.general.hideDisabledTools.mobileScanner.description', 'Hide mobile QR scanner button when not enabled')} + +
+ + setSettings({ ...settings, ui: { ...settings.ui, hideDisabledTools: { ...settings.ui?.hideDisabledTools, mobileQRScanner: e.target.checked } } })} + disabled={!loginEnabled} + /> + + +
+
{t('admin.settings.general.showUpdate.label', 'Show Update Notifications')}