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:
2026-08-29 10:40:05 +03:00
parent f73c7bd6cb
commit f04401031c
10 changed files with 1008 additions and 268 deletions
+50 -6
View File
@@ -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>