mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-01-14 20:11:17 +01:00
Add automatic unlock prompt for encrypted PDFs (#4912)
## Summary - propagate an `isEncrypted` flag from thumbnail generation into processed file metadata so uploads know when a password is still present - add queueing logic inside `FileContext` that detects encrypted uploads, prompts the user via a new modal, and automatically runs the Remove Password endpoint to replace the file and preserve history - introduce a dedicated `EncryptedPdfUnlockModal` component that mirrors existing styling and messaging for unlocking PDFs ## Testing - npm run typecheck:core ------ [Codex Task](https://chatgpt.com/codex/tasks/task_b_6919a0a418bc8328b886ec76a28170b7)
This commit is contained in:
parent
6c8d2c89fe
commit
c87da6d5cc
@ -5557,6 +5557,24 @@
|
||||
"starting": "Backend starting up...",
|
||||
"wait": "Please wait for the backend to finish launching and try again."
|
||||
},
|
||||
"encryptedPdfUnlock": {
|
||||
"unlockPrompt": "Unlock PDF to continue",
|
||||
"title": "Remove password to continue",
|
||||
"description": "This PDF is password protected. Enter the password so you can continue working with it.",
|
||||
"password": {
|
||||
"label": "PDF password",
|
||||
"placeholder": "Enter the PDF password"
|
||||
},
|
||||
"skip": "Skip for now",
|
||||
"unlock": "Unlock & Continue",
|
||||
"incorrectPassword": "Incorrect password",
|
||||
"missingFile": "The selected file is no longer available.",
|
||||
"emptyResponse": "Password removal did not produce a file.",
|
||||
"required": "Enter the password to continue.",
|
||||
"successTitle": "Password removed",
|
||||
"successBodyWithName": "Password removed from {{fileName}}",
|
||||
"successBody": "Password removed successfully."
|
||||
},
|
||||
"setup": {
|
||||
"welcome": "Welcome to Stirling PDF",
|
||||
"description": "Get started by choosing how you want to use Stirling PDF",
|
||||
|
||||
@ -9,6 +9,7 @@ import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import UnarchiveIcon from '@mui/icons-material/Unarchive';
|
||||
import PushPinIcon from '@mui/icons-material/PushPin';
|
||||
import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined';
|
||||
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
||||
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
||||
import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { StirlingFileStub } from '@app/types/fileContext';
|
||||
@ -56,7 +57,14 @@ const FileEditorThumbnail = ({
|
||||
isSupported = true,
|
||||
}: FileEditorThumbnailProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { pinFile, unpinFile, isFilePinned, activeFiles, actions: fileActions } = useFileContext();
|
||||
const {
|
||||
pinFile,
|
||||
unpinFile,
|
||||
isFilePinned,
|
||||
activeFiles,
|
||||
actions: fileActions,
|
||||
openEncryptedUnlockPrompt,
|
||||
} = useFileContext();
|
||||
const { state } = useFileState();
|
||||
const hasError = state.ui.errorFileIds.includes(file.id);
|
||||
|
||||
@ -77,6 +85,7 @@ const FileEditorThumbnail = ({
|
||||
const isZipFile = zipFileService.isZipFileStub(file);
|
||||
|
||||
const pageCount = file.processedFile?.totalPages || 0;
|
||||
const isEncrypted = Boolean(file.processedFile?.isEncrypted);
|
||||
|
||||
const handleRef = useRef<HTMLSpanElement | null>(null);
|
||||
|
||||
@ -301,6 +310,21 @@ const FileEditorThumbnail = ({
|
||||
|
||||
{/* Action buttons group */}
|
||||
<div className={styles.headerActions}>
|
||||
{isEncrypted && (
|
||||
<Tooltip label={t('encryptedPdfUnlock.unlockPrompt', 'Unlock PDF to continue')}>
|
||||
<ActionIcon
|
||||
aria-label={t('encryptedPdfUnlock.unlockPrompt', 'Unlock PDF to continue')}
|
||||
variant="subtle"
|
||||
className={styles.headerIconButton}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openEncryptedUnlockPrompt(file.id);
|
||||
}}
|
||||
>
|
||||
<LockOpenIcon fontSize="small" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
{/* Pin/Unpin icon */}
|
||||
<Tooltip label={isPinned ? t('unpin', 'Unpin File (replace after tool run)') : t('pin', 'Pin File (keep active after tool run)')}>
|
||||
<ActionIcon
|
||||
|
||||
@ -0,0 +1,85 @@
|
||||
import { Modal, Stack, Text, Button, PasswordInput, Group } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { type KeyboardEventHandler } from 'react';
|
||||
import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '@app/styles/zIndex';
|
||||
|
||||
interface EncryptedPdfUnlockModalProps {
|
||||
opened: boolean;
|
||||
fileName?: string;
|
||||
password: string;
|
||||
errorMessage?: string | null;
|
||||
isProcessing: boolean;
|
||||
onPasswordChange: (value: string) => void;
|
||||
onUnlock: () => void;
|
||||
onSkip: () => void;
|
||||
}
|
||||
|
||||
const EncryptedPdfUnlockModal = ({
|
||||
opened,
|
||||
fileName,
|
||||
password,
|
||||
errorMessage,
|
||||
isProcessing,
|
||||
onPasswordChange,
|
||||
onUnlock,
|
||||
onSkip,
|
||||
}: EncryptedPdfUnlockModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (event) => {
|
||||
if (event.key === 'Enter' && !isProcessing && password.trim().length > 0) {
|
||||
onUnlock();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onSkip}
|
||||
title={t('encryptedPdfUnlock.title', 'Remove password to continue')}
|
||||
centered
|
||||
size="md"
|
||||
closeOnClickOutside={!isProcessing}
|
||||
closeOnEscape={!isProcessing}
|
||||
zIndex={Z_INDEX_OVER_FULLSCREEN_SURFACE}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text fw={600} ta="center">{fileName}</Text>
|
||||
<Text c="dimmed" ta="center">
|
||||
{t(
|
||||
'encryptedPdfUnlock.description',
|
||||
'This PDF is password protected. Enter the password so you can continue working with it.'
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<Stack gap={4}>
|
||||
<PasswordInput
|
||||
label={t('encryptedPdfUnlock.password.label', 'PDF password')}
|
||||
placeholder={t('encryptedPdfUnlock.password.placeholder', 'Enter the PDF password')}
|
||||
value={password}
|
||||
onChange={(event) => onPasswordChange(event.currentTarget.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={isProcessing}
|
||||
autoFocus
|
||||
/>
|
||||
{errorMessage ? (
|
||||
<Text c="red" size="sm">
|
||||
{errorMessage}
|
||||
</Text>
|
||||
) : null}
|
||||
</Stack>
|
||||
|
||||
<Group justify="space-between">
|
||||
<Button variant="light" color="var(--mantine-color-gray-8)" onClick={onSkip} disabled={isProcessing}>
|
||||
{t('encryptedPdfUnlock.skip', 'Skip for now')}
|
||||
</Button>
|
||||
<Button onClick={onUnlock} loading={isProcessing} disabled={password.trim().length === 0}>
|
||||
{t('encryptedPdfUnlock.unlock', 'Unlock & Continue')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EncryptedPdfUnlockModal;
|
||||
@ -12,7 +12,7 @@
|
||||
* Memory management handled by FileLifecycleManager (PDF.js cleanup, blob URL revocation).
|
||||
*/
|
||||
|
||||
import { useReducer, useCallback, useEffect, useRef, useMemo } from 'react';
|
||||
import { useReducer, useCallback, useEffect, useRef, useMemo, useState } from 'react';
|
||||
import {
|
||||
FileContextProviderProps,
|
||||
FileContextSelectors,
|
||||
@ -22,17 +22,27 @@ import {
|
||||
FileId,
|
||||
StirlingFileStub,
|
||||
StirlingFile,
|
||||
createStirlingFile,
|
||||
} from '@app/types/fileContext';
|
||||
|
||||
// Import modular components
|
||||
import { fileContextReducer, initialFileContextState } from '@app/contexts/file/FileReducer';
|
||||
import { createFileSelectors } from '@app/contexts/file/fileSelectors';
|
||||
import { addFiles, addStirlingFileStubs, consumeFiles, undoConsumeFiles, createFileActions } from '@app/contexts/file/fileActions';
|
||||
import { addFiles, addStirlingFileStubs, consumeFiles, undoConsumeFiles, createFileActions, createChildStub, generateProcessedFileMetadata } from '@app/contexts/file/fileActions';
|
||||
import { FileLifecycleManager } from '@app/contexts/file/lifecycle';
|
||||
import { FileStateContext, FileActionsContext } from '@app/contexts/file/contexts';
|
||||
import { IndexedDBProvider, useIndexedDB } from '@app/contexts/IndexedDBContext';
|
||||
import { useZipConfirmation } from '@app/hooks/useZipConfirmation';
|
||||
import ZipWarningModal from '@app/components/shared/ZipWarningModal';
|
||||
import EncryptedPdfUnlockModal from '@app/components/shared/EncryptedPdfUnlockModal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { alert } from '@app/components/toast';
|
||||
import { buildRemovePasswordFormData } from '@app/hooks/tools/removePassword/buildRemovePasswordFormData';
|
||||
import type { RemovePasswordParameters } from '@app/hooks/tools/removePassword/useRemovePasswordParameters';
|
||||
import apiClient from '@app/services/apiClient';
|
||||
import { processResponse } from '@app/utils/toolResponseProcessor';
|
||||
import { ToolOperation } from '@app/types/file';
|
||||
import { handlePasswordError } from '@app/utils/toolErrorHandler';
|
||||
|
||||
const DEBUG = process.env.NODE_ENV === 'development';
|
||||
|
||||
@ -63,6 +73,98 @@ function FileContextInner({
|
||||
lifecycleManagerRef.current = new FileLifecycleManager(filesRef, dispatch);
|
||||
}
|
||||
const lifecycleManager = lifecycleManagerRef.current;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [encryptedQueue, setEncryptedQueue] = useState<FileId[]>([]);
|
||||
const [activeEncryptedFileId, setActiveEncryptedFileId] = useState<FileId | null>(null);
|
||||
const [unlockPassword, setUnlockPassword] = useState('');
|
||||
const [unlockError, setUnlockError] = useState<string | null>(null);
|
||||
const [isUnlocking, setIsUnlocking] = useState(false);
|
||||
const dismissedEncryptedFilesRef = useRef<Set<FileId>>(new Set());
|
||||
const observedFileIdsRef = useRef<Set<FileId>>(new Set());
|
||||
|
||||
const enqueueEncryptedFiles = useCallback((fileIds: FileId[]) => {
|
||||
if (fileIds.length === 0) return;
|
||||
setEncryptedQueue(prevQueue => {
|
||||
const existing = new Set(prevQueue);
|
||||
const next = [...prevQueue];
|
||||
for (const id of fileIds) {
|
||||
if (dismissedEncryptedFilesRef.current.has(id)) continue;
|
||||
if (id === activeEncryptedFileId) continue;
|
||||
if (existing.has(id)) continue;
|
||||
existing.add(id);
|
||||
next.push(id);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, [activeEncryptedFileId]);
|
||||
|
||||
useEffect(() => {
|
||||
const previousIds = observedFileIdsRef.current;
|
||||
const nextIds = new Set(state.files.ids);
|
||||
const newEncryptedIds: FileId[] = [];
|
||||
|
||||
for (const id of state.files.ids) {
|
||||
if (!previousIds.has(id)) {
|
||||
const stub = state.files.byId[id];
|
||||
if ((stub?.versionNumber ?? 1) <= 1 && stub?.processedFile?.isEncrypted) {
|
||||
newEncryptedIds.push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newEncryptedIds.length > 0) {
|
||||
enqueueEncryptedFiles(newEncryptedIds);
|
||||
}
|
||||
|
||||
observedFileIdsRef.current = nextIds;
|
||||
}, [state.files.ids, state.files.byId, enqueueEncryptedFiles]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeEncryptedFileId && encryptedQueue.length > 0) {
|
||||
setActiveEncryptedFileId(encryptedQueue[0]);
|
||||
setEncryptedQueue(prev => prev.slice(1));
|
||||
}
|
||||
}, [activeEncryptedFileId, encryptedQueue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeEncryptedFileId && !state.files.ids.includes(activeEncryptedFileId)) {
|
||||
setActiveEncryptedFileId(null);
|
||||
}
|
||||
}, [activeEncryptedFileId, state.files.ids]);
|
||||
|
||||
useEffect(() => {
|
||||
setUnlockPassword('');
|
||||
setUnlockError(null);
|
||||
}, [activeEncryptedFileId]);
|
||||
|
||||
const handleUnlockSkip = useCallback(() => {
|
||||
if (activeEncryptedFileId) {
|
||||
dismissedEncryptedFilesRef.current.add(activeEncryptedFileId);
|
||||
}
|
||||
setActiveEncryptedFileId(null);
|
||||
}, [activeEncryptedFileId]);
|
||||
|
||||
const promptEncryptedUnlock = useCallback((fileId: FileId) => {
|
||||
const stub = stateRef.current.files.byId[fileId];
|
||||
if (!stub?.processedFile?.isEncrypted) {
|
||||
return;
|
||||
}
|
||||
|
||||
dismissedEncryptedFilesRef.current.delete(fileId);
|
||||
|
||||
setEncryptedQueue(prevQueue => prevQueue.filter(id => id !== fileId));
|
||||
|
||||
setActiveEncryptedFileId(currentActiveId => {
|
||||
if (currentActiveId && currentActiveId !== fileId) {
|
||||
setEncryptedQueue(prevQueue => {
|
||||
const withoutDuplicates = prevQueue.filter(id => id !== currentActiveId && id !== fileId);
|
||||
return [currentActiveId, ...withoutDuplicates];
|
||||
});
|
||||
}
|
||||
return fileId;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Create stable selectors (memoized once to avoid re-renders)
|
||||
const selectors = useMemo<FileContextSelectors>(() =>
|
||||
@ -131,6 +233,80 @@ function FileContextInner({
|
||||
return consumeFiles(inputFileIds, outputStirlingFiles, outputStirlingFileStubs, filesRef, dispatch);
|
||||
}, []);
|
||||
|
||||
const runAutomaticPasswordRemoval = useCallback(async (fileId: FileId, password: string): Promise<void> => {
|
||||
const file = filesRef.current.get(fileId);
|
||||
const parentStub = stateRef.current.files.byId[fileId];
|
||||
|
||||
if (!file || !parentStub) {
|
||||
throw new Error(t('encryptedPdfUnlock.missingFile', 'The selected file is no longer available.'));
|
||||
}
|
||||
|
||||
const params: RemovePasswordParameters = { password };
|
||||
const formData = buildRemovePasswordFormData(params, file);
|
||||
|
||||
const response = await apiClient.post('/api/v1/security/remove-password', formData, {
|
||||
responseType: 'blob',
|
||||
suppressErrorToast: true // Handle errors in modal UI instead of toast
|
||||
});
|
||||
const responseFiles = await processResponse(response.data, [file]);
|
||||
|
||||
const unlockedFile = responseFiles[0];
|
||||
if (!unlockedFile) {
|
||||
throw new Error(t('encryptedPdfUnlock.emptyResponse', 'Password removal did not produce a file.'));
|
||||
}
|
||||
|
||||
const processedMetadata = await generateProcessedFileMetadata(unlockedFile);
|
||||
const thumbnail = processedMetadata?.thumbnailUrl;
|
||||
|
||||
const operation: ToolOperation = {
|
||||
toolId: 'removePassword',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
const childStub = createChildStub(parentStub, operation, unlockedFile, thumbnail, processedMetadata);
|
||||
const stirlingUnlockedFile = createStirlingFile(unlockedFile, childStub.id);
|
||||
|
||||
await consumeFilesWrapper([fileId], [stirlingUnlockedFile], [childStub]);
|
||||
}, [consumeFilesWrapper, t]);
|
||||
|
||||
const handleUnlockSubmit = useCallback(async () => {
|
||||
if (!activeEncryptedFileId) return;
|
||||
if (!unlockPassword.trim()) {
|
||||
setUnlockError(t('encryptedPdfUnlock.required', 'Enter the password to continue.'));
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUnlocking(true);
|
||||
setUnlockError(null);
|
||||
try {
|
||||
await runAutomaticPasswordRemoval(activeEncryptedFileId, unlockPassword.trim());
|
||||
const fileName = stateRef.current.files.byId[activeEncryptedFileId]?.name;
|
||||
alert({
|
||||
alertType: 'success',
|
||||
title: t('encryptedPdfUnlock.successTitle', 'Password removed'),
|
||||
body: fileName
|
||||
? t('encryptedPdfUnlock.successBodyWithName', {
|
||||
defaultValue: 'Removed password from {{fileName}}',
|
||||
fileName,
|
||||
})
|
||||
: t('encryptedPdfUnlock.successBody', 'Password removed successfully.'),
|
||||
expandable: false,
|
||||
isPersistentPopup: false,
|
||||
});
|
||||
dismissedEncryptedFilesRef.current.delete(activeEncryptedFileId);
|
||||
setActiveEncryptedFileId(null);
|
||||
} catch (error) {
|
||||
const errorMessage = await handlePasswordError(
|
||||
error,
|
||||
t('encryptedPdfUnlock.incorrectPassword', 'Incorrect password'),
|
||||
t('removePassword.error.failed', 'An error occurred while removing the password from the PDF.')
|
||||
);
|
||||
setUnlockError(errorMessage);
|
||||
} finally {
|
||||
setIsUnlocking(false);
|
||||
}
|
||||
}, [activeEncryptedFileId, unlockPassword, runAutomaticPasswordRemoval, t]);
|
||||
|
||||
const undoConsumeFilesWrapper = useCallback(async (inputFiles: File[], inputStirlingFileStubs: StirlingFileStub[], outputFileIds: FileId[]): Promise<void> => {
|
||||
return undoConsumeFiles(inputFiles, inputStirlingFileStubs, outputFileIds, filesRef, dispatch, indexedDB);
|
||||
}, [indexedDB]);
|
||||
@ -199,7 +375,8 @@ function FileContextInner({
|
||||
trackBlobUrl: lifecycleManager.trackBlobUrl,
|
||||
cleanupFile: (fileId: FileId) => lifecycleManager.cleanupFile(fileId, stateRef),
|
||||
scheduleCleanup: (fileId: FileId, delay?: number) =>
|
||||
lifecycleManager.scheduleCleanup(fileId, delay, stateRef)
|
||||
lifecycleManager.scheduleCleanup(fileId, delay, stateRef),
|
||||
openEncryptedUnlockPrompt: promptEncryptedUnlock
|
||||
}), [
|
||||
baseActions,
|
||||
addRawFiles,
|
||||
@ -211,7 +388,8 @@ function FileContextInner({
|
||||
pinFileWrapper,
|
||||
unpinFileWrapper,
|
||||
indexedDB,
|
||||
enablePersistence
|
||||
enablePersistence,
|
||||
promptEncryptedUnlock
|
||||
]);
|
||||
|
||||
// Split context values to minimize re-renders
|
||||
@ -225,6 +403,9 @@ function FileContextInner({
|
||||
dispatch
|
||||
}), [actions]);
|
||||
|
||||
const activeEncryptedStub = activeEncryptedFileId ? state.files.byId[activeEncryptedFileId] : undefined;
|
||||
const isUnlockModalOpen = Boolean(activeEncryptedFileId && activeEncryptedStub);
|
||||
|
||||
// Persistence loading disabled - files only loaded on explicit user action
|
||||
// useEffect(() => {
|
||||
// if (!enablePersistence || !indexedDB) return;
|
||||
@ -251,6 +432,16 @@ function FileContextInner({
|
||||
fileCount={confirmationState.fileCount}
|
||||
zipFileName={confirmationState.fileName}
|
||||
/>
|
||||
<EncryptedPdfUnlockModal
|
||||
opened={isUnlockModalOpen}
|
||||
fileName={activeEncryptedStub?.name}
|
||||
password={unlockPassword}
|
||||
errorMessage={unlockError}
|
||||
isProcessing={isUnlocking}
|
||||
onPasswordChange={setUnlockPassword}
|
||||
onUnlock={handleUnlockSubmit}
|
||||
onSkip={handleUnlockSkip}
|
||||
/>
|
||||
</FileActionsContext.Provider>
|
||||
</FileStateContext.Provider>
|
||||
);
|
||||
|
||||
@ -63,7 +63,7 @@ export function createProcessedFile(
|
||||
thumbnail?: string,
|
||||
pageRotations?: number[],
|
||||
pageDimensions?: Array<{ width: number; height: number }>
|
||||
) {
|
||||
): ProcessedFileMetadata {
|
||||
return {
|
||||
totalPages: pageCount,
|
||||
pages: Array.from({ length: pageCount }, (_, index) => ({
|
||||
@ -106,6 +106,10 @@ export async function generateProcessedFileMetadata(file: File): Promise<Process
|
||||
// Use rotated thumbnail for file manager
|
||||
processedFile.thumbnailUrl = rotatedResult.thumbnail;
|
||||
|
||||
if (unrotatedResult.isEncrypted || rotatedResult.isEncrypted) {
|
||||
processedFile.isEncrypted = true;
|
||||
}
|
||||
|
||||
return processedFile;
|
||||
} catch (error) {
|
||||
if (DEBUG) console.warn(`📄 Failed to generate processedFileMetadata for ${file.name}:`, error);
|
||||
|
||||
@ -188,6 +188,7 @@ export function useFileContext() {
|
||||
|
||||
// Active files
|
||||
activeFiles: selectors.getFiles(),
|
||||
openEncryptedUnlockPrompt: actions.openEncryptedUnlockPrompt,
|
||||
|
||||
// Direct access to actions and selectors (for advanced use cases)
|
||||
actions,
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
import { RemovePasswordParameters } from '@app/hooks/tools/removePassword/useRemovePasswordParameters';
|
||||
|
||||
/**
|
||||
* Builds FormData for remove password API request.
|
||||
* Separated from operation config to avoid circular dependencies with FileContext.
|
||||
*/
|
||||
export const buildRemovePasswordFormData = (parameters: RemovePasswordParameters, file: File): FormData => {
|
||||
const formData = new FormData();
|
||||
formData.append("fileInput", file);
|
||||
formData.append("password", parameters.password);
|
||||
return formData;
|
||||
};
|
||||
@ -2,14 +2,10 @@ import { useTranslation } from 'react-i18next';
|
||||
import { ToolType, useToolOperation } from '@app/hooks/tools/shared/useToolOperation';
|
||||
import { createStandardErrorHandler } from '@app/utils/toolErrorHandler';
|
||||
import { RemovePasswordParameters, defaultParameters } from '@app/hooks/tools/removePassword/useRemovePasswordParameters';
|
||||
import { buildRemovePasswordFormData } from '@app/hooks/tools/removePassword/buildRemovePasswordFormData';
|
||||
|
||||
// Static function that can be used by both the hook and automation executor
|
||||
export const buildRemovePasswordFormData = (parameters: RemovePasswordParameters, file: File): FormData => {
|
||||
const formData = new FormData();
|
||||
formData.append("fileInput", file);
|
||||
formData.append("password", parameters.password);
|
||||
return formData;
|
||||
};
|
||||
// Re-export for backwards compatibility with any other imports
|
||||
export { buildRemovePasswordFormData };
|
||||
|
||||
// Static configuration object
|
||||
export const removePasswordOperationConfig = {
|
||||
|
||||
@ -23,6 +23,7 @@ export interface ProcessedFileMetadata {
|
||||
pages: ProcessedFilePage[];
|
||||
totalPages?: number;
|
||||
lastProcessed?: number;
|
||||
isEncrypted?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@ -301,6 +302,7 @@ export interface FileContextActions {
|
||||
trackBlobUrl: (url: string) => void;
|
||||
scheduleCleanup: (fileId: FileId, delay?: number) => void;
|
||||
cleanupFile: (fileId: FileId) => void;
|
||||
openEncryptedUnlockPrompt: (fileId: FileId) => void;
|
||||
}
|
||||
|
||||
// File selectors (separate from actions to avoid re-renders)
|
||||
|
||||
@ -5,6 +5,7 @@ export interface ThumbnailWithMetadata {
|
||||
pageCount: number;
|
||||
pageRotations?: number[]; // Rotation for each page (0, 90, 180, 270)
|
||||
pageDimensions?: Array<{ width: number; height: number }>;
|
||||
isEncrypted?: boolean;
|
||||
}
|
||||
|
||||
interface ColorScheme {
|
||||
@ -451,7 +452,7 @@ export async function generateThumbnailWithMetadata(file: File, applyRotation: b
|
||||
if (error instanceof Error && error.name === "PasswordException") {
|
||||
// Handle encrypted PDFs
|
||||
const thumbnail = generateEncryptedPDFThumbnail(file);
|
||||
return { thumbnail, pageCount: 1 };
|
||||
return { thumbnail, pageCount: 1, isEncrypted: true };
|
||||
}
|
||||
|
||||
const thumbnail = generatePlaceholderThumbnail(file);
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
* Standardized error handling utilities for tool operations
|
||||
*/
|
||||
|
||||
import { normalizeAxiosErrorData } from '@app/services/errorUtils';
|
||||
|
||||
/**
|
||||
* Default error extractor that follows the standard pattern
|
||||
*/
|
||||
@ -30,4 +32,36 @@ export const createStandardErrorHandler = (fallbackMessage: string) => {
|
||||
}
|
||||
return fallbackMessage;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles password-related errors with status code checking
|
||||
* @param error - The error object from axios
|
||||
* @param incorrectPasswordMessage - Message to show for incorrect password (typically 500 status)
|
||||
* @param fallbackMessage - Message to show for other errors
|
||||
* @returns Error message string
|
||||
*/
|
||||
export const handlePasswordError = async (
|
||||
error: any,
|
||||
incorrectPasswordMessage: string,
|
||||
fallbackMessage: string
|
||||
): Promise<string> => {
|
||||
const status = error?.response?.status;
|
||||
|
||||
// Handle specific error cases with user-friendly messages
|
||||
if (status === 500) {
|
||||
// 500 typically means incorrect password for encrypted PDFs
|
||||
return incorrectPasswordMessage;
|
||||
}
|
||||
|
||||
// For other errors, try to extract the message
|
||||
const normalizedData = await normalizeAxiosErrorData(error?.response?.data);
|
||||
const errorWithNormalizedData = {
|
||||
...error,
|
||||
response: {
|
||||
...error?.response,
|
||||
data: normalizedData
|
||||
}
|
||||
};
|
||||
return extractErrorMessage(errorWithNormalizedData) || fallbackMessage;
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user