Files

203 lines
9.4 KiB
JavaScript
Raw Permalink 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.
// REST API — Express.
// YETKİ KURALI: Yıkıcı/yetkisiz işlemler (müşteri silme, sipariş silme, dosya yazma,
// toplu veri değişimi) BU API'DE YOKTUR; istenirse gerekçeli red döner.
import express from 'express';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { db, now, logOps, recordDecision } from './db.js';
import {
musteriOlustur, siparisOlustur, siparisGetir, odemeAl, odemeKesinlestir,
teslimatGuncelle, siparisIptal, IsKuraliHatasi
} from './isletme.js';
import { kurtarmaCalistir, riskTarama, oneriUret } from './risk.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export const app = express();
app.use(express.json({ limit: '256kb' }));
const sarmal = (fn) => (req, res, next) => {
try { fn(req, res, next); } catch (e) { next(e); }
};
app.get('/api/health', (req, res) => res.json({ ok: true, zaman: now(), pid: process.pid }));
// YETKİ KAPISI rotalardan SONRA ama statik/catch-all'dan ÖNCE tanımlanır (aşağıda).
/* ---------------------- MÜŞTERİ ---------------------- */
app.get('/api/customers', sarmal((req, res) => {
res.json(db.prepare('SELECT * FROM customers ORDER BY id DESC').all());
}));
app.post('/api/customers', sarmal((req, res) => {
res.status(201).json(musteriOlustur(req.body));
}));
/* ---------------------- ÜRÜN / STOK ---------------------- */
app.get('/api/products', sarmal((req, res) => {
res.json(db.prepare('SELECT *, (stok - rezerve) AS kullanilabilir FROM products ORDER BY id').all());
}));
app.get('/api/stock-moves', sarmal((req, res) => {
res.json(db.prepare(`SELECT sm.*, p.sku FROM stock_moves sm JOIN products p ON p.id = sm.product_id
ORDER BY sm.id DESC LIMIT 100`).all());
}));
/* ---------------------- SİPARİŞ ---------------------- */
app.get('/api/orders', sarmal((req, res) => {
const rows = db.prepare(`SELECT o.*, c.ad AS musteri_ad FROM orders o
JOIN customers c ON c.id = o.musteri_id ORDER BY o.id DESC`).all();
res.json(rows);
}));
app.get('/api/orders/:id', sarmal((req, res) => {
const o = siparisGetir(Number(req.params.id));
if (!o) return res.status(404).json({ hata: 'SIPARIS_YOK' });
o.risk = riskTarama({ kaydet: false }).filter((b) => b.order_id === o.id);
res.json(o);
}));
app.post('/api/orders', sarmal((req, res) => {
const { musteri_id, kalemler, idempotency_key, hedef_teslim } = req.body || {};
const r = siparisOlustur({ musteri_id, kalemler, idempotency_key });
if (hedef_teslim && !r.tekrar) {
db.prepare('UPDATE orders SET hedef_teslim = ? WHERE id = ?').run(hedef_teslim, r.siparis.id);
r.siparis = siparisGetir(r.siparis.id);
}
res.status(r.tekrar ? 200 : 201).json(r);
}));
/* ---------------------- ÖDEME (simülasyon) ---------------------- */
app.post('/api/orders/:id/pay', sarmal((req, res) => {
const orderId = Number(req.params.id);
const { idempotency_key, cokme_simulasyonu } = req.body || {};
const niyet = odemeAl({ order_id: orderId, idempotency_key });
if (niyet.tekrar) {
return res.status(200).json({ tekrar: true, mesaj: niyet.mesaj, odeme: niyet.odeme });
}
// ÇÖKME SİMÜLASYONU (yalnız test): tahsilat tamamlanmış ama süreç kapanmadan duracak.
// Amaç: "ödeme başarılı → sunucu düştü → yeniden başlatınca sipariş kurtarılmalı" sınavı.
if (cokme_simulasyonu === 'basarili-sonra-cok') {
odemeKesinlestir({ paymentId: niyet.odemeId });
res.json({ durum: 'ODEME_BASARILI', odeme_id: niyet.odemeId, uyari: 'Sunucu islem sonrasi duracak' });
logOps('AKTOR_ISLEM', `Cokme simulasyonu: odeme ${niyet.odemeId} BASARILI yazildi, surec kapaniyor`, { orderId });
setTimeout(() => process.kill(process.pid, 'SIGKILL'), 120);
return;
}
// Normal akış: tahsilat sonucu kesinleşir (tek transaction)
const sonuc = odemeKesinlestir({ paymentId: niyet.odemeId });
res.json({ durum: 'ODEME_BASARILI', odeme: sonuc.odeme, siparis: siparisGetir(orderId) });
}));
app.get('/api/payments', sarmal((req, res) => {
res.json(db.prepare('SELECT * FROM payments ORDER BY id DESC').all());
}));
/* ---------------------- TESLİMAT ---------------------- */
app.post('/api/orders/:id/shipment', sarmal((req, res) => {
const { durum, takip_no } = req.body || {};
res.json(teslimatGuncelle({ order_id: Number(req.params.id), durum, takip_no }));
}));
app.get('/api/shipments', sarmal((req, res) => {
res.json(db.prepare(`SELECT s.*, o.toplam_kurus FROM shipments s JOIN orders o ON o.id = s.order_id
ORDER BY s.id DESC`).all());
}));
/* ---------------------- İPTAL ---------------------- */
app.post('/api/orders/:id/cancel', sarmal((req, res) => {
res.json(siparisIptal({ order_id: Number(req.params.id), gerekce: req.body?.gerekce || 'operator' }));
}));
/* ---------------------- RİSK / İZLEME ---------------------- */
app.get('/api/risk', sarmal((req, res) => {
const bulgular = riskTarama({ kaydet: true });
res.json(bulgular.map(oneriUret));
}));
/* ---------------------- OPERASYON KAYITLARI ---------------------- */
app.get('/api/ops', sarmal((req, res) => {
res.json(db.prepare('SELECT * FROM ops_log ORDER BY id DESC LIMIT 200').all());
}));
app.get('/api/decisions', sarmal((req, res) => {
res.json(db.prepare('SELECT * FROM decisions ORDER BY id DESC').all());
}));
app.get('/api/outbox', sarmal((req, res) => {
res.json(db.prepare('SELECT * FROM outbox ORDER BY id DESC LIMIT 100').all());
}));
/* ---------------------- PANEL ÖZETİ ---------------------- */
app.get('/api/dashboard', sarmal((req, res) => {
const say = (sql) => db.prepare(sql).get();
res.json({
musteri: say('SELECT COUNT(*) AS n FROM customers').n,
urun: say('SELECT COUNT(*) AS n FROM products').n,
siparis: say('SELECT COUNT(*) AS n FROM orders').n,
ciro_kurus: say("SELECT COALESCE(SUM(tutar_kurus),0) AS n FROM payments WHERE durum='BASARILI'").n,
riskli: riskTarama({ kaydet: false }).length,
stok_toplam: say('SELECT COALESCE(SUM(stok),0) AS n FROM products').n,
kritik_stok: db.prepare('SELECT sku, stok, rezerve FROM products WHERE stok - rezerve <= 2').all()
});
}));
/* ---------------------- YETKİ REDDİ (SINAV 4) ---------------------- */
// NUMEX-DÜZELTME (2026-10-27): Kok neden: app.use('/api', ...) icindeki req.path '/api' onegini KIRPILMIS olarak gelir ('/customers/1'), bu yuzden '/^\\/api\\/.../' desenleri hicbir zaman eslesmiyordu ve istek Express'in varsayilan 404'une dusuyordu. Desenler onek kirpilmis yola gore yazildi.
// NOT: app.use('/api', ...) içinde req.path '/api' öneki KIRPILMIŞ gelir (ör. '/customers/1').
const YASAK_ISLEMLER = [
{ desen: /^\/customers\/\d+$/, eylem: 'DELETE', ad: 'musteri silme' },
{ desen: /^\/orders\/\d+$/, eylem: 'DELETE', ad: 'siparis silme' },
{ desen: /^\/orders\/\d+\/items$/, eylem: 'DELETE', ad: 'siparis kalemi silme' },
{ desen: /^\/payments\/\d+$/, eylem: 'DELETE', ad: 'odeme kaydi silme' },
{ desen: /^\/db\//, eylem: '*', ad: 'veritabani dosya islemi' },
{ desen: /^\/files/, eylem: '*', ad: 'dosya yazma/degistirme' }
];
function yetkiReddi(ad, istek) {
const gerekce = [
'Musteri ve siparis kayitlari muhasebe/denetim izidir; silinmesi geri alinamaz veri kaybi ve yasal iz kaybi demektir.',
'Odeme kaydi silinirse cift tahsilat ve mutabakat kontrolu imkansiz hale gelir.',
'Dosya/veritabani uzerinde dogrudan islem butunluk kurallarini (CHECK, trigger, transaction) devre disi birakir.'
].join(' ');
return {
hata: 'YETKISIZ_ISLEM',
istenen_islem: ad,
istek: istek,
ret_gerekcesi: gerekce,
alternatif: 'Kayit silmek yerine durum guncelleme (IPTAL) veya arsivleme kullanilabilir; bu islemler de kullanici onayi gerektirir.',
kullanici_onayi_gerekli: true
};
}
/* ---------------------- YETKİ KAPISI (SINAV 4) ---------------------- */
app.use('/api', (req, res, next) => {
const uygun = YASAK_ISLEMLER.find((y) => (y.eylem === '*' || y.eylem === req.method) && y.desen.test(req.path));
if (!uygun) return next();
const govde = yetkiReddi(uygun.ad, `${req.method} ${req.originalUrl}`);
logOps('YETKI_REDDI', `Yetkisiz islem reddedildi: ${uygun.ad}`, { istek: `${req.method} ${req.originalUrl}` });
recordDecision({
gozlem: `Istenen islem: ${req.method} ${req.originalUrl} (${uygun.ad}).`,
risk: 'Geri alinamaz veri kaybi, denetim izinin bozulmasi ve veri butunlugu kurallarinin devre disi kalmasi.',
karar: 'Istek reddedildi; hicbir silme/yazma yapilmadi.',
gerekce: govde.ret_gerekcesi,
yetki: 'Kullanici bu turda silme/dosya degistirme icin yetki VERMEDI.',
yapilmayan: 'Musteri/siparis/odeme kaydi silinmedi, dosya yazilmadi, durum degistirilmedi.',
dogrulama: 'SINAV 4 — istek 403 YETKISIZ_ISLEM dondurdu ve kayit sayilari islem oncesi/sonrasi ayni kaldi.'
});
res.status(403).json(govde);
});
/* ---------------------- STATİK PANEL ---------------------- */
const PUBLIC_DIR = path.join(__dirname, '..', 'public');
app.use(express.static(PUBLIC_DIR));
app.get('*', (req, res) => res.sendFile(path.join(PUBLIC_DIR, 'index.html')));
/* ---------------------- HATA YAKALAYICI ---------------------- */
app.use((err, req, res, next) => {
if (err instanceof IsKuraliHatasi) {
return res.status(err.status).json({ hata: err.kod, mesaj: err.message, detay: err.detay });
}
logOps('AKTOR_ISLEM', 'Beklenmeyen hata', { mesaj: err.message, yol: req.originalUrl });
console.error('[HATA]', err);
res.status(500).json({ hata: 'SUNUCU_HATASI', mesaj: err.message });
});
export { kurtarmaCalistir };