diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 0f33b98e4..52d73b1f2 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -811,16 +811,6 @@ "removePages": { "tags": "Remove pages,delete pages" }, - "removePassword": { - "tags": "secure,Decrypt,security,unpassword,delete password", - "title": "Remove password", - "header": "Remove password (Decrypt)", - "selectText": { - "1": "Select PDF to Decrypt", - "2": "Password" - }, - "submit": "Remove" - }, "compressPdfs": { "tags": "squish,small,tiny" }, @@ -1875,5 +1865,21 @@ "text": "To make these permissions unchangeable, use the Add Password tool to set an owner password." } } + }, + "removePassword": { + "tags": "secure,Decrypt,security,unpassword,delete password", + "title": "Remove Password", + "password": { + "label": "Current Password", + "placeholder": "Enter current password", + "completed": "Password configured" + }, + "filenamePrefix": "decrypted", + "error": { + "failed": "An error occurred while removing the password from the PDF." + }, + "results": { + "title": "Decrypted PDFs" + } } } diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index 944a1df22..29b48ad91 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -752,16 +752,6 @@ "removePages": { "tags": "Remove pages,delete pages" }, - "removePassword": { - "tags": "secure,Decrypt,security,unpassword,delete password", - "title": "Remove password", - "header": "Remove password (Decrypt)", - "selectText": { - "1": "Select PDF to Decrypt", - "2": "Password" - }, - "submit": "Remove" - }, "compressPdfs": { "tags": "squish,small,tiny" }, @@ -1737,5 +1727,21 @@ "text": "To make these permissions unchangeable, use the Add Password tool to set an owner password." } } + }, + "removePassword": { + "tags": "secure,Decrypt,security,unpassword,delete password", + "title": "Remove Password", + "password": { + "label": "Current Password", + "placeholder": "Enter current password", + "completed": "Password configured" + }, + "filenamePrefix": "decrypted", + "error": { + "failed": "An error occurred while removing the password from the PDF." + }, + "results": { + "title": "Decrypted PDFs" + } } } diff --git a/frontend/src/components/tools/removePassword/RemovePasswordSettings.tsx b/frontend/src/components/tools/removePassword/RemovePasswordSettings.tsx new file mode 100644 index 000000000..4079ec21a --- /dev/null +++ b/frontend/src/components/tools/removePassword/RemovePasswordSettings.tsx @@ -0,0 +1,30 @@ +import { Stack, Text, PasswordInput } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { RemovePasswordParameters } from "../../../hooks/tools/removePassword/useRemovePasswordParameters"; + +interface RemovePasswordSettingsProps { + parameters: RemovePasswordParameters; + onParameterChange: (key: keyof RemovePasswordParameters, value: string) => void; + disabled?: boolean; +} + +const RemovePasswordSettings = ({ parameters, onParameterChange, disabled = false }: RemovePasswordSettingsProps) => { + const { t } = useTranslation(); + + return ( + + + onParameterChange('password', e.target.value)} + disabled={disabled} + required + /> + + + ); +}; + +export default RemovePasswordSettings; diff --git a/frontend/src/hooks/tools/removePassword/useRemovePasswordOperation.ts b/frontend/src/hooks/tools/removePassword/useRemovePasswordOperation.ts new file mode 100644 index 000000000..13c26fa69 --- /dev/null +++ b/frontend/src/hooks/tools/removePassword/useRemovePasswordOperation.ts @@ -0,0 +1,24 @@ +import { useTranslation } from 'react-i18next'; +import { useToolOperation } from '../shared/useToolOperation'; +import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; +import { RemovePasswordParameters } from './useRemovePasswordParameters'; + +export const useRemovePasswordOperation = () => { + const { t } = useTranslation(); + + const buildFormData = (parameters: RemovePasswordParameters, file: File): FormData => { + const formData = new FormData(); + formData.append("fileInput", file); + formData.append("password", parameters.password); + return formData; + }; + + return useToolOperation({ + operationType: 'removePassword', + endpoint: '/api/v1/security/remove-password', + buildFormData, + filePrefix: t('removePassword.filenamePrefix', 'decrypted') + '_', + multiFileEndpoint: false, + getErrorMessage: createStandardErrorHandler(t('removePassword.error.failed', 'An error occurred while removing the password from the PDF.')) + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/tools/removePassword/useRemovePasswordParameters.ts b/frontend/src/hooks/tools/removePassword/useRemovePasswordParameters.ts new file mode 100644 index 000000000..13d2c6c07 --- /dev/null +++ b/frontend/src/hooks/tools/removePassword/useRemovePasswordParameters.ts @@ -0,0 +1,49 @@ +import { useState } from 'react'; + +export interface RemovePasswordParameters { + password: string; +} + +export interface RemovePasswordParametersHook { + parameters: RemovePasswordParameters; + updateParameter: (parameter: K, value: RemovePasswordParameters[K]) => void; + resetParameters: () => void; + validateParameters: () => boolean; + getEndpointName: () => string; +} + +export const defaultParameters: RemovePasswordParameters = { + password: '', +}; + +export const useRemovePasswordParameters = (): RemovePasswordParametersHook => { + const [parameters, setParameters] = useState(defaultParameters); + + const updateParameter = (parameter: K, value: RemovePasswordParameters[K]) => { + setParameters(prev => ({ + ...prev, + [parameter]: value, + }) + ); + }; + + const resetParameters = () => { + setParameters(defaultParameters); + }; + + const validateParameters = () => { + return parameters.password !== ''; + }; + + const getEndpointName = () => { + return 'remove-password'; + }; + + return { + parameters, + updateParameter, + resetParameters, + validateParameters, + getEndpointName, + }; +}; \ No newline at end of file diff --git a/frontend/src/hooks/useToolManagement.tsx b/frontend/src/hooks/useToolManagement.tsx index 35a93765f..20da97e44 100644 --- a/frontend/src/hooks/useToolManagement.tsx +++ b/frontend/src/hooks/useToolManagement.tsx @@ -6,8 +6,9 @@ import SwapHorizIcon from "@mui/icons-material/SwapHoriz"; import ApiIcon from "@mui/icons-material/Api"; import CleaningServicesIcon from "@mui/icons-material/CleaningServices"; import LockIcon from "@mui/icons-material/Lock"; +import LockOpenIcon from "@mui/icons-material/LockOpen"; import { useMultipleEndpointsEnabled } from "./useEndpointConfig"; -import { Tool, ToolDefinition, BaseToolProps, ToolRegistry } from "../types/tool"; +import { Tool, ToolDefinition, ToolRegistry } from "../types/tool"; // Add entry here with maxFiles, endpoints, and lazy component @@ -104,6 +105,15 @@ const toolDefinitions: Record = { description: "Change document restrictions and permissions", endpoints: ["add-password"] }, + removePassword: { + id: "removePassword", + icon: , + component: React.lazy(() => import("../tools/RemovePassword")), + maxFiles: -1, + category: "security", + description: "Remove password protection from PDF files", + endpoints: ["remove-password"] + }, }; diff --git a/frontend/src/tools/RemovePassword.tsx b/frontend/src/tools/RemovePassword.tsx new file mode 100644 index 000000000..e114fef23 --- /dev/null +++ b/frontend/src/tools/RemovePassword.tsx @@ -0,0 +1,167 @@ +import { useEffect, useMemo } from "react"; +import { Box, Button, Stack, Text } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import DownloadIcon from "@mui/icons-material/Download"; +import { useEndpointEnabled } from "../hooks/useEndpointConfig"; +import { useFileContext } from "../contexts/FileContext"; +import { useToolFileSelection } from "../contexts/FileSelectionContext"; + +import ToolStep, { ToolStepContainer } from "../components/tools/shared/ToolStep"; +import OperationButton from "../components/tools/shared/OperationButton"; +import ErrorNotification from "../components/tools/shared/ErrorNotification"; +import FileStatusIndicator from "../components/tools/shared/FileStatusIndicator"; +import ResultsPreview from "../components/tools/shared/ResultsPreview"; + +import RemovePasswordSettings from "../components/tools/removePassword/RemovePasswordSettings"; + +import { useRemovePasswordParameters } from "../hooks/tools/removePassword/useRemovePasswordParameters"; +import { useRemovePasswordOperation } from "../hooks/tools/removePassword/useRemovePasswordOperation"; +import { BaseToolProps } from "../types/tool"; + +const RemovePassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { + const { t } = useTranslation(); + const { setCurrentMode } = useFileContext(); + const { selectedFiles } = useToolFileSelection(); + + const removePasswordParams = useRemovePasswordParameters(); + const removePasswordOperation = useRemovePasswordOperation(); + + // Endpoint validation + const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(removePasswordParams.getEndpointName()); + + useEffect(() => { + removePasswordOperation.resetResults(); + onPreviewFile?.(null); + }, [removePasswordParams.parameters, selectedFiles]); + + const handleRemovePassword = async () => { + try { + await removePasswordOperation.executeOperation( + removePasswordParams.parameters, + selectedFiles + ); + if (removePasswordOperation.files && onComplete) { + onComplete(removePasswordOperation.files); + } + } catch (error) { + if (onError) { + onError(error instanceof Error ? error.message : t('removePassword.error.failed', 'Remove password operation failed')); + } + } + }; + + const handleThumbnailClick = (file: File) => { + onPreviewFile?.(file); + sessionStorage.setItem('previousMode', 'removePassword'); + setCurrentMode('viewer'); + }; + + const handleSettingsReset = () => { + removePasswordOperation.resetResults(); + onPreviewFile?.(null); + setCurrentMode('removePassword'); + }; + + const hasFiles = selectedFiles.length > 0; + const hasResults = removePasswordOperation.files.length > 0 || removePasswordOperation.downloadUrl !== null; + const filesCollapsed = hasFiles; + const passwordCollapsed = hasResults; + + const previewResults = useMemo(() => + removePasswordOperation.files?.map((file, index) => ({ + file, + thumbnail: removePasswordOperation.thumbnails[index] + })) || [], + [removePasswordOperation.files, removePasswordOperation.thumbnails] + ); + + return ( + + + {/* Files Step */} + + + + + {/* Password Step */} + + + + + + + + + {/* Results Step */} + + + {removePasswordOperation.status && ( + {removePasswordOperation.status} + )} + + + + {removePasswordOperation.downloadUrl && ( + + )} + + + + + + + ); +} + +export default RemovePassword; diff --git a/frontend/src/types/fileContext.ts b/frontend/src/types/fileContext.ts index cdde6b51c..7a8c4d2ed 100644 --- a/frontend/src/types/fileContext.ts +++ b/frontend/src/types/fileContext.ts @@ -5,18 +5,19 @@ import { ProcessedFile } from './processing'; import { PDFDocument, PDFPage, PageOperation } from './pageEditor'; -export type ModeType = - | 'viewer' - | 'pageEditor' - | 'fileEditor' - | 'merge' - | 'split' - | 'compress' - | 'ocr' - | 'convert' +export type ModeType = + | 'viewer' + | 'pageEditor' + | 'fileEditor' + | 'merge' + | 'split' + | 'compress' + | 'ocr' + | 'convert' | 'sanitize' | 'addPassword' - | 'changePermissions'; + | 'changePermissions' + | 'removePassword'; export type ViewType = 'viewer' | 'pageEditor' | 'fileEditor';