feat: Hide secrets on api and channels
Chart Release / chart (push) Successful in 15s
Server Deploy / deploy (push) Successful in 8m5s

This commit is contained in:
2026-08-14 12:23:36 +00:00
parent ac61015cc0
commit aa1c8e4aa1
6 changed files with 156 additions and 14 deletions
+30 -12
View File
@@ -3,7 +3,14 @@
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import Link from "next/link";
import { api, ChannelInput, ChannelType, NotificationChannel } from "@/lib/api";
import {
api,
CHANNEL_SECRET_FIELDS,
ChannelInput,
ChannelType,
NotificationChannel,
REDACTED_SECRET,
} from "@/lib/api";
import { Badge, Button, Card, ConfirmDialog, friendlyMessage, useToast } from "@/components/ui";
import { VulnAlertRulesCard } from "@/components/vulnerabilities/VulnAlertRulesCard";
@@ -67,17 +74,28 @@ function ChannelForm({ initial, onDone }: { initial?: NotificationChannel; onDon
))}
</select>
</div>
{CONFIG_FIELDS[type].map((field) => (
<div key={field}>
<label className={labelClass}>{field}</label>
<input
className={inputClass}
type={field === "password" ? "password" : "text"}
value={config[field] ?? ""}
onChange={(e) => setConfig({ ...config, [field]: e.target.value })}
/>
</div>
))}
{CONFIG_FIELDS[type].map((field) => {
// A secret comes back from the API as the sentinel, never as itself.
// The field renders empty rather than showing bullets in a URL box, and
// the sentinel is left sitting in state so an untouched save preserves
// the credential. Typing replaces it; clearing the field back to empty
// is how a credential is removed.
const secret = CHANNEL_SECRET_FIELDS[type].includes(field);
const value = config[field] ?? "";
const unchanged = secret && value === REDACTED_SECRET;
return (
<div key={field}>
<label className={labelClass}>{field}</label>
<input
className={inputClass}
type={field === "password" ? "password" : "text"}
value={unchanged ? "" : value}
placeholder={unchanged ? "unchanged — type to replace" : undefined}
onChange={(e) => setConfig({ ...config, [field]: e.target.value })}
/>
</div>
);
})}
{error && <p className="text-sm text-danger">{(error as Error).message}</p>}
<div className="flex gap-3">
<Button type="submit" variant="primary" loading={isPending}>