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:
13
src/app.ts
13
src/app.ts
@@ -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' }
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user