option to hide google drive and add settings (#5863)

Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
This commit is contained in:
Anthony Stirling
2026-03-06 10:09:33 +00:00
committed by GitHub
parent cafcee6c99
commit 7d640e9ce6
13 changed files with 905 additions and 332 deletions

View File

@@ -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 {

View File

@@ -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

View File

@@ -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'])

View File

@@ -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"

View File

@@ -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';

View File

@@ -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>
)}
</>
);

View File

@@ -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) && (

View File

@@ -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,
];
};

View File

@@ -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

View File

@@ -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,
};
}

View File

@@ -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';

View File

@@ -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>

View File

@@ -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>