feat: add SensitiveKeysPage admin page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
39
ui/src/pages/Admin/SensitiveKeysPage.module.css
Normal file
39
ui/src/pages/Admin/SensitiveKeysPage.module.css
Normal 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;
|
||||||
|
}
|
||||||
116
ui/src/pages/Admin/SensitiveKeysPage.tsx
Normal file
116
ui/src/pages/Admin/SensitiveKeysPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user