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:
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Plan hakları ve kota uygulaması.
|
||||
* Önceden billing.mjs'te `sinirlar` tanımlıydı ama HİÇBİR yerde kontrol
|
||||
* edilmiyordu — herkes her şeyi kullanabiliyordu. Burada gerçekten uygulanır.
|
||||
*/
|
||||
|
||||
import { getUser, documentUsage, listApiKeys, getData } from './user-store.mjs';
|
||||
|
||||
export const PLAN_HAKLARI = {
|
||||
ucretsiz: {
|
||||
ad: 'Ücretsiz',
|
||||
takip: 10, // takip edilebilecek ihale
|
||||
ekip: 1, // ekip üyesi
|
||||
bildirimKriteri: 1, // otomatik bildirim kriteri
|
||||
belgeAdet: 5,
|
||||
belgeBayt: 25 * 1024 * 1024,
|
||||
apiAnahtari: 0,
|
||||
ozellikler: ['arama', 'favori', 'push'],
|
||||
},
|
||||
pro: {
|
||||
ad: 'Pro',
|
||||
takip: null, // sınırsız
|
||||
ekip: 5,
|
||||
bildirimKriteri: 10,
|
||||
belgeAdet: 200,
|
||||
belgeBayt: 2 * 1024 * 1024 * 1024,
|
||||
apiAnahtari: 0,
|
||||
ozellikler: ['arama', 'favori', 'push', 'kanban', 'rakip', 'fiyat', 'ai', 'pdf', 'eposta'],
|
||||
},
|
||||
kurumsal: {
|
||||
ad: 'Kurumsal',
|
||||
takip: null,
|
||||
ekip: null,
|
||||
bildirimKriteri: null,
|
||||
belgeAdet: null,
|
||||
belgeBayt: 20 * 1024 * 1024 * 1024,
|
||||
apiAnahtari: 10,
|
||||
ozellikler: ['arama', 'favori', 'push', 'kanban', 'rakip', 'fiyat', 'ai', 'pdf', 'eposta', 'kirmizibayrak', 'kik', 'ted', 'api'],
|
||||
},
|
||||
};
|
||||
|
||||
/** Kullanıcının etkin planı (süresi dolmuşsa ücretsize düşer). */
|
||||
export function planOf(email, { isAdmin = false } = {}) {
|
||||
if (isAdmin) return 'kurumsal'; // yöneticiler tam erişimli
|
||||
const u = getUser(email);
|
||||
if (!u) return 'ucretsiz';
|
||||
if (u.plan_bitis && new Date(u.plan_bitis) < new Date()) return 'ucretsiz';
|
||||
return PLAN_HAKLARI[u.plan] ? u.plan : 'ucretsiz';
|
||||
}
|
||||
|
||||
export function haklar(email, opts) {
|
||||
return PLAN_HAKLARI[planOf(email, opts)] || PLAN_HAKLARI.ucretsiz;
|
||||
}
|
||||
|
||||
export function ozellikVar(email, ozellik, opts) {
|
||||
return haklar(email, opts).ozellikler.includes(ozellik);
|
||||
}
|
||||
|
||||
/** Mevcut kullanım — arayüzde "3/10" göstermek için. */
|
||||
export function kullanim(email, opts) {
|
||||
const h = haklar(email, opts);
|
||||
const belge = documentUsage(email);
|
||||
const ekip = (getData(email, 'ekip') || []).length;
|
||||
const takip = (getData(email, 'kanban') || []).filter?.((t) => t?.takipte)?.length ?? null;
|
||||
return {
|
||||
plan: planOf(email, opts),
|
||||
planAdi: h.ad,
|
||||
haklar: h,
|
||||
kullanilan: {
|
||||
belgeAdet: belge.adet,
|
||||
belgeBayt: belge.bayt,
|
||||
ekip,
|
||||
apiAnahtari: listApiKeys(email).length,
|
||||
takip,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Kota kontrolü. Aşılırsa açıklayıcı hata fırlatır.
|
||||
* @param {string} tur 'ekip' | 'belgeAdet' | 'belgeBayt' | 'apiAnahtari' | 'bildirimKriteri' | 'takip'
|
||||
*/
|
||||
export function kotaKontrol(email, tur, mevcutSayi, ekleme = 1, opts) {
|
||||
const h = haklar(email, opts);
|
||||
const limit = h[tur];
|
||||
if (limit === null || limit === undefined) return; // sınırsız
|
||||
if (mevcutSayi + ekleme > limit) {
|
||||
const isim = {
|
||||
ekip: 'ekip üyesi', belgeAdet: 'belge', belgeBayt: 'belge alanı',
|
||||
apiAnahtari: 'API anahtarı', bildirimKriteri: 'bildirim kriteri', takip: 'ihale takibi',
|
||||
}[tur] || tur;
|
||||
const e = new Error(`${h.ad} planında en fazla ${limit} ${isim} hakkınız var. Yükseltmek için Planlar sekmesine bakın.`);
|
||||
e.kod = 'KOTA_ASILDI';
|
||||
e.limit = limit;
|
||||
e.plan = h.ad;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user