feat: sunucu tarafı hesap altyapısı — Stripe webhook, kotalar, gerçek API anahtarı ve belge deposu
Denetlenen 5 teknik borç kalemi de kapatıldı:
1) Stripe webhook + gerçek yetkilendirme (ÖNCEDEN YOKTU)
- POST /api/billing/webhook — SDK'sız HMAC-SHA256 imza doğrulaması,
tekrar saldırısına karşı ±300sn zaman penceresi, timingSafeEqual
- checkout.session.completed / subscription.created|updated|deleted işlenir
- Ödeme → kullanıcının planı sunucuda güncellenir (önce ödeme hiçbir şeyi
değiştirmiyordu)
2) Plan kotaları GERÇEKTEN uygulanıyor (önce sadece tanımlıydı)
- server/entitlements.mjs: plan hakları + kotaKontrol()
- ekip, belge adedi/boyutu, API anahtarı kotaları sunucuda zorlanır
- Kota aşımında 402 + anlaşılır Türkçe mesaj; arayüzde KotaUyarisi bandı
- /api/me/plan: plan, haklar ve gerçek kullanım
3) Kullanıcı verisi localStorage'dan sunucuya taşındı
- server/user-store.mjs: kullanici_verisi tablosu (izinli anahtar listesi)
- AppContext girişte sunucudan yükler, değişiklikleri sunucuya yazar;
localStorage yalnızca çevrimdışı önbellek
- Artık cihaz değişince veri kaybolmuyor
4) API anahtarları gerçek (önce Math.random ile tarayıcıda üretiliyordu)
- Sunucuda crypto.randomBytes ile üretilir, YALNIZCA sha256 hash'i saklanır
- Ham değer bir kez gösterilir; verifyApiKey ile doğrulanır, son kullanım
izlenir, iptal anında geçerli olur
5) Belge deposu gerçek (önce boyut Math.random'dı, dosya hiç yüklenmiyordu)
- Dosyalar kalıcı volume'e yazılır, boyut GERÇEK bayt
- Auth'lu indirme (blob), silme diskten de siler, kota sunucuda uygulanır
This commit is contained in:
@@ -2,6 +2,7 @@ import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { parseWebsiteCapabilitiesDynamically } from '../services/webScraperService';
|
||||
import { fetchLiveTenders, fetchTenderDetail, mergeUserState } from '../services/tenderApi';
|
||||
import { restoreSession, clearSession, getStoredUser } from '../services/auth';
|
||||
import { getAllUserData, setUserData, getPlan } from '../services/account';
|
||||
|
||||
const AppContext = createContext();
|
||||
|
||||
@@ -43,6 +44,39 @@ export const AppProvider = ({ children }) => {
|
||||
.finally(() => setAuthReady(true));
|
||||
}, []);
|
||||
|
||||
// Giriş yapınca kullanıcı verisini SUNUCUDAN yükle (localStorage'ı ezer)
|
||||
useEffect(() => {
|
||||
if (!authUser) { setVeriSenkron(false); setPlan(null); return; }
|
||||
planYenile();
|
||||
getAllUserData()
|
||||
.then((v) => {
|
||||
if (v.favori_kurumlar) setFavoriteKurumlar(v.favori_kurumlar);
|
||||
if (v.ekip) setTeamMembers(v.ekip);
|
||||
if (v.saved_filters) setSavedFilters(v.saved_filters);
|
||||
if (v.reminder) setReminderSettings((s) => ({ ...s, ...v.reminder }));
|
||||
if (v.rakip_izleme) setCompetitorWatchlist(v.rakip_izleme);
|
||||
})
|
||||
.catch((e) => console.warn('[veri] sunucudan alınamadı:', e.message))
|
||||
.finally(() => setVeriSenkron(true));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [authUser?.email]);
|
||||
|
||||
// --- Sunucu tarafı kullanıcı verisi ---
|
||||
// localStorage yalnızca çevrimdışı önbellek; kaynak doğruluk sunucuda.
|
||||
const [plan, setPlan] = useState(null);
|
||||
const [veriSenkron, setVeriSenkron] = useState(false);
|
||||
|
||||
const sunucuyaYaz = (anahtar, deger) => {
|
||||
if (!authUser) return;
|
||||
setUserData(anahtar, deger).catch((e) => {
|
||||
if (e.kod === 'KOTA_ASILDI') setKotaUyarisi(e.message);
|
||||
else console.warn('[veri] sunucuya yazılamadı:', e.message);
|
||||
});
|
||||
};
|
||||
const [kotaUyarisi, setKotaUyarisi] = useState(null);
|
||||
|
||||
const planYenile = () => getPlan().then(setPlan).catch(() => {});
|
||||
|
||||
const handleLogin = (user) => setAuthUser(user);
|
||||
const handleLogout = () => {
|
||||
clearSession();
|
||||
@@ -109,7 +143,8 @@ export const AppProvider = ({ children }) => {
|
||||
});
|
||||
useEffect(() => {
|
||||
localStorage.setItem('ihaleist_rakip_izleme', JSON.stringify(competitorWatchlist));
|
||||
}, [competitorWatchlist]);
|
||||
if (veriSenkron) sunucuyaYaz('rakip_izleme', competitorWatchlist);
|
||||
}, [competitorWatchlist, veriSenkron]);
|
||||
|
||||
// --- Favori kurumlar (localStorage, gerçek) ---
|
||||
const [favoriteKurumlar, setFavoriteKurumlar] = useState(() => {
|
||||
@@ -117,7 +152,8 @@ export const AppProvider = ({ children }) => {
|
||||
});
|
||||
useEffect(() => {
|
||||
localStorage.setItem('ihaleist_favori_kurumlar', JSON.stringify(favoriteKurumlar));
|
||||
}, [favoriteKurumlar]);
|
||||
if (veriSenkron) sunucuyaYaz('favori_kurumlar', favoriteKurumlar);
|
||||
}, [favoriteKurumlar, veriSenkron]);
|
||||
const toggleFavoriteKurum = (kurum) => {
|
||||
const ad = (kurum || '').trim();
|
||||
if (!ad) return;
|
||||
@@ -136,7 +172,8 @@ export const AppProvider = ({ children }) => {
|
||||
});
|
||||
useEffect(() => {
|
||||
localStorage.setItem('ihaleist_ekip', JSON.stringify(teamMembers));
|
||||
}, [teamMembers]);
|
||||
if (veriSenkron) sunucuyaYaz('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));
|
||||
|
||||
@@ -162,7 +199,8 @@ export const AppProvider = ({ children }) => {
|
||||
});
|
||||
useEffect(() => {
|
||||
localStorage.setItem('ihaleist_reminder', JSON.stringify(reminderSettings));
|
||||
}, [reminderSettings]);
|
||||
if (veriSenkron) sunucuyaYaz('reminder', reminderSettings);
|
||||
}, [reminderSettings, veriSenkron]);
|
||||
|
||||
// Şu an sessiz saat aralığında mıyız (gece yarısını geçen aralıkları da destekler)
|
||||
const isQuietNow = () => {
|
||||
@@ -321,7 +359,8 @@ export const AppProvider = ({ children }) => {
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('ihaleist_saved_filters', JSON.stringify(savedFilters));
|
||||
}, [savedFilters]);
|
||||
if (veriSenkron) sunucuyaYaz('saved_filters', savedFilters);
|
||||
}, [savedFilters, veriSenkron]);
|
||||
|
||||
// (Sahte "online kullanıcı" rastgele sayacı kaldırıldı — canlı istatistikler
|
||||
// gerçek /api/stats verisinden gelir.)
|
||||
@@ -447,7 +486,12 @@ export const AppProvider = ({ children }) => {
|
||||
removeTeamMember,
|
||||
toggleTrackTender,
|
||||
updateKanbanStatus,
|
||||
clearAllFilters
|
||||
clearAllFilters,
|
||||
plan,
|
||||
planYenile,
|
||||
kotaUyarisi,
|
||||
setKotaUyarisi,
|
||||
veriSenkron
|
||||
}}>
|
||||
{children}
|
||||
</AppContext.Provider>
|
||||
|
||||
Reference in New Issue
Block a user