Files
ihaleist/server/user-store.mjs
T

260 lines
9.8 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.
/**
* 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,
ikn TEXT, -- belge bir ihaleye aitse İKN/DTN
olusturma TEXT
);
CREATE INDEX IF NOT EXISTS idx_belge_email ON belgeler(email);
CREATE INDEX IF NOT EXISTS idx_belge_ikn ON belgeler(email, ikn);
`;
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);
// Geçiş: belgeler ihaleye bağlanabilsin diye ikn kolonu eklendi
try {
const k = db.prepare('PRAGMA table_info(belgeler)').all().map((c) => c.name);
if (!k.includes('ikn')) {
db.exec('ALTER TABLE belgeler ADD COLUMN ikn TEXT');
console.log('[user-store] belgeler.ikn kolonu eklendi');
}
} catch (e) {
console.warn('[user-store] belge şema geçişi atlandı:', e.message);
}
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',
'iptal_geri_bildirim',
]);
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, ikn }) {
const id = crypto.randomUUID();
withDb((db) => db.prepare(
'INSERT INTO belgeler (id,email,ad,kategori,boyut,mime,dosya,son_kullanma,ikn,olusturma) VALUES (?,?,?,?,?,?,?,?,?,?)'
).run(id, email, ad, kategori || null, boyut, mime || null, dosya, sonKullanma || null, ikn || null, now()));
return id;
}
export function listDocuments(email, { ikn } = {}) {
const alanlar = 'id, ad, kategori, boyut, mime, son_kullanma, ikn, olusturma';
return withDb((db) => (ikn
? db.prepare(`SELECT ${alanlar} FROM belgeler WHERE email=? AND ikn=? ORDER BY olusturma DESC`).all(email, ikn)
: db.prepare(`SELECT ${alanlar} 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 });
}