diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md
index 39266f6..7d404e2 100644
--- a/docs/quality/uygulama-durumu.md
+++ b/docs/quality/uygulama-durumu.md
@@ -595,3 +595,13 @@ Ortak kalem araması başarısız olduğunda istemci boş dizi, AppContext ise s
Liste kalem araması sürerken sonuçların genişleyebileceğini, hata olduğunda yalnız başlık/kurum/yüklü kart aramasıyla devam edildiğini ve sonuçların eksik olabileceğini açıklar. Kalem aramasını yeniden deneme düğmesi eklendi. Gerçek boş kalem sonucu hata sayılmaz.
Doğrulama: boş başarılı sonuç, depo 503 hatası ve iptal edilmiş arama için servis testi. Mevcut sorgu eşleşmesi testleriyle birlikte tüm 129 test ve üretim derlemesi geçti. Yeni hata durumunun tarayıcı kabulü açık. Büyük paket uyarıları sürüyor; canlı yayın yapılmadı.
+
+### D09 — API anahtarı hata, oturum ve iptal akışı
+
+İlk listeleme hatasında boş `durum.anahtarlar` erişimi kaldırıldı; yükleme/hata/boş liste ayrı gösterilir. Hesap ve token bazında anahtar ekranı yeniden kurulur; eski oturum yanıtları uygulanmaz. Oluşturma/iptal çift tıklaması engellenir; liste alınmadan oluşturma açılmaz. İptal yanıtı plan limitini korur ve iptal edilen tek seferlik anahtar kutusu temizlenir. Pano başarısı ancak gerçek kopyalama tamamlanınca gösterilir; hata görünürdür.
+
+API anahtarı istemcisinde 30 saniye zaman aşımı, yanıt biçimi kontrolü ve yanıt sonrası oturum kontrolü eklendi. Sonucu alınamayan mutasyon otomatik tekrarlanmaz; kullanıcı listeyi kontrol etmeye yönlendirilir. Liste yenileme hatası yeni oluşturulmuş anahtarı silmez.
+
+Doğrulama: boş başarılı liste, 503, geçersiz JSON/veri, geç gelen farklı oturum yanıtı ve zaman aşımında tek oluşturma isteği. Gerçek izole yerel HTTP sunucusunda oluşturma → gizli alanları içermeyen liste → MCP initialize erişimi → son kullanım zamanı → iptal → 401 zinciri geçti. Başka hesap iptal edemedi; iptal yanıtında limit korundu. Test anahtarları geçici veritabanında tutuldu ve çıktıya yazılmadı.
+
+Sonuç: 132 test başarılı; üretim derlemesi geçti. Tarayıcıda hata/hesap değişimi/kopyalama kabulü henüz yapılmadı. Büyük paket uyarıları ve D01–D11 kalan işleri açık. Canlı yayın yapılmadı.
diff --git a/server/index.js b/server/index.js
index 6c07d44..6befbf9 100644
--- a/server/index.js
+++ b/server/index.js
@@ -1186,7 +1186,7 @@ const server = http.createServer(async (req, res) => {
}
if (req.method === 'DELETE') {
const ok = store.revokeApiKey(user.email, url.searchParams.get('id'));
- return json(res, ok ? 200 : 404, { success: ok, anahtarlar: store.listApiKeys(user.email) });
+ return json(res, ok ? 200 : 404, { success: ok, error: ok ? undefined : 'Anahtar bulunamadı veya bu hesaba ait değil.', anahtarlar: store.listApiKeys(user.email), limit: haklar(user.email, { isAdmin: user.isAdmin }).apiAnahtari });
}
return json(res, 200, { success: true, anahtarlar: store.listApiKeys(user.email), limit: haklar(user.email, { isAdmin: user.isAdmin }).apiAnahtari });
}
diff --git a/src/components/tools/ApiWebhooks.jsx b/src/components/tools/ApiWebhooks.jsx
index a4b9259..f619b39 100644
--- a/src/components/tools/ApiWebhooks.jsx
+++ b/src/components/tools/ApiWebhooks.jsx
@@ -1,7 +1,8 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useRef } from 'react';
import { useApp } from '../../context/AppContext';
import { listApiKeys, createApiKey, revokeApiKey } from '../../services/account';
import { Webhook, Key, Plus, Trash2, Loader2, Copy, Check, AlertTriangle, ShieldCheck, Lock } from 'lucide-react';
+import { getToken } from '../../services/auth.js';
import { McpRssKarti } from './McpRssKarti';
/**
@@ -11,6 +12,15 @@ import { McpRssKarti } from './McpRssKarti';
*/
export const ApiWebhooks = () => {
const { authUser } = useApp();
+ return authUser ?
API anahtarlarınızı yönetmek için giriş yapın.
; +}; + +const ApiKeySession = () => { + const token = useRef(getToken()); + const alive = useRef(true); + const operation = useRef(false); + const copyTimer = useRef(null); + const current = () => alive.current && getToken() === token.current; const [durum, setDurum] = useState(null); const [busy, setBusy] = useState(false); const [hata, setHata] = useState(null); @@ -18,35 +28,60 @@ export const ApiWebhooks = () => { const [yeniAnahtar, setYeniAnahtar] = useState(null); // yalnızca bir kez gösterilir const [kopyalandi, setKopyalandi] = useState(false); - const yukle = () => { - if (!authUser) return; - setHata(null); - listApiKeys().then(setDurum).catch((e) => setHata(e.message)); + const [loading, setLoading] = useState(true); + const yukle = async () => { + if (!current()) return; + setLoading(true); setHata(null); + try { + const data = await listApiKeys(); + if (current()) setDurum(data); + } catch (e) { if (current()) setHata(e.message); } + finally { if (current()) setLoading(false); } }; - useEffect(yukle, [authUser]); + useEffect(() => { + alive.current = true; + void yukle(); + return () => { alive.current = false; clearTimeout(copyTimer.current); }; + }, []); const olustur = async () => { - setBusy(true); setHata(null); setYeniAnahtar(null); + if (operation.current || !current() || !durum || loading) return; + operation.current = true; + setBusy(true); setHata(null); try { - const d = await createApiKey(yeniAd || 'API Anahtarı'); - setYeniAnahtar(d.anahtar); + const d = await createApiKey(yeniAd.trim() || 'API Anahtarı'); + if (!current()) return; + setYeniAnahtar(d.anahtar); setKopyalandi(false); setYeniAd(''); - yukle(); - } catch (e) { setHata(e.message); } - finally { setBusy(false); } + await yukle(); + } catch (e) { if (current()) setHata(e.message); } + finally { operation.current = false; if (current()) setBusy(false); } }; const iptal = async (id) => { + if (operation.current || !current() || loading) return; + operation.current = true; setBusy(true); setHata(null); - try { setDurum(await revokeApiKey(id)); } - catch (e) { setHata(e.message); } - finally { setBusy(false); } + try { + const data = await revokeApiKey(id); + if (!current()) return; + setDurum(data); + setYeniAnahtar((key) => key?.id === id ? null : key); + } catch (e) { if (current()) setHata(e.message); } + finally { operation.current = false; if (current()) setBusy(false); } }; - const kopyala = (t) => { - navigator.clipboard?.writeText(t); - setKopyalandi(true); - setTimeout(() => setKopyalandi(false), 2000); + const kopyala = async (t) => { + try { + if (!navigator.clipboard) throw new Error('clipboard unavailable'); + await navigator.clipboard.writeText(t); + if (!current()) return; + setKopyalandi(true); + clearTimeout(copyTimer.current); + copyTimer.current = setTimeout(() => { if (current()) setKopyalandi(false); }, 2000); + } catch { + if (current()) { setKopyalandi(false); setHata('Anahtar kopyalanamadı. Anahtarı seçerek elle kopyalayabilirsiniz.'); } + } }; const limit = durum?.limit; @@ -54,7 +89,7 @@ export const ApiWebhooks = () => { return (Anahtar listesi alınamadı. Yeniden deneyebilirsiniz.
) : durum.anahtarlar.length === 0 ? (Henüz anahtar yok.
) : ( @@ -130,7 +167,7 @@ export const ApiWebhooks = () => { {k.son_kullanim ? `son kullanım: ${new Date(k.son_kullanim).toLocaleString('tr-TR')}` : 'hiç kullanılmadı'}