From aaf6c30413cc8a50f62787ecca4a186c26c66386 Mon Sep 17 00:00:00 2001 From: James Brunton Date: Thu, 6 Nov 2025 12:22:08 +0000 Subject: [PATCH] More `any` type fixes --- frontend/src/core/components/toast/index.ts | 61 +++++++++------ .../useAdjustContrastOperation.ts | 21 +++-- frontend/src/core/hooks/usePDFProcessor.ts | 17 +++- .../src/core/services/automationStorage.ts | 6 +- .../services/enhancedPDFProcessingService.ts | 10 ++- .../services/thumbnailGenerationService.ts | 11 ++- .../core/services/userManagementService.ts | 40 ++++++---- frontend/src/core/services/zipFileService.ts | 6 +- frontend/src/core/tools/AddAttachments.tsx | 12 ++- frontend/src/core/tools/AddPageNumbers.tsx | 14 ++-- frontend/src/core/tools/AddStamp.tsx | 13 ++-- frontend/src/core/tools/ReorganizePages.tsx | 18 +++-- frontend/src/core/utils/fileResponseUtils.ts | 50 +++++++++++- frontend/src/core/utils/thumbnailUtils.ts | 10 ++- frontend/src/desktop/auth/springAuthClient.ts | 78 +++++++++++++------ 15 files changed, 260 insertions(+), 107 deletions(-) diff --git a/frontend/src/core/components/toast/index.ts b/frontend/src/core/components/toast/index.ts index e13ab3801..f6a4225c6 100644 --- a/frontend/src/core/components/toast/index.ts +++ b/frontend/src/core/components/toast/index.ts @@ -1,22 +1,32 @@ -import { ToastOptions } from '@app/components/toast/types'; +import { ToastOptions, ToastApi } from '@app/components/toast/types'; import { useToast, ToastProvider } from '@app/components/toast/ToastContext'; import ToastRenderer from '@app/components/toast/ToastRenderer'; export { useToast, ToastProvider, ToastRenderer }; -// Global imperative API via module singleton -let _api: ReturnType | null = null; +type ToastContextInstance = ReturnType; -function createImperativeApi() { - const subscribers: Array<(fn: any) => void> = []; - let api: any = null; +interface ImperativeApi { + provide(instance: ToastContextInstance): void; + get(): ToastContextInstance | null; + onReady(cb: (api: ToastContextInstance) => void): void; +} + +// Global imperative API via module singleton +let _api: ImperativeApi | null = null; + +function createImperativeApi(): ImperativeApi { + const subscribers: Array<(api: ToastContextInstance) => void> = []; + let api: ToastContextInstance | null = null; return { - provide(instance: any) { + provide(instance: ToastContextInstance) { api = instance; - subscribers.splice(0).forEach(cb => cb(api)); + const queued = [...subscribers]; + subscribers.length = 0; + queued.forEach(cb => cb(instance)); }, - get(): any | null { return api; }, - onReady(cb: (api: any) => void) { + get(): ToastContextInstance | null { return api; }, + onReady(cb: (readyApi: ToastContextInstance) => void) { if (api) cb(api); else subscribers.push(cb); } }; @@ -32,30 +42,33 @@ export function ToastPortalBinder() { return null; } -export function alert(options: ToastOptions) { - if (_api?.get()) { - return _api.get()!.show(options); +function getImperativeApi(): ToastApi | null { + return _api?.get() ?? null; +} + +export function alert(options: ToastOptions): string { + const api = getImperativeApi(); + if (api) { + return api.show(options); } // Queue until provider mounts let id = ''; - _api?.onReady((api) => { id = api.show(options); }); + _api?.onReady((readyApi) => { id = readyApi.show(options); }); return id; } -export function updateToast(id: string, options: Partial) { - _api?.get()?.update(id, options); +export function updateToast(id: string, options: Partial): void { + getImperativeApi()?.update(id, options); } -export function updateToastProgress(id: string, progress: number) { - _api?.get()?.updateProgress(id, progress); +export function updateToastProgress(id: string, progress: number): void { + getImperativeApi()?.updateProgress(id, progress); } -export function dismissToast(id: string) { - _api?.get()?.dismiss(id); +export function dismissToast(id: string): void { + getImperativeApi()?.dismiss(id); } -export function dismissAllToasts() { - _api?.get()?.dismissAll(); +export function dismissAllToasts(): void { + getImperativeApi()?.dismissAll(); } - - diff --git a/frontend/src/core/hooks/tools/adjustContrast/useAdjustContrastOperation.ts b/frontend/src/core/hooks/tools/adjustContrast/useAdjustContrastOperation.ts index 176ddd4b5..f26af51f1 100644 --- a/frontend/src/core/hooks/tools/adjustContrast/useAdjustContrastOperation.ts +++ b/frontend/src/core/hooks/tools/adjustContrast/useAdjustContrastOperation.ts @@ -4,17 +4,23 @@ import { AdjustContrastParameters, defaultParameters } from '@app/hooks/tools/ad import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { applyAdjustmentsToCanvas } from '@app/components/tools/adjustContrast/utils'; import { pdfWorkerManager } from '@app/services/pdfWorkerManager'; -import { createFileFromApiResponse } from '@app/utils/fileResponseUtils'; +import { createFileFromApiResponse, toArrayBuffer } from '@app/utils/fileResponseUtils'; +import type { PDFDocumentProxy, PDFPageProxy, RenderParameters } from 'pdfjs-dist/types/src/display/api'; -async function renderPdfPageToCanvas(pdf: any, pageNumber: number, scale: number): Promise { - const page = await pdf.getPage(pageNumber); +async function renderPdfPageToCanvas(pdf: PDFDocumentProxy, pageNumber: number, scale: number): Promise { + const page: PDFPageProxy = await pdf.getPage(pageNumber); const viewport = page.getViewport({ scale }); const canvas = document.createElement('canvas'); canvas.width = viewport.width; canvas.height = viewport.height; const ctx = canvas.getContext('2d'); if (!ctx) throw new Error('Canvas 2D context unavailable'); - await page.render({ canvasContext: ctx, viewport }).promise; + const renderConfig: RenderParameters = { + canvasContext: ctx, + viewport, + canvas, + }; + await page.render(renderConfig).promise; return canvas; } @@ -41,7 +47,11 @@ async function buildAdjustedPdfForFile(file: File, params: AdjustContrastParamet } const pdfBytes = await newDoc.save(); - const out = createFileFromApiResponse(pdfBytes, { 'content-type': 'application/pdf' }, file.name); + const out = createFileFromApiResponse( + toArrayBuffer(pdfBytes), + { 'content-type': 'application/pdf' }, + file.name + ); pdfWorkerManager.destroyDocument(pdf); return out; } @@ -91,4 +101,3 @@ export const useAdjustContrastOperation = () => { getErrorMessage: () => t('adjustContrast.error.failed', 'Failed to adjust colors/contrast') }); }; - diff --git a/frontend/src/core/hooks/usePDFProcessor.ts b/frontend/src/core/hooks/usePDFProcessor.ts index b116545e2..d62b3376f 100644 --- a/frontend/src/core/hooks/usePDFProcessor.ts +++ b/frontend/src/core/hooks/usePDFProcessor.ts @@ -2,6 +2,7 @@ import { useState, useCallback } from 'react'; import { PDFDocument, PDFPage } from '@app/types/pageEditor'; import { pdfWorkerManager } from '@app/services/pdfWorkerManager'; import { createQuickKey } from '@app/types/fileContext'; +import type { PDFDocumentProxy, RenderParameters } from 'pdfjs-dist/types/src/display/api'; export function usePDFProcessor() { const [loading, setLoading] = useState(false); @@ -27,7 +28,12 @@ export function usePDFProcessor() { throw new Error('Could not get canvas context'); } - await page.render({ canvasContext: context, viewport, canvas }).promise; + const renderConfig: RenderParameters = { + canvasContext: context, + viewport, + canvas, + }; + await page.render(renderConfig).promise; const thumbnail = canvas.toDataURL(); // Clean up using worker manager @@ -42,7 +48,7 @@ export function usePDFProcessor() { // Internal function to generate thumbnail from already-opened PDF const generateThumbnailFromPDF = useCallback(async ( - pdf: any, + pdf: PDFDocumentProxy, pageNumber: number, scale: number = 0.5 ): Promise => { @@ -58,7 +64,12 @@ export function usePDFProcessor() { throw new Error('Could not get canvas context'); } - await page.render({ canvasContext: context, viewport }).promise; + const renderConfig: RenderParameters = { + canvasContext: context, + viewport, + canvas, + }; + await page.render(renderConfig).promise; return canvas.toDataURL(); }, []); diff --git a/frontend/src/core/services/automationStorage.ts b/frontend/src/core/services/automationStorage.ts index 990e95a7f..df927e977 100644 --- a/frontend/src/core/services/automationStorage.ts +++ b/frontend/src/core/services/automationStorage.ts @@ -2,13 +2,15 @@ * Service for managing automation configurations in IndexedDB */ +import type { AutomationParameters } from '@app/types/automation'; + export interface AutomationConfig { id: string; name: string; description?: string; operations: Array<{ operation: string; - parameters: any; + parameters: AutomationParameters; }>; createdAt: string; updatedAt: string; @@ -180,4 +182,4 @@ class AutomationStorage { } // Export singleton instance -export const automationStorage = new AutomationStorage(); \ No newline at end of file +export const automationStorage = new AutomationStorage(); diff --git a/frontend/src/core/services/enhancedPDFProcessingService.ts b/frontend/src/core/services/enhancedPDFProcessingService.ts index b084d2eb1..38a0f7d6c 100644 --- a/frontend/src/core/services/enhancedPDFProcessingService.ts +++ b/frontend/src/core/services/enhancedPDFProcessingService.ts @@ -5,6 +5,7 @@ import { FileAnalyzer } from '@app/services/fileAnalyzer'; import { ProcessingErrorHandler } from '@app/services/processingErrorHandler'; import { pdfWorkerManager } from '@app/services/pdfWorkerManager'; import { createQuickKey } from '@app/types/fileContext'; +import type { PDFPageProxy, RenderParameters } from 'pdfjs-dist/types/src/display/api'; export class EnhancedPDFProcessingService { private static instance: EnhancedPDFProcessingService; @@ -400,7 +401,7 @@ export class EnhancedPDFProcessingService { /** * Render a page thumbnail with specified quality */ - private async renderPageThumbnail(page: any, quality: 'low' | 'medium' | 'high'): Promise { + private async renderPageThumbnail(page: PDFPageProxy, quality: 'low' | 'medium' | 'high'): Promise { const scales = { low: 0.2, medium: 0.5, high: 0.8 }; // Reduced low quality for page editor const scale = scales[quality]; @@ -414,7 +415,12 @@ export class EnhancedPDFProcessingService { throw new Error('Could not get canvas context'); } - await page.render({ canvasContext: context, viewport }).promise; + const renderConfig: RenderParameters = { + canvasContext: context, + viewport, + canvas, + }; + await page.render(renderConfig).promise; return canvas.toDataURL('image/jpeg', 0.8); // Use JPEG for better compression } diff --git a/frontend/src/core/services/thumbnailGenerationService.ts b/frontend/src/core/services/thumbnailGenerationService.ts index c20b7aa47..f4ef186e8 100644 --- a/frontend/src/core/services/thumbnailGenerationService.ts +++ b/frontend/src/core/services/thumbnailGenerationService.ts @@ -4,7 +4,7 @@ import { FileId } from '@app/types/file'; import { pdfWorkerManager } from '@app/services/pdfWorkerManager'; -import { PDFDocumentProxy } from 'pdfjs-dist'; +import type { PDFDocumentProxy, RenderParameters } from 'pdfjs-dist/types/src/display/api'; interface ThumbnailResult { pageNumber: number; @@ -49,7 +49,7 @@ export class ThumbnailGenerationService { /** * Get or create a cached PDF document */ - private async getCachedPDFDocument(fileId: FileId, pdfArrayBuffer: ArrayBuffer): Promise { + private async getCachedPDFDocument(fileId: FileId, pdfArrayBuffer: ArrayBuffer): Promise { const cached = this.pdfDocumentCache.get(fileId); if (cached) { cached.lastUsed = Date.now(); @@ -176,7 +176,12 @@ export class ThumbnailGenerationService { throw new Error('Could not get canvas context'); } - await page.render({ canvasContext: context, viewport }).promise; + const renderConfig: RenderParameters = { + canvasContext: context, + viewport, + canvas, + }; + await page.render(renderConfig).promise; const thumbnail = canvas.toDataURL('image/jpeg', quality); allResults.push({ pageNumber, thumbnail, success: true }); diff --git a/frontend/src/core/services/userManagementService.ts b/frontend/src/core/services/userManagementService.ts index 8e92cf350..53df82862 100644 --- a/frontend/src/core/services/userManagementService.ts +++ b/frontend/src/core/services/userManagementService.ts @@ -1,4 +1,5 @@ import apiClient from '@app/services/apiClient'; +import type { AxiosRequestConfig } from 'axios'; export interface User { id: number; @@ -29,10 +30,17 @@ export interface AdminSettingsData { disabledUsers: number; currentUsername?: string; roleDetails?: Record; - teams?: any[]; + teams?: TeamSummary[]; maxPaidUsers?: number; } +export interface TeamSummary { + id: number; + name: string; + memberCount?: number; + [key: string]: unknown; +} + export interface CreateUserRequest { username: string; password?: string; @@ -100,9 +108,10 @@ export const userManagementService = { if (data.forceChange !== undefined) { formData.append('forceChange', data.forceChange.toString()); } - await apiClient.post('/api/v1/user/admin/saveUser', formData, { - suppressErrorToast: true, // Component will handle error display - } as any); + const config: AxiosRequestConfig & { suppressErrorToast?: boolean } = { + suppressErrorToast: true, + }; + await apiClient.post('/api/v1/user/admin/saveUser', formData, config); }, /** @@ -115,9 +124,10 @@ export const userManagementService = { if (data.teamId) { formData.append('teamId', data.teamId.toString()); } - await apiClient.post('/api/v1/user/admin/changeRole', formData, { + const config: AxiosRequestConfig & { suppressErrorToast?: boolean } = { suppressErrorToast: true, - } as any); + }; + await apiClient.post('/api/v1/user/admin/changeRole', formData, config); }, /** @@ -126,18 +136,20 @@ export const userManagementService = { async toggleUserEnabled(username: string, enabled: boolean): Promise { const formData = new FormData(); formData.append('enabled', enabled.toString()); - await apiClient.post(`/api/v1/user/admin/changeUserEnabled/${username}`, formData, { + const config: AxiosRequestConfig & { suppressErrorToast?: boolean } = { suppressErrorToast: true, - } as any); + }; + await apiClient.post(`/api/v1/user/admin/changeUserEnabled/${username}`, formData, config); }, /** * Delete a user (admin only) */ async deleteUser(username: string): Promise { - await apiClient.post(`/api/v1/user/admin/deleteUser/${username}`, null, { + const config: AxiosRequestConfig & { suppressErrorToast?: boolean } = { suppressErrorToast: true, - } as any); + }; + await apiClient.post(`/api/v1/user/admin/deleteUser/${username}`, null, config); }, /** @@ -153,12 +165,14 @@ export const userManagementService = { formData.append('teamId', data.teamId.toString()); } + const config: AxiosRequestConfig & { suppressErrorToast?: boolean } = { + suppressErrorToast: true, + }; + const response = await apiClient.post( '/api/v1/user/admin/inviteUsers', formData, - { - suppressErrorToast: true, // Component will handle error display - } as any + config ); return response.data; diff --git a/frontend/src/core/services/zipFileService.ts b/frontend/src/core/services/zipFileService.ts index f7f157283..adeb16728 100644 --- a/frontend/src/core/services/zipFileService.ts +++ b/frontend/src/core/services/zipFileService.ts @@ -2,6 +2,7 @@ import JSZip, { JSZipObject } from 'jszip'; import { StirlingFileStub, createStirlingFile } from '@app/types/fileContext'; import { generateThumbnailForFile } from '@app/utils/thumbnailUtils'; import { fileStorage } from '@app/services/fileStorage'; +import { toArrayBuffer } from '@app/utils/fileResponseUtils'; // Undocumented interface in JSZip for JSZipObject._data interface CompressedObject { @@ -13,7 +14,8 @@ interface CompressedObject { } const getData = (zipEntry: JSZipObject): CompressedObject | undefined => { - return (zipEntry as any)._data as CompressedObject; + const candidate = zipEntry as JSZipObject & { _data?: CompressedObject }; + return candidate._data; }; export interface ZipExtractionResult { @@ -216,7 +218,7 @@ export class ZipFileService { const content = await zipEntry.async('uint8array'); // Create File object - const extractedFile = new File([content as any], this.sanitizeFilename(filename), { + const extractedFile = new File([toArrayBuffer(content)], this.sanitizeFilename(filename), { type: 'application/pdf', lastModified: zipEntry.date?.getTime() || Date.now() }); diff --git a/frontend/src/core/tools/AddAttachments.tsx b/frontend/src/core/tools/AddAttachments.tsx index c248eab96..202e3781c 100644 --- a/frontend/src/core/tools/AddAttachments.tsx +++ b/frontend/src/core/tools/AddAttachments.tsx @@ -1,7 +1,7 @@ import { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { useFileSelection } from "@app/contexts/FileContext"; -import { createToolFlow } from "@app/components/tools/shared/createToolFlow"; +import { createToolFlow, type MiddleStepConfig } from "@app/components/tools/shared/createToolFlow"; import { BaseToolProps, ToolComponent } from "@app/types/tool"; import { useEndpointEnabled } from "@app/hooks/useEndpointConfig"; import { useAddAttachmentsParameters } from "@app/hooks/tools/addAttachments/useAddAttachmentsParameters"; @@ -29,8 +29,12 @@ const AddAttachments = ({ onPreviewFile, onComplete, onError }: BaseToolProps) = if (operation.files && onComplete) { onComplete(operation.files); } - } catch (error: any) { - onError?.(error?.message || t("AddAttachmentsRequest.error.failed", "Add attachments operation failed")); + } catch (error: unknown) { + const message = + error instanceof Error + ? error.message + : t("AddAttachmentsRequest.error.failed", "Add attachments operation failed"); + onError?.(message); } }; @@ -56,7 +60,7 @@ const AddAttachments = ({ onPreviewFile, onComplete, onError }: BaseToolProps) = }); const getSteps = () => { - const steps: any[] = []; + const steps: MiddleStepConfig[] = []; // Step 1: Attachments Selection steps.push({ diff --git a/frontend/src/core/tools/AddPageNumbers.tsx b/frontend/src/core/tools/AddPageNumbers.tsx index 3740c5f22..193e77f19 100644 --- a/frontend/src/core/tools/AddPageNumbers.tsx +++ b/frontend/src/core/tools/AddPageNumbers.tsx @@ -1,7 +1,7 @@ import { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { useFileSelection } from "@app/contexts/FileContext"; -import { createToolFlow } from "@app/components/tools/shared/createToolFlow"; +import { createToolFlow, type MiddleStepConfig } from "@app/components/tools/shared/createToolFlow"; import { BaseToolProps, ToolComponent } from "@app/types/tool"; import { useEndpointEnabled } from "@app/hooks/useEndpointConfig"; import { useAddPageNumbersParameters } from "@app/components/tools/addPageNumbers/useAddPageNumbersParameters"; @@ -30,8 +30,12 @@ const AddPageNumbers = ({ onPreviewFile, onComplete, onError }: BaseToolProps) = if (operation.files && onComplete) { onComplete(operation.files); } - } catch (error: any) { - onError?.(error?.message || t("addPageNumbers.error.failed", "Add page numbers operation failed")); + } catch (error: unknown) { + const message = + error instanceof Error + ? error.message + : t("addPageNumbers.error.failed", "Add page numbers operation failed"); + onError?.(message); } }; @@ -58,7 +62,7 @@ const AddPageNumbers = ({ onPreviewFile, onComplete, onError }: BaseToolProps) = }); const getSteps = () => { - const steps: any[] = []; + const steps: MiddleStepConfig[] = []; // Step 1: Position Selection & Pages/Starting Number steps.push({ @@ -123,4 +127,4 @@ const AddPageNumbers = ({ onPreviewFile, onComplete, onError }: BaseToolProps) = AddPageNumbers.tool = () => useAddPageNumbersOperation; -export default AddPageNumbers as ToolComponent; \ No newline at end of file +export default AddPageNumbers as ToolComponent; diff --git a/frontend/src/core/tools/AddStamp.tsx b/frontend/src/core/tools/AddStamp.tsx index efb99ee8a..5046a5057 100644 --- a/frontend/src/core/tools/AddStamp.tsx +++ b/frontend/src/core/tools/AddStamp.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useFileSelection } from "@app/contexts/FileContext"; -import { createToolFlow } from "@app/components/tools/shared/createToolFlow"; +import { createToolFlow, type MiddleStepConfig } from "@app/components/tools/shared/createToolFlow"; import { BaseToolProps, ToolComponent } from "@app/types/tool"; import { useEndpointEnabled } from "@app/hooks/useEndpointConfig"; import { useAddStampParameters } from "@app/components/tools/addStamp/useAddStampParameters"; @@ -39,8 +39,12 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { if (operation.files && onComplete) { onComplete(operation.files); } - } catch (error: any) { - onError?.(error?.message || t("AddStampRequest.error.failed", "Add stamp operation failed")); + } catch (error: unknown) { + const message = + error instanceof Error + ? error.message + : t("AddStampRequest.error.failed", "Add stamp operation failed"); + onError?.(message); } }; @@ -67,7 +71,7 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { }); const getSteps = () => { - const steps: any[] = []; + const steps: MiddleStepConfig[] = []; // Step 1: Stamp Setup steps.push({ @@ -185,4 +189,3 @@ AddStamp.tool = () => useAddStampOperation; export default AddStamp as ToolComponent; - diff --git a/frontend/src/core/tools/ReorganizePages.tsx b/frontend/src/core/tools/ReorganizePages.tsx index 9ce1ec2da..c09c5e75c 100644 --- a/frontend/src/core/tools/ReorganizePages.tsx +++ b/frontend/src/core/tools/ReorganizePages.tsx @@ -1,6 +1,6 @@ import { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { createToolFlow } from "@app/components/tools/shared/createToolFlow"; +import { createToolFlow, type MiddleStepConfig } from "@app/components/tools/shared/createToolFlow"; import { BaseToolProps, ToolComponent } from "@app/types/tool"; import { useEndpointEnabled } from "@app/hooks/useEndpointConfig"; import { useFileSelection } from "@app/contexts/FileContext"; @@ -29,8 +29,12 @@ const ReorganizePages = ({ onPreviewFile, onComplete, onError }: BaseToolProps) if (operation.files && onComplete) { onComplete(operation.files); } - } catch (error: any) { - onError?.(error?.message || t("reorganizePages.error.failed", "Failed to reorganize pages")); + } catch (error: unknown) { + const message = + error instanceof Error + ? error.message + : t("reorganizePages.error.failed", "Failed to reorganize pages"); + onError?.(message); } }; @@ -55,7 +59,7 @@ const ReorganizePages = ({ onPreviewFile, onComplete, onError }: BaseToolProps) } }); - const steps = [ + const steps: MiddleStepConfig[] = [ { title: t("reorganizePages.settings.title", "Settings"), isCollapsed: accordion.getCollapsedState(Step.SETTINGS), @@ -97,8 +101,8 @@ const ReorganizePages = ({ onPreviewFile, onComplete, onError }: BaseToolProps) }); }; -(ReorganizePages as any).tool = () => useReorganizePagesOperation; - -export default ReorganizePages as ToolComponent; +const ReorganizePagesTool = ReorganizePages as ToolComponent; +ReorganizePagesTool.tool = () => useReorganizePagesOperation; +export default ReorganizePagesTool; diff --git a/frontend/src/core/utils/fileResponseUtils.ts b/frontend/src/core/utils/fileResponseUtils.ts index 659c2948d..4728ae35c 100644 --- a/frontend/src/core/utils/fileResponseUtils.ts +++ b/frontend/src/core/utils/fileResponseUtils.ts @@ -30,16 +30,58 @@ export const getFilenameFromHeaders = (contentDisposition: string = ''): string * @param fallbackFilename - Filename to use if none provided in headers * @returns File object */ +type HeaderSource = + | Record + | Headers + | Array<[string, string]> + | undefined + | null; + +function getHeaderValue(headers: HeaderSource, key: string): string | undefined { + if (!headers) return undefined; + if (headers instanceof Headers) { + const value = headers.get(key); + return value === null ? undefined : value; + } + if (Array.isArray(headers)) { + const entry = headers.find(([headerKey]) => headerKey.toLowerCase() === key.toLowerCase()); + return entry?.[1]; + } + const value = headers[key]; + return value === null || value === undefined ? undefined : value; +} + export const createFileFromApiResponse = ( - responseData: any, - headers: any, + responseData: BlobPart, + headers: HeaderSource, fallbackFilename: string ): File => { - const contentType = headers?.['content-type'] || 'application/octet-stream'; - const contentDisposition = headers?.['content-disposition'] || ''; + const contentType = getHeaderValue(headers, 'content-type') || 'application/octet-stream'; + const contentDisposition = getHeaderValue(headers, 'content-disposition') || ''; const filename = getFilenameFromHeaders(contentDisposition) || fallbackFilename; const blob = new Blob([responseData], { type: contentType }); return new File([blob], filename, { type: contentType }); }; + +export function toArrayBuffer(view: ArrayBufferView): ArrayBuffer { + const { buffer, byteOffset, byteLength } = view; + + const start = byteOffset; + const end = byteOffset + byteLength; + + if (typeof SharedArrayBuffer !== 'undefined' && buffer instanceof SharedArrayBuffer) { + const out = new ArrayBuffer(byteLength); + new Uint8Array(out).set(new Uint8Array(buffer, start, byteLength)); + return out; + } + + if (buffer instanceof ArrayBuffer && typeof buffer.slice === 'function') { + return buffer.slice(start, end); + } + + const out = new ArrayBuffer(byteLength); + new Uint8Array(out).set(new Uint8Array(buffer, start, byteLength)); + return out; +} diff --git a/frontend/src/core/utils/thumbnailUtils.ts b/frontend/src/core/utils/thumbnailUtils.ts index 8faec2644..33b66454a 100644 --- a/frontend/src/core/utils/thumbnailUtils.ts +++ b/frontend/src/core/utils/thumbnailUtils.ts @@ -1,4 +1,5 @@ import { pdfWorkerManager } from '@app/services/pdfWorkerManager'; +import type { PDFDocumentProxy, RenderParameters } from 'pdfjs-dist/types/src/display/api'; export interface ThumbnailWithMetadata { thumbnail: string; // Always returns a thumbnail (placeholder if needed) @@ -256,7 +257,7 @@ function drawLargeLockIcon(ctx: CanvasRenderingContext2D, centerX: number, cente /** * Generate standard PDF thumbnail by rendering first page */ -async function generateStandardPDFThumbnail(pdf: any, scale: number): Promise { +async function generateStandardPDFThumbnail(pdf: PDFDocumentProxy, scale: number): Promise { const page = await pdf.getPage(1); const viewport = page.getViewport({ scale }); const canvas = document.createElement("canvas"); @@ -268,7 +269,12 @@ async function generateStandardPDFThumbnail(pdf: any, scale: number): Promise; + app_metadata?: Record; } export interface Session { @@ -46,6 +47,35 @@ export type AuthChangeEvent = type AuthChangeCallback = (event: AuthChangeEvent, session: Session | null) => void; +type OAuthQueryParams = Record; + +interface ErrorResponsePayload { + message?: string; + error?: string; + [key: string]: unknown; +} + +function extractAxiosErrorMessage(error: unknown, fallback: string): string { + if (axios.isAxiosError(error)) { + const data = error.response?.data; + if (data && typeof data === 'object') { + if (typeof data.message === 'string' && data.message) return data.message; + if (typeof data.error === 'string' && data.error) return data.error; + } + if (typeof error.message === 'string' && error.message) { + return error.message; + } + } + if (error instanceof Error) { + return error.message; + } + return fallback; +} + +function getAxiosStatus(error: unknown): number | undefined { + return axios.isAxiosError(error) ? error.response?.status : undefined; +} + class SpringAuthClient { private listeners: AuthChangeCallback[] = []; private sessionCheckInterval: NodeJS.Timeout | null = null; @@ -105,21 +135,22 @@ class SpringAuthClient { console.debug('[SpringAuth] getSession: Session retrieved successfully'); return { data: { session }, error: null }; - } catch (error: any) { + } catch (error: unknown) { console.error('[SpringAuth] getSession error:', error); - // If 401/403, token is invalid - clear it - if (error?.response?.status === 401 || error?.response?.status === 403) { - localStorage.removeItem('stirling_jwt'); - console.debug('[SpringAuth] getSession: Not authenticated'); - return { data: { session: null }, error: null }; + if (axios.isAxiosError(error)) { + const status = error.response?.status; + if (status === 401 || status === 403) { + localStorage.removeItem('stirling_jwt'); + console.debug('[SpringAuth] getSession: Not authenticated'); + return { data: { session: null }, error: null }; + } } - // Clear potentially invalid token on other errors too localStorage.removeItem('stirling_jwt'); return { data: { session: null }, - error: { message: error?.response?.data?.message || error?.message || 'Unknown error' }, + error: { message: extractAxiosErrorMessage(error, 'Unknown error') }, }; } } @@ -157,9 +188,9 @@ class SpringAuthClient { this.notifyListeners('SIGNED_IN', session); return { user: data.user, session, error: null }; - } catch (error: any) { + } catch (error: unknown) { console.error('[SpringAuth] signInWithPassword error:', error); - const errorMessage = error?.response?.data?.error || error?.message || 'Login failed'; + const errorMessage = extractAxiosErrorMessage(error, 'Login failed'); return { user: null, session: null, @@ -189,9 +220,9 @@ class SpringAuthClient { // Note: Spring backend auto-confirms users (no email verification) // Return user but no session (user needs to login) return { user: data.user, session: null, error: null }; - } catch (error: any) { + } catch (error: unknown) { console.error('[SpringAuth] signUp error:', error); - const errorMessage = error?.response?.data?.error || error?.message || 'Registration failed'; + const errorMessage = extractAxiosErrorMessage(error, 'Registration failed'); return { user: null, session: null, @@ -206,7 +237,7 @@ class SpringAuthClient { */ async signInWithOAuth(params: { provider: 'github' | 'google' | 'apple' | 'azure'; - options?: { redirectTo?: string; queryParams?: Record }; + options?: { redirectTo?: string; queryParams?: OAuthQueryParams }; }): Promise<{ error: AuthError | null }> { try { const redirectUrl = `/oauth2/authorization/${params.provider}`; @@ -233,13 +264,11 @@ class SpringAuthClient { }); return { data: {}, error: null }; - } catch (error: any) { + } catch (error: unknown) { console.error('[SpringAuth] resetPasswordForEmail error:', error); return { data: {}, - error: { - message: error?.response?.data?.error || error?.message || 'Password reset failed', - }, + error: { message: extractAxiosErrorMessage(error, 'Password reset failed') }, }; } } @@ -267,12 +296,10 @@ class SpringAuthClient { this.notifyListeners('SIGNED_OUT', null); return { error: null }; - } catch (error: any) { + } catch (error: unknown) { console.error('[SpringAuth] signOut error:', error); return { - error: { - message: error?.response?.data?.error || error?.message || 'Logout failed', - }, + error: { message: extractAxiosErrorMessage(error, 'Logout failed') }, }; } } @@ -306,18 +333,19 @@ class SpringAuthClient { this.notifyListeners('TOKEN_REFRESHED', session); return { data: { session }, error: null }; - } catch (error: any) { + } catch (error: unknown) { console.error('[SpringAuth] refreshSession error:', error); localStorage.removeItem('stirling_jwt'); // Handle different error statuses - if (error?.response?.status === 401 || error?.response?.status === 403) { + const status = getAxiosStatus(error); + if (status === 401 || status === 403) { return { data: { session: null }, error: { message: 'Token refresh failed - please log in again' } }; } return { data: { session: null }, - error: { message: error?.response?.data?.message || error?.message || 'Token refresh failed' }, + error: { message: extractAxiosErrorMessage(error, 'Token refresh failed') }, }; } }