grantforge-api / frontend-react /src /components /settings /DeveloperApiKeysPanel.tsx
GrantForge Bot
Deploy sha-565ad85979610064f6d1c18ab3b6404357d61073 — source build (no GHCR)
ce8f04a
Raw
History Blame Contribute Delete
5.87 kB
import React, { useState } from 'react';
import { Key, Loader2, Plus, Trash2, Copy } from 'lucide-react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import toast from 'react-hot-toast';
import { createDeveloperApiKey, listDeveloperApiKeys, revokeDeveloperApiKey } from '../../api/client';
export const DeveloperApiKeysPanel: React.FC = () => {
const [label, setLabel] = useState('');
const [revealedKey, setRevealedKey] = useState<string | null>(null);
const queryClient = useQueryClient();
const { data, isLoading } = useQuery({
queryKey: ['developer-api-keys'],
queryFn: listDeveloperApiKeys,
});
const createMutation = useMutation({
mutationFn: () => createDeveloperApiKey(label.trim()),
onSuccess: (res) => {
setRevealedKey(res.api_key);
setLabel('');
queryClient.invalidateQueries({ queryKey: ['developer-api-keys'] });
toast.success('Klucz API utworzony — skopiuj go teraz (nie będzie widoczny ponownie).');
},
onError: () => toast.error('Nie udało się utworzyć klucza.'),
});
const revokeMutation = useMutation({
mutationFn: revokeDeveloperApiKey,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['developer-api-keys'] });
toast.success('Klucz unieważniony.');
},
onError: () => toast.error('Nie udało się unieważnić klucza.'),
});
const keys = data?.keys || [];
return (
<div style={{ padding: '1.5rem 0' }}>
<h3 style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.5rem' }}>
<Key size={18} /> Public API v2 — klucze deweloperskie
</h3>
<p style={{ color: 'var(--text-secondary)', fontSize: '0.9rem', marginBottom: '1.5rem' }}>
Klucze umożliwiają dostęp do <code>/api/v2/public/*</code> (katalog naborów, health). Specyfikacja: <code>/api/v2/public/openapi.json</code>
</p>
{revealedKey && (
<div style={{ background: 'rgba(16,185,129,0.1)', border: '1px solid #10b981', borderRadius: '8px', padding: '1rem', marginBottom: '1rem' }}>
<div style={{ fontSize: '0.85rem', color: '#10b981', marginBottom: '0.5rem', fontWeight: 600 }}>Nowy klucz (skopiuj teraz):</div>
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center', flexWrap: 'wrap' }}>
<code style={{ wordBreak: 'break-all', flex: 1 }}>{revealedKey}</code>
<button
type="button"
onClick={() => { navigator.clipboard.writeText(revealedKey); toast.success('Skopiowano'); }}
style={{ background: 'transparent', border: '1px solid #10b981', color: '#10b981', padding: '0.35rem 0.6rem', borderRadius: '6px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '0.25rem' }}
>
<Copy size={14} /> Kopiuj
</button>
</div>
</div>
)}
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1.5rem', flexWrap: 'wrap' }}>
<input
placeholder="Etykieta klucza (np. integracja CRM)"
value={label}
onChange={(e) => setLabel(e.target.value)}
style={{ flex: 1, minWidth: '200px', padding: '0.5rem 0.75rem', borderRadius: '6px', border: '1px solid rgba(255,255,255,0.15)', background: 'rgba(0,0,0,0.2)', color: '#fff' }}
/>
<button
className="btn btn-primary"
disabled={!label.trim() || createMutation.isPending}
onClick={() => createMutation.mutate()}
style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}
>
{createMutation.isPending ? <Loader2 size={16} className="animate-spin" /> : <Plus size={16} />}
Utwórz klucz
</button>
</div>
{isLoading ? (
<Loader2 className="animate-spin" size={24} />
) : keys.length === 0 ? (
<p style={{ color: 'var(--text-muted)', fontSize: '0.9rem' }}>Brak aktywnych kluczy.</p>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
{keys.map((k) => (
<div key={k.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '0.75rem 1rem', background: 'rgba(255,255,255,0.03)', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.08)' }}>
<div>
<div style={{ fontWeight: 600 }}>{k.label}</div>
<div style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>{k.key_prefix}… · {new Date(k.created_at).toLocaleDateString('pl-PL')}</div>
</div>
<button
type="button"
onClick={() => revokeMutation.mutate(k.id)}
disabled={revokeMutation.isPending}
style={{ background: 'transparent', border: 'none', color: '#ef4444', cursor: 'pointer' }}
title="Unieważnij"
>
<Trash2 size={18} />
</button>
</div>
))}
</div>
)}
</div>
);
};
export default DeveloperApiKeysPanel;