Keep vault state and file transfers within their originating session
This commit is contained in:
@@ -313,3 +313,9 @@ Doğrulama: üretim derlemesi geçti; HTTP testi, başarısız çıkarım ve yen
|
|||||||
Doküman Deposu'nda İKN bağlantılı, okunabilir ihale belgelerine yeniden kalem çıkarımı eklendi. İhale detayı ve depo artık aynı storedDocument servisiyle mevcut dosyayı okuyor, okuma hatasını kaydediyor ve sahiplik kontrollü kalem aktarımı yapıyor. Özel firma belgeleri bu işleme dahil değil. İşlem sonucu ve liste yenileme hatası ayrı gösteriliyor. Belge okuma sırasında oturum değişirse eski belge metninin yeni oturumla aktarımı veya hata bildirimi durduruluyor.
|
Doküman Deposu'nda İKN bağlantılı, okunabilir ihale belgelerine yeniden kalem çıkarımı eklendi. İhale detayı ve depo artık aynı storedDocument servisiyle mevcut dosyayı okuyor, okuma hatasını kaydediyor ve sahiplik kontrollü kalem aktarımı yapıyor. Özel firma belgeleri bu işleme dahil değil. İşlem sonucu ve liste yenileme hatası ayrı gösteriliyor. Belge okuma sırasında oturum değişirse eski belge metninin yeni oturumla aktarımı veya hata bildirimi durduruluyor.
|
||||||
|
|
||||||
71 test geçti; üretim derlemesi geçti. Yeni servis testi mevcut dosya ID/İKN/metin aktarımını, okuma hatasının önceki durumla raporlanmasını ve hesap değişikliğinde ikinci isteğin gönderilmemesini doğruluyor. Gerçek tarayıcı PDF kabulü ve tüm depo işlemlerinin hesap değişiminde ekran durumlarını temizlemesi henüz açık.
|
71 test geçti; üretim derlemesi geçti. Yeni servis testi mevcut dosya ID/İKN/metin aktarımını, okuma hatasının önceki durumla raporlanmasını ve hesap değişikliğinde ikinci isteğin gönderilmemesini doğruluyor. Gerçek tarayıcı PDF kabulü ve tüm depo işlemlerinin hesap değişiminde ekran durumlarını temizlemesi henüz açık.
|
||||||
|
|
||||||
|
### Depo oturum ayrımı ve dosya aktarımı
|
||||||
|
|
||||||
|
Doküman Deposu hesap kimliğine göre yeniden oluşturuluyor; eski bileşenden veya değişmiş token'dan gelen sonuçlar durum güncellemesine uygulanmıyor. Toplu yüklemenin sonraki dosyası ve kalem aktarımı öncesinde oturum kontrolü var. Ortak yükleme servisi FileReader tamamlandığında token değişmişse istek göndermiyor; ortak indirme servisi blob tamamlandığında token değişmişse tarayıcı indirmesi başlatmıyor. Bu kontroller sunucunun sahiplik kontrollerine ek olarak istemcide eski oturum işinin devamını sınırlıyor.
|
||||||
|
|
||||||
|
72 test ve üretim derlemesi geçti. Yeni test, FileReader sırasında hesap değiştiğinde sıfır yükleme isteği gönderildiğini ve blob okuma sırasında hesap değiştiğinde indirmenin reddedildiğini doğruluyor. Gerçek tarayıcıda çok dosyalı yükleme/hesap değişimi kabulü, aynı hesapta eşzamanlı metadata yanıt sıralaması ve diğer modüllerin oturum denetimi halen açık.
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { getToken } from '../../services/auth';
|
||||||
|
import { sessionGuard } from '../../services/sessionGuard';
|
||||||
import { readableDocument, retryDocumentExtraction } from '../../services/storedDocument';
|
import { readableDocument, retryDocumentExtraction } from '../../services/storedDocument';
|
||||||
import { documentProcessingLabel } from '../../services/documentProcessing';
|
import { documentProcessingLabel } from '../../services/documentProcessing';
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { useApp } from '../../context/AppContext';
|
import { useApp } from '../../context/AppContext';
|
||||||
import { listDocuments, uploadDocument, deleteDocument, downloadDocument, formatBayt, updateDocumentMeta } from '../../services/account';
|
import { listDocuments, uploadDocument, deleteDocument, downloadDocument, formatBayt, updateDocumentMeta } from '../../services/account';
|
||||||
import { FolderKanban, Upload, Trash2, Download, Loader2, AlertTriangle, FileText, HardDrive, Layers, Package, CheckCircle2, XCircle, CalendarClock } from 'lucide-react';
|
import { FolderKanban, Upload, Trash2, Download, Loader2, AlertTriangle, FileText, HardDrive, Layers, Package, CheckCircle2, XCircle, CalendarClock } from 'lucide-react';
|
||||||
@@ -14,28 +16,42 @@ const KATEGORILER = ['Yasal Belgeler', 'Kalite Belgeleri', 'Mali Belgeler', 'Res
|
|||||||
*/
|
*/
|
||||||
export const DocumentVault = () => {
|
export const DocumentVault = () => {
|
||||||
const { authUser } = useApp();
|
const { authUser } = useApp();
|
||||||
const [durum, setDurum] = useState(null);
|
return <DocumentVaultSession key={authUser?.email || 'signed-out'} signedIn={Boolean(authUser)} />;
|
||||||
const [busy, setBusy] = useState(false);
|
};
|
||||||
const [hata, setHata] = useState(null);
|
|
||||||
const [islemeBilgi, setIslemeBilgi] = useState(null);
|
const DocumentVaultSession = ({signedIn}) => {
|
||||||
const [kategori, setKategori] = useState(KATEGORILER[0]);
|
const live = useRef(true);
|
||||||
|
const session = useRef(sessionGuard(getToken));
|
||||||
|
const current = () => live.current && session.current();
|
||||||
|
useEffect(() => { live.current = true; return () => { live.current = false; }; }, []);
|
||||||
|
const useOwnedState = (initial) => {
|
||||||
|
const [value, setValue] = useState(initial);
|
||||||
|
const setOwned = useCallback((next) => { if (current()) setValue(next); }, []);
|
||||||
|
return [value, setOwned];
|
||||||
|
};
|
||||||
|
const [durum, setDurum] = useOwnedState(null);
|
||||||
|
const [busy, setBusy] = useOwnedState(false);
|
||||||
|
const [hata, setHata] = useOwnedState(null);
|
||||||
|
const [islemeBilgi, setIslemeBilgi] = useOwnedState(null);
|
||||||
|
const [kategori, setKategori] = useOwnedState(KATEGORILER[0]);
|
||||||
const dosyaRef = useRef(null);
|
const dosyaRef = useRef(null);
|
||||||
|
|
||||||
const yukle = () => {
|
const yukle = () => {
|
||||||
if (!authUser) return;
|
if (!signedIn || !current()) return;
|
||||||
setHata(null);
|
setHata(null);
|
||||||
listDocuments().then(setDurum).catch((e) => setHata(e.message));
|
listDocuments().then(setDurum).catch((e) => setHata(e.message));
|
||||||
};
|
};
|
||||||
useEffect(yukle, [authUser]);
|
useEffect(yukle, [signedIn]);
|
||||||
|
|
||||||
const yenidenIsle = async (document) => {
|
const yenidenIsle = async (document) => {
|
||||||
|
if (!current()) return;
|
||||||
setBusy(true); setHata(null); setIslemeBilgi(null);
|
setBusy(true); setHata(null); setIslemeBilgi(null);
|
||||||
try {
|
try {
|
||||||
const result = await retryDocumentExtraction(document);
|
const result = await retryDocumentExtraction(document);
|
||||||
setIslemeBilgi(`${document.ad}: ${result.sayi || 0} kalem çıkarıldı.`);
|
setIslemeBilgi(`${document.ad}: ${result.sayi || 0} kalem çıkarıldı.`);
|
||||||
} catch (error) { setHata(error.message); }
|
} catch (error) { setHata(error.message); }
|
||||||
finally {
|
finally {
|
||||||
try { setDurum(await listDocuments()); }
|
try { if (current()) setDurum(await listDocuments()); }
|
||||||
catch { setHata(previous => [previous, 'Belge listesi yenilenemedi.'].filter(Boolean).join(' ')); }
|
catch { setHata(previous => [previous, 'Belge listesi yenilenemedi.'].filter(Boolean).join(' ')); }
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
@@ -59,7 +75,7 @@ export const DocumentVault = () => {
|
|||||||
// sorulmaz; kalemler otomatik havuza yazılır. docx/xlsx/zip sunucuda,
|
// sorulmaz; kalemler otomatik havuza yazılır. docx/xlsx/zip sunucuda,
|
||||||
// PDF tarayıcıda (pdfjs) okunur. Onlarca şartnameyi tek seferde atmak için.
|
// PDF tarayıcıda (pdfjs) okunur. Onlarca şartnameyi tek seferde atmak için.
|
||||||
const topluRef = useRef(null);
|
const topluRef = useRef(null);
|
||||||
const [toplu, setToplu] = useState([]); // [{ad, durum, ikn, kalem, hata}]
|
const [toplu, setToplu] = useOwnedState([]); // [{ad, durum, ikn, kalem, hata}]
|
||||||
const topluSecildi = async (e) => {
|
const topluSecildi = async (e) => {
|
||||||
const dosyalar = [...(e.target.files || [])];
|
const dosyalar = [...(e.target.files || [])];
|
||||||
if (!dosyalar.length) return;
|
if (!dosyalar.length) return;
|
||||||
@@ -67,6 +83,7 @@ export const DocumentVault = () => {
|
|||||||
setToplu(dosyalar.map((f) => ({ ad: f.name, durum: 'bekliyor' })));
|
setToplu(dosyalar.map((f) => ({ ad: f.name, durum: 'bekliyor' })));
|
||||||
const guncelle = (i, veri) => setToplu((o) => o.map((x, j) => (j === i ? { ...x, ...veri } : x)));
|
const guncelle = (i, veri) => setToplu((o) => o.map((x, j) => (j === i ? { ...x, ...veri } : x)));
|
||||||
for (let i = 0; i < dosyalar.length; i++) {
|
for (let i = 0; i < dosyalar.length; i++) {
|
||||||
|
if (!current()) break;
|
||||||
const f = dosyalar[i];
|
const f = dosyalar[i];
|
||||||
guncelle(i, { durum: 'yukleniyor' });
|
guncelle(i, { durum: 'yukleniyor' });
|
||||||
try {
|
try {
|
||||||
@@ -80,19 +97,22 @@ export const DocumentVault = () => {
|
|||||||
metin = (await extractText(f)).text;
|
metin = (await extractText(f)).text;
|
||||||
ikn = iknCikar(metin);
|
ikn = iknCikar(metin);
|
||||||
}
|
}
|
||||||
|
if (!current()) break;
|
||||||
const cevap = await uploadDocument(f, 'İhale Dokümanı', ikn, { otoIkn: true, documentType:'ihale' });
|
const cevap = await uploadDocument(f, 'İhale Dokümanı', ikn, { otoIkn: true, documentType:'ihale' });
|
||||||
|
if (!current()) break;
|
||||||
ikn = cevap?.ikn || ikn;
|
ikn = cevap?.ikn || ikn;
|
||||||
let kalem = cevap?.kalem?.sayi || null;
|
let kalem = cevap?.kalem?.sayi || null;
|
||||||
let kalemHata = cevap.processing?.status === 'error' ? cevap.processing.error : null;
|
let kalemHata = cevap.processing?.status === 'error' ? cevap.processing.error : null;
|
||||||
if (ikn && !kalem && metin) {
|
if (ikn && !kalem && metin) {
|
||||||
try {
|
try {
|
||||||
const { kalemCikar } = await import('../../services/kalem');
|
const { kalemCikar } = await import('../../services/kalem');
|
||||||
|
if (!current()) break;
|
||||||
kalem = (await kalemCikar({ ikn, metin, dosya: f.name, documentId:cevap.id })).sayi || null;
|
kalem = (await kalemCikar({ ikn, metin, dosya: f.name, documentId:cevap.id })).sayi || null;
|
||||||
} catch (error) { kalemHata = error.message; }
|
} catch (error) { kalemHata = error.message; }
|
||||||
}
|
}
|
||||||
guncelle(i, { durum: 'tamam', ikn, kalem, kalemHata });
|
guncelle(i, { durum: 'tamam', ikn, kalem, kalemHata });
|
||||||
setDurum(cevap);
|
setDurum(cevap);
|
||||||
try { setDurum(await listDocuments()); }
|
try { if (current()) setDurum(await listDocuments()); }
|
||||||
catch { setHata('Dosya kayıtlı; güncel belge listesi alınamadı. Listeyi yeniden yükleyin.'); }
|
catch { setHata('Dosya kayıtlı; güncel belge listesi alınamadı. Listeyi yeniden yükleyin.'); }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
guncelle(i, { durum: 'hata', hata: err.message });
|
guncelle(i, { durum: 'hata', hata: err.message });
|
||||||
@@ -105,6 +125,7 @@ export const DocumentVault = () => {
|
|||||||
// Şirket belgesi süre takibi: imza sirküleri, oda kayıt, borcu yoktur —
|
// Şirket belgesi süre takibi: imza sirküleri, oda kayıt, borcu yoktur —
|
||||||
// hepsinin süresi dolar, kimse teklif hazırlanana kadar fark etmez.
|
// hepsinin süresi dolar, kimse teklif hazırlanana kadar fark etmez.
|
||||||
const sureYaz = async (id, tarih) => {
|
const sureYaz = async (id, tarih) => {
|
||||||
|
if (!current()) return;
|
||||||
setHata(null);
|
setHata(null);
|
||||||
try { setDurum(await updateDocumentMeta(id, { sonKullanma: tarih || null })); }
|
try { setDurum(await updateDocumentMeta(id, { sonKullanma: tarih || null })); }
|
||||||
catch (e) { setHata(e.message); }
|
catch (e) { setHata(e.message); }
|
||||||
@@ -114,6 +135,7 @@ export const DocumentVault = () => {
|
|||||||
const yakinda = (durum?.belgeler || []).filter((b) => kalanGun(b) != null && kalanGun(b) >= 0 && kalanGun(b) <= 30);
|
const yakinda = (durum?.belgeler || []).filter((b) => kalanGun(b) != null && kalanGun(b) >= 0 && kalanGun(b) <= 30);
|
||||||
|
|
||||||
const sil = async (id) => {
|
const sil = async (id) => {
|
||||||
|
if (!current()) return;
|
||||||
setBusy(true); setHata(null);
|
setBusy(true); setHata(null);
|
||||||
try { setDurum(await deleteDocument(id)); }
|
try { setDurum(await deleteDocument(id)); }
|
||||||
catch (e) { setHata(e.message); }
|
catch (e) { setHata(e.message); }
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { sessionGuard } from './sessionGuard.js';
|
||||||
/**
|
/**
|
||||||
* Sunucu tarafı hesap servisleri: plan/kota, kullanıcı verisi, API anahtarları, belgeler.
|
* Sunucu tarafı hesap servisleri: plan/kota, kullanıcı verisi, API anahtarları, belgeler.
|
||||||
* (Önceden bunların hepsi localStorage'daydı veya sahteydi.)
|
* (Önceden bunların hepsi localStorage'daydı veya sahteydi.)
|
||||||
@@ -45,12 +46,14 @@ export const listDocuments = () => iste('/api/me/documents');
|
|||||||
export const deleteDocument = (id) => iste(`/api/me/documents?id=${encodeURIComponent(id)}`, { method: 'DELETE' });
|
export const deleteDocument = (id) => iste(`/api/me/documents?id=${encodeURIComponent(id)}`, { method: 'DELETE' });
|
||||||
|
|
||||||
export async function uploadDocument(file, kategori, ikn, ek = {}) {
|
export async function uploadDocument(file, kategori, ikn, ek = {}) {
|
||||||
|
const current = sessionGuard(getToken);
|
||||||
const icerikBase64 = await new Promise((resolve, reject) => {
|
const icerikBase64 = await new Promise((resolve, reject) => {
|
||||||
const r = new FileReader();
|
const r = new FileReader();
|
||||||
r.onload = () => resolve(String(r.result).split(',')[1]);
|
r.onload = () => resolve(String(r.result).split(',')[1]);
|
||||||
r.onerror = reject;
|
r.onerror = reject;
|
||||||
r.readAsDataURL(file);
|
r.readAsDataURL(file);
|
||||||
});
|
});
|
||||||
|
if (!current()) throw new Error('Oturum değişti; dosyayı yeniden seçin.');
|
||||||
return iste('/api/me/documents', {
|
return iste('/api/me/documents', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ ad: file.name, mime: file.type, kategori, ikn, icerikBase64, ...ek }),
|
body: JSON.stringify({ ad: file.name, mime: file.type, kategori, ikn, icerikBase64, ...ek }),
|
||||||
@@ -59,6 +62,7 @@ export async function uploadDocument(file, kategori, ikn, ek = {}) {
|
|||||||
|
|
||||||
/** Auth başlığı gerektiği için <a href> yerine blob ile indirilir. */
|
/** Auth başlığı gerektiği için <a href> yerine blob ile indirilir. */
|
||||||
export async function downloadDocument(id, ad) {
|
export async function downloadDocument(id, ad) {
|
||||||
|
const current = sessionGuard(getToken);
|
||||||
const res = await fetch(`/api/me/documents/download?id=${encodeURIComponent(id)}`, {
|
const res = await fetch(`/api/me/documents/download?id=${encodeURIComponent(id)}`, {
|
||||||
headers: { Authorization: `Bearer ${getToken()}` },
|
headers: { Authorization: `Bearer ${getToken()}` },
|
||||||
});
|
});
|
||||||
@@ -67,6 +71,7 @@ export async function downloadDocument(id, ad) {
|
|||||||
throw new Error(d.error || `İndirilemedi (HTTP ${res.status})`);
|
throw new Error(d.error || `İndirilemedi (HTTP ${res.status})`);
|
||||||
}
|
}
|
||||||
const blob = await res.blob();
|
const blob = await res.blob();
|
||||||
|
if (!current()) throw new Error('Oturum değişti; belgeyi yeniden açın.');
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/** Snapshot of an authenticated operation; never continue it under another token. */
|
||||||
|
export function sessionGuard(readToken) {
|
||||||
|
const token = readToken();
|
||||||
|
return () => Boolean(token) && readToken() === token;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import {test} from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {uploadDocument,downloadDocument} from '../src/services/account.js';
|
||||||
|
|
||||||
|
test('account switch during file reading prevents upload and download delivery',async()=>{
|
||||||
|
const storage=Object.getOwnPropertyDescriptor(globalThis,'localStorage');
|
||||||
|
const originalFetch=globalThis.fetch, originalReader=globalThis.FileReader;
|
||||||
|
let token='first', calls=0;
|
||||||
|
Object.defineProperty(globalThis,'localStorage',{configurable:true,value:{getItem:()=>token}});
|
||||||
|
try {
|
||||||
|
globalThis.FileReader=class {
|
||||||
|
readAsDataURL(){token='second';this.result='data:text/plain;base64,VGVzdA==';this.onload();}
|
||||||
|
};
|
||||||
|
globalThis.fetch=async()=>{calls++;return Response.json({success:true});};
|
||||||
|
await assert.rejects(uploadDocument({name:'test.txt',type:'text/plain'},'Test'),/Oturum değişti/);
|
||||||
|
assert.equal(calls,0);
|
||||||
|
token='first';
|
||||||
|
globalThis.fetch=async()=>({ok:true,blob:async()=>{token='second';return new Blob(['private document']);}});
|
||||||
|
await assert.rejects(downloadDocument('id','test.txt'),/Oturum değişti/);
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch=originalFetch;globalThis.FileReader=originalReader;
|
||||||
|
if(storage) Object.defineProperty(globalThis,'localStorage',storage);else delete globalThis.localStorage;
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user