fix: validate document uploads and allow clearing expiry dates

This commit is contained in:
2026-09-06 09:14:43 +03:00
parent 0d2cf584e7
commit 43dc4feb31
5 changed files with 62 additions and 10 deletions
+7
View File
@@ -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. - 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. - 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. - 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.
+29
View File
@@ -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;
}
+6 -6
View File
@@ -1,3 +1,4 @@
import { documentMetadataPatch, decodeDocumentUpload } from './document-metadata.mjs';
import { deadlineInfo } from '../src/services/tenderCalendar.js'; import { deadlineInfo } from '../src/services/tenderCalendar.js';
import { exchangeRates } from './exchange-rates.mjs'; import { exchangeRates } from './exchange-rates.mjs';
import { uniqueTenders, announcementFields } from './tender-quality.mjs'; import { uniqueTenders, announcementFields } from './tender-quality.mjs';
@@ -953,7 +954,8 @@ const server = http.createServer(async (req, res) => {
if (!body.ad || !body.icerikBase64) { if (!body.ad || !body.icerikBase64) {
return json(res, 400, { success: false, error: 'ad ve icerikBase64 gerekli' }); 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); const kullanimDurumu = store.documentUsage(user.email);
kotaKontrol(user.email, 'belgeAdet', kullanimDurumu.adet, 1, opts); kotaKontrol(user.email, 'belgeAdet', kullanimDurumu.adet, 1, opts);
kotaKontrol(user.email, 'belgeBayt', kullanimDurumu.bayt, buf.length, 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); fs.writeFileSync(path.join(store.BELGE_DIR, dosyaAdi), buf);
const id = store.addDocument(user.email, { const id = store.addDocument(user.email, {
ad: String(body.ad).slice(0, 200), ad: String(body.ad).slice(0, 200),
kategori: body.kategori || null, kategori: metadata.kategori || null,
boyut: buf.length, // GERÇEK bayt — rastgele değil boyut: buf.length, // GERÇEK bayt — rastgele değil
mime: body.mime || null, mime: body.mime || null,
dosya: dosyaAdi, dosya: dosyaAdi,
sonKullanma: body.sonKullanma || null, sonKullanma: metadata.sonKullanma || null,
ikn, // ihaleye bağlı belge ikn, // ihaleye bağlı belge
}); });
// Yükleme anında otomatik kalem çıkarımı — AI kullanmaz, kullanıcıdan // 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 { try {
const body = await readBody(req); const body = await readBody(req);
if (!body.id) return json(res, 400, { success: false, error: 'id gerekli' }); if (!body.id) return json(res, 400, { success: false, error: 'id gerekli' });
const sonKullanma = body.sonKullanma ? String(body.sonKullanma).slice(0, 10) : null; const ok = store.updateDocument(user.email, String(body.id), documentMetadataPatch(body));
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 });
if (!ok) return json(res, 404, { success: false, error: 'belge bulunamadı' }); if (!ok) return json(res, 404, { success: false, error: 'belge bulunamadı' });
return json(res, 200, { success: true, belgeler: store.listDocuments(user.email) }); return json(res, 200, { success: true, belgeler: store.listDocuments(user.email) });
} catch (e) { return json(res, 400, { success: false, error: e.message }); } } catch (e) { return json(res, 400, { success: false, error: e.message }); }
+6 -4
View File
@@ -262,12 +262,14 @@ export function getDocument(email, id) {
} }
/** Belge üst verisi: son kullanma / kategori (süre takibi için). */ /** 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) => { return withDb((db) => {
const hasDate = Object.hasOwn(patch,'sonKullanma');
const hasCategory = Object.hasOwn(patch,'kategori');
const r = db.prepare(`UPDATE belgeler SET const r = db.prepare(`UPDATE belgeler SET
son_kullanma = COALESCE(?, son_kullanma), son_kullanma = CASE WHEN ? THEN ? ELSE son_kullanma END,
kategori = COALESCE(?, kategori) kategori = CASE WHEN ? THEN ? ELSE kategori END
WHERE id=? AND email=?`).run(sonKullanma ?? null, kategori ?? null, id, email); WHERE id=? AND email=?`).run(Number(hasDate), patch.sonKullanma ?? null, Number(hasCategory), patch.kategori ?? null, id, email);
return r.changes > 0; return r.changes > 0;
}, false); }, false);
} }
+14
View File
@@ -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()=>{ 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.'); 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'}); 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); assert.equal(uploaded.status,200);
const id=uploaded.data.id; 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/download?id='+id,'GET',null,other)).status,404);
assert.equal((await request('/api/me/documents/meta','PATCH',{id,sonKullanma:'2027-01-01'})).status,200); 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'); 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?id='+id,'DELETE')).status,200);
assert.equal((await request('/api/me/documents/download?id='+id)).status,404); assert.equal((await request('/api/me/documents/download?id='+id)).status,404);
}); });