Fix API key error states and verify revocation lifecycle
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiKeyRequest } from './apiKeys.js';
|
||||
import { sessionGuard } from './sessionGuard.js';
|
||||
/**
|
||||
* Sunucu tarafı hesap servisleri: plan/kota, kullanıcı verisi, API anahtarları, belgeler.
|
||||
@@ -37,9 +38,9 @@ export const setUserData = (anahtar, deger) =>
|
||||
iste('/api/me/data', { method: 'PUT', body: JSON.stringify({ anahtar, deger }) });
|
||||
|
||||
// --- API anahtarları ---
|
||||
export const listApiKeys = () => iste('/api/me/apikeys');
|
||||
export const createApiKey = (ad) => iste('/api/me/apikeys', { method: 'POST', body: JSON.stringify({ ad }) });
|
||||
export const revokeApiKey = (id) => iste(`/api/me/apikeys?id=${encodeURIComponent(id)}`, { method: 'DELETE' });
|
||||
export const listApiKeys = () => apiKeyRequest();
|
||||
export const createApiKey = (ad) => apiKeyRequest('POST', { ad });
|
||||
export const revokeApiKey = (id) => apiKeyRequest('DELETE', { id });
|
||||
|
||||
// --- Belgeler ---
|
||||
export const listDocuments = () => iste('/api/me/documents');
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { getToken } from './auth.js';
|
||||
|
||||
// A lost mutation response is uncertain: never automatically repeat key creation.
|
||||
export async function apiKeyRequest(method = 'GET', { id, ad, timeoutMs = 30000 } = {}) {
|
||||
const token = getToken();
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const response = await fetch(`/api/me/apikeys${id ? `?id=${encodeURIComponent(id)}` : ''}`, {
|
||||
method, signal: controller.signal,
|
||||
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||||
...(method === 'POST' ? { body: JSON.stringify({ ad }) } : {}),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (getToken() !== token) throw new Error('Oturum değişti; anahtar ekranını yeniden açın.');
|
||||
if (!response.ok || data?.success !== true) throw new Error(data?.error || 'API anahtarı işlemi tamamlanamadı.');
|
||||
const valid = method === 'POST'
|
||||
? typeof data.anahtar?.id === 'string' && typeof data.anahtar?.anahtar === 'string' && data.anahtar.anahtar.startsWith('ih_live_')
|
||||
: Array.isArray(data.anahtarlar) && data.anahtarlar.every(k => k && typeof k.id === 'string' && typeof k.ad === 'string') && (data.limit === null || (Number.isFinite(data.limit) && data.limit >= 0));
|
||||
if (!valid) throw new Error('Anahtar bilgileri eksik veya geçersiz. Listeyi yeniden yükleyin.');
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted) throw new Error(method === 'GET'
|
||||
? 'Anahtar listesi zamanında alınamadı. Yeniden deneyin.'
|
||||
: 'İşlem sonucu alınamadı. Tekrar işlem yapmadan önce listeyi yenileyin; işlem sunucuda gerçekleşmiş olabilir.');
|
||||
if (error instanceof SyntaxError) throw new Error('Sunucu geçerli anahtar bilgisi döndürmedi. Listeyi yeniden yükleyin.');
|
||||
throw error;
|
||||
} finally { clearTimeout(timer); }
|
||||
}
|
||||
Reference in New Issue
Block a user