diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 637ab59e1..5a1563ea5 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -426,6 +426,10 @@ "title": "Flatten", "desc": "Remove all interactive elements and forms from a PDF" }, + "manageSignatures": { + "title": "Sign with Certificate", + "desc": "Add digital signatures to PDF documents using certificates" + }, "repair": { "title": "Repair", "desc": "Tries to repair a corrupt/broken PDF" @@ -2384,5 +2388,145 @@ "processImages": "Process Images", "processImagesDesc": "Converts multiple image files into a single PDF document, then applies OCR technology to extract searchable text from the images." } + }, + "manageSignatures": { + "title": "Sign with Certificate", + "filenamePrefix": "signed", + "files": { + "placeholder": "Select PDF files to sign with certificates" + }, + "fileStatus": { + "stepTitle": "File Status" + }, + "fileNavigation": "File {{current}} of {{total}}", + "hasSignatures": "Contains {{count}} signature(s)", + "noSignatures": "No signatures detected", + "signed": "Signed", + "certType": { + "stepTitle": "Certificate Type", + "tooltip": { + "header": { + "title": "About Certificate Types" + }, + "what": { + "title": "What's a certificate?", + "text": "It's a secure ID for your signature that proves you signed. Unless you're required to sign via certificate, we recommend using another secure method like Type, Draw, or Upload." + }, + "which": { + "title": "Which option should I use?", + "text": "Choose the format that matches your certificate file:", + "bullet1": "PKCS#12 (.p12 / .pfx) – one combined file (most common)", + "bullet2": "PEM – separate private-key and certificate .pem files", + "bullet3": "JKS – Java .jks keystore for dev / CI-CD workflows" + }, + "convert": { + "title": "Key not listed?", + "text": "Convert your file to a Java keystore (.jks) with keytool, then pick JKS." + } + } + }, + "certFiles": { + "stepTitle": "Certificate Files" + }, + "appearance": { + "stepTitle": "Signature Appearance", + "title": "Signature Appearance", + "invisible": "Invisible", + "visible": "Visible", + "options": { + "title": "Signature Details" + }, + "tooltip": { + "header": { + "title": "About Signature Appearance" + }, + "invisible": { + "title": "Invisible Signatures", + "text": "The signature is added to the PDF for security but won't be visible when viewing the document. Perfect for legal requirements without changing the document's appearance.", + "bullet1": "Provides security without visual changes", + "bullet2": "Meets legal requirements for digital signing", + "bullet3": "Doesn't affect document layout or design" + }, + "visible": { + "title": "Visible Signatures", + "text": "Shows a signature block on the PDF with your name, date, and optional details. Useful when you want readers to clearly see the document is signed.", + "bullet1": "Shows signer name and date on the document", + "bullet2": "Can include reason and location for signing", + "bullet3": "Choose which page to place the signature", + "bullet4": "Optional logo can be included" + } + } + }, + "mode": { + "title": "Action", + "validate": "Check for Signatures", + "viewEdit": "View/Edit Signatures", + "sign": "Add New Signature" + }, + "validation": { + "title": "Validation Options", + "customCert": "Custom Certificate (Optional)", + "customCert.desc": "Upload a custom certificate for validation" + }, + "signing": { + "title": "Certificate Settings", + "certType": "Certificate Type", + "choosePrivateKey": "Choose Private Key File", + "chooseCertificate": "Choose Certificate File", + "chooseP12File": "Choose PKCS12 File", + "chooseJksFile": "Choose JKS File", + "password": "Certificate Password", + "showSignature": "Show visible signature on PDF", + "reason": "Reason for Signing", + "location": "Location", + "name": "Signer Name", + "pageNumber": "Page Number", + "logoTitle": "Logo", + "noLogo": "No Logo", + "showLogo": "Show Logo" + }, + "validate": { + "submit": "Validate Signatures", + "results": "Signature Validation Results" + }, + "sign": { + "submit": "Sign PDF", + "results": "Signed PDF" + }, + "results": { + "title": "Signature Results" + }, + "error": { + "failed": "An error occurred whilst processing signatures." + }, + "tooltip": { + "header": { + "title": "About Managing Signatures" + }, + "overview": { + "title": "What can this tool do?", + "text": "This tool lets you check if your PDFs are digitally signed and add new digital signatures. Digital signatures prove who created or approved a document and show if it has been changed since signing.", + "bullet1": "Check existing signatures and their validity", + "bullet2": "View detailed information about signers and certificates", + "bullet3": "Add new digital signatures to secure your documents", + "bullet4": "Multiple files supported with easy navigation" + }, + "validation": { + "title": "Checking Signatures", + "text": "When you check signatures, the tool tells you if they're valid, who signed the document, when it was signed, and whether the document has been changed since signing.", + "bullet1": "Shows if signatures are valid or invalid", + "bullet2": "Displays signer information and signing date", + "bullet3": "Checks if the document was modified after signing", + "bullet4": "Can use custom certificates for verification" + }, + "signing": { + "title": "Adding Signatures", + "text": "To sign a PDF, you need a digital certificate (like PEM, PKCS12, or JKS). You can choose to make the signature visible on the document or keep it invisible for security only.", + "bullet1": "Supports PEM, PKCS12, and JKS certificate formats", + "bullet2": "Option to show or hide signature on the PDF", + "bullet3": "Add reason, location, and signer name", + "bullet4": "Choose which page to place visible signatures" + } + } } } diff --git a/frontend/src/components/tools/manageSignatures/CertificateFilesSettings.tsx b/frontend/src/components/tools/manageSignatures/CertificateFilesSettings.tsx new file mode 100644 index 000000000..b1dd90536 --- /dev/null +++ b/frontend/src/components/tools/manageSignatures/CertificateFilesSettings.tsx @@ -0,0 +1,77 @@ +import { Stack, Text, TextInput } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { ManageSignaturesParameters } from "../../../hooks/tools/manageSignatures/useManageSignaturesParameters"; +import FileUploadButton from "../../shared/FileUploadButton"; + +interface CertificateFilesSettingsProps { + parameters: ManageSignaturesParameters; + onParameterChange: (key: keyof ManageSignaturesParameters, value: any) => void; + disabled?: boolean; +} + +const CertificateFilesSettings = ({ parameters, onParameterChange, disabled = false }: CertificateFilesSettingsProps) => { + const { t } = useTranslation(); + + return ( + + {/* Certificate Files based on type */} + {parameters.certType === 'PEM' && ( + + onParameterChange('privateKeyFile', file || undefined)} + accept=".pem,.der" + disabled={disabled} + placeholder={t('manageSignatures.signing.choosePrivateKey', 'Choose Private Key File')} + /> + {parameters.privateKeyFile && ( + onParameterChange('certFile', file || undefined)} + accept=".pem,.der" + disabled={disabled} + placeholder={t('manageSignatures.signing.chooseCertificate', 'Choose Certificate File')} + /> + )} + + )} + + {parameters.certType === 'PKCS12' && ( + onParameterChange('p12File', file || undefined)} + accept=".p12,.pfx" + disabled={disabled} + placeholder={t('manageSignatures.signing.chooseP12File', 'Choose PKCS12 File')} + /> + )} + + {parameters.certType === 'JKS' && ( + onParameterChange('jksFile', file || undefined)} + accept=".jks,.keystore" + disabled={disabled} + placeholder={t('manageSignatures.signing.chooseJksFile', 'Choose JKS File')} + /> + )} + + {/* Password - only show when files are uploaded */} + {parameters.certType && ( + (parameters.certType === 'PEM' && parameters.privateKeyFile && parameters.certFile) || + (parameters.certType === 'PKCS12' && parameters.p12File) || + (parameters.certType === 'JKS' && parameters.jksFile) + ) && ( + onParameterChange('password', event.currentTarget.value)} + disabled={disabled} + /> + )} + + ); +}; + +export default CertificateFilesSettings; \ No newline at end of file diff --git a/frontend/src/components/tools/manageSignatures/CertificateTypeSettings.tsx b/frontend/src/components/tools/manageSignatures/CertificateTypeSettings.tsx new file mode 100644 index 000000000..9c8bb3645 --- /dev/null +++ b/frontend/src/components/tools/manageSignatures/CertificateTypeSettings.tsx @@ -0,0 +1,60 @@ +import { Stack, Button } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { ManageSignaturesParameters } from "../../../hooks/tools/manageSignatures/useManageSignaturesParameters"; + +interface CertificateTypeSettingsProps { + parameters: ManageSignaturesParameters; + onParameterChange: (key: keyof ManageSignaturesParameters, value: any) => void; + disabled?: boolean; +} + +const CertificateTypeSettings = ({ parameters, onParameterChange, disabled = false }: CertificateTypeSettingsProps) => { + const { t } = useTranslation(); + + return ( + + {/* Certificate Type Selection */} + + + + onParameterChange('certType', 'PKCS12')} + disabled={disabled} + style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }} + > + + PKCS#12 + + + onParameterChange('certType', 'PEM')} + disabled={disabled} + style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }} + > + + PEM + + + + onParameterChange('certType', 'JKS')} + disabled={disabled} + style={{ width: '100%', height: 'auto', minHeight: '40px', fontSize: '11px' }} + > + + JKS + + + + + + ); +}; + +export default CertificateTypeSettings; \ No newline at end of file diff --git a/frontend/src/components/tools/manageSignatures/SignatureAppearanceSettings.tsx b/frontend/src/components/tools/manageSignatures/SignatureAppearanceSettings.tsx new file mode 100644 index 000000000..501179608 --- /dev/null +++ b/frontend/src/components/tools/manageSignatures/SignatureAppearanceSettings.tsx @@ -0,0 +1,113 @@ +import { Stack, Text, Button, TextInput, NumberInput } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { ManageSignaturesParameters } from "../../../hooks/tools/manageSignatures/useManageSignaturesParameters"; + +interface SignatureAppearanceSettingsProps { + parameters: ManageSignaturesParameters; + onParameterChange: (key: keyof ManageSignaturesParameters, value: any) => void; + disabled?: boolean; +} + +const SignatureAppearanceSettings = ({ parameters, onParameterChange, disabled = false }: SignatureAppearanceSettingsProps) => { + const { t } = useTranslation(); + + return ( + + {/* Signature Visibility */} + + + {t('manageSignatures.appearance.title', 'Signature Appearance')} + + + onParameterChange('showSignature', false)} + disabled={disabled} + style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }} + > + + {t('manageSignatures.appearance.invisible', 'Invisible')} + + + onParameterChange('showSignature', true)} + disabled={disabled} + style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }} + > + + {t('manageSignatures.appearance.visible', 'Visible')} + + + + + + {/* Visible Signature Options */} + {parameters.showSignature && ( + + + {t('manageSignatures.appearance.options.title', 'Signature Details')} + + onParameterChange('reason', event.currentTarget.value)} + disabled={disabled} + /> + onParameterChange('location', event.currentTarget.value)} + disabled={disabled} + /> + onParameterChange('name', event.currentTarget.value)} + disabled={disabled} + /> + onParameterChange('pageNumber', value || 1)} + min={1} + disabled={disabled} + /> + + + {t('manageSignatures.signing.logoTitle', 'Logo')} + + + onParameterChange('showLogo', false)} + disabled={disabled} + style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }} + > + + {t('manageSignatures.signing.noLogo', 'No Logo')} + + + onParameterChange('showLogo', true)} + disabled={disabled} + style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }} + > + + {t('manageSignatures.signing.showLogo', 'Show Logo')} + + + + + + )} + + ); +}; + +export default SignatureAppearanceSettings; \ No newline at end of file diff --git a/frontend/src/components/tooltips/useCertificateTypeTips.ts b/frontend/src/components/tooltips/useCertificateTypeTips.ts new file mode 100644 index 000000000..c3bc19788 --- /dev/null +++ b/frontend/src/components/tooltips/useCertificateTypeTips.ts @@ -0,0 +1,31 @@ +import { useTranslation } from 'react-i18next'; +import { TooltipContent } from '../../types/tips'; + +export const useCertificateTypeTips = (): TooltipContent => { + const { t } = useTranslation(); + + return { + header: { + title: t("manageSignatures.certType.tooltip.header.title", "About Certificate Types") + }, + tips: [ + { + title: t("manageSignatures.certType.tooltip.what.title", "What's a certificate?"), + description: t("manageSignatures.certType.tooltip.what.text", "It's a secure ID for your signature that proves you signed. Unless you're required to sign via certificate, we recommend using another secure method like Type, Draw, or Upload.") + }, + { + title: t("manageSignatures.certType.tooltip.which.title", "Which option should I use?"), + description: t("manageSignatures.certType.tooltip.which.text", "Choose the format that matches your certificate file:"), + bullets: [ + t("manageSignatures.certType.tooltip.which.bullet1", "PKCS#12 (.p12 / .pfx) – one combined file (most common)"), + t("manageSignatures.certType.tooltip.which.bullet2", "PEM – separate private-key and certificate .pem files"), + t("manageSignatures.certType.tooltip.which.bullet3", "JKS – Java .jks keystore for dev / CI-CD workflows") + ] + }, + { + title: t("manageSignatures.certType.tooltip.convert.title", "Key not listed?"), + description: t("manageSignatures.certType.tooltip.convert.text", "Convert your file to a Java keystore (.jks) with keytool, then pick JKS.") + } + ] + }; +}; \ No newline at end of file diff --git a/frontend/src/components/tooltips/useManageSignaturesTooltips.ts b/frontend/src/components/tooltips/useManageSignaturesTooltips.ts new file mode 100644 index 000000000..6e0a3e706 --- /dev/null +++ b/frontend/src/components/tooltips/useManageSignaturesTooltips.ts @@ -0,0 +1,44 @@ +import { useTranslation } from 'react-i18next'; +import { TooltipContent } from '../../types/tips'; + +export const useManageSignaturesTooltips = (): TooltipContent => { + const { t } = useTranslation(); + + return { + header: { + title: t("manageSignatures.tooltip.header.title", "About Managing Signatures") + }, + tips: [ + { + title: t("manageSignatures.tooltip.overview.title", "What can this tool do?"), + description: t("manageSignatures.tooltip.overview.text", "This tool lets you check if your PDFs are digitally signed and add new digital signatures. Digital signatures prove who created or approved a document and show if it has been changed since signing."), + bullets: [ + t("manageSignatures.tooltip.overview.bullet1", "Check existing signatures and their validity"), + t("manageSignatures.tooltip.overview.bullet2", "View detailed information about signers and certificates"), + t("manageSignatures.tooltip.overview.bullet3", "Add new digital signatures to secure your documents"), + t("manageSignatures.tooltip.overview.bullet4", "Multiple files supported with easy navigation") + ] + }, + { + title: t("manageSignatures.tooltip.validation.title", "Checking Signatures"), + description: t("manageSignatures.tooltip.validation.text", "When you check signatures, the tool tells you if they're valid, who signed the document, when it was signed, and whether the document has been changed since signing."), + bullets: [ + t("manageSignatures.tooltip.validation.bullet1", "Shows if signatures are valid or invalid"), + t("manageSignatures.tooltip.validation.bullet2", "Displays signer information and signing date"), + t("manageSignatures.tooltip.validation.bullet3", "Checks if the document was modified after signing"), + t("manageSignatures.tooltip.validation.bullet4", "Can use custom certificates for verification") + ] + }, + { + title: t("manageSignatures.tooltip.signing.title", "Adding Signatures"), + description: t("manageSignatures.tooltip.signing.text", "To sign a PDF, you need a digital certificate (like PEM, PKCS12, or JKS). You can choose to make the signature visible on the document or keep it invisible for security only."), + bullets: [ + t("manageSignatures.tooltip.signing.bullet1", "Supports PEM, PKCS12, and JKS certificate formats"), + t("manageSignatures.tooltip.signing.bullet2", "Option to show or hide signature on the PDF"), + t("manageSignatures.tooltip.signing.bullet3", "Add reason, location, and signer name"), + t("manageSignatures.tooltip.signing.bullet4", "Choose which page to place visible signatures") + ] + } + ] + }; +}; \ No newline at end of file diff --git a/frontend/src/components/tooltips/useSignatureAppearanceTips.ts b/frontend/src/components/tooltips/useSignatureAppearanceTips.ts new file mode 100644 index 000000000..20580dabb --- /dev/null +++ b/frontend/src/components/tooltips/useSignatureAppearanceTips.ts @@ -0,0 +1,33 @@ +import { useTranslation } from 'react-i18next'; +import { TooltipContent } from '../../types/tips'; + +export const useSignatureAppearanceTips = (): TooltipContent => { + const { t } = useTranslation(); + + return { + header: { + title: t("manageSignatures.appearance.tooltip.header.title", "About Signature Appearance") + }, + tips: [ + { + title: t("manageSignatures.appearance.tooltip.invisible.title", "Invisible Signatures"), + description: t("manageSignatures.appearance.tooltip.invisible.text", "The signature is added to the PDF for security but won't be visible when viewing the document. Perfect for legal requirements without changing the document's appearance."), + bullets: [ + t("manageSignatures.appearance.tooltip.invisible.bullet1", "Provides security without visual changes"), + t("manageSignatures.appearance.tooltip.invisible.bullet2", "Meets legal requirements for digital signing"), + t("manageSignatures.appearance.tooltip.invisible.bullet3", "Doesn't affect document layout or design") + ] + }, + { + title: t("manageSignatures.appearance.tooltip.visible.title", "Visible Signatures"), + description: t("manageSignatures.appearance.tooltip.visible.text", "Shows a signature block on the PDF with your name, date, and optional details. Useful when you want readers to clearly see the document is signed."), + bullets: [ + t("manageSignatures.appearance.tooltip.visible.bullet1", "Shows signer name and date on the document"), + t("manageSignatures.appearance.tooltip.visible.bullet2", "Can include reason and location for signing"), + t("manageSignatures.appearance.tooltip.visible.bullet3", "Choose which page to place the signature"), + t("manageSignatures.appearance.tooltip.visible.bullet4", "Optional logo can be included") + ] + } + ] + }; +}; \ No newline at end of file diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx index 68883fe92..0ed955c00 100644 --- a/frontend/src/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/data/useTranslatedToolRegistry.tsx @@ -15,6 +15,7 @@ import Repair from "../tools/Repair"; import SingleLargePage from "../tools/SingleLargePage"; import UnlockPdfForms from "../tools/UnlockPdfForms"; import RemoveCertificateSign from "../tools/RemoveCertificateSign"; +import ManageSignatures from "../tools/ManageSignatures"; import { compressOperationConfig } from "../hooks/tools/compress/useCompressOperation"; import { splitOperationConfig } from "../hooks/tools/split/useSplitOperation"; import { addPasswordOperationConfig } from "../hooks/tools/addPassword/useAddPasswordOperation"; @@ -28,6 +29,7 @@ import { ocrOperationConfig } from "../hooks/tools/ocr/useOCROperation"; import { convertOperationConfig } from "../hooks/tools/convert/useConvertOperation"; import { removeCertificateSignOperationConfig } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation"; import { changePermissionsOperationConfig } from "../hooks/tools/changePermissions/useChangePermissionsOperation"; +import { manageSignaturesOperationConfig } from "../hooks/tools/manageSignatures/useManageSignaturesOperation"; import CompressSettings from "../components/tools/compress/CompressSettings"; import SplitSettings from "../components/tools/split/SplitSettings"; import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings"; @@ -39,6 +41,7 @@ import AddWatermarkSingleStepSettings from "../components/tools/addWatermark/Add import OCRSettings from "../components/tools/ocr/OCRSettings"; import ConvertSettings from "../components/tools/convert/ConvertSettings"; import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings"; +import CertificateTypeSettings from "../components/tools/manageSignatures/CertificateTypeSettings"; import { ToolId } from "../types/toolId"; const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI @@ -132,13 +135,17 @@ export function useFlatToolRegistry(): ToolRegistry { const allTools: ToolRegistry = { // Signing - certSign: { + manageSignatures: { icon: , - name: t("home.certSign.title", "Sign with Certificate"), - component: null, - description: t("home.certSign.desc", "Signs a PDF with a Certificate/Key (PEM/P12)"), + name: t("home.manageSignatures.title", "Sign with Certificate"), + component: ManageSignatures, + description: t("home.manageSignatures.desc", "Add digital signatures to PDF documents using certificates"), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.SIGNING, + maxFiles: -1, + endpoints: ["cert-sign"], + operationConfig: manageSignaturesOperationConfig, + settingsComponent: CertificateTypeSettings, }, sign: { icon: , @@ -248,14 +255,6 @@ export function useFlatToolRegistry(): ToolRegistry { categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.VERIFICATION, }, - "validate-pdf-signature": { - icon: , - name: t("home.validateSignature.title", "Validate PDF Signature"), - component: null, - description: t("home.validateSignature.desc", "Verify digital signatures and certificates in PDF documents"), - categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.VERIFICATION, - }, // Document Review diff --git a/frontend/src/hooks/tools/manageSignatures/useManageSignaturesOperation.ts b/frontend/src/hooks/tools/manageSignatures/useManageSignaturesOperation.ts new file mode 100644 index 000000000..e2df7c281 --- /dev/null +++ b/frontend/src/hooks/tools/manageSignatures/useManageSignaturesOperation.ts @@ -0,0 +1,67 @@ +import { useTranslation } from 'react-i18next'; +import { ToolType, useToolOperation } from '../shared/useToolOperation'; +import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; +import { ManageSignaturesParameters, defaultParameters } from './useManageSignaturesParameters'; + +// Build form data for signing +export const buildManageSignaturesFormData = (parameters: ManageSignaturesParameters, file: File): FormData => { + const formData = new FormData(); + formData.append('fileInput', file); + formData.append('certType', parameters.certType); + formData.append('password', parameters.password); + + // Add certificate files based on type + switch (parameters.certType) { + case 'PEM': + if (parameters.privateKeyFile) { + formData.append('privateKeyFile', parameters.privateKeyFile); + } + if (parameters.certFile) { + formData.append('certFile', parameters.certFile); + } + break; + case 'PKCS12': + if (parameters.p12File) { + formData.append('p12File', parameters.p12File); + } + break; + case 'JKS': + if (parameters.jksFile) { + formData.append('jksFile', parameters.jksFile); + } + break; + } + + // Add signature appearance options if enabled + if (parameters.showSignature) { + formData.append('showSignature', 'true'); + formData.append('reason', parameters.reason); + formData.append('location', parameters.location); + formData.append('name', parameters.name); + formData.append('pageNumber', parameters.pageNumber.toString()); + formData.append('showLogo', parameters.showLogo.toString()); + } + + return formData; +}; + +// Static configuration object +export const manageSignaturesOperationConfig = { + toolType: ToolType.singleFile, + buildFormData: buildManageSignaturesFormData, + operationType: 'manageSignatures', + endpoint: '/api/v1/security/cert-sign', + filePrefix: 'signed_', // Will be overridden in hook with translation + multiFileEndpoint: false, + defaultParameters, +} as const; + +export const useManageSignaturesOperation = () => { + const { t } = useTranslation(); + + return useToolOperation({ + ...manageSignaturesOperationConfig, + filePrefix: t('manageSignatures.filenamePrefix', 'signed') + '_', + getErrorMessage: createStandardErrorHandler(t('manageSignatures.error.failed', 'An error occurred while processing signatures.')) + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/tools/manageSignatures/useManageSignaturesParameters.ts b/frontend/src/hooks/tools/manageSignatures/useManageSignaturesParameters.ts new file mode 100644 index 000000000..6d3a9dc9e --- /dev/null +++ b/frontend/src/hooks/tools/manageSignatures/useManageSignaturesParameters.ts @@ -0,0 +1,58 @@ +import { BaseParameters } from '../../../types/parameters'; +import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters'; + +export interface ManageSignaturesParameters extends BaseParameters { + // Certificate signing options + certType: '' | 'PEM' | 'PKCS12' | 'JKS'; + privateKeyFile?: File; + certFile?: File; + p12File?: File; + jksFile?: File; + password: string; + + // Signature appearance options + showSignature: boolean; + reason: string; + location: string; + name: string; + pageNumber: number; + showLogo: boolean; +} + +export const defaultParameters: ManageSignaturesParameters = { + certType: '', + password: '', + showSignature: false, + reason: '', + location: '', + name: '', + pageNumber: 1, + showLogo: true, +}; + +export type ManageSignaturesParametersHook = BaseParametersHook; + +export const useManageSignaturesParameters = (): ManageSignaturesParametersHook => { + return useBaseParameters({ + defaultParameters, + endpointName: 'manage-signatures', + validateFn: (params) => { + // Requires certificate type and password + if (!params.certType || !params.password) { + return false; + } + + // Check for required files based on cert type + switch (params.certType) { + case 'PEM': + return !!(params.privateKeyFile && params.certFile); + case 'PKCS12': + return !!params.p12File; + case 'JKS': + return !!params.jksFile; + default: + return false; + } + }, + }); +}; \ No newline at end of file diff --git a/frontend/src/services/signatureDetectionService.ts b/frontend/src/services/signatureDetectionService.ts new file mode 100644 index 000000000..44a12be92 --- /dev/null +++ b/frontend/src/services/signatureDetectionService.ts @@ -0,0 +1,140 @@ +/** + * Service for detecting signatures in PDF files using PDF.js + * This provides a quick client-side check to determine if a PDF contains signatures + * without needing to make API calls + */ + +// PDF.js types (simplified) +declare global { + interface Window { + pdfjsLib?: any; + } +} + +export interface SignatureDetectionResult { + hasSignatures: boolean; + signatureCount?: number; + error?: string; +} + +export interface FileSignatureStatus { + file: File; + result: SignatureDetectionResult; +} + +/** + * Detect signatures in a single PDF file using PDF.js + */ +const detectSignaturesInFile = async (file: File): Promise => { + try { + // Ensure PDF.js is available + if (!window.pdfjsLib) { + return { + hasSignatures: false, + error: 'PDF.js not available' + }; + } + + // Convert file to ArrayBuffer + const arrayBuffer = await file.arrayBuffer(); + + // Load the PDF document + const pdf = await window.pdfjsLib.getDocument({ data: arrayBuffer }).promise; + + let totalSignatures = 0; + + // Check each page for signature annotations + for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) { + const page = await pdf.getPage(pageNum); + const annotations = await page.getAnnotations(); + + // Count signature annotations (Type: /Sig) + const signatureAnnotations = annotations.filter((annotation: any) => + annotation.subtype === 'Widget' && + annotation.fieldType === 'Sig' + ); + + totalSignatures += signatureAnnotations.length; + } + + // Also check for document-level signatures in AcroForm + const metadata = await pdf.getMetadata(); + if (metadata?.info?.Signature || metadata?.metadata?.has('dc:signature')) { + totalSignatures = Math.max(totalSignatures, 1); + } + + // Clean up PDF.js document + pdf.destroy(); + + return { + hasSignatures: totalSignatures > 0, + signatureCount: totalSignatures + }; + + } catch (error) { + console.warn('PDF signature detection failed:', error); + return { + hasSignatures: false, + signatureCount: 0, + error: error instanceof Error ? error.message : 'Detection failed' + }; + } +}; + +/** + * Detect if PDF files contain signatures using PDF.js client-side processing + */ +export const detectSignaturesInFiles = async (files: File[]): Promise => { + const results: FileSignatureStatus[] = []; + + for (const file of files) { + const result = await detectSignaturesInFile(file); + results.push({ file, result }); + } + + return results; +}; + +/** + * Hook for managing signature detection state + */ +export const useSignatureDetection = () => { + const [detectionResults, setDetectionResults] = React.useState([]); + const [isDetecting, setIsDetecting] = React.useState(false); + + const detectSignatures = async (files: File[]) => { + if (files.length === 0) { + setDetectionResults([]); + return; + } + + setIsDetecting(true); + try { + const results = await detectSignaturesInFiles(files); + setDetectionResults(results); + } finally { + setIsDetecting(false); + } + }; + + const getFileSignatureStatus = (file: File): SignatureDetectionResult | null => { + const result = detectionResults.find(r => r.file === file); + return result ? result.result : null; + }; + + const hasAnySignatures = detectionResults.some(r => r.result.hasSignatures); + const totalSignatures = detectionResults.reduce((sum, r) => sum + (r.result.signatureCount || 0), 0); + + return { + detectionResults, + isDetecting, + detectSignatures, + getFileSignatureStatus, + hasAnySignatures, + totalSignatures, + reset: () => setDetectionResults([]) + }; +}; + +// Import React for the hook +import React from 'react'; \ No newline at end of file diff --git a/frontend/src/tools/ManageSignatures.tsx b/frontend/src/tools/ManageSignatures.tsx new file mode 100644 index 000000000..d9ad57967 --- /dev/null +++ b/frontend/src/tools/ManageSignatures.tsx @@ -0,0 +1,93 @@ +import { useTranslation } from "react-i18next"; +import { createToolFlow } from "../components/tools/shared/createToolFlow"; +import CertificateTypeSettings from "../components/tools/manageSignatures/CertificateTypeSettings"; +import CertificateFilesSettings from "../components/tools/manageSignatures/CertificateFilesSettings"; +import SignatureAppearanceSettings from "../components/tools/manageSignatures/SignatureAppearanceSettings"; +import { useManageSignaturesParameters } from "../hooks/tools/manageSignatures/useManageSignaturesParameters"; +import { useManageSignaturesOperation } from "../hooks/tools/manageSignatures/useManageSignaturesOperation"; +import { useCertificateTypeTips } from "../components/tooltips/useCertificateTypeTips"; +import { useSignatureAppearanceTips } from "../components/tooltips/useSignatureAppearanceTips"; +import { useBaseTool } from "../hooks/tools/shared/useBaseTool"; +import { BaseToolProps, ToolComponent } from "../types/tool"; + +const ManageSignatures = (props: BaseToolProps) => { + const { t } = useTranslation(); + + const base = useBaseTool( + 'manageSignatures', + useManageSignaturesParameters, + useManageSignaturesOperation, + props + ); + + const certTypeTips = useCertificateTypeTips(); + const appearanceTips = useSignatureAppearanceTips(); + + return createToolFlow({ + forceStepNumbers: true, + files: { + selectedFiles: base.selectedFiles, + isCollapsed: base.hasResults, + placeholder: t("manageSignatures.files.placeholder", "Select PDF files to sign with certificates"), + }, + steps: [ + { + title: t("manageSignatures.certType.stepTitle", "Certificate Type"), + isCollapsed: base.settingsCollapsed, + onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined, + tooltip: certTypeTips, + content: ( + + ), + }, + { + title: t("manageSignatures.certFiles.stepTitle", "Certificate Files"), + isCollapsed: base.settingsCollapsed, + onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined, + content: ( + + ), + }, + { + title: t("manageSignatures.appearance.stepTitle", "Signature Appearance"), + isCollapsed: base.settingsCollapsed, + onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined, + tooltip: appearanceTips, + content: ( + + ), + }, + ], + executeButton: { + text: t("manageSignatures.sign.submit", "Sign PDF"), + 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("manageSignatures.sign.results", "Signed PDF"), + onFileClick: base.handleThumbnailClick, + onUndo: base.handleUndo, + }, + }); +}; + +// Static method to get the operation hook for automation +ManageSignatures.tool = () => useManageSignaturesOperation; + +export default ManageSignatures as ToolComponent; \ No newline at end of file diff --git a/frontend/src/types/toolId.ts b/frontend/src/types/toolId.ts index be38bdf37..93d8dec00 100644 --- a/frontend/src/types/toolId.ts +++ b/frontend/src/types/toolId.ts @@ -12,7 +12,7 @@ const TOOL_IDS = [ 'flatten', 'remove-certificate-sign', 'unlock-pdf-forms', 'compress', 'extract-page', 'reorganize-pages', 'extract-images', 'add-stamp', 'add-attachments', 'change-metadata', 'overlay-pdfs', - 'manage-certificates', 'get-all-info-on-pdf', 'validate-pdf-signature', 'read', 'automate', 'replace-and-invert-color', + 'manage-certificates', 'get-all-info-on-pdf', 'manageSignatures', 'read', 'automate', 'replace-and-invert-color', 'show-javascript', 'dev-api', 'dev-folder-scanning', 'dev-sso-guide', 'dev-airgapped' ] as const;