mirror of
https://github.com/ONLYOFFICE/server.git
synced 2026-04-07 14:04:35 +08:00
[feature] Add uploading of PDF signing certificate to Admin Panel
This commit is contained in:
@ -244,6 +244,61 @@ const callCommandService = async body => {
|
|||||||
return response.json();
|
return response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getSigningCertificateStatus = async () => {
|
||||||
|
const response = await safeFetch(`${API_BASE_PATH}/config/signing-certificate/status`, {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 401) throw new Error('UNAUTHORIZED');
|
||||||
|
throw new Error('Failed to check certificate status');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uploadSigningCertificate = async file => {
|
||||||
|
const response = await safeFetch(`${API_BASE_PATH}/config/signing-certificate`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/octet-stream'
|
||||||
|
},
|
||||||
|
body: file
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 401) throw new Error('UNAUTHORIZED');
|
||||||
|
if (response.status === 403) throw new Error('Only admin can upload signing certificates');
|
||||||
|
let errorMessage = 'Failed to upload certificate';
|
||||||
|
try {
|
||||||
|
const errorData = await response.json();
|
||||||
|
errorMessage = errorData.error || errorMessage;
|
||||||
|
} catch {
|
||||||
|
// JSON parsing failed, use default message
|
||||||
|
}
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteSigningCertificate = async () => {
|
||||||
|
const response = await safeFetch(`${API_BASE_PATH}/config/signing-certificate`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 401) throw new Error('UNAUTHORIZED');
|
||||||
|
if (response.status === 403) throw new Error('Only admin can delete signing certificates');
|
||||||
|
let errorMessage = 'Failed to delete certificate';
|
||||||
|
try {
|
||||||
|
const errorData = await response.json();
|
||||||
|
errorMessage = errorData.error || errorMessage;
|
||||||
|
} catch {
|
||||||
|
// JSON parsing failed, use default message
|
||||||
|
}
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
};
|
||||||
|
|
||||||
export const getForgottenList = async () => {
|
export const getForgottenList = async () => {
|
||||||
const result = await callCommandService({c: 'getForgottenList'});
|
const result = await callCommandService({c: 'getForgottenList'});
|
||||||
const files = result.keys || [];
|
const files = result.keys || [];
|
||||||
|
|||||||
@ -1,9 +1,55 @@
|
|||||||
import {resetConfiguration} from '../../api';
|
import {useState, useEffect, useRef, useCallback} from 'react';
|
||||||
|
import {useSelector, useDispatch} from 'react-redux';
|
||||||
|
import {resetConfiguration, uploadSigningCertificate, deleteSigningCertificate, getSigningCertificateStatus} from '../../api';
|
||||||
|
import {saveConfig, selectConfig} from '../../store/slices/configSlice';
|
||||||
|
import {getNestedValue} from '../../utils/getNestedValue';
|
||||||
|
import {mergeNestedObjects} from '../../utils/mergeNestedObjects';
|
||||||
import Button from '../../components/Button/Button';
|
import Button from '../../components/Button/Button';
|
||||||
|
import Input from '../../components/Input/Input';
|
||||||
import Section from '../../components/Section/Section';
|
import Section from '../../components/Section/Section';
|
||||||
|
import PasswordInput from '../../components/PasswordInput/PasswordInput';
|
||||||
|
import Note from '../../components/Note/Note';
|
||||||
import './Settings.scss';
|
import './Settings.scss';
|
||||||
|
|
||||||
const Settings = () => {
|
const Settings = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const config = useSelector(selectConfig);
|
||||||
|
const fileInputRef = useRef(null);
|
||||||
|
|
||||||
|
// PDF Signing state
|
||||||
|
const [certificateExists, setCertificateExists] = useState(false);
|
||||||
|
const [selectedFile, setSelectedFile] = useState(null);
|
||||||
|
const [signingPassphrase, setSigningPassphrase] = useState('');
|
||||||
|
const [savedPassphrase, setSavedPassphrase] = useState('');
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [successMessage, setSuccessMessage] = useState(null);
|
||||||
|
|
||||||
|
// Check certificate status on server
|
||||||
|
const checkCertificateStatus = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const status = await getSigningCertificateStatus();
|
||||||
|
setCertificateExists(status.exists);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to check certificate status:', err);
|
||||||
|
setCertificateExists(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Load config data and check certificate status
|
||||||
|
useEffect(() => {
|
||||||
|
if (config) {
|
||||||
|
const passphrase = getNestedValue(config, 'FileConverter.converter.spawnOptions.env.SIGNING_KEYSTORE_PASSPHRASE') || '';
|
||||||
|
setSigningPassphrase(passphrase);
|
||||||
|
setSavedPassphrase(passphrase);
|
||||||
|
checkCertificateStatus();
|
||||||
|
}
|
||||||
|
}, [config, checkCertificateStatus]);
|
||||||
|
|
||||||
|
const showSuccess = message => {
|
||||||
|
setSuccessMessage(message);
|
||||||
|
setTimeout(() => setSuccessMessage(null), 3000);
|
||||||
|
};
|
||||||
|
|
||||||
const handleResetConfig = async () => {
|
const handleResetConfig = async () => {
|
||||||
if (!window.confirm('Are you sure you want to reset the configuration? This action cannot be undone.')) {
|
if (!window.confirm('Are you sure you want to reset the configuration? This action cannot be undone.')) {
|
||||||
throw new Error('Operation cancelled');
|
throw new Error('Operation cancelled');
|
||||||
@ -12,6 +58,130 @@ const Settings = () => {
|
|||||||
await resetConfiguration();
|
await resetConfiguration();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle file selection - stores file in browser state
|
||||||
|
const handleFileSelect = event => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
// Validate file extension
|
||||||
|
const fileName = file.name.toLowerCase();
|
||||||
|
if (!fileName.endsWith('.p12') && !fileName.endsWith('.pfx')) {
|
||||||
|
setError('Invalid file format. Please select a .p12 or .pfx file.');
|
||||||
|
setSelectedFile(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(null);
|
||||||
|
setSelectedFile(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectFileClick = () => {
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save - uploads file (if selected) AND saves/removes passphrase
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
let fileUploaded = false;
|
||||||
|
|
||||||
|
// If file is selected, upload it first
|
||||||
|
if (selectedFile) {
|
||||||
|
await uploadSigningCertificate(selectedFile);
|
||||||
|
fileUploaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle passphrase: empty means remove, non-empty means save
|
||||||
|
const passphraseChanged = signingPassphrase !== savedPassphrase;
|
||||||
|
if (signingPassphrase) {
|
||||||
|
// Save passphrase to config
|
||||||
|
const configUpdate = {
|
||||||
|
'FileConverter.converter.spawnOptions': {
|
||||||
|
env: {
|
||||||
|
SIGNING_KEYSTORE_PASSPHRASE: signingPassphrase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const mergedConfig = mergeNestedObjects([configUpdate]);
|
||||||
|
await dispatch(saveConfig(mergedConfig)).unwrap();
|
||||||
|
} else if (passphraseChanged) {
|
||||||
|
// Empty passphrase means remove the key from config (only if changed)
|
||||||
|
await resetConfiguration(['FileConverter.converter.spawnOptions.env.SIGNING_KEYSTORE_PASSPHRASE']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileUploaded) {
|
||||||
|
setCertificateExists(true);
|
||||||
|
}
|
||||||
|
setSavedPassphrase(signingPassphrase);
|
||||||
|
setSelectedFile(null);
|
||||||
|
|
||||||
|
// Reset file input
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show appropriate success message
|
||||||
|
let message;
|
||||||
|
if (fileUploaded && passphraseChanged) {
|
||||||
|
message = signingPassphrase ? 'Certificate uploaded and passphrase saved' : 'Certificate uploaded and passphrase cleared';
|
||||||
|
} else if (fileUploaded) {
|
||||||
|
message = 'Certificate uploaded successfully';
|
||||||
|
} else if (passphraseChanged) {
|
||||||
|
message = signingPassphrase ? 'Passphrase saved successfully' : 'Passphrase cleared successfully';
|
||||||
|
} else {
|
||||||
|
message = 'Settings saved';
|
||||||
|
}
|
||||||
|
showSuccess(message);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message || 'Failed to save');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove - deletes file AND resets passphrase in config
|
||||||
|
const handleRemove = async () => {
|
||||||
|
if (!certificateExists && !selectedFile) return;
|
||||||
|
|
||||||
|
if (!window.confirm('Are you sure you want to remove the signing certificate? This will also clear the passphrase.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// Delete file from server if exists
|
||||||
|
if (certificateExists) {
|
||||||
|
await deleteSigningCertificate();
|
||||||
|
setCertificateExists(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset passphrase in config only if it was set
|
||||||
|
if (savedPassphrase) {
|
||||||
|
await resetConfiguration(['FileConverter.converter.spawnOptions.env.SIGNING_KEYSTORE_PASSPHRASE']);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedFile(null);
|
||||||
|
setSigningPassphrase('');
|
||||||
|
setSavedPassphrase('');
|
||||||
|
|
||||||
|
// Reset file input
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccess('Certificate removed successfully');
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message || 'Failed to remove certificate');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePassphraseChange = value => {
|
||||||
|
setSigningPassphrase(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasChanges = selectedFile || signingPassphrase !== savedPassphrase;
|
||||||
|
const canRemove = certificateExists || selectedFile;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='settings-page'>
|
<div className='settings-page'>
|
||||||
<div className='page-header'>
|
<div className='page-header'>
|
||||||
@ -25,6 +195,66 @@ const Settings = () => {
|
|||||||
>
|
>
|
||||||
<Button onClick={handleResetConfig}>Reset</Button>
|
<Button onClick={handleResetConfig}>Reset</Button>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
<Section title='PDF Digital Signature' description='Configure PKCS#12 (.p12/.pfx) certificate for digitally signing submitted PDF forms'>
|
||||||
|
<Note type='note'>
|
||||||
|
The signing certificate will be used to digitally sign PDF forms when they are submitted. Only submitted PDF forms will be signed, not
|
||||||
|
regular PDF conversions.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
<div className='form-row'>
|
||||||
|
<div className='certificate-status'>
|
||||||
|
<span className='certificate-label'>Certificate Status:</span>
|
||||||
|
{certificateExists ? (
|
||||||
|
<span className='certificate-installed'>Certificate installed</span>
|
||||||
|
) : (
|
||||||
|
<span className='certificate-not-installed'>No certificate</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='form-row'>
|
||||||
|
<input ref={fileInputRef} type='file' accept='.p12,.pfx' onChange={handleFileSelect} style={{display: 'none'}} />
|
||||||
|
<div className='file-input-row'>
|
||||||
|
<Input
|
||||||
|
label='Certificate File'
|
||||||
|
value={selectedFile ? selectedFile.name : ''}
|
||||||
|
onChange={() => {}}
|
||||||
|
placeholder='No file selected'
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<Button onClick={handleSelectFileClick} disableResult>
|
||||||
|
Browse
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='form-row'>
|
||||||
|
<PasswordInput
|
||||||
|
label='Certificate Passphrase'
|
||||||
|
value={signingPassphrase}
|
||||||
|
onChange={handlePassphraseChange}
|
||||||
|
placeholder='Leave empty if certificate is not encrypted'
|
||||||
|
description='Passphrase to unlock the PKCS#12 certificate. Leave empty if the certificate is not password-protected.'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='form-row'>
|
||||||
|
<div className='actions-section'>
|
||||||
|
<Button onClick={handleSave} disabled={!hasChanges}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
{canRemove && (
|
||||||
|
<Button onClick={handleRemove} className='delete-button'>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className='message-error'>{error}</div>}
|
||||||
|
{successMessage && <div className='message-success'>{successMessage}</div>}
|
||||||
|
</Section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
.settings-page {
|
.settings-page {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
@ -89,32 +90,91 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
.form-row {
|
||||||
background: #f8d7da;
|
margin-bottom: 24px;
|
||||||
border: 1px solid #f5c6cb;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 15px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
|
|
||||||
p {
|
&:last-child {
|
||||||
margin: 0;
|
margin-bottom: 0;
|
||||||
color: #721c24;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-message {
|
.certificate-status {
|
||||||
background: #d4edda;
|
display: flex;
|
||||||
border: 1px solid #c3e6cb;
|
align-items: center;
|
||||||
border-radius: 4px;
|
gap: 12px;
|
||||||
padding: 15px;
|
padding: 12px 16px;
|
||||||
margin-bottom: 20px;
|
background: #f8f9fa;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
|
||||||
p {
|
.certificate-label {
|
||||||
margin: 0;
|
font-weight: 500;
|
||||||
color: #155724;
|
color: #333;
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.certificate-installed {
|
||||||
|
color: #007b14;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '✓ ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.certificate-not-installed {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
input[readonly] {
|
||||||
|
cursor: default;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-section {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-button {
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
color: #666 !important;
|
||||||
|
border: 1px solid #e0e0e0 !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #e8e8e8 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-error {
|
||||||
|
color: #cb0000;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #fef2f2;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-success {
|
||||||
|
color: #007b14;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f0fdf4;
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
const config = require('config');
|
const config = require('config');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const bodyParser = require('body-parser');
|
const bodyParser = require('body-parser');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
const tenantManager = require('../../../../../Common/sources/tenantManager');
|
const tenantManager = require('../../../../../Common/sources/tenantManager');
|
||||||
const runtimeConfigManager = require('../../../../../Common/sources/runtimeConfigManager');
|
const runtimeConfigManager = require('../../../../../Common/sources/runtimeConfigManager');
|
||||||
const {getScopedConfig, getScopedBaseConfig, validateScoped, getDiffFromBase} = require('./config.service');
|
const {getScopedConfig, getScopedBaseConfig, validateScoped, getDiffFromBase} = require('./config.service');
|
||||||
@ -113,9 +115,9 @@ router.post('/reset', validateJWT, rawFileParser, async (req, res) => {
|
|||||||
} else {
|
} else {
|
||||||
resetConfig = JSON.parse(JSON.stringify(currentConfig));
|
resetConfig = JSON.parse(JSON.stringify(currentConfig));
|
||||||
|
|
||||||
paths.forEach(path => {
|
paths.forEach(pathItem => {
|
||||||
if (path && path !== '*') {
|
if (pathItem && pathItem !== '*') {
|
||||||
const pathParts = path.split('.');
|
const pathParts = pathItem.split('.');
|
||||||
let current = resetConfig;
|
let current = resetConfig;
|
||||||
|
|
||||||
for (let i = 0; i < pathParts.length - 1; i++) {
|
for (let i = 0; i < pathParts.length - 1; i++) {
|
||||||
@ -160,4 +162,112 @@ router.post('/reset', validateJWT, rawFileParser, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get the fixed signing certificate path from config
|
||||||
|
function getSigningCertPath() {
|
||||||
|
return config.get('FileConverter.converter.signingKeyStorePath') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check signing certificate status (does file exist on disk)
|
||||||
|
router.get('/signing-certificate/status', validateJWT, async (req, res) => {
|
||||||
|
const ctx = req.ctx;
|
||||||
|
try {
|
||||||
|
// Only admin can check certificate status
|
||||||
|
if (tenantManager.isMultitenantMode(ctx) && !tenantManager.isDefaultTenant(ctx)) {
|
||||||
|
return res.status(403).json({error: 'Only admin can check signing certificate status'});
|
||||||
|
}
|
||||||
|
|
||||||
|
const certPath = getSigningCertPath();
|
||||||
|
|
||||||
|
if (!certPath) {
|
||||||
|
return res.status(200).json({exists: false, configured: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileExists = fs.existsSync(certPath);
|
||||||
|
// Don't expose full path - only return existence status
|
||||||
|
res.status(200).json({exists: fileExists, configured: true});
|
||||||
|
} catch (error) {
|
||||||
|
ctx.logger.error('Signing certificate status check error: %s', error.stack);
|
||||||
|
res.status(500).json({error: 'Failed to check certificate status'});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Upload signing certificate (.p12/.pfx file) - replaces file at fixed path from config
|
||||||
|
router.post('/signing-certificate', validateJWT, rawFileParser, async (req, res) => {
|
||||||
|
const ctx = req.ctx;
|
||||||
|
try {
|
||||||
|
ctx.logger.info('signing certificate upload start');
|
||||||
|
|
||||||
|
// Only admin can upload certificates
|
||||||
|
if (tenantManager.isMultitenantMode(ctx) && !tenantManager.isDefaultTenant(ctx)) {
|
||||||
|
return res.status(403).json({error: 'Only admin can upload signing certificates'});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.body || req.body.length === 0) {
|
||||||
|
return res.status(400).json({error: 'No file uploaded'});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic validation: P12/PFX files should have reasonable size (1KB - 100KB typically)
|
||||||
|
const MAX_CERT_SIZE = 1024 * 1024; // 1MB max
|
||||||
|
if (req.body.length > MAX_CERT_SIZE) {
|
||||||
|
return res.status(400).json({error: 'File too large. Certificate files should be less than 1MB'});
|
||||||
|
}
|
||||||
|
|
||||||
|
const certPath = getSigningCertPath();
|
||||||
|
if (!certPath) {
|
||||||
|
return res.status(400).json({error: 'signingKeyStorePath is not configured'});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
const certDir = path.dirname(certPath);
|
||||||
|
if (!fs.existsSync(certDir)) {
|
||||||
|
fs.mkdirSync(certDir, {recursive: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the file (overwrites existing)
|
||||||
|
fs.writeFileSync(certPath, req.body);
|
||||||
|
|
||||||
|
ctx.logger.info('Signing certificate uploaded successfully: %s', certPath);
|
||||||
|
|
||||||
|
// Don't expose path in response
|
||||||
|
res.status(200).json({success: true});
|
||||||
|
} catch (error) {
|
||||||
|
ctx.logger.error('Signing certificate upload error: %s', error.stack);
|
||||||
|
res.status(500).json({error: 'Failed to upload certificate'});
|
||||||
|
} finally {
|
||||||
|
ctx.logger.info('signing certificate upload end');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete signing certificate - removes file at fixed path from config
|
||||||
|
router.delete('/signing-certificate', validateJWT, async (req, res) => {
|
||||||
|
const ctx = req.ctx;
|
||||||
|
try {
|
||||||
|
ctx.logger.info('signing certificate delete start');
|
||||||
|
|
||||||
|
// Only admin can delete certificates
|
||||||
|
if (tenantManager.isMultitenantMode(ctx) && !tenantManager.isDefaultTenant(ctx)) {
|
||||||
|
return res.status(403).json({error: 'Only admin can delete signing certificates'});
|
||||||
|
}
|
||||||
|
|
||||||
|
const certPath = getSigningCertPath();
|
||||||
|
|
||||||
|
if (!certPath) {
|
||||||
|
return res.status(404).json({error: 'signingKeyStorePath is not configured'});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the file if it exists
|
||||||
|
if (fs.existsSync(certPath)) {
|
||||||
|
fs.unlinkSync(certPath);
|
||||||
|
ctx.logger.info('Signing certificate deleted: %s', certPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({success: true});
|
||||||
|
} catch (error) {
|
||||||
|
ctx.logger.error('Signing certificate delete error: %s', error.stack);
|
||||||
|
res.status(500).json({error: 'Failed to delete certificate'});
|
||||||
|
} finally {
|
||||||
|
ctx.logger.info('signing certificate delete end');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -563,6 +563,7 @@
|
|||||||
"presentationThemesDir": "null",
|
"presentationThemesDir": "null",
|
||||||
"x2tPath": "null",
|
"x2tPath": "null",
|
||||||
"docbuilderPath": "null",
|
"docbuilderPath": "null",
|
||||||
|
"signingKeyStorePath": "",
|
||||||
"args": "",
|
"args": "",
|
||||||
"spawnOptions": {},
|
"spawnOptions": {},
|
||||||
"errorfiles": "",
|
"errorfiles": "",
|
||||||
|
|||||||
@ -80,6 +80,7 @@
|
|||||||
"presentationThemesDir": "../../sdkjs/slide/themes",
|
"presentationThemesDir": "../../sdkjs/slide/themes",
|
||||||
"x2tPath": "../FileConverter/bin/x2t",
|
"x2tPath": "../FileConverter/bin/x2t",
|
||||||
"docbuilderPath": "../FileConverter/bin/docbuilder",
|
"docbuilderPath": "../FileConverter/bin/docbuilder",
|
||||||
|
"signingKeyStorePath": "./../signing-keystore.p12",
|
||||||
"spawnOptions": {
|
"spawnOptions": {
|
||||||
"env": {
|
"env": {
|
||||||
"X2T_MEMORY_LIMIT": "16GB"
|
"X2T_MEMORY_LIMIT": "16GB"
|
||||||
|
|||||||
@ -86,6 +86,7 @@
|
|||||||
"presentationThemesDir": "../../sdkjs/slide/themes",
|
"presentationThemesDir": "../../sdkjs/slide/themes",
|
||||||
"x2tPath": "../FileConverter/bin/x2t",
|
"x2tPath": "../FileConverter/bin/x2t",
|
||||||
"docbuilderPath": "../FileConverter/Bin/docbuilder",
|
"docbuilderPath": "../FileConverter/Bin/docbuilder",
|
||||||
|
"signingKeyStorePath": "./../signing-keystore.p12",
|
||||||
"spawnOptions": {
|
"spawnOptions": {
|
||||||
"env": {
|
"env": {
|
||||||
"X2T_MEMORY_LIMIT": "16GB"
|
"X2T_MEMORY_LIMIT": "16GB"
|
||||||
|
|||||||
@ -86,6 +86,7 @@
|
|||||||
"presentationThemesDir": "../../sdkjs/slide/themes",
|
"presentationThemesDir": "../../sdkjs/slide/themes",
|
||||||
"x2tPath": "../FileConverter/Bin/x2t.exe",
|
"x2tPath": "../FileConverter/Bin/x2t.exe",
|
||||||
"docbuilderPath": "../FileConverter/Bin/docbuilder.exe",
|
"docbuilderPath": "../FileConverter/Bin/docbuilder.exe",
|
||||||
|
"signingKeyStorePath": "./../signing-keystore.p12",
|
||||||
"spawnOptions": {
|
"spawnOptions": {
|
||||||
"env": {
|
"env": {
|
||||||
"X2T_MEMORY_LIMIT": "16GB"
|
"X2T_MEMORY_LIMIT": "16GB"
|
||||||
|
|||||||
@ -71,7 +71,8 @@
|
|||||||
"fontDir": "/usr/share/fonts",
|
"fontDir": "/usr/share/fonts",
|
||||||
"presentationThemesDir": "/var/www/onlyoffice/documentserver/sdkjs/slide/themes",
|
"presentationThemesDir": "/var/www/onlyoffice/documentserver/sdkjs/slide/themes",
|
||||||
"x2tPath": "/var/www/onlyoffice/documentserver/server/FileConverter/bin/x2t",
|
"x2tPath": "/var/www/onlyoffice/documentserver/server/FileConverter/bin/x2t",
|
||||||
"docbuilderPath": "/var/www/onlyoffice/documentserver/server/FileConverter/bin/docbuilder"
|
"docbuilderPath": "/var/www/onlyoffice/documentserver/server/FileConverter/bin/docbuilder",
|
||||||
|
"signingKeyStorePath": "/var/www/onlyoffice/documentserver/../Data/signing-keystore.p12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SpellChecker": {
|
"SpellChecker": {
|
||||||
|
|||||||
@ -67,7 +67,8 @@
|
|||||||
"fontDir": "",
|
"fontDir": "",
|
||||||
"presentationThemesDir": "../../sdkjs/slide/themes",
|
"presentationThemesDir": "../../sdkjs/slide/themes",
|
||||||
"x2tPath": "../FileConverter/bin/x2t.exe",
|
"x2tPath": "../FileConverter/bin/x2t.exe",
|
||||||
"docbuilderPath": "../FileConverter/bin/docbuilder.exe"
|
"docbuilderPath": "../FileConverter/bin/docbuilder.exe",
|
||||||
|
"signingKeyStorePath": "./../signing-keystore.p12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SpellChecker": {
|
"SpellChecker": {
|
||||||
|
|||||||
@ -217,6 +217,28 @@
|
|||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
"maxDownloadBytes": {"type": "integer", "minimum": 0, "maximum": 10485760000, "x-scope": ["admin", "tenant"]},
|
"maxDownloadBytes": {"type": "integer", "minimum": 0, "maximum": 10485760000, "x-scope": ["admin", "tenant"]},
|
||||||
|
"signingKeyStorePath": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Path to PKCS#12 (.p12/.pfx) keystore file for PDF document signing",
|
||||||
|
"x-scope": "admin"
|
||||||
|
},
|
||||||
|
"spawnOptions": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"x-scope": "admin",
|
||||||
|
"properties": {
|
||||||
|
"env": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"SIGNING_KEYSTORE_PASSPHRASE": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Passphrase for the PKCS#12 keystore file"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"inputLimits": {
|
"inputLimits": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"x-scope": ["admin", "tenant"],
|
"x-scope": ["admin", "tenant"],
|
||||||
|
|||||||
@ -69,6 +69,7 @@ const cfgSpawnOptions = config.util.cloneDeep(config.get('FileConverter.converte
|
|||||||
const cfgErrorFiles = config.get('FileConverter.converter.errorfiles');
|
const cfgErrorFiles = config.get('FileConverter.converter.errorfiles');
|
||||||
const cfgInputLimits = config.get('FileConverter.converter.inputLimits');
|
const cfgInputLimits = config.get('FileConverter.converter.inputLimits');
|
||||||
const cfgStreamWriterBufferSize = config.get('FileConverter.converter.streamWriterBufferSize');
|
const cfgStreamWriterBufferSize = config.get('FileConverter.converter.streamWriterBufferSize');
|
||||||
|
const cfgSigningKeyStorePath = config.get('FileConverter.converter.signingKeyStorePath');
|
||||||
//cfgMaxRequestChanges was obtained as a result of the test: 84408 changes - 5,16 MB
|
//cfgMaxRequestChanges was obtained as a result of the test: 84408 changes - 5,16 MB
|
||||||
const cfgMaxRequestChanges = config.get('services.CoAuthoring.server.maxRequestChanges');
|
const cfgMaxRequestChanges = config.get('services.CoAuthoring.server.maxRequestChanges');
|
||||||
const cfgForgottenFiles = config.get('services.CoAuthoring.server.forgottenfiles');
|
const cfgForgottenFiles = config.get('services.CoAuthoring.server.forgottenfiles');
|
||||||
@ -182,6 +183,12 @@ TaskQueueDataConvert.prototype = {
|
|||||||
xml += this.serializeXmlProp('m_oTimestamp', this.timestamp.toISOString());
|
xml += this.serializeXmlProp('m_oTimestamp', this.timestamp.toISOString());
|
||||||
xml += this.serializeXmlProp('m_bIsNoBase64', this.noBase64);
|
xml += this.serializeXmlProp('m_bIsNoBase64', this.noBase64);
|
||||||
xml += this.serializeXmlProp('m_sConvertToOrigin', this.convertToOrigin);
|
xml += this.serializeXmlProp('m_sConvertToOrigin', this.convertToOrigin);
|
||||||
|
if (this.formatTo === constants.AVS_OFFICESTUDIO_FILE_DOCUMENT_OFORM_PDF) {
|
||||||
|
const signingKeyStorePath = ctx.getCfg('FileConverter.converter.signingKeyStorePath', cfgSigningKeyStorePath);
|
||||||
|
if (signingKeyStorePath && fs.existsSync(signingKeyStorePath)) {
|
||||||
|
xml += this.serializeXmlProp('m_sSigningKeyStorePath', signingKeyStorePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
xml += this.serializeLimit(ctx);
|
xml += this.serializeLimit(ctx);
|
||||||
xml += this.serializeOptions(ctx, false, this.oformAsPdf);
|
xml += this.serializeOptions(ctx, false, this.oformAsPdf);
|
||||||
xml += '</TaskQueueDataConvert>';
|
xml += '</TaskQueueDataConvert>';
|
||||||
|
|||||||
Reference in New Issue
Block a user