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 ?? [] }
|
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 }) => {
|
.post('/api/admin/api-keys', async ({ request, set }) => {
|
||||||
const auth = await requireDeveloper(request, set)
|
const auth = await requireDeveloper(request, set)
|
||||||
if (!auth) return { error: set.status === 401 ? 'Unauthorized' : 'Forbidden' }
|
if (!auth) return { error: set.status === 401 ? 'Unauthorized' : 'Forbidden' }
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import {
|
|||||||
TbApps,
|
TbApps,
|
||||||
TbBug,
|
TbBug,
|
||||||
TbChevronRight,
|
TbChevronRight,
|
||||||
|
TbCheck,
|
||||||
TbCopy,
|
TbCopy,
|
||||||
TbCircleFilled,
|
TbCircleFilled,
|
||||||
TbCode,
|
TbCode,
|
||||||
@@ -1833,15 +1834,23 @@ function ApiKeysPanel() {
|
|||||||
const [createdKey, setCreatedKey] = useState<string | null>(null)
|
const [createdKey, setCreatedKey] = useState<string | null>(null)
|
||||||
const [keyCopied, setKeyCopied] = useState(false)
|
const [keyCopied, setKeyCopied] = useState(false)
|
||||||
const [revealedOpened, { open: openRevealed, close: closeRevealed }] = useDisclosure(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) => {
|
const copyFullKey = async (id: string) => {
|
||||||
setVisibleKeys((prev) => {
|
setCopyingId(id)
|
||||||
const next = new Set(prev)
|
try {
|
||||||
if (next.has(keyId)) next.delete(keyId)
|
const res = await fetch(`/api/admin/api-keys/${id}`, { credentials: 'include' })
|
||||||
else next.add(keyId)
|
const json = await res.json()
|
||||||
return next
|
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({
|
const { data, isLoading } = useQuery({
|
||||||
@@ -1947,28 +1956,20 @@ function ApiKeysPanel() {
|
|||||||
<Table.Td fw={500}>{k.name}</Table.Td>
|
<Table.Td fw={500}>{k.name}</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Group gap={4} wrap="nowrap">
|
<Group gap={4} wrap="nowrap">
|
||||||
<Text size="xs" ff="monospace" c="dimmed" style={{ userSelect: visibleKeys.has(k.id) ? 'text' : 'none' }}>
|
<Text size="xs" ff="monospace" c="dimmed">
|
||||||
{visibleKeys.has(k.id) ? k.key : '•'.repeat(32)}
|
{k.key}
|
||||||
</Text>
|
</Text>
|
||||||
<Tooltip label={visibleKeys.has(k.id) ? 'Sembunyikan' : 'Tampilkan'}>
|
<Tooltip label={copiedId === k.id ? 'Tersalin!' : 'Salin full key'}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
size="xs"
|
size="xs"
|
||||||
color="gray"
|
color={copiedId === k.id ? 'green' : 'gray'}
|
||||||
onClick={() => toggleKeyVisibility(k.id)}
|
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>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</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>
|
</Group>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
|
|||||||
Reference in New Issue
Block a user