From 19142f40a95850e1ff74a047f51ffbbfe8a53acf Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Wed, 30 Jul 2025 09:32:44 -0700 Subject: [PATCH] Featurebase integration --- .vscode/settings.json | 1 + apps/dashboard/.env | 2 +- apps/dashboard/.env.development | 2 + apps/dashboard/package.json | 1 + .../integrations/featurebase/sso/page.tsx | 44 +++++++++++++++++++ apps/e2e/package.json | 8 ++-- pnpm-lock.yaml | 6 ++- 7 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 apps/dashboard/src/app/(main)/integrations/featurebase/sso/page.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index 4aa2e10c4..ddfb4ff1c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -33,6 +33,7 @@ "Emailable", "EMESSAGE", "Falsey", + "Featurebase", "fkey", "frontends", "geoip", diff --git a/apps/dashboard/.env b/apps/dashboard/.env index 83c48bc03..30a8a440b 100644 --- a/apps/dashboard/.env +++ b/apps/dashboard/.env @@ -12,5 +12,5 @@ NEXT_PUBLIC_STACK_SVIX_SERVER_URL=# For prod, leave it empty. For local developm NEXT_PUBLIC_STACK_HEAD_TAGS='[{ "tagName": "script", "attributes": {}, "innerHTML": "// insert head tags here" }]' STACK_DEVELOPMENT_TRANSLATION_LOCALE=# enter the locale to use for the translation provider here, for example: de-DE. Only works during development, not in production. Optional, by default don't translate NEXT_PUBLIC_STACK_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS='["internal"]' - NEXT_PUBLIC_STACK_DEBUGGER_ON_ASSERTION_ERROR=# set to true to open the debugger on assertion errors (set to true in .env.development) +STACK_FEATUREBASE_JWT_SECRET=# used for Featurebase SSO, you probably won't have to set this diff --git a/apps/dashboard/.env.development b/apps/dashboard/.env.development index 8e8c4b741..9cb84bc67 100644 --- a/apps/dashboard/.env.development +++ b/apps/dashboard/.env.development @@ -8,3 +8,5 @@ NEXT_PUBLIC_STACK_SVIX_SERVER_URL=http://localhost:8113 STACK_ARTIFICIAL_DEVELOPMENT_DELAY_MS=50 NEXT_PUBLIC_STACK_DEBUGGER_ON_ASSERTION_ERROR=true + +STACK_FEATUREBASE_JWT_SECRET=secret-value diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 7372b91a5..8500c40cb 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -38,6 +38,7 @@ "clsx": "^2.0.0", "dotenv-cli": "^7.3.0", "geist": "^1", + "jose": "^5.2.2", "lodash": "^4.17.21", "lucide-react": "^0.508.0", "next": "15.4.1", diff --git a/apps/dashboard/src/app/(main)/integrations/featurebase/sso/page.tsx b/apps/dashboard/src/app/(main)/integrations/featurebase/sso/page.tsx new file mode 100644 index 000000000..e0ab747ac --- /dev/null +++ b/apps/dashboard/src/app/(main)/integrations/featurebase/sso/page.tsx @@ -0,0 +1,44 @@ +import { stackServerApp } from "@/stack"; +import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; +import { urlString } from "@stackframe/stack-shared/dist/utils/urls"; +import * as jose from "jose"; +import { redirect } from "next/navigation"; + +export default async function FeaturebaseSSO({ + searchParams, +}: { + searchParams: Promise<{ return_to?: string }>, +}) { + const { return_to: returnTo } = await searchParams; + + if (!returnTo) { + return
Missing return_to parameter. Please go back and try again.
; + } + + const user = await stackServerApp.getUser(); + if (!user) { + redirect(urlString`/handler/sign-in?after_auth_return_to=${urlString`/integrations/featurebase/sso?return_to=${returnTo}`}`); + } + + const featurebaseSecret = getEnvVariable("STACK_FEATUREBASE_JWT_SECRET"); + + // Create JWT token + const secret = new TextEncoder().encode(featurebaseSecret); + const jwt = await new jose.SignJWT({ + userId: user.id, + email: user.primaryEmail, + name: user.displayName || undefined, + profilePicture: user.profileImageUrl || undefined, + }) + .setProtectedHeader({ alg: "HS256" }) + .setIssuer("stack-auth") + .setExpirationTime("10min") + .sign(secret); + + // Redirect to Featurebase with JWT and return_to + const featurebaseUrl = new URL("https://feedback.stack-auth.com/api/v1/auth/access/jwt"); + featurebaseUrl.searchParams.set("jwt", jwt); + featurebaseUrl.searchParams.set("return_to", returnTo); + + redirect(featurebaseUrl.toString()); +} diff --git a/apps/e2e/package.json b/apps/e2e/package.json index a3b847998..1a4590fb6 100644 --- a/apps/e2e/package.json +++ b/apps/e2e/package.json @@ -11,10 +11,12 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@oslojs/otp": "^1.1.0", "@stackframe/js": "workspace:*", "@stackframe/stack-shared": "workspace:*", - "dotenv": "^16.4.5", - "jose": "^5.2.2", - "@oslojs/otp": "^1.1.0" + "dotenv": "^16.4.5" + }, + "devDependencies": { + "jose": "^5.6.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd8fcf0f5..12b06d5d3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -370,6 +370,9 @@ importers: geist: specifier: ^1 version: 1.3.0(next@15.4.1(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)) + jose: + specifier: ^5.2.2 + version: 5.6.3 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -491,8 +494,9 @@ importers: dotenv: specifier: ^16.4.5 version: 16.4.5 + devDependencies: jose: - specifier: ^5.2.2 + specifier: ^5.6.3 version: 5.6.3 apps/mcp-server: