PageSourceSearch

https://morfoz.ai/public/admin-callbacks.js?v=1790769371

js morfoz.ai collected 2026-10-05 15:08:42 UTC 12,031 bytes, 252 lines download raw bytes

1// Geri Çağırma (Callback) adresleri — admin ekranı.
2//
3// Dış sistemler (Salesforce, SAP, herhangi bir CRM/ERP) bize dönebilsin diye
4// üretilen adresler. Adresin KENDİSİ sırdır: `token` tahmin edilemez.
5//
6// İKİ KURAL BU DOSYAYI ŞEKİLLENDİRİYOR:
7//   1. Tam adres LİSTEDE GÖSTERİLMEZ — yalnız "Göster" ile tekil uçtan alınır.
8//      Listeyi açan herkese her entegrasyonun canlı adresini vermek olurdu.
9//   2. prompt()/confirm() KULLANILMAZ. Tarayıcı bunları susturduğunda akış
10//      SESSİZCE ölüyor (bkz. panel-tarayici-kutulari). Onay `onay.js`'ten.
11import { api, escapeHtml, toast } from './api-client.js';
12import { onayla } from './onay.js';
13
14const API = '/api/admin/callbacks';
15
16// Kaçış paylaşılan istemciden gelir; ikinci bir kopya zamanla ondan sapardı.
17const esc = (v) => escapeHtml(String(v ?? ''));
18
19function zaman(v) {
20  if (!v) return '—';
21  const d = new Date(String(v).replace(' ', 'T') + (String(v).endsWith('Z') ? '' : 'Z'));
22  return isNaN(d) ? esc(v) : d.toLocaleString('tr-TR');
23}
24
25/**
26 * @param host  Çizim yapılacak kap.
27 * @param opts  { provider_id, assistant_id, channel_id, baslik }
28 *   `provider_id` verilirse ekran O ENTEGRASYONA kilitlenir: liste yalnız o
29 *   sağlayıcının adreslerini gösterir, yeni adres formunda sağlayıcı alanı
30 *   sabittir. Entegrasyon merkezindeki kart bu kipi kullanıyor — "hangi
31 *   sağlayıcının adresi hangisiydi" sorusu, elle doğru dizeyi yazmaya
32 *   bırakılmasın diye (yanlış yazılan sağlayıcı sessizce çalışmayan bir
33 *   adres üretiyordu).
34 */
35export function mountCallbacks(host, opts = {}) {
36  if (!host) return;
37
38  const kapsam = String(opts.provider_id || '');
39  const cek = () => api.get(kapsam
40    ? `${API}?provider_id=${encodeURIComponent(kapsam)}`
41    : API);
42
43  function satir(k) {
44    const durum = k.status === 'active'
45      ? '<span class="cb-rozet cb-ok">aktif</span>'
46      : '<span class="cb-rozet cb-off">iptal</span>';
47    const imza = k.imza_var
48      ? '<span class="cb-rozet cb-ok">imzalı</span>'
49      : '<span class="cb-rozet cb-warn">imzasız</span>';
50    const hata = k.fail_count
51      ? `<span class="cb-rozet cb-err" title="${esc(k.last_error)}">${k.fail_count} hata</span>`
52      : '';
53    return `<tr data-id="${k.id}">
54      <td>
55        <div class="cb-ad">${esc(k.label || '(adsız)')}</div>
56        <div class="cb-alt">${esc(k.provider_id)}${k.assistant_id ? ' · asistan #' + k.assistant_id : ''}</div>
57      </td>
58      <td><code class="cb-jeton">${esc(k.token_masked)}</code></td>
59      <td>${durum} ${imza} ${hata}</td>
60      <td>
61        <div>${k.call_count || 0} çağrı</div>
62        <div class="cb-alt">${zaman(k.last_called_at)}</div>
63      </td>
64      <td class="cb-aksiyon">
65        <button class="cb-btn" data-act="show">Göster</button>
66        <button class="cb-btn" data-act="rotate">Adresi yenile</button>
67        ${k.status === 'active'
68          ? '<button class="cb-btn cb-danger" data-act="revoke">İptal</button>'
69          : '<button class="cb-btn" data-act="restore">Geri aç</button>'}
70      </td>
71    </tr>`;
72  }
73
74  async function ciz() {
75    host.innerHTML = '<div class="cb-bos">Yükleniyor…</div>';
76    let d;
77    try {
78      d = await cek();
79    } catch (e) {
80      host.innerHTML = `<div class="cb-hata">${esc(e.message)}</div>`;
81      return;
82    }
83    const o = d.ozet || {};
84    const kayitlar = d.kayitlar || [];
85    // Kapsamlı kipte ÜST SATIR YANILTICI olurdu: `ozet` sunucuda her zaman
86    // TÜM adresleri sayar. Bir entegrasyonun kartında "3 aktif adres" yazıp
87    // altta tek satır göstermek, okuyanı yanlış yere bakmaya iter.
88    const ozet = kapsam
89      ? `<div class="cb-ozet">
90           <div class="cb-kutu"><b>${kayitlar.filter((k) => k.status === 'active').length}</b><span>bu entegrasyonda aktif</span></div>
91           <div class="cb-kutu"><b>${kayitlar.reduce((t, k) => t + (k.call_count || 0), 0)}</b><span>gelen çağrı</span></div>
92           <div class="cb-kutu ${kayitlar.some((k) => k.fail_count) ? 'cb-kotu' : ''}"><b>${kayitlar.reduce((t, k) => t + (k.fail_count || 0), 0)}</b><span>başarısız</span></div>
93         </div>`
94      : `<div class="cb-ozet">
95           <div class="cb-kutu"><b>${o.aktif || 0}</b><span>aktif adres</span></div>
96           <div class="cb-kutu"><b>${o.toplam || 0}</b><span>toplam</span></div>
97           <div class="cb-kutu ${o.hata ? 'cb-kotu' : ''}"><b>${o.hata || 0}</b><span>başarısız çağrı</span></div>
98           <div class="cb-kutu ${o.reddedilen_24s ? 'cb-kotu' : ''}"><b>${o.reddedilen_24s || 0}</b><span>reddedilen (24s)</span></div>
99         </div>`;
100    host.innerHTML = `
101      ${ozet}
102      <div class="cb-arac">
103        <button class="cb-btn cb-primary" id="cbYeni">Yeni adres üret</button>
104        ${kapsam ? '' : '<button class="cb-btn" id="cbRed">Reddedilen denemeler</button>'}
105      </div>
106      <div id="cbForm" hidden></div>
107      <table class="cb-tablo">
108        <thead><tr><th>Bağlantı</th><th>Adres</th><th>Durum</th><th>Kullanım</th><th></th></tr></thead>
109        <tbody>${kayitlar.map(satir).join('') || `<tr><td colspan="5" class="cb-bos">${kapsam ? 'Bu entegrasyon için henüz adres üretilmedi.' : 'Henüz adres yok.'}</td></tr>`}</tbody>
110      </table>
111      <div id="cbDetay"></div>`;
112
113    host.querySelector('#cbYeni')?.addEventListener('click', formuAc);
114    host.querySelector('#cbRed')?.addEventListener('click', reddedilenleriGoster);
115    host.querySelectorAll('.cb-aksiyon button').forEach((b) => {
116      b.addEventListener('click', () => aksiyon(b));
117    });
118  }
119
120  function formuAc() {
121    const f = host.querySelector('#cbForm');
122    if (!f) return;
123    f.hidden = false;
124    f.innerHTML = `
125      <div class="cb-form">
126        <label>Sağlayıcı <input id="cbProv" placeholder="salesforce" value="${esc(kapsam)}"${kapsam ? ' readonly' : ''} /></label>
127        <label>Asistan no <input id="cbAsis" type="number" placeholder="(isteğe bağlı)" value="${opts.assistant_id || ''}" /></label>
128        <label>Bağlantı no <input id="cbKanal" type="number" placeholder="(isteğe bağlı)" value="${opts.channel_id || ''}" /></label>
129        <label>Etiket <input id="cbEtiket" placeholder="Salesforce olayları" /></label>
130        <label>İzinli IP'ler <input id="cbIp" placeholder="1.2.3.4, 5.6.7.0/24" /></label>
131        <label class="cb-onay"><input type="checkbox" id="cbImza" /> HMAC imzası iste</label>
132        <div class="cb-form-alt">
133          <button class="cb-btn cb-primary" id="cbKaydet">Üret</button>
134          <button class="cb-btn" id="cbIptalForm">Vazgeç</button>
135        </div>
136        <div class="cb-not">İmza isteyen sağlayıcılar için sır üretilir.${
137          (!kapsam || kapsam === 'salesforce')
138            ? ' <b>Salesforce Outbound Message özel başlık gönderemez</b> — orada imza'
139              + ' kullanılamaz; koruma tahmin edilemez adres + Organization Id + IP listesidir.'
140            : ''}</div>
141      </div>`;
142    f.querySelector('#cbIptalForm').addEventListener('click', () => { f.hidden = true; });
143    f.querySelector('#cbKaydet').addEventListener('click', async () => {
144      const prov = f.querySelector('#cbProv').value.trim();
145      if (!prov) { toast('Sağlayıcı gerekli', 'error'); return; }
146      try {
147        const r = await api.post(API, {
148          provider_id: prov,
149          assistant_id: +f.querySelector('#cbAsis').value || null,
150          channel_id: +f.querySelector('#cbKanal').value || null,
151          label: f.querySelector('#cbEtiket').value.trim(),
152          allowed_ips: f.querySelector('#cbIp').value.trim(),
153          imza_iste: f.querySelector('#cbImza').checked,
154        });
155        f.hidden = true;
156        await ciz();
157        detayGoster(r, true);
158      } catch (e) { toast(e.message, 'error'); }
159    });
160  }
161
162  function kopyala(metin, etiket) {
163    // navigator.clipboard yalnız güvenli bağlamda var; yoksa seçilebilir
164    // metne düşüyoruz — sessizce hiçbir şey yapmamak en kötüsü.
165    try {
166      navigator.clipboard.writeText(metin);
167      toast(`${etiket} kopyalandı`, 'success');
168    } catch {
169      toast('Kopyalanamadı — metni elle seçin', 'error');
170    }
171  }
172
173  function detayGoster(d, yeni = false) {
174    const kap = host.querySelector('#cbDetay');
175    if (!kap) return;
176    const sir = d.secret
177      ? `<div class="cb-satir"><span>İmza sırrı</span>
178           <code id="cbSir">${esc(d.secret)}</code>
179           <button class="cb-btn" data-kopya="sir">Kopyala</button></div>`
180      : '';
181    const log = (d.log || []).map((l) => `
182      <tr><td>${zaman(l.created_at)}</td>
183          <td>${l.ok ? '✓' : '✗'} ${l.status_code || ''}</td>
184          <td>${esc(l.summary || '')}</td>
185          <td class="cb-alt">${esc((l.error || '').slice(0, 120))}</td></tr>`).join('');
186    kap.innerHTML = `
187      <div class="cb-detay">
188        <div class="cb-detay-bas">${yeni ? 'Adres üretildi' : 'Adres'}</div>
189        <div class="cb-satir"><span>Callback URL</span>
190          <code id="cbUrl">${esc(d.url || '')}</code>
191          <button class="cb-btn" data-kopya="url">Kopyala</button></div>
192        ${sir}
193        <div class="cb-not">Bu adresi sağlayıcının konsoluna yapıştırın.
194          ${d.secret ? 'Sır yalnız şimdi görünür.' : ''}</div>
195        ${log ? `<table class="cb-tablo cb-log"><thead><tr><th>Zaman</th><th>Sonuç</th><th>Özet</th><th>Hata</th></tr></thead><tbody>${log}</tbody></table>` : ''}
196      </div>`;
197    kap.querySelectorAll('[data-kopya]').forEach((b) => {
198      b.addEventListener('click', () => {
199        const hedef = b.dataset.kopya === 'url' ? '#cbUrl' : '#cbSir';
200        kopyala(kap.querySelector(hedef).textContent, b.dataset.kopya === 'url' ? 'Adres' : 'Sır');
201      });
202    });
203    kap.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
204  }
205
206  async function reddedilenleriGoster() {
207    const kap = host.querySelector('#cbDetay');
208    try {
209      const liste = await api.get(`${API}/log/rejected?limit=50`);
210      kap.innerHTML = `<div class="cb-detay">
211        <div class="cb-detay-bas">Reddedilen denemeler</div>
212        <div class="cb-not">Bilinmeyen ya da iptal edilmiş jetonla gelen istekler.
213          Sürekli artıyorsa biri adres uzayını tarıyor olabilir.</div>
214        <table class="cb-tablo cb-log"><thead><tr><th>Zaman</th><th>Kaynak IP</th><th>Jeton</th><th>Sebep</th></tr></thead>
215        <tbody>${liste.map((l) => `<tr><td>${zaman(l.created_at)}</td><td>${esc(l.source_ip || '')}</td>
216          <td>${esc(l.summary || '')}</td><td class="cb-alt">${esc(l.error || '')}</td></tr>`).join('')
217          || '<tr><td colspan="4" class="cb-bos">Hiç yok — iyi haber.</td></tr>'}</tbody></table></div>`;
218    } catch (e) { toast(e.message, 'error'); }
219  }
220
221  async function aksiyon(btn) {
222    const id = btn.closest('tr')?.dataset.id;
223    const act = btn.dataset.act;
224    if (!id) return;
225    try {
226      if (act === 'show') {
227        detayGoster(await api.get(`${API}/${id}`));
228      } else if (act === 'rotate') {
229        const ok = await onayla(
230          'Eski adres ANINDA çalışmayı bırakır. Sağlayıcı tarafındaki kaydı da '
231          + 'güncellemeniz gerekir, yoksa entegrasyon durur.',
232          { baslik: 'Adres yenilensin mi?', onayEtiketi: 'Yenile', tehlike: true });
233        if (!ok) return;
234        detayGoster(await api.post(`${API}/${id}/rotate`, {}), true);
235        await ciz();
236      } else if (act === 'revoke') {
237        const ok = await onayla(
238          'Bu adrese gelen tüm istekler reddedilecek.',
239          { baslik: 'Adres iptal edilsin mi?', onayEtiketi: 'İptal et', tehlike: true });
240        if (!ok) return;
241        await api.post(`${API}/${id}/revoke`, {});
242        await ciz();
243      } else if (act === 'restore') {
244        await api.post(`${API}/${id}/restore`, {});
245        await ciz();
246      }
247    } catch (e) { toast(e.message, 'error'); }
248  }
249
250  ciz().catch((e) => { host.innerHTML = `<div class="cb-hata">${esc(e.message)}</div>`; });
251  return { refresh: ciz };
252}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.