mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 02:54:06 +02:00
feat(security): add RFC 3161 PDF timestamp tool (#5855)
Co-authored-by: Anthony Stirling <[email protected]>
This commit is contained in:
co-authored by
Anthony Stirling
parent
7b3985e34a
commit
8bbfbd63d7
@@ -0,0 +1,80 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Stack, Text, Select } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
TimestampPdfParameters,
|
||||
FALLBACK_TSA_PRESETS,
|
||||
} from "@app/hooks/tools/timestampPdf/useTimestampPdfParameters";
|
||||
import { useAppConfig } from "@app/contexts/AppConfigContext";
|
||||
|
||||
interface TimestampPdfSettingsProps {
|
||||
parameters: TimestampPdfParameters;
|
||||
onParameterChange: <K extends keyof TimestampPdfParameters>(
|
||||
key: K,
|
||||
value: TimestampPdfParameters[K]
|
||||
) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const TimestampPdfSettings = ({
|
||||
parameters,
|
||||
onParameterChange,
|
||||
disabled = false,
|
||||
}: TimestampPdfSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { config } = useAppConfig();
|
||||
const defaultApplied = useRef(false);
|
||||
|
||||
// Use backend presets (single source of truth) with fallback (TASK-10)
|
||||
const presets = config?.timestampTsaPresets ?? FALLBACK_TSA_PRESETS;
|
||||
|
||||
// Build dropdown: presets + admin custom URLs, deduplicated (TASK-9)
|
||||
const presetUrls = new Set(presets.map((p) => p.url));
|
||||
const adminCustomUrls = (config?.timestampCustomTsaUrls ?? []).filter(
|
||||
(url) => !presetUrls.has(url)
|
||||
);
|
||||
const selectData = [
|
||||
...presets.map((preset) => ({ value: preset.url, label: preset.label })),
|
||||
...adminCustomUrls.map((url) => ({ value: url, label: url })),
|
||||
];
|
||||
|
||||
// Apply admin default TSA URL on first config load (TASK-2)
|
||||
useEffect(() => {
|
||||
if (!defaultApplied.current && config?.timestampDefaultTsaUrl) {
|
||||
defaultApplied.current = true;
|
||||
const adminDefault = config.timestampDefaultTsaUrl;
|
||||
if (adminDefault && adminDefault !== parameters.tsaUrl) {
|
||||
onParameterChange("tsaUrl", adminDefault);
|
||||
}
|
||||
}
|
||||
}, [config?.timestampDefaultTsaUrl]);
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<Text size="sm" fw={500}>
|
||||
{t("timestampPdf.options.title", "Timestamp Server (TSA)")}
|
||||
</Text>
|
||||
|
||||
<Select
|
||||
label={t("timestampPdf.options.tsaUrl.label", "Select a TSA server")}
|
||||
description={t(
|
||||
"timestampPdf.options.tsaUrl.desc",
|
||||
"Pick a trusted Time Stamp Authority"
|
||||
)}
|
||||
data={selectData}
|
||||
value={parameters.tsaUrl}
|
||||
onChange={(value) => onParameterChange("tsaUrl", value ?? presets[0].url)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Text size="xs" c="dimmed">
|
||||
{t(
|
||||
"timestampPdf.options.note",
|
||||
"Only a SHA-256 hash of your document is sent to the TSA server; the PDF file itself is never sent to the TSA server."
|
||||
)}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimestampPdfSettings;
|
||||
@@ -43,6 +43,7 @@ import FormFill from "@app/tools/formFill/FormFill";
|
||||
import RemoveCertificateSign from "@app/tools/RemoveCertificateSign";
|
||||
import RemoveImage from "@app/tools/RemoveImage";
|
||||
import CertSign from "@app/tools/CertSign";
|
||||
import TimestampPdf from "@app/tools/TimestampPdf";
|
||||
import BookletImposition from "@app/tools/BookletImposition";
|
||||
import Flatten from "@app/tools/Flatten";
|
||||
import Rotate from "@app/tools/Rotate";
|
||||
@@ -69,6 +70,7 @@ import { convertOperationConfig } from "@app/hooks/tools/convert/useConvertOpera
|
||||
import { removeCertificateSignOperationConfig } from "@app/hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation";
|
||||
import { changePermissionsOperationConfig } from "@app/hooks/tools/changePermissions/useChangePermissionsOperation";
|
||||
import { certSignOperationConfig } from "@app/hooks/tools/certSign/useCertSignOperation";
|
||||
import { timestampPdfOperationConfig } from "@app/hooks/tools/timestampPdf/useTimestampPdfOperation";
|
||||
import { bookletImpositionOperationConfig } from "@app/hooks/tools/bookletImposition/useBookletImpositionOperation";
|
||||
import { mergeOperationConfig } from '@app/hooks/tools/merge/useMergeOperation';
|
||||
import { editTableOfContentsOperationConfig } from '@app/hooks/tools/editTableOfContents/useEditTableOfContentsOperation';
|
||||
@@ -213,6 +215,19 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
|
||||
operationConfig: certSignOperationConfig,
|
||||
automationSettings: CertSignAutomationSettings,
|
||||
},
|
||||
timestampPdf: {
|
||||
icon: <LocalIcon icon="schedule-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.timestampPdf.title", "Timestamp PDF"),
|
||||
component: TimestampPdf,
|
||||
description: t("home.timestampPdf.desc", "Add an RFC 3161 document timestamp to prove when your PDF existed"),
|
||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||
subcategoryId: SubcategoryId.SIGNING,
|
||||
maxFiles: -1,
|
||||
endpoints: ["timestamp-pdf"],
|
||||
operationConfig: timestampPdfOperationConfig,
|
||||
automationSettings: null,
|
||||
synonyms: getSynonyms(t, "timestampPdf"),
|
||||
},
|
||||
sign: {
|
||||
icon: <LocalIcon icon="signature-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.sign.title", "Sign"),
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ToolType, useToolOperation } from '@app/hooks/tools/shared/useToolOperation';
|
||||
import { createStandardErrorHandler } from '@app/utils/toolErrorHandler';
|
||||
import { TimestampPdfParameters, defaultParameters } from '@app/hooks/tools/timestampPdf/useTimestampPdfParameters';
|
||||
|
||||
export const buildTimestampPdfFormData = (parameters: TimestampPdfParameters, file: File): FormData => {
|
||||
const formData = new FormData();
|
||||
formData.append('fileInput', file);
|
||||
|
||||
formData.append('tsaUrl', parameters.tsaUrl);
|
||||
|
||||
return formData;
|
||||
};
|
||||
|
||||
export const timestampPdfOperationConfig = {
|
||||
toolType: ToolType.singleFile,
|
||||
buildFormData: buildTimestampPdfFormData,
|
||||
operationType: 'timestampPdf',
|
||||
endpoint: '/api/v1/security/timestamp-pdf',
|
||||
multiFileEndpoint: false,
|
||||
defaultParameters,
|
||||
} as const;
|
||||
|
||||
export const useTimestampPdfOperation = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useToolOperation<TimestampPdfParameters>({
|
||||
...timestampPdfOperationConfig,
|
||||
getErrorMessage: createStandardErrorHandler(
|
||||
t('timestampPdf.error.failed', 'An error occurred while timestamping the PDF.')
|
||||
),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { BaseParameters } from '@app/types/parameters';
|
||||
import { useBaseParameters, BaseParametersHook } from '@app/hooks/tools/shared/useBaseParameters';
|
||||
|
||||
// Fallback presets used only when AppConfig hasn't loaded yet.
|
||||
// The real presets are served by the backend via /api/v1/config/app-config
|
||||
// (field: timestampTsaPresets) — single source of truth.
|
||||
export const FALLBACK_TSA_PRESETS = [
|
||||
{ label: 'DigiCert', url: 'http://timestamp.digicert.com' },
|
||||
{ label: 'Sectigo', url: 'http://timestamp.sectigo.com' },
|
||||
{ label: 'SSL.com', url: 'http://ts.ssl.com' },
|
||||
{ label: 'FreeTSA', url: 'https://freetsa.org/tsr' },
|
||||
{ label: 'MeSign', url: 'http://tsa.mesign.com' },
|
||||
] as const;
|
||||
|
||||
export interface TimestampPdfParameters extends BaseParameters {
|
||||
tsaUrl: string;
|
||||
}
|
||||
|
||||
export const defaultParameters: TimestampPdfParameters = {
|
||||
tsaUrl: FALLBACK_TSA_PRESETS[0].url,
|
||||
};
|
||||
|
||||
export type TimestampPdfParametersHook = BaseParametersHook<TimestampPdfParameters>;
|
||||
|
||||
export const useTimestampPdfParameters = (): TimestampPdfParametersHook => {
|
||||
return useBaseParameters({
|
||||
defaultParameters,
|
||||
endpointName: 'timestamp-pdf',
|
||||
validateFn: (params) => {
|
||||
return params.tsaUrl.trim().length > 0;
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
|
||||
import TimestampPdfSettings from "@app/components/tools/timestampPdf/TimestampPdfSettings";
|
||||
import { useTimestampPdfParameters } from "@app/hooks/tools/timestampPdf/useTimestampPdfParameters";
|
||||
import { useTimestampPdfOperation } from "@app/hooks/tools/timestampPdf/useTimestampPdfOperation";
|
||||
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
|
||||
import { BaseToolProps, ToolComponent } from "@app/types/tool";
|
||||
|
||||
const TimestampPdf = (props: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const base = useBaseTool(
|
||||
"timestampPdf",
|
||||
useTimestampPdfParameters,
|
||||
useTimestampPdfOperation,
|
||||
props
|
||||
);
|
||||
|
||||
return createToolFlow({
|
||||
files: {
|
||||
selectedFiles: base.selectedFiles,
|
||||
isCollapsed: base.hasResults,
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
title: t("timestampPdf.steps.settings", "Settings"),
|
||||
isCollapsed: base.settingsCollapsed,
|
||||
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
|
||||
content: (
|
||||
<TimestampPdfSettings
|
||||
parameters={base.params.parameters}
|
||||
onParameterChange={base.params.updateParameter}
|
||||
disabled={base.endpointLoading}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
executeButton: {
|
||||
text: t("timestampPdf.submit", "Apply Timestamp"),
|
||||
isVisible: !base.hasResults,
|
||||
loadingText: t("loading"),
|
||||
onClick: base.handleExecute,
|
||||
disabled:
|
||||
!base.params.validateParameters() ||
|
||||
!base.hasFiles ||
|
||||
!base.endpointEnabled,
|
||||
},
|
||||
review: {
|
||||
isVisible: base.hasResults,
|
||||
operation: base.operation,
|
||||
title: t("timestampPdf.results", "Timestamp Results"),
|
||||
onFileClick: base.handleThumbnailClick,
|
||||
onUndo: base.handleUndo,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
TimestampPdf.tool = () => useTimestampPdfOperation;
|
||||
|
||||
export default TimestampPdf as ToolComponent;
|
||||
@@ -50,6 +50,9 @@ export interface AppConfig {
|
||||
googleDriveClientId?: string;
|
||||
googleDriveApiKey?: string;
|
||||
googleDriveAppId?: string;
|
||||
timestampDefaultTsaUrl?: string;
|
||||
timestampCustomTsaUrls?: string[];
|
||||
timestampTsaPresets?: { label: string; url: string }[];
|
||||
}
|
||||
|
||||
export type AppConfigBootstrapMode = 'blocking' | 'non-blocking';
|
||||
|
||||
@@ -52,6 +52,7 @@ export const CORE_REGULAR_TOOL_IDS = [
|
||||
'overlayPdfs',
|
||||
'getPdfInfo',
|
||||
'validateSignature',
|
||||
'timestampPdf',
|
||||
'replaceColor',
|
||||
'showJS',
|
||||
'bookletImposition',
|
||||
|
||||
Reference in New Issue
Block a user