svix embedded portal (#1007)

https://www.loom.com/share/ade557d34b674ecb9ae1d703b5332c9d
<!--

Make sure you've read the CONTRIBUTING.md guidelines:
https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md

-->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added support for inline webhook configuration portal rendering when
available
  * Enhanced webhooks page with improved theming support

* **Refactor**
* Updated webhook token API to return structured data including optional
server URL alongside token

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Enables embedded Svix portal on the Webhooks page when available,
updating the token API and shared types to return an optional portal URL
and wiring it through the admin app.
> 
> - **Frontend (Dashboard Webhooks page)**:
> - Conditionally render Svix `AppPortal` when `svixToken.url` is
provided; otherwise fall back to `SvixProvider` with token.
> - Integrate theme support (`next-themes`) for portal `darkMode`;
import `svix-react` styles.
> - **Backend (API)**:
> - Update `POST /api/latest/webhooks/svix-token` to return `{ token,
url? }`, deriving `url` only when no `STACK_SVIX_SERVER_URL` is set.
> - **Shared Types/SDK**:
>   - Extend `svixTokenAdminReadSchema` to include optional `url`.
> - Change admin app `useSvixToken()` to return `{ token, url }` and
propagate through implementation.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
9f5dc52ecf. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Konsti Wohlwend <n2d4xc@gmail.com>
This commit is contained in:
BilalG1 2025-11-11 11:06:09 -08:00 committed by GitHub
parent 6763f1ac03
commit e843a2b637
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 61 additions and 44 deletions

View File

@ -2,15 +2,20 @@ import { getSvixClient } from "@/lib/webhooks";
import { createCrudHandlers } from "@/route-handlers/crud-handler";
import { svixTokenCrud } from "@stackframe/stack-shared/dist/interface/crud/svix-token";
import { yupObject } from "@stackframe/stack-shared/dist/schema-fields";
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies";
const svixServerUrl = getEnvVariable("STACK_SVIX_SERVER_URL", "");
const appPortalCrudHandlers = createLazyProxy(() => createCrudHandlers(svixTokenCrud, {
paramsSchema: yupObject({}),
onCreate: async ({ auth }) => {
const svix = getSvixClient();
await svix.application.getOrCreate({ uid: auth.project.id, name: auth.project.id });
const result = await svix.authentication.appPortalAccess(auth.project.id, {});
return { token: result.token };
// svix embedded app portal is only available on hosted svix.
const url = svixServerUrl ? undefined : result.url;
return { token: result.token, url };
},
}));

View File

@ -17,6 +17,56 @@ import { PageLayout } from "../page-layout";
import { useAdminApp } from "../use-admin-app";
import { getSvixResult } from "./utils";
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
import { useTheme } from "next-themes";
import { AppPortal } from "svix-react";
import "svix-react/style.css";
export default function PageClient() {
const stackAdminApp = useAdminApp();
const svixToken = stackAdminApp.useSvixToken();
const { resolvedTheme } = useTheme();
const [updateCounter, setUpdateCounter] = useState(0);
const [testDialogEndpoint, setTestDialogEndpoint] = useState<Endpoint | null>(null);
return (
<AppEnabledGuard appId="webhooks">
<PageLayout
title="Webhooks"
description="Webhooks are used to sync users and teams events from Stack to your own server."
>
{svixToken.url ? (
<div>
<AppPortal url={svixToken.url} darkMode={resolvedTheme === "dark"} fullSize />
</div>
) : (
<SvixProvider
key={updateCounter}
token={svixToken.token}
appId={stackAdminApp.projectId}
options={{ serverUrl: getPublicEnvVar('NEXT_PUBLIC_STACK_SVIX_SERVER_URL') }}
>
<Endpoints
updateFn={() => setUpdateCounter(x => x + 1)}
onTestRequested={(endpoint) => setTestDialogEndpoint(endpoint)}
/>
{testDialogEndpoint && (
<TestEndpointDialog
endpoint={testDialogEndpoint}
open
onOpenChange={(open) => {
if (!open) {
setTestDialogEndpoint(null);
}
}}
/>
)}
</SvixProvider>
)}
</PageLayout>
</AppEnabledGuard>
);
}
type Endpoint = {
id: string,
@ -157,7 +207,7 @@ function CreateDialog(props: {
);
}
export function EndpointEditDialog(props: {
function EndpointEditDialog(props: {
open: boolean,
onClose: () => void,
endpoint: Endpoint,
@ -369,42 +419,3 @@ function Endpoints(props: { updateFn: () => void, onTestRequested: (endpoint: En
);
}
}
export default function PageClient() {
const stackAdminApp = useAdminApp();
const svixToken = stackAdminApp.useSvixToken();
const [updateCounter, setUpdateCounter] = useState(0);
const [testDialogEndpoint, setTestDialogEndpoint] = useState<Endpoint | null>(null);
return (
<AppEnabledGuard appId="webhooks">
<PageLayout
title="Webhooks"
description="Webhooks are used to sync users and teams events from Stack to your own server."
>
<SvixProvider
key={updateCounter}
token={svixToken}
appId={stackAdminApp.projectId}
options={{ serverUrl: getPublicEnvVar('NEXT_PUBLIC_STACK_SVIX_SERVER_URL') }}
>
<Endpoints
updateFn={() => setUpdateCounter(x => x + 1)}
onTestRequested={(endpoint) => setTestDialogEndpoint(endpoint)}
/>
{testDialogEndpoint && (
<TestEndpointDialog
endpoint={testDialogEndpoint}
open
onOpenChange={(open) => {
if (!open) {
setTestDialogEndpoint(null);
}
}}
/>
)}
</SvixProvider>
</PageLayout>
</AppEnabledGuard>
);
}

View File

@ -3,6 +3,7 @@ import { yupObject, yupString } from "../../schema-fields";
export const svixTokenAdminReadSchema = yupObject({
token: yupString().defined(),
url: yupString().optional(),
}).defined();
export const svixTokenAdminCreateSchema = yupObject({}).defined();

View File

@ -416,9 +416,9 @@ export class _StackAdminAppImplIncomplete<HasTokenStore extends boolean, Project
}
// END_PLATFORM
// IF_PLATFORM react-like
useSvixToken(): string {
useSvixToken(): { token: string, url: string | undefined } {
const crud = useAsyncCache(this._svixTokenCache, [], "adminApp.useSvixToken()");
return crud.token;
return { token: crud.token, url: crud.url };
}
// END_PLATFORM

View File

@ -49,7 +49,7 @@ export type StackAdminApp<HasTokenStore extends boolean = boolean, ProjectId ext
updateProjectPermissionDefinition(permissionId: string, data: AdminProjectPermissionDefinitionUpdateOptions): Promise<void>,
deleteProjectPermissionDefinition(permissionId: string): Promise<void>,
useSvixToken(): string, // THIS_LINE_PLATFORM react-like
useSvixToken(): { token: string, url: string | undefined }, // THIS_LINE_PLATFORM react-like
sendTestEmail(options: {
recipientEmail: string,