diff --git a/server/routers.ts b/server/routers.ts index 042d8f5..91a39a9 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -107,6 +107,7 @@ import { drawBapCartouche } from "./bapCartouche"; import { resolveBapExportDestination } from "./exportDestinationResolver"; import { startEmailImportService, stopEmailImportService, isEmailImportServiceRunning, triggerEmailCheck, triggerManualEmailCheck, testImapConnection } from "./emailImportService"; import { startFolderImportService, stopFolderImportService, isFolderImportServiceRunning } from "./folderImportService"; +import { getSafeWebImportSourceError, logWebImportSourceSaveFailure } from "./webImportSourcesError"; import { TRPCError } from "@trpc/server"; import { processFreeproExcel } from "./freeproService"; import { runFreeproAutoImport, testFreeproConnection, startFreeproAutoJob, stopFreeproAutoJob, frequencyToMs } from "./freeproAutoImport"; @@ -2731,7 +2732,14 @@ export const appRouter = router({ autoEnabled: z.number().min(0).max(1).default(0), })) .mutation(async ({ input, ctx }) => { - return createWebImportSource({ ...input, userId: ctx.user.id }); + try { + return await createWebImportSource({ ...input, userId: ctx.user.id }); + } catch (error) { + // Les erreurs MySQL/Drizzle peuvent contenir la requête et les paramètres saisis. + // Elles restent masquées côté navigateur. + logWebImportSourceSaveFailure(error); + throw getSafeWebImportSourceError(error); + } }), update: protectedProcedure diff --git a/server/webImportSourcesError.test.ts b/server/webImportSourcesError.test.ts new file mode 100644 index 0000000..347f5a6 --- /dev/null +++ b/server/webImportSourcesError.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "vitest"; +import { WEB_IMPORT_SOURCE_SAVE_ERROR, getSafeWebImportSourceError } from "./webImportSourcesError"; + +describe("getSafeWebImportSourceError", () => { + it("never returns the SQL statement or connector parameters to the client", () => { + const databaseError = new Error( + "Failed query: insert into `webImportSources` (...) values (...) params: amazon@example.fr, secret-value", + ); + + const safeError = getSafeWebImportSourceError(databaseError); + + expect(safeError.code).toBe("INTERNAL_SERVER_ERROR"); + expect(safeError.message).toBe(WEB_IMPORT_SOURCE_SAVE_ERROR); + expect(safeError.message).not.toContain("insert into"); + expect(safeError.message).not.toContain("secret-value"); + }); +}); diff --git a/server/webImportSourcesError.ts b/server/webImportSourcesError.ts new file mode 100644 index 0000000..907846a --- /dev/null +++ b/server/webImportSourcesError.ts @@ -0,0 +1,25 @@ +import { TRPCError } from "@trpc/server"; + +export const WEB_IMPORT_SOURCE_SAVE_ERROR = + "Impossible d’enregistrer le connecteur. Vérifiez les informations saisies puis réessayez."; + +/** + * Masque toute erreur Drizzle/MySQL avant son retour au navigateur : les erreurs + * de pilote peuvent contenir la requête SQL et les paramètres de connexion. + */ +export function getSafeWebImportSourceError(_error: unknown): TRPCError { + return new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: WEB_IMPORT_SOURCE_SAVE_ERROR, + }); +} + +/** Journalise uniquement les métadonnées techniques non sensibles. */ +export function logWebImportSourceSaveFailure(error: unknown): void { + const technicalCode = + error && typeof error === "object" && "code" in error && typeof error.code === "string" + ? error.code + : "unknown"; + + console.error("[WebImportSources] Échec d’enregistrement", { technicalCode }); +}