feat: copy full API key on-demand di halaman dev

Sebelumnya copy button mengcopy key yang sudah ter-mask dari list endpoint
Desa+ API. Sekarang klik copy fetch full key via GET /api-keys/:id lalu
salin ke clipboard.
This commit is contained in:
2026-05-25 11:59:54 +08:00
parent e32addbc85
commit a19846f589
2 changed files with 37 additions and 23 deletions

View File

@@ -1688,6 +1688,19 @@ export function createApp() {
return { keys: json.data ?? [] }
})
.get('/api/admin/api-keys/:id', async ({ request, set, params }) => {
const auth = await requireDeveloper(request, set)
if (!auth) return { error: set.status === 401 ? 'Unauthorized' : 'Forbidden' }
const app = await prisma.app.findUnique({ where: { id: 'desa-plus' } })
if (!app?.urlApi) { set.status = 503; return { error: 'desa-plus belum dikonfigurasi' } }
const res = await fetch(`${app.urlApi.replace(/\/$/, '')}/api/monitoring/api-keys/${params.id}`, {
headers: { 'x-api-key': app.apiKey ?? '' },
})
const json = await res.json()
set.status = res.status
return json
})
.post('/api/admin/api-keys', async ({ request, set }) => {
const auth = await requireDeveloper(request, set)
if (!auth) return { error: set.status === 401 ? 'Unauthorized' : 'Forbidden' }

View File

@@ -55,6 +55,7 @@ import {
TbApps,
TbBug,
TbChevronRight,
TbCheck,
TbCopy,
TbCircleFilled,
TbCode,
@@ -1833,15 +1834,23 @@ function ApiKeysPanel() {
const [createdKey, setCreatedKey] = useState<string | null>(null)
const [keyCopied, setKeyCopied] = useState(false)
const [revealedOpened, { open: openRevealed, close: closeRevealed }] = useDisclosure(false)
const [visibleKeys, setVisibleKeys] = useState<Set<string>>(new Set())
const [copyingId, setCopyingId] = useState<string | null>(null)
const [copiedId, setCopiedId] = useState<string | null>(null)
const toggleKeyVisibility = (keyId: string) => {
setVisibleKeys((prev) => {
const next = new Set(prev)
if (next.has(keyId)) next.delete(keyId)
else next.add(keyId)
return next
})
const copyFullKey = async (id: string) => {
setCopyingId(id)
try {
const res = await fetch(`/api/admin/api-keys/${id}`, { credentials: 'include' })
const json = await res.json()
const fullKey = json.data?.key
if (fullKey) {
await navigator.clipboard.writeText(fullKey)
setCopiedId(id)
setTimeout(() => setCopiedId(null), 2000)
}
} finally {
setCopyingId(null)
}
}
const { data, isLoading } = useQuery({
@@ -1947,28 +1956,20 @@ function ApiKeysPanel() {
<Table.Td fw={500}>{k.name}</Table.Td>
<Table.Td>
<Group gap={4} wrap="nowrap">
<Text size="xs" ff="monospace" c="dimmed" style={{ userSelect: visibleKeys.has(k.id) ? 'text' : 'none' }}>
{visibleKeys.has(k.id) ? k.key : '•'.repeat(32)}
<Text size="xs" ff="monospace" c="dimmed">
{k.key}
</Text>
<Tooltip label={visibleKeys.has(k.id) ? 'Sembunyikan' : 'Tampilkan'}>
<Tooltip label={copiedId === k.id ? 'Tersalin!' : 'Salin full key'}>
<ActionIcon
variant="subtle"
size="xs"
color="gray"
onClick={() => toggleKeyVisibility(k.id)}
color={copiedId === k.id ? 'green' : 'gray'}
loading={copyingId === k.id}
onClick={() => copyFullKey(k.id)}
>
{visibleKeys.has(k.id) ? <TbEyeOff size={12} /> : <TbEye size={12} />}
{copiedId === k.id ? <TbCheck size={12} /> : <TbCopy size={12} />}
</ActionIcon>
</Tooltip>
<CopyButton value={k.key}>
{({ copy }) => (
<Tooltip label="Salin">
<ActionIcon variant="subtle" size="xs" color="gray" onClick={copy}>
<TbCopy size={12} />
</ActionIcon>
</Tooltip>
)}
</CopyButton>
</Group>
</Table.Td>
<Table.Td>