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 a63dd4c4ad..81b1835fbd 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:watch": "pnpm run prisma generate --watch", + "codegen-prisma": "STACK_DATABASE_CONNECTION_STRING=\"${STACK_DATABASE_CONNECTION_STRING:-placeholder-database-connection-string}\" pnpm run prisma generate", + "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-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/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/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/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..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,3 +1,56 @@ -import { configOverridesCrudHandlers } from "./crud"; +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"; +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(["success"]).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, + }); + } + + return { + statusCode: 200, + bodyType: "success", + }; + }, +}); 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/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..dad19ea9c1 --- /dev/null +++ b/apps/backend/src/lib/dev-request-stats.tsx @@ -0,0 +1,109 @@ +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) +// 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}`; +} + +/** + * Record stats for a completed request. + * Only records in development mode. + */ +export function recordRequestStats(method: string, path: string, durationMs: number): void { + if (getNodeEnvironment() !== "development") { + return; + } + + const key = getKey(method, 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, + 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..a413d29524 100644 --- a/apps/backend/src/proxy.tsx +++ b/apps/backend/src/proxy.tsx @@ -55,6 +55,7 @@ const corsAllowedResponseHeaders = [ // This function can be marked `async` if using `await` inside export async function proxy(request: NextRequest) { + const url = new URL(request.url); const delay = +getEnvVariable('STACK_ARTIFICIAL_DEVELOPMENT_DELAY_MS', '0'); if (delay) { if (getNodeEnvironment().includes('production')) { @@ -64,8 +65,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 ? { 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 }); } diff --git a/apps/e2e/tests/backend/backend-helpers.ts b/apps/e2e/tests/backend/backend-helpers.ts index a2ff1140b9..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); } } @@ -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..32330ddd1f 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 @@ -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 {