From ba33dde116312b6ac8226cf87bab8502c26370a6 Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Sun, 28 Dec 2025 19:38:17 +0100 Subject: [PATCH 1/9] Fix Docker build --- apps/backend/package.json | 34 ++++++++++--------- docker/server/Dockerfile | 1 - docs/scripts/generate-functional-api-docs.mjs | 2 +- package.json | 6 ++-- turbo.json | 3 -- 5 files changed, 22 insertions(+), 24 deletions(-) diff --git a/apps/backend/package.json b/apps/backend/package.json index a63dd4c4ad..8332712e23 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -6,38 +6,40 @@ "scripts": { "clean": "rimraf src/generated && rimraf .next && rimraf node_modules", "typecheck": "tsc --noEmit", - "with-env": "dotenv -c development --", - "with-env:prod": "dotenv -c --", + "with-env": "dotenv -c --", + "with-env:dev": "dotenv -c development --", + "with-env:prod": "dotenv -c production --", "dev": "concurrently -n \"dev,codegen,prisma-studio,email-queue\" -k \"next dev --port ${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02\" \"pnpm run codegen:watch\" \"pnpm run prisma-studio\" \"pnpm run run-email-queue\"", "build": "pnpm run codegen && next build", "docker-build": "pnpm run codegen && next build --experimental-build-mode compile", "build-self-host-migration-script": "tsup --config scripts/db-migrations.tsup.config.ts", "analyze-bundle": "next experimental-analyze", "start": "next start --port ${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02", - "codegen-prisma": "pnpm run prisma generate", + "codegen-prisma": "STACK_DATABASE_CONNECTION_STRING=\"${STACK_DATABASE_CONNECTION_STRING:-placeholder-database-connection-string}\" pnpm run prisma generate", "codegen-prisma:watch": "pnpm run prisma generate --watch", "codegen-route-info": "pnpm run with-env tsx scripts/generate-route-info.ts", "codegen-route-info:watch": "pnpm run with-env tsx watch --clear-screen=false scripts/generate-route-info.ts", - "codegen": "pnpm run with-env pnpm run generate-migration-imports && pnpm run with-env bash -c 'if [ \"$STACK_ACCELERATE_ENABLED\" = \"true\" ]; then pnpm run prisma generate --no-engine; else pnpm run codegen-prisma; fi' && pnpm run codegen-route-info", - "codegen:watch": "concurrently -n \"prisma,docs,route-info,migration-imports\" -k \"pnpm run codegen-prisma:watch\" \"pnpm run watch-docs\" \"pnpm run codegen-route-info:watch\" \"pnpm run generate-migration-imports:watch\"", + "codegen": "pnpm run with-env pnpm run generate-migration-imports && pnpm run with-env bash -c 'if [ \"$STACK_ACCELERATE_ENABLED\" = \"true\" ]; then pnpm run prisma generate --no-engine; else pnpm run codegen-prisma; fi' && pnpm run codegen-docs && pnpm run codegen-route-info", + "codegen:watch": "concurrently -n \"prisma,docs,route-info,migration-imports\" -k \"pnpm run codegen-prisma:watch\" \"pnpm run codegen-docs:watch\" \"pnpm run codegen-route-info:watch\" \"pnpm run generate-migration-imports:watch\"", "psql-inner": "psql $(echo $STACK_DATABASE_CONNECTION_STRING | sed 's/\\?.*$//')", - "psql": "pnpm run with-env pnpm run psql-inner", - "prisma-studio": "pnpm run with-env prisma studio --port ${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}06 --browser none", + "psql": "pnpm run with-env:dev pnpm run psql-inner", + "prisma-studio": "pnpm run with-env:dev prisma studio --port ${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}06 --browser none", + "prisma:dev": "pnpm run with-env:dev prisma", "prisma": "pnpm run with-env prisma", - "db:migration-gen": "pnpm run with-env tsx scripts/db-migrations.ts generate-migration-file", - "db:reset": "pnpm run with-env tsx scripts/db-migrations.ts reset", - "db:seed": "pnpm run with-env tsx scripts/db-migrations.ts seed", - "db:init": "pnpm run with-env tsx scripts/db-migrations.ts init", - "db:migrate": "pnpm run with-env tsx scripts/db-migrations.ts migrate", + "db:migration-gen": "pnpm run with-env:dev tsx scripts/db-migrations.ts generate-migration-file", + "db:reset": "pnpm run with-env:dev tsx scripts/db-migrations.ts reset", + "db:seed": "pnpm run with-env:dev tsx scripts/db-migrations.ts seed", + "db:init": "pnpm run with-env:dev tsx scripts/db-migrations.ts init", + "db:migrate": "pnpm run with-env:dev tsx scripts/db-migrations.ts migrate", "generate-migration-imports": "pnpm run with-env tsx scripts/generate-migration-imports.ts", "generate-migration-imports:watch": "chokidar 'prisma/migrations/**/*.sql' -c 'pnpm run generate-migration-imports'", "lint": "eslint .", - "watch-docs": "pnpm run with-env tsx watch --exclude '**/node_modules/**' --clear-screen=false scripts/generate-openapi-fumadocs.ts", - "generate-openapi-fumadocs": "pnpm run with-env tsx scripts/generate-openapi-fumadocs.ts", + "codegen-docs": "pnpm run with-env tsx scripts/generate-openapi-fumadocs.ts", + "codegen-docs:watch": "pnpm run with-env tsx watch --exclude '**/node_modules/**' --clear-screen=false scripts/generate-openapi-fumadocs.ts", "generate-keys": "pnpm run with-env tsx scripts/generate-keys.ts", "db-seed-script": "pnpm run db:seed", - "verify-data-integrity": "pnpm run with-env tsx scripts/verify-data-integrity.ts", - "run-email-queue": "pnpm run with-env tsx scripts/run-email-queue.ts" + "verify-data-integrity": "pnpm run with-env:dev tsx scripts/verify-data-integrity.ts", + "run-email-queue": "pnpm run with-env:dev tsx scripts/run-email-queue.ts" }, "prisma": { "seed": "pnpm run db-seed-script" diff --git a/docker/server/Dockerfile b/docker/server/Dockerfile index 12efa4ed4a..3dea6fc3ae 100644 --- a/docker/server/Dockerfile +++ b/docker/server/Dockerfile @@ -40,7 +40,6 @@ COPY .gitignore . COPY pnpm-workspace.yaml . COPY turbo.json . COPY configs ./configs -RUN cat ./pnpm-lock.yaml RUN --mount=type=cache,id=pnpm,target=/pnpm/store STACK_SKIP_TEMPLATE_GENERATION=true pnpm install --frozen-lockfile # copy over the rest of the code for the build diff --git a/docs/scripts/generate-functional-api-docs.mjs b/docs/scripts/generate-functional-api-docs.mjs index 0ebb79c7c1..49db90a4b5 100644 --- a/docs/scripts/generate-functional-api-docs.mjs +++ b/docs/scripts/generate-functional-api-docs.mjs @@ -427,7 +427,7 @@ async function processApiTypeInIsolation(apiType) { if (!fs.existsSync(jsonFile)) { console.log(`⚠️ OpenAPI file not found: ${jsonFile}`); - console.log(` Run 'pnpm run generate-openapi-fumadocs' from the root to generate OpenAPI schemas first.`); + console.log(` Run 'pnpm run codegen' from the root to generate OpenAPI schemas first.`); return; } diff --git a/package.json b/package.json index e72b84c1f6..e69e20f0ab 100644 --- a/package.json +++ b/package.json @@ -14,13 +14,14 @@ "build:backend": "pnpm pre && turbo run build --filter=@stackframe/stack-backend...", "build:dashboard": "pnpm pre && turbo run build --filter=@stackframe/stack-dashboard...", "build:demo": "pnpm pre && turbo run build --filter=demo-app...", - "build:docs": "pnpm run build:packages && pnpm run build:backend && turbo run generate-openapi-fumadocs && pnpm run --filter=@stackframe/stack-docs generate-openapi-docs && turbo run build --filter=@stackframe/stack-docs", + "build:docs": "pnpm run build:packages && pnpm run codegen && pnpm run build:backend && pnpm run --filter=@stackframe/stack-docs generate-openapi-docs && turbo run build --filter=@stackframe/stack-docs", "build:packages": "pnpm pre && turbo run build --filter=./packages/*", "restart-dev-in-background": "pnpm pre && pnpm run kill-dev:named && (pnpm run dev:named > dev-server.log.untracked.txt 2>&1 &) && echo 'Starting dev server in background... (Logs are in dev-server.log.untracked.txt)' && pnpx wait-on http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02 -t 120000 && echo 'Dev server running.'", "restart-dev-environment": "pnpm pre && pnpm run build:packages && pnpm run codegen && pnpm run restart-deps && pnpm run restart-dev-in-background", "stop-dev-environment": "pnpm pre && pnpm run kill-dev:named && pnpm run stop-deps", "clean": "pnpm pre-no-codegen && turbo run clean && rimraf --glob **/.next && rimraf --glob **/.turbo && rimraf .turbo && rimraf --glob **/node_modules", - "codegen": "pnpm pre && turbo run codegen && pnpm run generate-sdks && pnpm run generate-openapi-fumadocs", + "codegen": "pnpm pre && turbo run codegen && pnpm run generate-sdks", + "codegen:backend": "pnpm pre && turbo run codegen --filter=@stackframe/stack-backend...", "deps-compose": "docker compose -p stack-dependencies-${NEXT_PUBLIC_STACK_PORT_PREFIX:-81} -f docker/dependencies/docker.compose.yaml", "stop-deps": "POSTGRES_DELAY_MS=0 pnpm run deps-compose kill && POSTGRES_DELAY_MS=0 pnpm run deps-compose down -v", "wait-until-postgres-is-ready:pg_isready": "until pg_isready -h localhost -p ${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}28; do sleep 1; done", @@ -55,7 +56,6 @@ "test:e2e": "pnpm pre && vitest e2e/tests", "test:unit": "pnpm pre && vitest src", "verify-data-integrity": "pnpm pre && pnpm -C apps/backend run verify-data-integrity", - "generate-openapi-fumadocs": "turbo run generate-openapi-fumadocs", "generate-keys": "pnpm pre && turbo run generate-keys", "generate-sdks": "pnpx --package=tsx tsx ./scripts/generate-sdks.ts", "generate-sdks:watch": "chokidar --silent -c 'pnpm run generate-sdks' './packages/template' --ignore './packages/template/package.json' --ignore '**/node_modules/**' --ignore '**/dist/**' --ignore '**/.turbo/**' --throttle 2000", diff --git a/turbo.json b/turbo.json index 26860ac694..cf6655b383 100644 --- a/turbo.json +++ b/turbo.json @@ -75,9 +75,6 @@ "^build" ] }, - "@stackframe/stack-backend#generate-openapi-fumadocs": { - "cache": false - }, "clean": { "cache": false }, From 8f74949a7fab6a286093b7737d84462ebfd8bbf2 Mon Sep 17 00:00:00 2001 From: Konsti Wohlwend Date: Sun, 28 Dec 2025 20:25:04 +0100 Subject: [PATCH 2/9] Speed up tests (#1063) --- .../workflows/check-prisma-migrations.yaml | 2 +- apps/backend/package.json | 2 +- apps/backend/scripts/run-email-queue.ts | 4 +- apps/e2e/tests/backend/backend-helpers.ts | 7 +- .../endpoints/api/v1/data-vault.test.ts | 8 +- .../api/v1/integrations/custom/domain.test.ts | 10 +- .../api/v1/integrations/custom/oauth.test.ts | 8 +- .../custom/projects/transfer.test.ts | 12 +- .../api/v1/integrations/neon/api-keys.test.ts | 4 +- .../api/v1/integrations/neon/domain.test.ts | 12 +- .../integrations/neon/oauth-providers.test.ts | 10 +- .../api/v1/integrations/neon/oauth.test.ts | 8 +- .../neon/projects/current.test.ts | 6 +- .../neon/projects/transfer.test.ts | 12 +- .../api/v1/internal/api-keys.test.ts | 4 +- .../api/v1/internal/email-templates.test.ts | 4 +- .../api/v1/internal/payments/setup.test.ts | 4 +- .../api/v1/internal/projects.test.ts | 12 +- .../api/v1/internal/projects/transfer.test.ts | 27 ++-- .../api/v1/notification-preferences.test.ts | 6 +- .../endpoints/api/v1/oauth-providers.test.ts | 40 +++--- .../outdated--create-purchase-url.test.ts | 6 +- .../outdated--purchase-session.test.ts | 2 +- .../v1/payments/create-purchase-url.test.ts | 8 +- .../endpoints/api/v1/payments/items.test.ts | 4 +- .../api/v1/payments/products.test.ts | 2 +- .../api/v1/payments/purchase-session.test.ts | 4 +- .../api/v1/project-permissions.test.ts | 16 +-- .../backend/endpoints/api/v1/projects.test.ts | 28 ++-- .../endpoints/api/v1/team-invitations.test.ts | 45 +++--- .../api/v1/team-member-profiles.test.ts | 16 +-- .../endpoints/api/v1/team-memberships.test.ts | 136 ++++++++---------- .../endpoints/api/v1/team-permissions.test.ts | 16 +-- .../backend/endpoints/api/v1/teams.test.ts | 76 +++++----- .../backend/endpoints/api/v1/users.test.ts | 39 +++-- apps/e2e/tests/general/setup-wizard.test.ts | 3 +- 36 files changed, 288 insertions(+), 315 deletions(-) diff --git a/.github/workflows/check-prisma-migrations.yaml b/.github/workflows/check-prisma-migrations.yaml index fcf0087952..f8eda7877d 100644 --- a/.github/workflows/check-prisma-migrations.yaml +++ b/.github/workflows/check-prisma-migrations.yaml @@ -58,4 +58,4 @@ jobs: run: pnpm run db:init - name: Check for differences in Prisma schema and current DB - run: cd apps/backend && pnpm run prisma migrate diff --from-config-datasource --to-schema ./prisma/schema.prisma --exit-code + run: cd apps/backend && pnpm run prisma:dev migrate diff --from-config-datasource --to-schema ./prisma/schema.prisma --exit-code diff --git a/apps/backend/package.json b/apps/backend/package.json index 8332712e23..81b1835fbd 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -16,7 +16,7 @@ "analyze-bundle": "next experimental-analyze", "start": "next start --port ${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02", "codegen-prisma": "STACK_DATABASE_CONNECTION_STRING=\"${STACK_DATABASE_CONNECTION_STRING:-placeholder-database-connection-string}\" pnpm run prisma generate", - "codegen-prisma:watch": "pnpm run prisma generate --watch", + "codegen-prisma:watch": "STACK_DATABASE_CONNECTION_STRING=\"${STACK_DATABASE_CONNECTION_STRING:-placeholder-database-connection-string}\" pnpm run prisma generate --watch", "codegen-route-info": "pnpm run with-env tsx scripts/generate-route-info.ts", "codegen-route-info:watch": "pnpm run with-env tsx watch --clear-screen=false scripts/generate-route-info.ts", "codegen": "pnpm run with-env pnpm run generate-migration-imports && pnpm run with-env bash -c 'if [ \"$STACK_ACCELERATE_ENABLED\" = \"true\" ]; then pnpm run prisma generate --no-engine; else pnpm run codegen-prisma; fi' && pnpm run codegen-docs && pnpm run codegen-route-info", diff --git a/apps/backend/scripts/run-email-queue.ts b/apps/backend/scripts/run-email-queue.ts index edb56da117..de68726962 100644 --- a/apps/backend/scripts/run-email-queue.ts +++ b/apps/backend/scripts/run-email-queue.ts @@ -8,12 +8,14 @@ async function main() { const baseUrl = `http://localhost:${getEnvVariable('NEXT_PUBLIC_STACK_PORT_PREFIX', '81')}02`; + // Wait a few seconds to make sure the server is fully started + await wait(5_000); + const run = () => runAsynchronously(async () => { // If a the server is restarted, then the existing email queue step may be cancelled prematurely. That's why we // have an extra loop here to detect and restart the email queue step if it completes too quickly. const startTime = performance.now(); while (true) { - console.log("Running email queue step..."); const res = await fetch(`${baseUrl}/api/latest/internal/email-queue-step`, { method: "GET", diff --git a/apps/e2e/tests/backend/backend-helpers.ts b/apps/e2e/tests/backend/backend-helpers.ts index a2ff1140b9..79c1c43d31 100644 --- a/apps/e2e/tests/backend/backend-helpers.ts +++ b/apps/e2e/tests/backend/backend-helpers.ts @@ -1355,7 +1355,12 @@ export namespace User { accessType: "server", body: body ?? {}, }); - expect(createUserResponse.status).toBe(201); + expect(createUserResponse).toMatchObject({ + status: 201, + body: { + id: expect.any(String), + }, + }); return { userId: createUserResponse.body.id, }; diff --git a/apps/e2e/tests/backend/endpoints/api/v1/data-vault.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/data-vault.test.ts index efc37cf929..5acd1a7d60 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/data-vault.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/data-vault.test.ts @@ -1,5 +1,5 @@ import { it } from "../../../../helpers"; -import { Auth, Project, niceBackendFetch } from "../../../backend-helpers"; +import { Project, niceBackendFetch } from "../../../backend-helpers"; async function createDataVaultEnabledProject() { await Project.createAndSwitch({ @@ -39,7 +39,6 @@ function hashKey(key: string): string { it("can store and retrieve values from data vault", async ({ expect }: { expect: any }) => { await createDataVaultEnabledProject(); - await Auth.Otp.signIn(); const storeId = "test-store-1"; const key = "my-secret-key"; @@ -78,7 +77,6 @@ it("can store and retrieve values from data vault", async ({ expect }: { expect: it("can update existing values in data vault", async ({ expect }: { expect: any }) => { await createDataVaultEnabledProject(); - await Auth.Otp.signIn(); const storeId = "test-store-update"; const key = "update-key"; @@ -133,7 +131,6 @@ it("can update existing values in data vault", async ({ expect }: { expect: any it("returns 400 when trying to get non-existent value", async ({ expect }: { expect: any }) => { await createDataVaultEnabledProject(); - await Auth.Otp.signIn(); const storeId = "test-store-404"; const hashedKey = hashKey("non-existent-key"); @@ -167,7 +164,6 @@ it("returns 400 when trying to get non-existent value", async ({ expect }: { exp it("validates required fields", async ({ expect }: { expect: any }) => { await createDataVaultEnabledProject(); - await Auth.Otp.signIn(); // Test empty store ID (by using spaces which get trimmed) const emptyStoreResponse = await niceBackendFetch(`/api/latest/data-vault/stores/${encodeURIComponent(" ")}/set`, { @@ -280,7 +276,6 @@ it("validates required fields", async ({ expect }: { expect: any }) => { it("isolates data between different stores", async ({ expect }: { expect: any }) => { await createDataVaultEnabledProject(); - await Auth.Otp.signIn(); const key = "shared-key"; const hashedKey = hashKey(key); @@ -336,7 +331,6 @@ it("isolates data between different stores", async ({ expect }: { expect: any }) it("handles multiple keys in the same store", async ({ expect }: { expect: any }) => { await createDataVaultEnabledProject(); - await Auth.Otp.signIn(); const storeId = "multi-key-store"; const key1 = "key-1"; diff --git a/apps/e2e/tests/backend/endpoints/api/v1/integrations/custom/domain.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/integrations/custom/domain.test.ts index 2d452d70f3..bf3767c984 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/integrations/custom/domain.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/integrations/custom/domain.test.ts @@ -2,7 +2,7 @@ import { it } from "../../../../../../helpers"; import { Auth, Project, niceBackendFetch } from "../../../../../backend-helpers"; it("list domains", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); const response = await niceBackendFetch("/api/v1/integrations/custom/domains", { accessType: "admin", @@ -23,7 +23,7 @@ it("list domains", async ({ expect }) => { }); it("creates domains for internal project", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); const response = await niceBackendFetch("/api/v1/integrations/custom/domains", { accessType: "admin", @@ -45,7 +45,7 @@ it("creates domains for internal project", async ({ expect }) => { }); it("adds two different domains", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); // Add first domain @@ -96,7 +96,7 @@ it("adds two different domains", async ({ expect }) => { }); it("does not add if the domain has a path, query parameters, or hash", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); const response = await niceBackendFetch("/api/v1/integrations/custom/domains", { accessType: "admin", @@ -127,7 +127,7 @@ it("does not add if the domain has a path, query parameters, or hash", async ({ }); it("adds two domains and deletes one", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); // Add first domain diff --git a/apps/e2e/tests/backend/endpoints/api/v1/integrations/custom/oauth.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/integrations/custom/oauth.test.ts index da8054d314..4e0fe0cd0f 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/integrations/custom/oauth.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/integrations/custom/oauth.test.ts @@ -151,14 +151,14 @@ async function authorize(projectId: string) { } it(`should redirect to the correct callback URL`, async ({}) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const createdProject = await Project.create(); await authorize(createdProject.projectId); }); it(`should not redirect to the incorrect callback URL`, async ({}) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); await Project.create(); const result = await authorizePart1("http://localhost:30000/api/v2/wrong-url/authorize"); @@ -190,7 +190,7 @@ it(`should not redirect to the incorrect callback URL`, async ({}) => { }); it(`should exchange the authorization code for an admin API key that works`, async ({}) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const createdProject = await Project.create(); const { authorizationCode } = await authorize(createdProject.projectId); @@ -244,7 +244,7 @@ it(`should exchange the authorization code for an admin API key that works`, asy }); it(`should not exchange the authorization code when the client secret is incorrect`, async ({}) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const createdProject = await Project.create(); const { authorizationCode } = await authorize(createdProject.projectId); diff --git a/apps/e2e/tests/backend/endpoints/api/v1/integrations/custom/projects/transfer.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/integrations/custom/projects/transfer.test.ts index 078e14d448..523c18cefc 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/integrations/custom/projects/transfer.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/integrations/custom/projects/transfer.test.ts @@ -30,7 +30,7 @@ async function provisionAndTransferProject() { const provisioned = await provisionProject(); const projectId = provisioned.body.project_id; const { code } = await initiateTransfer(projectId); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch(`/api/v1/integrations/custom/projects/transfer/confirm`, { method: "POST", accessType: "client", @@ -68,7 +68,7 @@ it("should return that the project is transferrable if it is provisioned", async }); it("should return that the project is not transferrable if it is not provisioned", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const createdProject = await Project.create(); const response = await niceBackendFetch(urlString`/api/v1/integrations/custom/projects/transfer?project_id=${createdProject.createProjectResponse.body.id}`, { method: "GET", @@ -128,7 +128,7 @@ it("should initiate multiple transfers for the same project, but only one can be const projectId = provisioned.body.project_id; const { code: code1 } = await initiateTransfer(projectId); const { code: code2 } = await initiateTransfer(projectId); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response1 = await niceBackendFetch(`/api/v1/integrations/custom/projects/transfer/confirm`, { method: "POST", accessType: "client", @@ -163,7 +163,7 @@ it("should initiate multiple transfers for the same project, but only one can be }); it("should fail if the project to transfer was not provisioned by Neon", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const createdProject = await Project.create(); const response = await niceBackendFetch("/api/v1/integrations/custom/projects/transfer/initiate", { method: "POST", @@ -248,7 +248,7 @@ it("should check if the project exists before initiating transfer", async ({ exp const provisioned = await provisionProject(); const projectId = provisioned.body.project_id; const { code } = await initiateTransfer(projectId); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch(`/api/v1/integrations/custom/projects/transfer/confirm/check`, { method: "POST", accessType: "client", @@ -269,7 +269,7 @@ it("should fail the check if project was already transferred", async ({ expect } const provisioned = await provisionProject(); const projectId = provisioned.body.project_id; const { code } = await initiateTransfer(projectId); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch(`/api/v1/integrations/custom/projects/transfer/confirm`, { method: "POST", accessType: "client", diff --git a/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/api-keys.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/api-keys.test.ts index a6b130c119..236281eb71 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/api-keys.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/api-keys.test.ts @@ -56,13 +56,13 @@ describe("with server access", () => { describe("with admin access to the internal project", () => { it("list api keys", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch("/api/v1/integrations/neon/api-keys", { accessType: "admin" }); expect(response.status).toBe(200); // not doing snapshot as it contains all the test api keys }); it("creates api keys for internal project", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response1 = await niceBackendFetch("/api/v1/integrations/neon/api-keys", { accessType: "admin", method: "POST", diff --git a/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/domain.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/domain.test.ts index df4d3ba257..90bbd213e6 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/domain.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/domain.test.ts @@ -2,7 +2,7 @@ import { it } from "../../../../../../helpers"; import { Auth, Project, niceBackendFetch } from "../../../../../backend-helpers"; it("list domains", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); const response = await niceBackendFetch("/api/v1/integrations/neon/domains", { accessType: "admin", @@ -23,7 +23,7 @@ it("list domains", async ({ expect }) => { }); it("creates domains for internal project", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); const response = await niceBackendFetch("/api/v1/integrations/neon/domains", { accessType: "admin", @@ -45,7 +45,7 @@ it("creates domains for internal project", async ({ expect }) => { }); it("adds two different domains", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); // Add first domain @@ -96,7 +96,7 @@ it("adds two different domains", async ({ expect }) => { }); it("does not add if the domain has a path, query parameters, or hash", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); const response = await niceBackendFetch("/api/v1/integrations/neon/domains", { accessType: "admin", @@ -127,7 +127,7 @@ it("does not add if the domain has a path, query parameters, or hash", async ({ }); it("adds two domains and deletes one", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); // Add first domain @@ -192,7 +192,7 @@ it("adds two domains and deletes one", async ({ expect }) => { }); it("fails when not specifying a domain", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); const response = await niceBackendFetch("/api/v1/integrations/neon/domains", { diff --git a/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/oauth-providers.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/oauth-providers.test.ts index 193e69475a..5291c903db 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/oauth-providers.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/oauth-providers.test.ts @@ -3,7 +3,7 @@ import { Auth, Project, niceBackendFetch } from "../../../../../backend-helpers" it("creates a new oauth provider", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); const response = await niceBackendFetch(`/api/v1/integrations/neon/oauth-providers`, { @@ -31,7 +31,7 @@ it("creates a new oauth provider", async ({ expect }) => { }); it("lists oauth providers", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); const response1 = await niceBackendFetch(`/api/v1/integrations/neon/oauth-providers`, { @@ -140,7 +140,7 @@ it("lists oauth providers", async ({ expect }) => { }); it("creates standard oauth providers", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); const response1 = await niceBackendFetch(`/api/v1/integrations/neon/oauth-providers`, { @@ -198,7 +198,7 @@ it("creates standard oauth providers", async ({ expect }) => { }); it("updates shared to standard oauth provider", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); const response1 = await niceBackendFetch(`/api/v1/integrations/neon/oauth-providers`, { @@ -252,7 +252,7 @@ it("updates shared to standard oauth provider", async ({ expect }) => { }); it("deletes an oauth provider", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); // Create a provider first diff --git a/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/oauth.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/oauth.test.ts index 65b1bcda5b..2eeffa596c 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/oauth.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/oauth.test.ts @@ -151,14 +151,14 @@ async function authorize(projectId: string) { } it(`should redirect to the correct callback URL`, async ({}) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const createdProject = await Project.create(); await authorize(createdProject.projectId); }); it(`should not redirect to the incorrect callback URL`, async ({}) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); await Project.create(); const result = await authorizePart1("http://localhost:30000/api/v2/wrong-url/authorize"); @@ -190,7 +190,7 @@ it(`should not redirect to the incorrect callback URL`, async ({}) => { }); it(`should exchange the authorization code for an admin API key that works`, async ({}) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const createdProject = await Project.create(); const { authorizationCode } = await authorize(createdProject.projectId); @@ -244,7 +244,7 @@ it(`should exchange the authorization code for an admin API key that works`, asy }); it(`should not exchange the authorization code when the client secret is incorrect`, async ({}) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const createdProject = await Project.create(); const { authorizationCode } = await authorize(createdProject.projectId); diff --git a/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/current.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/current.test.ts index b5ce9c57a7..52f3e68445 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/current.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/current.test.ts @@ -3,7 +3,7 @@ import { Auth, Project, niceBackendFetch } from "../../../../../../backend-helpe it("get project details", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); const response = await niceBackendFetch("/api/v1/integrations/neon/projects/current", { accessType: "admin", @@ -53,7 +53,7 @@ it("get project details", async ({ expect }) => { }); it("creates and updates the basic project information of a project", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); const response = await niceBackendFetch("/api/v1/integrations/neon/projects/current", { accessType: "admin", @@ -110,7 +110,7 @@ it("creates and updates the basic project information of a project", async ({ ex }); it("creates and updates the email config of a project", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); const response = await niceBackendFetch("/api/v1/integrations/neon/projects/current", { accessType: "admin", diff --git a/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/transfer.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/transfer.test.ts index a4a645512d..a4899fce6e 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/transfer.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/transfer.test.ts @@ -30,7 +30,7 @@ async function provisionAndTransferProject() { const provisioned = await provisionProject(); const projectId = provisioned.body.project_id; const { code } = await initiateTransfer(projectId); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch(`/api/v1/integrations/neon/projects/transfer/confirm`, { method: "POST", accessType: "client", @@ -68,7 +68,7 @@ it("should return that the project is transferrable if it is provisioned by Neon }); it("should return that the project is not transferrable if it is not provisioned by Neon", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const createdProject = await Project.create(); const response = await niceBackendFetch(urlString`/api/v1/integrations/neon/projects/transfer?project_id=${createdProject.createProjectResponse.body.id}`, { method: "GET", @@ -128,7 +128,7 @@ it("should initiate multiple transfers for the same project, but only one can be const projectId = provisioned.body.project_id; const { code: code1 } = await initiateTransfer(projectId); const { code: code2 } = await initiateTransfer(projectId); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response1 = await niceBackendFetch(`/api/v1/integrations/neon/projects/transfer/confirm`, { method: "POST", accessType: "client", @@ -163,7 +163,7 @@ it("should initiate multiple transfers for the same project, but only one can be }); it("should fail if the project to transfer was not provisioned by Neon", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const createdProject = await Project.create(); const response = await niceBackendFetch("/api/v1/integrations/neon/projects/transfer/initiate", { method: "POST", @@ -248,7 +248,7 @@ it("should check if the project exists before initiating transfer", async ({ exp const provisioned = await provisionProject(); const projectId = provisioned.body.project_id; const { code } = await initiateTransfer(projectId); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch(`/api/v1/integrations/neon/projects/transfer/confirm/check`, { method: "POST", accessType: "client", @@ -269,7 +269,7 @@ it("should fail the check if project was already transferred", async ({ expect } const provisioned = await provisionProject(); const projectId = provisioned.body.project_id; const { code } = await initiateTransfer(projectId); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch(`/api/v1/integrations/neon/projects/transfer/confirm`, { method: "POST", accessType: "client", diff --git a/apps/e2e/tests/backend/endpoints/api/v1/internal/api-keys.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/internal/api-keys.test.ts index 61643a3944..42a68ed973 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/internal/api-keys.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/internal/api-keys.test.ts @@ -56,13 +56,13 @@ describe("with server access", () => { describe("with admin access to the internal project", () => { it("list api keys", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch("/api/v1/internal/api-keys", { accessType: "admin" }); expect(response.status).toBe(200); // not doing snapshot as it contains all the test api keys }); it("creates api keys for internal project", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response1 = await niceBackendFetch("/api/v1/internal/api-keys", { accessType: "admin", method: "POST", diff --git a/apps/e2e/tests/backend/endpoints/api/v1/internal/email-templates.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/internal/email-templates.test.ts index 89df16157d..dbb9eaee6f 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/internal/email-templates.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/internal/email-templates.test.ts @@ -4,7 +4,7 @@ import { Auth, Project, niceBackendFetch } from "../../../../backend-helpers"; it("should not allow updating email templates when using shared email config", async ({ expect }) => { // Create a project with shared email config (default) - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); // Try to update an email template @@ -47,7 +47,7 @@ it("should not allow updating email templates when using shared email config", a it("should allow adding and updating email templates with custom email config", async ({ expect }) => { // Create a project with custom email config - await Auth.Otp.signIn(); + await Auth.fastSignUp(); await Project.createAndSwitch({ config: { email_config: { diff --git a/apps/e2e/tests/backend/endpoints/api/v1/internal/payments/setup.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/internal/payments/setup.test.ts index a69d455015..6394ed3ef6 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/internal/payments/setup.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/internal/payments/setup.test.ts @@ -91,7 +91,7 @@ describe("POST /api/v1/internal/payments/setup", () => { describe("with admin access", () => { it("should return a setup URL when creating new stripe account", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); await Project.createAndSwitch(); const response = await niceBackendFetch("/api/v1/internal/payments/setup", { method: "POST", @@ -109,7 +109,7 @@ describe("POST /api/v1/internal/payments/setup", () => { }); it("should reuse existing stripe account when already configured", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); await Project.createAndSwitch(); // First call to setup diff --git a/apps/e2e/tests/backend/endpoints/api/v1/internal/projects.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/internal/projects.test.ts index cb01c36444..40bac3524c 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/internal/projects.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/internal/projects.test.ts @@ -1,5 +1,5 @@ import { it } from "../../../../../helpers"; -import { Auth, InternalProjectClientKeys, Project, backendContext, niceBackendFetch } from "../../../../backend-helpers"; +import { Auth, InternalProjectClientKeys, InternalProjectKeys, Project, backendContext, niceBackendFetch } from "../../../../backend-helpers"; it("should not have have access to the project", async ({ expect }) => { @@ -44,7 +44,7 @@ it("is not allowed to list all current projects without signing in", async ({ ex }); it("lists all current projects (empty list)", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch("/api/v1/internal/projects", { accessType: "client" }); expect(response).toMatchInlineSnapshot(` NiceResponse { @@ -59,8 +59,9 @@ it("lists all current projects (empty list)", async ({ expect }) => { }); it("creates a new project", async ({ expect }) => { + backendContext.set({ projectKeys: InternalProjectKeys }); + await Auth.fastSignUp(); backendContext.set({ projectKeys: InternalProjectClientKeys }); - await Auth.Otp.signIn(); const result = await Project.createAndGetAdminToken({ display_name: "Test Project", }); @@ -106,8 +107,9 @@ it("creates a new project", async ({ expect }) => { }); it("creates a new project with different configurations", async ({ expect }) => { + backendContext.set({ projectKeys: InternalProjectKeys }); + await Auth.fastSignUp(); backendContext.set({ projectKeys: InternalProjectClientKeys }); - await Auth.Otp.signIn(); const { createProjectResponse: response1 } = await Project.create({ display_name: "Test Project", description: "Test description", @@ -396,7 +398,7 @@ it("creates a new project with different configurations", async ({ expect }) => }); it("lists the current projects after creating a new project", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); await Project.create(); const response = await niceBackendFetch("/api/v1/internal/projects", { accessType: "client" }); expect(response).toMatchInlineSnapshot(` diff --git a/apps/e2e/tests/backend/endpoints/api/v1/internal/projects/transfer.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/internal/projects/transfer.test.ts index 414dacf558..9d9c419a71 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/internal/projects/transfer.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/internal/projects/transfer.test.ts @@ -8,7 +8,7 @@ describe("internal project transfer", () => { backendContext.set({ projectKeys: InternalProjectKeys }); // Create and sign in user in internal project - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); // Create two teams where user is admin const team1Response = await niceBackendFetch("/api/v1/teams", { @@ -109,16 +109,13 @@ describe("internal project transfer", () => { backendContext.set({ projectKeys: InternalProjectKeys }); // Create admin user - const adminMailbox = await bumpEmailAddress(); - const { userId: adminUserId } = await Auth.Otp.signIn(); + const { userId: adminUserId, accessToken: adminAccessToken, refreshToken: adminRefreshToken } = await Auth.fastSignUp(); // Create member user - const memberMailbox = await bumpEmailAddress(); - const { userId: memberUserId } = await Auth.Otp.signIn(); + const { userId: memberUserId, accessToken: memberAccessToken, refreshToken: memberRefreshToken } = await Auth.fastSignUp(); // Switch back to admin user - backendContext.set({ mailbox: adminMailbox }); - await Auth.Otp.signIn(); + backendContext.set({ userAuth: { accessToken: adminAccessToken, refreshToken: adminRefreshToken } }); const team1Response = await niceBackendFetch("/api/v1/teams", { method: "POST", @@ -183,8 +180,7 @@ describe("internal project transfer", () => { const project = projectResponse.body; // Switch to member user - backendContext.set({ mailbox: memberMailbox }); - await Auth.Otp.signIn(); + backendContext.set({ userAuth: { accessToken: memberAccessToken, refreshToken: memberRefreshToken } }); const transferResponse = await niceBackendFetch("/api/v1/internal/projects/transfer", { method: "POST", @@ -220,7 +216,7 @@ describe("internal project transfer", () => { backendContext.set({ projectKeys: InternalProjectKeys }); // Create user and sign in - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); // Create two teams const team1Response = await niceBackendFetch("/api/v1/teams", { @@ -304,8 +300,7 @@ describe("internal project transfer", () => { backendContext.set({ projectKeys: InternalProjectKeys }); // Create first user and sign in - const user1Mailbox = await bumpEmailAddress(); - const { userId: user1Id } = await Auth.Otp.signIn(); + const { userId: user1Id, accessToken: user1AccessToken, refreshToken: user1RefreshToken } = await Auth.fastSignUp(); // Create team1 with user1 const team1Response = await niceBackendFetch("/api/v1/teams", { @@ -318,8 +313,7 @@ describe("internal project transfer", () => { const team1 = team1Response.body; // Create second user - const user2Mailbox = await bumpEmailAddress(); - const { userId: user2Id } = await Auth.Otp.signIn(); + const { userId: user2Id } = await Auth.fastSignUp(); // Create team2 with user2 const team2Response = await niceBackendFetch("/api/v1/teams", { @@ -331,9 +325,8 @@ describe("internal project transfer", () => { }); const team2 = team2Response.body; - // Sign back in as user1 (call signIn again) - backendContext.set({ mailbox: user1Mailbox }); - await Auth.Otp.signIn(); + // Switch back to user1 + backendContext.set({ userAuth: { accessToken: user1AccessToken, refreshToken: user1RefreshToken } }); // Add user1 to team1 first await niceBackendFetch(`/api/v1/team-memberships/${team1.id}/${user1Id}`, { diff --git a/apps/e2e/tests/backend/endpoints/api/v1/notification-preferences.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/notification-preferences.test.ts index 6766a86be6..f78a3c9463 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/notification-preferences.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/notification-preferences.test.ts @@ -31,7 +31,7 @@ describe("invalid requests", () => { }); it("should return 404 when invalid notification category id is provided", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch( `/api/v1/emails/notification-preference/me/${randomUUID()}`, { @@ -53,7 +53,7 @@ describe("invalid requests", () => { }); it("lists default notification preferences", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch( "/api/v1/emails/notification-preference/me", { @@ -87,7 +87,7 @@ it("lists default notification preferences", async ({ expect }) => { }); it("updates notification preferences", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch( "/api/v1/emails/notification-preference/me/4f6f8873-3d04-46bd-8bef-18338b1a1b4c", { diff --git a/apps/e2e/tests/backend/endpoints/api/v1/oauth-providers.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/oauth-providers.test.ts index fc98d2d61e..9b3010a107 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/oauth-providers.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/oauth-providers.test.ts @@ -19,7 +19,7 @@ async function createAndSwitchToOAuthEnabledProject() { it("should create an OAuth provider connection", async ({ expect }: { expect: any }) => { const { createProjectResponse } = await createAndSwitchToOAuthEnabledProject(); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const providerConfig = createProjectResponse.body.config.oauth_providers.find((p: any) => p.provider_config_id === "spotify"); expect(providerConfig).toBeDefined(); @@ -57,7 +57,7 @@ it("should create an OAuth provider connection", async ({ expect }: { expect: an it("should read an OAuth provider connection", async ({ expect }: { expect: any }) => { const { createProjectResponse } = await createAndSwitchToOAuthEnabledProject(); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const providerConfig = createProjectResponse.body.config.oauth_providers.find((p: any) => p.provider_config_id === "spotify"); expect(providerConfig).toBeDefined(); @@ -101,7 +101,7 @@ it("should read an OAuth provider connection", async ({ expect }: { expect: any it("should list all OAuth provider connections for a user", async ({ expect }: { expect: any }) => { const { createProjectResponse } = await createAndSwitchToOAuthEnabledProject(); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const providerConfig = createProjectResponse.body.config.oauth_providers.find((p: any) => p.provider_config_id === "spotify"); expect(providerConfig).toBeDefined(); @@ -151,7 +151,7 @@ it("should list all OAuth provider connections for a user", async ({ expect }: { it("should update an OAuth provider connection on the client", async ({ expect }: { expect: any }) => { const { createProjectResponse } = await createAndSwitchToOAuthEnabledProject(); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const providerConfig = createProjectResponse.body.config.oauth_providers.find((p: any) => p.provider_config_id === "spotify"); expect(providerConfig).toBeDefined(); @@ -196,7 +196,7 @@ it("should update an OAuth provider connection on the client", async ({ expect } it("should update an OAuth provider connection on the server", async ({ expect }: { expect: any }) => { const { createProjectResponse } = await createAndSwitchToOAuthEnabledProject(); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const providerConfig = createProjectResponse.body.config.oauth_providers.find((p: any) => p.provider_config_id === "spotify"); expect(providerConfig).toBeDefined(); @@ -244,7 +244,7 @@ it("should update an OAuth provider connection on the server", async ({ expect } it("should delete an OAuth provider connection", async ({ expect }: { expect: any }) => { const { createProjectResponse } = await createAndSwitchToOAuthEnabledProject(); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const providerConfig = createProjectResponse.body.config.oauth_providers.find((p: any) => p.provider_config_id === "spotify"); expect(providerConfig).toBeDefined(); @@ -295,7 +295,7 @@ it("should delete an OAuth provider connection", async ({ expect }: { expect: an it("should return 404 when reading non-existent OAuth provider", async ({ expect }: { expect: any }) => { await createAndSwitchToOAuthEnabledProject(); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const readResponse = await niceBackendFetch("/api/v1/oauth-providers/me/e889e6de-8da5-47fd-87fd-a8db34b14ec4", { method: "GET", @@ -313,7 +313,7 @@ it("should return 404 when reading non-existent OAuth provider", async ({ expect it("should return 404 when updating non-existent OAuth provider", async ({ expect }: { expect: any }) => { await createAndSwitchToOAuthEnabledProject(); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const updateResponse = await niceBackendFetch("/api/v1/oauth-providers/me/e889e6de-8da5-47fd-87fd-a8db34b14ec4", { method: "PATCH", @@ -334,7 +334,7 @@ it("should return 404 when updating non-existent OAuth provider", async ({ expec it("should return 404 when deleting non-existent OAuth provider", async ({ expect }: { expect: any }) => { await createAndSwitchToOAuthEnabledProject(); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const deleteResponse = await niceBackendFetch("/api/v1/oauth-providers/me/e889e6de-8da5-47fd-87fd-a8db34b14ec4", { method: "DELETE", @@ -352,7 +352,7 @@ it("should return 404 when deleting non-existent OAuth provider", async ({ expec it("should forbid client access to other users' OAuth providers", async ({ expect }: { expect: any }) => { const { createProjectResponse } = await createAndSwitchToOAuthEnabledProject(); - const user1 = await Auth.Otp.signIn(); + const user1 = await Auth.fastSignUp(); const providerConfig = createProjectResponse.body.config.oauth_providers.find((p: any) => p.provider_config_id === "spotify"); expect(providerConfig).toBeDefined(); @@ -371,7 +371,7 @@ it("should forbid client access to other users' OAuth providers", async ({ expec }); backendContext.set({ mailbox: createMailbox() }); - const user2 = await Auth.Otp.signIn(); + const user2 = await Auth.fastSignUp(); const createResponse2 = await niceBackendFetch("/api/v1/oauth-providers", { method: "POST", @@ -472,7 +472,7 @@ it("should forbid client access to other users' OAuth providers", async ({ expec it("should allow server access to any user's OAuth providers", async ({ expect }: { expect: any }) => { const { createProjectResponse } = await createAndSwitchToOAuthEnabledProject(); - const user1 = await Auth.Otp.signIn(); + const user1 = await Auth.fastSignUp(); const providerConfig = createProjectResponse.body.config.oauth_providers.find((p: any) => p.provider_config_id === "spotify"); expect(providerConfig).toBeDefined(); @@ -493,7 +493,7 @@ it("should allow server access to any user's OAuth providers", async ({ expect } expect(createResponse1.status).toBe(201); backendContext.set({ mailbox: createMailbox() }); - const user2 = await Auth.Otp.signIn(); + const user2 = await Auth.fastSignUp(); const createResponse2 = await niceBackendFetch("/api/v1/oauth-providers", { method: "POST", @@ -619,7 +619,7 @@ it("should allow server access to any user's OAuth providers", async ({ expect } it("should handle account_id updates correctly", async ({ expect }: { expect: any }) => { const { createProjectResponse } = await createAndSwitchToOAuthEnabledProject(); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const providerConfig = createProjectResponse.body.config.oauth_providers.find((p: any) => p.provider_config_id === "spotify"); expect(providerConfig).toBeDefined(); @@ -691,7 +691,7 @@ it("should handle account_id updates correctly", async ({ expect }: { expect: an it("should return empty list when user has no OAuth providers", async ({ expect }: { expect: any }) => { await createAndSwitchToOAuthEnabledProject(); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); // List providers for a user who has none const listResponse = await niceBackendFetch("/api/v1/oauth-providers?user_id=me", { @@ -719,7 +719,7 @@ it("should handle provider not configured error", async ({ expect }: { expect: a oauth_providers: [] // No OAuth providers configured } }); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); // Try to create an OAuth provider connection with an unconfigured provider const createResponse = await niceBackendFetch("/api/v1/oauth-providers", { @@ -746,7 +746,7 @@ it("should handle provider not configured error", async ({ expect }: { expect: a it("should toggle sign-in and connected accounts capabilities", async ({ expect }: { expect: any }) => { const { createProjectResponse } = await createAndSwitchToOAuthEnabledProject(); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const providerConfig = createProjectResponse.body.config.oauth_providers.find((p: any) => p.provider_config_id === "spotify"); expect(providerConfig).toBeDefined(); @@ -812,7 +812,7 @@ it("should toggle sign-in and connected accounts capabilities", async ({ expect it("should prevent multiple providers of the same type from being enabled for signing in", async ({ expect }: { expect: any }) => { // Test with multiple spotify accounts (same provider type, different account IDs) const { createProjectResponse } = await createAndSwitchToOAuthEnabledProject(); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const providerConfig = createProjectResponse.body.config.oauth_providers.find((p: any) => p.provider_config_id === "spotify"); expect(providerConfig).toBeDefined(); @@ -932,7 +932,7 @@ it("should not allow get, update, delete oauth providers with wrong user id and const { createProjectResponse } = await createAndSwitchToOAuthEnabledProject(); // Create user1 and their OAuth provider - const user1 = await Auth.Otp.signIn(); + const user1 = await Auth.fastSignUp(); const providerConfig = createProjectResponse.body.config.oauth_providers.find((p: any) => p.provider_config_id === "spotify"); expect(providerConfig).toBeDefined(); @@ -953,7 +953,7 @@ it("should not allow get, update, delete oauth providers with wrong user id and const provider1Id = createResponse1.body.id; backendContext.set({ mailbox: createMailbox() }); - const user2 = await Auth.Otp.signIn(); + const user2 = await Auth.fastSignUp(); const createResponse2 = await niceBackendFetch("/api/v1/oauth-providers", { method: "POST", diff --git a/apps/e2e/tests/backend/endpoints/api/v1/payments/before-offer-to-product-rename/outdated--create-purchase-url.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/payments/before-offer-to-product-rename/outdated--create-purchase-url.test.ts index 2511d9d5f8..b6af3669b1 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/payments/before-offer-to-product-rename/outdated--create-purchase-url.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/payments/before-offer-to-product-rename/outdated--create-purchase-url.test.ts @@ -97,7 +97,7 @@ it("should error for invalid customer_id", async ({ expect }) => { }, }); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch("/api/v1/payments/purchases/create-purchase-url", { method: "POST", accessType: "client", @@ -174,7 +174,7 @@ it("should not allow offer_inline when calling from client", async ({ expect }) await Project.createAndSwitch({ config: { magic_link_enabled: true } }); await Payments.setup(); - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); const response = await niceBackendFetch("/api/v1/payments/purchases/create-purchase-url", { method: "POST", accessType: "client", @@ -254,7 +254,7 @@ it("should allow offer_inline when calling from server", async ({ expect }) => { await Project.createAndSwitch({ config: { magic_link_enabled: true } }); await Payments.setup(); - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); const response = await niceBackendFetch("/api/v1/payments/purchases/create-purchase-url", { method: "POST", accessType: "server", diff --git a/apps/e2e/tests/backend/endpoints/api/v1/payments/before-offer-to-product-rename/outdated--purchase-session.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/payments/before-offer-to-product-rename/outdated--purchase-session.test.ts index e40371fcf6..09e8410b24 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/payments/before-offer-to-product-rename/outdated--purchase-session.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/payments/before-offer-to-product-rename/outdated--purchase-session.test.ts @@ -304,7 +304,7 @@ it("should create purchase URL with inline offer, validate code, and create purc await Project.createAndSwitch({ config: { magic_link_enabled: true } }); await Payments.setup(); - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); const response = await niceBackendFetch("/api/v1/payments/purchases/create-purchase-url", { method: "POST", accessType: "server", diff --git a/apps/e2e/tests/backend/endpoints/api/v1/payments/create-purchase-url.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/payments/create-purchase-url.test.ts index dac73d3488..2253444213 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/payments/create-purchase-url.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/payments/create-purchase-url.test.ts @@ -97,7 +97,7 @@ it("should error for invalid customer_id", async ({ expect }) => { }, }); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch("/api/latest/payments/purchases/create-purchase-url", { method: "POST", accessType: "client", @@ -174,7 +174,7 @@ it("should not allow product_inline when calling from client", async ({ expect } await Project.createAndSwitch({ config: { magic_link_enabled: true } }); await Payments.setup(); - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); const response = await niceBackendFetch("/api/latest/payments/purchases/create-purchase-url", { method: "POST", accessType: "client", @@ -254,7 +254,7 @@ it("should allow product_inline when calling from server", async ({ expect }) => await Project.createAndSwitch({ config: { magic_link_enabled: true } }); await Payments.setup(); - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); const response = await niceBackendFetch("/api/latest/payments/purchases/create-purchase-url", { method: "POST", accessType: "server", @@ -283,7 +283,7 @@ it("should return inline product metadata when validating purchase code", async await Project.createAndSwitch({ config: { magic_link_enabled: true } }); await Payments.setup(); - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); const createResponse = await niceBackendFetch("/api/latest/payments/purchases/create-purchase-url", { method: "POST", accessType: "server", diff --git a/apps/e2e/tests/backend/endpoints/api/v1/payments/items.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/payments/items.test.ts index 2ee69f858c..c2c2d46278 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/payments/items.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/payments/items.test.ts @@ -337,7 +337,7 @@ it("should error when deducting more quantity than available", async ({ expect } it("allows team admins to be added when item quantity is increased", async ({ expect }) => { backendContext.set({ projectKeys: InternalProjectKeys }); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { createProjectResponse } = await Project.create(); const ownerTeamId: string = createProjectResponse.body.owner_team_id; @@ -371,7 +371,7 @@ it("allows team admins to be added when item quantity is increased", async ({ ex `); backendContext.set({ mailbox: mailboxB }); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const invitationMessages = await mailboxB.waitForMessagesWithSubject("join"); const acceptResponse = await niceBackendFetch("/api/v1/team-invitations/accept", { diff --git a/apps/e2e/tests/backend/endpoints/api/v1/payments/products.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/payments/products.test.ts index 659c17d96a..aa30985bbc 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/payments/products.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/payments/products.test.ts @@ -364,7 +364,7 @@ it("should allow granting stackable product with custom quantity", async ({ expe it("should grant inline product without needing configuration", async ({ expect }) => { await Project.createAndSwitch({ config: { magic_link_enabled: true } }); await Payments.setup(); - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); const grantResponse = await niceBackendFetch(`/api/v1/payments/products/user/${userId}`, { method: "POST", diff --git a/apps/e2e/tests/backend/endpoints/api/v1/payments/purchase-session.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/payments/purchase-session.test.ts index c02ce9c381..fabb21d0fc 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/payments/purchase-session.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/payments/purchase-session.test.ts @@ -304,7 +304,7 @@ it("should create purchase URL with inline product, validate code, and create pu await Project.createAndSwitch({ config: { magic_link_enabled: true } }); await Payments.setup(); - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); const response = await niceBackendFetch("/api/latest/payments/purchases/create-purchase-url", { method: "POST", accessType: "server", @@ -446,7 +446,7 @@ it("should list inline product metadata after completing test-mode purchase", as }, }); - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); const createPurchaseResponse = await niceBackendFetch("/api/latest/payments/purchases/create-purchase-url", { method: "POST", accessType: "server", diff --git a/apps/e2e/tests/backend/endpoints/api/v1/project-permissions.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/project-permissions.test.ts index 08210585ba..bc34516e6a 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/project-permissions.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/project-permissions.test.ts @@ -2,7 +2,7 @@ import { it } from "../../../../helpers"; import { Auth, InternalApiKey, InternalProjectKeys, Project, Webhook, backendContext, niceBackendFetch } from "../../../backend-helpers"; it("is not allowed to list permissions from the other users on the client", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch(`/api/v1/project-permissions`, { accessType: "client", @@ -18,7 +18,7 @@ it("is not allowed to list permissions from the other users on the client", asyn }); it("is not allowed to grant non-existing permission to a user on the server", async ({ expect }) => { - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); const response = await niceBackendFetch(`/api/v1/project-permissions/${userId}/does_not_exist`, { accessType: "server", @@ -42,8 +42,8 @@ it("is not allowed to grant non-existing permission to a user on the server", as }); it("does not grant a team permission to a user", async ({ expect }) => { - await Project.createAndSwitch({ config: { magic_link_enabled: true } }); - const { userId } = await Auth.Otp.signIn(); + await Project.createAndSwitch(); + const { userId } = await Auth.fastSignUp(); const teamPermissionDefinitionResponse = await niceBackendFetch(`/api/v1/team-permission-definitions`, { accessType: "admin", @@ -177,7 +177,7 @@ it("can create a new permission and grant it to a user on the server", async ({ }); it("can customize default user permissions", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); const response1 = await niceBackendFetch(`/api/v1/project-permission-definitions`, { @@ -276,7 +276,7 @@ it("can customize default user permissions", async ({ expect }) => { it("should trigger project permission webhook when a permission is granted to a user", async ({ expect }) => { const { projectId, svixToken, endpointId } = await Webhook.createProjectWithEndpoint(); - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); await niceBackendFetch(`/api/v1/project-permission-definitions`, { accessType: "admin", @@ -315,7 +315,7 @@ it("should trigger project permission webhook when a permission is granted to a it("should trigger project permission webhook when a permission is revoked from a user", async ({ expect }) => { const { projectId, svixToken, endpointId } = await Webhook.createProjectWithEndpoint(); - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); await niceBackendFetch(`/api/v1/project-permission-definitions`, { accessType: "admin", @@ -361,7 +361,7 @@ it("should trigger project permission webhook when a permission is revoked from }); it("should not be able to create a permission with the same name as an existing project permission", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); // First, create a project permission definition diff --git a/apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts index 7af9e3d751..3ee7397b8e 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts @@ -63,7 +63,7 @@ it("gets current project (internal)", async ({ expect }) => { }); it("creates and updates the basic project information of a project", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); const { updateProjectResponse: response } = await Project.updateCurrent(adminAccessToken, { display_name: "Updated Project", @@ -112,7 +112,7 @@ it("creates and updates the basic project information of a project", async ({ ex }); it("updates the basic project configuration", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); const { updateProjectResponse: response } = await Project.updateCurrent(adminAccessToken, { config: { @@ -164,7 +164,7 @@ it("updates the basic project configuration", async ({ expect }) => { }); it("updates the project domains configuration", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); const { updateProjectResponse: response1 } = await Project.updateCurrent(adminAccessToken, { config: { @@ -284,7 +284,7 @@ it("updates the project domains configuration", async ({ expect }) => { }); it("should allow insecure HTTP connections if insecureHttp is true", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); const { updateProjectResponse: response } = await Project.updateCurrent(adminAccessToken, { config: { @@ -341,7 +341,7 @@ it("should allow insecure HTTP connections if insecureHttp is true", async ({ ex }); it("should not allow protocols other than http(s) in trusted domains", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); const { updateProjectResponse: response } = await Project.updateCurrent(adminAccessToken, { config: { @@ -376,7 +376,7 @@ it("should not allow protocols other than http(s) in trusted domains", async ({ }); it("updates the project email configuration", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); const { updateProjectResponse: response1 } = await Project.updateCurrent(adminAccessToken, { config: { @@ -700,7 +700,7 @@ it("does not update project email config to empty host", async ({ expect }) => { }); it("updates the project email configuration with invalid parameters", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); const { updateProjectResponse: response1 } = await Project.updateCurrent(adminAccessToken, { config: { @@ -777,7 +777,7 @@ it("updates the project email configuration with invalid parameters", async ({ e }); it("updates the project oauth configuration", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); // create google oauth provider with shared type const { updateProjectResponse: response1 } = await Project.updateCurrent(adminAccessToken, { @@ -1182,7 +1182,7 @@ it("fails when trying to update OAuth provider with empty client_secret", async }); it("deletes a project with admin access", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); // Delete the project @@ -1204,7 +1204,7 @@ it("deletes a project with admin access", async ({ expect }) => { }); it("deletes a project with server access", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); // Delete the project @@ -1236,7 +1236,7 @@ it("deletes a project with server access", async ({ expect }) => { }); it("deletes a project with users, teams, and permissions", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken, createProjectResponse } = await Project.createAndGetAdminToken({ config: { magic_link_enabled: true, @@ -1436,7 +1436,7 @@ it("makes sure users own the correct projects after creating a project", async ( it("removes a deleted project from a user", async ({ expect }) => { backendContext.set({ projectKeys: InternalProjectKeys }); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const adminAccessToken = backendContext.value.userAuth?.accessToken; const { projectId } = await Project.create(); @@ -1481,7 +1481,7 @@ it("removes a deleted project from a user", async ({ expect }) => { it("makes sure other users are not affected by project deletion", async ({ expect }) => { backendContext.set({ projectKeys: InternalProjectKeys }); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const user1Auth = backendContext.value.userAuth; const { projectId } = await Project.create(); @@ -1567,7 +1567,7 @@ it("should increment and decrement userCount when a user is added to a project", }); it("should preserve API Keys app enabled state when updating allowUserApiKeys config", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); // Enable the API Keys app diff --git a/apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts index 3d77b9a1fc..96c567386e 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts @@ -1,6 +1,6 @@ import { expect } from "vitest"; import { it } from "../../../../helpers"; -import { Auth, InternalProjectKeys, Project, Team, User, backendContext, bumpEmailAddress, createMailbox, niceBackendFetch } from "../../../backend-helpers"; +import { Auth, InternalProjectKeys, Project, Team, User, backendContext, createMailbox, niceBackendFetch } from "../../../backend-helpers"; async function createAndAddCurrentUserWithoutMemberPermission() { const { teamId } = await Team.create(); @@ -24,7 +24,7 @@ async function createAndAddCurrentUserWithoutMemberPermission() { } it("requires $invite_members permission to send invitation", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { teamId } = await createAndAddCurrentUserWithoutMemberPermission(); const sendTeamInvitationResponse = await niceBackendFetch("/api/v1/team-invitations/send-code", { @@ -58,8 +58,8 @@ it("requires $invite_members permission to send invitation", async ({ expect }) }); it("can send invitation", async ({ expect }) => { - await Project.createAndSwitch({ config: { magic_link_enabled: true } }); - const { userId: userId1 } = await Auth.Otp.signIn(); + await Project.createAndSwitch(); + const { userId: userId1 } = await Auth.fastSignUp(); const { teamId } = await createAndAddCurrentUserWithoutMemberPermission(); const receiveMailbox = createMailbox(); @@ -74,7 +74,7 @@ it("can send invitation", async ({ expect }) => { await Team.sendInvitation(receiveMailbox, teamId); backendContext.set({ mailbox: receiveMailbox }); - await Auth.Otp.signIn(); + await Auth.fastSignUp({ primary_email: receiveMailbox.emailAddress, primary_email_verified: true }); await Team.acceptInvitation(); @@ -131,7 +131,7 @@ it("can send invitation without a current user on the server", async ({ expect } `); backendContext.set({ mailbox: receiveMailbox }); - await Auth.Otp.signIn(); + await Auth.fastSignUp({ primary_email: receiveMailbox.emailAddress, primary_email_verified: true }); await Team.acceptInvitation(); const response = await niceBackendFetch(`/api/v1/teams?user_id=me`, { @@ -144,7 +144,7 @@ it("can send invitation without a current user on the server", async ({ expect } it("can list invitations on the server", async ({ expect }) => { - const { userId: inviter } = await Auth.Otp.signIn(); + const { userId: inviter } = await Auth.fastSignUp(); const { teamId } = await createAndAddCurrentUserWithoutMemberPermission(); await niceBackendFetch(`/api/v1/team-permissions/${teamId}/${inviter}/$invite_members`, { @@ -209,8 +209,8 @@ it("can't list invitations across teams", async ({ expect }) => { it("allows team admins to list invitations", async ({ expect }) => { - await Project.createAndSwitch({ config: { magic_link_enabled: true } }); - const { userId: inviter } = await Auth.Otp.signIn(); + await Project.createAndSwitch(); + const { userId: inviter } = await Auth.fastSignUp(); const { teamId } = await createAndAddCurrentUserWithoutMemberPermission(); await niceBackendFetch(`/api/v1/team-permissions/${teamId}/${inviter}/$invite_members`, { @@ -220,8 +220,7 @@ it("allows team admins to list invitations", async ({ expect }) => { }); await Team.sendInvitation("some-email-test@example.com", teamId); - await bumpEmailAddress(); - const { userId: teamAdmin } = await Auth.Otp.signIn(); + const { userId: teamAdmin } = await Auth.fastSignUp(); await Team.addMember(teamId, teamAdmin); const listInvitationsResponse = await niceBackendFetch(`/api/v1/team-invitations?team_id=${teamId}`, { @@ -248,7 +247,7 @@ it("allows team admins to list invitations", async ({ expect }) => { }); it("requires $invite_members permission to list invitations", async ({ expect }) => { - const { userId: inviter } = await Auth.Otp.signIn(); + const { userId: inviter } = await Auth.fastSignUp(); const { teamId } = await createAndAddCurrentUserWithoutMemberPermission(); // Create an invitation to list @@ -259,8 +258,7 @@ it("requires $invite_members permission to list invitations", async ({ expect }) }); await Team.sendInvitation("some-email-test@example.com", teamId); - await bumpEmailAddress(); - const { userId: teamAdmin } = await Auth.Otp.signIn(); + const { userId: teamAdmin } = await Auth.fastSignUp(); await Team.addMember(teamId, teamAdmin); const deletePermissionResponse = await niceBackendFetch(`/api/v1/team-permissions/${teamId}/${teamAdmin}/team_member`, { @@ -319,7 +317,7 @@ it("requires $invite_members permission to list invitations", async ({ expect }) it("requires $read_members permission to list invitations", async ({ expect }) => { - const { userId: inviter } = await Auth.Otp.signIn(); + const { userId: inviter } = await Auth.fastSignUp(); const { teamId } = await createAndAddCurrentUserWithoutMemberPermission(); await niceBackendFetch(`/api/v1/team-permissions/${teamId}/${inviter}/$invite_members`, { @@ -329,8 +327,7 @@ it("requires $read_members permission to list invitations", async ({ expect }) = }); const { sendTeamInvitationResponse } = await Team.sendInvitation("some-email-test@example.com", teamId); - await bumpEmailAddress(); - const { userId: teamAdmin } = await Auth.Otp.signIn(); + const { userId: teamAdmin } = await Auth.fastSignUp(); await Team.addMember(teamId, teamAdmin); await niceBackendFetch(`/api/v1/team-permissions/${teamId}/${teamAdmin}/team_member`, { accessType: "server", @@ -369,7 +366,7 @@ it("requires $read_members permission to list invitations", async ({ expect }) = }); it("allows team admins to revoke invitations", async ({ expect }) => { - const { userId: inviter } = await Auth.Otp.signIn(); + const { userId: inviter } = await Auth.fastSignUp(); const { teamId } = await createAndAddCurrentUserWithoutMemberPermission(); await niceBackendFetch(`/api/v1/team-permissions/${teamId}/${inviter}/$invite_members`, { accessType: "server", @@ -379,8 +376,7 @@ it("allows team admins to revoke invitations", async ({ expect }) => { const { sendTeamInvitationResponse } = await Team.sendInvitation("some-email-test@example.com", teamId); const invitationId = sendTeamInvitationResponse.body.id; - await bumpEmailAddress(); - const { userId: teamAdmin } = await Auth.Otp.signIn(); + const { userId: teamAdmin } = await Auth.fastSignUp(); await Team.addMember(teamId, teamAdmin); await niceBackendFetch(`/api/v1/team-permissions/${teamId}/${teamAdmin}/$remove_members`, { @@ -429,7 +425,7 @@ it("allows team admins to revoke invitations", async ({ expect }) => { it("requires $remove_members permission to revoke invitations", async ({ expect }) => { - const { userId: inviter } = await Auth.Otp.signIn(); + const { userId: inviter } = await Auth.fastSignUp(); const { teamId } = await createAndAddCurrentUserWithoutMemberPermission(); await niceBackendFetch(`/api/v1/team-permissions/${teamId}/${inviter}/$invite_members`, { @@ -440,8 +436,7 @@ it("requires $remove_members permission to revoke invitations", async ({ expect const { sendTeamInvitationResponse } = await Team.sendInvitation("some-email-test@example.com", teamId); const invitationId = sendTeamInvitationResponse.body.id; - await bumpEmailAddress(); - const { userId: teamAdmin } = await Auth.Otp.signIn(); + const { userId: teamAdmin } = await Auth.fastSignUp(); await Team.addMember(teamId, teamAdmin); const revokeInvitationResponse = await niceBackendFetch(`/api/v1/team-invitations/${invitationId}?team_id=${teamId}`, { @@ -471,7 +466,7 @@ it("requires $remove_members permission to revoke invitations", async ({ expect it("errors with item_quantity_insufficient_amount when accepting invite without remaining dashboard_admins", async ({ expect }) => { backendContext.set({ projectKeys: InternalProjectKeys }); - await Auth.Otp.signIn(); + await Auth.fastSignUp({}); const { createProjectResponse } = await Project.create({ display_name: "Test Project (Insufficient Admins)" }); const ownerTeamId: string = createProjectResponse.body.owner_team_id; const mailboxB = createMailbox(); @@ -496,7 +491,7 @@ it("errors with item_quantity_insufficient_amount when accepting invite without `); backendContext.set({ mailbox: mailboxB }); - await Auth.Otp.signIn(); + await Auth.fastSignUp({ primary_email: mailboxB.emailAddress, primary_email_verified: true }); const invitationMessages = await mailboxB.waitForMessagesWithSubject("join"); const acceptResponse = await niceBackendFetch("/api/v1/team-invitations/accept", { diff --git a/apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts index 57e3147294..d65d1939fe 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts @@ -1,16 +1,16 @@ import { expect } from "vitest"; import { it } from "../../../../helpers"; -import { Auth, Team, backendContext, bumpEmailAddress, niceBackendFetch } from "../../../backend-helpers"; +import { Auth, Team, User, backendContext, bumpEmailAddress, niceBackendFetch } from "../../../backend-helpers"; async function signInAndCreateTeam() { - const { userId: userId1 } = await Auth.Otp.signIn(); - const mailbox1 = backendContext.value.mailbox; + const { userId: userId1 } = await Auth.fastSignUp(); + const userAuth = backendContext.value.userAuth; await bumpEmailAddress(); - const { userId: userId2 } = await Auth.Otp.signIn(); + const { userId: userId2 } = await Auth.fastSignUp(); await bumpEmailAddress(); - const { userId: userId3 } = await Auth.Otp.signIn(); + const { userId: userId3 } = await Auth.fastSignUp(); // update names of users await niceBackendFetch(`/api/v1/users/${userId1}`, { @@ -53,10 +53,10 @@ async function signInAndCreateTeam() { // Sign back in as user 1 backendContext.set({ - mailbox: mailbox1, + userAuth: userAuth, }); - const { userId: signedInUserId } = await Auth.Otp.signIn(); - expect(signedInUserId).toBe(userId1); + const currentUser = await User.getCurrent(); + expect(currentUser.id).toBe(userId1); // Remove any permissions from user 1 const permissionsResponse = await niceBackendFetch(`/api/v1/team-permissions?team_id=${teamId}&user_id=${userId1}`, { diff --git a/apps/e2e/tests/backend/endpoints/api/v1/team-memberships.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/team-memberships.test.ts index a28e7b1636..e1e4416ce6 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/team-memberships.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/team-memberships.test.ts @@ -4,7 +4,7 @@ import { Auth, InternalApiKey, InternalProjectKeys, Project, Team, Webhook, back it("is not allowed to add user to team on client", async ({ expect }) => { - const { userId: userId1 } = await Auth.Otp.signIn(); + const { userId: userId1 } = await Auth.fastSignUp(); const { teamId } = await Team.createWithCurrentAsCreator(); const response = await niceBackendFetch(`/api/v1/team-memberships/${teamId}/${userId1}`, { @@ -35,9 +35,8 @@ it("is not allowed to add user to team on client", async ({ expect }) => { }); it("creates a team and allows managing users on the server", async ({ expect }) => { - const { userId: userId1 } = await Auth.Otp.signIn(); - await bumpEmailAddress(); - const { userId: userId2 } = await Auth.Otp.signIn(); + const { userId: userId1 } = await Auth.fastSignUp(); + const { userId: userId2 } = await Auth.fastSignUp(); const { teamId } = await Team.createWithCurrentAsCreator(); const response = await niceBackendFetch(`/api/v1/team-memberships/${teamId}/${userId1}`, { @@ -67,7 +66,7 @@ it("creates a team and allows managing users on the server", async ({ expect }) "is_paginated": true, "items": [ { - "auth_with_email": true, + "auth_with_email": false, "client_metadata": null, "client_read_only_metadata": null, "display_name": null, @@ -76,18 +75,18 @@ it("creates a team and allows managing users on the server", async ({ expect }) "is_anonymous": false, "last_active_at_millis": , "oauth_providers": [], - "otp_auth_enabled": true, + "otp_auth_enabled": false, "passkey_auth_enabled": false, - "primary_email": "default-mailbox--@stack-generated.example.com", - "primary_email_auth_enabled": true, - "primary_email_verified": true, + "primary_email": null, + "primary_email_auth_enabled": false, + "primary_email_verified": false, "profile_image_url": null, "requires_totp_mfa": false, "selected_team": { "client_metadata": null, "client_read_only_metadata": null, "created_at_millis": , - "display_name": "default-mailbox--@stack-generated.example.com's Team", + "display_name": "Personal Team", "id": "", "profile_image_url": null, "server_metadata": null, @@ -97,7 +96,7 @@ it("creates a team and allows managing users on the server", async ({ expect }) "signed_up_at_millis": , }, { - "auth_with_email": true, + "auth_with_email": false, "client_metadata": null, "client_read_only_metadata": null, "display_name": null, @@ -106,18 +105,18 @@ it("creates a team and allows managing users on the server", async ({ expect }) "is_anonymous": false, "last_active_at_millis": , "oauth_providers": [], - "otp_auth_enabled": true, + "otp_auth_enabled": false, "passkey_auth_enabled": false, - "primary_email": "mailbox-1--@stack-generated.example.com", - "primary_email_auth_enabled": true, - "primary_email_verified": true, + "primary_email": null, + "primary_email_auth_enabled": false, + "primary_email_verified": false, "profile_image_url": null, "requires_totp_mfa": false, "selected_team": { "client_metadata": null, "client_read_only_metadata": null, "created_at_millis": , - "display_name": "mailbox-1--@stack-generated.example.com's Team", + "display_name": "Personal Team", "id": "", "profile_image_url": null, "server_metadata": null, @@ -158,7 +157,7 @@ it("creates a team and allows managing users on the server", async ({ expect }) "is_paginated": true, "items": [ { - "auth_with_email": true, + "auth_with_email": false, "client_metadata": null, "client_read_only_metadata": null, "display_name": null, @@ -167,18 +166,18 @@ it("creates a team and allows managing users on the server", async ({ expect }) "is_anonymous": false, "last_active_at_millis": , "oauth_providers": [], - "otp_auth_enabled": true, + "otp_auth_enabled": false, "passkey_auth_enabled": false, - "primary_email": "default-mailbox--@stack-generated.example.com", - "primary_email_auth_enabled": true, - "primary_email_verified": true, + "primary_email": null, + "primary_email_auth_enabled": false, + "primary_email_verified": false, "profile_image_url": null, "requires_totp_mfa": false, "selected_team": { "client_metadata": null, "client_read_only_metadata": null, "created_at_millis": , - "display_name": "default-mailbox--@stack-generated.example.com's Team", + "display_name": "Personal Team", "id": "", "profile_image_url": null, "server_metadata": null, @@ -196,12 +195,11 @@ it("creates a team and allows managing users on the server", async ({ expect }) }); it("lets users be on multiple teams", async ({ expect }) => { - const { userId: creatorUserId } = await Auth.Otp.signIn(); + const { userId: creatorUserId } = await Auth.fastSignUp(); const { teamId: teamId1 } = await Team.createWithCurrentAsCreator(); const { teamId: teamId2 } = await Team.createWithCurrentAsCreator(); - await bumpEmailAddress(); - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); await niceBackendFetch(`/api/v1/team-memberships/${teamId1}/${userId}`, { accessType: "server", method: "POST", @@ -224,7 +222,7 @@ it("lets users be on multiple teams", async ({ expect }) => { "is_paginated": true, "items": [ { - "auth_with_email": true, + "auth_with_email": false, "client_metadata": null, "client_read_only_metadata": null, "display_name": null, @@ -233,18 +231,18 @@ it("lets users be on multiple teams", async ({ expect }) => { "is_anonymous": false, "last_active_at_millis": , "oauth_providers": [], - "otp_auth_enabled": true, + "otp_auth_enabled": false, "passkey_auth_enabled": false, - "primary_email": "default-mailbox--@stack-generated.example.com", - "primary_email_auth_enabled": true, - "primary_email_verified": true, + "primary_email": null, + "primary_email_auth_enabled": false, + "primary_email_verified": false, "profile_image_url": null, "requires_totp_mfa": false, "selected_team": { "client_metadata": null, "client_read_only_metadata": null, "created_at_millis": , - "display_name": "default-mailbox--@stack-generated.example.com's Team", + "display_name": "Personal Team", "id": "", "profile_image_url": null, "server_metadata": null, @@ -254,7 +252,7 @@ it("lets users be on multiple teams", async ({ expect }) => { "signed_up_at_millis": , }, { - "auth_with_email": true, + "auth_with_email": false, "client_metadata": null, "client_read_only_metadata": null, "display_name": null, @@ -263,18 +261,18 @@ it("lets users be on multiple teams", async ({ expect }) => { "is_anonymous": false, "last_active_at_millis": , "oauth_providers": [], - "otp_auth_enabled": true, + "otp_auth_enabled": false, "passkey_auth_enabled": false, - "primary_email": "mailbox-1--@stack-generated.example.com", - "primary_email_auth_enabled": true, - "primary_email_verified": true, + "primary_email": null, + "primary_email_auth_enabled": false, + "primary_email_verified": false, "profile_image_url": null, "requires_totp_mfa": false, "selected_team": { "client_metadata": null, "client_read_only_metadata": null, "created_at_millis": , - "display_name": "mailbox-1--@stack-generated.example.com's Team", + "display_name": "Personal Team", "id": "", "profile_image_url": null, "server_metadata": null, @@ -301,7 +299,7 @@ it("lets users be on multiple teams", async ({ expect }) => { "is_paginated": true, "items": [ { - "auth_with_email": true, + "auth_with_email": false, "client_metadata": null, "client_read_only_metadata": null, "display_name": null, @@ -310,18 +308,18 @@ it("lets users be on multiple teams", async ({ expect }) => { "is_anonymous": false, "last_active_at_millis": , "oauth_providers": [], - "otp_auth_enabled": true, + "otp_auth_enabled": false, "passkey_auth_enabled": false, - "primary_email": "default-mailbox--@stack-generated.example.com", - "primary_email_auth_enabled": true, - "primary_email_verified": true, + "primary_email": null, + "primary_email_auth_enabled": false, + "primary_email_verified": false, "profile_image_url": null, "requires_totp_mfa": false, "selected_team": { "client_metadata": null, "client_read_only_metadata": null, "created_at_millis": , - "display_name": "default-mailbox--@stack-generated.example.com's Team", + "display_name": "Personal Team", "id": "", "profile_image_url": null, "server_metadata": null, @@ -331,7 +329,7 @@ it("lets users be on multiple teams", async ({ expect }) => { "signed_up_at_millis": , }, { - "auth_with_email": true, + "auth_with_email": false, "client_metadata": null, "client_read_only_metadata": null, "display_name": null, @@ -340,18 +338,18 @@ it("lets users be on multiple teams", async ({ expect }) => { "is_anonymous": false, "last_active_at_millis": , "oauth_providers": [], - "otp_auth_enabled": true, + "otp_auth_enabled": false, "passkey_auth_enabled": false, - "primary_email": "mailbox-1--@stack-generated.example.com", - "primary_email_auth_enabled": true, - "primary_email_verified": true, + "primary_email": null, + "primary_email_auth_enabled": false, + "primary_email_verified": false, "profile_image_url": null, "requires_totp_mfa": false, "selected_team": { "client_metadata": null, "client_read_only_metadata": null, "created_at_millis": , - "display_name": "mailbox-1--@stack-generated.example.com's Team", + "display_name": "Personal Team", "id": "", "profile_image_url": null, "server_metadata": null, @@ -369,7 +367,7 @@ it("lets users be on multiple teams", async ({ expect }) => { }); it("does not allow adding a user to a team if the user is already a member of the team", async ({ expect }) => { - const { userId: userId1 } = await Auth.Otp.signIn(); + const { userId: userId1 } = await Auth.fastSignUp(); const { teamId } = await Team.createWithCurrentAsCreator(); const response1 = await niceBackendFetch(`/api/v1/team-memberships/${teamId}/${userId1}`, { @@ -455,7 +453,7 @@ it("should give team creator default permissions", async ({ expect }) => { }); it("allows leaving team", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { teamId } = await Team.createWithCurrentAsCreator(); // Does not have permission to remove user from team @@ -474,9 +472,8 @@ it("allows leaving team", async ({ expect }) => { }); it("removes user from team on the client", async ({ expect }) => { - const { userId: userId1 } = await Auth.Otp.signIn(); - await bumpEmailAddress(); - const { userId: userId2 } = await Auth.Otp.signIn(); + const { userId: userId1 } = await Auth.fastSignUp(); + const { userId: userId2 } = await Auth.fastSignUp(); const { teamId } = await Team.create(); await Team.addMember(teamId, userId1); await Team.addMember(teamId, userId2); @@ -528,11 +525,9 @@ it("removes user from team on the client", async ({ expect }) => { }); it("creates a team on the server and adds a different user as the creator", async ({ expect }) => { - await Project.createAndSwitch({ config: { magic_link_enabled: true } }); - const user1Mailbox = await bumpEmailAddress(); - const { userId: userId1 } = await Auth.Otp.signIn(); - await bumpEmailAddress(); - await Auth.Otp.signIn(); + await Project.createAndSwitch(); + const { userId: userId1, accessToken: user1AccessToken, refreshToken: user1RefreshToken } = await Auth.fastSignUp(); + await Auth.fastSignUp(); const response = await niceBackendFetch("/api/v1/teams", { accessType: "server", @@ -558,8 +553,7 @@ it("creates a team on the server and adds a different user as the creator", asyn } `); - backendContext.set({ mailbox: user1Mailbox }); - await Auth.Otp.signIn(); + backendContext.set({ userAuth: { accessToken: user1AccessToken, refreshToken: user1RefreshToken } }); const response2 = await niceBackendFetch(`/api/v1/teams?user_id=me`, { accessType: "client", @@ -589,11 +583,10 @@ it("creates a team on the server and adds a different user as the creator", asyn it("should trigger team membership webhook when a user is added to a team", async ({ expect }) => { const { projectId, svixToken, endpointId } = await Webhook.createProjectWithEndpoint(); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { teamId } = await Team.createWithCurrentAsCreator(); - await bumpEmailAddress(); - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); const addUserResponse = await niceBackendFetch(`/api/v1/team-memberships/${teamId}/${userId}`, { accessType: "server", @@ -628,11 +621,10 @@ it("should trigger team membership webhook when a user is added to a team", asyn it("should trigger team membership webhook when a user is removed from a team", async ({ expect }) => { const { projectId, svixToken, endpointId } = await Webhook.createProjectWithEndpoint(); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { teamId } = await Team.createWithCurrentAsCreator(); - await bumpEmailAddress(); - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); const addUserResponse = await niceBackendFetch(`/api/v1/team-memberships/${teamId}/${userId}`, { accessType: "server", @@ -672,11 +664,10 @@ it("should trigger team membership webhook when a user is removed from a team", it("should trigger team permission webhook when a user is added to a team", async ({ expect }) => { const { projectId, svixToken, endpointId } = await Webhook.createProjectWithEndpoint(); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { teamId } = await Team.createWithCurrentAsCreator(); - await bumpEmailAddress(); - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); const addUserResponse = await niceBackendFetch(`/api/v1/team-memberships/${teamId}/${userId}`, { accessType: "server", @@ -765,11 +756,10 @@ it("should trigger multiple permission webhooks when a custom permission is incl await InternalApiKey.createAndSetProjectKeys(adminAccessToken); // Create a user and team - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { teamId } = await Team.createWithCurrentAsCreator(); - await bumpEmailAddress(); - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); // Add the user to the team const addUserResponse = await niceBackendFetch(`/api/v1/team-memberships/${teamId}/${userId}`, { diff --git a/apps/e2e/tests/backend/endpoints/api/v1/team-permissions.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/team-permissions.test.ts index b5e4ee5a1b..9e2f3936b5 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/team-permissions.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/team-permissions.test.ts @@ -2,7 +2,7 @@ import { it } from "../../../../helpers"; import { Auth, InternalApiKey, InternalProjectKeys, Project, Team, Webhook, backendContext, niceBackendFetch } from "../../../backend-helpers"; it("is not allowed to list permissions from the other users on the client", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { teamId } = await Team.createWithCurrentAsCreator(); const response = await niceBackendFetch(`/api/v1/team-permissions?team_id=${teamId}`, { @@ -19,7 +19,7 @@ it("is not allowed to list permissions from the other users on the client", asyn }); it("is not allowed to grant non-existing permission to a user on the server", async ({ expect }) => { - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); const { teamId } = await Team.createWithCurrentAsCreator(); const response = await niceBackendFetch(`/api/v1/team-permissions/${teamId}/${userId}/does_not_exist`, { @@ -44,8 +44,8 @@ it("is not allowed to grant non-existing permission to a user on the server", as }); it("does not grant a project permission to a user", async ({ expect }) => { - await Project.createAndSwitch({ config: { magic_link_enabled: true } }); - const { userId } = await Auth.Otp.signIn(); + await Project.createAndSwitch(); + const { userId } = await Auth.fastSignUp(); const { teamId } = await Team.createWithCurrentAsCreator(); const projectPermissionDefinitionResponse = await niceBackendFetch(`/api/v1/project-permission-definitions`, { @@ -195,7 +195,7 @@ it("can create a new permission and grant it to a user on the server", async ({ }); it("can customize default team permissions", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); const response1 = await niceBackendFetch(`/api/v1/team-permission-definitions`, { @@ -271,7 +271,7 @@ it("can customize default team permissions", async ({ expect }) => { it("should trigger team permission webhook when a permission is granted to a user", async ({ expect }) => { const { projectId, svixToken, endpointId } = await Webhook.createProjectWithEndpoint(); - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); const { teamId } = await Team.createWithCurrentAsCreator(); const grantPermissionResponse = await niceBackendFetch(`/api/v1/team-permissions/${teamId}/${userId}/$update_team`, { @@ -316,7 +316,7 @@ it("should trigger team permission webhook when a permission is granted to a use it("should trigger team permission webhook when a permission is revoked from a user", async ({ expect }) => { const { projectId, svixToken, endpointId } = await Webhook.createProjectWithEndpoint(); - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); const { teamId } = await Team.createWithCurrentAsCreator(); // First grant the permission @@ -358,7 +358,7 @@ it("should trigger team permission webhook when a permission is revoked from a u }); it("should not be able to create a permission with the same name as an existing team permission", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { adminAccessToken } = await Project.createAndGetAdminToken(); // First, create a team permission definition diff --git a/apps/e2e/tests/backend/endpoints/api/v1/teams.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/teams.test.ts index b7ec119036..5930b25031 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/teams.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/teams.test.ts @@ -3,7 +3,7 @@ import { Auth, InternalApiKey, Project, Team, Webhook, bumpEmailAddress, niceBac it("is not allowed to list all the teams in a project on the client", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch("/api/v1/teams", { accessType: "client" }); expect(response).toMatchInlineSnapshot(` NiceResponse { @@ -15,7 +15,7 @@ it("is not allowed to list all the teams in a project on the client", async ({ e }); it("lists all the teams in a project with server access", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch("/api/v1/teams", { accessType: "server" }); expect(response).toMatchObject({ status: 200, @@ -28,8 +28,8 @@ it("lists all the teams in a project with server access", async ({ expect }) => }); it("lists all the teams the current user has on the client", async ({ expect }) => { - await Project.createAndSwitch({ config: { magic_link_enabled: true } }); - const { userId } = await Auth.Otp.signIn(); + await Project.createAndSwitch(); + const { userId } = await Auth.fastSignUp(); const response1 = await niceBackendFetch("/api/v1/teams?user_id=me", { accessType: "client" }); expect(response1).toMatchInlineSnapshot(` NiceResponse { @@ -56,8 +56,8 @@ it("lists all the teams the current user has on the client", async ({ expect }) }); it("lists all the teams the current user has on the server", async ({ expect }) => { - await Project.createAndSwitch({ config: { magic_link_enabled: true } }); - const { userId } = await Auth.Otp.signIn(); + await Project.createAndSwitch(); + const { userId } = await Auth.fastSignUp(); const response1 = await niceBackendFetch("/api/v1/teams?user_id=me", { accessType: "server" }); expect(response1).toMatchInlineSnapshot(` NiceResponse { @@ -84,12 +84,12 @@ it("lists all the teams the current user has on the server", async ({ expect }) }); it("creates a team on the client", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); await Team.createWithCurrentAsCreator(); }); it("does not allow creating a team when not signed in", async ({ expect }) => { - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); await Auth.signOut(); const response = await niceBackendFetch("/api/v1/teams", { accessType: "client", @@ -115,10 +115,9 @@ it("does not allow creating a team when not signed in", async ({ expect }) => { }); it("does not allow creating teams on the client for a different creator", async ({ expect }) => { - await Project.createAndSwitch({ config: { client_team_creation_enabled: true, magic_link_enabled: true } }); - const { userId: userId1 } = await Auth.Otp.signIn(); - await bumpEmailAddress(); - await Auth.Otp.signIn(); + await Project.createAndSwitch({ config: { client_team_creation_enabled: true } }); + const { userId: userId1 } = await Auth.fastSignUp(); + await Auth.fastSignUp(); const response = await niceBackendFetch("/api/v1/teams", { accessType: "client", method: "POST", @@ -137,22 +136,22 @@ it("does not allow creating teams on the client for a different creator", async }); it("creates a team on the server", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); await Team.createWithCurrentAsCreator({ accessType: "server" }); }); it("creates a team on the server without a creator", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); await Team.create({ accessType: "server" }); }); it("creates a team with a specific creator user id", async ({ expect }) => { - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); await Team.create({ accessType: "server", creatorUserId: userId }); }); it("does not create a team when the creator user id does not exist", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch("/api/v1/teams", { accessType: "server", method: "POST", @@ -177,7 +176,7 @@ it("does not create a team when the creator user id does not exist", async ({ ex }); it("gets a specific team on the client", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { createTeamResponse: response, teamId } = await Team.createWithCurrentAsCreator(); expect(response).toMatchInlineSnapshot(` NiceResponse { @@ -212,11 +211,10 @@ it("gets a specific team on the client", async ({ expect }) => { }); it("gets a specific team that the user is not part of on the client", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { createTeamResponse: response, teamId } = await Team.createWithCurrentAsCreator(); - await bumpEmailAddress(); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response2 = await niceBackendFetch(`/api/v1/teams/${teamId}`, { accessType: "client" }); expect(response2).toMatchInlineSnapshot(` @@ -239,12 +237,10 @@ it("gets a specific team that the user is not part of on the client", async ({ e }); it("gets a team that the user is not part of on the server", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { teamId } = await Team.createWithCurrentAsCreator(); - await bumpEmailAddress(); - - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { createTeamResponse: response } = await Team.createWithCurrentAsCreator(); expect(response).toMatchInlineSnapshot(` NiceResponse { @@ -281,12 +277,10 @@ it("gets a team that the user is not part of on the server", async ({ expect }) }); it("should not be allowed to get a team that the user is not part of on the client", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { teamId } = await Team.createWithCurrentAsCreator(); - await bumpEmailAddress(); - - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { createTeamResponse: response } = await Team.createWithCurrentAsCreator(); expect(response).toMatchInlineSnapshot(` NiceResponse { @@ -325,7 +319,7 @@ it("should not be allowed to get a team that the user is not part of on the clie }); it("updates a team on the client", async ({ expect }) => { - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); const { teamId } = await Team.createWithCurrentAsCreator(); // grant permission to update a team @@ -359,7 +353,7 @@ it("updates a team on the client", async ({ expect }) => { }); it("can set a team's display name to the empty string", async ({ expect }) => { - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); const { teamId } = await Team.createWithCurrentAsCreator(); // grant permission to update a team @@ -393,7 +387,7 @@ it("can set a team's display name to the empty string", async ({ expect }) => { }); it("updates team client metadata on the client", async ({ expect }) => { - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); const { teamId } = await Team.createWithCurrentAsCreator(); // grant permission to update a team @@ -429,7 +423,7 @@ it("updates team client metadata on the client", async ({ expect }) => { }); it("should not be able to update team client read only metadata on the client", async ({ expect }) => { - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); const { teamId } = await Team.createWithCurrentAsCreator(); // grant permission to update a team @@ -474,7 +468,7 @@ it("should not be able to update team client read only metadata on the client", }); it("should not update a team without permission on the client", async ({ expect }) => { - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); const { teamId } = await Team.create(); // add user to the team @@ -509,8 +503,8 @@ it("should not update a team without permission on the client", async ({ expect }); it("updates a team on the server", async ({ expect }) => { - await Project.createAndSwitch({ config: { magic_link_enabled: true } }); - await Auth.Otp.signIn(); + await Project.createAndSwitch(); + await Auth.fastSignUp(); const { teamId } = await Team.createWithCurrentAsCreator({ accessType: "server" }); const response1 = await niceBackendFetch(`/api/v1/teams/${teamId}`, { @@ -564,7 +558,7 @@ it("updates a team on the server", async ({ expect }) => { }); it("updates team client read only metadata on the server", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const { teamId } = await Team.createWithCurrentAsCreator({ accessType: "server" }); const response1 = await niceBackendFetch(`/api/v1/teams/${teamId}`, { @@ -610,7 +604,7 @@ it("updates team client read only metadata on the server", async ({ expect }) => }); it("deletes a team on the client", async ({ expect }) => { - const { userId } = await Auth.Otp.signIn(); + const { userId } = await Auth.fastSignUp(); const { teamId } = await Team.createWithCurrentAsCreator(); // grant permission to delete a team @@ -637,8 +631,8 @@ it("deletes a team on the client", async ({ expect }) => { `); }); -it("should not update a team without permission on the client", async ({ expect }) => { - const { userId } = await Auth.Otp.signIn(); +it("should not delete a team without permission on the client", async ({ expect }) => { + const { userId } = await Auth.fastSignUp(); const { teamId } = await Team.create(); // add user to the team @@ -673,8 +667,8 @@ it("should not update a team without permission on the client", async ({ expect }); it("deletes a team on the server", async ({ expect }) => { - await Project.createAndSwitch({ config: { magic_link_enabled: true } }); - await Auth.Otp.signIn(); + await Project.createAndSwitch(); + await Auth.fastSignUp(); const { teamId } = await Team.createWithCurrentAsCreator({ accessType: "server" }); const response1 = await niceBackendFetch(`/api/v1/teams/${teamId}`, { diff --git a/apps/e2e/tests/backend/endpoints/api/v1/users.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/users.test.ts index 477bf0957b..9694eba193 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/users.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/users.test.ts @@ -204,7 +204,7 @@ describe("with client access", () => { }); it("should not be able to read own user without access token even if refresh token is given", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); backendContext.set({ userAuth: { ...backendContext.value.userAuth, accessToken: undefined } }); const response = await niceBackendFetch("/api/v1/users/me", { accessType: "client", @@ -225,7 +225,7 @@ describe("with client access", () => { }); it("should return access token invalid error when reading own user with invalid access token", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); backendContext.set({ userAuth: { ...backendContext.value.userAuth, accessToken: "12341234" } }); const response = await niceBackendFetch("/api/v1/users/me", { accessType: "client", @@ -334,7 +334,7 @@ describe("with client access", () => { it.todo("should not be able to set own profile image URL to a localhost/non-public URL"); it("should not be able to set own server_metadata", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch("/api/v1/users/me", { accessType: "client", method: "PATCH", @@ -371,11 +371,10 @@ describe("with client access", () => { await Project.createAndSwitch({ config: { client_user_deletion_enabled: false, - magic_link_enabled: true, }, }); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch("/api/v1/users/me", { accessType: "client", method: "DELETE", @@ -393,11 +392,10 @@ describe("with client access", () => { await Project.createAndSwitch({ config: { client_user_deletion_enabled: true, - magic_link_enabled: true, }, }); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch("/api/v1/users/me", { accessType: "client", method: "DELETE", @@ -520,7 +518,7 @@ describe("with client access", () => { }); it("should be able to update totp_secret_base64 to valid base64", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const secret = generateSecureRandomString(32); const response = await niceBackendFetch("/api/v1/users/me", { accessType: "client", @@ -533,7 +531,7 @@ describe("with client access", () => { }); it("should not be able to update totp_secret_base64 to invalid base64", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch("/api/v1/users/me", { accessType: "client", method: "PATCH", @@ -593,7 +591,7 @@ describe("with client access", () => { }); it("should not be able to read a user", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); backendContext.set({ userAuth: null, }); @@ -665,7 +663,7 @@ describe("with client access", () => { }); it("should not be able to update own client read-only metadata", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch("/api/v1/users/me", { accessType: "client", method: "PATCH", @@ -698,7 +696,7 @@ describe("with client access", () => { }); it("should not be able to update profile image url", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch("/api/v1/users/me", { accessType: "client", method: "PATCH", @@ -716,7 +714,7 @@ describe("with client access", () => { }); it("should be able to update profile image url with base64", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch("/api/v1/users/me", { accessType: "client", method: "PATCH", @@ -728,7 +726,7 @@ describe("with client access", () => { }); it("should not be able to update profile image url with invalid base64", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch("/api/v1/users/me", { accessType: "client", method: "PATCH", @@ -746,8 +744,8 @@ describe("with client access", () => { }); it("should be able to update selected team", async ({ expect }) => { - await Project.createAndSwitch({ config: { magic_link_enabled: true } }); - await Auth.Otp.signIn(); + await Project.createAndSwitch(); + await Auth.fastSignUp(); const { teamId: team1Id } = await Team.createWithCurrentAsCreator({}); const { teamId: team2Id } = await Team.createWithCurrentAsCreator({}); const response1 = await niceBackendFetch("/api/v1/users/me", { @@ -873,7 +871,7 @@ describe("with server access", () => { }); it("should be able to delete own user", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch("/api/v1/users/me", { accessType: "server", method: "DELETE", @@ -931,10 +929,9 @@ describe("with server access", () => { }); it("lists users with pagination", async ({ expect }) => { - await Project.createAndSwitch({ config: { magic_link_enabled: true } }); + await Project.createAndSwitch(); for (let i = 0; i < 5; i++) { - await bumpEmailAddress(); - await Auth.Otp.signIn(); + await Auth.fastSignUp(); } const allResponse = await niceBackendFetch("/api/v1/users", { accessType: "server", @@ -2098,7 +2095,7 @@ describe("with server access", () => { }); it("should not be able to set profile image url to empty string", async ({ expect }) => { - await Auth.Otp.signIn(); + await Auth.fastSignUp(); const response = await niceBackendFetch("/api/v1/users/me", { accessType: "server", method: "PATCH", diff --git a/apps/e2e/tests/general/setup-wizard.test.ts b/apps/e2e/tests/general/setup-wizard.test.ts index 3814d0172d..dfd86160ec 100644 --- a/apps/e2e/tests/general/setup-wizard.test.ts +++ b/apps/e2e/tests/general/setup-wizard.test.ts @@ -3,7 +3,8 @@ import { describe } from "vitest"; import { it } from "../helpers"; describe("Setup wizard", () => { - it("completes successfully", async ({ expect }) => { + // note that we run this only in CI environments + it.runIf(!!process.env.CI)("completes successfully", async ({ expect }) => { const [error, stdout, stderr] = await new Promise<[Error | null, string, string]>((resolve) => { exec("pnpm -C packages/init-stack run test-run", (error, stdout, stderr) => { resolve([error, stdout, stderr]); From a09188f9479829fc116301803c7b8169b23412af Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Sun, 28 Dec 2025 20:30:52 +0100 Subject: [PATCH 3/9] Make config endpoints SmartRouteHandlers --- .../app/api/latest/internal/config/crud.tsx | 13 ---- .../latest/internal/config/override/crud.tsx | 46 ------------- .../latest/internal/config/override/route.tsx | 69 ++++++++++++++++++- .../app/api/latest/internal/config/route.tsx | 33 ++++++++- .../src/interface/admin-interface.ts | 6 +- .../stack-shared/src/interface/crud/config.ts | 38 ---------- .../apps/implementations/admin-app-impl.ts | 3 +- 7 files changed, 101 insertions(+), 107 deletions(-) delete mode 100644 apps/backend/src/app/api/latest/internal/config/crud.tsx delete mode 100644 apps/backend/src/app/api/latest/internal/config/override/crud.tsx delete mode 100644 packages/stack-shared/src/interface/crud/config.ts diff --git a/apps/backend/src/app/api/latest/internal/config/crud.tsx b/apps/backend/src/app/api/latest/internal/config/crud.tsx deleted file mode 100644 index 03ed2092eb..0000000000 --- a/apps/backend/src/app/api/latest/internal/config/crud.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { createCrudHandlers } from "@/route-handlers/crud-handler"; -import { configCrud } from "@stackframe/stack-shared/dist/interface/crud/config"; -import { yupObject } from "@stackframe/stack-shared/dist/schema-fields"; -import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies"; - -export const configCrudHandlers = createLazyProxy(() => createCrudHandlers(configCrud, { - paramsSchema: yupObject({}), - onRead: async ({ auth }) => { - return { - config_string: JSON.stringify(auth.tenancy.config), - }; - }, -})); diff --git a/apps/backend/src/app/api/latest/internal/config/override/crud.tsx b/apps/backend/src/app/api/latest/internal/config/override/crud.tsx deleted file mode 100644 index fcc1b75b6b..0000000000 --- a/apps/backend/src/app/api/latest/internal/config/override/crud.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { getRenderedEnvironmentConfigQuery, overrideEnvironmentConfigOverride } from "@/lib/config"; -import { globalPrismaClient, rawQuery } from "@/prisma-client"; -import { createCrudHandlers } from "@/route-handlers/crud-handler"; -import { environmentConfigSchema, getConfigOverrideErrors, migrateConfigOverride } from "@stackframe/stack-shared/dist/config/schema"; -import { configOverrideCrud } from "@stackframe/stack-shared/dist/interface/crud/config"; -import { yupObject } from "@stackframe/stack-shared/dist/schema-fields"; -import { StatusError } from "@stackframe/stack-shared/dist/utils/errors"; -import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies"; - -export const configOverridesCrudHandlers = createLazyProxy(() => createCrudHandlers(configOverrideCrud, { - paramsSchema: yupObject({}), - onUpdate: async ({ auth, data }) => { - if (data.config_override_string) { - let parsedConfig; - try { - parsedConfig = JSON.parse(data.config_override_string); - } catch (e) { - if (e instanceof SyntaxError) { - throw new StatusError(StatusError.BadRequest, 'Invalid config JSON'); - } - throw e; - } - - // TODO instead of doing this check here, we should change overrideEnvironmentConfigOverride to return the errors from its ensureNoConfigOverrideErrors call - const overrideError = await getConfigOverrideErrors(environmentConfigSchema, migrateConfigOverride("environment", parsedConfig)); - if (overrideError.status === "error") { - throw new StatusError(StatusError.BadRequest, overrideError.error); - } - - await overrideEnvironmentConfigOverride({ - projectId: auth.tenancy.project.id, - branchId: auth.tenancy.branchId, - environmentConfigOverrideOverride: parsedConfig, - }); - } - - const updatedConfig = await rawQuery(globalPrismaClient, getRenderedEnvironmentConfigQuery({ - projectId: auth.tenancy.project.id, - branchId: auth.tenancy.branchId, - })); - - return { - config_override_string: JSON.stringify(updatedConfig), - }; - }, -})); diff --git a/apps/backend/src/app/api/latest/internal/config/override/route.tsx b/apps/backend/src/app/api/latest/internal/config/override/route.tsx index 9fc6faa6fb..4f85093d37 100644 --- a/apps/backend/src/app/api/latest/internal/config/override/route.tsx +++ b/apps/backend/src/app/api/latest/internal/config/override/route.tsx @@ -1,3 +1,68 @@ -import { configOverridesCrudHandlers } from "./crud"; +import { getRenderedEnvironmentConfigQuery, overrideEnvironmentConfigOverride } from "@/lib/config"; +import { globalPrismaClient, rawQuery } from "@/prisma-client"; +import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; +import { environmentConfigSchema, getConfigOverrideErrors, migrateConfigOverride } from "@stackframe/stack-shared/dist/config/schema"; +import { adaptSchema, adminAuthTypeSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; +import { StatusError } from "@stackframe/stack-shared/dist/utils/errors"; -export const PATCH = configOverridesCrudHandlers.updateHandler; +export const PATCH = createSmartRouteHandler({ + metadata: { + summary: 'Update the config', + description: 'Update the config for a project and branch with an override', + tags: ['Config'], + }, + request: yupObject({ + auth: yupObject({ + type: adminAuthTypeSchema, + tenancy: adaptSchema, + }).defined(), + body: yupObject({ + config_override_string: yupString().optional(), + }).defined(), + }), + response: yupObject({ + statusCode: yupNumber().oneOf([200]).defined(), + bodyType: yupString().oneOf(["json"]).defined(), + body: yupObject({ + config_override_string: yupString().defined(), + }).defined(), + }), + handler: async (req) => { + if (req.body.config_override_string) { + let parsedConfig; + try { + parsedConfig = JSON.parse(req.body.config_override_string); + } catch (e) { + if (e instanceof SyntaxError) { + throw new StatusError(StatusError.BadRequest, 'Invalid config JSON'); + } + throw e; + } + + // TODO instead of doing this check here, we should change overrideEnvironmentConfigOverride to return the errors from its ensureNoConfigOverrideErrors call + const overrideError = await getConfigOverrideErrors(environmentConfigSchema, migrateConfigOverride("environment", parsedConfig)); + if (overrideError.status === "error") { + throw new StatusError(StatusError.BadRequest, overrideError.error); + } + + await overrideEnvironmentConfigOverride({ + projectId: req.auth.tenancy.project.id, + branchId: req.auth.tenancy.branchId, + environmentConfigOverrideOverride: parsedConfig, + }); + } + + const updatedConfig = await rawQuery(globalPrismaClient, getRenderedEnvironmentConfigQuery({ + projectId: req.auth.tenancy.project.id, + branchId: req.auth.tenancy.branchId, + })); + + return { + statusCode: 200, + bodyType: "json", + body: { + config_override_string: JSON.stringify(updatedConfig), + }, + }; + }, +}); diff --git a/apps/backend/src/app/api/latest/internal/config/route.tsx b/apps/backend/src/app/api/latest/internal/config/route.tsx index 014d6a7ba3..ab55505266 100644 --- a/apps/backend/src/app/api/latest/internal/config/route.tsx +++ b/apps/backend/src/app/api/latest/internal/config/route.tsx @@ -1,3 +1,32 @@ -import { configCrudHandlers } from "./crud"; +import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; +import { adaptSchema, adminAuthTypeSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -export const GET = configCrudHandlers.readHandler; +export const GET = createSmartRouteHandler({ + metadata: { + summary: 'Get the config', + description: 'Get the config for a project and branch', + tags: ['Config'], + }, + request: yupObject({ + auth: yupObject({ + type: adminAuthTypeSchema, + tenancy: adaptSchema, + }).defined(), + }), + response: yupObject({ + statusCode: yupNumber().oneOf([200]).defined(), + bodyType: yupString().oneOf(["json"]).defined(), + body: yupObject({ + config_string: yupString().defined(), + }).defined(), + }), + handler: async (req) => { + return { + statusCode: 200, + bodyType: "json", + body: { + config_string: JSON.stringify(req.auth.tenancy.config), + }, + }; + }, +}); diff --git a/packages/stack-shared/src/interface/admin-interface.ts b/packages/stack-shared/src/interface/admin-interface.ts index 40c80d5831..be0df3f8cb 100644 --- a/packages/stack-shared/src/interface/admin-interface.ts +++ b/packages/stack-shared/src/interface/admin-interface.ts @@ -1,7 +1,6 @@ import { KnownErrors } from "../known-errors"; import { AccessToken, InternalSession, RefreshToken } from "../sessions"; import { Result } from "../utils/results"; -import { ConfigCrud, ConfigOverrideCrud } from "./crud/config"; import { InternalEmailsCrud } from "./crud/emails"; import { InternalApiKeysCrud } from "./crud/internal-api-keys"; import { ProjectPermissionDefinitionsCrud } from "./crud/project-permissions"; @@ -512,7 +511,7 @@ export class StackAdminInterface extends StackServerInterface { return await response.json(); } - async getConfig(): Promise { + async getConfig(): Promise<{ config_string: string }> { const response = await this.sendAdminRequest( `/internal/config`, { method: "GET" }, @@ -521,7 +520,7 @@ export class StackAdminInterface extends StackServerInterface { return await response.json(); } - async updateConfig(data: { configOverride: any }): Promise { + async updateConfig(data: { configOverride: any }): Promise { const response = await this.sendAdminRequest( `/internal/config/override`, { @@ -533,7 +532,6 @@ export class StackAdminInterface extends StackServerInterface { }, null, ); - return await response.json(); } async createEmailTemplate(displayName: string): Promise<{ id: string }> { const response = await this.sendAdminRequest( diff --git a/packages/stack-shared/src/interface/crud/config.ts b/packages/stack-shared/src/interface/crud/config.ts deleted file mode 100644 index d1af15e252..0000000000 --- a/packages/stack-shared/src/interface/crud/config.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { CrudTypeOf, createCrud } from "../../crud"; -import * as schemaFields from "../../schema-fields"; -import { yupObject } from "../../schema-fields"; - -export const configOverrideCrudAdminReadSchema = yupObject({}).defined(); - -export const configOverrideCrudAdminUpdateSchema = yupObject({ - config_override_string: schemaFields.yupString().optional(), -}).defined(); - -export const configOverrideCrud = createCrud({ - adminReadSchema: configOverrideCrudAdminReadSchema, - adminUpdateSchema: configOverrideCrudAdminUpdateSchema, - docs: { - adminUpdate: { - summary: 'Update the config', - description: 'Update the config for a project and branch with an override', - tags: ['Config'], - }, - }, -}); -export type ConfigOverrideCrud = CrudTypeOf; - -export const configCrudAdminReadSchema = yupObject({ - config_string: schemaFields.yupString().defined(), -}).defined(); - -export const configCrud = createCrud({ - adminReadSchema: configCrudAdminReadSchema, - docs: { - adminRead: { - summary: 'Get the config', - description: 'Get the config for a project and branch', - tags: ['Config'], - }, - }, -}); -export type ConfigCrud = CrudTypeOf; diff --git a/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts b/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts index ea0b7f0a8a..c906a324e7 100644 --- a/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts +++ b/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts @@ -21,7 +21,6 @@ import { _StackServerAppImplIncomplete } from "./server-app-impl"; import { CompleteConfig, EnvironmentConfigOverrideOverride } from "@stackframe/stack-shared/dist/config/schema"; import { ChatContent } from "@stackframe/stack-shared/dist/interface/admin-interface"; -import { ConfigCrud } from "@stackframe/stack-shared/dist/interface/crud/config"; import { useAsyncCache } from "./common"; // THIS_LINE_PLATFORM react-like export class _StackAdminAppImplIncomplete extends _StackServerAppImplIncomplete implements StackAdminApp { @@ -99,7 +98,7 @@ export class _StackAdminAppImplIncomplete Date: Sun, 28 Dec 2025 20:34:12 +0100 Subject: [PATCH 4/9] internal/config/override endpoint no longer returns updated config --- .../latest/internal/config/override/route.tsx | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/apps/backend/src/app/api/latest/internal/config/override/route.tsx b/apps/backend/src/app/api/latest/internal/config/override/route.tsx index 4f85093d37..78a73cfe47 100644 --- a/apps/backend/src/app/api/latest/internal/config/override/route.tsx +++ b/apps/backend/src/app/api/latest/internal/config/override/route.tsx @@ -1,5 +1,4 @@ -import { getRenderedEnvironmentConfigQuery, overrideEnvironmentConfigOverride } from "@/lib/config"; -import { globalPrismaClient, rawQuery } from "@/prisma-client"; +import { overrideEnvironmentConfigOverride } from "@/lib/config"; import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { environmentConfigSchema, getConfigOverrideErrors, migrateConfigOverride } from "@stackframe/stack-shared/dist/config/schema"; import { adaptSchema, adminAuthTypeSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; @@ -22,10 +21,7 @@ export const PATCH = createSmartRouteHandler({ }), response: yupObject({ statusCode: yupNumber().oneOf([200]).defined(), - bodyType: yupString().oneOf(["json"]).defined(), - body: yupObject({ - config_override_string: yupString().defined(), - }).defined(), + bodyType: yupString().oneOf(["success"]).defined(), }), handler: async (req) => { if (req.body.config_override_string) { @@ -52,17 +48,9 @@ export const PATCH = createSmartRouteHandler({ }); } - const updatedConfig = await rawQuery(globalPrismaClient, getRenderedEnvironmentConfigQuery({ - projectId: req.auth.tenancy.project.id, - branchId: req.auth.tenancy.branchId, - })); - return { statusCode: 200, - bodyType: "json", - body: { - config_override_string: JSON.stringify(updatedConfig), - }, + bodyType: "success", }; }, }); From af2436ec134971fd9e36f872a08fba080c50b537 Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Mon, 29 Dec 2025 17:06:40 +0100 Subject: [PATCH 5/9] Dev stats view --- apps/backend/src/app/dev-stats/api/route.tsx | 86 +++ apps/backend/src/app/dev-stats/page.tsx | 694 +++++++++++++++++++ apps/backend/src/lib/dev-request-stats.tsx | 138 ++++ apps/backend/src/proxy.tsx | 16 +- 4 files changed, 932 insertions(+), 2 deletions(-) create mode 100644 apps/backend/src/app/dev-stats/api/route.tsx create mode 100644 apps/backend/src/app/dev-stats/page.tsx create mode 100644 apps/backend/src/lib/dev-request-stats.tsx diff --git a/apps/backend/src/app/dev-stats/api/route.tsx b/apps/backend/src/app/dev-stats/api/route.tsx new file mode 100644 index 0000000000..8ced6f14bf --- /dev/null +++ b/apps/backend/src/app/dev-stats/api/route.tsx @@ -0,0 +1,86 @@ +import { + clearRequestStats, + getAggregateStats, + getMostCommonRequests, + getMostTimeConsumingRequests, + getSlowestRequests, +} from "@/lib/dev-request-stats"; +import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; +import { yupArray, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; +import { getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env"; +import { StatusError } from "@stackframe/stack-shared/dist/utils/errors"; + +const requestStatSchema = yupObject({ + method: yupString().defined(), + path: yupString().defined(), + count: yupNumber().defined(), + totalTimeMs: yupNumber().defined(), + minTimeMs: yupNumber().defined(), + maxTimeMs: yupNumber().defined(), + lastCalledAt: yupNumber().defined(), +}); + +const aggregateStatsSchema = yupObject({ + totalRequests: yupNumber().defined(), + totalTimeMs: yupNumber().defined(), + uniqueEndpoints: yupNumber().defined(), + averageTimeMs: yupNumber().defined(), +}); + +function assertDevelopmentMode() { + if (getNodeEnvironment() !== "development") { + throw new StatusError(403, "This endpoint is only available in development mode"); + } +} + +export const GET = createSmartRouteHandler({ + metadata: { + hidden: true, + }, + request: yupObject({}), + response: yupObject({ + statusCode: yupNumber().oneOf([200]).defined(), + bodyType: yupString().oneOf(["json"]).defined(), + body: yupObject({ + aggregate: aggregateStatsSchema.defined(), + mostCommon: yupArray(requestStatSchema.defined()).defined(), + mostTimeConsuming: yupArray(requestStatSchema.defined()).defined(), + slowest: yupArray(requestStatSchema.defined()).defined(), + }).defined(), + }), + handler: async () => { + assertDevelopmentMode(); + + return { + statusCode: 200, + bodyType: "json", + body: { + aggregate: getAggregateStats(), + mostCommon: getMostCommonRequests(20), + mostTimeConsuming: getMostTimeConsumingRequests(20), + slowest: getSlowestRequests(20), + }, + }; + }, +}); + +export const DELETE = createSmartRouteHandler({ + metadata: { + hidden: true, + }, + request: yupObject({}), + response: yupObject({ + statusCode: yupNumber().oneOf([200]).defined(), + bodyType: yupString().oneOf(["success"]).defined(), + }), + handler: async () => { + assertDevelopmentMode(); + + clearRequestStats(); + + return { + statusCode: 200, + bodyType: "success", + }; + }, +}); diff --git a/apps/backend/src/app/dev-stats/page.tsx b/apps/backend/src/app/dev-stats/page.tsx new file mode 100644 index 0000000000..54ec0bae33 --- /dev/null +++ b/apps/backend/src/app/dev-stats/page.tsx @@ -0,0 +1,694 @@ +"use client"; + +import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises"; +import { stringCompare } from "@stackframe/stack-shared/dist/utils/strings"; +import { useCallback, useMemo, useState } from "react"; + +type RequestStat = { + method: string, + path: string, + count: number, + totalTimeMs: number, + minTimeMs: number, + maxTimeMs: number, + lastCalledAt: number, +}; + +type AggregateStats = { + totalRequests: number, + totalTimeMs: number, + uniqueEndpoints: number, + averageTimeMs: number, +}; + +type StatsData = { + aggregate: AggregateStats, + mostCommon: RequestStat[], + mostTimeConsuming: RequestStat[], + slowest: RequestStat[], +}; + +type SortColumn = "endpoint" | "count" | "totalTime" | "avgTime" | "minTime" | "maxTime" | "lastCalled"; +type SortDirection = "asc" | "desc"; + +function formatDuration(ms: number): string { + if (ms < 1) return `${ms.toFixed(2)}ms`; + if (ms < 1000) return `${ms.toFixed(0)}ms`; + return `${(ms / 1000).toFixed(2)}s`; +} + +function formatRelativeTime(timestamp: number): string { + const now = Date.now(); + const diff = now - timestamp; + if (diff < 1000) return "just now"; + if (diff < 60000) return `${Math.floor(diff / 1000)}s ago`; + if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`; + return `${Math.floor(diff / 3600000)}h ago`; +} + +const methodColors: Record = { + GET: { bg: "rgba(16, 185, 129, 0.2)", text: "#6ee7b7", border: "rgba(16, 185, 129, 0.3)" }, + POST: { bg: "rgba(59, 130, 246, 0.2)", text: "#93c5fd", border: "rgba(59, 130, 246, 0.3)" }, + PUT: { bg: "rgba(245, 158, 11, 0.2)", text: "#fcd34d", border: "rgba(245, 158, 11, 0.3)" }, + PATCH: { bg: "rgba(249, 115, 22, 0.2)", text: "#fdba74", border: "rgba(249, 115, 22, 0.3)" }, + DELETE: { bg: "rgba(239, 68, 68, 0.2)", text: "#fca5a5", border: "rgba(239, 68, 68, 0.3)" }, + OPTIONS: { bg: "rgba(100, 116, 139, 0.2)", text: "#cbd5e1", border: "rgba(100, 116, 139, 0.3)" }, +}; + +function MethodBadge({ method }: { method: string }) { + const colors = methodColors[method] ?? { bg: "rgba(107, 114, 128, 0.2)", text: "#d1d5db", border: "rgba(107, 114, 128, 0.3)" }; + + return ( + + {method} + + ); +} + +function SortArrow({ direction, active }: { direction: SortDirection, active: boolean }) { + return ( + + {direction === "asc" ? "↑" : "↓"} + + ); +} + +function StatCard({ + title, + value, + subtitle, + gradient, +}: { + title: string, + value: string | number, + subtitle?: string, + gradient: string, +}) { + return ( +
+
+
+

{title}

+

+ {value} +

+ {subtitle && ( +

+ {subtitle} +

+ )} +
+
+ ); +} + +function RequestTable({ + title, + description, + requests, + defaultSortColumn, +}: { + title: string, + description: string, + requests: RequestStat[], + defaultSortColumn: SortColumn, +}) { + const [sortColumn, setSortColumn] = useState(defaultSortColumn); + const [sortDirection, setSortDirection] = useState("desc"); + + const sortedRequests = useMemo(() => { + const sorted = [...requests]; + + const getSortValue = (stat: RequestStat, column: SortColumn): number | string => { + const columnGetters: Record number | string> = { + endpoint: () => `${stat.method}:${stat.path}`, + count: () => stat.count, + totalTime: () => stat.totalTimeMs, + avgTime: () => stat.totalTimeMs / stat.count, + minTime: () => stat.minTimeMs, + maxTime: () => stat.maxTimeMs, + lastCalled: () => stat.lastCalledAt, + }; + return columnGetters[column](); + }; + + sorted.sort((a, b) => { + const aVal = getSortValue(a, sortColumn); + const bVal = getSortValue(b, sortColumn); + + if (typeof aVal === "string" && typeof bVal === "string") { + const cmp = stringCompare(aVal, bVal); + return sortDirection === "asc" ? cmp : -cmp; + } + + return sortDirection === "asc" ? (aVal as number) - (bVal as number) : (bVal as number) - (aVal as number); + }); + return sorted; + }, [requests, sortColumn, sortDirection]); + + const handleSort = (column: SortColumn) => { + if (sortColumn === column) { + setSortDirection(sortDirection === "asc" ? "desc" : "asc"); + } else { + setSortColumn(column); + setSortDirection("desc"); + } + }; + + const headerStyle: React.CSSProperties = { + padding: "12px 24px", + textAlign: "left", + fontSize: "11px", + fontWeight: 500, + textTransform: "uppercase", + letterSpacing: "0.05em", + color: "#94a3b8", + cursor: "pointer", + userSelect: "none", + transition: "color 0.15s", + }; + + const headerStyleRight: React.CSSProperties = { ...headerStyle, textAlign: "right" }; + + const cellStyle: React.CSSProperties = { + padding: "16px 24px", + whiteSpace: "nowrap", + }; + + const cellStyleRight: React.CSSProperties = { ...cellStyle, textAlign: "right" }; + + const getHeaderColor = (column: SortColumn) => sortColumn === column ? "#22d3ee" : "#94a3b8"; + + if (requests.length === 0) { + return ( +
+

No requests recorded yet

+
+ ); + } + + return ( +
+
+

{title}

+

{description}

+
+
+ + + + + + + + + + + + + + {sortedRequests.map((stat) => ( + + + + + + + + + + ))} + +
handleSort("endpoint")} + > + Endpoint + + handleSort("count")} + > + Count + + handleSort("totalTime")} + > + Total Time + + handleSort("avgTime")} + > + Avg Time + + handleSort("minTime")} + > + Min + + handleSort("maxTime")} + > + Max + + handleSort("lastCalled")} + > + Last Called + +
+
+ + + {stat.path} + +
+
+ {stat.count.toLocaleString()} + + {formatDuration(stat.totalTimeMs)} + + {formatDuration(stat.totalTimeMs / stat.count)} + + {formatDuration(stat.minTimeMs)} + + {formatDuration(stat.maxTimeMs)} + + {formatRelativeTime(stat.lastCalledAt)} +
+
+
+ ); +} + +export default function DevStatsPage() { + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [lastRefresh, setLastRefresh] = useState(null); + + const fetchStats = useCallback(async () => { + setLoading(true); + setError(null); + try { + const res = await fetch("/dev-stats/api"); + if (!res.ok) { + throw new Error(await res.text()); + } + const data = await res.json(); + setStats(data); + setLastRefresh(new Date()); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to fetch stats"); + } finally { + setLoading(false); + } + }, []); + + const clearStats = useCallback(async () => { + setLoading(true); + setError(null); + try { + const res = await fetch("/dev-stats/api", { method: "DELETE" }); + if (!res.ok) { + throw new Error(await res.text()); + } + setStats(null); + setLastRefresh(null); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to clear stats"); + } finally { + setLoading(false); + } + }, []); + + const buttonStyle: React.CSSProperties = { + padding: "8px 16px", + borderRadius: "8px", + fontWeight: 500, + fontSize: "14px", + border: "1px solid", + cursor: "pointer", + transition: "all 0.2s", + }; + + return ( +
+ {/* Background pattern */} +
+ +
+ {/* Header */} +
+
+
+

+ Dev Request Stats +

+

+ Monitor API performance during development +

+
+
+ {lastRefresh && ( + + Last refresh: {lastRefresh.toLocaleTimeString()} + + )} + + +
+
+
+ + {error && ( +
+ {error} +
+ )} + + {!stats ? ( +
+
+ + + +
+

+ No stats loaded yet +

+

+ Click the “Refresh” button to load request statistics. + Stats are collected automatically when requests hit the API. +

+ +
+ ) : ( +
+ {/* Aggregate stats cards */} +
+ + + + +
+ + {/* Tables */} +
+ + + + + +
+
+ )} + + {/* Footer */} +
+

+ This page is only available in development mode. +
+ Stats are stored in memory and will be cleared when the server restarts. +

+
+
+
+ ); +} diff --git a/apps/backend/src/lib/dev-request-stats.tsx b/apps/backend/src/lib/dev-request-stats.tsx new file mode 100644 index 0000000000..5de479d13b --- /dev/null +++ b/apps/backend/src/lib/dev-request-stats.tsx @@ -0,0 +1,138 @@ +import { getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env"; + +export type RequestStat = { + method: string, + path: string, + count: number, + totalTimeMs: number, + minTimeMs: number, + maxTimeMs: number, + lastCalledAt: number, +}; + +// In-memory storage for request stats (only used in development) +const requestStatsMap = (globalThis as any).requestStatsMap ??= new Map(); + +// Ongoing requests that haven't finished yet +const ongoingRequests = (globalThis as any).ongoingRequests ??= new Map(); + +let requestIdCounter = 0; + +function getKey(method: string, path: string): string { + return `${method}:${path}`; +} + +/** + * Call this at the start of a request. Returns a unique ID that should be passed to `endRequest`. + */ +export function startRequest(method: string, path: string): string | null { + if (getNodeEnvironment() !== "development") { + return null; + } + + const requestId = `req-${++requestIdCounter}`; + ongoingRequests.set(requestId, { + startTime: performance.now(), + method, + path, + }); + + return requestId; +} + +/** + * Call this at the end of a request with the ID from `startRequest`. + */ +export function endRequest(requestId: string | null): void { + if (requestId === null || getNodeEnvironment() !== "development") { + return; + } + + const ongoing = ongoingRequests.get(requestId); + if (!ongoing) { + return; + } + + ongoingRequests.delete(requestId); + + const durationMs = performance.now() - ongoing.startTime; + const key = getKey(ongoing.method, ongoing.path); + + const existing = requestStatsMap.get(key); + if (existing) { + existing.count++; + existing.totalTimeMs += durationMs; + existing.minTimeMs = Math.min(existing.minTimeMs, durationMs); + existing.maxTimeMs = Math.max(existing.maxTimeMs, durationMs); + existing.lastCalledAt = Date.now(); + } else { + requestStatsMap.set(key, { + method: ongoing.method, + path: ongoing.path, + count: 1, + totalTimeMs: durationMs, + minTimeMs: durationMs, + maxTimeMs: durationMs, + lastCalledAt: Date.now(), + }); + } +} + +/** + * Get all request stats + */ +export function getAllRequestStats(): RequestStat[] { + return Array.from(requestStatsMap.values()); +} + +/** + * Get the most common requests by count + */ +export function getMostCommonRequests(limit: number = 20): RequestStat[] { + return getAllRequestStats() + .sort((a, b) => b.count - a.count) + .slice(0, limit); +} + +/** + * Get requests sorted by total time spent (most time first) + */ +export function getMostTimeConsumingRequests(limit: number = 20): RequestStat[] { + return getAllRequestStats() + .sort((a, b) => b.totalTimeMs - a.totalTimeMs) + .slice(0, limit); +} + +/** + * Get requests sorted by average time (slowest first) + */ +export function getSlowestRequests(limit: number = 20): RequestStat[] { + return getAllRequestStats() + .sort((a, b) => (b.totalTimeMs / b.count) - (a.totalTimeMs / a.count)) + .slice(0, limit); +} + +/** + * Get aggregate stats + */ +export function getAggregateStats() { + const stats = getAllRequestStats(); + const totalRequests = stats.reduce((sum, s) => sum + s.count, 0); + const totalTimeMs = stats.reduce((sum, s) => sum + s.totalTimeMs, 0); + const uniqueEndpoints = stats.length; + + return { + totalRequests, + totalTimeMs, + uniqueEndpoints, + averageTimeMs: totalRequests > 0 ? totalTimeMs / totalRequests : 0, + }; +} + +/** + * Clear all stats + */ +export function clearRequestStats(): void { + requestStatsMap.clear(); +} + diff --git a/apps/backend/src/proxy.tsx b/apps/backend/src/proxy.tsx index 20352bad74..c2ce74b827 100644 --- a/apps/backend/src/proxy.tsx +++ b/apps/backend/src/proxy.tsx @@ -8,6 +8,7 @@ import './polyfills'; import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; import { SmartRouter } from './smart-router'; +import { endRequest, startRequest } from './lib/dev-request-stats'; const DEV_RATE_LIMIT_MAX_REQUESTS = 100; const DEV_RATE_LIMIT_WINDOW_MS = 10_000; @@ -55,6 +56,19 @@ const corsAllowedResponseHeaders = [ // This function can be marked `async` if using `await` inside export async function proxy(request: NextRequest) { + const url = new URL(request.url); + + // Start tracking the request in development mode + const requestId = startRequest(request.method, url.pathname); + + try { + return await proxyInner(request, url); + } finally { + endRequest(requestId); + } +} + +async function proxyInner(request: NextRequest, url: URL) { const delay = +getEnvVariable('STACK_ARTIFICIAL_DEVELOPMENT_DELAY_MS', '0'); if (delay) { if (getNodeEnvironment().includes('production')) { @@ -64,8 +78,6 @@ export async function proxy(request: NextRequest) { await wait(delay); } } - - const url = new URL(request.url); const isApiRequest = url.pathname.startsWith('/api/'); const corsHeadersInit = isApiRequest ? { From 8514363ae7a7d15ca9a124d97d84ad765127667a Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Mon, 29 Dec 2025 17:15:08 +0100 Subject: [PATCH 6/9] Fix dev stats accuracy --- apps/backend/src/lib/dev-request-stats.tsx | 45 ++++--------------- apps/backend/src/proxy.tsx | 13 ------ .../route-handlers/smart-route-handler.tsx | 5 +++ 3 files changed, 13 insertions(+), 50 deletions(-) diff --git a/apps/backend/src/lib/dev-request-stats.tsx b/apps/backend/src/lib/dev-request-stats.tsx index 5de479d13b..dad19ea9c1 100644 --- a/apps/backend/src/lib/dev-request-stats.tsx +++ b/apps/backend/src/lib/dev-request-stats.tsx @@ -11,52 +11,23 @@ export type RequestStat = { }; // In-memory storage for request stats (only used in development) -const requestStatsMap = (globalThis as any).requestStatsMap ??= new Map(); - -// Ongoing requests that haven't finished yet -const ongoingRequests = (globalThis as any).ongoingRequests ??= new Map(); - -let requestIdCounter = 0; +// Use globalThis to persist across hot reloads +const requestStatsMap: Map = (globalThis as any).__devRequestStatsMap ??= new Map(); function getKey(method: string, path: string): string { return `${method}:${path}`; } /** - * Call this at the start of a request. Returns a unique ID that should be passed to `endRequest`. + * Record stats for a completed request. + * Only records in development mode. */ -export function startRequest(method: string, path: string): string | null { +export function recordRequestStats(method: string, path: string, durationMs: number): void { if (getNodeEnvironment() !== "development") { - return null; - } - - const requestId = `req-${++requestIdCounter}`; - ongoingRequests.set(requestId, { - startTime: performance.now(), - method, - path, - }); - - return requestId; -} - -/** - * Call this at the end of a request with the ID from `startRequest`. - */ -export function endRequest(requestId: string | null): void { - if (requestId === null || getNodeEnvironment() !== "development") { - return; - } - - const ongoing = ongoingRequests.get(requestId); - if (!ongoing) { return; } - ongoingRequests.delete(requestId); - - const durationMs = performance.now() - ongoing.startTime; - const key = getKey(ongoing.method, ongoing.path); + const key = getKey(method, path); const existing = requestStatsMap.get(key); if (existing) { @@ -67,8 +38,8 @@ export function endRequest(requestId: string | null): void { existing.lastCalledAt = Date.now(); } else { requestStatsMap.set(key, { - method: ongoing.method, - path: ongoing.path, + method, + path, count: 1, totalTimeMs: durationMs, minTimeMs: durationMs, diff --git a/apps/backend/src/proxy.tsx b/apps/backend/src/proxy.tsx index c2ce74b827..a413d29524 100644 --- a/apps/backend/src/proxy.tsx +++ b/apps/backend/src/proxy.tsx @@ -8,7 +8,6 @@ import './polyfills'; import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; import { SmartRouter } from './smart-router'; -import { endRequest, startRequest } from './lib/dev-request-stats'; const DEV_RATE_LIMIT_MAX_REQUESTS = 100; const DEV_RATE_LIMIT_WINDOW_MS = 10_000; @@ -57,18 +56,6 @@ const corsAllowedResponseHeaders = [ // This function can be marked `async` if using `await` inside export async function proxy(request: NextRequest) { const url = new URL(request.url); - - // Start tracking the request in development mode - const requestId = startRequest(request.method, url.pathname); - - try { - return await proxyInner(request, url); - } finally { - endRequest(requestId); - } -} - -async function proxyInner(request: NextRequest, url: URL) { const delay = +getEnvVariable('STACK_ARTIFICIAL_DEVELOPMENT_DELAY_MS', '0'); if (delay) { if (getNodeEnvironment().includes('production')) { diff --git a/apps/backend/src/route-handlers/smart-route-handler.tsx b/apps/backend/src/route-handlers/smart-route-handler.tsx index c8351964b6..76be22d611 100644 --- a/apps/backend/src/route-handlers/smart-route-handler.tsx +++ b/apps/backend/src/route-handlers/smart-route-handler.tsx @@ -10,6 +10,7 @@ import { runAsynchronously, wait } from "@stackframe/stack-shared/dist/utils/pro import { traceSpan } from "@stackframe/stack-shared/dist/utils/telemetry"; import { NextRequest } from "next/server"; import * as yup from "yup"; +import { recordRequestStats } from "@/lib/dev-request-stats"; import { DeepPartialSmartRequestWithSentinel, MergeSmartRequest, SmartRequest, createSmartRequest, validateSmartRequest } from "./smart-request"; import { SmartResponse, createResponse, validateSmartResponse } from "./smart-response"; @@ -116,6 +117,10 @@ export function handleApiRequest(handler: (req: NextRequest, options: any, reque const timeStart = performance.now(); const res = await handler(req, options, requestId); const time = (performance.now() - timeStart); + + // Record request stats for dev-stats page + recordRequestStats(req.method, req.nextUrl.pathname, time); + if ([301, 302].includes(res.status)) { throw new StackAssertionError("HTTP status codes 301 and 302 should not be returned by our APIs because the behavior for non-GET methods is inconsistent across implementations. Use 303 (to rewrite method to GET) or 307/308 (to preserve the original method and data) instead.", { status: res.status, url: req.nextUrl, req, res }); } From 48ac44e771405a6e6058a7620b50fc45c78012da Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Mon, 29 Dec 2025 17:17:36 +0100 Subject: [PATCH 7/9] fix tests --- apps/e2e/tests/backend/backend-helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/e2e/tests/backend/backend-helpers.ts b/apps/e2e/tests/backend/backend-helpers.ts index 79c1c43d31..4fef8ee6d5 100644 --- a/apps/e2e/tests/backend/backend-helpers.ts +++ b/apps/e2e/tests/backend/backend-helpers.ts @@ -1218,7 +1218,7 @@ export namespace Project { method: "PATCH", body: { config_override_string: JSON.stringify(config) }, }); - expect(response.body).toMatchInlineSnapshot(`{}`); + expect(response.body).toMatchInlineSnapshot(`{ "success": true }`); expect(response.status).toBe(200); } } From e6998cca8a722a05964e4f33627bc20806067dd7 Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Tue, 30 Dec 2025 03:52:28 +0100 Subject: [PATCH 8/9] Add more indexes --- .../migration.sql | 24 +++++++++++++++++++ apps/backend/prisma/schema.prisma | 3 +++ .../backend/endpoints/api/v1/projects.test.ts | 2 +- 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 apps/backend/prisma/migrations/20251230020000_email_outbox_partial_indices/migration.sql diff --git a/apps/backend/prisma/migrations/20251230020000_email_outbox_partial_indices/migration.sql b/apps/backend/prisma/migrations/20251230020000_email_outbox_partial_indices/migration.sql new file mode 100644 index 0000000000..928a4f66de --- /dev/null +++ b/apps/backend/prisma/migrations/20251230020000_email_outbox_partial_indices/migration.sql @@ -0,0 +1,24 @@ +-- CreateIndex +-- Partial index for emails currently being rendered (finishedRenderingAt is NULL) +-- Indexed by startedRenderingAt to efficiently query in-progress rendering jobs +CREATE INDEX "EmailOutbox_rendering_in_progress_idx" + ON "EmailOutbox" ("startedRenderingAt") + WHERE "finishedRenderingAt" IS NULL; + +-- CreateIndex +-- Partial index for emails currently being sent (finishedSendingAt is NULL) +-- Indexed by startedSendingAt to efficiently query in-progress sending jobs +CREATE INDEX "EmailOutbox_sending_in_progress_idx" + ON "EmailOutbox" ("startedSendingAt") + WHERE "finishedSendingAt" IS NULL; + +-- CreateIndex +-- Index for looking up team members by user and selection status +CREATE INDEX "TeamMember_projectUserId_isSelected_idx" + ON "TeamMember" ("tenancyId", "projectUserId", "isSelected"); + +-- CreateIndex +-- Index for looking up projects by owner team +CREATE INDEX "Project_ownerTeamId_idx" + ON "Project" ("ownerTeamId"); + diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index 856be2c75b..aed17ea5c4 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -40,6 +40,8 @@ model Project { provisionedProject ProvisionedProject? tenancies Tenancy[] environmentConfigOverrides EnvironmentConfigOverride[] + + @@index([ownerTeamId], map: "Project_ownerTeamId_idx") } model Tenancy { @@ -127,6 +129,7 @@ model TeamMember { @@id([tenancyId, projectUserId, teamId]) @@unique([tenancyId, projectUserId, isSelected]) + @@index([tenancyId, projectUserId, isSelected], map: "TeamMember_projectUserId_isSelected_idx") } model ProjectUserDirectPermission { diff --git a/apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts index 3ee7397b8e..32330ddd1f 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts @@ -1588,7 +1588,7 @@ it("should preserve API Keys app enabled state when updating allowUserApiKeys co expect(enableAppResponse).toMatchInlineSnapshot(` NiceResponse { "status": 200, - "body": {}, + "body": { "success": true }, "headers": Headers {