This commit is contained in:
@@ -66,6 +66,85 @@ function Field({
|
||||
const inputClass =
|
||||
"w-full rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary placeholder:text-text-tertiary focus:border-accent/50 focus:outline-none focus:ring-1 focus:ring-accent/30";
|
||||
|
||||
function SecretsTokenCard({
|
||||
tokenSet,
|
||||
rotatedAt,
|
||||
}: {
|
||||
tokenSet: boolean;
|
||||
rotatedAt?: string;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const readUrl =
|
||||
typeof window !== "undefined"
|
||||
? `${window.location.origin}/secrets/<group>`
|
||||
: "/secrets/<group>";
|
||||
|
||||
const { mutate: rotate, isPending } = useMutation({
|
||||
mutationFn: api.rotateSecretsToken,
|
||||
onSuccess: (res) => {
|
||||
setToken(res.token);
|
||||
queryClient.invalidateQueries({ queryKey: ["settings"] });
|
||||
},
|
||||
});
|
||||
|
||||
async function copy() {
|
||||
if (!token) return;
|
||||
await navigator.clipboard.writeText(token);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Secrets Read Token (ESO)</CardTitle>
|
||||
</CardHeader>
|
||||
<p className="mb-5 text-sm text-text-secondary">
|
||||
Kubernetes External Secrets Operator authenticates to the read endpoint with this bearer
|
||||
token. Point your <span className="font-mono">ClusterSecretStore</span> at{" "}
|
||||
<span className="font-mono text-text-primary">{readUrl}</span>.
|
||||
</p>
|
||||
|
||||
<div className="mb-4 flex items-center gap-2 text-sm">
|
||||
<span
|
||||
className={`inline-block h-2 w-2 rounded-full ${tokenSet ? "bg-success" : "bg-text-tertiary"}`}
|
||||
/>
|
||||
<span className="text-text-secondary">
|
||||
{tokenSet ? "A read token is configured" : "No read token configured yet"}
|
||||
{tokenSet && rotatedAt && ` · rotated ${new Date(rotatedAt).toLocaleString()}`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{token && (
|
||||
<div className="mb-4 rounded-lg border border-warning/30 bg-warning/10 p-3">
|
||||
<p className="mb-2 text-xs font-medium text-warning">
|
||||
Copy this token now — it will not be shown again.
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 overflow-x-auto rounded bg-surface-2 px-2 py-1.5 font-mono text-xs text-text-primary">
|
||||
{token}
|
||||
</code>
|
||||
<Button type="button" variant="ghost" size="sm" onClick={copy}>
|
||||
{copied ? "Copied!" : "Copy"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button type="button" variant="primary" loading={isPending} onClick={() => rotate()}>
|
||||
{tokenSet ? "Rotate Token" : "Generate Token"}
|
||||
</Button>
|
||||
{tokenSet && (
|
||||
<p className="mt-2 text-xs text-text-tertiary">
|
||||
Rotating invalidates the previous token. Update the Kubernetes secret afterwards.
|
||||
</p>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SettingsPage() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -302,6 +381,13 @@ export default function SettingsPage() {
|
||||
{saved && <span className="text-sm text-success">Settings saved successfully.</span>}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 max-w-xl">
|
||||
<SecretsTokenCard
|
||||
tokenSet={settings?.secrets?.read_token_set ?? false}
|
||||
rotatedAt={settings?.secrets?.rotated_at}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user