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
+62 -97
View File
@@ -5,7 +5,7 @@ import { getToken } from '../services/auth';
import { fetchLiveTenders, fetchTenderDetail, mergeUserState } from '../services/tenderApi';
import { restoreSession, clearSession, getStoredUser } from '../services/auth';
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 { SECTORS_LIST, INSTITUTION_TYPES_LIST } from '../data/constants';
@@ -25,16 +25,13 @@ const yerelKaydet = (anahtar, deger) => {
return false;
}
};
const yerelOku = (anahtar, varsayilan) => {
try {
const ham = localStorage.getItem(anahtar);
if (!ham) return varsayilan;
const v = JSON.parse(ham);
return v == null ? varsayilan : v;
} catch {
return varsayilan;
}
};
const defaultReminder = () => ({emailEnabled:true,smsEnabled:true,pushEnabled:true,
notify3DaysBefore:true,notify1DayBefore:true,notify3HoursBefore:true,
googleCalendarSync:true,outlookSync:false,quietEnabled:false,quietStart:'22:00',quietEnd:'08:00'});
const defaultProfile = user => ({name:user?.name || user?.email || 'Kullanıcı',
username:(user?.email || '').split('@')[0] || 'kullanici',company:'',website:'',trialDaysLeft:7,
role:'Yönetici Tedarikçi',uyumEsigi:60,scrapedCapabilities:{scrapedAt:null,extractedSectors:[],
extractedProducts:[],extractedCertifications:[],ihaleKonulari:[],matchScoreBoost:0}});
export const AppProvider = ({ children }) => {
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 {
const saved = localStorage.getItem(TENDER_CACHE_KEY);
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 {
try { localStorage.removeItem(TENDER_CACHE_KEY); } catch { /* yok say */ }
return [];
@@ -107,10 +104,7 @@ export const AppProvider = ({ children }) => {
const [onlyValidOnly, setOnlyValidOnly] = useState(true);
const [quickFilter, setQuickFilter] = useState('all');
const [savedFilters, setSavedFilters] = useState(() => {
const v = yerelOku('ihaleist_saved_filters', []);
return Array.isArray(v) ? v : [];
});
const [savedFilters, setSavedFilters] = useState([]);
const [isLiveMode, setIsLiveMode] = useState(true);
const [isRefreshingLive, setIsRefreshingLive] = useState(false);
@@ -129,6 +123,12 @@ export const AppProvider = ({ children }) => {
.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ı
// sekmesi oturumu (sessionStorage): sayfa yenilemek sayacı sıfırlamaz, yeni
// sekme/gün açmak önceki damgayı "önceki giriş" yapar. Eskiden sabit 14'tü.
@@ -136,42 +136,48 @@ export const AppProvider = ({ children }) => {
useEffect(() => {
if (!authUser) return;
try {
const anahtar = 'ihaleist_son_giris';
const anahtar = 'ihaleist_son_giris:' + authUser.email;
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
setOncekiGiris(Number(oturum) || null);
return;
}
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()));
} catch { /* depolama kapalıysa kart bugünü gösterir */ }
}, [authUser?.email]);
// Giriş yapınca kullanıcı verisini SUNUCUDAN yükle (localStorage'ı ezer)
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();
let cancelled = false;
setVeriSenkron(false);
ilkYazmaAtlanan.current.clear();
savedTenderStates.current = {};
const loadSession = getToken();
Promise.all([getAllUserData(), getTenderStates()])
.then(([v, records]) => {
if (cancelled) return;
if (cancelled || getToken() !== loadSession) return;
setFavoriteKurumlar(v.favori_kurumlar || []);
setTeamMembers(v.ekip || []);
setSavedFilters(v.saved_filters || []);
if (v.reminder) setReminderSettings((s) => ({ ...s, ...v.reminder }));
setReminderSettings({...defaultReminder(), ...v.reminder});
setCompetitorWatchlist(v.rakip_izleme || []);
if (v.firma_profil) setUserProfile(p => ({...p,...v.firma_profil}));
setUserProfile({...defaultProfile(authUser), ...v.firma_profil});
savedTenderStates.current = records;
setTenders(prev => applyTenderStates(prev, records));
setTenders(prev => applyTenderStates(prev.map(publicTender), records));
setVeriSenkron(true);
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; };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [authUser?.email]);
@@ -190,21 +196,23 @@ export const AppProvider = ({ children }) => {
const key = tenderStateKey(t);
if (inFlight.current.has(key)) return false;
const owner = authUser.email;
const session = getToken();
inFlight.current.add(key); setSavingCount(n => n + 1); setSaveError(null);
try {
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};
setTenders(prev => applyTenderStates(prev, savedTenderStates.current));
return true;
} catch (e) {
if (getStoredUser()?.email !== owner || getToken() !== session) return false;
setSaveError(e.message);
if (e.kod === 'VERSION_CONFLICT') {
try {
const records = await getTenderStates();
if (getStoredUser()?.email === owner) {
if (getStoredUser()?.email === owner && getToken() === session) {
savedTenderStates.current = records;
setTenders(prev => applyTenderStates(prev, records));
setTenders(prev => applyTenderStates(prev.map(publicTender), records));
}
} catch { /* retain visible error */ }
}
@@ -223,15 +231,17 @@ export const AppProvider = ({ children }) => {
const sunucuyaYaz = (anahtar, deger) => {
if (!authUser) return;
if (!authUser || !veriSenkron || getStoredUser()?.email !== authUser.email) return;
const session = getToken();
setUserData(anahtar, deger).catch((e) => {
if (getToken() !== session) return;
if (e.kod === 'KOTA_ASILDI') setKotaUyarisi(e.message);
else setSaveError('Kaydedilemedi: ' + e.message);
});
};
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 handleLogout = () => {
@@ -303,21 +313,15 @@ export const AppProvider = ({ children }) => {
const [aiAnalyzing, setAiAnalyzing] = useState(false);
// Enterprise Module States
// Rakip izleme listesi — kullanıcı gerçek kazanan firma analizinden ekler (localStorage)
const [competitorWatchlist, setCompetitorWatchlist] = useState(() => {
try { return JSON.parse(localStorage.getItem('ihaleist_rakip_izleme') || '[]'); } catch { return []; }
});
// Rakip izleme listesi — kullanıcı gerçek kazanan firma analizinden ekler (hesaba özel sunucu kaydı)
const [competitorWatchlist, setCompetitorWatchlist] = useState([]);
useEffect(() => {
yerelKaydet('ihaleist_rakip_izleme', competitorWatchlist);
if (veriSenkron) ilkYazmaDegil('rakip_izleme', competitorWatchlist);
}, [competitorWatchlist, veriSenkron]);
// --- Favori kurumlar (localStorage, gerçek) ---
const [favoriteKurumlar, setFavoriteKurumlar] = useState(() => {
try { return JSON.parse(localStorage.getItem('ihaleist_favori_kurumlar') || '[]'); } catch { return []; }
});
// --- Favori kurumlar (sunucu kaydı) ---
const [favoriteKurumlar, setFavoriteKurumlar] = useState([]);
useEffect(() => {
yerelKaydet('ihaleist_favori_kurumlar', favoriteKurumlar);
if (veriSenkron) ilkYazmaDegil('favori_kurumlar', favoriteKurumlar);
}, [favoriteKurumlar, veriSenkron]);
const toggleFavoriteKurum = (kurum) => {
@@ -326,44 +330,19 @@ export const AppProvider = ({ children }) => {
setFavoriteKurumlar((prev) => (prev.includes(ad) ? prev.filter((k) => k !== ad) : [...prev, ad]));
};
// --- Ekip yönetimi (localStorage, gerçek CRUD) ---
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' }] : [];
});
// --- Ekip yönetimi (sunucu kaydı) ---
const [teamMembers, setTeamMembers] = useState([]);
useEffect(() => {
yerelKaydet('ihaleist_ekip', teamMembers);
if (veriSenkron) ilkYazmaDegil('ekip', teamMembers);
}, [teamMembers, veriSenkron]);
const addTeamMember = (m) => setTeamMembers((prev) => [...prev, { id: 'tm-' + Date.now(), ...m }]);
const removeTeamMember = (id) => setTeamMembers((prev) => prev.filter((m) => m.id !== id));
// Belge kasası — kullanıcı kendi belgelerini ekler (localStorage)
const [vaultDocuments, setVaultDocuments] = useState(() => {
try { return JSON.parse(localStorage.getItem('ihaleist_belgeler') || '[]'); } catch { return []; }
});
useEffect(() => {
yerelKaydet('ihaleist_belgeler', vaultDocuments);
}, [vaultDocuments]);
// Belge kasası — kullanıcı kendi belgelerini ekler (hesaba özel sunucu kaydı)
const [vaultDocuments, setVaultDocuments] = useState([]);
const [reminderSettings, setReminderSettings] = useState(() => {
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; }
});
const [reminderSettings, setReminderSettings] = useState(defaultReminder);
useEffect(() => {
yerelKaydet('ihaleist_reminder', reminderSettings);
if (veriSenkron) ilkYazmaDegil('reminder', reminderSettings);
}, [reminderSettings, veriSenkron]);
@@ -441,6 +420,7 @@ export const AppProvider = ({ children }) => {
// seçilen model zinciri (fallback) ile.
// mode='derin' → ince detaylar, belge kontrol listesi ve yol haritası da üretilir
const analyzeSpecPdfWithGemini = async ({ text, fileName, tender, mode } = {}) => {
const session = getToken();
setAiAnalyzing(true);
setAiAnalysisResults(null);
try {
@@ -457,6 +437,7 @@ export const AppProvider = ({ children }) => {
attempts: data.attempts,
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);
return result;
} finally {
@@ -464,33 +445,12 @@ export const AppProvider = ({ children }) => {
}
};
const BOS_YETKINLIK = {
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,
};
});
const [userProfile, setUserProfile] = useState(() => defaultProfile(getStoredUser()));
useEffect(() => {
const veri = {
company: userProfile.company, website: userProfile.website,
scrapedCapabilities: userProfile.scrapedCapabilities, uyumEsigi: userProfile.uyumEsigi,
};
yerelKaydet('ihaleist_firma_profil', veri);
if (veriSenkron) ilkYazmaDegil('firma_profil', veri);
}, [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)) }));
@@ -532,6 +492,7 @@ export const AppProvider = ({ children }) => {
// Ö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.
const scanCompanyWebsite = async (url) => {
const session = getToken();
let yeni = null;
try {
const r = await fetch('/api/company/scan', {
@@ -556,6 +517,7 @@ export const AppProvider = ({ children }) => {
}
} catch { /* aşağıda yedek yol */ }
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 }));
return yeni;
};
@@ -596,7 +558,7 @@ export const AppProvider = ({ children }) => {
// boyamaya yetecek kadarını, hataya dayanıklı biçimde saklıyoruz.
useEffect(() => {
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 {
// Kota dolduysa önbelleği bırak — veri zaten sunucudan geliyor.
try { localStorage.removeItem(TENDER_CACHE_KEY); } catch { /* yok say */ }
@@ -604,7 +566,6 @@ export const AppProvider = ({ children }) => {
}, [tenders]);
useEffect(() => {
yerelKaydet('ihaleist_saved_filters', savedFilters);
if (veriSenkron) ilkYazmaDegil('saved_filters', savedFilters);
}, [savedFilters, veriSenkron]);
@@ -654,11 +615,12 @@ export const AppProvider = ({ children }) => {
useEffect(() => {
if (!veriSenkron) return;
let iptal = false;
const session = getToken();
const cek = async () => {
try {
const liste = await bildirimleriGetir();
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);
await bildirimOkundu(yeni.map((b) => b.id));
} catch { /* sessiz */ }
@@ -674,9 +636,12 @@ export const AppProvider = ({ children }) => {
// fark etmiyor — fikir: MOBIT-DURT).
useEffect(() => {
if (!veriSenkron) return;
const session = getToken();
const uyariAnahtari = 'ihaleist_belge_uyari_gunu:' + authUser?.email;
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) => {
if(getToken() !== session) return;
const simdi = Date.now();
const dolan = [], yakin = [];
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 (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(() => {});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [veriSenkron]);
+2
View File
@@ -53,8 +53,10 @@ export async function restoreSession() {
if (!token) return null;
try {
const res = await fetch('/api/auth/me', { headers: { Authorization: `Bearer ${token}` } });
if (getToken() !== token) return getStoredUser();
if (!res.ok) { clearSession(); return null; }
const data = await res.json();
if (getToken() !== token) return getStoredUser();
if (data.success) { setSession(token, data.user); return data.user; }
clearSession();
return null;
+4
View File
@@ -16,3 +16,7 @@ export function applyTenderStates(tenders, records = {}) {
export function tenderSnapshot(t) {
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)));
}