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 && (
+ }
+ color="green"
+ fullWidth
+ mb="md"
+ >
+ {t("download", "Download")}
+
+ )}
+
+
+
+
+
+
+ );
+}
+
+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';