From 4f198bd55b0c030ce7eccaabcbf161ae107c055a Mon Sep 17 00:00:00 2001 From: BilalG1 Date: Wed, 22 Apr 2026 17:27:37 -0700 Subject: [PATCH] Fix dashboard UI bugs: webhook detail crash and http domain silent https upgrade (#1362) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes two dashboard UI bugs surfaced while auditing the project area for large user-visible issues: 1. **Webhook detail page completely broken** — the page shows a blank screen because the SvixProvider token was being set to the string `"[object Object]"`. 2. **Editing a trusted domain with an `http://` base URL silently upgrades it to `https://`** — saving the edit dialog without changing anything changes the protocol, breaking callbacks to the original host. Both are corrected with minimal, targeted changes in the dashboard app. No API, schema, or shared package changes are required. --- ## Bug 1 — Webhook detail page crashes because `svixToken + ''` yields `"[object Object]"` ### Where `apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/webhooks/[endpointId]/page-client.tsx` ### Root cause `stackAdminApp.useSvixToken()` returns an object of shape `{ token: string, url: string | null }` (see `packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts`). The page was doing: ```ts const svixToken = stackAdminApp.useSvixToken(); const [updateCounter, setUpdateCounter] = useState(0); // This is a hack to make sure svix hooks update when content changes const svixTokenUpdated = useMemo(() => { return svixToken + ''; }, [svixToken, updateCounter]); // … ``` `svixToken + ''` coerces the object to the string `"[object Object]"`, which is then passed to `` as the auth token. Every nested Svix hook (`useEndpoint`, `useEndpointSecret`, `useEndpointMessageAttempts`) authenticates with that bogus token, gets a `401 {"code":"authentication_failed","detail":"Invalid token"}` from Svix, and `getSvixResult` (`apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/webhooks/utils.tsx`) throws, crashing the page. Additional notes while in there: - `setUpdateCounter` was declared but never called anywhere, so the surrounding `useMemo`/`useState` was dead weight as well as broken. Removing it removes the dead code too. - The neighbouring list page (`webhooks/page-client.tsx`) already uses the correct shape (`svixToken.token`, `svixToken.url`), which is why the list page rendered correctly while the detail page didn't. ### Fix Pass `svixToken.token` directly to `` and drop the unused counter/memo. ```ts export default function PageClient(props: { endpointId: string }) { const stackAdminApp = useAdminApp(); const svixToken = stackAdminApp.useSvixToken(); return ( ); } ``` ### Reproduction (before fix) 1. Enable the Webhooks app on a project. 2. Create an endpoint with any URL. 3. Open the row's action menu and click **View Details**. 4. The page renders blank (Svix hooks throw 401 Invalid token; the error boundary unmounts the detail tree). URL, Description, Verification Secret, and Events History never appear. ### Before / After | Before | After | | --- | --- | | ![Webhook detail blank before fix](https://gist.githubusercontent.com/BilalG1/f31b7631cb914ea8fd0113b97d26319e/raw/bug1-webhook-detail-before.png) | ![Webhook detail renders after fix](https://gist.githubusercontent.com/BilalG1/f31b7631cb914ea8fd0113b97d26319e/raw/bug1-webhook-detail-after.png) | --- ## Bug 2 — Editing an `http://` trusted domain silently upgrades it to `https://` ### Where `apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx` ### Root cause In `EditDialog`, the form's `defaultValues` always set `insecureHttp: false`, regardless of the protocol of the domain being edited: ```ts defaultValues={{ addWww: props.type === 'create', domain: props.type === 'update' ? props.defaultDomain.replace(/^https?:\/\//, "") : undefined, handlerPath: props.type === 'update' ? props.defaultHandlerPath : "/handler", insecureHttp: false, // ← ignores the existing protocol }} ``` The `domain` field strips `http(s)://` for display but the protocol itself is only tracked through the `insecureHttp` switch, which lives inside the collapsed-by-default **Advanced** accordion. On submit: ```ts const protocol = values.insecureHttp ? 'http://' : 'https://'; const baseUrl = protocol + values.domain; ``` So an `http://myapp.test` entry reopens with `insecureHttp: false`, the Advanced section stays collapsed, the user sees nothing wrong, and hitting **Save** (even with zero visible changes) writes `https://myapp.test` back to config. Existing redirects from SSO / email verification flows that depend on the original `http://` host stop working. ### Fix Derive `insecureHttp` from the existing `defaultDomain` when editing: ```ts insecureHttp: props.type === 'update' ? props.defaultDomain.startsWith('http://') : false, ``` This makes the switch in the Advanced panel pre-check itself correctly and the submit path emits the preserved protocol. ### Reproduction (before fix) 1. Go to **Project Settings → Trusted Domains**. 2. Add a new domain, expand **Advanced**, toggle **Use HTTP instead of HTTPS** on, enter `myapp.test`, click **Create**. The list now shows `http://myapp.test`. 3. Click the row's **⋯ → Edit**, then **Save** without changing anything. 4. Observe the list now shows `https://myapp.test`. ### Before / After **Domain list after an edit+save:** | Before (http silently became https) | After (http preserved) | | --- | --- | | ![Domain list before](https://gist.githubusercontent.com/BilalG1/f31b7631cb914ea8fd0113b97d26319e/raw/bug4-domain-list-before.png) | ![Domain list after](https://gist.githubusercontent.com/BilalG1/f31b7631cb914ea8fd0113b97d26319e/raw/bug4-domain-list-after.png) | In the "before" screenshot, `http://myapp.test` was edited with no changes and silently became `https://myapp.test`. `http://www.myapp.test` (not edited) stayed `http://`, confirming the bug is triggered only through the edit-save path. **Edit dialog (Advanced expanded):** | Before (HTTP switch always off) | After (reflects stored protocol) | | --- | --- | | ![Edit dialog before](https://gist.githubusercontent.com/BilalG1/f31b7631cb914ea8fd0113b97d26319e/raw/bug4-edit-dialog-before.png) | ![Edit dialog after](https://gist.githubusercontent.com/BilalG1/f31b7631cb914ea8fd0113b97d26319e/raw/bug4-edit-dialog-after.png) | The "after" dialog also shows the protocol prefix label flip from `https://` to `http://` next to the input — a second visual cue that the user is editing an HTTP domain. --- ## Scope / out of scope In scope here: - The two fixes above, plus a small amount of dead-code cleanup adjacent to the first fix (the unused `updateCounter` / `useMemo` hack). Intentionally **not** included (tracked separately from the same audit — see internal notes): - Cursor pagination cache wipe across Users/Teams/Transactions tables (`data-table/common/cursor-pagination.tsx`) - Email Outbox "Scheduled At" input being reset on every keystroke and rendered in the wrong timezone (`email-outbox/page-client.tsx`) - Latent empty-group handling in the sign-up rule builder (validator + CEL emitter), which is real in code but not currently reachable through the editor UI These are broader and deserve their own PRs. ## Test plan - [ ] **Bug 1 (webhook detail):** Enable Webhooks on a project, create an endpoint, open **View Details**. Confirm URL, Description, Verification Secret, and Events History render (no 401s in the console, no blank page). Confirm the Copy button on the verification secret still copies the key. - [ ] **Bug 2 (domain edit preserves http):** Add an `http://` trusted domain. Edit it and save with no changes — list should still show `http://`. Edit again, flip the Advanced switch to HTTPS, save — list should show `https://`. Repeat with the inverse direction (start https, flip to http). - [ ] **Regression sweep:** Webhooks list page, create/delete endpoint, copy signing secret; Trusted Domains add/delete; auth-methods callbacks against an `http://localhost` domain continue to work. - [ ] `pnpm typecheck` passes locally. (`pnpm lint` was also run against the dashboard app and is clean.) ## Summary by CodeRabbit * **Bug Fixes** * Domain editing now correctly initializes and preserves the protocol type (HTTP or HTTPS) based on the existing domain configuration. --- .../projects/[projectId]/domains/page-client.tsx | 2 +- .../[projectId]/webhooks/[endpointId]/page-client.tsx | 11 ++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx index 2a6269bc2..de60fc93a 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx @@ -113,7 +113,7 @@ function EditDialog(props: { addWww: props.type === 'create', domain: props.type === 'update' ? props.defaultDomain.replace(/^https?:\/\//, "") : undefined, handlerPath: props.type === 'update' ? props.defaultHandlerPath : "/handler", - insecureHttp: false, + insecureHttp: props.type === 'update' ? props.defaultDomain.startsWith('http://') : false, }} onOpenChange={props.onOpenChange} trigger={props.trigger} diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/webhooks/[endpointId]/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/webhooks/[endpointId]/page-client.tsx index 438626bf9..daca97828 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/webhooks/[endpointId]/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/webhooks/[endpointId]/page-client.tsx @@ -4,7 +4,7 @@ import { DesignAlert, DesignBadge, DesignButton, DesignCard, DesignEditableGrid, import { CopyButton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui"; import { getPublicEnvVar } from '@/lib/env'; import { CaretLeftIcon, CaretRightIcon, InfoIcon, KeyIcon, LinkIcon, TextAlignLeftIcon } from "@phosphor-icons/react"; -import { useMemo, useState } from "react"; +import { useMemo } from "react"; import { SvixProvider, useEndpoint, useEndpointMessageAttempts, useEndpointSecret } from "svix-react"; import { AppEnabledGuard } from "../../app-enabled-guard"; import { PageLayout } from "../../page-layout"; @@ -159,18 +159,11 @@ function MessageTable(props: { endpointId: string }) { export default function PageClient(props: { endpointId: string }) { const stackAdminApp = useAdminApp(); const svixToken = stackAdminApp.useSvixToken(); - const [updateCounter, setUpdateCounter] = useState(0); - - // This is a hack to make sure svix hooks update when content changes - const svixTokenUpdated = useMemo(() => { - return svixToken + ''; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [svixToken, updateCounter]); return (