perf(web): route-level code-splitting; harden channel config rendering

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwxdSt4reTm7Dj1oxRvpP3
This commit is contained in:
2026-07-04 16:04:17 +07:00
parent 41844d49a0
commit 8c35aed8f2
3 changed files with 43 additions and 25 deletions
+10
View File
@@ -48,6 +48,16 @@ test("отрисовывает список каналов без секрета
expect(screen.queryByDisplayValue(/123456/)).not.toBeInTheDocument()
})
test("formatConfig — вайтлист по типу канала не печатает незнакомые поля config", async () => {
vi.spyOn(api, "listChannels").mockResolvedValue([
{ id: "c5", type: "webhook", config: { url: "https://x", token: "LEAK" }, enabled: true },
])
renderPage()
expect(await screen.findByText(/url: https:\/\/x/)).toBeInTheDocument()
expect(document.body.textContent).not.toMatch(/LEAK/)
})
test("создание telegram-канала собирает config.chat_id + secret=bot_token", async () => {
const createSpy = vi.spyOn(api, "createChannel").mockResolvedValue({
id: "c3", type: "telegram", config: { chat_id: "999" }, enabled: true,
+10 -8
View File
@@ -69,13 +69,15 @@ type ChannelForm = z.infer<typeof channelFormSchema>
const EMPTY_FORM: ChannelForm = { type: "telegram", chatId: "", botToken: "", url: "" }
// Channel.config is a generic `object` end-to-end (T5/T7) — it never carries
// the secret (bot_token/signing key), only the public half (chat_id/url), so
// rendering every key is always safe to show in the list.
function formatConfig(config: object): string {
const entries = Object.entries(config as Record<string, unknown>)
if (entries.length === 0) return "—"
return entries.map(([k, v]) => `${k}=${String(v)}`).join(" · ")
// Печатаем ТОЛЬКО известные несекретные поля по типу канала — чтобы новый
// тип канала с чувствительным полем в config не «протёк» в DOM автоматически
// (Object.entries по всему config печатал бы любое поле, включая случайно
// сохранённый секрет).
function formatConfig(type: string, config: object): string {
const c = config as Record<string, unknown>
if (type === "telegram") return c.chat_id ? `chat_id: ${String(c.chat_id)}` : ""
if (type === "webhook") return c.url ? `url: ${String(c.url)}` : ""
return ""
}
function ChannelForm({ onCreated }: { onCreated: () => void }) {
@@ -294,7 +296,7 @@ export function ChannelsPage() {
<TableRow key={c.id}>
<TableCell className="font-dns">{c.type}</TableCell>
<TableCell className="font-dns text-xs text-muted-foreground">
{formatConfig(c.config)}
{formatConfig(c.type, c.config)}
</TableCell>
<TableCell>
<span