Spaces:
Running
Running
| 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; | |