Files
ihaleist/server/user-store.mjs
T
hikmettanandClaude Fable 5.1 df89eaac33 feat: takip izleme (zeyilname + sonuc bildirimi), hazirlik gorevi, belge sure uyarisi, Telegram, OCR, yasakli sorgu
28 depo taramasindan alinan alti ozellik. Kod kopyalanmadi (depolar
lisanssiz), fikirler uygulandi.

1) Zeyilname / duzeltme ilani takibi (server/zeyilname.mjs): takipteki
   IKN'ler sunucuya yazilir; 3 saatte bir EKAP detayindan ilan sayisi ve
   ihale tarihi karsilastirilir, degisince "ne degisti" bildirimi (uygulama
   ici + e-posta + Telegram). Test: anlik kaydi eskitilince tur "yeni ilan
   yayimlandi; ihale tarihi 01.01.2026 → 08.10.2026" bildirimi uretti.
2) Sonuc bildirimi: takipteki ihale sonuclaninca "X firmasi Y'ye aldi, %Z
   kirim". Yalniz takiptekiler; kismi ihalede oran yazilmaz.
3) Ihaleden goreve (MOBIT-DURT): detayda "Hazirlik Gorevi Ac" → teslim
   tarihi ihale saati, pano "Teklif Hazirlaniyor", derin analizden gelen
   belge kontrol listesi gorevde; Kanban kartinda geri sayim rozeti.
4) Sirket belgesi sure takibi: Belge Kasasi'nda son kullanma tarihi girisi,
   dolmus / 30 gun icinde dolacak uyarisi; girise gunde bir bildirim.
   PATCH /api/me/documents/meta.
5) Telegram (server/telegram.mjs): TELEGRAM_BOT_TOKEN sunucuda; kullanici
   6 haneli kodu bota yazar, "Dogrula" → getUpdates ile chat id baglanir.
   Arayuzde adim adim kurulum rehberi (BotFather → env → Redeploy → kod).
   Ihale ozeti, zeyilname ve sonuc bildirimleri Telegram'a da gider.
6) OCR (tesseract.js, tur): PDF metin katmani bossa (taranmis) ilk 8 sayfa
   OCR. Olcum: sentetik cetvelde guven 87, ~0,3 sn/sayfa; IKN ve kalemler
   dogru okundu. Kalem havuzu ve derin analiz artik taranmis PDF'ten de
   besleniyor.
7) Rakip Radari'nda KIK yasakli sorgusu baglantisi (firma adi panoya
   kopyalanir; EKAP sorguyu API'den vermiyor).

Sinir deger hesaplayici KURULMADI: Kamu Ihale Genel Tebligi'ne ulasilamadi
(mevzuat.gov.tr / ihale.gov.tr bu makineden erisilemiyor); formul kaynaksiz
yazilmadi.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 22:58:13 +03:00

278 lines
11 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',
'firma_profil', // yetkinlikler + uyum eşiği
'takip', // takipteki İKN'ler — zeyilname/sonuç izleme sunucuda çalışır
'gorevler', // ihaleden açılan hazırlık görevleri
'telegram', // { chatId, ad, baglanma }
]);
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);
}
/** Belge üst verisi: son kullanma / kategori (süre takibi için). */
export function updateDocument(email, id, { sonKullanma, kategori } = {}) {
return withDb((db) => {
const r = db.prepare(`UPDATE belgeler SET
son_kullanma = COALESCE(?, son_kullanma),
kategori = COALESCE(?, kategori)
WHERE id=? AND email=?`).run(sonKullanma ?? null, kategori ?? null, id, email);
return r.changes > 0;
}, false);
}
/** Arka plan işleri için tüm kullanıcılar (zeyilname/sonuç izleme). */
export function listUsers() {
return withDb((db) => db.prepare('SELECT email, ad FROM kullanicilar').all(), []);
}
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 });
}