From 49841241beb8071ad22d306e5f4f0905fc3b9dbb Mon Sep 17 00:00:00 2001 From: Mustafa Yildiz Date: Sat, 15 Aug 2026 10:20:54 +0300 Subject: [PATCH] fix: add sanitizePostgresText to filter null bytes and malformed Unicode escape sequences before inserting template text into Supabase --- src/controllers/document.controller.ts | 28 ++++++++++++++++++++------ src/controllers/template.controller.ts | 15 +++++++++----- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/controllers/document.controller.ts b/src/controllers/document.controller.ts index 9a0b62e..e3ecc93 100644 --- a/src/controllers/document.controller.ts +++ b/src/controllers/document.controller.ts @@ -397,6 +397,20 @@ export const ocrImage = async (req: AuthenticatedRequest, res: Response) => { } }; +/** + * PostgreSQL ve Supabase JSON/Text alanlarının reddettiği null byte (\u0000), + * bozuk unicode kaçış dizileri ve kontrol karakterlerini temizler. + */ +export function sanitizePostgresText(text: string | null | undefined): string { + if (!text) return ''; + return String(text) + .replace(/\0/g, '') + .replace(/\\u0000/gi, '') + .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, '') + .replace(/[\uD800-\uDFFF]/g, '') + .replace(/\uFFFD/g, ''); +} + // uploadDocument ve uploadTemplate'in (template.controller.ts) ortak kullandığı // metin çıkarma adımı: resimler için Tesseract OCR, ofis belgeleri için officeParser. export async function extractTextFromUploadedFile(file: { buffer: Buffer; mimetype: string; originalname: string }): Promise { @@ -444,7 +458,7 @@ export async function extractTextFromUploadedFile(file: { buffer: Buffer; mimety .replace(/\s+/g, " ") .trim(); if (plainText.length > 20) { - return plainText; + return sanitizePostgresText(plainText); } } } @@ -455,7 +469,7 @@ export async function extractTextFromUploadedFile(file: { buffer: Buffer; mimety const pdfResult: any = await officeParser.parseOffice(pdfBuffer); const pdfText = typeof pdfResult === 'string' ? pdfResult : (pdfResult?.toText ? pdfResult.toText() : String(pdfResult || '')); if (pdfText && pdfText.trim()) { - return pdfText.trim(); + return sanitizePostgresText(pdfText.trim()); } } } @@ -464,8 +478,9 @@ export async function extractTextFromUploadedFile(file: { buffer: Buffer; mimety const rawText = file.buffer.toString('utf8'); if (rawText && rawText.trim().length > 20) { const plainText = rawText.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim(); - if (plainText.length > 20) { - return plainText; + const cleaned = sanitizePostgresText(plainText); + if (cleaned.length > 20) { + return cleaned; } } @@ -482,14 +497,15 @@ export async function extractTextFromUploadedFile(file: { buffer: Buffer; mimety imageBuffer = await heicConvert({ buffer: file.buffer, format: 'JPEG', quality: 1 }); } const { data: { text } } = await Tesseract.recognize(imageBuffer, 'tur'); - return text; + return sanitizePostgresText(text); } // officeParser natively supports pdf, docx, doc, rtf, txt, etc. // officeparser@7.x parseOffice() bir sonuc objesi donduruyor (duz string degil), // duz metin icin .toText() cagirmak gerekiyor. const result: any = await officeParser.parseOffice(file.buffer); - return typeof result === 'string' ? result : (result?.toText ? result.toText() : String(result || '')); + const rawResult = typeof result === 'string' ? result : (result?.toText ? result.toText() : String(result || '')); + return sanitizePostgresText(rawResult); } // Sohbet ekranlarındaki (genel Sohbet ve dosya bazlı "AyrisLegal'e Sor") ataç diff --git a/src/controllers/template.controller.ts b/src/controllers/template.controller.ts index 8f01b1f..bfb06a4 100644 --- a/src/controllers/template.controller.ts +++ b/src/controllers/template.controller.ts @@ -1,7 +1,7 @@ import { Response } from 'express'; import { supabase } from '../lib/supabase'; import { AuthenticatedRequest } from '../middleware/auth'; -import { extractTextFromUploadedFile } from './document.controller'; +import { extractTextFromUploadedFile, sanitizePostgresText } from './document.controller'; // Belge Şablonları: avukatın büro genelinde kullandığı dilekçe/evrak şablonları. // Ayrı bir Storage bucket açmaya gerek kalmasın diye documents'ın kullandığı @@ -45,17 +45,22 @@ export const uploadTemplate = async (req: AuthenticatedRequest, res: Response) = return res.status(500).json({ error: 'Şablon dosyası yüklenemedi.' }); } + const cleanName = sanitizePostgresText(String(name)).slice(0, 200); + const cleanCategory = category ? sanitizePostgresText(String(category)) : null; + const cleanDescription = description ? sanitizePostgresText(String(description)) : null; + const cleanText = extractedText ? sanitizePostgresText(extractedText) : null; + const { data, error } = await supabase .from('templates') .insert([{ user_id: userId, - category: category || null, - name: String(name).slice(0, 200), - description: description || null, + category: cleanCategory, + name: cleanName, + description: cleanDescription, storage_path: storagePath, file_size: file.size, mime_type: file.mimetype, - extracted_text: extractedText, + extracted_text: cleanText, }]) .select() .single();