Fix API key error states and verify revocation lifecycle

This commit is contained in:
2026-09-06 10:37:14 +03:00
parent 21e80d0dd6
commit 372c0e76e0
7 changed files with 164 additions and 30 deletions
+63 -26
View File
@@ -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 ? <ApiKeySession key={`${authUser.email}:${getToken()}`} /> : <p>API anahtarlarınızı yönetmek için giriş yapın.</p>;
};
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 (
<div className="space-y-6 animate-fade-in">
{hata && <button onClick={yukle} className="px-4 py-2 border rounded-lg">Yeniden dene</button>}
{hata && <button disabled={busy || loading} onClick={yukle} className="px-4 py-2 border rounded-lg">Yeniden dene</button>}
<div className="bg-linear-to-r from-slate-900 via-violet-950 to-indigo-900 text-white p-6 rounded-2xl shadow-lg border border-violet-800/40">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-2xl bg-violet-600 flex items-center justify-center shadow-md">
@@ -113,8 +148,10 @@ export const ApiWebhooks = () => {
)}
</div>
{!durum && !hata && !hata ? (
{loading && !durum ? (
<div className="flex items-center gap-2 text-xs text-slate-400"><Loader2 className="w-3.5 h-3.5 animate-spin" /> yükleniyor…</div>
) : !durum ? (
<p className="text-xs text-slate-500">Anahtar listesi alınamadı. Yeniden deneyebilirsiniz.</p>
) : durum.anahtarlar.length === 0 ? (
<p className="text-xs text-slate-400">Henüz anahtar yok.</p>
) : (
@@ -130,7 +167,7 @@ export const ApiWebhooks = () => {
{k.son_kullanim ? `son kullanım: ${new Date(k.son_kullanim).toLocaleString('tr-TR')}` : 'hiç kullanılmadı'}
</p>
</div>
<button onClick={() => iptal(k.id)} disabled={busy}
<button onClick={() => iptal(k.id)} disabled={busy || loading}
className="text-slate-400 hover:text-red-500 shrink-0" title="Anahtarı iptal et">
<Trash2 className="w-4 h-4" />
</button>
@@ -141,16 +178,16 @@ export const ApiWebhooks = () => {
<div className="flex gap-2 pt-2 border-t border-slate-100">
<input value={yeniAd} onChange={(e) => setYeniAd(e.target.value)}
placeholder="Anahtar adı (ör. ERP Entegrasyonu)" disabled={kilitli}
className="flex-1 bg-slate-50 border border-slate-200 rounded-lg px-2.5 py-2 text-xs disabled:opacity-50" />
<button onClick={olustur} disabled={busy || kilitli}
aria-label="Anahtar adı" placeholder="Anahtar adı (ör. ERP Entegrasyonu)" disabled={kilitli || busy || loading || !durum}
className="min-w-0 flex-1 bg-slate-50 border border-slate-200 rounded-lg px-2.5 py-2 text-xs disabled:opacity-50" />
<button onClick={olustur} disabled={busy || loading || !durum || kilitli || (limit != null && durum.anahtarlar.length >= limit)}
className="bg-violet-600 hover:bg-violet-500 disabled:opacity-40 text-white font-bold text-xs px-4 rounded-lg flex items-center gap-1.5">
{busy ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Plus className="w-3.5 h-3.5" />} Oluştur
</button>
</div>
{hata && (
<div className="bg-red-50 border border-red-200 rounded-lg p-2.5 flex gap-2">
<div role="alert" className="bg-red-50 border border-red-200 rounded-lg p-2.5 flex gap-2">
<AlertTriangle className="w-3.5 h-3.5 text-red-600 shrink-0 mt-0.5" />
<p className="text-[11px] text-red-800">{hata}</p>
</div>