Initial Remove Password implementation

This commit is contained in:
James Brunton 2025-08-15 13:35:29 +01:00
parent 4c17c520d7
commit 25cc9ec8d3
8 changed files with 324 additions and 31 deletions

View File

@ -811,16 +811,6 @@
"removePages": { "removePages": {
"tags": "Remove pages,delete pages" "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": { "compressPdfs": {
"tags": "squish,small,tiny" "tags": "squish,small,tiny"
}, },
@ -1875,5 +1865,21 @@
"text": "To make these permissions unchangeable, use the Add Password tool to set an owner password." "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"
}
} }
} }

View File

@ -752,16 +752,6 @@
"removePages": { "removePages": {
"tags": "Remove pages,delete pages" "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": { "compressPdfs": {
"tags": "squish,small,tiny" "tags": "squish,small,tiny"
}, },
@ -1737,5 +1727,21 @@
"text": "To make these permissions unchangeable, use the Add Password tool to set an owner password." "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"
}
} }
} }

View File

@ -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 (
<Stack gap="md">
<Stack gap="sm">
<PasswordInput
label={t('removePassword.password.label', 'Current Password')}
placeholder={t('removePassword.password.placeholder', 'Enter current password')}
value={parameters.password}
onChange={(e) => onParameterChange('password', e.target.value)}
disabled={disabled}
required
/>
</Stack>
</Stack>
);
};
export default RemovePasswordSettings;

View File

@ -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<RemovePasswordParameters>({
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.'))
});
};

View File

@ -0,0 +1,49 @@
import { useState } from 'react';
export interface RemovePasswordParameters {
password: string;
}
export interface RemovePasswordParametersHook {
parameters: RemovePasswordParameters;
updateParameter: <K extends keyof RemovePasswordParameters>(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<RemovePasswordParameters>(defaultParameters);
const updateParameter = <K extends keyof RemovePasswordParameters>(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,
};
};

View File

@ -6,8 +6,9 @@ import SwapHorizIcon from "@mui/icons-material/SwapHoriz";
import ApiIcon from "@mui/icons-material/Api"; import ApiIcon from "@mui/icons-material/Api";
import CleaningServicesIcon from "@mui/icons-material/CleaningServices"; import CleaningServicesIcon from "@mui/icons-material/CleaningServices";
import LockIcon from "@mui/icons-material/Lock"; import LockIcon from "@mui/icons-material/Lock";
import LockOpenIcon from "@mui/icons-material/LockOpen";
import { useMultipleEndpointsEnabled } from "./useEndpointConfig"; 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 // Add entry here with maxFiles, endpoints, and lazy component
@ -104,6 +105,15 @@ const toolDefinitions: Record<string, ToolDefinition> = {
description: "Change document restrictions and permissions", description: "Change document restrictions and permissions",
endpoints: ["add-password"] endpoints: ["add-password"]
}, },
removePassword: {
id: "removePassword",
icon: <LockOpenIcon />,
component: React.lazy(() => import("../tools/RemovePassword")),
maxFiles: -1,
category: "security",
description: "Remove password protection from PDF files",
endpoints: ["remove-password"]
},
}; };

View File

@ -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 (
<ToolStepContainer>
<Stack gap="sm" h="94vh" p="sm" style={{ overflow: 'auto' }}>
{/* Files Step */}
<ToolStep
title={t('files.title', 'Files')}
isVisible={true}
isCollapsed={filesCollapsed}
isCompleted={filesCollapsed}
completedMessage={hasFiles ?
selectedFiles.length === 1
? t('files.selected.single', 'Selected: {{filename}}', { filename: selectedFiles[0].name })
: t('files.selected.multiple', 'Selected: {{count}} files', { count: selectedFiles.length })
: undefined}
>
<FileStatusIndicator
selectedFiles={selectedFiles}
placeholder={t('files.placeholder', 'Select a PDF file in the main view to get started')}
/>
</ToolStep>
{/* Password Step */}
<ToolStep
title={t('removePassword.title', 'Remove Password')}
isVisible={hasFiles}
isCollapsed={passwordCollapsed}
isCompleted={passwordCollapsed}
onCollapsedClick={hasResults ? handleSettingsReset : undefined}
completedMessage={passwordCollapsed ? t('removePassword.password.completed', 'Password configured') : undefined}
>
<RemovePasswordSettings
parameters={removePasswordParams.parameters}
onParameterChange={removePasswordParams.updateParameter}
disabled={endpointLoading}
/>
</ToolStep>
<Box mt="md">
<OperationButton
onClick={handleRemovePassword}
isLoading={removePasswordOperation.isLoading}
disabled={!removePasswordParams.validateParameters() || !hasFiles || !endpointEnabled}
loadingText={t('loading')}
submitText={t('removePassword.submit', 'Remove Password')}
/>
</Box>
{/* Results Step */}
<ToolStep
title={t('results.title', 'Results')}
isVisible={hasResults}
>
<Stack gap="sm">
{removePasswordOperation.status && (
<Text size="sm" c="dimmed">{removePasswordOperation.status}</Text>
)}
<ErrorNotification
error={removePasswordOperation.errorMessage}
onClose={removePasswordOperation.clearError}
/>
{removePasswordOperation.downloadUrl && (
<Button
component="a"
href={removePasswordOperation.downloadUrl}
download={removePasswordOperation.downloadFilename}
leftSection={<DownloadIcon />}
color="green"
fullWidth
mb="md"
>
{t("download", "Download")}
</Button>
)}
<ResultsPreview
files={previewResults}
onFileClick={handleThumbnailClick}
isGeneratingThumbnails={removePasswordOperation.isGeneratingThumbnails}
title={t('removePassword.results.title', 'Decrypted PDFs')}
/>
</Stack>
</ToolStep>
</Stack>
</ToolStepContainer>
);
}
export default RemovePassword;

View File

@ -16,7 +16,8 @@ export type ModeType =
| 'convert' | 'convert'
| 'sanitize' | 'sanitize'
| 'addPassword' | 'addPassword'
| 'changePermissions'; | 'changePermissions'
| 'removePassword';
export type ViewType = 'viewer' | 'pageEditor' | 'fileEditor'; export type ViewType = 'viewer' | 'pageEditor' | 'fileEditor';