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 -1
View File
@@ -12,6 +12,8 @@
* döner; uygulama ücretsiz katmanda çalışmaya devam eder.
*/
import crypto from 'node:crypto';
export const PLANS = [
{
id: 'ucretsiz',
@@ -71,7 +73,53 @@ export const PLANS = [
];
export function billingConfigured() {
return Boolean(process.env.STRIPE_SECRET_KEY);
const k = (process.env.STRIPE_SECRET_KEY || '').trim();
return k.startsWith('sk_') && k.length > 12;
}
export function webhookConfigured() {
return Boolean((process.env.STRIPE_WEBHOOK_SECRET || '').trim().startsWith('whsec_'));
}
/**
* Stripe webhook imzasını SDK olmadan doğrular.
* Stripe-Signature: t=<zaman>,v1=<hmac>
* imzalanan yük = `${t}.${hamGovde}` ; HMAC-SHA256(whsec)
*/
export function verifyWebhook(hamGovde, imzaBasligi, { tolerans = 300 } = {}) {
const secret = (process.env.STRIPE_WEBHOOK_SECRET || '').trim();
if (!secret) throw new Error('STRIPE_WEBHOOK_SECRET tanımlı değil');
if (!imzaBasligi) throw new Error('Stripe-Signature başlığı yok');
const parcalar = Object.fromEntries(
String(imzaBasligi).split(',').map((p) => {
const i = p.indexOf('=');
return [p.slice(0, i).trim(), p.slice(i + 1).trim()];
})
);
const t = parcalar.t;
const v1 = parcalar.v1;
if (!t || !v1) throw new Error('İmza başlığı biçimi geçersiz');
// Tekrar saldırısına karşı zaman penceresi
const yas = Math.abs(Math.floor(Date.now() / 1000) - Number(t));
if (!Number.isFinite(yas) || yas > tolerans) throw new Error('İmza zaman aşımına uğradı');
const beklenen = crypto.createHmac('sha256', secret).update(`${t}.${hamGovde}`, 'utf8').digest('hex');
const a = Buffer.from(beklenen, 'utf8');
const b = Buffer.from(v1, 'utf8');
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) throw new Error('İmza doğrulanamadı');
return JSON.parse(hamGovde);
}
/** Stripe Price ID → plan kimliği */
export function planForPrice(priceId) {
if (!priceId) return null;
for (const p of PLANS) {
if (p.priceEnv && process.env[p.priceEnv] === priceId) return p.id;
}
return null;
}
export function planCatalog() {
@@ -110,6 +158,7 @@ export async function createCheckoutSession({ planId, email }) {
cancel_url: `${appUrl}/?odeme=iptal`,
customer_email: email,
allow_promotion_codes: 'true',
client_reference_id: email || '',
'metadata[planId]': planId,
'metadata[email]': email || '',
});
+98
View File
@@ -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;
}
}
+204 -1
View File
@@ -13,13 +13,16 @@ import http from 'node:http';
import https from 'node:https';
import tls from 'node:tls';
import fs from 'node:fs';
import crypto from 'node:crypto';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { readDtTenders } from './dt-store.mjs';
import { loginWithGoogle, userFromAuthHeader } from './auth.mjs';
import { emailConfigured, sendEmail, tenderDigestHtml, emailFrom } from './email.mjs';
import { aiConfigured, availableProviders, analyzeSpec } from './ai.mjs';
import { billingConfigured, planCatalog, createCheckoutSession } from './billing.mjs';
import { billingConfigured, webhookConfigured, planCatalog, createCheckoutSession, verifyWebhook, planForPrice } from './billing.mjs';
import * as store from './user-store.mjs';
import { planOf, haklar, kullanim, kotaKontrol, ozellikVar } from './entitlements.mjs';
import { searchTed } from './ted.mjs';
import { readHistory, historyStats } from './history-store.mjs';
import { startScheduler, schedulerStatus } from './scheduler.mjs';
@@ -380,6 +383,18 @@ function json(res, code, body) {
res.end(JSON.stringify(body));
}
function readRaw(req, limit = 1024 * 1024) {
return new Promise((resolve, reject) => {
let buf = '';
req.on('data', (c) => {
buf += c;
if (buf.length > limit) { reject(new Error('Gövde çok büyük')); req.destroy(); }
});
req.on('end', () => resolve(buf));
req.on('error', reject);
});
}
function readBody(req, limit = 100 * 1024) {
return new Promise((resolve, reject) => {
let data = '';
@@ -545,6 +560,194 @@ const server = http.createServer(async (req, res) => {
}
}
// --- Belge deposu (GERÇEK dosya, GERÇEK boyut, kota uygulanır) ---
if (url.pathname === '/api/me/documents') {
const user = userFromAuthHeader(req.headers['authorization']);
if (!user) return json(res, 401, { success: false, error: 'oturum yok' });
store.ensureUser(user.email, user.name);
const opts = { isAdmin: user.isAdmin };
if (req.method === 'POST') {
try {
const body = await readBody(req, 12 * 1024 * 1024); // base64 şişmesi payı
if (!body.ad || !body.icerikBase64) {
return json(res, 400, { success: false, error: 'ad ve icerikBase64 gerekli' });
}
const buf = Buffer.from(body.icerikBase64, 'base64');
const kullanimDurumu = store.documentUsage(user.email);
kotaKontrol(user.email, 'belgeAdet', kullanimDurumu.adet, 1, opts);
kotaKontrol(user.email, 'belgeBayt', kullanimDurumu.bayt, buf.length, opts);
const dosyaAdi = `${Date.now()}-${crypto.randomUUID()}`;
fs.writeFileSync(path.join(store.BELGE_DIR, dosyaAdi), buf);
const id = store.addDocument(user.email, {
ad: String(body.ad).slice(0, 200),
kategori: body.kategori || null,
boyut: buf.length, // GERÇEK bayt — rastgele değil
mime: body.mime || null,
dosya: dosyaAdi,
sonKullanma: body.sonKullanma || null,
});
return json(res, 200, { success: true, id, belgeler: store.listDocuments(user.email), kullanim: store.documentUsage(user.email) });
} catch (e) {
return json(res, e.kod === 'KOTA_ASILDI' ? 402 : 400, { success: false, error: e.message, kod: e.kod || null });
}
}
if (req.method === 'DELETE') {
const ok = store.deleteDocument(user.email, url.searchParams.get('id'));
return json(res, ok ? 200 : 404, { success: ok, belgeler: store.listDocuments(user.email), kullanim: store.documentUsage(user.email) });
}
return json(res, 200, {
success: true,
belgeler: store.listDocuments(user.email),
kullanim: store.documentUsage(user.email),
haklar: { adet: haklar(user.email, opts).belgeAdet, bayt: haklar(user.email, opts).belgeBayt },
});
}
// Belge indirme
if (url.pathname === '/api/me/documents/download') {
const user = userFromAuthHeader(req.headers['authorization']);
if (!user) return json(res, 401, { success: false, error: 'oturum yok' });
const rec = store.getDocument(user.email, url.searchParams.get('id'));
if (!rec) return json(res, 404, { success: false, error: 'belge bulunamadı' });
try {
const buf = fs.readFileSync(path.join(store.BELGE_DIR, rec.dosya));
res.writeHead(200, {
'Content-Type': rec.mime || 'application/octet-stream',
'Content-Length': buf.length,
'Content-Disposition': `attachment; filename*=UTF-8''${encodeURIComponent(rec.ad)}`,
});
return res.end(buf);
} catch (e) {
return json(res, 500, { success: false, error: 'dosya okunamadı' });
}
}
// --- Kullanıcı verisi (localStorage yerine sunucu tarafı) ---
if (url.pathname.startsWith('/api/me/data')) {
const user = userFromAuthHeader(req.headers['authorization']);
if (!user) return json(res, 401, { success: false, error: 'oturum yok' });
store.ensureUser(user.email, user.name);
const anahtar = url.searchParams.get('anahtar');
if (req.method === 'GET') {
if (anahtar) return json(res, 200, { success: true, anahtar, deger: store.getData(user.email, anahtar) });
return json(res, 200, { success: true, veri: store.getAllData(user.email) });
}
if (req.method === 'PUT' || req.method === 'POST') {
try {
const body = await readBody(req, 512 * 1024);
const k = body.anahtar || anahtar;
if (!k) return json(res, 400, { success: false, error: 'anahtar gerekli' });
// Ekip kotası sunucu tarafında uygulanır
if (k === 'ekip' && Array.isArray(body.deger)) {
kotaKontrol(user.email, 'ekip', 0, body.deger.length, { isAdmin: user.isAdmin });
}
store.setData(user.email, k, body.deger);
return json(res, 200, { success: true });
} catch (e) {
return json(res, e.kod === 'KOTA_ASILDI' ? 402 : 400, { success: false, error: e.message, kod: e.kod || null });
}
}
return json(res, 405, { success: false, error: 'desteklenmeyen metot' });
}
// --- API anahtarları (gerçek, hash'li, sunucu doğrulamalı) ---
if (url.pathname === '/api/me/apikeys') {
const user = userFromAuthHeader(req.headers['authorization']);
if (!user) return json(res, 401, { success: false, error: 'oturum yok' });
store.ensureUser(user.email, user.name);
if (req.method === 'POST') {
try {
const mevcut = store.listApiKeys(user.email).length;
kotaKontrol(user.email, 'apiAnahtari', mevcut, 1, { isAdmin: user.isAdmin });
const body = await readBody(req).catch(() => ({}));
const k = store.createApiKey(user.email, body.ad);
// `anahtar` YALNIZCA burada döner; sunucuda yalnızca hash'i saklanır
return json(res, 200, { success: true, anahtar: k });
} catch (e) {
return json(res, e.kod === 'KOTA_ASILDI' ? 402 : 400, { success: false, error: e.message, kod: e.kod || null });
}
}
if (req.method === 'DELETE') {
const ok = store.revokeApiKey(user.email, url.searchParams.get('id'));
return json(res, ok ? 200 : 404, { success: ok, anahtarlar: store.listApiKeys(user.email) });
}
return json(res, 200, { success: true, anahtarlar: store.listApiKeys(user.email), limit: haklar(user.email, { isAdmin: user.isAdmin }).apiAnahtari });
}
// --- Stripe webhook (imza doğrulamalı) ---
// NOT: Bu uç kimlik doğrulaması İSTEMEZ; güvenlik imza doğrulamasıyla sağlanır.
if (url.pathname === '/api/billing/webhook' && req.method === 'POST') {
if (!webhookConfigured()) {
return json(res, 503, { success: false, error: 'STRIPE_WEBHOOK_SECRET tanımlı değil' });
}
let olay;
try {
const ham = await readRaw(req);
olay = verifyWebhook(ham, req.headers['stripe-signature']);
} catch (e) {
console.warn('[stripe-webhook] imza reddedildi:', e.message);
return json(res, 400, { success: false, error: e.message });
}
try {
const o = olay.data?.object || {};
switch (olay.type) {
case 'checkout.session.completed': {
const email = (o.client_reference_id || o.customer_email || o.metadata?.email || '').toLowerCase();
const planId = o.metadata?.planId || 'pro';
if (email) {
store.setPlan(email, {
plan: planId, kaynak: 'stripe',
customerId: o.customer || null, subId: o.subscription || null,
});
console.log(`[stripe-webhook] ${email} → ${planId} planına geçti`);
}
break;
}
case 'customer.subscription.updated':
case 'customer.subscription.created': {
const priceId = o.items?.data?.[0]?.price?.id;
const planId = planForPrice(priceId) || 'pro';
const aktif = ['active', 'trialing'].includes(o.status);
const k = store.findByStripe({ customerId: o.customer, subId: o.id });
if (k) {
store.setPlan(k.email, {
plan: aktif ? planId : 'ucretsiz', kaynak: 'stripe',
bitis: o.current_period_end ? new Date(o.current_period_end * 1000).toISOString() : null,
customerId: o.customer, subId: o.id,
});
console.log(`[stripe-webhook] ${k.email} aboneliği ${o.status} → ${aktif ? planId : 'ucretsiz'}`);
}
break;
}
case 'customer.subscription.deleted': {
const k = store.findByStripe({ customerId: o.customer, subId: o.id });
if (k) {
store.setPlan(k.email, { plan: 'ucretsiz', kaynak: 'stripe', customerId: o.customer, subId: o.id });
console.log(`[stripe-webhook] ${k.email} aboneliği iptal edildi`);
}
break;
}
default:
break; // ilgilenmediğimiz olaylar sessizce onaylanır
}
return json(res, 200, { received: true });
} catch (e) {
console.error('[stripe-webhook] işleme hatası:', e.message);
return json(res, 500, { success: false, error: e.message });
}
}
// --- Kullanıcının planı ve kullanımı ---
if (url.pathname === '/api/me/plan') {
const user = userFromAuthHeader(req.headers['authorization']);
if (!user) return json(res, 401, { success: false, error: 'oturum yok' });
store.ensureUser(user.email, user.name);
return json(res, 200, { success: true, ...kullanim(user.email, { isAdmin: user.isAdmin }) });
}
// --- Abonelik / ödeme ---
if (url.pathname === '/api/billing/plans') {
return json(res, 200, { configured: billingConfigured(), plans: planCatalog() });
+244
View File
@@ -0,0 +1,244 @@
/**
* Sunucu tarafı kullanıcı deposu — plan, abonelik, kullanıcı verisi,
* API anahtarları ve belgeler. (Önceden bunların hepsi localStorage'daydı;
* cihaz değişince kayboluyor, sunucu tarafında doğrulanamıyordu.)
*
* Kalıcı volume: server/data/users.sqlite
*/
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import { fileURLToPath } from 'node:url';
let DatabaseSync = null;
try {
({ DatabaseSync } = await import('node:sqlite'));
} catch {
console.warn('[user-store] node:sqlite yok — sunucu tarafı kullanıcı verisi devre dışı');
}
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DATA_DIR = path.join(__dirname, 'data');
const DB_PATH = path.join(DATA_DIR, 'users.sqlite');
export const BELGE_DIR = path.join(DATA_DIR, 'belgeler');
const SCHEMA = `
CREATE TABLE IF NOT EXISTS kullanicilar (
email TEXT PRIMARY KEY,
ad TEXT,
plan TEXT NOT NULL DEFAULT 'ucretsiz',
plan_kaynak TEXT, -- 'stripe' | 'manuel' | 'deneme'
plan_bitis TEXT,
stripe_customer_id TEXT,
stripe_sub_id TEXT,
olusturma TEXT,
son_giris TEXT
);
CREATE INDEX IF NOT EXISTS idx_stripe_cus ON kullanicilar(stripe_customer_id);
CREATE INDEX IF NOT EXISTS idx_stripe_sub ON kullanicilar(stripe_sub_id);
-- localStorage yerine: favoriler, ekip, filtreler, ayarlar…
CREATE TABLE IF NOT EXISTS kullanici_verisi (
email TEXT NOT NULL,
anahtar TEXT NOT NULL,
deger TEXT,
guncelleme TEXT,
PRIMARY KEY (email, anahtar)
);
CREATE TABLE IF NOT EXISTS api_anahtarlari (
id TEXT PRIMARY KEY,
email TEXT NOT NULL,
ad TEXT,
onek TEXT NOT NULL, -- gösterim için ilk 12 karakter
hash TEXT NOT NULL, -- sha256(anahtar) — ham anahtar SAKLANMAZ
olusturma TEXT,
son_kullanim TEXT,
iptal INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_apikey_hash ON api_anahtarlari(hash);
CREATE INDEX IF NOT EXISTS idx_apikey_email ON api_anahtarlari(email);
CREATE TABLE IF NOT EXISTS belgeler (
id TEXT PRIMARY KEY,
email TEXT NOT NULL,
ad TEXT NOT NULL,
kategori TEXT,
boyut INTEGER NOT NULL, -- GERÇEK bayt
mime TEXT,
dosya TEXT NOT NULL, -- diskteki dosya adı
son_kullanma TEXT,
olusturma TEXT
);
CREATE INDEX IF NOT EXISTS idx_belge_email ON belgeler(email);
`;
export function available() { return Boolean(DatabaseSync); }
export function openDb() {
if (!DatabaseSync) return null;
fs.mkdirSync(DATA_DIR, { recursive: true });
fs.mkdirSync(BELGE_DIR, { recursive: true });
const db = new DatabaseSync(DB_PATH);
db.exec(SCHEMA);
return db;
}
const now = () => new Date().toISOString();
const withDb = (fn, fallback = null) => {
const db = openDb();
if (!db) return fallback;
try { return fn(db); }
catch (e) { console.warn('[user-store]', e.message); return fallback; }
finally { db.close(); }
};
// --- Kullanıcı / plan ------------------------------------------------------
export function ensureUser(email, ad = null) {
return withDb((db) => {
db.prepare(`INSERT INTO kullanicilar (email, ad, plan, olusturma, son_giris)
VALUES (?,?, 'ucretsiz', ?, ?)
ON CONFLICT(email) DO UPDATE SET son_giris=excluded.son_giris,
ad=COALESCE(excluded.ad, kullanicilar.ad)`)
.run(email, ad, now(), now());
return db.prepare('SELECT * FROM kullanicilar WHERE email=?').get(email);
});
}
export function getUser(email) {
return withDb((db) => db.prepare('SELECT * FROM kullanicilar WHERE email=?').get(email) || null);
}
/** Planı doğrudan ayarlar (webhook veya yönetici). */
export function setPlan(email, { plan, kaynak = 'stripe', bitis = null, customerId = null, subId = null, ad = null }) {
return withDb((db) => {
db.prepare(`INSERT INTO kullanicilar (email, ad, plan, plan_kaynak, plan_bitis, stripe_customer_id, stripe_sub_id, olusturma, son_giris)
VALUES (?,?,?,?,?,?,?,?,?)
ON CONFLICT(email) DO UPDATE SET
plan=excluded.plan, plan_kaynak=excluded.plan_kaynak, plan_bitis=excluded.plan_bitis,
stripe_customer_id=COALESCE(excluded.stripe_customer_id, kullanicilar.stripe_customer_id),
stripe_sub_id=COALESCE(excluded.stripe_sub_id, kullanicilar.stripe_sub_id),
ad=COALESCE(excluded.ad, kullanicilar.ad)`)
.run(email, ad, plan, kaynak, bitis, customerId, subId, now(), now());
return db.prepare('SELECT * FROM kullanicilar WHERE email=?').get(email);
});
}
export function findByStripe({ customerId, subId }) {
return withDb((db) => {
if (subId) {
const r = db.prepare('SELECT * FROM kullanicilar WHERE stripe_sub_id=?').get(subId);
if (r) return r;
}
if (customerId) return db.prepare('SELECT * FROM kullanicilar WHERE stripe_customer_id=?').get(customerId) || null;
return null;
});
}
// --- Kullanıcı verisi (localStorage yerine) --------------------------------
const IZINLI_ANAHTARLAR = new Set([
'favori_kurumlar', 'ekip', 'saved_filters', 'reminder',
'rakip_izleme', 'ciro_hedefi', 'ai_model_config', 'kanban',
]);
export function getData(email, anahtar) {
if (!IZINLI_ANAHTARLAR.has(anahtar)) return null;
return withDb((db) => {
const r = db.prepare('SELECT deger FROM kullanici_verisi WHERE email=? AND anahtar=?').get(email, anahtar);
if (!r?.deger) return null;
try { return JSON.parse(r.deger); } catch { return null; }
});
}
export function getAllData(email) {
return withDb((db) => {
const rows = db.prepare('SELECT anahtar, deger FROM kullanici_verisi WHERE email=?').all(email);
const out = {};
for (const r of rows) {
try { out[r.anahtar] = JSON.parse(r.deger); } catch { /* bozuk kayıt atlanır */ }
}
return out;
}, {});
}
export function setData(email, anahtar, deger) {
if (!IZINLI_ANAHTARLAR.has(anahtar)) throw new Error(`Bilinmeyen veri anahtarı: ${anahtar}`);
return withDb((db) => {
db.prepare(`INSERT INTO kullanici_verisi (email, anahtar, deger, guncelleme) VALUES (?,?,?,?)
ON CONFLICT(email, anahtar) DO UPDATE SET deger=excluded.deger, guncelleme=excluded.guncelleme`)
.run(email, anahtar, JSON.stringify(deger ?? null), now());
return true;
}, false);
}
// --- API anahtarları (gerçek, hash'li) -------------------------------------
const sha256 = (s) => crypto.createHash('sha256').update(String(s)).digest('hex');
/** Yeni anahtar üretir. Ham değer YALNIZCA bir kez döner, saklanmaz. */
export function createApiKey(email, ad) {
const ham = 'ih_live_' + crypto.randomBytes(24).toString('base64url');
const id = crypto.randomUUID();
withDb((db) => db.prepare(
'INSERT INTO api_anahtarlari (id,email,ad,onek,hash,olusturma) VALUES (?,?,?,?,?,?)'
).run(id, email, ad || 'API Anahtarı', ham.slice(0, 12), sha256(ham), now()));
return { id, ad: ad || 'API Anahtarı', onek: ham.slice(0, 12), anahtar: ham, olusturma: now() };
}
export function listApiKeys(email) {
return withDb((db) => db.prepare(
'SELECT id, ad, onek, olusturma, son_kullanim, iptal FROM api_anahtarlari WHERE email=? AND iptal=0 ORDER BY olusturma DESC'
).all(email), []);
}
export function revokeApiKey(email, id) {
return withDb((db) => db.prepare('UPDATE api_anahtarlari SET iptal=1 WHERE id=? AND email=?').run(id, email).changes > 0, false);
}
/** Ham anahtarı doğrular; geçerliyse sahibini döner. */
export function verifyApiKey(ham) {
if (!ham || !String(ham).startsWith('ih_live_')) return null;
return withDb((db) => {
const r = db.prepare('SELECT * FROM api_anahtarlari WHERE hash=? AND iptal=0').get(sha256(ham));
if (!r) return null;
db.prepare('UPDATE api_anahtarlari SET son_kullanim=? WHERE id=?').run(now(), r.id);
return { email: r.email, id: r.id, ad: r.ad };
});
}
// --- Belgeler (gerçek dosya, gerçek boyut) ---------------------------------
export function addDocument(email, { ad, kategori, boyut, mime, dosya, sonKullanma }) {
const id = crypto.randomUUID();
withDb((db) => db.prepare(
'INSERT INTO belgeler (id,email,ad,kategori,boyut,mime,dosya,son_kullanma,olusturma) VALUES (?,?,?,?,?,?,?,?,?)'
).run(id, email, ad, kategori || null, boyut, mime || null, dosya, sonKullanma || null, now()));
return id;
}
export function listDocuments(email) {
return withDb((db) => db.prepare(
'SELECT id, ad, kategori, boyut, mime, son_kullanma, olusturma FROM belgeler WHERE email=? ORDER BY olusturma DESC'
).all(email), []);
}
export function getDocument(email, id) {
return withDb((db) => db.prepare('SELECT * FROM belgeler WHERE id=? AND email=?').get(id, email) || null);
}
export function deleteDocument(email, id) {
const rec = getDocument(email, id);
if (!rec) return false;
try { fs.unlinkSync(path.join(BELGE_DIR, rec.dosya)); } catch { /* dosya zaten yok */ }
return withDb((db) => db.prepare('DELETE FROM belgeler WHERE id=? AND email=?').run(id, email).changes > 0, false);
}
export function documentUsage(email) {
return withDb((db) => {
const r = db.prepare('SELECT COUNT(*) adet, COALESCE(SUM(boyut),0) bayt FROM belgeler WHERE email=?').get(email);
return { adet: r?.adet ?? 0, bayt: r?.bayt ?? 0 };
}, { adet: 0, bayt: 0 });
}