Files
ihaleist/server/entitlements.mjs
T
hikmettan f04401031c 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
2026-08-29 10:40:05 +03:00

99 lines
3.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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;
}
}