amalia/22-mei-26 #25
@@ -370,6 +370,8 @@ export function createApp() {
|
||||
errors: app.bugs.length,
|
||||
active: app.active,
|
||||
urlApi: app.urlApi,
|
||||
apiKey: app.apiKey ?? '',
|
||||
clientApiKey: app.clientApiKey ?? '',
|
||||
hasClientApiKey: !!app.clientApiKey,
|
||||
}))
|
||||
}, {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Button,
|
||||
Card,
|
||||
Center,
|
||||
CopyButton,
|
||||
Container,
|
||||
Divider,
|
||||
Group,
|
||||
@@ -59,6 +60,8 @@ import {
|
||||
TbCode,
|
||||
TbDatabase,
|
||||
TbDots,
|
||||
TbEye,
|
||||
TbEyeOff,
|
||||
TbFileText,
|
||||
TbKey,
|
||||
TbLayoutDashboard,
|
||||
@@ -1474,6 +1477,8 @@ interface AppEntry {
|
||||
id: string
|
||||
name: string
|
||||
urlApi: string | null
|
||||
apiKey: string
|
||||
clientApiKey: string
|
||||
status: string
|
||||
active: boolean
|
||||
hasClientApiKey: boolean
|
||||
@@ -1501,10 +1506,24 @@ function SettingsPanel() {
|
||||
const [keyOpened, { open: openKey, close: closeKey }] = useDisclosure(false)
|
||||
const [generatedKey, setGeneratedKey] = useState('')
|
||||
const [keyCopied, setKeyCopied] = useState(false)
|
||||
const [generatedKeyVisible, setGeneratedKeyVisible] = useState(false)
|
||||
const [addKeyVisible, setAddKeyVisible] = useState(false)
|
||||
const [apiConfigKeyVisible, setApiConfigKeyVisible] = useState(false)
|
||||
const [visibleAppKeys, setVisibleAppKeys] = useState<Set<string>>(new Set())
|
||||
|
||||
const toggleAppKeyVisibility = (appId: string) => {
|
||||
setVisibleAppKeys((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(appId)) next.delete(appId)
|
||||
else next.add(appId)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const openApiModal = (app: AppEntry) => {
|
||||
setApiTarget(app)
|
||||
setApiForm({ urlApi: app.urlApi ?? '', apiKey: '' })
|
||||
setApiConfigKeyVisible(false)
|
||||
openApi()
|
||||
}
|
||||
|
||||
@@ -1562,6 +1581,7 @@ function SettingsPanel() {
|
||||
qc.invalidateQueries({ queryKey: ['apps'] })
|
||||
setGeneratedKey(res.clientApiKey)
|
||||
setKeyCopied(false)
|
||||
setGeneratedKeyVisible(false)
|
||||
openKey()
|
||||
},
|
||||
})
|
||||
@@ -1626,6 +1646,54 @@ function SettingsPanel() {
|
||||
: <Badge color="red" variant="dot" size="xs">No client key</Badge>
|
||||
}
|
||||
</Group>
|
||||
{app.clientApiKey && (
|
||||
<>
|
||||
<Text size="xs" fw={500} c="gray" mt={4}>Client Key (untuk mobile app mengakses monitoring):</Text>
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<Text size="xs" ff="monospace" c="dimmed" style={{ userSelect: visibleAppKeys.has(app.id) ? 'text' : 'none' }}>
|
||||
{visibleAppKeys.has(app.id) ? app.clientApiKey : '•'.repeat(32)}
|
||||
</Text>
|
||||
<Tooltip label={visibleAppKeys.has(app.id) ? 'Sembunyikan' : 'Tampilkan'}>
|
||||
<ActionIcon variant="subtle" size="xs" color="gray" onClick={() => toggleAppKeyVisibility(app.id)}>
|
||||
{visibleAppKeys.has(app.id) ? <TbEyeOff size={12} /> : <TbEye size={12} />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<CopyButton value={app.clientApiKey}>
|
||||
{({ copy }) => (
|
||||
<Tooltip label="Salin">
|
||||
<ActionIcon variant="subtle" size="xs" color="gray" onClick={copy}>
|
||||
<TbCopy size={12} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</CopyButton>
|
||||
</Group>
|
||||
</>
|
||||
)}
|
||||
{app.apiKey && (
|
||||
<>
|
||||
<Text size="xs" fw={500} c="gray" mt={4}>Server Key (untuk monitoring mengakses API external):</Text>
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<Text size="xs" ff="monospace" c="dimmed" style={{ userSelect: visibleAppKeys.has(`server-${app.id}`) ? 'text' : 'none' }}>
|
||||
{visibleAppKeys.has(`server-${app.id}`) ? app.apiKey : '•'.repeat(32)}
|
||||
</Text>
|
||||
<Tooltip label={visibleAppKeys.has(`server-${app.id}`) ? 'Sembunyikan' : 'Tampilkan'}>
|
||||
<ActionIcon variant="subtle" size="xs" color="gray" onClick={() => toggleAppKeyVisibility(`server-${app.id}`)}>
|
||||
{visibleAppKeys.has(`server-${app.id}`) ? <TbEyeOff size={12} /> : <TbEye size={12} />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<CopyButton value={app.apiKey}>
|
||||
{({ copy }) => (
|
||||
<Tooltip label="Salin">
|
||||
<ActionIcon variant="subtle" size="xs" color="gray" onClick={copy}>
|
||||
<TbCopy size={12} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</CopyButton>
|
||||
</Group>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Group>
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
@@ -1659,7 +1727,19 @@ function SettingsPanel() {
|
||||
<TextInput label="App ID" description="Unique slug used as identifier (e.g. desa-plus)" placeholder="my-app" value={newApp.id} onChange={(e) => setNewApp((p) => ({ ...p, id: e.target.value }))} required />
|
||||
<TextInput label="Name" placeholder="My Application" value={newApp.name} onChange={(e) => setNewApp((p) => ({ ...p, name: e.target.value }))} required />
|
||||
<TextInput label="URL API" placeholder="https://api.example.com" value={newApp.urlApi} onChange={(e) => setNewApp((p) => ({ ...p, urlApi: e.target.value }))} />
|
||||
<TextInput label="API Key" placeholder="secret-key" type="password" value={newApp.apiKey} onChange={(e) => setNewApp((p) => ({ ...p, apiKey: e.target.value }))} />
|
||||
<TextInput
|
||||
label="Server Key (API External)"
|
||||
description="Key untuk monitoring mengakses API external app ini."
|
||||
placeholder="secret-key"
|
||||
type={addKeyVisible ? 'text' : 'password'}
|
||||
value={newApp.apiKey}
|
||||
onChange={(e) => setNewApp((p) => ({ ...p, apiKey: e.target.value }))}
|
||||
rightSection={
|
||||
<ActionIcon variant="subtle" size="sm" color="gray" onClick={() => setAddKeyVisible((v) => !v)}>
|
||||
{addKeyVisible ? <TbEyeOff size={14} /> : <TbEye size={14} />}
|
||||
</ActionIcon>
|
||||
}
|
||||
/>
|
||||
<Group justify="flex-end" mt="xs">
|
||||
<Button variant="subtle" color="gray" onClick={closeAdd}>Cancel</Button>
|
||||
<Button loading={createMutation.isPending} disabled={!newApp.id || !newApp.name} onClick={() => createMutation.mutate(newApp)}>Add</Button>
|
||||
@@ -1671,21 +1751,28 @@ function SettingsPanel() {
|
||||
<Modal opened={keyOpened} onClose={closeKey} title="Client API Key Generated" radius="md">
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" c="dimmed">Copy this key now — it will not be shown again after you close this dialog.</Text>
|
||||
<Box
|
||||
p="sm"
|
||||
style={{ background: 'var(--mantine-color-dark-6)', borderRadius: 6, fontFamily: 'monospace', fontSize: 13, wordBreak: 'break-all' }}
|
||||
>
|
||||
{generatedKey}
|
||||
</Box>
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
variant="light"
|
||||
color={keyCopied ? 'green' : 'blue'}
|
||||
leftSection={<TbCopy size={14} />}
|
||||
onClick={() => { navigator.clipboard.writeText(generatedKey); setKeyCopied(true) }}
|
||||
<Group gap={4} wrap="nowrap" align="center">
|
||||
<Box
|
||||
p="sm"
|
||||
flex={1}
|
||||
style={{ background: 'var(--mantine-color-dark-6)', borderRadius: 6, fontFamily: 'monospace', fontSize: 13, wordBreak: 'break-all', userSelect: generatedKeyVisible ? 'text' : 'none' }}
|
||||
>
|
||||
{keyCopied ? 'Copied!' : 'Copy to Clipboard'}
|
||||
</Button>
|
||||
{generatedKeyVisible ? generatedKey : '•'.repeat(48)}
|
||||
</Box>
|
||||
<Tooltip label={generatedKeyVisible ? 'Sembunyikan' : 'Tampilkan'}>
|
||||
<ActionIcon variant="subtle" color="gray" onClick={() => setGeneratedKeyVisible((v) => !v)}>
|
||||
{generatedKeyVisible ? <TbEyeOff size={16} /> : <TbEye size={16} />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
<Group justify="flex-end">
|
||||
<CopyButton value={generatedKey}>
|
||||
{({ copy }) => (
|
||||
<Button variant="light" color={keyCopied ? 'green' : 'blue'} leftSection={<TbCopy size={14} />} onClick={() => { copy(); setKeyCopied(true) }}>
|
||||
{keyCopied ? 'Copied!' : 'Copy to Clipboard'}
|
||||
</Button>
|
||||
)}
|
||||
</CopyButton>
|
||||
<Button variant="subtle" color="gray" onClick={closeKey}>Close</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
@@ -1695,14 +1782,26 @@ function SettingsPanel() {
|
||||
<Modal opened={apiOpened} onClose={closeApi} title={`API Config — ${apiTarget?.name}`} radius="md">
|
||||
<Stack gap="sm">
|
||||
<TextInput label="URL API" description="Base URL for proxying requests to the external API." placeholder="https://api.example.com" value={apiForm.urlApi} onChange={(e) => setApiForm((p) => ({ ...p, urlApi: e.target.value }))} />
|
||||
<TextInput label="API Key" description="Leave blank to keep the existing key unchanged." placeholder="Leave blank to keep unchanged" type="password" value={apiForm.apiKey} onChange={(e) => setApiForm((p) => ({ ...p, apiKey: e.target.value }))} />
|
||||
<TextInput
|
||||
label="Server Key (API External)"
|
||||
description="Key untuk monitoring mengakses API external. Kosongkan untuk tetap menggunakan key yang ada."
|
||||
placeholder="Kosongkan untuk tetap menggunakan key yang ada"
|
||||
type={apiConfigKeyVisible ? 'text' : 'password'}
|
||||
value={apiForm.apiKey}
|
||||
onChange={(e) => setApiForm((p) => ({ ...p, apiKey: e.target.value }))}
|
||||
rightSection={
|
||||
<ActionIcon variant="subtle" size="sm" color="gray" onClick={() => setApiConfigKeyVisible((v) => !v)}>
|
||||
{apiConfigKeyVisible ? <TbEyeOff size={14} /> : <TbEye size={14} />}
|
||||
</ActionIcon>
|
||||
}
|
||||
/>
|
||||
<Group justify="flex-end" mt="xs">
|
||||
<Button variant="subtle" color="gray" onClick={closeApi}>Cancel</Button>
|
||||
<Button
|
||||
loading={apiMutation.isPending}
|
||||
onClick={() => {
|
||||
if (!apiTarget) return
|
||||
const body: any = { urlApi: apiForm.urlApi }
|
||||
const body: { urlApi: string; apiKey?: string } = { urlApi: apiForm.urlApi }
|
||||
if (apiForm.apiKey) body.apiKey = apiForm.apiKey
|
||||
apiMutation.mutate({ id: apiTarget.id, body })
|
||||
}}
|
||||
@@ -1734,6 +1833,16 @@ 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 toggleKeyVisibility = (keyId: string) => {
|
||||
setVisibleKeys((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(keyId)) next.delete(keyId)
|
||||
else next.add(keyId)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['admin', 'api-keys'],
|
||||
@@ -1837,7 +1946,30 @@ function ApiKeysPanel() {
|
||||
<Table.Tr key={k.id}>
|
||||
<Table.Td fw={500}>{k.name}</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs" ff="monospace" c="dimmed">{k.key}</Text>
|
||||
<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>
|
||||
<Tooltip label={visibleKeys.has(k.id) ? 'Sembunyikan' : 'Tampilkan'}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
color="gray"
|
||||
onClick={() => toggleKeyVisibility(k.id)}
|
||||
>
|
||||
{visibleKeys.has(k.id) ? <TbEyeOff size={12} /> : <TbEye 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>
|
||||
<Badge color={k.isActive ? 'green' : 'gray'} variant="light">
|
||||
|
||||
Reference in New Issue
Block a user