From 43dc4feb31acfe976926f89563ff230286eef51d Mon Sep 17 00:00:00 2001 From: ht Date: Sun, 6 Sep 2026 09:14:43 +0300 Subject: [PATCH] fix: validate document uploads and allow clearing expiry dates --- docs/quality/uygulama-durumu.md | 7 +++++++ server/document-metadata.mjs | 29 +++++++++++++++++++++++++++++ server/index.js | 12 ++++++------ server/user-store.mjs | 10 ++++++---- tests/http-state.test.mjs | 14 ++++++++++++++ 5 files changed, 62 insertions(+), 10 deletions(-) create mode 100644 server/document-metadata.mjs diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md index 62ada5f..e086753 100644 --- a/docs/quality/uygulama-durumu.md +++ b/docs/quality/uygulama-durumu.md @@ -192,3 +192,10 @@ SQLite snapshot, referanslı belge kopyası, SHA-256 manifesti, bütünlük kont - Kaynak anlık görüntüsü kullanılmayan analiz listesini de içeriyor; analiz listesi değişimi önceki kaynak onayını geçersizleştiriyor. Fiyatlar seçilen gerçek satır kimlikleriyle eşleniyor. - Bekleyen taslak kaydı sonrasında kapatılmış bileşene hata yazılması da önlendi. - Kanıt: 55/55 test ve üretim derlemesi geçti. Testler iki farklı miktar listesi, açık seçim, sürüm/liste değişimi ve aynı satırların farklı sırada bulunmasını kapsıyor. Gerçek resmi cetvellerin semantik eşleştirmesi ve kaynak seçiminin tarayıcıda uçtan uca kabulü hâlâ açık; farklı isimlendirmeler kullanıcı karşılaştırması gerektirebilir. + +### D01 / D09 — Belge tarihi ve yükleme doğrulaması + +- Son kullanma tarihini kaldırma artık eski tarihi korumuyor: PATCH alanının gönderilmemesi ile açık null gönderilmesi ayrıldı. Kategori değişikliği tarihi, tarih değişikliği kategoriyi koruyor. +- Yükleme ve üst veri güncellemesinde gerçek takvim günü doğrulanıyor; 31 Şubat, artık yıl olmayan 29 Şubat, tarih sonuna eklenen metin ve yanlış türler reddediliyor. +- Dosya aktarımının base64 kodlaması ve 10 MB dosya sınırı doğrulanıyor. Bozuk kodlamanın boş veya kısmen çözülmüş dosya olarak kaydedilmesi önlendi. Bu kontrol PDF/DOCX iç yapısının sağlam olduğunu kanıtlamaz; ayrıştırma/OCR hata senaryoları açık. +- Kanıt: genişletilen HTTP testleriyle 55/55 test geçti; bozuk yükleme ve tarih isteği 400, diğer hesabın tarih silmesi 404, geçerli silme ve kategori koruma doğrulandı. 10 MB tam sınırı ve bir bayt fazlası ayrıca gerçek Buffer ile geçti. Gerçek dosya indirme ve bayt eşitliği regresyonu geçti. Tarayıcıda ardışık tarih güncelleme yarışı ayrıca incelenmeli. diff --git a/server/document-metadata.mjs b/server/document-metadata.mjs new file mode 100644 index 0000000..9e21356 --- /dev/null +++ b/server/document-metadata.mjs @@ -0,0 +1,29 @@ +export function decodeDocumentUpload(value) { + if (typeof value !== 'string' || !value.length || value.length % 4 !== 0 || + !/^[A-Za-z0-9+/]*={0,2}$/.test(value)) + throw new Error('Dosya içeriği geçersiz; dosyayı yeniden seçin.'); + const bytes = Buffer.from(value, 'base64'); + if (!bytes.length || bytes.toString('base64') !== value) throw new Error('Dosya içeriği boş veya bozuk.'); + if (bytes.length > 10 * 1024 * 1024) throw new Error('Dosya en fazla 10 MB olabilir.'); + return bytes; +} + +export function documentMetadataPatch(body) { + const patch = {}; + if (Object.hasOwn(body, 'sonKullanma')) { + const value = body.sonKullanma; + if (value !== null && value !== '') { + if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) + throw new Error('Son kullanma tarihi YYYY-AA-GG biçiminde olmalı.'); + const date = new Date(value + 'T00:00:00Z'); + if (!Number.isFinite(date.getTime()) || date.toISOString().slice(0,10) !== value) + throw new Error('Son kullanma tarihi geçerli bir takvim günü olmalı.'); + } + patch.sonKullanma = value || null; + } + if (Object.hasOwn(body, 'kategori')) { + if (body.kategori !== null && typeof body.kategori !== 'string') throw new Error('Belge kategorisi metin olmalı.'); + patch.kategori = body.kategori?.trim().slice(0,60) || null; + } + return patch; +} diff --git a/server/index.js b/server/index.js index ea57e57..9ce315c 100644 --- a/server/index.js +++ b/server/index.js @@ -1,3 +1,4 @@ +import { documentMetadataPatch, decodeDocumentUpload } from './document-metadata.mjs'; import { deadlineInfo } from '../src/services/tenderCalendar.js'; import { exchangeRates } from './exchange-rates.mjs'; import { uniqueTenders, announcementFields } from './tender-quality.mjs'; @@ -953,7 +954,8 @@ const server = http.createServer(async (req, res) => { if (!body.ad || !body.icerikBase64) { return json(res, 400, { success: false, error: 'ad ve icerikBase64 gerekli' }); } - const buf = Buffer.from(body.icerikBase64, 'base64'); + const metadata = documentMetadataPatch(body); + const buf = decodeDocumentUpload(body.icerikBase64); const kullanimDurumu = store.documentUsage(user.email); kotaKontrol(user.email, 'belgeAdet', kullanimDurumu.adet, 1, opts); kotaKontrol(user.email, 'belgeBayt', kullanimDurumu.bayt, buf.length, opts); @@ -971,11 +973,11 @@ const server = http.createServer(async (req, res) => { fs.writeFileSync(path.join(store.BELGE_DIR, dosyaAdi), buf); const id = store.addDocument(user.email, { ad: String(body.ad).slice(0, 200), - kategori: body.kategori || null, + kategori: metadata.kategori || null, boyut: buf.length, // GERÇEK bayt — rastgele değil mime: body.mime || null, dosya: dosyaAdi, - sonKullanma: body.sonKullanma || null, + sonKullanma: metadata.sonKullanma || null, ikn, // ihaleye bağlı belge }); // Yükleme anında otomatik kalem çıkarımı — AI kullanmaz, kullanıcıdan @@ -1037,9 +1039,7 @@ const server = http.createServer(async (req, res) => { try { const body = await readBody(req); if (!body.id) return json(res, 400, { success: false, error: 'id gerekli' }); - const sonKullanma = body.sonKullanma ? String(body.sonKullanma).slice(0, 10) : null; - if (sonKullanma && !/^\d{4}-\d{2}-\d{2}$/.test(sonKullanma)) return json(res, 400, { success: false, error: 'tarih YYYY-AA-GG olmalı' }); - const ok = store.updateDocument(user.email, String(body.id), { sonKullanma, kategori: body.kategori ? String(body.kategori).slice(0, 60) : null }); + const ok = store.updateDocument(user.email, String(body.id), documentMetadataPatch(body)); if (!ok) return json(res, 404, { success: false, error: 'belge bulunamadı' }); return json(res, 200, { success: true, belgeler: store.listDocuments(user.email) }); } catch (e) { return json(res, 400, { success: false, error: e.message }); } diff --git a/server/user-store.mjs b/server/user-store.mjs index c61243b..d2a0e1e 100644 --- a/server/user-store.mjs +++ b/server/user-store.mjs @@ -262,12 +262,14 @@ export function getDocument(email, id) { } /** Belge üst verisi: son kullanma / kategori (süre takibi için). */ -export function updateDocument(email, id, { sonKullanma, kategori } = {}) { +export function updateDocument(email, id, patch = {}) { return withDb((db) => { + const hasDate = Object.hasOwn(patch,'sonKullanma'); + const hasCategory = Object.hasOwn(patch,'kategori'); const r = db.prepare(`UPDATE belgeler SET - son_kullanma = COALESCE(?, son_kullanma), - kategori = COALESCE(?, kategori) - WHERE id=? AND email=?`).run(sonKullanma ?? null, kategori ?? null, id, email); + son_kullanma = CASE WHEN ? THEN ? ELSE son_kullanma END, + kategori = CASE WHEN ? THEN ? ELSE kategori END + WHERE id=? AND email=?`).run(Number(hasDate), patch.sonKullanma ?? null, Number(hasCategory), patch.kategori ?? null, id, email); return r.changes > 0; }, false); } diff --git a/tests/http-state.test.mjs b/tests/http-state.test.mjs index 9ecf841..0342011 100644 --- a/tests/http-state.test.mjs +++ b/tests/http-state.test.mjs @@ -42,6 +42,9 @@ test('HTTP documents and API keys expose empty successful state, not endless loa test('HTTP file upload, owned download, metadata update and deletion operate on actual bytes',async()=>{ const bytes=Buffer.from('QA belge — Türkçe içerik, yalnız izole test.'); + for (const invalid of ['!!!!','a','a===','AB==',[],{}]) { + assert.equal((await request('/api/me/documents','POST',{ad:'broken.txt',icerikBase64:invalid})).status,400); + } const uploaded=await request('/api/me/documents','POST',{ad:'qa.txt',mime:'text/plain',icerikBase64:bytes.toString('base64'),kategori:'Firma',sonKullanma:'2026-12-31'}); assert.equal(uploaded.status,200); const id=uploaded.data.id; @@ -52,6 +55,17 @@ test('HTTP file upload, owned download, metadata update and deletion operate on assert.equal((await request('/api/me/documents/download?id='+id,'GET',null,other)).status,404); assert.equal((await request('/api/me/documents/meta','PATCH',{id,sonKullanma:'2027-01-01'})).status,200); const changed=await request('/api/me/documents');assert.equal(changed.data.belgeler.find(f=>f.id===id).son_kullanma,'2027-01-01'); + for (const invalid of ['2027-02-29','2026-02-31','2026-12-31suffix',42]) { + assert.equal((await request('/api/me/documents/meta','PATCH',{id,sonKullanma:invalid})).status,400); + assert.equal((await request('/api/me/documents','POST',{ad:'invalid.txt',icerikBase64:bytes.toString('base64'),sonKullanma:invalid})).status,400); + } + const categoryOnly=await request('/api/me/documents/meta','PATCH',{id,kategori:'Yeterlik'}); + assert.equal(categoryOnly.data.belgeler.find(f=>f.id===id).son_kullanma,'2027-01-01'); + assert.equal((await request('/api/me/documents/meta','PATCH',{id,sonKullanma:null},other)).status,404); + const cleared=await request('/api/me/documents/meta','PATCH',{id,sonKullanma:null}); + assert.equal(cleared.data.belgeler.find(f=>f.id===id).son_kullanma,null); + assert.equal(cleared.data.belgeler.find(f=>f.id===id).kategori,'Yeterlik'); + assert.equal((await request('/api/me/documents')).data.belgeler.length,1); assert.equal((await request('/api/me/documents?id='+id,'DELETE')).status,200); assert.equal((await request('/api/me/documents/download?id='+id)).status,404); });