fix: isolate account state and ignore stale session responses

This commit is contained in:
2026-09-06 08:46:05 +03:00
parent 05c7e7ecdc
commit a291b186df
5 changed files with 109 additions and 98 deletions
+8 -1
View File
@@ -49,9 +49,16 @@ Tarayıcı kanıtı: [Yenileme sonrası beş kart](evidence/qa-kanban-after-relo
- Derin rapor ve hazırlık formu, açılışta seçilmiş eski nesne yerine ihale havuzundaki güncel kullanıcı kaydını okur. Böylece AI penceresinden tekrar açılan formda yeni kaydedilmiş taslağa erişilir. - Derin rapor ve hazırlık formu, açılışta seçilmiş eski nesne yerine ihale havuzundaki güncel kullanıcı kaydını okur. Böylece AI penceresinden tekrar açılan formda yeni kaydedilmiş taslağa erişilir.
- HTTP testinde yol haritası kaynak/durum alanlarının yazma-okuma kontrolü eklendi. Arayüzde iki pencere arasında geçiş ve yeniden giriş senaryosu hâlâ ayrıca doğrulanmalıdır. - HTTP testinde yol haritası kaynak/durum alanlarının yazma-okuma kontrolü eklendi. Arayüzde iki pencere arasında geçiş ve yeniden giriş senaryosu hâlâ ayrıca doğrulanmalıdır.
## Hesap ve oturum ayrımı
- Firma profili, ekip, favoriler ve filtreler ortak localStorage kayıtlarından başlatılmıyor; yeni hesaba boş varsayılanlar ve ardından sunucudaki kendi kaydı yükleniyor. Önceki hesaba ait eksik alanlar yeni profille birleştirilmiyor.
- Ortak ihale önbelleği yalnız kamuya açık ihale alanlarını tutar; not, checklist, takip ve teklif taslağı çıkarılır. Eski önbellek okunurken de bu alanlar temizlenir. Eski yerel kayıtlar otomatik başka hesaba aktarılmaz.
- Oturum geri yükleme, kayıt/hata, plan, analiz, firma taraması ve bildirim yanıtları oturum değişmişse yeni hesaba uygulanmaz. Başka sekmedeki oturum değişikliği dinlenir. Ziyaret ve belge uyarısı anahtarları hesaba ayrılır.
- İki regresyon testi eski oturum yanıtının yeni hesabı silememesini/değiştirememesini ve ortak ihale önbelleğinin özel hazırlık durumunu taşımamasını doğrular. Tarayıcıda iki gerçek test hesabı ve offline senaryosu ayrıca kabul edilmeli. Bu değişiklik, özel profil verisinin offline erişimini sunucu senkronu tamamlanana kadar sınırlar.
## Doğrulama ## Doğrulama
`npm test`: 25/25 geçti (son yeniden çalıştırma: 6 Eylül 2026). `npm test`: 27/27 geçti (son yeniden çalıştırma: 6 Eylül 2026).
`npm run build`: geçti; mevcut büyük paket/dinamik import uyarıları sürüyor. `npm run build`: geçti; mevcut büyük paket/dinamik import uyarıları sürüyor.
`node --check server/index.js`, `node --check server/user-store.mjs`, `git diff --check`: geçti. `node --check server/index.js`, `node --check server/user-store.mjs`, `git diff --check`: geçti.
+62 -97
View File
@@ -5,7 +5,7 @@ import { getToken } from '../services/auth';
import { fetchLiveTenders, fetchTenderDetail, mergeUserState } from '../services/tenderApi'; import { fetchLiveTenders, fetchTenderDetail, mergeUserState } from '../services/tenderApi';
import { restoreSession, clearSession, getStoredUser } from '../services/auth'; import { restoreSession, clearSession, getStoredUser } from '../services/auth';
import { getAllUserData, setUserData, getPlan, bildirimleriGetir, bildirimOkundu, listDocuments } from '../services/account'; import { getAllUserData, setUserData, getPlan, bildirimleriGetir, bildirimOkundu, listDocuments } from '../services/account';
import { applyTenderStates, tenderStateKey, tenderSnapshot } from '../services/tenderState'; import { applyTenderStates, tenderStateKey, tenderSnapshot, publicTender } from '../services/tenderState';
import { getTenderStates, saveTenderState } from '../services/account'; import { getTenderStates, saveTenderState } from '../services/account';
import { SECTORS_LIST, INSTITUTION_TYPES_LIST } from '../data/constants'; import { SECTORS_LIST, INSTITUTION_TYPES_LIST } from '../data/constants';
@@ -25,16 +25,13 @@ const yerelKaydet = (anahtar, deger) => {
return false; return false;
} }
}; };
const yerelOku = (anahtar, varsayilan) => { const defaultReminder = () => ({emailEnabled:true,smsEnabled:true,pushEnabled:true,
try { notify3DaysBefore:true,notify1DayBefore:true,notify3HoursBefore:true,
const ham = localStorage.getItem(anahtar); googleCalendarSync:true,outlookSync:false,quietEnabled:false,quietStart:'22:00',quietEnd:'08:00'});
if (!ham) return varsayilan; const defaultProfile = user => ({name:user?.name || user?.email || 'Kullanıcı',
const v = JSON.parse(ham); username:(user?.email || '').split('@')[0] || 'kullanici',company:'',website:'',trialDaysLeft:7,
return v == null ? varsayilan : v; role:'Yönetici Tedarikçi',uyumEsigi:60,scrapedCapabilities:{scrapedAt:null,extractedSectors:[],
} catch { extractedProducts:[],extractedCertifications:[],ihaleKonulari:[],matchScoreBoost:0}});
return varsayilan;
}
};
export const AppProvider = ({ children }) => { export const AppProvider = ({ children }) => {
const [activeTab, setActiveTab] = useState('list'); // 'list', 'bulten', 'sonuc', 'takvim', 'sozlesme', 'kdv', 'fiyat', 'kanban', 'smart_filter_builder' const [activeTab, setActiveTab] = useState('list'); // 'list', 'bulten', 'sonuc', 'takvim', 'sozlesme', 'kdv', 'fiyat', 'kanban', 'smart_filter_builder'
@@ -44,7 +41,7 @@ export const AppProvider = ({ children }) => {
try { try {
const saved = localStorage.getItem(TENDER_CACHE_KEY); const saved = localStorage.getItem(TENDER_CACHE_KEY);
const parsed = saved ? JSON.parse(saved) : null; const parsed = saved ? JSON.parse(saved) : null;
return Array.isArray(parsed) ? parsed.map(t => ({...t,takipte:false,gorev:null,notes:[],checklist:[],incelendi:false,atananPersonel:null,kanbanStatus:'incelenecek',bidDraft:null})) : []; return Array.isArray(parsed) ? parsed.map(publicTender) : [];
} catch { } catch {
try { localStorage.removeItem(TENDER_CACHE_KEY); } catch { /* yok say */ } try { localStorage.removeItem(TENDER_CACHE_KEY); } catch { /* yok say */ }
return []; return [];
@@ -107,10 +104,7 @@ export const AppProvider = ({ children }) => {
const [onlyValidOnly, setOnlyValidOnly] = useState(true); const [onlyValidOnly, setOnlyValidOnly] = useState(true);
const [quickFilter, setQuickFilter] = useState('all'); const [quickFilter, setQuickFilter] = useState('all');
const [savedFilters, setSavedFilters] = useState(() => { const [savedFilters, setSavedFilters] = useState([]);
const v = yerelOku('ihaleist_saved_filters', []);
return Array.isArray(v) ? v : [];
});
const [isLiveMode, setIsLiveMode] = useState(true); const [isLiveMode, setIsLiveMode] = useState(true);
const [isRefreshingLive, setIsRefreshingLive] = useState(false); const [isRefreshingLive, setIsRefreshingLive] = useState(false);
@@ -129,6 +123,12 @@ export const AppProvider = ({ children }) => {
.finally(() => setAuthReady(true)); .finally(() => setAuthReady(true));
}, []); }, []);
useEffect(() => {
const sync = event => { if (event.key === 'ihaleist_user' || event.key === 'ihaleist_token') setAuthUser(getStoredUser()); };
window.addEventListener('storage', sync);
return () => window.removeEventListener('storage', sync);
}, []);
// "Son girişten beri" kartı için önceki ziyaretin zamanı. Ziyaret = tarayıcı // "Son girişten beri" kartı için önceki ziyaretin zamanı. Ziyaret = tarayıcı
// sekmesi oturumu (sessionStorage): sayfa yenilemek sayacı sıfırlamaz, yeni // sekmesi oturumu (sessionStorage): sayfa yenilemek sayacı sıfırlamaz, yeni
// sekme/gün açmak önceki damgayı "önceki giriş" yapar. Eskiden sabit 14'tü. // sekme/gün açmak önceki damgayı "önceki giriş" yapar. Eskiden sabit 14'tü.
@@ -136,42 +136,48 @@ export const AppProvider = ({ children }) => {
useEffect(() => { useEffect(() => {
if (!authUser) return; if (!authUser) return;
try { try {
const anahtar = 'ihaleist_son_giris'; const anahtar = 'ihaleist_son_giris:' + authUser.email;
const onceki = Number(localStorage.getItem(anahtar)) || null; const onceki = Number(localStorage.getItem(anahtar)) || null;
const oturum = sessionStorage.getItem('ihaleist_oturum_giris'); const oturum = sessionStorage.getItem('ihaleist_oturum_giris:' + authUser.email);
if (oturum !== null) { // aynı sekmede yenileme: damga sabit kalır if (oturum !== null) { // aynı sekmede yenileme: damga sabit kalır
setOncekiGiris(Number(oturum) || null); setOncekiGiris(Number(oturum) || null);
return; return;
} }
setOncekiGiris(onceki); setOncekiGiris(onceki);
sessionStorage.setItem('ihaleist_oturum_giris', String(onceki || 0)); sessionStorage.setItem('ihaleist_oturum_giris:' + authUser.email, String(onceki || 0));
localStorage.setItem(anahtar, String(Date.now())); localStorage.setItem(anahtar, String(Date.now()));
} catch { /* depolama kapalıysa kart bugünü gösterir */ } } catch { /* depolama kapalıysa kart bugünü gösterir */ }
}, [authUser?.email]); }, [authUser?.email]);
// Giriş yapınca kullanıcı verisini SUNUCUDAN yükle (localStorage'ı ezer) // Giriş yapınca kullanıcı verisini SUNUCUDAN yükle (localStorage'ı ezer)
useEffect(() => { useEffect(() => {
if (!authUser) { setVeriSenkron(false); setPlan(null); return; } setVeriSenkron(false); setPlan(null); setSaveError(null);
setFavoriteKurumlar([]); setTeamMembers([]); setSavedFilters([]); setCompetitorWatchlist([]);
setReminderSettings(defaultReminder()); setUserProfile(defaultProfile(authUser));
setVaultDocuments([]); setActiveTenderDetail(null); setAiAnalysisResults(null); setNotifications([]); setOncekiGiris(null);
setTenders(prev => prev.map(publicTender));
if (!authUser) return;
planYenile(); planYenile();
let cancelled = false; let cancelled = false;
setVeriSenkron(false); setVeriSenkron(false);
ilkYazmaAtlanan.current.clear(); ilkYazmaAtlanan.current.clear();
savedTenderStates.current = {}; savedTenderStates.current = {};
const loadSession = getToken();
Promise.all([getAllUserData(), getTenderStates()]) Promise.all([getAllUserData(), getTenderStates()])
.then(([v, records]) => { .then(([v, records]) => {
if (cancelled) return; if (cancelled || getToken() !== loadSession) return;
setFavoriteKurumlar(v.favori_kurumlar || []); setFavoriteKurumlar(v.favori_kurumlar || []);
setTeamMembers(v.ekip || []); setTeamMembers(v.ekip || []);
setSavedFilters(v.saved_filters || []); setSavedFilters(v.saved_filters || []);
if (v.reminder) setReminderSettings((s) => ({ ...s, ...v.reminder })); setReminderSettings({...defaultReminder(), ...v.reminder});
setCompetitorWatchlist(v.rakip_izleme || []); setCompetitorWatchlist(v.rakip_izleme || []);
if (v.firma_profil) setUserProfile(p => ({...p,...v.firma_profil})); setUserProfile({...defaultProfile(authUser), ...v.firma_profil});
savedTenderStates.current = records; savedTenderStates.current = records;
setTenders(prev => applyTenderStates(prev, records)); setTenders(prev => applyTenderStates(prev.map(publicTender), records));
setVeriSenkron(true); setVeriSenkron(true);
setSaveError(null); setSaveError(null);
}) })
.catch((e) => { if (!cancelled) setSaveError('Hesap verileri yüklenemedi: ' + e.message); }); .catch((e) => { if (!cancelled && getToken() === loadSession) setSaveError('Hesap verileri yüklenemedi: ' + e.message); });
return () => { cancelled = true; }; return () => { cancelled = true; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [authUser?.email]); }, [authUser?.email]);
@@ -190,21 +196,23 @@ export const AppProvider = ({ children }) => {
const key = tenderStateKey(t); const key = tenderStateKey(t);
if (inFlight.current.has(key)) return false; if (inFlight.current.has(key)) return false;
const owner = authUser.email; const owner = authUser.email;
const session = getToken();
inFlight.current.add(key); setSavingCount(n => n + 1); setSaveError(null); inFlight.current.add(key); setSavingCount(n => n + 1); setSaveError(null);
try { try {
const record = await saveTenderState(key, patch, tenderSnapshot(t), savedTenderStates.current[key]?.version || 0); const record = await saveTenderState(key, patch, tenderSnapshot(t), savedTenderStates.current[key]?.version || 0);
if (getStoredUser()?.email !== owner) return false; if (getStoredUser()?.email !== owner || getToken() !== session) return false;
savedTenderStates.current = {...savedTenderStates.current,[key]:record}; savedTenderStates.current = {...savedTenderStates.current,[key]:record};
setTenders(prev => applyTenderStates(prev, savedTenderStates.current)); setTenders(prev => applyTenderStates(prev, savedTenderStates.current));
return true; return true;
} catch (e) { } catch (e) {
if (getStoredUser()?.email !== owner || getToken() !== session) return false;
setSaveError(e.message); setSaveError(e.message);
if (e.kod === 'VERSION_CONFLICT') { if (e.kod === 'VERSION_CONFLICT') {
try { try {
const records = await getTenderStates(); const records = await getTenderStates();
if (getStoredUser()?.email === owner) { if (getStoredUser()?.email === owner && getToken() === session) {
savedTenderStates.current = records; savedTenderStates.current = records;
setTenders(prev => applyTenderStates(prev, records)); setTenders(prev => applyTenderStates(prev.map(publicTender), records));
} }
} catch { /* retain visible error */ } } catch { /* retain visible error */ }
} }
@@ -223,15 +231,17 @@ export const AppProvider = ({ children }) => {
const sunucuyaYaz = (anahtar, deger) => { const sunucuyaYaz = (anahtar, deger) => {
if (!authUser) return; if (!authUser || !veriSenkron || getStoredUser()?.email !== authUser.email) return;
const session = getToken();
setUserData(anahtar, deger).catch((e) => { setUserData(anahtar, deger).catch((e) => {
if (getToken() !== session) return;
if (e.kod === 'KOTA_ASILDI') setKotaUyarisi(e.message); if (e.kod === 'KOTA_ASILDI') setKotaUyarisi(e.message);
else setSaveError('Kaydedilemedi: ' + e.message); else setSaveError('Kaydedilemedi: ' + e.message);
}); });
}; };
const [kotaUyarisi, setKotaUyarisi] = useState(null); const [kotaUyarisi, setKotaUyarisi] = useState(null);
const planYenile = () => getPlan().then(setPlan).catch(() => {}); const planYenile = () => { const session=getToken(); return getPlan().then(p => {if(getToken()===session)setPlan(p);}).catch(() => {}); };
const handleLogin = (user) => setAuthUser(user); const handleLogin = (user) => setAuthUser(user);
const handleLogout = () => { const handleLogout = () => {
@@ -303,21 +313,15 @@ export const AppProvider = ({ children }) => {
const [aiAnalyzing, setAiAnalyzing] = useState(false); const [aiAnalyzing, setAiAnalyzing] = useState(false);
// Enterprise Module States // Enterprise Module States
// Rakip izleme listesi — kullanıcı gerçek kazanan firma analizinden ekler (localStorage) // Rakip izleme listesi — kullanıcı gerçek kazanan firma analizinden ekler (hesaba özel sunucu kaydı)
const [competitorWatchlist, setCompetitorWatchlist] = useState(() => { const [competitorWatchlist, setCompetitorWatchlist] = useState([]);
try { return JSON.parse(localStorage.getItem('ihaleist_rakip_izleme') || '[]'); } catch { return []; }
});
useEffect(() => { useEffect(() => {
yerelKaydet('ihaleist_rakip_izleme', competitorWatchlist);
if (veriSenkron) ilkYazmaDegil('rakip_izleme', competitorWatchlist); if (veriSenkron) ilkYazmaDegil('rakip_izleme', competitorWatchlist);
}, [competitorWatchlist, veriSenkron]); }, [competitorWatchlist, veriSenkron]);
// --- Favori kurumlar (localStorage, gerçek) --- // --- Favori kurumlar (sunucu kaydı) ---
const [favoriteKurumlar, setFavoriteKurumlar] = useState(() => { const [favoriteKurumlar, setFavoriteKurumlar] = useState([]);
try { return JSON.parse(localStorage.getItem('ihaleist_favori_kurumlar') || '[]'); } catch { return []; }
});
useEffect(() => { useEffect(() => {
yerelKaydet('ihaleist_favori_kurumlar', favoriteKurumlar);
if (veriSenkron) ilkYazmaDegil('favori_kurumlar', favoriteKurumlar); if (veriSenkron) ilkYazmaDegil('favori_kurumlar', favoriteKurumlar);
}, [favoriteKurumlar, veriSenkron]); }, [favoriteKurumlar, veriSenkron]);
const toggleFavoriteKurum = (kurum) => { const toggleFavoriteKurum = (kurum) => {
@@ -326,44 +330,19 @@ export const AppProvider = ({ children }) => {
setFavoriteKurumlar((prev) => (prev.includes(ad) ? prev.filter((k) => k !== ad) : [...prev, ad])); setFavoriteKurumlar((prev) => (prev.includes(ad) ? prev.filter((k) => k !== ad) : [...prev, ad]));
}; };
// --- Ekip yönetimi (localStorage, gerçek CRUD) --- // --- Ekip yönetimi (sunucu kaydı) ---
const [teamMembers, setTeamMembers] = useState(() => { const [teamMembers, setTeamMembers] = useState([]);
try {
const saved = localStorage.getItem('ihaleist_ekip');
if (saved) return JSON.parse(saved);
} catch { /* yoksay */ }
const u = getStoredUser();
return u ? [{ id: 'tm-1', name: u.name || u.email, email: u.email || '', role: 'Yönetici' }] : [];
});
useEffect(() => { useEffect(() => {
yerelKaydet('ihaleist_ekip', teamMembers);
if (veriSenkron) ilkYazmaDegil('ekip', teamMembers); if (veriSenkron) ilkYazmaDegil('ekip', teamMembers);
}, [teamMembers, veriSenkron]); }, [teamMembers, veriSenkron]);
const addTeamMember = (m) => setTeamMembers((prev) => [...prev, { id: 'tm-' + Date.now(), ...m }]); const addTeamMember = (m) => setTeamMembers((prev) => [...prev, { id: 'tm-' + Date.now(), ...m }]);
const removeTeamMember = (id) => setTeamMembers((prev) => prev.filter((m) => m.id !== id)); const removeTeamMember = (id) => setTeamMembers((prev) => prev.filter((m) => m.id !== id));
// Belge kasası — kullanıcı kendi belgelerini ekler (localStorage) // Belge kasası — kullanıcı kendi belgelerini ekler (hesaba özel sunucu kaydı)
const [vaultDocuments, setVaultDocuments] = useState(() => { const [vaultDocuments, setVaultDocuments] = useState([]);
try { return JSON.parse(localStorage.getItem('ihaleist_belgeler') || '[]'); } catch { return []; }
});
useEffect(() => {
yerelKaydet('ihaleist_belgeler', vaultDocuments);
}, [vaultDocuments]);
const [reminderSettings, setReminderSettings] = useState(() => { const [reminderSettings, setReminderSettings] = useState(defaultReminder);
const def = {
emailEnabled: true, smsEnabled: true, pushEnabled: true,
notify3DaysBefore: true, notify1DayBefore: true, notify3HoursBefore: true,
googleCalendarSync: true, outlookSync: false,
quietEnabled: false, quietStart: '22:00', quietEnd: '08:00',
};
try {
const saved = JSON.parse(localStorage.getItem('ihaleist_reminder') || 'null');
return saved ? { ...def, ...saved } : def;
} catch { return def; }
});
useEffect(() => { useEffect(() => {
yerelKaydet('ihaleist_reminder', reminderSettings);
if (veriSenkron) ilkYazmaDegil('reminder', reminderSettings); if (veriSenkron) ilkYazmaDegil('reminder', reminderSettings);
}, [reminderSettings, veriSenkron]); }, [reminderSettings, veriSenkron]);
@@ -441,6 +420,7 @@ export const AppProvider = ({ children }) => {
// seçilen model zinciri (fallback) ile. // seçilen model zinciri (fallback) ile.
// mode='derin' → ince detaylar, belge kontrol listesi ve yol haritası da üretilir // mode='derin' → ince detaylar, belge kontrol listesi ve yol haritası da üretilir
const analyzeSpecPdfWithGemini = async ({ text, fileName, tender, mode } = {}) => { const analyzeSpecPdfWithGemini = async ({ text, fileName, tender, mode } = {}) => {
const session = getToken();
setAiAnalyzing(true); setAiAnalyzing(true);
setAiAnalysisResults(null); setAiAnalysisResults(null);
try { try {
@@ -457,6 +437,7 @@ export const AppProvider = ({ children }) => {
attempts: data.attempts, attempts: data.attempts,
analyzedAt: new Date(data.analyzedAt).toLocaleString('tr-TR'), analyzedAt: new Date(data.analyzedAt).toLocaleString('tr-TR'),
}; };
if(getToken() !== session) throw new Error('Oturum değişti; analiz sonucu bu hesaba aktarılmadı.');
setAiAnalysisResults(result); setAiAnalysisResults(result);
return result; return result;
} finally { } finally {
@@ -464,33 +445,12 @@ export const AppProvider = ({ children }) => {
} }
}; };
const BOS_YETKINLIK = { const [userProfile, setUserProfile] = useState(() => defaultProfile(getStoredUser()));
scrapedAt: null, extractedSectors: [], extractedProducts: [], extractedCertifications: [],
ihaleKonulari: [], matchScoreBoost: 0,
};
const [userProfile, setUserProfile] = useState(() => {
// Yetkinlikler uyum skorunun girdisi — sayfa yenilenince sıfırlanmasın
let kayitli = null;
try { kayitli = JSON.parse(localStorage.getItem('ihaleist_firma_profil') || 'null'); } catch { /* yoksay */ }
const u = getStoredUser();
return {
name: u?.name || u?.email || 'Kullanıcı',
username: (u?.email || '').split('@')[0] || 'kullanici',
company: kayitli?.company || '',
website: kayitli?.website || '',
trialDaysLeft: 7,
role: 'Yönetici Tedarikçi',
scrapedCapabilities: { ...BOS_YETKINLIK, ...(kayitli?.scrapedCapabilities || {}) },
// Firma eşiği: "uyumlu" filtresi ve bildirim bu skordan itibaren
uyumEsigi: Number.isFinite(kayitli?.uyumEsigi) ? kayitli.uyumEsigi : 60,
};
});
useEffect(() => { useEffect(() => {
const veri = { const veri = {
company: userProfile.company, website: userProfile.website, company: userProfile.company, website: userProfile.website,
scrapedCapabilities: userProfile.scrapedCapabilities, uyumEsigi: userProfile.uyumEsigi, scrapedCapabilities: userProfile.scrapedCapabilities, uyumEsigi: userProfile.uyumEsigi,
}; };
yerelKaydet('ihaleist_firma_profil', veri);
if (veriSenkron) ilkYazmaDegil('firma_profil', veri); if (veriSenkron) ilkYazmaDegil('firma_profil', veri);
}, [userProfile.company, userProfile.website, userProfile.scrapedCapabilities, userProfile.uyumEsigi, veriSenkron]); }, [userProfile.company, userProfile.website, userProfile.scrapedCapabilities, userProfile.uyumEsigi, veriSenkron]);
const setUyumEsigi = (v) => setUserProfile((p) => ({ ...p, uyumEsigi: Math.max(0, Math.min(100, Number(v) || 0)) })); const setUyumEsigi = (v) => setUserProfile((p) => ({ ...p, uyumEsigi: Math.max(0, Math.min(100, Number(v) || 0)) }));
@@ -532,6 +492,7 @@ export const AppProvider = ({ children }) => {
// Önce gerçek tarama (/api/company/scan: site metni + model), başarısızsa // Önce gerçek tarama (/api/company/scan: site metni + model), başarısızsa
// alan adından tahmin eden eski yol. Sonuç uyum skorunun girdisi olur. // alan adından tahmin eden eski yol. Sonuç uyum skorunun girdisi olur.
const scanCompanyWebsite = async (url) => { const scanCompanyWebsite = async (url) => {
const session = getToken();
let yeni = null; let yeni = null;
try { try {
const r = await fetch('/api/company/scan', { const r = await fetch('/api/company/scan', {
@@ -556,6 +517,7 @@ export const AppProvider = ({ children }) => {
} }
} catch { /* aşağıda yedek yol */ } } catch { /* aşağıda yedek yol */ }
if (!yeni) yeni = { ...parseWebsiteCapabilitiesDynamically(url), ihaleKonulari: [], gercekTarama: false }; if (!yeni) yeni = { ...parseWebsiteCapabilitiesDynamically(url), ihaleKonulari: [], gercekTarama: false };
if(getToken() !== session) throw new Error('Oturum değişti; firma taraması bu hesaba aktarılmadı.');
setUserProfile((prev) => ({ ...prev, website: url, scrapedCapabilities: yeni })); setUserProfile((prev) => ({ ...prev, website: url, scrapedCapabilities: yeni }));
return yeni; return yeni;
}; };
@@ -596,7 +558,7 @@ export const AppProvider = ({ children }) => {
// boyamaya yetecek kadarını, hataya dayanıklı biçimde saklıyoruz. // boyamaya yetecek kadarını, hataya dayanıklı biçimde saklıyoruz.
useEffect(() => { useEffect(() => {
try { try {
localStorage.setItem(TENDER_CACHE_KEY, JSON.stringify(tenders.slice(0, TENDER_CACHE_LIMIT))); localStorage.setItem(TENDER_CACHE_KEY, JSON.stringify(tenders.slice(0, TENDER_CACHE_LIMIT).map(publicTender)));
} catch { } catch {
// Kota dolduysa önbelleği bırak — veri zaten sunucudan geliyor. // Kota dolduysa önbelleği bırak — veri zaten sunucudan geliyor.
try { localStorage.removeItem(TENDER_CACHE_KEY); } catch { /* yok say */ } try { localStorage.removeItem(TENDER_CACHE_KEY); } catch { /* yok say */ }
@@ -604,7 +566,6 @@ export const AppProvider = ({ children }) => {
}, [tenders]); }, [tenders]);
useEffect(() => { useEffect(() => {
yerelKaydet('ihaleist_saved_filters', savedFilters);
if (veriSenkron) ilkYazmaDegil('saved_filters', savedFilters); if (veriSenkron) ilkYazmaDegil('saved_filters', savedFilters);
}, [savedFilters, veriSenkron]); }, [savedFilters, veriSenkron]);
@@ -654,11 +615,12 @@ export const AppProvider = ({ children }) => {
useEffect(() => { useEffect(() => {
if (!veriSenkron) return; if (!veriSenkron) return;
let iptal = false; let iptal = false;
const session = getToken();
const cek = async () => { const cek = async () => {
try { try {
const liste = await bildirimleriGetir(); const liste = await bildirimleriGetir();
const yeni = liste.filter((b) => !b.okundu); const yeni = liste.filter((b) => !b.okundu);
if (iptal || !yeni.length) return; if (iptal || getToken() !== session || !yeni.length) return;
for (const b of yeni.slice(0, 5)) notify(b.baslik, b.metin); for (const b of yeni.slice(0, 5)) notify(b.baslik, b.metin);
await bildirimOkundu(yeni.map((b) => b.id)); await bildirimOkundu(yeni.map((b) => b.id));
} catch { /* sessiz */ } } catch { /* sessiz */ }
@@ -674,9 +636,12 @@ export const AppProvider = ({ children }) => {
// fark etmiyor — fikir: MOBIT-DURT). // fark etmiyor — fikir: MOBIT-DURT).
useEffect(() => { useEffect(() => {
if (!veriSenkron) return; if (!veriSenkron) return;
const session = getToken();
const uyariAnahtari = 'ihaleist_belge_uyari_gunu:' + authUser?.email;
const bugun = new Date().toISOString().slice(0, 10); const bugun = new Date().toISOString().slice(0, 10);
try { if (localStorage.getItem('ihaleist_belge_uyari_gunu') === bugun) return; } catch { /* yoksay */ } try { if (localStorage.getItem(uyariAnahtari) === bugun) return; } catch { /* yoksay */ }
listDocuments().then((d) => { listDocuments().then((d) => {
if(getToken() !== session) return;
const simdi = Date.now(); const simdi = Date.now();
const dolan = [], yakin = []; const dolan = [], yakin = [];
for (const b of d?.belgeler || []) { for (const b of d?.belgeler || []) {
@@ -686,7 +651,7 @@ export const AppProvider = ({ children }) => {
} }
if (dolan.length) notify(`${dolan.length} belgenin süresi dolmuş`, dolan.slice(0, 4).join(', ')); if (dolan.length) notify(`${dolan.length} belgenin süresi dolmuş`, dolan.slice(0, 4).join(', '));
if (yakin.length) notify(`${yakin.length} belgenin süresi 30 gün içinde doluyor`, yakin.slice(0, 4).join(', ')); if (yakin.length) notify(`${yakin.length} belgenin süresi 30 gün içinde doluyor`, yakin.slice(0, 4).join(', '));
try { localStorage.setItem('ihaleist_belge_uyari_gunu', bugun); } catch { /* yoksay */ } try { localStorage.setItem(uyariAnahtari, bugun); } catch { /* yoksay */ }
}).catch(() => {}); }).catch(() => {});
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [veriSenkron]); }, [veriSenkron]);
+2
View File
@@ -53,8 +53,10 @@ export async function restoreSession() {
if (!token) return null; if (!token) return null;
try { try {
const res = await fetch('/api/auth/me', { headers: { Authorization: `Bearer ${token}` } }); const res = await fetch('/api/auth/me', { headers: { Authorization: `Bearer ${token}` } });
if (getToken() !== token) return getStoredUser();
if (!res.ok) { clearSession(); return null; } if (!res.ok) { clearSession(); return null; }
const data = await res.json(); const data = await res.json();
if (getToken() !== token) return getStoredUser();
if (data.success) { setSession(token, data.user); return data.user; } if (data.success) { setSession(token, data.user); return data.user; }
clearSession(); clearSession();
return null; return null;
+4
View File
@@ -16,3 +16,7 @@ export function applyTenderStates(tenders, records = {}) {
export function tenderSnapshot(t) { export function tenderSnapshot(t) {
return Object.fromEntries(['id','ikn','baslik','kurum','tarih','saat','sehir','alimTuru'].map(k => [k,t[k] ?? ''])); return Object.fromEntries(['id','ikn','baslik','kurum','tarih','saat','sehir','alimTuru'].map(k => [k,t[k] ?? '']));
} }
export function publicTender(tender) {
return Object.fromEntries(Object.entries(tender).filter(([key]) => !USER_STATE_FIELDS.includes(key)));
}
+33
View File
@@ -0,0 +1,33 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {restoreSession,setSession,getStoredUser,getToken,clearSession} from '../src/services/auth.js';
import {publicTender,USER_STATE_FIELDS,applyTenderStates} from '../src/services/tenderState.js';
test('late session restore cannot replace or clear a newer account',async()=>{
const savedStorage=globalThis.localStorage,savedFetch=globalThis.fetch;
const values=new Map();
globalThis.localStorage={getItem:k=>values.get(k)??null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)};
try {
for(const response of [{ok:false},{ok:true,json:async()=>({success:true,user:{email:'a@test'}})}]) {
setSession('session-a',{email:'a@test'});
let resolve;globalThis.fetch=()=>new Promise(r=>resolve=r);
const pending=restoreSession();
setSession('session-b',{email:'b@test'});
resolve(response);
assert.equal((await pending).email,'b@test');
assert.equal(getToken(),'session-b');assert.equal(getStoredUser().email,'b@test');
}
setSession('session-a',{email:'a@test'});
let resolve;globalThis.fetch=()=>new Promise(r=>resolve=r);
const pending=restoreSession();clearSession();
resolve({ok:true,json:async()=>({success:true,user:{email:'a@test'}})});
assert.equal(await pending,null);assert.equal(getToken(),null);
}finally{globalThis.localStorage=savedStorage;globalThis.fetch=savedFetch;}
});
test('shared tender cache contains no preparation state and account changes clear previous state',()=>{
const source={id:'x',ikn:'2026/x',baslik:'Lab',items:[{ad:'Kit'}]};
const a={...source,...Object.fromEntries(USER_STATE_FIELDS.map(k=>[k,'private-a']))};
assert.deepEqual(publicTender(a),source);
assert.deepEqual(applyTenderStates([a].map(publicTender),{}),[source]);
const b=applyTenderStates([a].map(publicTender),{'2026/x':{data:{notes:['private-b']}}});
assert.deepEqual(b[0].notes,['private-b']);assert.equal(b[0].bidDraft,undefined);
});