feat: add SensitiveKeysPage admin page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-14 18:23:34 +02:00
parent 06c719f0dd
commit 813ec6904e
2 changed files with 155 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
.page {
display: flex;
flex-direction: column;
gap: var(--space-lg);
max-width: 720px;
}
.infoBanner {
font-size: var(--font-size-sm);
color: var(--text-secondary);
background: var(--surface-secondary);
padding: var(--space-md);
border-radius: var(--radius-md);
line-height: 1.5;
}
.pillList {
display: flex;
flex-wrap: wrap;
gap: var(--space-xs);
min-height: 36px;
align-items: center;
}
.inputRow {
display: flex;
gap: var(--space-sm);
align-items: center;
}
.inputRow input {
flex: 1;
}
.footer {
display: flex;
gap: var(--space-sm);
align-items: center;
}

View File

@@ -0,0 +1,116 @@
import { useState, useEffect, useCallback } from 'react';
import { Button, SectionHeader, Tag, Input, Toggle, Label, useToast } from '@cameleer/design-system';
import { PageLoader } from '../../components/PageLoader';
import { useSensitiveKeys, useUpdateSensitiveKeys } from '../../api/queries/admin/sensitive-keys';
import styles from './SensitiveKeysPage.module.css';
import sectionStyles from '../../styles/section-card.module.css';
export default function SensitiveKeysPage() {
const { data, isLoading } = useSensitiveKeys();
const updateKeys = useUpdateSensitiveKeys();
const { toast } = useToast();
const [draft, setDraft] = useState<string[]>([]);
const [inputValue, setInputValue] = useState('');
const [pushToAgents, setPushToAgents] = useState(false);
const [initialized, setInitialized] = useState(false);
useEffect(() => {
if (data !== undefined && !initialized) {
setDraft(data?.keys ?? []);
setInitialized(true);
}
}, [data, initialized]);
const addKey = useCallback(() => {
const trimmed = inputValue.trim();
if (!trimmed) return;
if (draft.some((k) => k.toLowerCase() === trimmed.toLowerCase())) {
toast({ title: 'Duplicate key', description: `"${trimmed}" is already in the list`, variant: 'warning' });
return;
}
setDraft((prev) => [...prev, trimmed]);
setInputValue('');
}, [inputValue, draft, toast]);
const removeKey = useCallback((index: number) => {
setDraft((prev) => prev.filter((_, i) => i !== index));
}, []);
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
addKey();
}
}, [addKey]);
function handleSave() {
updateKeys.mutate({ keys: draft, pushToAgents }, {
onSuccess: (result) => {
if (result.pushResult) {
toast({
title: 'Sensitive keys saved and pushed',
description: `${result.pushResult.total} agent(s) notified, ${result.pushResult.responded} responded`,
variant: result.pushResult.success ? 'success' : 'warning',
});
} else {
toast({ title: 'Sensitive keys saved', variant: 'success' });
}
},
onError: () => {
toast({ title: 'Failed to save sensitive keys', variant: 'error', duration: 86_400_000 });
},
});
}
if (isLoading) return <PageLoader />;
return (
<div className={styles.page}>
<SectionHeader>Sensitive Keys</SectionHeader>
<div className={styles.infoBanner}>
Agents ship with built-in defaults (Authorization, Cookie, Set-Cookie, X-API-Key,
X-Auth-Token, Proxy-Authorization). Configuring keys here replaces agent defaults for
all applications. Leave unconfigured to use agent defaults.
</div>
<div className={sectionStyles.section}>
<Label>Global sensitive keys</Label>
<div className={styles.pillList}>
{draft.map((key, i) => (
<Tag key={`${key}-${i}`} label={key} onRemove={() => removeKey(i)} />
))}
{draft.length === 0 && (
<span style={{ color: 'var(--text-tertiary)', fontSize: 'var(--font-size-sm)' }}>
No keys configured agents use built-in defaults
</span>
)}
</div>
<div className={styles.inputRow}>
<Input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Add key or glob pattern (e.g. *password*)"
/>
<Button variant="secondary" size="sm" onClick={addKey} disabled={!inputValue.trim()}>
Add
</Button>
</div>
</div>
<div className={styles.footer}>
<Toggle
checked={pushToAgents}
onChange={(e) => setPushToAgents((e.target as HTMLInputElement).checked)}
label="Push to all connected agents immediately"
/>
<Button variant="primary" onClick={handleSave} loading={updateKeys.isPending}>
Save
</Button>
</div>
</div>
);
}