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.