diff --git a/.github/workflows/e2e-api-tests.yaml b/.github/workflows/e2e-api-tests.yaml index a3ad8d9a7..a0a20357e 100644 --- a/.github/workflows/e2e-api-tests.yaml +++ b/.github/workflows/e2e-api-tests.yaml @@ -30,45 +30,31 @@ jobs: uses: pnpm/action-setup@v3 with: version: 9.1.2 - - - name: Create .env.local file for stack-server - run: | - cat > packages/stack-server/.env.local < + withSentryConfig( + nextConfig, + { + // For all available options, see: + // https://github.com/getsentry/sentry-webpack-plugin#options + + // Suppresses source map uploading logs during build + silent: true, + org: "stackframe-pw", + project: "stack-api", + }, + { + // For all available options, see: + // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ + + // Upload a larger set of source maps for prettier stack traces (increases build time) + widenClientFileUpload: true, + + // Transpiles SDK to be compatible with IE11 (increases bundle size) + transpileClientSDK: true, + + // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. + // This can increase your server load as well as your hosting bill. + // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- + // side errors will fail. + tunnelRoute: "/monitoring", + + // Hides source maps from generated client bundles + hideSourceMaps: true, + + // Automatically tree-shake Sentry logger statements to reduce bundle size + disableLogger: true, + + // Enables automatic instrumentation of Vercel Cron Monitors. + // See the following for more information: + // https://docs.sentry.io/product/crons/ + // https://vercel.com/docs/cron-jobs + automaticVercelMonitors: true, + } + ); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + // we're open-source, so we can provide source maps + productionBrowserSourceMaps: true, + poweredByHeader: false, + + async headers() { + return [ + { + source: "/(.*)", + headers: [ + { + key: "Cross-Origin-Opener-Policy", + value: "same-origin", + }, + { + key: "Permissions-Policy", + value: "", + }, + { + key: "Referrer-Policy", + value: "strict-origin-when-cross-origin", + }, + { + key: "X-Content-Type-Options", + value: "nosniff", + }, + { + key: "X-Frame-Options", + value: "SAMEORIGIN", + }, + { + key: "Content-Security-Policy", + value: "", + }, + ], + }, + ]; + }, +}; + +export default withConfiguredSentryConfig( + withBundleAnalyzer( + nextConfig + ) +); diff --git a/apps/backend/package.json b/apps/backend/package.json new file mode 100644 index 000000000..1916889ce --- /dev/null +++ b/apps/backend/package.json @@ -0,0 +1,66 @@ +{ + "name": "@stackframe/stack-backend", + "version": "2.4.26", + "private": true, + "scripts": { + "clean": "rimraf .next && rimraf node_modules", + "typecheck": "tsc --noEmit", + "with-env": "dotenv -c development --", + "with-env:prod": "dotenv -c --", + "dev": "concurrently \"next dev --port 8102\" \"npm run watch-docs\"", + "build": "npm run codegen && next build", + "analyze-bundle": "ANALYZE_BUNDLE=1 npm run build", + "start": "next start --port 8102", + "codegen": "npm run prisma -- generate && npm run generate-docs", + "psql": "npm run with-env -- bash -c 'psql $DATABASE_CONNECTION_STRING'", + "prisma": "npm run with-env -- prisma", + "lint": "next lint", + "watch-docs": "npm run with-env -- chokidar --silent '../../**/*' -i '../../docs/**' -i '../../**/node_modules/**' -i '../../**/.next/**' -i '../../**/dist/**' -c 'tsx scripts/generate-docs.ts'", + "generate-docs": "npm run with-env -- tsx scripts/generate-docs.ts", + "generate-keys": "npm run with-env -- tsx scripts/generate-keys.ts" + }, + "prisma": { + "seed": "npm run with-env -- tsx prisma/seed.ts" + }, + "dependencies": { + "@hookform/resolvers": "^3.3.4", + "@next/bundle-analyzer": "^14.0.3", + "@node-oauth/oauth2-server": "^5.1.0", + "@prisma/client": "^5.9.1", + "@react-email/components": "^0.0.14", + "@react-email/render": "^0.0.12", + "@react-email/tailwind": "^0.0.14", + "@sentry/nextjs": "^7.105.0", + "@stackframe/stack-shared": "workspace:*", + "@vercel/analytics": "^1.2.2", + "bcrypt": "^5.1.1", + "date-fns": "^3.6.0", + "dotenv-cli": "^7.3.0", + "handlebars": "^4.7.8", + "jose": "^5.2.2", + "lodash": "^4.17.21", + "next": "^14.1", + "nodemailer": "^6.9.10", + "openid-client": "^5.6.4", + "pg": "^8.11.3", + "posthog-js": "^1.138.1", + "prettier": "^3.2.5", + "react": "^18.2", + "react-email": "2.1.0", + "server-only": "^0.0.1", + "sharp": "^0.32.6", + "yaml": "^2.4.5", + "yup": "^1.4.0" + }, + "devDependencies": { + "@types/bcrypt": "^5.0.2", + "@types/lodash": "^4.17.4", + "@types/node": "^20.8.10", + "@types/nodemailer": "^6.4.14", + "@types/react": "^18.2.66", + "prisma": "^5.9.1", + "rimraf": "^5.0.5", + "tsx": "^4.7.2", + "glob": "^10.4.1" + } +} diff --git a/packages/stack-server/prisma/migrations/20240306152532_initial_migration/migration.sql b/apps/backend/prisma/migrations/20240306152532_initial_migration/migration.sql similarity index 100% rename from packages/stack-server/prisma/migrations/20240306152532_initial_migration/migration.sql rename to apps/backend/prisma/migrations/20240306152532_initial_migration/migration.sql diff --git a/packages/stack-server/prisma/migrations/20240313024014_authroization_code_new_user/migration.sql b/apps/backend/prisma/migrations/20240313024014_authroization_code_new_user/migration.sql similarity index 100% rename from packages/stack-server/prisma/migrations/20240313024014_authroization_code_new_user/migration.sql rename to apps/backend/prisma/migrations/20240313024014_authroization_code_new_user/migration.sql diff --git a/packages/stack-server/prisma/migrations/20240418090527_magic_link/migration.sql b/apps/backend/prisma/migrations/20240418090527_magic_link/migration.sql similarity index 100% rename from packages/stack-server/prisma/migrations/20240418090527_magic_link/migration.sql rename to apps/backend/prisma/migrations/20240418090527_magic_link/migration.sql diff --git a/packages/stack-server/prisma/migrations/20240507195652_team/migration.sql b/apps/backend/prisma/migrations/20240507195652_team/migration.sql similarity index 100% rename from packages/stack-server/prisma/migrations/20240507195652_team/migration.sql rename to apps/backend/prisma/migrations/20240507195652_team/migration.sql diff --git a/packages/stack-server/prisma/migrations/20240518151916_email_config/migration.sql b/apps/backend/prisma/migrations/20240518151916_email_config/migration.sql similarity index 100% rename from packages/stack-server/prisma/migrations/20240518151916_email_config/migration.sql rename to apps/backend/prisma/migrations/20240518151916_email_config/migration.sql diff --git a/packages/stack-server/prisma/migrations/20240520152704_selected_team/migration.sql b/apps/backend/prisma/migrations/20240520152704_selected_team/migration.sql similarity index 100% rename from packages/stack-server/prisma/migrations/20240520152704_selected_team/migration.sql rename to apps/backend/prisma/migrations/20240520152704_selected_team/migration.sql diff --git a/packages/stack-server/prisma/migrations/20240528090210_email_templates/migration.sql b/apps/backend/prisma/migrations/20240528090210_email_templates/migration.sql similarity index 100% rename from packages/stack-server/prisma/migrations/20240528090210_email_templates/migration.sql rename to apps/backend/prisma/migrations/20240528090210_email_templates/migration.sql diff --git a/packages/stack-server/prisma/migrations/20240529121811_spotify_oauth/migration.sql b/apps/backend/prisma/migrations/20240529121811_spotify_oauth/migration.sql similarity index 100% rename from packages/stack-server/prisma/migrations/20240529121811_spotify_oauth/migration.sql rename to apps/backend/prisma/migrations/20240529121811_spotify_oauth/migration.sql diff --git a/packages/stack-server/prisma/migrations/20240608142105_oauth_access_token/migration.sql b/apps/backend/prisma/migrations/20240608142105_oauth_access_token/migration.sql similarity index 100% rename from packages/stack-server/prisma/migrations/20240608142105_oauth_access_token/migration.sql rename to apps/backend/prisma/migrations/20240608142105_oauth_access_token/migration.sql diff --git a/packages/stack-server/prisma/migrations/20240610085756_outer_oauth_info/migration.sql b/apps/backend/prisma/migrations/20240610085756_outer_oauth_info/migration.sql similarity index 100% rename from packages/stack-server/prisma/migrations/20240610085756_outer_oauth_info/migration.sql rename to apps/backend/prisma/migrations/20240610085756_outer_oauth_info/migration.sql diff --git a/packages/stack-server/prisma/migrations/migration_lock.toml b/apps/backend/prisma/migrations/migration_lock.toml similarity index 100% rename from packages/stack-server/prisma/migrations/migration_lock.toml rename to apps/backend/prisma/migrations/migration_lock.toml diff --git a/packages/stack-server/prisma/schema.prisma b/apps/backend/prisma/schema.prisma similarity index 100% rename from packages/stack-server/prisma/schema.prisma rename to apps/backend/prisma/schema.prisma diff --git a/apps/backend/prisma/seed.ts b/apps/backend/prisma/seed.ts new file mode 100644 index 000000000..97207193d --- /dev/null +++ b/apps/backend/prisma/seed.ts @@ -0,0 +1,77 @@ +import { PrismaClient } from '@prisma/client'; +import { getEnvVariable } from '@stackframe/stack-shared/dist/utils/env'; +const prisma = new PrismaClient(); + + +async function seed() { + console.log('Seeding database...'); + + const oldProjects = await prisma.project.findUnique({ + where: { + id: 'internal', + }, + }); + + if (oldProjects) { + console.log('Internal project already exists, skipping seeding'); + return; + } + + await prisma.project.upsert({ + where: { + id: 'internal', + }, + create: { + id: 'internal', + displayName: 'Stack Dashboard', + description: 'Stack\'s admin dashboard', + isProductionMode: false, + apiKeySets: { + create: [{ + description: "Internal API key set", + publishableClientKey: "this-publishable-client-key-is-for-local-development-only", + secretServerKey: "this-secret-server-key-is-for-local-development-only", + expiresAt: new Date('2099-12-31T23:59:59Z'), + }], + }, + config: { + create: { + allowLocalhost: true, + oauthProviderConfigs: { + create: (['github', 'facebook', 'google', 'microsoft'] as const).map((id) => ({ + id, + proxiedOAuthConfig: { + create: { + type: id.toUpperCase() as any, + } + }, + projectUserOAuthAccounts: { + create: [] + } + })), + }, + emailServiceConfig: { + create: { + proxiedEmailServiceConfig: { + create: {} + } + } + }, + credentialEnabled: true, + magicLinkEnabled: true, + createTeamOnSignUp: false, + }, + }, + }, + update: {}, + }); + console.log('Internal project created'); + console.log('Seeding complete!'); +} + +seed().catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); +// eslint-disable-next-line @typescript-eslint/no-misused-promises, @typescript-eslint/return-await +}).finally(async () => await prisma.$disconnect()); diff --git a/packages/stack-server/scripts/generate-docs.ts b/apps/backend/scripts/generate-docs.ts similarity index 100% rename from packages/stack-server/scripts/generate-docs.ts rename to apps/backend/scripts/generate-docs.ts diff --git a/packages/stack-server/scripts/generate-keys.ts b/apps/backend/scripts/generate-keys.ts similarity index 100% rename from packages/stack-server/scripts/generate-keys.ts rename to apps/backend/scripts/generate-keys.ts diff --git a/apps/backend/sentry.client.config.ts b/apps/backend/sentry.client.config.ts new file mode 100644 index 000000000..5030d896d --- /dev/null +++ b/apps/backend/sentry.client.config.ts @@ -0,0 +1,32 @@ +// This file configures the initialization of Sentry on the client. +// The config you add here will be used whenever a users loads a page in their browser. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import * as Sentry from "@sentry/nextjs"; + +Sentry.init({ + dsn: "https://0dc90570e0d280c1b4252ed61a328bfc@o4507084192022528.ingest.us.sentry.io/4507442898272256", + + // Adjust this value in production, or use tracesSampler for greater control + tracesSampleRate: 1, + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, + + enabled: process.env.NODE_ENV !== "development" && !process.env.CI, + + replaysOnErrorSampleRate: 1.0, + + // This sets the sample rate to be 10%. You may want this to be 100% while + // in development and sample at a lower rate in production + replaysSessionSampleRate: 1.0, + + // You can remove this option if you're not planning to use the Sentry Session Replay feature: + integrations: [ + Sentry.replayIntegration({ + // Additional Replay configuration goes in here, for example: + maskAllText: false, + blockAllMedia: false, + }), + ], +}); diff --git a/apps/backend/sentry.edge.config.ts b/apps/backend/sentry.edge.config.ts new file mode 100644 index 000000000..2d15b5a74 --- /dev/null +++ b/apps/backend/sentry.edge.config.ts @@ -0,0 +1,18 @@ +// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on). +// The config you add here will be used whenever one of the edge features is loaded. +// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import * as Sentry from "@sentry/nextjs"; + +Sentry.init({ + dsn: "https://0dc90570e0d280c1b4252ed61a328bfc@o4507084192022528.ingest.us.sentry.io/4507442898272256", + + // Adjust this value in production, or use tracesSampler for greater control + tracesSampleRate: 1, + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, + + enabled: process.env.NODE_ENV !== "development" && !process.env.CI, +}); diff --git a/apps/backend/sentry.server.config.ts b/apps/backend/sentry.server.config.ts new file mode 100644 index 000000000..68a3aef1f --- /dev/null +++ b/apps/backend/sentry.server.config.ts @@ -0,0 +1,17 @@ +// This file configures the initialization of Sentry on the server. +// The config you add here will be used whenever the server handles a request. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import * as Sentry from "@sentry/nextjs"; + +Sentry.init({ + dsn: "https://0dc90570e0d280c1b4252ed61a328bfc@o4507084192022528.ingest.us.sentry.io/4507442898272256", + + // Adjust this value in production, or use tracesSampler for greater control + tracesSampleRate: 1, + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, + + enabled: process.env.NODE_ENV !== "development" && !process.env.CI, +}); diff --git a/packages/stack-server/src/app/api/v1/route.ts b/apps/backend/src/app/api/v1/route.ts similarity index 100% rename from packages/stack-server/src/app/api/v1/route.ts rename to apps/backend/src/app/api/v1/route.ts diff --git a/packages/stack-server/src/app/favicon.ico b/apps/backend/src/app/favicon.ico similarity index 100% rename from packages/stack-server/src/app/favicon.ico rename to apps/backend/src/app/favicon.ico diff --git a/apps/backend/src/app/global-error.tsx b/apps/backend/src/app/global-error.tsx new file mode 100644 index 000000000..edb8a4419 --- /dev/null +++ b/apps/backend/src/app/global-error.tsx @@ -0,0 +1,21 @@ +"use client"; + +import * as Sentry from "@sentry/nextjs"; +import Error from "next/error"; +import { useEffect } from "react"; + +export default function GlobalError({ error }: any) { + useEffect(() => { + Sentry.captureException(error); + }, [error]); + + return ( + + + + + + ); +} diff --git a/apps/backend/src/app/layout.tsx b/apps/backend/src/app/layout.tsx new file mode 100644 index 000000000..1c6bd78ed --- /dev/null +++ b/apps/backend/src/app/layout.tsx @@ -0,0 +1,22 @@ +import '../polyfills'; +import React from 'react'; +import type { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Stack Auth API', + description: 'API endpoint of Stack Auth.', +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode, +}) { + return ( + + + {children} + + + ); +} diff --git a/apps/backend/src/app/not-found.tsx b/apps/backend/src/app/not-found.tsx new file mode 100644 index 000000000..6879ab7de --- /dev/null +++ b/apps/backend/src/app/not-found.tsx @@ -0,0 +1,3 @@ +export default function NotFound() { + return "Not Found"; +} diff --git a/apps/backend/src/app/page.tsx b/apps/backend/src/app/page.tsx new file mode 100644 index 000000000..a0b9bf1c6 --- /dev/null +++ b/apps/backend/src/app/page.tsx @@ -0,0 +1,9 @@ +import Link from "next/link"; + +export default function Home() { + return <> + Welcome to Stack's API endpoint.
+
+ API v1
+ ; +} diff --git a/apps/backend/src/globals.d.ts b/apps/backend/src/globals.d.ts new file mode 100644 index 000000000..e69de29bb diff --git a/apps/backend/src/lib/api-keys.tsx b/apps/backend/src/lib/api-keys.tsx new file mode 100644 index 000000000..e32050247 --- /dev/null +++ b/apps/backend/src/lib/api-keys.tsx @@ -0,0 +1,142 @@ +// TODO remove and replace with CRUD handler + +import * as yup from 'yup'; +import { ApiKeySetFirstViewJson, ApiKeySetJson } from '@stackframe/stack-shared'; +import { ApiKeySet } from '@prisma/client'; +import { generateSecureRandomString } from '@stackframe/stack-shared/dist/utils/crypto'; +import { prismaClient } from '@/prisma-client'; +import { generateUuid } from '@stackframe/stack-shared/dist/utils/uuids'; + +export const publishableClientKeyHeaderSchema = yup.string().matches(/^[a-zA-Z0-9_-]*$/); +export const secretServerKeyHeaderSchema = publishableClientKeyHeaderSchema; +export const superSecretAdminKeyHeaderSchema = secretServerKeyHeaderSchema; + +export async function checkApiKeySet( + ...args: Parameters +): Promise { + const set = await getApiKeySet(...args); + if (!set) return false; + if (set.manuallyRevokedAtMillis) return false; + if (set.expiresAtMillis < Date.now()) return false; + return true; +} + + +export async function getApiKeySet( + projectId: string, + whereOrId: + | string + | { publishableClientKey: string } + | { secretServerKey: string } + | { superSecretAdminKey: string }, +): Promise { + const where = typeof whereOrId === 'string' + ? { + projectId_id: { + projectId, + id: whereOrId, + } + } + : whereOrId; + + const set = await prismaClient.apiKeySet.findUnique({ + where, + }); + + if (!set) { + return null; + } + + return createSummaryFromDbType(set); +} + +export async function listApiKeySets( + projectId: string, +): Promise { + const sets = await prismaClient.apiKeySet.findMany({ + where: { + projectId, + }, + }); + + return sets.map(createSummaryFromDbType); +} + +export async function createApiKeySet( + projectId: string, + description: string, + expiresAt: Date, + hasPublishableClientKey: boolean, + hasSecretServerKey: boolean, + hasSuperSecretAdminKey: boolean, +): Promise { + const set = await prismaClient.apiKeySet.create({ + data: { + id: generateUuid(), + projectId, + description, + expiresAt, + ...hasPublishableClientKey ? { + publishableClientKey: `pck_${generateSecureRandomString()}`, + } : {}, + ...hasSecretServerKey ? { + secretServerKey: `ssk_${generateSecureRandomString()}`, + } : {}, + ...hasSuperSecretAdminKey ? { + superSecretAdminKey: `sak_${generateSecureRandomString()}`, + } : {}, + }, + }); + + return { + id: set.id, + ...set.publishableClientKey ? { + publishableClientKey: set.publishableClientKey, + } : {}, + ...set.secretServerKey ? { + secretServerKey: set.secretServerKey, + } : {}, + ...set.superSecretAdminKey ? { + superSecretAdminKey: set.superSecretAdminKey, + } : {}, + createdAtMillis: set.createdAt.getTime(), + expiresAtMillis: set.expiresAt.getTime(), + description: set.description, + manuallyRevokedAtMillis: set.manuallyRevokedAt?.getTime() ?? null, + }; +} + +export async function revokeApiKeySet(projectId: string, apiKeyId: string) { + const set = await prismaClient.apiKeySet.update({ + where: { + projectId_id: { + projectId, + id: apiKeyId, + }, + }, + data: { + manuallyRevokedAt: new Date(), + }, + }); + + return createSummaryFromDbType(set); +} + +function createSummaryFromDbType(set: ApiKeySet): ApiKeySetJson { + return { + id: set.id, + description: set.description, + publishableClientKey: set.publishableClientKey === null ? null : { + lastFour: set.publishableClientKey.slice(-4), + }, + secretServerKey: set.secretServerKey === null ? null : { + lastFour: set.secretServerKey.slice(-4), + }, + superSecretAdminKey: set.superSecretAdminKey === null ? null : { + lastFour: set.superSecretAdminKey.slice(-4), + }, + createdAtMillis: set.createdAt.getTime(), + expiresAtMillis: set.expiresAt.getTime(), + manuallyRevokedAtMillis: set.manuallyRevokedAt?.getTime() ?? null, + }; +} diff --git a/packages/stack-server/src/lib/openapi.tsx b/apps/backend/src/lib/openapi.tsx similarity index 100% rename from packages/stack-server/src/lib/openapi.tsx rename to apps/backend/src/lib/openapi.tsx diff --git a/apps/backend/src/lib/projects.tsx b/apps/backend/src/lib/projects.tsx new file mode 100644 index 000000000..4ed3d49bf --- /dev/null +++ b/apps/backend/src/lib/projects.tsx @@ -0,0 +1,687 @@ +// TODO remove and replace with CRUD handler + +import * as yup from "yup"; +import { KnownErrors, OAuthProviderConfigJson, ProjectJson, ServerUserJson } from "@stackframe/stack-shared"; +import { Prisma, ProxiedOAuthProviderType, StandardOAuthProviderType } from "@prisma/client"; +import { prismaClient } from "@/prisma-client"; +import { decodeAccessToken } from "./tokens"; +import { getServerUser } from "./users"; +import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids"; +import { EmailConfigJson, SharedProvider, StandardProvider, sharedProviders, standardProviders } from "@stackframe/stack-shared/dist/interface/clientInterface"; +import { OAuthProviderUpdateOptions, ProjectUpdateOptions } from "@stackframe/stack-shared/dist/interface/adminInterface"; +import { StackAssertionError, StatusError, captureError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; + + +function toDBSharedProvider(type: SharedProvider): ProxiedOAuthProviderType { + return ({ + "shared-github": "GITHUB", + "shared-google": "GOOGLE", + "shared-facebook": "FACEBOOK", + "shared-microsoft": "MICROSOFT", + "shared-spotify": "SPOTIFY", + } as const)[type]; +} + +function toDBStandardProvider(type: StandardProvider): StandardOAuthProviderType { + return ({ + "github": "GITHUB", + "facebook": "FACEBOOK", + "google": "GOOGLE", + "microsoft": "MICROSOFT", + "spotify": "SPOTIFY", + } as const)[type]; +} + +function fromDBSharedProvider(type: ProxiedOAuthProviderType): SharedProvider { + return ({ + "GITHUB": "shared-github", + "GOOGLE": "shared-google", + "FACEBOOK": "shared-facebook", + "MICROSOFT": "shared-microsoft", + "SPOTIFY": "shared-spotify", + } as const)[type]; +} + +function fromDBStandardProvider(type: StandardOAuthProviderType): StandardProvider { + return ({ + "GITHUB": "github", + "FACEBOOK": "facebook", + "GOOGLE": "google", + "MICROSOFT": "microsoft", + "SPOTIFY": "spotify", + } as const)[type]; +} + + +export const fullProjectInclude = { + config: { + include: { + oauthProviderConfigs: { + include: { + proxiedOAuthConfig: true, + standardOAuthConfig: true, + }, + }, + emailServiceConfig: { + include: { + proxiedEmailServiceConfig: true, + standardEmailServiceConfig: true, + }, + }, + domains: true, + }, + }, + configOverride: true, + _count: { + select: { + users: true, // Count the users related to the project + }, + }, +} as const satisfies Prisma.ProjectInclude; +type FullProjectInclude = typeof fullProjectInclude; +export type ProjectDB = Prisma.ProjectGetPayload<{ include: FullProjectInclude }> & { + config: { + oauthProviderConfigs: (Prisma.OAuthProviderConfigGetPayload< + typeof fullProjectInclude.config.include.oauthProviderConfigs + >)[], + emailServiceConfig: Prisma.EmailServiceConfigGetPayload< + typeof fullProjectInclude.config.include.emailServiceConfig + > | null, + domains: Prisma.ProjectDomainGetPayload< + typeof fullProjectInclude.config.include.domains + >[], + }, +}; + +export async function whyNotProjectAdmin(projectId: string, adminAccessToken: string): Promise<"unparsable-access-token" | "access-token-expired" | "wrong-project-id" | "not-admin" | null> { + if (!adminAccessToken) { + return "unparsable-access-token"; + } + + let decoded; + try { + decoded = await decodeAccessToken(adminAccessToken); + } catch (error) { + if (error instanceof KnownErrors.AccessTokenExpired) { + return "access-token-expired"; + } + console.warn("Failed to decode a user-provided admin access token. This may not be an error (for example, it could happen if the client changed Stack app hosts), but could indicate one.", error); + return "unparsable-access-token"; + } + const { userId, projectId: accessTokenProjectId } = decoded; + if (accessTokenProjectId !== "internal") { + return "wrong-project-id"; + } + + const projectUser = await getServerUser("internal", userId); + if (!projectUser) { + return "not-admin"; + } + + const allProjects = listProjectIds(projectUser); + if (!allProjects.includes(projectId)) { + return "not-admin"; + } + + return null; +} + +export async function isProjectAdmin(projectId: string, adminAccessToken: string) { + return !await whyNotProjectAdmin(projectId, adminAccessToken); +} + +function listProjectIds(projectUser: ServerUserJson) { + const serverMetadata = projectUser.serverMetadata; + if (typeof serverMetadata !== "object" || !(!serverMetadata || "managedProjectIds" in serverMetadata)) { + throw new StackAssertionError("Invalid server metadata, did something go wrong?", { serverMetadata }); + } + const managedProjectIds = serverMetadata?.managedProjectIds ?? []; + if (!isStringArray(managedProjectIds)) { + throw new StackAssertionError("Invalid server metadata, did something go wrong? Expected string array", { managedProjectIds }); + } + + return managedProjectIds; +} + +export async function listProjects(projectUser: ServerUserJson): Promise { + const managedProjectIds = listProjectIds(projectUser); + + const projects = await prismaClient.project.findMany({ + where: { + id: { + in: managedProjectIds, + }, + }, + include: fullProjectInclude, + }); + + return projects.map(p => projectJsonFromDbType(p)); +} + +export async function createProject( + projectUser: ServerUserJson, + projectOptions: ProjectUpdateOptions & { displayName: string }, +): Promise { + if (projectUser.projectId !== "internal") { + throw new Error("Only internal project users can create projects"); + } + + const project = await prismaClient.$transaction(async (tx) => { + const project = await tx.project.create({ + data: { + id: generateUuid(), + isProductionMode: false, + displayName: projectOptions.displayName, + description: projectOptions.description, + config: { + create: { + allowLocalhost: projectOptions.config?.allowLocalhost ?? true, + credentialEnabled: !!projectOptions.config?.credentialEnabled, + magicLinkEnabled: !!projectOptions.config?.magicLinkEnabled, + createTeamOnSignUp: !!projectOptions.config?.createTeamOnSignUp, + emailServiceConfig: { + create: { + proxiedEmailServiceConfig: { + create: {} + } + } + }, + }, + }, + }, + include: fullProjectInclude, + }); + + const projectUserTx = await tx.projectUser.findUniqueOrThrow({ + where: { + projectId_projectUserId: { + projectId: "internal", + projectUserId: projectUser.id, + }, + }, + }); + + const serverMetadataTx: any = projectUserTx.serverMetadata ?? {}; + + await tx.projectUser.update({ + where: { + projectId_projectUserId: { + projectId: "internal", + projectUserId: projectUserTx.projectUserId, + }, + }, + data: { + serverMetadata: { + ...serverMetadataTx ?? {}, + managedProjectIds: [ + ...serverMetadataTx?.managedProjectIds ?? [], + project.id, + ], + }, + }, + }); + + return project; + }); + + const updatedProject = await updateProject(project.id, projectOptions); + + if (!updatedProject) { + throw new Error("Failed to update project after creation"); + } + + return updatedProject; +} + +export async function getProject(projectId: string): Promise { + return await updateProject(projectId, {}); +} + +async function _createOAuthConfigUpdateTransactions( + projectId: string, + options: ProjectUpdateOptions +) { + const project = await prismaClient.project.findUnique({ + where: { id: projectId }, + include: fullProjectInclude, + }); + + if (!project) { + throw new Error(`Project with id '${projectId}' not found`); + } + + const transactions = []; + const oauthProvidersUpdate = options.config?.oauthProviders; + if (!oauthProvidersUpdate) { + return []; + } + const oldProviders = project.config.oauthProviderConfigs; + const providerMap = new Map(oldProviders.map((provider) => [ + provider.id, + { + providerUpdate: oauthProvidersUpdate.find((p) => p.id === provider.id) ?? throwErr(`Missing provider update for provider '${provider.id}'`), + oldProvider: provider, + } + ])); + + const newProviders = oauthProvidersUpdate.map((providerUpdate) => ({ + id: providerUpdate.id, + update: providerUpdate + })).filter(({ id }) => !providerMap.has(id)); + + // Update existing proxied/standard providers + for (const [id, { providerUpdate, oldProvider }] of providerMap) { + // remove existing provider configs + if (oldProvider.proxiedOAuthConfig) { + transactions.push(prismaClient.proxiedOAuthProviderConfig.delete({ + where: { projectConfigId_id: { projectConfigId: project.config.id, id } }, + })); + } + + if (oldProvider.standardOAuthConfig) { + transactions.push(prismaClient.standardOAuthProviderConfig.delete({ + where: { projectConfigId_id: { projectConfigId: project.config.id, id } }, + })); + } + + // update provider configs with newly created proxied/standard provider configs + let providerConfigUpdate; + if (sharedProviders.includes(providerUpdate.type as SharedProvider)) { + providerConfigUpdate = { + proxiedOAuthConfig: { + create: { + type: toDBSharedProvider(providerUpdate.type as SharedProvider), + }, + }, + }; + + } else if (standardProviders.includes(providerUpdate.type as StandardProvider)) { + const typedProviderConfig = providerUpdate as OAuthProviderUpdateOptions & { type: StandardProvider }; + providerConfigUpdate = { + standardOAuthConfig: { + create: { + type: toDBStandardProvider(providerUpdate.type as StandardProvider), + clientId: typedProviderConfig.clientId, + clientSecret: typedProviderConfig.clientSecret, + }, + }, + }; + } else { + throw new StackAssertionError(`Invalid provider type '${providerUpdate.type}'`, { providerUpdate }); + } + + transactions.push(prismaClient.oAuthProviderConfig.update({ + where: { projectConfigId_id: { projectConfigId: project.config.id, id } }, + data: { + enabled: providerUpdate.enabled, + ...providerConfigUpdate, + }, + })); + } + + // Create new providers + for (const provider of newProviders) { + let providerConfigData; + if (sharedProviders.includes(provider.update.type as SharedProvider)) { + providerConfigData = { + proxiedOAuthConfig: { + create: { + type: toDBSharedProvider(provider.update.type as SharedProvider), + }, + }, + }; + } else if (standardProviders.includes(provider.update.type as StandardProvider)) { + const typedProviderConfig = provider.update as OAuthProviderUpdateOptions & { type: StandardProvider }; + + providerConfigData = { + standardOAuthConfig: { + create: { + type: toDBStandardProvider(provider.update.type as StandardProvider), + clientId: typedProviderConfig.clientId, + clientSecret: typedProviderConfig.clientSecret, + }, + }, + }; + } else { + throw new StackAssertionError(`Invalid provider type '${provider.update.type}'`, { provider }); + } + + transactions.push(prismaClient.oAuthProviderConfig.create({ + data: { + id: provider.id, + projectConfigId: project.config.id, + enabled: provider.update.enabled, + ...providerConfigData, + }, + })); + } + return transactions; +} + +async function _createEmailConfigUpdateTransactions( + projectId: string, + options: ProjectUpdateOptions +) { + const project = await prismaClient.project.findUnique({ + where: { id: projectId }, + include: fullProjectInclude, + }); + + if (!project) { + throw new Error(`Project with id '${projectId}' not found`); + } + + const transactions = []; + const emailConfig = options.config?.emailConfig; + if (!emailConfig) { + return []; + } + + let emailServiceConfig = project.config.emailServiceConfig; + if (!emailServiceConfig) { + emailServiceConfig = await prismaClient.emailServiceConfig.create({ + data: { + projectConfigId: project.config.id, + }, + include: { + proxiedEmailServiceConfig: true, + standardEmailServiceConfig: true, + }, + }); + } + + if (emailServiceConfig.proxiedEmailServiceConfig) { + transactions.push(prismaClient.proxiedEmailServiceConfig.delete({ + where: { projectConfigId: project.config.id }, + })); + } + + if (emailServiceConfig.standardEmailServiceConfig) { + transactions.push(prismaClient.standardEmailServiceConfig.delete({ + where: { projectConfigId: project.config.id }, + })); + } + + switch (emailConfig.type) { + case "shared": { + transactions.push(prismaClient.proxiedEmailServiceConfig.create({ + data: { + projectConfigId: project.config.id, + }, + })); + break; + } + case "standard": { + transactions.push(prismaClient.standardEmailServiceConfig.create({ + data: { + projectConfigId: project.config.id, + host: emailConfig.host, + port: emailConfig.port, + username: emailConfig.username, + password: emailConfig.password, + senderEmail: emailConfig.senderEmail, + senderName: emailConfig.senderName, + }, + })); + break; + } + } + + return transactions; +} + +export async function updateProject( + projectId: string, + options: ProjectUpdateOptions, +): Promise { + // TODO: Validate production mode consistency + const transaction = []; + + const project = await prismaClient.project.findUnique({ + where: { id: projectId }, + include: fullProjectInclude, + }); + + if (!project) { + return null; + } + + if (options.config?.domains) { + const newDomains = options.config.domains; + + // delete existing domains + transaction.push(prismaClient.projectDomain.deleteMany({ + where: { projectConfigId: project.config.id }, + })); + + // create new domains + newDomains.forEach(domainConfig => { + transaction.push(prismaClient.projectDomain.create({ + data: { + projectConfigId: project.config.id, + domain: domainConfig.domain, + handlerPath: domainConfig.handlerPath, + }, + })); + }); + } + + transaction.push(...(await _createOAuthConfigUpdateTransactions(projectId, options))); + transaction.push(...(await _createEmailConfigUpdateTransactions(projectId, options))); + + transaction.push(prismaClient.projectConfig.update({ + where: { id: project.config.id }, + data: { + credentialEnabled: options.config?.credentialEnabled, + magicLinkEnabled: options.config?.magicLinkEnabled, + allowLocalhost: options.config?.allowLocalhost, + createTeamOnSignUp: options.config?.createTeamOnSignUp, + }, + })); + + transaction.push(prismaClient.project.update({ + where: { id: projectId }, + data: { + displayName: options.displayName, + description: options.description, + isProductionMode: options.isProductionMode + }, + })); + + await prismaClient.$transaction(transaction); + + const updatedProject = await prismaClient.project.findUnique({ + where: { id: projectId }, + include: fullProjectInclude, // Ensure you have defined this include object correctly elsewhere + }); + + if (!updatedProject) { + return null; + } + + return projectJsonFromDbType(updatedProject); +} + +export function projectJsonFromDbType(project: ProjectDB): ProjectJson { + let emailConfig: EmailConfigJson | undefined; + const emailServiceConfig = project.config.emailServiceConfig; + if (emailServiceConfig) { + if (emailServiceConfig.proxiedEmailServiceConfig) { + emailConfig = { + type: "shared", + }; + } + if (emailServiceConfig.standardEmailServiceConfig) { + const standardEmailConfig = emailServiceConfig.standardEmailServiceConfig; + emailConfig = { + type: "standard", + host: standardEmailConfig.host, + port: standardEmailConfig.port, + username: standardEmailConfig.username, + password: standardEmailConfig.password, + senderEmail: standardEmailConfig.senderEmail, + senderName: standardEmailConfig.senderName, + }; + } + } + return { + id: project.id, + displayName: project.displayName, + description: project.description ?? undefined, + createdAtMillis: project.createdAt.getTime(), + userCount: project._count.users, + isProductionMode: project.isProductionMode, + evaluatedConfig: { + id: project.config.id, + allowLocalhost: project.config.allowLocalhost, + credentialEnabled: project.config.credentialEnabled, + magicLinkEnabled: project.config.magicLinkEnabled, + createTeamOnSignUp: project.config.createTeamOnSignUp, + domains: project.config.domains.map((domain) => ({ + domain: domain.domain, + handlerPath: domain.handlerPath, + })), + oauthProviders: project.config.oauthProviderConfigs.flatMap((provider): OAuthProviderConfigJson[] => { + if (provider.proxiedOAuthConfig) { + return [{ + id: provider.id, + enabled: provider.enabled, + type: fromDBSharedProvider(provider.proxiedOAuthConfig.type), + }]; + } + if (provider.standardOAuthConfig) { + return [{ + id: provider.id, + enabled: provider.enabled, + type: fromDBStandardProvider(provider.standardOAuthConfig.type), + clientId: provider.standardOAuthConfig.clientId, + clientSecret: provider.standardOAuthConfig.clientSecret, + }]; + } + captureError("projectJsonFromDbType", new StackAssertionError(`Exactly one of the provider configs should be set on provider config '${provider.id}' of project '${project.id}'. Ignoring it`, { project })); + return []; + }), + emailConfig, + }, + }; +} + +function isStringArray(value: any): value is string[] { + return Array.isArray(value) && value.every((id) => typeof id === "string"); +} + +function requiredWhenShared(schema: S): S { + return schema.when('shared', { + is: 'false', + then: (schema: S) => schema.required(), + otherwise: (schema: S) => schema.optional() + }); +} + +const nonRequiredSchemas = { + description: yup.string().optional(), + isProductionMode: yup.boolean().optional(), + config: yup.object({ + domains: yup.array(yup.object({ + domain: yup.string().required(), + handlerPath: yup.string().required(), + })).optional().default(undefined), + oauthProviders: yup.array( + yup.object({ + id: yup.string().required(), + enabled: yup.boolean().required(), + type: yup.string().required(), + clientId: yup.string().optional(), + clientSecret: yup.string().optional(), + }) + ).optional().default(undefined), + credentialEnabled: yup.boolean().optional(), + magicLinkEnabled: yup.boolean().optional(), + allowLocalhost: yup.boolean().optional(), + createTeamOnSignUp: yup.boolean().optional(), + emailConfig: yup.object({ + type: yup.string().oneOf(["shared", "standard"]).required(), + senderName: requiredWhenShared(yup.string()), + host: requiredWhenShared(yup.string()), + port: requiredWhenShared(yup.number()), + username: requiredWhenShared(yup.string()), + password: requiredWhenShared(yup.string()), + senderEmail: requiredWhenShared(yup.string().email()), + }).optional().default(undefined), + }).optional().default(undefined), +}; + +export const getProjectUpdateSchema = () => yup.object({ + displayName: yup.string().optional(), + ...nonRequiredSchemas, +}); + +export const getProjectCreateSchema = () => yup.object({ + displayName: yup.string().required(), + ...nonRequiredSchemas, +}); + +export const projectSchemaToUpdateOptions = ( + update: yup.InferType> +): ProjectUpdateOptions => { + return { + displayName: update.displayName, + description: update.description, + isProductionMode: update.isProductionMode, + config: update.config && { + domains: update.config.domains, + allowLocalhost: update.config.allowLocalhost, + credentialEnabled: update.config.credentialEnabled, + magicLinkEnabled: update.config.magicLinkEnabled, + createTeamOnSignUp: update.config.createTeamOnSignUp, + oauthProviders: update.config.oauthProviders && update.config.oauthProviders.map((provider) => { + if (sharedProviders.includes(provider.type as SharedProvider)) { + return { + id: provider.id, + enabled: provider.enabled, + type: provider.type as SharedProvider, + }; + } else if (standardProviders.includes(provider.type as StandardProvider)) { + if (!provider.clientId) { + throw new StatusError(StatusError.BadRequest, "Missing clientId"); + } + if (!provider.clientSecret) { + throw new StatusError(StatusError.BadRequest, "Missing clientSecret"); + } + + return { + id: provider.id, + enabled: provider.enabled, + type: provider.type as StandardProvider, + clientId: provider.clientId, + clientSecret: provider.clientSecret, + }; + } else { + throw new StatusError(StatusError.BadRequest, "Invalid oauth provider type"); + } + }), + emailConfig: update.config.emailConfig && ( + update.config.emailConfig.type === "shared" ? { + type: update.config.emailConfig.type, + } : { + type: update.config.emailConfig.type, + senderName: update.config.emailConfig.senderName!, + host: update.config.emailConfig.host!, + port: update.config.emailConfig.port!, + username: update.config.emailConfig.username!, + password: update.config.emailConfig.password!, + senderEmail: update.config.emailConfig.senderEmail!, + } + ), + }, + }; +}; + +export const projectSchemaToCreateOptions = ( + create: yup.InferType> +): ProjectUpdateOptions & { displayName: string } => { + return { + ...projectSchemaToUpdateOptions(create), + displayName: create.displayName, + }; +}; diff --git a/apps/backend/src/lib/redirect-urls.tsx b/apps/backend/src/lib/redirect-urls.tsx new file mode 100644 index 000000000..ae6c856ad --- /dev/null +++ b/apps/backend/src/lib/redirect-urls.tsx @@ -0,0 +1,16 @@ +import { DomainConfigJson } from "@stackframe/stack-shared/dist/interface/clientInterface"; + +export function validateRedirectUrl(url: string, domains: DomainConfigJson[], allowLocalhost: boolean): boolean { + if (allowLocalhost && (new URL(url).hostname === "localhost" || new URL(url).hostname.match(/^127\.\d+\.\d+\.\d+$/))) { + return true; + } + return domains.some((domain) => { + const testUrl = new URL(url); + const baseUrl = new URL(domain.handlerPath, domain.domain); + + const sameOrigin = baseUrl.protocol === testUrl.protocol && baseUrl.hostname === testUrl.hostname; + const isSubPath = testUrl.pathname.startsWith(baseUrl.pathname); + + return sameOrigin && isSubPath; + }); +} diff --git a/apps/backend/src/lib/teams.tsx b/apps/backend/src/lib/teams.tsx new file mode 100644 index 000000000..56ed2a354 --- /dev/null +++ b/apps/backend/src/lib/teams.tsx @@ -0,0 +1,164 @@ +// TODO remove and replace with CRUD handler + +import { prismaClient } from "@/prisma-client"; +import { TeamJson } from "@stackframe/stack-shared/dist/interface/clientInterface"; +import { ServerTeamCustomizableJson, ServerTeamJson, ServerTeamMemberJson } from "@stackframe/stack-shared/dist/interface/serverInterface"; +import { filterUndefined } from "@stackframe/stack-shared/dist/utils/objects"; +import { Prisma } from "@prisma/client"; +import { getServerUserFromDbType } from "./users"; +import { serverUserInclude } from "./users"; + +// TODO technically we can split this; listUserTeams only needs `team`, and listServerTeams only needs `projectUser`; listTeams needs neither +// note: this is a function to prevent circular dependencies between the teams and users file +export const createFullTeamMemberInclude = () => ({ + team: true, + projectUser: { + include: serverUserInclude, + }, +} as const satisfies Prisma.TeamMemberInclude); + +export type ServerTeamMemberDB = Prisma.TeamMemberGetPayload<{ include: ReturnType }>; + +export async function listUserTeams(projectId: string, userId: string): Promise { + const members = await prismaClient.teamMember.findMany({ + where: { + projectId, + projectUserId: userId, + }, + include: createFullTeamMemberInclude(), + }); + + return members.map((member) => ({ + id: member.teamId, + displayName: member.team.displayName, + createdAtMillis: member.team.createdAt.getTime(), + })); +} + +export async function listUserServerTeams(projectId: string, userId: string): Promise { + return await listUserTeams(projectId, userId); // currently ServerTeam and ClientTeam are the same +} + +export async function listTeams(projectId: string): Promise { + const result = await prismaClient.team.findMany({ + where: { + projectId, + }, + }); + + return result.map(team => ({ + id: team.teamId, + displayName: team.displayName, + createdAtMillis: team.createdAt.getTime(), + })); +} + +export async function listServerTeams(projectId: string): Promise { + return await listTeams(projectId); // currently ServerTeam and ClientTeam are the same +} + +export async function listServerTeamMembers(projectId: string, teamId: string): Promise { + const members = await prismaClient.teamMember.findMany({ + where: { + projectId, + teamId, + }, + include: createFullTeamMemberInclude(), + }); + + return members.map((member) => getServerTeamMemberFromDbType(member)); +} + +export async function getTeam(projectId: string, teamId: string): Promise { + // TODO more efficient filtering + const teams = await listTeams(projectId); + return teams.find(team => team.id === teamId) || null; +} + +export async function getServerTeam(projectId: string, teamId: string): Promise { + // TODO more efficient filtering + const teams = await listServerTeams(projectId); + return teams.find(team => team.id === teamId) || null; +} + +export async function updateServerTeam(projectId: string, teamId: string, update: Partial): Promise { + await prismaClient.team.update({ + where: { + projectId_teamId: { + projectId, + teamId, + }, + }, + data: filterUndefined(update), + }); +} + +export async function createServerTeam(projectId: string, team: ServerTeamCustomizableJson): Promise { + const result = await prismaClient.team.create({ + data: { + projectId, + displayName: team.displayName, + }, + }); + return { + id: result.teamId, + displayName: result.displayName, + createdAtMillis: result.createdAt.getTime(), + }; +} + +export async function deleteServerTeam(projectId: string, teamId: string): Promise { + const deleted = await prismaClient.team.delete({ + where: { + projectId_teamId: { + projectId, + teamId, + }, + }, + }); +} + +export async function addUserToTeam(projectId: string, teamId: string, userId: string): Promise { + await prismaClient.teamMember.create({ + data: { + projectId, + teamId, + projectUserId: userId, + }, + }); +} + +export async function removeUserFromTeam(projectId: string, teamId: string, userId: string): Promise { + await prismaClient.teamMember.deleteMany({ + where: { + projectId, + teamId, + projectUserId: userId, + }, + }); +} + +export function getClientTeamFromServerTeam(team: ServerTeamJson): TeamJson { + return { + id: team.id, + displayName: team.displayName, + createdAtMillis: team.createdAtMillis, + }; +} + +export function getServerTeamFromDbType(team: Prisma.TeamGetPayload<{}>): ServerTeamJson { + return { + id: team.teamId, + displayName: team.displayName, + createdAtMillis: team.createdAt.getTime(), + }; +} + +export function getServerTeamMemberFromDbType(member: ServerTeamMemberDB): ServerTeamMemberJson { + return { + userId: member.projectUserId, + user: getServerUserFromDbType(member.projectUser), + teamId: member.teamId, + displayName: member.projectUser.displayName, + }; +} diff --git a/packages/stack-server/src/lib/tokens.tsx b/apps/backend/src/lib/tokens.tsx similarity index 100% rename from packages/stack-server/src/lib/tokens.tsx rename to apps/backend/src/lib/tokens.tsx diff --git a/apps/backend/src/lib/users.tsx b/apps/backend/src/lib/users.tsx new file mode 100644 index 000000000..14c4734ae --- /dev/null +++ b/apps/backend/src/lib/users.tsx @@ -0,0 +1,173 @@ +// TODO remove and replace with CRUD handler + +import { UserJson, ServerUserJson, KnownErrors } from "@stackframe/stack-shared"; +import { Prisma } from "@prisma/client"; +import { prismaClient } from "@/prisma-client"; +import { getProject } from "@/lib/projects"; +import { filterUndefined } from "@stackframe/stack-shared/dist/utils/objects"; +import { UserUpdateJson } from "@stackframe/stack-shared/dist/interface/clientInterface"; +import { ServerUserUpdateJson } from "@stackframe/stack-shared/dist/interface/serverInterface"; +import { addUserToTeam, createServerTeam, getClientTeamFromServerTeam, getServerTeamFromDbType } from "./teams"; + +export const serverUserInclude = { + projectUserOAuthAccounts: true, + selectedTeam: true, +} as const satisfies Prisma.ProjectUserInclude; + +export type ServerUserDB = Prisma.ProjectUserGetPayload<{ include: typeof serverUserInclude }>; + +export async function getClientUser(projectId: string, userId: string): Promise { + return await updateClientUser(projectId, userId, {}); +} + +export async function getServerUser(projectId: string, userId: string): Promise { + return await updateServerUser(projectId, userId, {}); +} + +export async function listServerUsers(projectId: string): Promise { + const users = await prismaClient.projectUser.findMany({ + where: { + projectId, + }, + include: serverUserInclude, + }); + + return users.map((u) => getServerUserFromDbType(u)); +} + +export async function updateClientUser( + projectId: string, + userId: string, + update: UserUpdateJson, +): Promise { + const user = await updateServerUser( + projectId, + userId, + { + displayName: update.displayName, + clientMetadata: update.clientMetadata, + selectedTeamId: update.selectedTeamId, + }, + ); + if (!user) { + return null; + } + + return getClientUserFromServerUser(user); +} + +export async function updateServerUser( + projectId: string, + userId: string, + update: ServerUserUpdateJson, +): Promise { + let user; + try { + user = await prismaClient.projectUser.update({ + where: { + projectId: projectId, + projectId_projectUserId: { + projectId, + projectUserId: userId, + }, + }, + include: serverUserInclude, + data: filterUndefined({ + displayName: update.displayName, + primaryEmail: update.primaryEmail, + primaryEmailVerified: update.primaryEmailVerified, + clientMetadata: update.clientMetadata as any, + serverMetadata: update.serverMetadata as any, + selectedTeamId: update.selectedTeamId, + }), + }); + } catch (e) { + // TODO this is kinda hacky, instead we should have the entire method throw an error instead of returning null and have a separate getServerUser function that may return null + if ((e as any)?.code === 'P2025') { + return null; + } + throw e; + } + + return getServerUserFromDbType(user); +} + +export async function deleteServerUser(projectId: string, userId: string): Promise { + try { + await prismaClient.projectUser.delete({ + where: { + projectId: projectId, + projectId_projectUserId: { + projectId, + projectUserId: userId, + }, + }, + }); + } catch (e) { + if ((e as any)?.code === 'P2025') { + throw new KnownErrors.UserNotFound(); + } + throw e; + } +} + +function getClientUserFromServerUser(serverUser: ServerUserJson): UserJson { + return { + projectId: serverUser.projectId, + id: serverUser.id, + displayName: serverUser.displayName, + primaryEmail: serverUser.primaryEmail, + primaryEmailVerified: serverUser.primaryEmailVerified, + profileImageUrl: serverUser.profileImageUrl, + signedUpAtMillis: serverUser.signedUpAtMillis, + clientMetadata: serverUser.clientMetadata, + authMethod: serverUser.authMethod, // not used anymore, for backwards compatibility + authWithEmail: serverUser.authWithEmail, + hasPassword: serverUser.hasPassword, + oauthProviders: serverUser.oauthProviders, + selectedTeamId: serverUser.selectedTeamId, + selectedTeam: serverUser.selectedTeam && getClientTeamFromServerTeam(serverUser.selectedTeam), + }; +} + +export function getServerUserFromDbType( + user: ServerUserDB, +): ServerUserJson { + return { + projectId: user.projectId, + id: user.projectUserId, + displayName: user.displayName, + primaryEmail: user.primaryEmail, + primaryEmailVerified: user.primaryEmailVerified, + profileImageUrl: user.profileImageUrl, + signedUpAtMillis: user.createdAt.getTime(), + clientMetadata: user.clientMetadata as any, + serverMetadata: user.serverMetadata as any, + authMethod: user.passwordHash ? 'credential' : 'oauth', // not used anymore, for backwards compatibility + hasPassword: !!user.passwordHash, + authWithEmail: user.authWithEmail, + oauthProviders: user.projectUserOAuthAccounts.map((a) => a.oauthProviderConfigId), + selectedTeamId: user.selectedTeamId, + selectedTeam: user.selectedTeam && getServerTeamFromDbType(user.selectedTeam), + }; +} + +export async function createTeamOnSignUp(projectId: string, userId: string): Promise { + const project = await getProject(projectId); + if (!project) { + throw new Error('Project not found'); + } + if (!project.evaluatedConfig.createTeamOnSignUp) { + return; + } + const user = await getServerUser(projectId, userId); + if (!user) { + throw new Error('User not found'); + } + + const team = await createServerTeam( + projectId, + { displayName: user.displayName ? `${user.displayName}'s personal team` : 'Personal team' } + ); + await addUserToTeam(projectId, team.id, userId); +} diff --git a/packages/stack-server/src/middleware.tsx b/apps/backend/src/middleware.tsx similarity index 100% rename from packages/stack-server/src/middleware.tsx rename to apps/backend/src/middleware.tsx diff --git a/packages/stack-server/src/oauth/index.tsx b/apps/backend/src/oauth/index.tsx similarity index 100% rename from packages/stack-server/src/oauth/index.tsx rename to apps/backend/src/oauth/index.tsx diff --git a/apps/backend/src/oauth/model.tsx b/apps/backend/src/oauth/model.tsx new file mode 100644 index 000000000..af76b254f --- /dev/null +++ b/apps/backend/src/oauth/model.tsx @@ -0,0 +1,250 @@ +import { AuthorizationCode, AuthorizationCodeModel, Client, Falsey, RefreshToken, Token, User } from "@node-oauth/oauth2-server"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; +import { generateSecureRandomString } from "@stackframe/stack-shared/dist/utils/crypto"; +import { prismaClient } from "@/prisma-client"; +import { decodeAccessToken, encodeAccessToken } from "@/lib/tokens"; +import { validateRedirectUrl } from "@/lib/redirect-urls"; +import { checkApiKeySet } from "@/lib/api-keys"; +import { getProject } from "@/lib/projects"; +import { StackAssertionError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; + +const enabledScopes = ["openid"]; + +function checkScope(scope: string | string[] | undefined) { + if (typeof scope === "string") { + return enabledScopes.includes(scope); + } else if (Array.isArray(scope)){ + return scope.every((s) => enabledScopes.includes(s)); + } else { + return false; + } +} + +export class OAuthModel implements AuthorizationCodeModel { + async getClient(clientId: string, clientSecret: string): Promise { + if (clientSecret) { + const keySet = await checkApiKeySet(clientId, { publishableClientKey: clientSecret }); + if (!keySet) { + return false; + } + } + + const project = await getProject(clientId); + if (!project) { + return false; + } + + const redirectUris = project.evaluatedConfig.domains.map( + ({ domain, handlerPath }) => new URL(handlerPath, domain).toString() + ); + + if (redirectUris.length === 0 && project.evaluatedConfig.allowLocalhost) { + redirectUris.push("http://localhost"); + } + + return { + id: project.id, + grants: ["authorization_code", "refresh_token"], + redirectUris: redirectUris, + }; + } + + async validateScope(user: User | null, client: Client | null, scope?: string[]): Promise { + if (!user) { + return false; + } + + if (!client) { + return false; + } + + return checkScope(scope) ? scope : false; + } + + async generateAccessToken(client: Client, user: User, scope: string[]): Promise { + return await encodeAccessToken({ + projectId: client.id, + userId: user.id, + }); + } + + async generateRefreshToken(client: Client, user: User, scope: string[]): Promise { + return generateSecureRandomString(); + } + + async saveToken(token: Token, client: Client, user: User): Promise{ + if (token.refreshToken) { + await prismaClient.projectUserRefreshToken.create({ + data: { + refreshToken: token.refreshToken, + expiresAt: token.refreshTokenExpiresAt, + projectUser: { + connect: { + projectId_projectUserId: { + projectId: client.id, + projectUserId: user.id, + }, + }, + }, + }, + }); + } + + token.client = client; + token.user = user; + return { + ...token, + newUser: user.newUser, + afterCallbackRedirectUrl: user.afterCallbackRedirectUrl, + }; + } + + async getAccessToken(accessToken: string): Promise { + let decoded; + try { + decoded = await decodeAccessToken(accessToken); + } catch (e) { + captureError("getAccessToken", e); + return false; + } + + return { + accessToken, + accessTokenExpiresAt: new Date(decoded.exp * 1000), + user: { + id: decoded.userId, + }, + client: { + id: decoded.projectId, + grants: ["authorization_code", "refresh_token"], + }, + scope: enabledScopes, + }; + } + + async getRefreshToken(refreshToken: string): Promise { + const token = await prismaClient.projectUserRefreshToken.findUnique({ + where: { + refreshToken, + }, + }); + + if (!token) { + return false; + } + + return { + refreshToken, + refreshTokenExpiresAt: token.expiresAt === null ? undefined : token.expiresAt, + user: { + id: token.projectUserId, + }, + client: { + id: token.projectId, + grants: ["authorization_code", "refresh_token"], + }, + scope: enabledScopes, + }; + } + + async revokeToken(token: RefreshToken): Promise { + // No refreshToken rotation for now (see Git history for old code) + return true; + } + + async verifyScope(token: Token, scope: string[]): Promise { + return checkScope(scope); + } + + async saveAuthorizationCode( + code: Pick, + client: Client, + user: User + ): Promise { + await prismaClient.projectUserAuthorizationCode.create({ + data: { + authorizationCode: code.authorizationCode, + codeChallenge: code.codeChallenge || "", + codeChallengeMethod: code.codeChallengeMethod || "", + redirectUri: code.redirectUri, + expiresAt: code.expiresAt, + projectUserId: user.id, + newUser: user.newUser, + afterCallbackRedirectUrl: user.afterCallbackRedirectUrl, + projectId: client.id, + }, + }); + + return { + authorizationCode: code.authorizationCode, + expiresAt: code.expiresAt, + redirectUri: code.redirectUri, + scope: enabledScopes, + client: { + id: client.id, + grants: ["authorization_code", "refresh_token"], + }, + user, + }; + } + + async getAuthorizationCode(authorizationCode: string): Promise { + const code = await prismaClient.projectUserAuthorizationCode.findUnique({ + where: { + authorizationCode, + }, + }); + if (!code) { + return false; + } + return { + authorizationCode: code.authorizationCode, + expiresAt: code.expiresAt, + redirectUri: code.redirectUri, + scope: enabledScopes, + codeChallenge: code.codeChallenge, + codeChallengeMethod: code.codeChallengeMethod, + client: { + id: code.projectId, + grants: ["authorization_code", "refresh_token"], + }, + user: { + id: code.projectUserId, + newUser: code.newUser, + afterCallbackRedirectUrl: code.afterCallbackRedirectUrl, + }, + }; + } + + async revokeAuthorizationCode(code: AuthorizationCode): Promise { + try { + const deletedCode = await prismaClient.projectUserAuthorizationCode.delete({ + where: { + authorizationCode: code.authorizationCode, + } + }); + + return !!deletedCode; + } catch (e) { + if (!(e instanceof PrismaClientKnownRequestError)) { + throw e; + } + return false; + } + } + + async validateRedirectUri(redirect_uri: string, client: Client): Promise { + const project = await getProject(client.id); + + if (!project) { + // This should in theory never happen, make typescript happy + throw new StackAssertionError("Project not found"); + } + + return validateRedirectUrl( + redirect_uri, + project.evaluatedConfig.domains, + project.evaluatedConfig.allowLocalhost, + ); + } +} diff --git a/packages/stack-server/src/oauth/providers/base.tsx b/apps/backend/src/oauth/providers/base.tsx similarity index 100% rename from packages/stack-server/src/oauth/providers/base.tsx rename to apps/backend/src/oauth/providers/base.tsx diff --git a/packages/stack-server/src/oauth/providers/facebook.tsx b/apps/backend/src/oauth/providers/facebook.tsx similarity index 100% rename from packages/stack-server/src/oauth/providers/facebook.tsx rename to apps/backend/src/oauth/providers/facebook.tsx diff --git a/packages/stack-server/src/oauth/providers/github.tsx b/apps/backend/src/oauth/providers/github.tsx similarity index 100% rename from packages/stack-server/src/oauth/providers/github.tsx rename to apps/backend/src/oauth/providers/github.tsx diff --git a/packages/stack-server/src/oauth/providers/google.tsx b/apps/backend/src/oauth/providers/google.tsx similarity index 100% rename from packages/stack-server/src/oauth/providers/google.tsx rename to apps/backend/src/oauth/providers/google.tsx diff --git a/packages/stack-server/src/oauth/providers/microsoft.tsx b/apps/backend/src/oauth/providers/microsoft.tsx similarity index 100% rename from packages/stack-server/src/oauth/providers/microsoft.tsx rename to apps/backend/src/oauth/providers/microsoft.tsx diff --git a/packages/stack-server/src/oauth/providers/spotify.tsx b/apps/backend/src/oauth/providers/spotify.tsx similarity index 100% rename from packages/stack-server/src/oauth/providers/spotify.tsx rename to apps/backend/src/oauth/providers/spotify.tsx diff --git a/packages/stack-server/src/oauth/utils.tsx b/apps/backend/src/oauth/utils.tsx similarity index 100% rename from packages/stack-server/src/oauth/utils.tsx rename to apps/backend/src/oauth/utils.tsx diff --git a/packages/stack-server/src/polyfills.tsx b/apps/backend/src/polyfills.tsx similarity index 100% rename from packages/stack-server/src/polyfills.tsx rename to apps/backend/src/polyfills.tsx diff --git a/packages/stack-server/src/prisma-client.tsx b/apps/backend/src/prisma-client.tsx similarity index 100% rename from packages/stack-server/src/prisma-client.tsx rename to apps/backend/src/prisma-client.tsx diff --git a/apps/backend/src/route-handlers/crud-handler.tsx b/apps/backend/src/route-handlers/crud-handler.tsx new file mode 100644 index 000000000..8218c8dac --- /dev/null +++ b/apps/backend/src/route-handlers/crud-handler.tsx @@ -0,0 +1,189 @@ +import "../polyfills"; + +import * as yup from "yup"; +import { SmartRouteHandler, SmartRouteHandlerOverloadMetadata, routeHandlerTypeHelper, createSmartRouteHandler } from "./smart-route-handler"; +import { CrudOperation, CrudSchema, CrudTypeOf } from "@stackframe/stack-shared/dist/crud"; +import { FilterUndefined } from "@stackframe/stack-shared/dist/utils/objects"; +import { typedIncludes } from "@stackframe/stack-shared/dist/utils/arrays"; +import { deindent, typedToLowercase } from "@stackframe/stack-shared/dist/utils/strings"; +import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { SmartRequestAuth } from "./smart-request"; + +type GetAdminKey, K extends Capitalize> = K extends keyof T["Admin"] ? T["Admin"][K] : void; + +type CrudSingleRouteHandler, K extends Capitalize, Params extends {}, Multi extends boolean = false> = + K extends keyof T["Admin"] + ? (options: { + params: Params, + data: (K extends "Read" ? void : GetAdminKey), + auth: SmartRequestAuth, + }) => Promise< + K extends "Delete" + ? void + : ( + Multi extends true + ? GetAdminKey[] + : GetAdminKey + ) + > + : void; + +type CrudRouteHandlersUnfiltered, Params extends {}> = { + onCreate: CrudSingleRouteHandler, + onRead: CrudSingleRouteHandler, + onList: keyof Params extends never ? void : CrudSingleRouteHandler, true>, + onUpdate: CrudSingleRouteHandler, + onDelete: CrudSingleRouteHandler, +}; + +export type RouteHandlerMetadataMap = { + create?: SmartRouteHandlerOverloadMetadata, + read?: SmartRouteHandlerOverloadMetadata, + list?: SmartRouteHandlerOverloadMetadata, + update?: SmartRouteHandlerOverloadMetadata, + delete?: SmartRouteHandlerOverloadMetadata, +}; + +type CrudHandlerOptions, ParamNames extends string> = + & FilterUndefined>> + & { + paramNames: ParamNames[], + metadataMap?: RouteHandlerMetadataMap, + }; + +type CrudHandlersFromOptions, any>> = CrudHandlers< + | ("onCreate" extends keyof O ? "Create" : never) + | ("onRead" extends keyof O ? "Read" : never) + | ("onList" extends keyof O ? "List" : never) + | ("onUpdate" extends keyof O ? "Update" : never) + | ("onDelete" extends keyof O ? "Delete" : never) +> + +export type CrudHandlers< + T extends "Create" | "Read" | "List" | "Update" | "Delete", +> = { + [K in `${Lowercase}Handler`]: SmartRouteHandler +}; + +export function createCrudHandlers, any>>( + crud: S, + options: O, +): CrudHandlersFromOptions { + const optionsAsPartial = options as Partial, any>>; + + const operations = [ + ["GET", "Read"], + ["GET", "List"], + ["POST", "Create"], + ["PUT", "Update"], + ["DELETE", "Delete"], + ] as const; + const accessTypes = ["client", "server", "admin"] as const; + + const paramsSchema = yup.object(Object.fromEntries( + options.paramNames.map((paramName) => [paramName, yup.string().required()]) + )); + + return Object.fromEntries( + operations.filter(([_, crudOperation]) => optionsAsPartial[`on${crudOperation}`] !== undefined) + .map(([httpMethod, crudOperation]) => { + const getSchemas = (accessType: "admin" | "server" | "client") => { + const input = + typedIncludes(["Read", "List"] as const, crudOperation) + ? yup.mixed().oneOf([undefined]) + : crud[accessType][`${typedToLowercase(crudOperation)}Schema`] ?? throwErr(`No input schema for ${crudOperation} with access type ${accessType}; this should never happen`); + const read = crud[accessType].readSchema ?? yup.mixed().oneOf([undefined]); + const output = + crudOperation === "List" + ? yup.array(read).required() + : crudOperation === "Delete" + ? yup.mixed().oneOf([undefined]) + : read; + return { input, output }; + }; + + const availableAccessTypes = accessTypes.filter((accessType) => { + const crudOperationWithoutList = crudOperation === "List" ? "Read" : crudOperation; + return crud[accessType][`${typedToLowercase(crudOperationWithoutList)}Schema`] !== undefined; + }); + + const routeHandler = createSmartRouteHandler( + availableAccessTypes, + (accessType) => { + const adminSchemas = getSchemas("admin"); + const accessSchemas = getSchemas(accessType); + + const frw = routeHandlerTypeHelper({ + request: yup.object({ + auth: yup.object({ + type: yup.string().oneOf([accessType]).required(), + }).required(), + url: yup.string().required(), + method: yup.string().oneOf([httpMethod]).required(), + body: accessSchemas.input, + params: crudOperation === "List" ? paramsSchema.partial() : paramsSchema, + }), + response: yup.object({ + statusCode: yup.number().oneOf([200, 201]).required(), + headers: yup.object().shape({ + location: yup.array(yup.string().required()).optional(), + }), + bodyType: yup.string().oneOf(["json"]).required(), + body: accessSchemas.output, + }), + handler: async (req, fullReq) => { + const data = req.body; + const adminData = await validate(data, adminSchemas.input, "Input validation"); + + const result = await optionsAsPartial[`on${crudOperation}`]?.({ + params: req.params, + data: adminData, + auth: fullReq.auth ?? throwErr("Auth not found in CRUD handler; this should never happen! (all clients are at least client to access CRUD handler)"), + }); + + const resultAdminValidated = await validate(result, adminSchemas.output, "Result admin validation"); + const resultAccessValidated = await validate(resultAdminValidated, accessSchemas.output, `Result ${accessType} validation`); + + return { + statusCode: crudOperation === "Create" ? 201 : 200, + headers: { + location: crudOperation === "Create" ? [req.url] : undefined, + }, + bodyType: "json", + body: resultAccessValidated, + }; + }, + }); + return { + ...frw, + metadata: options.metadataMap?.[typedToLowercase(crudOperation)], + }; + } + ); + return [`${typedToLowercase(crudOperation)}Handler`, routeHandler]; + }) + ) as any; +} + +async function validate(obj: unknown, schema: yup.ISchema, name: string): Promise { + try { + return await schema.validate(obj, { + abortEarly: false, + stripUnknown: true, + }); + } catch (error) { + if (error instanceof yup.ValidationError) { + throw new StackAssertionError( + deindent` + ${name} failed in CRUD handler. + + Errors: + ${error.errors.join("\n")} + `, + { obj: JSON.stringify(obj), schema }, + { cause: error } + ); + } + throw error; + } +} diff --git a/packages/stack-server/src/route-handlers/prisma-handler.tsx b/apps/backend/src/route-handlers/prisma-handler.tsx similarity index 100% rename from packages/stack-server/src/route-handlers/prisma-handler.tsx rename to apps/backend/src/route-handlers/prisma-handler.tsx diff --git a/packages/stack-server/src/route-handlers/redirect-handler.tsx b/apps/backend/src/route-handlers/redirect-handler.tsx similarity index 100% rename from packages/stack-server/src/route-handlers/redirect-handler.tsx rename to apps/backend/src/route-handlers/redirect-handler.tsx diff --git a/apps/backend/src/route-handlers/smart-request.tsx b/apps/backend/src/route-handlers/smart-request.tsx new file mode 100644 index 000000000..6e017dd6e --- /dev/null +++ b/apps/backend/src/route-handlers/smart-request.tsx @@ -0,0 +1,245 @@ +import "../polyfills"; + +import { NextRequest } from "next/server"; +import { StackAssertionError, StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import * as yup from "yup"; +import { DeepPartial } from "@stackframe/stack-shared/dist/utils/objects"; +import { groupBy, typedIncludes } from "@stackframe/stack-shared/dist/utils/arrays"; +import { KnownErrors, ProjectJson, ServerUserJson } from "@stackframe/stack-shared"; +import { IsAny } from "@stackframe/stack-shared/dist/utils/types"; +import { checkApiKeySet } from "@/lib/api-keys"; +import { updateProject, whyNotProjectAdmin } from "@/lib/projects"; +import { updateServerUser } from "@/lib/users"; +import { decodeAccessToken } from "@/lib/tokens"; +import { deindent } from "@stackframe/stack-shared/dist/utils/strings"; + +const allowedMethods = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] as const; + +export type SmartRequestAuth = { + project: ProjectJson, + user: ServerUserJson | null, + projectAccessType: "key" | "internal-user-token", + type: "client" | "server" | "admin", +}; + +export type SmartRequest = { + auth: SmartRequestAuth | null, + url: string, + method: typeof allowedMethods[number], + body: unknown, + headers: Record, + query: Record, + params: Record, +}; + +export type MergeSmartRequest = + IsAny extends true ? MSQ : ( + T extends object ? (MSQ extends object ? { [K in keyof T]: K extends keyof MSQ ? MergeSmartRequest : undefined } : MSQ) + : T + ); + +async function validate(obj: unknown, schema: yup.Schema, req: NextRequest): Promise { + try { + return await schema.validate(obj, { + abortEarly: false, + stripUnknown: true, + }); + } catch (error) { + if (error instanceof yup.ValidationError) { + throw new KnownErrors.SchemaError( + deindent` + Request validation failed on ${req.method} ${req.nextUrl.pathname}: + ${(error.inner.length ? error.inner : [error]).map(e => deindent` + - ${e.message} + `).join("\n")} + `, + ); + } + throw error; + } +} + + +async function parseBody(req: NextRequest, bodyBuffer: ArrayBuffer): Promise { + const contentType = req.headers.get("content-type")?.split(";")[0]; + + const getText = () => { + try { + return new TextDecoder().decode(bodyBuffer); + } catch (e) { + throw new KnownErrors.BodyParsingError("Request body cannot be parsed as UTF-8"); + } + }; + + switch (contentType) { + case "": + case undefined: { + return undefined; + } + case "application/json": { + const text = getText(); + try { + return JSON.parse(text); + } catch (e) { + throw new KnownErrors.BodyParsingError("Invalid JSON in request body"); + } + } + case "application/octet-stream": { + return bodyBuffer; + } + case "text/plain": { + return getText(); + } + case "application/x-www-form-urlencoded": { + const text = getText(); + try { + return Object.fromEntries(new URLSearchParams(text).entries()); + } catch (e) { + throw new KnownErrors.BodyParsingError("Invalid form data in request body"); + } + } + default: { + throw new KnownErrors.BodyParsingError("Unknown content type in request body: " + contentType); + } + } +} + +async function parseAuth(req: NextRequest): Promise { + const projectId = req.headers.get("x-stack-project-id"); + let requestType = req.headers.get("x-stack-request-type"); + const publishableClientKey = req.headers.get("x-stack-publishable-client-key"); + const secretServerKey = req.headers.get("x-stack-secret-server-key"); + const superSecretAdminKey = req.headers.get("x-stack-super-secret-admin"); + const adminAccessToken = req.headers.get("x-stack-admin-access-token"); + const authorization = req.headers.get("authorization"); + + const eitherKeyOrToken = !!(publishableClientKey || secretServerKey || superSecretAdminKey || adminAccessToken); + + if (!requestType && eitherKeyOrToken) { + // TODO in the future, when all clients have updated, throw KnownErrors.ProjectKeyWithoutRequestType instead of guessing + if (adminAccessToken || superSecretAdminKey) { + requestType = "admin"; + } else if (secretServerKey) { + requestType = "server"; + } else if (publishableClientKey) { + requestType = "client"; + } + } + if (!requestType) return null; + if (!typedIncludes(["client", "server", "admin"] as const, requestType)) throw new KnownErrors.InvalidRequestType(requestType); + if (!projectId) throw new KnownErrors.RequestTypeWithoutProjectId(requestType); + + let projectAccessType: "key" | "internal-user-token"; + if (adminAccessToken) { + const reason = await whyNotProjectAdmin(projectId, adminAccessToken); + switch (reason) { + case null: { + projectAccessType = "internal-user-token"; + break; + } + case "unparsable-access-token": { + throw new KnownErrors.UnparsableAdminAccessToken(); + } + case "not-admin": { + throw new KnownErrors.AdminAccessTokenIsNotAdmin(); + } + case "wrong-project-id": { + throw new KnownErrors.InvalidProjectForAdminAccessToken(); + } + case "access-token-expired": { + throw new KnownErrors.AdminAccessTokenExpired(); + } + default: { + throw new StackAssertionError(`Unexpected reason for lack of project admin: ${reason}`); + } + } + } else { + switch (requestType) { + case "client": { + if (!publishableClientKey) throw new KnownErrors.ClientAuthenticationRequired(); + const isValid = await checkApiKeySet(projectId, { publishableClientKey }); + if (!isValid) throw new KnownErrors.InvalidPublishableClientKey(projectId); + projectAccessType = "key"; + break; + } + case "server": { + if (!secretServerKey) throw new KnownErrors.ServerAuthenticationRequired(); + const isValid = await checkApiKeySet(projectId, { secretServerKey }); + if (!isValid) throw new KnownErrors.InvalidSecretServerKey(projectId); + projectAccessType = "key"; + break; + } + case "admin": { + if (!superSecretAdminKey) throw new KnownErrors.AdminAuthenticationRequired(); + const isValid = await checkApiKeySet(projectId, { superSecretAdminKey }); + if (!isValid) throw new KnownErrors.InvalidSuperSecretAdminKey(projectId); + projectAccessType = "key"; + break; + } + } + } + + let project = await updateProject( + projectId, + {}, + ); + if (!project) { + throw new KnownErrors.ProjectNotFound(); + } + + let user = null; + if (authorization) { + const decodedAccessToken = await decodeAccessToken(authorization.split(" ")[1]); + const { userId, projectId: accessTokenProjectId } = decodedAccessToken; + + if (accessTokenProjectId !== projectId) { + throw new KnownErrors.InvalidProjectForAccessToken(); + } + + user = await updateServerUser( + projectId, + userId, + {}, + ); + } + + return { + project, + user, + projectAccessType, + type: requestType, + }; +} + +export async function createLazyRequestParser>(req: NextRequest, bodyBuffer: ArrayBuffer, schema: yup.Schema, options?: { params: Record }): Promise<() => Promise<[T, SmartRequest]>> { + const urlObject = new URL(req.url); + const toValidate: SmartRequest = { + url: req.url, + method: typedIncludes(allowedMethods, req.method) ? req.method : throwErr(new StatusError(405, "Method not allowed")), + body: await parseBody(req, bodyBuffer), + headers: Object.fromEntries( + [...groupBy(req.headers.entries(), ([key, _]) => key.toLowerCase())] + .map(([key, values]) => [key, values.map(([_, value]) => value)]), + ), + query: Object.fromEntries(urlObject.searchParams.entries()), + params: options?.params ?? {}, + auth: await parseAuth(req), + }; + + return async () => [await validate(toValidate, schema, req), toValidate]; +} + +export async function deprecatedParseRequest & { headers: Record }>>(req: NextRequest, schema: yup.Schema, options?: { params: Record }): Promise { + const urlObject = new URL(req.url); + const toValidate: Omit & { headers: Record } = { + url: req.url, + method: typedIncludes(allowedMethods, req.method) ? req.method : throwErr(new StatusError(405, "Method not allowed")), + body: await parseBody(req, await req.arrayBuffer()), + headers: Object.fromEntries([...req.headers.entries()].map(([k, v]) => [k.toLowerCase(), v])), + query: Object.fromEntries(urlObject.searchParams.entries()), + params: options?.params ?? {}, + auth: null, + }; + + return await validate(toValidate, schema, req); +} diff --git a/packages/stack-server/src/route-handlers/smart-response.tsx b/apps/backend/src/route-handlers/smart-response.tsx similarity index 100% rename from packages/stack-server/src/route-handlers/smart-response.tsx rename to apps/backend/src/route-handlers/smart-response.tsx diff --git a/packages/stack-server/src/route-handlers/smart-route-handler.tsx b/apps/backend/src/route-handlers/smart-route-handler.tsx similarity index 100% rename from packages/stack-server/src/route-handlers/smart-route-handler.tsx rename to apps/backend/src/route-handlers/smart-route-handler.tsx diff --git a/packages/stack-server/tsconfig.json b/apps/backend/tsconfig.json similarity index 100% rename from packages/stack-server/tsconfig.json rename to apps/backend/tsconfig.json diff --git a/packages/stack-server/.env b/apps/dashboard/.env similarity index 94% rename from packages/stack-server/.env rename to apps/dashboard/.env index 87d2ce447..c976acebe 100644 --- a/packages/stack-server/.env +++ b/apps/dashboard/.env @@ -1,7 +1,7 @@ # Basic NEXT_PUBLIC_STACK_URL=# enter your stack endpoint here, For local development: http://localhost:8101 (no trailing slash) NEXT_PUBLIC_STACK_PROJECT_ID=internal -NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=# enter your Stack publishable client key here. For local development, just enter a random string, then run `pnpm prisma:server migrate reset` +NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=# enter your Stack publishable client key here. For local development, just enter a random string, then run `pnpm prisma migrate reset` STACK_SECRET_SERVER_KEY=# enter your Stack secret client key here. For local development, do the same as above SERVER_SECRET=# enter a secret key generated by `pnpm generate-keys` here. This is used to sign the JWT tokens. @@ -33,4 +33,4 @@ DIRECT_DATABASE_CONNECTION_STRING=# enter your direct (unpooled or session mode) # Misc, optional STACK_ACCESS_TOKEN_EXPIRATION_TIME=# enter the expiration time for the access token here. Optional, don't specify it for default value -NEXT_PUBLIC_STACK_HEAD_TAGS=[{ "tagName": "script", "attributes": {}, "innerHTML": "// insert head tags here" }] \ No newline at end of file +NEXT_PUBLIC_STACK_HEAD_TAGS=[{ "tagName": "script", "attributes": {}, "innerHTML": "// insert head tags here" }] diff --git a/packages/stack-server/.env.development b/apps/dashboard/.env.development similarity index 100% rename from packages/stack-server/.env.development rename to apps/dashboard/.env.development diff --git a/packages/stack-server/.eslintrc.cjs b/apps/dashboard/.eslintrc.cjs similarity index 100% rename from packages/stack-server/.eslintrc.cjs rename to apps/dashboard/.eslintrc.cjs diff --git a/apps/dashboard/.gitignore b/apps/dashboard/.gitignore new file mode 100644 index 000000000..fd3dbb571 --- /dev/null +++ b/apps/dashboard/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/packages/stack-server/.npmrc b/apps/dashboard/.npmrc similarity index 100% rename from packages/stack-server/.npmrc rename to apps/dashboard/.npmrc diff --git a/packages/stack-server/CHANGELOG.md b/apps/dashboard/CHANGELOG.md similarity index 99% rename from packages/stack-server/CHANGELOG.md rename to apps/dashboard/CHANGELOG.md index 7d73748d1..a9c1756e0 100644 --- a/packages/stack-server/CHANGELOG.md +++ b/apps/dashboard/CHANGELOG.md @@ -1,4 +1,4 @@ -# @stackframe/stack-server +# @stackframe/stack-dashboard ## 2.4.27 diff --git a/apps/dashboard/LICENSE b/apps/dashboard/LICENSE new file mode 100644 index 000000000..be3f7b28e --- /dev/null +++ b/apps/dashboard/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/packages/stack-server/components.json b/apps/dashboard/components.json similarity index 100% rename from packages/stack-server/components.json rename to apps/dashboard/components.json diff --git a/packages/stack-server/next.config.mjs b/apps/dashboard/next.config.mjs similarity index 92% rename from packages/stack-server/next.config.mjs rename to apps/dashboard/next.config.mjs index a8c7a86d7..080cd106f 100644 --- a/packages/stack-server/next.config.mjs +++ b/apps/dashboard/next.config.mjs @@ -74,19 +74,6 @@ const nextConfig = { optimizePackageImports: ["@mui/joy"], }, - webpack(webpackConfig) { - return { - ...webpackConfig, - resolve: { - ...webpackConfig.resolve, - alias: { - handlebars: "handlebars/dist/handlebars.js", - ...webpackConfig.resolve.alias, - }, - }, - }; - }, - async headers() { return [ { diff --git a/packages/stack-server/package.json b/apps/dashboard/package.json similarity index 89% rename from packages/stack-server/package.json rename to apps/dashboard/package.json index 8c76464b7..6fc0c2e52 100644 --- a/packages/stack-server/package.json +++ b/apps/dashboard/package.json @@ -1,5 +1,6 @@ { - "name": "@stackframe/stack-server", + + "name": "@stackframe/stack-dashboard", "version": "2.4.27", "private": true, "scripts": { @@ -7,16 +8,14 @@ "typecheck": "tsc --noEmit", "with-env": "dotenv -c development --", "with-env:prod": "dotenv -c --", - "dev": "concurrently \"next dev --port 8101\" \"npm run watch-docs\"", + "dev": "next dev --port 8101", "build": "npm run codegen && next build", "analyze-bundle": "ANALYZE_BUNDLE=1 npm run build", "start": "next start --port 8101", - "codegen": "npm run prisma -- generate && npm run generate-docs", + "codegen": "npm run prisma -- generate", "psql": "npm run with-env -- bash -c 'psql $DATABASE_CONNECTION_STRING'", "prisma": "npm run with-env -- prisma", "lint": "next lint", - "watch-docs": "npm run with-env -- chokidar --silent '../../**/*' -i '../../docs/**' -i '../../**/node_modules/**' -i '../../**/.next/**' -i '../../**/dist/**' -c 'tsx scripts/generate-docs.ts'", - "generate-docs": "npm run with-env -- tsx scripts/generate-docs.ts", "generate-keys": "npm run with-env -- tsx scripts/generate-keys.ts" }, "prisma": { diff --git a/packages/stack-server/postcss.config.js b/apps/dashboard/postcss.config.js similarity index 100% rename from packages/stack-server/postcss.config.js rename to apps/dashboard/postcss.config.js diff --git a/apps/dashboard/prisma/migrations/20240306152532_initial_migration/migration.sql b/apps/dashboard/prisma/migrations/20240306152532_initial_migration/migration.sql new file mode 100644 index 000000000..62bbf3f5c --- /dev/null +++ b/apps/dashboard/prisma/migrations/20240306152532_initial_migration/migration.sql @@ -0,0 +1,297 @@ +-- CreateEnum +CREATE TYPE "ProxiedOAuthProviderType" AS ENUM ('GITHUB', 'FACEBOOK', 'GOOGLE', 'MICROSOFT'); + +-- CreateEnum +CREATE TYPE "StandardOAuthProviderType" AS ENUM ('GITHUB', 'FACEBOOK', 'GOOGLE', 'MICROSOFT'); + +-- CreateTable +CREATE TABLE "Project" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "displayName" TEXT NOT NULL, + "description" TEXT DEFAULT '', + "configId" UUID NOT NULL, + "isProductionMode" BOOLEAN NOT NULL, + + CONSTRAINT "Project_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ProjectConfig" ( + "id" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "allowLocalhost" BOOLEAN NOT NULL, + "credentialEnabled" BOOLEAN NOT NULL, + + CONSTRAINT "ProjectConfig_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ProjectDomain" ( + "projectConfigId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "domain" TEXT NOT NULL, + "handlerPath" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "ProjectConfigOverride" ( + "projectId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "ProjectConfigOverride_pkey" PRIMARY KEY ("projectId") +); + +-- CreateTable +CREATE TABLE "ProjectUser" ( + "projectId" TEXT NOT NULL, + "projectUserId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "primaryEmail" TEXT, + "primaryEmailVerified" BOOLEAN NOT NULL, + "profileImageUrl" TEXT, + "displayName" TEXT, + "passwordHash" TEXT, + "serverMetadata" JSONB, + "clientMetadata" JSONB, + + CONSTRAINT "ProjectUser_pkey" PRIMARY KEY ("projectId","projectUserId") +); + +-- CreateTable +CREATE TABLE "ProjectUserOAuthAccount" ( + "projectId" TEXT NOT NULL, + "projectUserId" UUID NOT NULL, + "projectConfigId" UUID NOT NULL, + "oauthProviderConfigId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "email" TEXT, + "providerAccountId" TEXT NOT NULL, + "providerRefreshToken" TEXT, + + CONSTRAINT "ProjectUserOAuthAccount_pkey" PRIMARY KEY ("projectId","oauthProviderConfigId","providerAccountId") +); + +-- CreateTable +CREATE TABLE "ProjectUserRefreshToken" ( + "projectId" TEXT NOT NULL, + "projectUserId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "refreshToken" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3), + + CONSTRAINT "ProjectUserRefreshToken_pkey" PRIMARY KEY ("projectId","refreshToken") +); + +-- CreateTable +CREATE TABLE "ProjectUserAuthorizationCode" ( + "projectId" TEXT NOT NULL, + "projectUserId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "authorizationCode" TEXT NOT NULL, + "redirectUri" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "codeChallenge" TEXT NOT NULL, + "codeChallengeMethod" TEXT NOT NULL, + + CONSTRAINT "ProjectUserAuthorizationCode_pkey" PRIMARY KEY ("projectId","authorizationCode") +); + +-- CreateTable +CREATE TABLE "ProjectUserEmailVerificationCode" ( + "projectId" TEXT NOT NULL, + "projectUserId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "code" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "usedAt" TIMESTAMP(3), + "redirectUrl" TEXT NOT NULL, + + CONSTRAINT "ProjectUserEmailVerificationCode_pkey" PRIMARY KEY ("projectId","code") +); + +-- CreateTable +CREATE TABLE "ProjectUserPasswordResetCode" ( + "projectId" TEXT NOT NULL, + "projectUserId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "code" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "usedAt" TIMESTAMP(3), + "redirectUrl" TEXT NOT NULL, + + CONSTRAINT "ProjectUserPasswordResetCode_pkey" PRIMARY KEY ("projectId","code") +); + +-- CreateTable +CREATE TABLE "ApiKeySet" ( + "projectId" TEXT NOT NULL, + "id" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "description" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "manuallyRevokedAt" TIMESTAMP(3), + "publishableClientKey" TEXT, + "secretServerKey" TEXT, + "superSecretAdminKey" TEXT, + + CONSTRAINT "ApiKeySet_pkey" PRIMARY KEY ("projectId","id") +); + +-- CreateTable +CREATE TABLE "EmailServiceConfig" ( + "projectConfigId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "senderName" TEXT NOT NULL, + + CONSTRAINT "EmailServiceConfig_pkey" PRIMARY KEY ("projectConfigId") +); + +-- CreateTable +CREATE TABLE "ProxiedEmailServiceConfig" ( + "projectConfigId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "ProxiedEmailServiceConfig_pkey" PRIMARY KEY ("projectConfigId") +); + +-- CreateTable +CREATE TABLE "StandardEmailServiceConfig" ( + "projectConfigId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "senderEmail" TEXT NOT NULL, + "host" TEXT NOT NULL, + "port" INTEGER NOT NULL, + "username" TEXT NOT NULL, + "password" TEXT NOT NULL, + + CONSTRAINT "StandardEmailServiceConfig_pkey" PRIMARY KEY ("projectConfigId") +); + +-- CreateTable +CREATE TABLE "OAuthProviderConfig" ( + "projectConfigId" UUID NOT NULL, + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "enabled" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "OAuthProviderConfig_pkey" PRIMARY KEY ("projectConfigId","id") +); + +-- CreateTable +CREATE TABLE "ProxiedOAuthProviderConfig" ( + "projectConfigId" UUID NOT NULL, + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "type" "ProxiedOAuthProviderType" NOT NULL, + + CONSTRAINT "ProxiedOAuthProviderConfig_pkey" PRIMARY KEY ("projectConfigId","id") +); + +-- CreateTable +CREATE TABLE "StandardOAuthProviderConfig" ( + "projectConfigId" UUID NOT NULL, + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "type" "StandardOAuthProviderType" NOT NULL, + "tenantId" TEXT, + "clientId" TEXT NOT NULL, + "clientSecret" TEXT NOT NULL, + + CONSTRAINT "StandardOAuthProviderConfig_pkey" PRIMARY KEY ("projectConfigId","id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "ProjectDomain_projectConfigId_domain_key" ON "ProjectDomain"("projectConfigId", "domain"); + +-- CreateIndex +CREATE UNIQUE INDEX "ProjectUserRefreshToken_refreshToken_key" ON "ProjectUserRefreshToken"("refreshToken"); + +-- CreateIndex +CREATE UNIQUE INDEX "ProjectUserAuthorizationCode_authorizationCode_key" ON "ProjectUserAuthorizationCode"("authorizationCode"); + +-- CreateIndex +CREATE UNIQUE INDEX "ProjectUserEmailVerificationCode_code_key" ON "ProjectUserEmailVerificationCode"("code"); + +-- CreateIndex +CREATE UNIQUE INDEX "ProjectUserPasswordResetCode_code_key" ON "ProjectUserPasswordResetCode"("code"); + +-- CreateIndex +CREATE UNIQUE INDEX "ApiKeySet_publishableClientKey_key" ON "ApiKeySet"("publishableClientKey"); + +-- CreateIndex +CREATE UNIQUE INDEX "ApiKeySet_secretServerKey_key" ON "ApiKeySet"("secretServerKey"); + +-- CreateIndex +CREATE UNIQUE INDEX "ApiKeySet_superSecretAdminKey_key" ON "ApiKeySet"("superSecretAdminKey"); + +-- CreateIndex +CREATE UNIQUE INDEX "ProxiedOAuthProviderConfig_projectConfigId_type_key" ON "ProxiedOAuthProviderConfig"("projectConfigId", "type"); + +-- AddForeignKey +ALTER TABLE "Project" ADD CONSTRAINT "Project_configId_fkey" FOREIGN KEY ("configId") REFERENCES "ProjectConfig"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProjectDomain" ADD CONSTRAINT "ProjectDomain_projectConfigId_fkey" FOREIGN KEY ("projectConfigId") REFERENCES "ProjectConfig"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProjectConfigOverride" ADD CONSTRAINT "ProjectConfigOverride_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProjectUser" ADD CONSTRAINT "ProjectUser_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProjectUserOAuthAccount" ADD CONSTRAINT "ProjectUserOAuthAccount_projectConfigId_oauthProviderConfi_fkey" FOREIGN KEY ("projectConfigId", "oauthProviderConfigId") REFERENCES "OAuthProviderConfig"("projectConfigId", "id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProjectUserOAuthAccount" ADD CONSTRAINT "ProjectUserOAuthAccount_projectId_projectUserId_fkey" FOREIGN KEY ("projectId", "projectUserId") REFERENCES "ProjectUser"("projectId", "projectUserId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProjectUserRefreshToken" ADD CONSTRAINT "ProjectUserRefreshToken_projectId_projectUserId_fkey" FOREIGN KEY ("projectId", "projectUserId") REFERENCES "ProjectUser"("projectId", "projectUserId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProjectUserAuthorizationCode" ADD CONSTRAINT "ProjectUserAuthorizationCode_projectId_projectUserId_fkey" FOREIGN KEY ("projectId", "projectUserId") REFERENCES "ProjectUser"("projectId", "projectUserId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProjectUserEmailVerificationCode" ADD CONSTRAINT "ProjectUserEmailVerificationCode_projectId_projectUserId_fkey" FOREIGN KEY ("projectId", "projectUserId") REFERENCES "ProjectUser"("projectId", "projectUserId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProjectUserPasswordResetCode" ADD CONSTRAINT "ProjectUserPasswordResetCode_projectId_projectUserId_fkey" FOREIGN KEY ("projectId", "projectUserId") REFERENCES "ProjectUser"("projectId", "projectUserId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ApiKeySet" ADD CONSTRAINT "ApiKeySet_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "EmailServiceConfig" ADD CONSTRAINT "EmailServiceConfig_projectConfigId_fkey" FOREIGN KEY ("projectConfigId") REFERENCES "ProjectConfig"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProxiedEmailServiceConfig" ADD CONSTRAINT "ProxiedEmailServiceConfig_projectConfigId_fkey" FOREIGN KEY ("projectConfigId") REFERENCES "EmailServiceConfig"("projectConfigId") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StandardEmailServiceConfig" ADD CONSTRAINT "StandardEmailServiceConfig_projectConfigId_fkey" FOREIGN KEY ("projectConfigId") REFERENCES "EmailServiceConfig"("projectConfigId") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OAuthProviderConfig" ADD CONSTRAINT "OAuthProviderConfig_projectConfigId_fkey" FOREIGN KEY ("projectConfigId") REFERENCES "ProjectConfig"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProxiedOAuthProviderConfig" ADD CONSTRAINT "ProxiedOAuthProviderConfig_projectConfigId_id_fkey" FOREIGN KEY ("projectConfigId", "id") REFERENCES "OAuthProviderConfig"("projectConfigId", "id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StandardOAuthProviderConfig" ADD CONSTRAINT "StandardOAuthProviderConfig_projectConfigId_id_fkey" FOREIGN KEY ("projectConfigId", "id") REFERENCES "OAuthProviderConfig"("projectConfigId", "id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/dashboard/prisma/migrations/20240313024014_authroization_code_new_user/migration.sql b/apps/dashboard/prisma/migrations/20240313024014_authroization_code_new_user/migration.sql new file mode 100644 index 000000000..a2ab0444e --- /dev/null +++ b/apps/dashboard/prisma/migrations/20240313024014_authroization_code_new_user/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "ProjectUserAuthorizationCode" ADD COLUMN "newUser" BOOLEAN NOT NULL DEFAULT false; diff --git a/apps/dashboard/prisma/migrations/20240418090527_magic_link/migration.sql b/apps/dashboard/prisma/migrations/20240418090527_magic_link/migration.sql new file mode 100644 index 000000000..a1a9ecf4d --- /dev/null +++ b/apps/dashboard/prisma/migrations/20240418090527_magic_link/migration.sql @@ -0,0 +1,27 @@ +-- AlterTable +ALTER TABLE "ProjectConfig" ADD COLUMN "magicLinkEnabled" BOOLEAN NOT NULL DEFAULT false; + +-- AlterTable, authWithEmail default to true if password hash is set previously, otherwise false +ALTER TABLE "ProjectUser" ADD COLUMN "authWithEmail" BOOLEAN NOT NULL DEFAULT false; +UPDATE "ProjectUser" SET "authWithEmail" = true WHERE "passwordHash" IS NOT NULL; + +-- CreateTable +CREATE TABLE "ProjectUserMagicLinkCode" ( + "projectId" TEXT NOT NULL, + "projectUserId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "code" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "usedAt" TIMESTAMP(3), + "redirectUrl" TEXT NOT NULL, + "newUser" BOOLEAN NOT NULL, + + CONSTRAINT "ProjectUserMagicLinkCode_pkey" PRIMARY KEY ("projectId","code") +); + +-- CreateIndex +CREATE UNIQUE INDEX "ProjectUserMagicLinkCode_code_key" ON "ProjectUserMagicLinkCode"("code"); + +-- AddForeignKey +ALTER TABLE "ProjectUserMagicLinkCode" ADD CONSTRAINT "ProjectUserMagicLinkCode_projectId_projectUserId_fkey" FOREIGN KEY ("projectId", "projectUserId") REFERENCES "ProjectUser"("projectId", "projectUserId") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/dashboard/prisma/migrations/20240507195652_team/migration.sql b/apps/dashboard/prisma/migrations/20240507195652_team/migration.sql new file mode 100644 index 000000000..0d5365c23 --- /dev/null +++ b/apps/dashboard/prisma/migrations/20240507195652_team/migration.sql @@ -0,0 +1,108 @@ + +-- CreateEnum +CREATE TYPE "PermissionScope" AS ENUM ('GLOBAL', 'TEAM'); + +-- AlterTable +ALTER TABLE "ProjectConfig" ADD COLUMN "createTeamOnSignUp" BOOLEAN NOT NULL DEFAULT false; +ALTER TABLE "ProjectConfig" ALTER COLUMN "createTeamOnSignUp" DROP DEFAULT; +ALTER TABLE "ProjectConfig" ALTER COLUMN "magicLinkEnabled" DROP DEFAULT; + + +-- AlterTable +ALTER TABLE "ProjectUser" ALTER COLUMN "authWithEmail" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "ProjectUserAuthorizationCode" ALTER COLUMN "newUser" DROP DEFAULT; + +-- CreateTable +CREATE TABLE "Team" ( + "projectId" TEXT NOT NULL, + "teamId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "displayName" TEXT NOT NULL, + + CONSTRAINT "Team_pkey" PRIMARY KEY ("projectId","teamId") +); + +-- CreateTable +CREATE TABLE "TeamMember" ( + "projectId" TEXT NOT NULL, + "projectUserId" UUID NOT NULL, + "teamId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "TeamMember_pkey" PRIMARY KEY ("projectId","projectUserId","teamId") +); + +-- CreateTable +CREATE TABLE "TeamMemberDirectPermission" ( + "projectId" TEXT NOT NULL, + "projectUserId" UUID NOT NULL, + "teamId" UUID NOT NULL, + "permissionDbId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "TeamMemberDirectPermission_pkey" PRIMARY KEY ("projectId","projectUserId","teamId","permissionDbId") +); + +-- CreateTable +CREATE TABLE "Permission" ( + "queryableId" TEXT NOT NULL, + "dbId" UUID NOT NULL, + "projectConfigId" UUID, + "projectId" TEXT, + "teamId" UUID, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "description" TEXT, + "scope" "PermissionScope" NOT NULL, + + CONSTRAINT "Permission_pkey" PRIMARY KEY ("dbId") +); + +-- CreateTable +CREATE TABLE "PermissionEdge" ( + "edgeId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "parentPermissionDbId" UUID NOT NULL, + "childPermissionDbId" UUID NOT NULL, + + CONSTRAINT "PermissionEdge_pkey" PRIMARY KEY ("edgeId") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Permission_projectConfigId_queryableId_key" ON "Permission"("projectConfigId", "queryableId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Permission_projectId_teamId_queryableId_key" ON "Permission"("projectId", "teamId", "queryableId"); + +-- AddForeignKey +ALTER TABLE "Team" ADD CONSTRAINT "Team_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TeamMember" ADD CONSTRAINT "TeamMember_projectId_projectUserId_fkey" FOREIGN KEY ("projectId", "projectUserId") REFERENCES "ProjectUser"("projectId", "projectUserId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TeamMember" ADD CONSTRAINT "TeamMember_projectId_teamId_fkey" FOREIGN KEY ("projectId", "teamId") REFERENCES "Team"("projectId", "teamId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TeamMemberDirectPermission" ADD CONSTRAINT "TeamMemberDirectPermission_projectId_projectUserId_teamId_fkey" FOREIGN KEY ("projectId", "projectUserId", "teamId") REFERENCES "TeamMember"("projectId", "projectUserId", "teamId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TeamMemberDirectPermission" ADD CONSTRAINT "TeamMemberDirectPermission_permissionDbId_fkey" FOREIGN KEY ("permissionDbId") REFERENCES "Permission"("dbId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Permission" ADD CONSTRAINT "Permission_projectConfigId_fkey" FOREIGN KEY ("projectConfigId") REFERENCES "ProjectConfig"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Permission" ADD CONSTRAINT "Permission_projectId_teamId_fkey" FOREIGN KEY ("projectId", "teamId") REFERENCES "Team"("projectId", "teamId") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PermissionEdge" ADD CONSTRAINT "PermissionEdge_parentPermissionDbId_fkey" FOREIGN KEY ("parentPermissionDbId") REFERENCES "Permission"("dbId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PermissionEdge" ADD CONSTRAINT "PermissionEdge_childPermissionDbId_fkey" FOREIGN KEY ("childPermissionDbId") REFERENCES "Permission"("dbId") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/dashboard/prisma/migrations/20240518151916_email_config/migration.sql b/apps/dashboard/prisma/migrations/20240518151916_email_config/migration.sql new file mode 100644 index 000000000..4209d68d7 --- /dev/null +++ b/apps/dashboard/prisma/migrations/20240518151916_email_config/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - You are about to drop the column `senderName` on the `EmailServiceConfig` table. All the data in the column will be lost. + - Added the required column `senderName` to the `StandardEmailServiceConfig` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "EmailServiceConfig" DROP COLUMN "senderName"; + +-- AlterTable +ALTER TABLE "StandardEmailServiceConfig" ADD COLUMN "senderName" TEXT NOT NULL; diff --git a/apps/dashboard/prisma/migrations/20240520152704_selected_team/migration.sql b/apps/dashboard/prisma/migrations/20240520152704_selected_team/migration.sql new file mode 100644 index 000000000..9d7a06a31 --- /dev/null +++ b/apps/dashboard/prisma/migrations/20240520152704_selected_team/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "ProjectUser" ADD COLUMN "selectedTeamId" UUID; + +-- AddForeignKey +ALTER TABLE "ProjectUser" ADD CONSTRAINT "ProjectUser_projectId_selectedTeamId_fkey" FOREIGN KEY ("projectId", "selectedTeamId") REFERENCES "Team"("projectId", "teamId") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/dashboard/prisma/migrations/20240528090210_email_templates/migration.sql b/apps/dashboard/prisma/migrations/20240528090210_email_templates/migration.sql new file mode 100644 index 000000000..5362c0f38 --- /dev/null +++ b/apps/dashboard/prisma/migrations/20240528090210_email_templates/migration.sql @@ -0,0 +1,17 @@ +-- CreateEnum +CREATE TYPE "EmailTemplateType" AS ENUM ('EMAIL_VERIFICATION', 'PASSWORD_RESET', 'MAGIC_LINK'); + +-- CreateTable +CREATE TABLE "EmailTemplate" ( + "projectConfigId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "content" JSONB NOT NULL, + "type" "EmailTemplateType" NOT NULL, + "subject" TEXT NOT NULL, + + CONSTRAINT "EmailTemplate_pkey" PRIMARY KEY ("projectConfigId","type") +); + +-- AddForeignKey +ALTER TABLE "EmailTemplate" ADD CONSTRAINT "EmailTemplate_projectConfigId_fkey" FOREIGN KEY ("projectConfigId") REFERENCES "EmailServiceConfig"("projectConfigId") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/dashboard/prisma/migrations/20240529121811_spotify_oauth/migration.sql b/apps/dashboard/prisma/migrations/20240529121811_spotify_oauth/migration.sql new file mode 100644 index 000000000..f9aa768c3 --- /dev/null +++ b/apps/dashboard/prisma/migrations/20240529121811_spotify_oauth/migration.sql @@ -0,0 +1,5 @@ +-- AlterEnum +ALTER TYPE "ProxiedOAuthProviderType" ADD VALUE 'SPOTIFY'; + +-- AlterEnum +ALTER TYPE "StandardOAuthProviderType" ADD VALUE 'SPOTIFY'; diff --git a/apps/dashboard/prisma/migrations/20240608142105_oauth_access_token/migration.sql b/apps/dashboard/prisma/migrations/20240608142105_oauth_access_token/migration.sql new file mode 100644 index 000000000..f0814bdbb --- /dev/null +++ b/apps/dashboard/prisma/migrations/20240608142105_oauth_access_token/migration.sql @@ -0,0 +1,32 @@ +/* + Warnings: + + - You are about to drop the column `providerRefreshToken` on the `ProjectUserOAuthAccount` table. All the data in the column will be lost. + - You are about to drop the column `tenantId` on the `StandardOAuthProviderConfig` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "ProjectUserAuthorizationCode" ADD COLUMN "afterCallbackRedirectUrl" TEXT; + +-- AlterTable +ALTER TABLE "ProjectUserOAuthAccount" DROP COLUMN "providerRefreshToken"; + +-- AlterTable +ALTER TABLE "StandardOAuthProviderConfig" DROP COLUMN "tenantId"; + +-- CreateTable +CREATE TABLE "OAuthToken" ( + "id" UUID NOT NULL, + "projectId" TEXT NOT NULL, + "oAuthProviderConfigId" TEXT NOT NULL, + "providerAccountId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "refreshToken" TEXT NOT NULL, + "scopes" TEXT[], + + CONSTRAINT "OAuthToken_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "OAuthToken" ADD CONSTRAINT "OAuthToken_projectId_oAuthProviderConfigId_providerAccount_fkey" FOREIGN KEY ("projectId", "oAuthProviderConfigId", "providerAccountId") REFERENCES "ProjectUserOAuthAccount"("projectId", "oauthProviderConfigId", "providerAccountId") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/dashboard/prisma/migrations/20240610085756_outer_oauth_info/migration.sql b/apps/dashboard/prisma/migrations/20240610085756_outer_oauth_info/migration.sql new file mode 100644 index 000000000..50146c5bd --- /dev/null +++ b/apps/dashboard/prisma/migrations/20240610085756_outer_oauth_info/migration.sql @@ -0,0 +1,10 @@ +-- CreateTable +CREATE TABLE "OAuthOuterInfo" ( + "id" UUID NOT NULL, + "info" JSONB NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "OAuthOuterInfo_pkey" PRIMARY KEY ("id") +); diff --git a/apps/dashboard/prisma/migrations/migration_lock.toml b/apps/dashboard/prisma/migrations/migration_lock.toml new file mode 100644 index 000000000..fbffa92c2 --- /dev/null +++ b/apps/dashboard/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/apps/dashboard/prisma/schema.prisma b/apps/dashboard/prisma/schema.prisma new file mode 100644 index 000000000..ae6b0f9d7 --- /dev/null +++ b/apps/dashboard/prisma/schema.prisma @@ -0,0 +1,480 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_CONNECTION_STRING") + directUrl = env("DIRECT_DATABASE_CONNECTION_STRING") +} + +model Project { + // Note that the project with ID `internal` is handled as a special case. + id String @id + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + displayName String + description String? @default("") + configId String @db.Uuid + config ProjectConfig @relation(fields: [configId], references: [id]) + configOverride ProjectConfigOverride? + isProductionMode Boolean + + users ProjectUser[] @relation("ProjectUsers") + teams Team[] + apiKeySets ApiKeySet[] +} + +// Contains all the configuration for a project. +// +// More specifically, "configuration" is what we call those settings that only depend on environment variables and overrides between different deployments. +model ProjectConfig { + id String @id @default(uuid()) @db.Uuid + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + allowLocalhost Boolean + credentialEnabled Boolean + magicLinkEnabled Boolean + createTeamOnSignUp Boolean + + projects Project[] + oauthProviderConfigs OAuthProviderConfig[] + emailServiceConfig EmailServiceConfig? + domains ProjectDomain[] + permissions Permission[] +} + +model ProjectDomain { + projectConfigId String @db.Uuid + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + domain String + handlerPath String + + projectConfig ProjectConfig @relation(fields: [projectConfigId], references: [id]) + + @@unique([projectConfigId, domain]) +} + +// Environment-specific overrides for a configuration. +// +// This is a quick and dirty way to allow for environment-specific overrides of the configuration. +// +// For most cases, you should prefer to use environment variables. +// +// Note: Overrides (and environment variables) are currently unimplemented, so this model is empty. +model ProjectConfigOverride { + projectId String @id + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project Project @relation(fields: [projectId], references: [id]) +} + +model Team { + projectId String + teamId String @default(uuid()) @db.Uuid + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + displayName String + + project Project @relation(fields: [projectId], references: [id]) + permissions Permission[] + teamMembers TeamMember[] + selectedProjectUser ProjectUser[] + + @@id([projectId, teamId]) +} + +model TeamMember { + projectId String + projectUserId String @db.Uuid + teamId String @db.Uuid + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + projectUser ProjectUser @relation(fields: [projectId, projectUserId], references: [projectId, projectUserId], onDelete: Cascade) + team Team @relation(fields: [projectId, teamId], references: [projectId, teamId], onDelete: Cascade) + + directPermissions TeamMemberDirectPermission[] + + @@id([projectId, projectUserId, teamId]) +} + +model TeamMemberDirectPermission { + projectId String + projectUserId String @db.Uuid + teamId String @db.Uuid + permissionDbId String @db.Uuid + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + teamMember TeamMember @relation(fields: [projectId, projectUserId, teamId], references: [projectId, projectUserId, teamId], onDelete: Cascade) + permission Permission @relation(fields: [permissionDbId], references: [dbId], onDelete: Cascade) + + @@id([projectId, projectUserId, teamId, permissionDbId]) +} + +model Permission { + // The ID of this permission, as is chosen by and exposed to the user. It is different from the database ID, which is randomly generated and only used internally. + queryableId String + // The database ID of this permission. This is never exposed to any client and is only used to make sure the database has an ID column. + dbId String @id @default(uuid()) @db.Uuid + // exactly one of [projectConfigId && projectConfig] or [projectId && teamId && team] must be set + projectConfigId String? @db.Uuid + projectId String? + teamId String? @db.Uuid + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + description String? + + // The scope of the permission. If projectConfigId is set, may be GLOBAL or TEAM; if teamId is set, must be TEAM. + scope PermissionScope + projectConfig ProjectConfig? @relation(fields: [projectConfigId], references: [id]) + team Team? @relation(fields: [projectId, teamId], references: [projectId, teamId]) + + parentEdges PermissionEdge[] @relation("ChildPermission") + childEdges PermissionEdge[] @relation("ParentPermission") + teamMemberDirectPermission TeamMemberDirectPermission[] + + @@unique([projectConfigId, queryableId]) + @@unique([projectId, teamId, queryableId]) +} + +enum PermissionScope { + GLOBAL + TEAM +} + +model PermissionEdge { + edgeId String @id @default(uuid()) @db.Uuid + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + parentPermissionDbId String @db.Uuid + parentPermission Permission @relation("ParentPermission", fields: [parentPermissionDbId], references: [dbId], onDelete: Cascade) + childPermissionDbId String @db.Uuid + childPermission Permission @relation("ChildPermission", fields: [childPermissionDbId], references: [dbId], onDelete: Cascade) +} + +model ProjectUser { + projectId String + projectUserId String @default(uuid()) @db.Uuid + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project Project @relation("ProjectUsers", fields: [projectId], references: [id]) + projectUserRefreshTokens ProjectUserRefreshToken[] + projectUserAuthorizationCodes ProjectUserAuthorizationCode[] + projectUserOAuthAccounts ProjectUserOAuthAccount[] + projectUserEmailVerificationCode ProjectUserEmailVerificationCode[] + projectUserPasswordResetCode ProjectUserPasswordResetCode[] + projectUserMagicLinkCode ProjectUserMagicLinkCode[] + teamMembers TeamMember[] + + primaryEmail String? + primaryEmailVerified Boolean + profileImageUrl String? + displayName String? + passwordHash String? + authWithEmail Boolean + + serverMetadata Json? + clientMetadata Json? + + selectedTeam Team? @relation(fields: [projectId, selectedTeamId], references: [projectId, teamId]) + selectedTeamId String? @db.Uuid + + @@id([projectId, projectUserId]) +} + +model ProjectUserOAuthAccount { + projectId String + projectUserId String @db.Uuid + projectConfigId String @db.Uuid + oauthProviderConfigId String + providerAccountId String + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + providerConfig OAuthProviderConfig @relation(fields: [projectConfigId, oauthProviderConfigId], references: [projectConfigId, id]) + projectUser ProjectUser @relation(fields: [projectId, projectUserId], references: [projectId, projectUserId], onDelete: Cascade) + oauthTokens OAuthToken[] + + email String? + + @@id([projectId, oauthProviderConfigId, providerAccountId]) +} + +model OAuthToken { + id String @id @default(uuid()) @db.Uuid + + projectId String + oAuthProviderConfigId String + providerAccountId String + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + projectUserOAuthAccount ProjectUserOAuthAccount @relation(fields: [projectId, oAuthProviderConfigId, providerAccountId], references: [projectId, oauthProviderConfigId, providerAccountId]) + + refreshToken String + scopes String[] +} + +model OAuthOuterInfo { + id String @id @default(uuid()) @db.Uuid + info Json + expiresAt DateTime + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model ProjectUserRefreshToken { + projectId String + projectUserId String @db.Uuid + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + refreshToken String @unique + expiresAt DateTime? + + projectUser ProjectUser @relation(fields: [projectId, projectUserId], references: [projectId, projectUserId], onDelete: Cascade) + + @@id([projectId, refreshToken]) +} + +model ProjectUserAuthorizationCode { + projectId String + projectUserId String @db.Uuid + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + authorizationCode String @unique + redirectUri String + expiresAt DateTime + + codeChallenge String + codeChallengeMethod String + + newUser Boolean + afterCallbackRedirectUrl String? + + projectUser ProjectUser @relation(fields: [projectId, projectUserId], references: [projectId, projectUserId], onDelete: Cascade) + + @@id([projectId, authorizationCode]) +} + +model ProjectUserEmailVerificationCode { + projectId String + projectUserId String @db.Uuid + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + code String @unique + expiresAt DateTime + usedAt DateTime? + redirectUrl String + + projectUser ProjectUser @relation(fields: [projectId, projectUserId], references: [projectId, projectUserId], onDelete: Cascade) + + @@id([projectId, code]) +} + +model ProjectUserPasswordResetCode { + projectId String + projectUserId String @db.Uuid + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + code String @unique + expiresAt DateTime + usedAt DateTime? + redirectUrl String + + projectUser ProjectUser @relation(fields: [projectId, projectUserId], references: [projectId, projectUserId], onDelete: Cascade) + + @@id([projectId, code]) +} + +model ProjectUserMagicLinkCode { + projectId String + projectUserId String @db.Uuid + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + code String @unique + expiresAt DateTime + usedAt DateTime? + redirectUrl String + newUser Boolean + + projectUser ProjectUser @relation(fields: [projectId, projectUserId], references: [projectId, projectUserId], onDelete: Cascade) + + @@id([projectId, code]) +} + +//#region API keys + +model ApiKeySet { + projectId String + project Project @relation(fields: [projectId], references: [id]) + id String @default(uuid()) @db.Uuid + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + description String + expiresAt DateTime + manuallyRevokedAt DateTime? + publishableClientKey String? @unique + secretServerKey String? @unique + superSecretAdminKey String? @unique + + @@id([projectId, id]) +} + +model EmailServiceConfig { + projectConfigId String @id @db.Uuid + projectConfig ProjectConfig @relation(fields: [projectConfigId], references: [id]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + proxiedEmailServiceConfig ProxiedEmailServiceConfig? + standardEmailServiceConfig StandardEmailServiceConfig? + + emailTemplates EmailTemplate[] +} + +enum EmailTemplateType { + EMAIL_VERIFICATION + PASSWORD_RESET + MAGIC_LINK +} + +model EmailTemplate { + projectConfigId String @db.Uuid + emailServiceConfig EmailServiceConfig @relation(fields: [projectConfigId], references: [projectConfigId]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + content Json + type EmailTemplateType + subject String + + @@id([projectConfigId, type]) +} + +model ProxiedEmailServiceConfig { + projectConfigId String @id @db.Uuid + emailServiceConfig EmailServiceConfig @relation(fields: [projectConfigId], references: [projectConfigId]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model StandardEmailServiceConfig { + projectConfigId String @id @db.Uuid + emailServiceConfig EmailServiceConfig @relation(fields: [projectConfigId], references: [projectConfigId]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + senderName String + senderEmail String + host String + port Int + username String + password String +} + +//#endregion + +//#region OAuth + +// Exactly one of the xyzOAuthConfig variables should be set. +model OAuthProviderConfig { + projectConfigId String @db.Uuid + projectConfig ProjectConfig @relation(fields: [projectConfigId], references: [id]) + id String + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + enabled Boolean @default(true) + + proxiedOAuthConfig ProxiedOAuthProviderConfig? + standardOAuthConfig StandardOAuthProviderConfig? + projectUserOAuthAccounts ProjectUserOAuthAccount[] + + @@id([projectConfigId, id]) +} + +model ProxiedOAuthProviderConfig { + projectConfigId String @db.Uuid + providerConfig OAuthProviderConfig @relation(fields: [projectConfigId, id], references: [projectConfigId, id]) + id String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + type ProxiedOAuthProviderType + + @@id([projectConfigId, id]) + @@unique([projectConfigId, type]) +} + +enum ProxiedOAuthProviderType { + GITHUB + FACEBOOK + GOOGLE + MICROSOFT + SPOTIFY +} + +model StandardOAuthProviderConfig { + projectConfigId String @db.Uuid + providerConfig OAuthProviderConfig @relation(fields: [projectConfigId, id], references: [projectConfigId, id]) + id String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + type StandardOAuthProviderType + clientId String + clientSecret String + + @@id([projectConfigId, id]) +} + +enum StandardOAuthProviderType { + GITHUB + FACEBOOK + GOOGLE + MICROSOFT + SPOTIFY +} + +//#endregion diff --git a/packages/stack-server/prisma/seed.ts b/apps/dashboard/prisma/seed.ts similarity index 100% rename from packages/stack-server/prisma/seed.ts rename to apps/dashboard/prisma/seed.ts diff --git a/packages/stack-server/public/.nojekyll b/apps/dashboard/public/.nojekyll similarity index 100% rename from packages/stack-server/public/.nojekyll rename to apps/dashboard/public/.nojekyll diff --git a/packages/stack-server/public/github-invertocat-white.svg b/apps/dashboard/public/github-invertocat-white.svg similarity index 100% rename from packages/stack-server/public/github-invertocat-white.svg rename to apps/dashboard/public/github-invertocat-white.svg diff --git a/packages/stack-server/public/github-invertocat.svg b/apps/dashboard/public/github-invertocat.svg similarity index 100% rename from packages/stack-server/public/github-invertocat.svg rename to apps/dashboard/public/github-invertocat.svg diff --git a/packages/stack-server/public/logo-bright.svg b/apps/dashboard/public/logo-bright.svg similarity index 100% rename from packages/stack-server/public/logo-bright.svg rename to apps/dashboard/public/logo-bright.svg diff --git a/packages/stack-server/public/logo-full-bright.svg b/apps/dashboard/public/logo-full-bright.svg similarity index 100% rename from packages/stack-server/public/logo-full-bright.svg rename to apps/dashboard/public/logo-full-bright.svg diff --git a/packages/stack-server/public/logo-full.svg b/apps/dashboard/public/logo-full.svg similarity index 100% rename from packages/stack-server/public/logo-full.svg rename to apps/dashboard/public/logo-full.svg diff --git a/packages/stack-server/public/logo.svg b/apps/dashboard/public/logo.svg similarity index 100% rename from packages/stack-server/public/logo.svg rename to apps/dashboard/public/logo.svg diff --git a/apps/dashboard/scripts/generate-keys.ts b/apps/dashboard/scripts/generate-keys.ts new file mode 100644 index 000000000..f18eb3fda --- /dev/null +++ b/apps/dashboard/scripts/generate-keys.ts @@ -0,0 +1,4 @@ +import crypto from "crypto"; +import * as jose from "jose"; + +console.log("Your generated key is:", jose.base64url.encode(crypto.randomBytes(32))); diff --git a/packages/stack-server/sentry.client.config.ts b/apps/dashboard/sentry.client.config.ts similarity index 100% rename from packages/stack-server/sentry.client.config.ts rename to apps/dashboard/sentry.client.config.ts diff --git a/packages/stack-server/sentry.edge.config.ts b/apps/dashboard/sentry.edge.config.ts similarity index 100% rename from packages/stack-server/sentry.edge.config.ts rename to apps/dashboard/sentry.edge.config.ts diff --git a/packages/stack-server/sentry.server.config.ts b/apps/dashboard/sentry.server.config.ts similarity index 100% rename from packages/stack-server/sentry.server.config.ts rename to apps/dashboard/sentry.server.config.ts diff --git a/packages/stack-server/src/app/(main)/(protected)/(outside-dashbaord)/layout.tsx b/apps/dashboard/src/app/(main)/(protected)/(outside-dashbaord)/layout.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/(outside-dashbaord)/layout.tsx rename to apps/dashboard/src/app/(main)/(protected)/(outside-dashbaord)/layout.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/(outside-dashbaord)/loading.tsx b/apps/dashboard/src/app/(main)/(protected)/(outside-dashbaord)/loading.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/(outside-dashbaord)/loading.tsx rename to apps/dashboard/src/app/(main)/(protected)/(outside-dashbaord)/loading.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/(outside-dashbaord)/new-project/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/(outside-dashbaord)/new-project/page-client.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/(outside-dashbaord)/new-project/page-client.tsx rename to apps/dashboard/src/app/(main)/(protected)/(outside-dashbaord)/new-project/page-client.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/(outside-dashbaord)/new-project/page.tsx b/apps/dashboard/src/app/(main)/(protected)/(outside-dashbaord)/new-project/page.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/(outside-dashbaord)/new-project/page.tsx rename to apps/dashboard/src/app/(main)/(protected)/(outside-dashbaord)/new-project/page.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/(outside-dashbaord)/projects/footer.tsx b/apps/dashboard/src/app/(main)/(protected)/(outside-dashbaord)/projects/footer.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/(outside-dashbaord)/projects/footer.tsx rename to apps/dashboard/src/app/(main)/(protected)/(outside-dashbaord)/projects/footer.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/(outside-dashbaord)/projects/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/(outside-dashbaord)/projects/page-client.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/(outside-dashbaord)/projects/page-client.tsx rename to apps/dashboard/src/app/(main)/(protected)/(outside-dashbaord)/projects/page-client.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/(outside-dashbaord)/projects/page.tsx b/apps/dashboard/src/app/(main)/(protected)/(outside-dashbaord)/projects/page.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/(outside-dashbaord)/projects/page.tsx rename to apps/dashboard/src/app/(main)/(protected)/(outside-dashbaord)/projects/page.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/layout.tsx b/apps/dashboard/src/app/(main)/(protected)/layout.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/layout.tsx rename to apps/dashboard/src/app/(main)/(protected)/layout.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/api-keys/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/api-keys/page-client.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/api-keys/page-client.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/api-keys/page-client.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/api-keys/page.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/api-keys/page.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/api-keys/page.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/api-keys/page.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page-client.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page-client.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page-client.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/domains/page.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/domains/page.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/emails/page.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/emails/page.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/emails/templates/[type]/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/templates/[type]/page-client.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/emails/templates/[type]/page-client.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/templates/[type]/page-client.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/emails/templates/[type]/page.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/templates/[type]/page.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/emails/templates/[type]/page.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/templates/[type]/page.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/layout.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/layout.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/layout.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/layout.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/loading.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/loading.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/loading.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/loading.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/onboarding-dialog.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/onboarding-dialog.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/onboarding-dialog.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/onboarding-dialog.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/page-layout.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/page-layout.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/page-layout.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/page-layout.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/project-settings/page.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/project-settings/page.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/route.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/route.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/route.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/route.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/team-permissions/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/team-permissions/page-client.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/team-permissions/page-client.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/team-permissions/page-client.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/team-permissions/page.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/team-permissions/page.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/team-permissions/page.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/team-permissions/page.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/team-settings/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/team-settings/page-client.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/team-settings/page-client.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/team-settings/page-client.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/team-settings/page.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/team-settings/page.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/team-settings/page.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/team-settings/page.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/teams/[teamId]/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/teams/[teamId]/page-client.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/teams/[teamId]/page-client.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/teams/[teamId]/page-client.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/teams/[teamId]/page.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/teams/[teamId]/page.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/teams/[teamId]/page.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/teams/[teamId]/page.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/teams/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/teams/page-client.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/teams/page-client.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/teams/page-client.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/teams/page.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/teams/page.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/teams/page.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/teams/page.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/use-admin-app.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/use-admin-app.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/use-admin-app.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/use-admin-app.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/users/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/page-client.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/users/page-client.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/page-client.tsx diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/users/page.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/page.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/users/page.tsx rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/page.tsx diff --git a/packages/stack-server/src/app/(main)/handler/[...stack]/layout.tsx b/apps/dashboard/src/app/(main)/handler/[...stack]/layout.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/handler/[...stack]/layout.tsx rename to apps/dashboard/src/app/(main)/handler/[...stack]/layout.tsx diff --git a/packages/stack-server/src/app/(main)/handler/[...stack]/page.tsx b/apps/dashboard/src/app/(main)/handler/[...stack]/page.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/handler/[...stack]/page.tsx rename to apps/dashboard/src/app/(main)/handler/[...stack]/page.tsx diff --git a/packages/stack-server/src/app/(main)/route.tsx b/apps/dashboard/src/app/(main)/route.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/route.tsx rename to apps/dashboard/src/app/(main)/route.tsx diff --git a/packages/stack-server/src/app/(main)/wizard-congrats/actions.tsx b/apps/dashboard/src/app/(main)/wizard-congrats/actions.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/wizard-congrats/actions.tsx rename to apps/dashboard/src/app/(main)/wizard-congrats/actions.tsx diff --git a/packages/stack-server/src/app/(main)/wizard-congrats/page.tsx b/apps/dashboard/src/app/(main)/wizard-congrats/page.tsx similarity index 100% rename from packages/stack-server/src/app/(main)/wizard-congrats/page.tsx rename to apps/dashboard/src/app/(main)/wizard-congrats/page.tsx diff --git a/packages/stack-server/src/app/api/v1/api-keys/[apiKeyId]/route.tsx b/apps/dashboard/src/app/api/v1/api-keys/[apiKeyId]/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/api-keys/[apiKeyId]/route.tsx rename to apps/dashboard/src/app/api/v1/api-keys/[apiKeyId]/route.tsx diff --git a/packages/stack-server/src/app/api/v1/api-keys/route.tsx b/apps/dashboard/src/app/api/v1/api-keys/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/api-keys/route.tsx rename to apps/dashboard/src/app/api/v1/api-keys/route.tsx diff --git a/packages/stack-server/src/app/api/v1/auth/access-token/[provider]/route.tsx b/apps/dashboard/src/app/api/v1/auth/access-token/[provider]/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/auth/access-token/[provider]/route.tsx rename to apps/dashboard/src/app/api/v1/auth/access-token/[provider]/route.tsx diff --git a/packages/stack-server/src/app/api/v1/auth/authorize/[provider]/route.tsx b/apps/dashboard/src/app/api/v1/auth/authorize/[provider]/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/auth/authorize/[provider]/route.tsx rename to apps/dashboard/src/app/api/v1/auth/authorize/[provider]/route.tsx diff --git a/packages/stack-server/src/app/api/v1/auth/callback/[provider]/route.tsx b/apps/dashboard/src/app/api/v1/auth/callback/[provider]/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/auth/callback/[provider]/route.tsx rename to apps/dashboard/src/app/api/v1/auth/callback/[provider]/route.tsx diff --git a/packages/stack-server/src/app/api/v1/auth/email-verification/route.tsx b/apps/dashboard/src/app/api/v1/auth/email-verification/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/auth/email-verification/route.tsx rename to apps/dashboard/src/app/api/v1/auth/email-verification/route.tsx diff --git a/packages/stack-server/src/app/api/v1/auth/forgot-password/route.tsx b/apps/dashboard/src/app/api/v1/auth/forgot-password/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/auth/forgot-password/route.tsx rename to apps/dashboard/src/app/api/v1/auth/forgot-password/route.tsx diff --git a/packages/stack-server/src/app/api/v1/auth/magic-link-verification/route.tsx b/apps/dashboard/src/app/api/v1/auth/magic-link-verification/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/auth/magic-link-verification/route.tsx rename to apps/dashboard/src/app/api/v1/auth/magic-link-verification/route.tsx diff --git a/packages/stack-server/src/app/api/v1/auth/password-reset/route.tsx b/apps/dashboard/src/app/api/v1/auth/password-reset/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/auth/password-reset/route.tsx rename to apps/dashboard/src/app/api/v1/auth/password-reset/route.tsx diff --git a/packages/stack-server/src/app/api/v1/auth/send-magic-link/route.tsx b/apps/dashboard/src/app/api/v1/auth/send-magic-link/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/auth/send-magic-link/route.tsx rename to apps/dashboard/src/app/api/v1/auth/send-magic-link/route.tsx diff --git a/packages/stack-server/src/app/api/v1/auth/send-verification-email/route.tsx b/apps/dashboard/src/app/api/v1/auth/send-verification-email/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/auth/send-verification-email/route.tsx rename to apps/dashboard/src/app/api/v1/auth/send-verification-email/route.tsx diff --git a/packages/stack-server/src/app/api/v1/auth/signin/route.tsx b/apps/dashboard/src/app/api/v1/auth/signin/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/auth/signin/route.tsx rename to apps/dashboard/src/app/api/v1/auth/signin/route.tsx diff --git a/packages/stack-server/src/app/api/v1/auth/signout/route.tsx b/apps/dashboard/src/app/api/v1/auth/signout/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/auth/signout/route.tsx rename to apps/dashboard/src/app/api/v1/auth/signout/route.tsx diff --git a/packages/stack-server/src/app/api/v1/auth/signup/route.tsx b/apps/dashboard/src/app/api/v1/auth/signup/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/auth/signup/route.tsx rename to apps/dashboard/src/app/api/v1/auth/signup/route.tsx diff --git a/packages/stack-server/src/app/api/v1/auth/token/route.tsx b/apps/dashboard/src/app/api/v1/auth/token/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/auth/token/route.tsx rename to apps/dashboard/src/app/api/v1/auth/token/route.tsx diff --git a/packages/stack-server/src/app/api/v1/auth/update-password/route.tsx b/apps/dashboard/src/app/api/v1/auth/update-password/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/auth/update-password/route.tsx rename to apps/dashboard/src/app/api/v1/auth/update-password/route.tsx diff --git a/packages/stack-server/src/app/api/v1/check-feature-support/route.tsx b/apps/dashboard/src/app/api/v1/check-feature-support/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/check-feature-support/route.tsx rename to apps/dashboard/src/app/api/v1/check-feature-support/route.tsx diff --git a/packages/stack-server/src/app/api/v1/current-user/crud.tsx b/apps/dashboard/src/app/api/v1/current-user/crud.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/current-user/crud.tsx rename to apps/dashboard/src/app/api/v1/current-user/crud.tsx diff --git a/packages/stack-server/src/app/api/v1/current-user/route.tsx b/apps/dashboard/src/app/api/v1/current-user/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/current-user/route.tsx rename to apps/dashboard/src/app/api/v1/current-user/route.tsx diff --git a/packages/stack-server/src/app/api/v1/current-user/teams/[teamId]/permissions/route.tsx b/apps/dashboard/src/app/api/v1/current-user/teams/[teamId]/permissions/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/current-user/teams/[teamId]/permissions/route.tsx rename to apps/dashboard/src/app/api/v1/current-user/teams/[teamId]/permissions/route.tsx diff --git a/packages/stack-server/src/app/api/v1/current-user/teams/route.tsx b/apps/dashboard/src/app/api/v1/current-user/teams/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/current-user/teams/route.tsx rename to apps/dashboard/src/app/api/v1/current-user/teams/route.tsx diff --git a/packages/stack-server/src/app/api/v1/email-templates/[type]/route.tsx b/apps/dashboard/src/app/api/v1/email-templates/[type]/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/email-templates/[type]/route.tsx rename to apps/dashboard/src/app/api/v1/email-templates/[type]/route.tsx diff --git a/packages/stack-server/src/app/api/v1/email-templates/route.tsx b/apps/dashboard/src/app/api/v1/email-templates/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/email-templates/route.tsx rename to apps/dashboard/src/app/api/v1/email-templates/route.tsx diff --git a/packages/stack-server/src/app/api/v1/permission-definitions/[permId]/route.tsx b/apps/dashboard/src/app/api/v1/permission-definitions/[permId]/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/permission-definitions/[permId]/route.tsx rename to apps/dashboard/src/app/api/v1/permission-definitions/[permId]/route.tsx diff --git a/packages/stack-server/src/app/api/v1/permission-definitions/route.tsx b/apps/dashboard/src/app/api/v1/permission-definitions/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/permission-definitions/route.tsx rename to apps/dashboard/src/app/api/v1/permission-definitions/route.tsx diff --git a/packages/stack-server/src/app/api/v1/projects/[projectId]/route.tsx b/apps/dashboard/src/app/api/v1/projects/[projectId]/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/projects/[projectId]/route.tsx rename to apps/dashboard/src/app/api/v1/projects/[projectId]/route.tsx diff --git a/packages/stack-server/src/app/api/v1/projects/route.tsx b/apps/dashboard/src/app/api/v1/projects/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/projects/route.tsx rename to apps/dashboard/src/app/api/v1/projects/route.tsx diff --git a/apps/dashboard/src/app/api/v1/route.ts b/apps/dashboard/src/app/api/v1/route.ts new file mode 100644 index 000000000..5039b4f1a --- /dev/null +++ b/apps/dashboard/src/app/api/v1/route.ts @@ -0,0 +1,33 @@ +import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; +import { deindent, typedCapitalize } from "@stackframe/stack-shared/dist/utils/strings"; +import * as yup from "yup"; + +export const GET = createSmartRouteHandler({ + request: yup.object({ + auth: yup.object({ + type: yup.mixed(), + user: yup.mixed(), + project: yup.mixed(), + }).nullable(), + method: yup.string().oneOf(["GET"]).required(), + }), + response: yup.object({ + statusCode: yup.number().oneOf([200]).required(), + bodyType: yup.string().oneOf(["text"]).required(), + body: yup.string().required(), + }), + handler: async (req) => { + return { + statusCode: 200, + bodyType: "text", + body: deindent` + Welcome to the Stack API endpoint! Please refer to the documentation at https://docs.stack-auth.com. + + Authentication: ${!req.auth ? "None" : deindent` ${typedCapitalize(req.auth.type)} + Project: ${req.auth.project ? req.auth.project.id : "None"} + User: ${req.auth.user ? req.auth.user.primaryEmail ?? req.auth.user.id : "None"} + `} + `, + }; + }, +}); diff --git a/packages/stack-server/src/app/api/v1/teams/[teamId]/route.tsx b/apps/dashboard/src/app/api/v1/teams/[teamId]/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/teams/[teamId]/route.tsx rename to apps/dashboard/src/app/api/v1/teams/[teamId]/route.tsx diff --git a/packages/stack-server/src/app/api/v1/teams/[teamId]/users/[userId]/permissions/[permId]/route.tsx b/apps/dashboard/src/app/api/v1/teams/[teamId]/users/[userId]/permissions/[permId]/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/teams/[teamId]/users/[userId]/permissions/[permId]/route.tsx rename to apps/dashboard/src/app/api/v1/teams/[teamId]/users/[userId]/permissions/[permId]/route.tsx diff --git a/packages/stack-server/src/app/api/v1/teams/[teamId]/users/[userId]/permissions/route.tsx b/apps/dashboard/src/app/api/v1/teams/[teamId]/users/[userId]/permissions/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/teams/[teamId]/users/[userId]/permissions/route.tsx rename to apps/dashboard/src/app/api/v1/teams/[teamId]/users/[userId]/permissions/route.tsx diff --git a/packages/stack-server/src/app/api/v1/teams/[teamId]/users/[userId]/route.tsx b/apps/dashboard/src/app/api/v1/teams/[teamId]/users/[userId]/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/teams/[teamId]/users/[userId]/route.tsx rename to apps/dashboard/src/app/api/v1/teams/[teamId]/users/[userId]/route.tsx diff --git a/packages/stack-server/src/app/api/v1/teams/[teamId]/users/route.tsx b/apps/dashboard/src/app/api/v1/teams/[teamId]/users/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/teams/[teamId]/users/route.tsx rename to apps/dashboard/src/app/api/v1/teams/[teamId]/users/route.tsx diff --git a/packages/stack-server/src/app/api/v1/teams/route.tsx b/apps/dashboard/src/app/api/v1/teams/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/teams/route.tsx rename to apps/dashboard/src/app/api/v1/teams/route.tsx diff --git a/packages/stack-server/src/app/api/v1/users/[userId]/route.tsx b/apps/dashboard/src/app/api/v1/users/[userId]/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/users/[userId]/route.tsx rename to apps/dashboard/src/app/api/v1/users/[userId]/route.tsx diff --git a/packages/stack-server/src/app/api/v1/users/crud.tsx b/apps/dashboard/src/app/api/v1/users/crud.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/users/crud.tsx rename to apps/dashboard/src/app/api/v1/users/crud.tsx diff --git a/packages/stack-server/src/app/api/v1/users/route.tsx b/apps/dashboard/src/app/api/v1/users/route.tsx similarity index 100% rename from packages/stack-server/src/app/api/v1/users/route.tsx rename to apps/dashboard/src/app/api/v1/users/route.tsx diff --git a/apps/dashboard/src/app/favicon.ico b/apps/dashboard/src/app/favicon.ico new file mode 100644 index 000000000..b2197e144 Binary files /dev/null and b/apps/dashboard/src/app/favicon.ico differ diff --git a/packages/stack-server/src/app/global-error.tsx b/apps/dashboard/src/app/global-error.tsx similarity index 100% rename from packages/stack-server/src/app/global-error.tsx rename to apps/dashboard/src/app/global-error.tsx diff --git a/packages/stack-server/src/app/globals.css b/apps/dashboard/src/app/globals.css similarity index 100% rename from packages/stack-server/src/app/globals.css rename to apps/dashboard/src/app/globals.css diff --git a/packages/stack-server/src/app/layout.tsx b/apps/dashboard/src/app/layout.tsx similarity index 100% rename from packages/stack-server/src/app/layout.tsx rename to apps/dashboard/src/app/layout.tsx diff --git a/packages/stack-server/src/app/loading.tsx b/apps/dashboard/src/app/loading.tsx similarity index 100% rename from packages/stack-server/src/app/loading.tsx rename to apps/dashboard/src/app/loading.tsx diff --git a/packages/stack-server/src/app/not-found.tsx b/apps/dashboard/src/app/not-found.tsx similarity index 100% rename from packages/stack-server/src/app/not-found.tsx rename to apps/dashboard/src/app/not-found.tsx diff --git a/packages/stack-server/src/app/providers.tsx b/apps/dashboard/src/app/providers.tsx similarity index 100% rename from packages/stack-server/src/app/providers.tsx rename to apps/dashboard/src/app/providers.tsx diff --git a/packages/stack-server/src/components/action-dialog.tsx b/apps/dashboard/src/components/action-dialog.tsx similarity index 100% rename from packages/stack-server/src/components/action-dialog.tsx rename to apps/dashboard/src/components/action-dialog.tsx diff --git a/packages/stack-server/src/components/browser-frame/LICENSE b/apps/dashboard/src/components/browser-frame/LICENSE similarity index 100% rename from packages/stack-server/src/components/browser-frame/LICENSE rename to apps/dashboard/src/components/browser-frame/LICENSE diff --git a/packages/stack-server/src/components/browser-frame/index.tsx b/apps/dashboard/src/components/browser-frame/index.tsx similarity index 100% rename from packages/stack-server/src/components/browser-frame/index.tsx rename to apps/dashboard/src/components/browser-frame/index.tsx diff --git a/packages/stack-server/src/components/confetti.tsx b/apps/dashboard/src/components/confetti.tsx similarity index 100% rename from packages/stack-server/src/components/confetti.tsx rename to apps/dashboard/src/components/confetti.tsx diff --git a/packages/stack-server/src/components/copy-button.tsx b/apps/dashboard/src/components/copy-button.tsx similarity index 100% rename from packages/stack-server/src/components/copy-button.tsx rename to apps/dashboard/src/components/copy-button.tsx diff --git a/packages/stack-server/src/components/data-table/api-key-table.tsx b/apps/dashboard/src/components/data-table/api-key-table.tsx similarity index 100% rename from packages/stack-server/src/components/data-table/api-key-table.tsx rename to apps/dashboard/src/components/data-table/api-key-table.tsx diff --git a/packages/stack-server/src/components/data-table/elements/cells.tsx b/apps/dashboard/src/components/data-table/elements/cells.tsx similarity index 100% rename from packages/stack-server/src/components/data-table/elements/cells.tsx rename to apps/dashboard/src/components/data-table/elements/cells.tsx diff --git a/packages/stack-server/src/components/data-table/elements/column-header.tsx b/apps/dashboard/src/components/data-table/elements/column-header.tsx similarity index 100% rename from packages/stack-server/src/components/data-table/elements/column-header.tsx rename to apps/dashboard/src/components/data-table/elements/column-header.tsx diff --git a/packages/stack-server/src/components/data-table/elements/data-table.tsx b/apps/dashboard/src/components/data-table/elements/data-table.tsx similarity index 100% rename from packages/stack-server/src/components/data-table/elements/data-table.tsx rename to apps/dashboard/src/components/data-table/elements/data-table.tsx diff --git a/packages/stack-server/src/components/data-table/elements/faceted-filter.tsx b/apps/dashboard/src/components/data-table/elements/faceted-filter.tsx similarity index 100% rename from packages/stack-server/src/components/data-table/elements/faceted-filter.tsx rename to apps/dashboard/src/components/data-table/elements/faceted-filter.tsx diff --git a/packages/stack-server/src/components/data-table/elements/pagination.tsx b/apps/dashboard/src/components/data-table/elements/pagination.tsx similarity index 100% rename from packages/stack-server/src/components/data-table/elements/pagination.tsx rename to apps/dashboard/src/components/data-table/elements/pagination.tsx diff --git a/packages/stack-server/src/components/data-table/elements/toolbar-items.tsx b/apps/dashboard/src/components/data-table/elements/toolbar-items.tsx similarity index 100% rename from packages/stack-server/src/components/data-table/elements/toolbar-items.tsx rename to apps/dashboard/src/components/data-table/elements/toolbar-items.tsx diff --git a/packages/stack-server/src/components/data-table/elements/toolbar.tsx b/apps/dashboard/src/components/data-table/elements/toolbar.tsx similarity index 100% rename from packages/stack-server/src/components/data-table/elements/toolbar.tsx rename to apps/dashboard/src/components/data-table/elements/toolbar.tsx diff --git a/packages/stack-server/src/components/data-table/elements/utils.tsx b/apps/dashboard/src/components/data-table/elements/utils.tsx similarity index 100% rename from packages/stack-server/src/components/data-table/elements/utils.tsx rename to apps/dashboard/src/components/data-table/elements/utils.tsx diff --git a/packages/stack-server/src/components/data-table/elements/view-options.tsx b/apps/dashboard/src/components/data-table/elements/view-options.tsx similarity index 100% rename from packages/stack-server/src/components/data-table/elements/view-options.tsx rename to apps/dashboard/src/components/data-table/elements/view-options.tsx diff --git a/packages/stack-server/src/components/data-table/team-member-table.tsx b/apps/dashboard/src/components/data-table/team-member-table.tsx similarity index 100% rename from packages/stack-server/src/components/data-table/team-member-table.tsx rename to apps/dashboard/src/components/data-table/team-member-table.tsx diff --git a/packages/stack-server/src/components/data-table/team-permission-table.tsx b/apps/dashboard/src/components/data-table/team-permission-table.tsx similarity index 100% rename from packages/stack-server/src/components/data-table/team-permission-table.tsx rename to apps/dashboard/src/components/data-table/team-permission-table.tsx diff --git a/packages/stack-server/src/components/data-table/team-table.tsx b/apps/dashboard/src/components/data-table/team-table.tsx similarity index 100% rename from packages/stack-server/src/components/data-table/team-table.tsx rename to apps/dashboard/src/components/data-table/team-table.tsx diff --git a/packages/stack-server/src/components/data-table/user-table.tsx b/apps/dashboard/src/components/data-table/user-table.tsx similarity index 100% rename from packages/stack-server/src/components/data-table/user-table.tsx rename to apps/dashboard/src/components/data-table/user-table.tsx diff --git a/packages/stack-server/src/components/dev-error-notifier.tsx b/apps/dashboard/src/components/dev-error-notifier.tsx similarity index 100% rename from packages/stack-server/src/components/dev-error-notifier.tsx rename to apps/dashboard/src/components/dev-error-notifier.tsx diff --git a/packages/stack-server/src/components/email-editor/LICENSE b/apps/dashboard/src/components/email-editor/LICENSE similarity index 100% rename from packages/stack-server/src/components/email-editor/LICENSE rename to apps/dashboard/src/components/email-editor/LICENSE diff --git a/packages/stack-server/src/components/email-editor/blocks/block-button.tsx b/apps/dashboard/src/components/email-editor/blocks/block-button.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/blocks/block-button.tsx rename to apps/dashboard/src/components/email-editor/blocks/block-button.tsx diff --git a/packages/stack-server/src/components/email-editor/blocks/block-columns-container.tsx b/apps/dashboard/src/components/email-editor/blocks/block-columns-container.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/blocks/block-columns-container.tsx rename to apps/dashboard/src/components/email-editor/blocks/block-columns-container.tsx diff --git a/packages/stack-server/src/components/email-editor/blocks/block-container.tsx b/apps/dashboard/src/components/email-editor/blocks/block-container.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/blocks/block-container.tsx rename to apps/dashboard/src/components/email-editor/blocks/block-container.tsx diff --git a/packages/stack-server/src/components/email-editor/blocks/block-divider.tsx b/apps/dashboard/src/components/email-editor/blocks/block-divider.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/blocks/block-divider.tsx rename to apps/dashboard/src/components/email-editor/blocks/block-divider.tsx diff --git a/packages/stack-server/src/components/email-editor/blocks/block-heading.tsx b/apps/dashboard/src/components/email-editor/blocks/block-heading.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/blocks/block-heading.tsx rename to apps/dashboard/src/components/email-editor/blocks/block-heading.tsx diff --git a/packages/stack-server/src/components/email-editor/blocks/block-image.tsx b/apps/dashboard/src/components/email-editor/blocks/block-image.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/blocks/block-image.tsx rename to apps/dashboard/src/components/email-editor/blocks/block-image.tsx diff --git a/packages/stack-server/src/components/email-editor/blocks/block-spacer.tsx b/apps/dashboard/src/components/email-editor/blocks/block-spacer.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/blocks/block-spacer.tsx rename to apps/dashboard/src/components/email-editor/blocks/block-spacer.tsx diff --git a/packages/stack-server/src/components/email-editor/blocks/block-text.tsx b/apps/dashboard/src/components/email-editor/blocks/block-text.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/blocks/block-text.tsx rename to apps/dashboard/src/components/email-editor/blocks/block-text.tsx diff --git a/packages/stack-server/src/components/email-editor/document-core/builders/buildBlockComponent.tsx b/apps/dashboard/src/components/email-editor/document-core/builders/buildBlockComponent.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/document-core/builders/buildBlockComponent.tsx rename to apps/dashboard/src/components/email-editor/document-core/builders/buildBlockComponent.tsx diff --git a/packages/stack-server/src/components/email-editor/document-core/builders/buildBlockConfigurationDictionary.ts b/apps/dashboard/src/components/email-editor/document-core/builders/buildBlockConfigurationDictionary.ts similarity index 100% rename from packages/stack-server/src/components/email-editor/document-core/builders/buildBlockConfigurationDictionary.ts rename to apps/dashboard/src/components/email-editor/document-core/builders/buildBlockConfigurationDictionary.ts diff --git a/packages/stack-server/src/components/email-editor/document-core/builders/buildBlockConfigurationSchema.ts b/apps/dashboard/src/components/email-editor/document-core/builders/buildBlockConfigurationSchema.ts similarity index 100% rename from packages/stack-server/src/components/email-editor/document-core/builders/buildBlockConfigurationSchema.ts rename to apps/dashboard/src/components/email-editor/document-core/builders/buildBlockConfigurationSchema.ts diff --git a/packages/stack-server/src/components/email-editor/document-core/index.ts b/apps/dashboard/src/components/email-editor/document-core/index.ts similarity index 100% rename from packages/stack-server/src/components/email-editor/document-core/index.ts rename to apps/dashboard/src/components/email-editor/document-core/index.ts diff --git a/packages/stack-server/src/components/email-editor/documents/blocks/columns-container/columns-container-editor.tsx b/apps/dashboard/src/components/email-editor/documents/blocks/columns-container/columns-container-editor.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/documents/blocks/columns-container/columns-container-editor.tsx rename to apps/dashboard/src/components/email-editor/documents/blocks/columns-container/columns-container-editor.tsx diff --git a/packages/stack-server/src/components/email-editor/documents/blocks/columns-container/columns-container-props-schema.ts b/apps/dashboard/src/components/email-editor/documents/blocks/columns-container/columns-container-props-schema.ts similarity index 100% rename from packages/stack-server/src/components/email-editor/documents/blocks/columns-container/columns-container-props-schema.ts rename to apps/dashboard/src/components/email-editor/documents/blocks/columns-container/columns-container-props-schema.ts diff --git a/packages/stack-server/src/components/email-editor/documents/blocks/container/container-editor.tsx b/apps/dashboard/src/components/email-editor/documents/blocks/container/container-editor.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/documents/blocks/container/container-editor.tsx rename to apps/dashboard/src/components/email-editor/documents/blocks/container/container-editor.tsx diff --git a/packages/stack-server/src/components/email-editor/documents/blocks/container/container-props-schema.tsx b/apps/dashboard/src/components/email-editor/documents/blocks/container/container-props-schema.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/documents/blocks/container/container-props-schema.tsx rename to apps/dashboard/src/components/email-editor/documents/blocks/container/container-props-schema.tsx diff --git a/packages/stack-server/src/components/email-editor/documents/blocks/email-layout/email-layout-editor.tsx b/apps/dashboard/src/components/email-editor/documents/blocks/email-layout/email-layout-editor.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/documents/blocks/email-layout/email-layout-editor.tsx rename to apps/dashboard/src/components/email-editor/documents/blocks/email-layout/email-layout-editor.tsx diff --git a/packages/stack-server/src/components/email-editor/documents/blocks/email-layout/email-layout-props-schema.tsx b/apps/dashboard/src/components/email-editor/documents/blocks/email-layout/email-layout-props-schema.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/documents/blocks/email-layout/email-layout-props-schema.tsx rename to apps/dashboard/src/components/email-editor/documents/blocks/email-layout/email-layout-props-schema.tsx diff --git a/packages/stack-server/src/components/email-editor/documents/blocks/helpers/block-wrappers/editor-block-wrapper.tsx b/apps/dashboard/src/components/email-editor/documents/blocks/helpers/block-wrappers/editor-block-wrapper.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/documents/blocks/helpers/block-wrappers/editor-block-wrapper.tsx rename to apps/dashboard/src/components/email-editor/documents/blocks/helpers/block-wrappers/editor-block-wrapper.tsx diff --git a/packages/stack-server/src/components/email-editor/documents/blocks/helpers/block-wrappers/reader-block-wrapper.tsx b/apps/dashboard/src/components/email-editor/documents/blocks/helpers/block-wrappers/reader-block-wrapper.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/documents/blocks/helpers/block-wrappers/reader-block-wrapper.tsx rename to apps/dashboard/src/components/email-editor/documents/blocks/helpers/block-wrappers/reader-block-wrapper.tsx diff --git a/packages/stack-server/src/components/email-editor/documents/blocks/helpers/block-wrappers/tune-menu.tsx b/apps/dashboard/src/components/email-editor/documents/blocks/helpers/block-wrappers/tune-menu.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/documents/blocks/helpers/block-wrappers/tune-menu.tsx rename to apps/dashboard/src/components/email-editor/documents/blocks/helpers/block-wrappers/tune-menu.tsx diff --git a/packages/stack-server/src/components/email-editor/documents/blocks/helpers/editor-children-ids/add-block-menu/block-button.tsx b/apps/dashboard/src/components/email-editor/documents/blocks/helpers/editor-children-ids/add-block-menu/block-button.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/documents/blocks/helpers/editor-children-ids/add-block-menu/block-button.tsx rename to apps/dashboard/src/components/email-editor/documents/blocks/helpers/editor-children-ids/add-block-menu/block-button.tsx diff --git a/packages/stack-server/src/components/email-editor/documents/blocks/helpers/editor-children-ids/add-block-menu/blocks-menu.tsx b/apps/dashboard/src/components/email-editor/documents/blocks/helpers/editor-children-ids/add-block-menu/blocks-menu.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/documents/blocks/helpers/editor-children-ids/add-block-menu/blocks-menu.tsx rename to apps/dashboard/src/components/email-editor/documents/blocks/helpers/editor-children-ids/add-block-menu/blocks-menu.tsx diff --git a/packages/stack-server/src/components/email-editor/documents/blocks/helpers/editor-children-ids/add-block-menu/buttons.tsx b/apps/dashboard/src/components/email-editor/documents/blocks/helpers/editor-children-ids/add-block-menu/buttons.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/documents/blocks/helpers/editor-children-ids/add-block-menu/buttons.tsx rename to apps/dashboard/src/components/email-editor/documents/blocks/helpers/editor-children-ids/add-block-menu/buttons.tsx diff --git a/packages/stack-server/src/components/email-editor/documents/blocks/helpers/editor-children-ids/add-block-menu/index.tsx b/apps/dashboard/src/components/email-editor/documents/blocks/helpers/editor-children-ids/add-block-menu/index.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/documents/blocks/helpers/editor-children-ids/add-block-menu/index.tsx rename to apps/dashboard/src/components/email-editor/documents/blocks/helpers/editor-children-ids/add-block-menu/index.tsx diff --git a/packages/stack-server/src/components/email-editor/documents/blocks/helpers/editor-children-ids/index.tsx b/apps/dashboard/src/components/email-editor/documents/blocks/helpers/editor-children-ids/index.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/documents/blocks/helpers/editor-children-ids/index.tsx rename to apps/dashboard/src/components/email-editor/documents/blocks/helpers/editor-children-ids/index.tsx diff --git a/packages/stack-server/src/components/email-editor/documents/blocks/helpers/font-family.ts b/apps/dashboard/src/components/email-editor/documents/blocks/helpers/font-family.ts similarity index 100% rename from packages/stack-server/src/components/email-editor/documents/blocks/helpers/font-family.ts rename to apps/dashboard/src/components/email-editor/documents/blocks/helpers/font-family.ts diff --git a/packages/stack-server/src/components/email-editor/documents/blocks/helpers/t-style.ts b/apps/dashboard/src/components/email-editor/documents/blocks/helpers/t-style.ts similarity index 100% rename from packages/stack-server/src/components/email-editor/documents/blocks/helpers/t-style.ts rename to apps/dashboard/src/components/email-editor/documents/blocks/helpers/t-style.ts diff --git a/packages/stack-server/src/components/email-editor/documents/blocks/helpers/zod.ts b/apps/dashboard/src/components/email-editor/documents/blocks/helpers/zod.ts similarity index 100% rename from packages/stack-server/src/components/email-editor/documents/blocks/helpers/zod.ts rename to apps/dashboard/src/components/email-editor/documents/blocks/helpers/zod.ts diff --git a/packages/stack-server/src/components/email-editor/documents/editor/core.tsx b/apps/dashboard/src/components/email-editor/documents/editor/core.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/documents/editor/core.tsx rename to apps/dashboard/src/components/email-editor/documents/editor/core.tsx diff --git a/packages/stack-server/src/components/email-editor/documents/editor/editor-block.tsx b/apps/dashboard/src/components/email-editor/documents/editor/editor-block.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/documents/editor/editor-block.tsx rename to apps/dashboard/src/components/email-editor/documents/editor/editor-block.tsx diff --git a/packages/stack-server/src/components/email-editor/documents/editor/editor-context.tsx b/apps/dashboard/src/components/email-editor/documents/editor/editor-context.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/documents/editor/editor-context.tsx rename to apps/dashboard/src/components/email-editor/documents/editor/editor-context.tsx diff --git a/packages/stack-server/src/components/email-editor/editor.tsx b/apps/dashboard/src/components/email-editor/editor.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/editor.tsx rename to apps/dashboard/src/components/email-editor/editor.tsx diff --git a/packages/stack-server/src/components/email-editor/email-builder/blocks/columns-container/columns-container-props-schema.ts b/apps/dashboard/src/components/email-editor/email-builder/blocks/columns-container/columns-container-props-schema.ts similarity index 100% rename from packages/stack-server/src/components/email-editor/email-builder/blocks/columns-container/columns-container-props-schema.ts rename to apps/dashboard/src/components/email-editor/email-builder/blocks/columns-container/columns-container-props-schema.ts diff --git a/packages/stack-server/src/components/email-editor/email-builder/blocks/columns-container/columns-container-reader.tsx b/apps/dashboard/src/components/email-editor/email-builder/blocks/columns-container/columns-container-reader.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/email-builder/blocks/columns-container/columns-container-reader.tsx rename to apps/dashboard/src/components/email-editor/email-builder/blocks/columns-container/columns-container-reader.tsx diff --git a/packages/stack-server/src/components/email-editor/email-builder/blocks/container/container-props-schema.tsx b/apps/dashboard/src/components/email-editor/email-builder/blocks/container/container-props-schema.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/email-builder/blocks/container/container-props-schema.tsx rename to apps/dashboard/src/components/email-editor/email-builder/blocks/container/container-props-schema.tsx diff --git a/packages/stack-server/src/components/email-editor/email-builder/blocks/container/container-reader.tsx b/apps/dashboard/src/components/email-editor/email-builder/blocks/container/container-reader.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/email-builder/blocks/container/container-reader.tsx rename to apps/dashboard/src/components/email-editor/email-builder/blocks/container/container-reader.tsx diff --git a/packages/stack-server/src/components/email-editor/email-builder/blocks/email-layout/email-layout-props-schema.tsx b/apps/dashboard/src/components/email-editor/email-builder/blocks/email-layout/email-layout-props-schema.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/email-builder/blocks/email-layout/email-layout-props-schema.tsx rename to apps/dashboard/src/components/email-editor/email-builder/blocks/email-layout/email-layout-props-schema.tsx diff --git a/packages/stack-server/src/components/email-editor/email-builder/blocks/email-layout/email-layout-reader.tsx b/apps/dashboard/src/components/email-editor/email-builder/blocks/email-layout/email-layout-reader.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/email-builder/blocks/email-layout/email-layout-reader.tsx rename to apps/dashboard/src/components/email-editor/email-builder/blocks/email-layout/email-layout-reader.tsx diff --git a/packages/stack-server/src/components/email-editor/email-builder/index.ts b/apps/dashboard/src/components/email-editor/email-builder/index.ts similarity index 100% rename from packages/stack-server/src/components/email-editor/email-builder/index.ts rename to apps/dashboard/src/components/email-editor/email-builder/index.ts diff --git a/packages/stack-server/src/components/email-editor/email-builder/reader/core.tsx b/apps/dashboard/src/components/email-editor/email-builder/reader/core.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/email-builder/reader/core.tsx rename to apps/dashboard/src/components/email-editor/email-builder/reader/core.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/configuration-panel/index.tsx b/apps/dashboard/src/components/email-editor/sidebar/configuration-panel/index.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/configuration-panel/index.tsx rename to apps/dashboard/src/components/email-editor/sidebar/configuration-panel/index.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/button-sidebar-panel.tsx b/apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/button-sidebar-panel.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/button-sidebar-panel.tsx rename to apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/button-sidebar-panel.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/columns-container-sidebar-panel.tsx b/apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/columns-container-sidebar-panel.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/columns-container-sidebar-panel.tsx rename to apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/columns-container-sidebar-panel.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/container-sidebar-panel.tsx b/apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/container-sidebar-panel.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/container-sidebar-panel.tsx rename to apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/container-sidebar-panel.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/divider-sidebar-panel.tsx b/apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/divider-sidebar-panel.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/divider-sidebar-panel.tsx rename to apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/divider-sidebar-panel.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/heading-sidebar-panel.tsx b/apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/heading-sidebar-panel.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/heading-sidebar-panel.tsx rename to apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/heading-sidebar-panel.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/base-sidebar-panel.tsx b/apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/base-sidebar-panel.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/base-sidebar-panel.tsx rename to apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/base-sidebar-panel.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/color-input/base-color-input.tsx b/apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/color-input/base-color-input.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/color-input/base-color-input.tsx rename to apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/color-input/base-color-input.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/color-input/index.tsx b/apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/color-input/index.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/color-input/index.tsx rename to apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/color-input/index.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/column-widths-input.tsx b/apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/column-widths-input.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/column-widths-input.tsx rename to apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/column-widths-input.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/font-family.tsx b/apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/font-family.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/font-family.tsx rename to apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/font-family.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/font-size-input.tsx b/apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/font-size-input.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/font-size-input.tsx rename to apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/font-size-input.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/font-weight-input.tsx b/apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/font-weight-input.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/font-weight-input.tsx rename to apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/font-weight-input.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/padding-input.tsx b/apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/padding-input.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/padding-input.tsx rename to apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/padding-input.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/raw/raw-slider-input.tsx b/apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/raw/raw-slider-input.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/raw/raw-slider-input.tsx rename to apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/raw/raw-slider-input.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/single-toggle-group.tsx b/apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/single-toggle-group.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/single-toggle-group.tsx rename to apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/single-toggle-group.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/slider-input.tsx b/apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/slider-input.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/slider-input.tsx rename to apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/slider-input.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/text-align-input.tsx b/apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/text-align-input.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/text-align-input.tsx rename to apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/text-align-input.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/text-dimension-input.tsx b/apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/text-dimension-input.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/text-dimension-input.tsx rename to apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/text-dimension-input.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/text-input.tsx b/apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/text-input.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/text-input.tsx rename to apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/inputs/text-input.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/style-inputs/multi-style-property-panel.tsx b/apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/style-inputs/multi-style-property-panel.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/style-inputs/multi-style-property-panel.tsx rename to apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/style-inputs/multi-style-property-panel.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/style-inputs/single-style-property-panel.tsx b/apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/style-inputs/single-style-property-panel.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/style-inputs/single-style-property-panel.tsx rename to apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/helpers/style-inputs/single-style-property-panel.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/image-sidebar-panel.tsx b/apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/image-sidebar-panel.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/image-sidebar-panel.tsx rename to apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/image-sidebar-panel.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/spacer-sidebar-panel.tsx b/apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/spacer-sidebar-panel.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/spacer-sidebar-panel.tsx rename to apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/spacer-sidebar-panel.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/text-sidebar-panel.tsx b/apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/text-sidebar-panel.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/configuration-panel/input-panels/text-sidebar-panel.tsx rename to apps/dashboard/src/components/email-editor/sidebar/configuration-panel/input-panels/text-sidebar-panel.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/index.tsx b/apps/dashboard/src/components/email-editor/sidebar/index.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/index.tsx rename to apps/dashboard/src/components/email-editor/sidebar/index.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/settings-panel.tsx b/apps/dashboard/src/components/email-editor/sidebar/settings-panel.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/settings-panel.tsx rename to apps/dashboard/src/components/email-editor/sidebar/settings-panel.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/toggle-inspector-panel-button.tsx b/apps/dashboard/src/components/email-editor/sidebar/toggle-inspector-panel-button.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/toggle-inspector-panel-button.tsx rename to apps/dashboard/src/components/email-editor/sidebar/toggle-inspector-panel-button.tsx diff --git a/packages/stack-server/src/components/email-editor/sidebar/variables-panel.tsx b/apps/dashboard/src/components/email-editor/sidebar/variables-panel.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/sidebar/variables-panel.tsx rename to apps/dashboard/src/components/email-editor/sidebar/variables-panel.tsx diff --git a/packages/stack-server/src/components/email-editor/template-panel/download-json/index.tsx b/apps/dashboard/src/components/email-editor/template-panel/download-json/index.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/template-panel/download-json/index.tsx rename to apps/dashboard/src/components/email-editor/template-panel/download-json/index.tsx diff --git a/packages/stack-server/src/components/email-editor/template-panel/import-json/import-json-dialog.tsx b/apps/dashboard/src/components/email-editor/template-panel/import-json/import-json-dialog.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/template-panel/import-json/import-json-dialog.tsx rename to apps/dashboard/src/components/email-editor/template-panel/import-json/import-json-dialog.tsx diff --git a/packages/stack-server/src/components/email-editor/template-panel/import-json/index.tsx b/apps/dashboard/src/components/email-editor/template-panel/import-json/index.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/template-panel/import-json/index.tsx rename to apps/dashboard/src/components/email-editor/template-panel/import-json/index.tsx diff --git a/packages/stack-server/src/components/email-editor/template-panel/import-json/validateJsonStringValue.ts b/apps/dashboard/src/components/email-editor/template-panel/import-json/validateJsonStringValue.ts similarity index 100% rename from packages/stack-server/src/components/email-editor/template-panel/import-json/validateJsonStringValue.ts rename to apps/dashboard/src/components/email-editor/template-panel/import-json/validateJsonStringValue.ts diff --git a/packages/stack-server/src/components/email-editor/template-panel/index.tsx b/apps/dashboard/src/components/email-editor/template-panel/index.tsx similarity index 100% rename from packages/stack-server/src/components/email-editor/template-panel/index.tsx rename to apps/dashboard/src/components/email-editor/template-panel/index.tsx diff --git a/packages/stack-server/src/components/env-keys.tsx b/apps/dashboard/src/components/env-keys.tsx similarity index 100% rename from packages/stack-server/src/components/env-keys.tsx rename to apps/dashboard/src/components/env-keys.tsx diff --git a/packages/stack-server/src/components/form-dialog.tsx b/apps/dashboard/src/components/form-dialog.tsx similarity index 100% rename from packages/stack-server/src/components/form-dialog.tsx rename to apps/dashboard/src/components/form-dialog.tsx diff --git a/packages/stack-server/src/components/form-fields.tsx b/apps/dashboard/src/components/form-fields.tsx similarity index 100% rename from packages/stack-server/src/components/form-fields.tsx rename to apps/dashboard/src/components/form-fields.tsx diff --git a/packages/stack-server/src/components/link.tsx b/apps/dashboard/src/components/link.tsx similarity index 100% rename from packages/stack-server/src/components/link.tsx rename to apps/dashboard/src/components/link.tsx diff --git a/packages/stack-server/src/components/logo.tsx b/apps/dashboard/src/components/logo.tsx similarity index 100% rename from packages/stack-server/src/components/logo.tsx rename to apps/dashboard/src/components/logo.tsx diff --git a/packages/stack-server/src/components/navbar.tsx b/apps/dashboard/src/components/navbar.tsx similarity index 100% rename from packages/stack-server/src/components/navbar.tsx rename to apps/dashboard/src/components/navbar.tsx diff --git a/packages/stack-server/src/components/permission-field.tsx b/apps/dashboard/src/components/permission-field.tsx similarity index 100% rename from packages/stack-server/src/components/permission-field.tsx rename to apps/dashboard/src/components/permission-field.tsx diff --git a/packages/stack-server/src/components/project-card.tsx b/apps/dashboard/src/components/project-card.tsx similarity index 100% rename from packages/stack-server/src/components/project-card.tsx rename to apps/dashboard/src/components/project-card.tsx diff --git a/packages/stack-server/src/components/project-switcher.tsx b/apps/dashboard/src/components/project-switcher.tsx similarity index 100% rename from packages/stack-server/src/components/project-switcher.tsx rename to apps/dashboard/src/components/project-switcher.tsx diff --git a/packages/stack-server/src/components/router.tsx b/apps/dashboard/src/components/router.tsx similarity index 100% rename from packages/stack-server/src/components/router.tsx rename to apps/dashboard/src/components/router.tsx diff --git a/packages/stack-server/src/components/search-bar.tsx b/apps/dashboard/src/components/search-bar.tsx similarity index 100% rename from packages/stack-server/src/components/search-bar.tsx rename to apps/dashboard/src/components/search-bar.tsx diff --git a/packages/stack-server/src/components/settings.tsx b/apps/dashboard/src/components/settings.tsx similarity index 100% rename from packages/stack-server/src/components/settings.tsx rename to apps/dashboard/src/components/settings.tsx diff --git a/packages/stack-server/src/components/simple-tooltip.tsx b/apps/dashboard/src/components/simple-tooltip.tsx similarity index 100% rename from packages/stack-server/src/components/simple-tooltip.tsx rename to apps/dashboard/src/components/simple-tooltip.tsx diff --git a/packages/stack-server/src/components/site-loading-indicator.tsx b/apps/dashboard/src/components/site-loading-indicator.tsx similarity index 100% rename from packages/stack-server/src/components/site-loading-indicator.tsx rename to apps/dashboard/src/components/site-loading-indicator.tsx diff --git a/packages/stack-server/src/components/smart-form.tsx b/apps/dashboard/src/components/smart-form.tsx similarity index 100% rename from packages/stack-server/src/components/smart-form.tsx rename to apps/dashboard/src/components/smart-form.tsx diff --git a/packages/stack-server/src/components/smart-image.tsx b/apps/dashboard/src/components/smart-image.tsx similarity index 100% rename from packages/stack-server/src/components/smart-image.tsx rename to apps/dashboard/src/components/smart-image.tsx diff --git a/packages/stack-server/src/components/style-link.tsx b/apps/dashboard/src/components/style-link.tsx similarity index 100% rename from packages/stack-server/src/components/style-link.tsx rename to apps/dashboard/src/components/style-link.tsx diff --git a/packages/stack-server/src/components/theme-provider.tsx b/apps/dashboard/src/components/theme-provider.tsx similarity index 100% rename from packages/stack-server/src/components/theme-provider.tsx rename to apps/dashboard/src/components/theme-provider.tsx diff --git a/packages/stack-server/src/components/ui/accordion.tsx b/apps/dashboard/src/components/ui/accordion.tsx similarity index 100% rename from packages/stack-server/src/components/ui/accordion.tsx rename to apps/dashboard/src/components/ui/accordion.tsx diff --git a/packages/stack-server/src/components/ui/alert-dialog.tsx b/apps/dashboard/src/components/ui/alert-dialog.tsx similarity index 100% rename from packages/stack-server/src/components/ui/alert-dialog.tsx rename to apps/dashboard/src/components/ui/alert-dialog.tsx diff --git a/packages/stack-server/src/components/ui/alert.tsx b/apps/dashboard/src/components/ui/alert.tsx similarity index 100% rename from packages/stack-server/src/components/ui/alert.tsx rename to apps/dashboard/src/components/ui/alert.tsx diff --git a/packages/stack-server/src/components/ui/aspect-ratio.tsx b/apps/dashboard/src/components/ui/aspect-ratio.tsx similarity index 100% rename from packages/stack-server/src/components/ui/aspect-ratio.tsx rename to apps/dashboard/src/components/ui/aspect-ratio.tsx diff --git a/packages/stack-server/src/components/ui/avatar.tsx b/apps/dashboard/src/components/ui/avatar.tsx similarity index 100% rename from packages/stack-server/src/components/ui/avatar.tsx rename to apps/dashboard/src/components/ui/avatar.tsx diff --git a/packages/stack-server/src/components/ui/badge.tsx b/apps/dashboard/src/components/ui/badge.tsx similarity index 100% rename from packages/stack-server/src/components/ui/badge.tsx rename to apps/dashboard/src/components/ui/badge.tsx diff --git a/packages/stack-server/src/components/ui/breadcrumb.tsx b/apps/dashboard/src/components/ui/breadcrumb.tsx similarity index 100% rename from packages/stack-server/src/components/ui/breadcrumb.tsx rename to apps/dashboard/src/components/ui/breadcrumb.tsx diff --git a/packages/stack-server/src/components/ui/button.tsx b/apps/dashboard/src/components/ui/button.tsx similarity index 100% rename from packages/stack-server/src/components/ui/button.tsx rename to apps/dashboard/src/components/ui/button.tsx diff --git a/packages/stack-server/src/components/ui/calendar.tsx b/apps/dashboard/src/components/ui/calendar.tsx similarity index 100% rename from packages/stack-server/src/components/ui/calendar.tsx rename to apps/dashboard/src/components/ui/calendar.tsx diff --git a/packages/stack-server/src/components/ui/card.tsx b/apps/dashboard/src/components/ui/card.tsx similarity index 100% rename from packages/stack-server/src/components/ui/card.tsx rename to apps/dashboard/src/components/ui/card.tsx diff --git a/packages/stack-server/src/components/ui/checkbox.tsx b/apps/dashboard/src/components/ui/checkbox.tsx similarity index 100% rename from packages/stack-server/src/components/ui/checkbox.tsx rename to apps/dashboard/src/components/ui/checkbox.tsx diff --git a/packages/stack-server/src/components/ui/collapsible.tsx b/apps/dashboard/src/components/ui/collapsible.tsx similarity index 100% rename from packages/stack-server/src/components/ui/collapsible.tsx rename to apps/dashboard/src/components/ui/collapsible.tsx diff --git a/packages/stack-server/src/components/ui/command.tsx b/apps/dashboard/src/components/ui/command.tsx similarity index 100% rename from packages/stack-server/src/components/ui/command.tsx rename to apps/dashboard/src/components/ui/command.tsx diff --git a/packages/stack-server/src/components/ui/context-menu.tsx b/apps/dashboard/src/components/ui/context-menu.tsx similarity index 100% rename from packages/stack-server/src/components/ui/context-menu.tsx rename to apps/dashboard/src/components/ui/context-menu.tsx diff --git a/packages/stack-server/src/components/ui/dialog.tsx b/apps/dashboard/src/components/ui/dialog.tsx similarity index 100% rename from packages/stack-server/src/components/ui/dialog.tsx rename to apps/dashboard/src/components/ui/dialog.tsx diff --git a/packages/stack-server/src/components/ui/dropdown-menu.tsx b/apps/dashboard/src/components/ui/dropdown-menu.tsx similarity index 100% rename from packages/stack-server/src/components/ui/dropdown-menu.tsx rename to apps/dashboard/src/components/ui/dropdown-menu.tsx diff --git a/packages/stack-server/src/components/ui/error-page.tsx b/apps/dashboard/src/components/ui/error-page.tsx similarity index 100% rename from packages/stack-server/src/components/ui/error-page.tsx rename to apps/dashboard/src/components/ui/error-page.tsx diff --git a/packages/stack-server/src/components/ui/form.tsx b/apps/dashboard/src/components/ui/form.tsx similarity index 100% rename from packages/stack-server/src/components/ui/form.tsx rename to apps/dashboard/src/components/ui/form.tsx diff --git a/packages/stack-server/src/components/ui/hover-card.tsx b/apps/dashboard/src/components/ui/hover-card.tsx similarity index 100% rename from packages/stack-server/src/components/ui/hover-card.tsx rename to apps/dashboard/src/components/ui/hover-card.tsx diff --git a/packages/stack-server/src/components/ui/inline-code.tsx b/apps/dashboard/src/components/ui/inline-code.tsx similarity index 100% rename from packages/stack-server/src/components/ui/inline-code.tsx rename to apps/dashboard/src/components/ui/inline-code.tsx diff --git a/packages/stack-server/src/components/ui/input-otp.tsx b/apps/dashboard/src/components/ui/input-otp.tsx similarity index 100% rename from packages/stack-server/src/components/ui/input-otp.tsx rename to apps/dashboard/src/components/ui/input-otp.tsx diff --git a/packages/stack-server/src/components/ui/input.tsx b/apps/dashboard/src/components/ui/input.tsx similarity index 100% rename from packages/stack-server/src/components/ui/input.tsx rename to apps/dashboard/src/components/ui/input.tsx diff --git a/packages/stack-server/src/components/ui/label.tsx b/apps/dashboard/src/components/ui/label.tsx similarity index 100% rename from packages/stack-server/src/components/ui/label.tsx rename to apps/dashboard/src/components/ui/label.tsx diff --git a/packages/stack-server/src/components/ui/menubar.tsx b/apps/dashboard/src/components/ui/menubar.tsx similarity index 100% rename from packages/stack-server/src/components/ui/menubar.tsx rename to apps/dashboard/src/components/ui/menubar.tsx diff --git a/packages/stack-server/src/components/ui/navigation-menu.tsx b/apps/dashboard/src/components/ui/navigation-menu.tsx similarity index 100% rename from packages/stack-server/src/components/ui/navigation-menu.tsx rename to apps/dashboard/src/components/ui/navigation-menu.tsx diff --git a/packages/stack-server/src/components/ui/popover.tsx b/apps/dashboard/src/components/ui/popover.tsx similarity index 100% rename from packages/stack-server/src/components/ui/popover.tsx rename to apps/dashboard/src/components/ui/popover.tsx diff --git a/packages/stack-server/src/components/ui/progress.tsx b/apps/dashboard/src/components/ui/progress.tsx similarity index 100% rename from packages/stack-server/src/components/ui/progress.tsx rename to apps/dashboard/src/components/ui/progress.tsx diff --git a/packages/stack-server/src/components/ui/radio-group.tsx b/apps/dashboard/src/components/ui/radio-group.tsx similarity index 100% rename from packages/stack-server/src/components/ui/radio-group.tsx rename to apps/dashboard/src/components/ui/radio-group.tsx diff --git a/packages/stack-server/src/components/ui/resizable.tsx b/apps/dashboard/src/components/ui/resizable.tsx similarity index 100% rename from packages/stack-server/src/components/ui/resizable.tsx rename to apps/dashboard/src/components/ui/resizable.tsx diff --git a/packages/stack-server/src/components/ui/scroll-area.tsx b/apps/dashboard/src/components/ui/scroll-area.tsx similarity index 100% rename from packages/stack-server/src/components/ui/scroll-area.tsx rename to apps/dashboard/src/components/ui/scroll-area.tsx diff --git a/packages/stack-server/src/components/ui/select.tsx b/apps/dashboard/src/components/ui/select.tsx similarity index 100% rename from packages/stack-server/src/components/ui/select.tsx rename to apps/dashboard/src/components/ui/select.tsx diff --git a/packages/stack-server/src/components/ui/separator.tsx b/apps/dashboard/src/components/ui/separator.tsx similarity index 100% rename from packages/stack-server/src/components/ui/separator.tsx rename to apps/dashboard/src/components/ui/separator.tsx diff --git a/packages/stack-server/src/components/ui/sheet.tsx b/apps/dashboard/src/components/ui/sheet.tsx similarity index 100% rename from packages/stack-server/src/components/ui/sheet.tsx rename to apps/dashboard/src/components/ui/sheet.tsx diff --git a/packages/stack-server/src/components/ui/skeleton.tsx b/apps/dashboard/src/components/ui/skeleton.tsx similarity index 100% rename from packages/stack-server/src/components/ui/skeleton.tsx rename to apps/dashboard/src/components/ui/skeleton.tsx diff --git a/packages/stack-server/src/components/ui/slider.tsx b/apps/dashboard/src/components/ui/slider.tsx similarity index 100% rename from packages/stack-server/src/components/ui/slider.tsx rename to apps/dashboard/src/components/ui/slider.tsx diff --git a/packages/stack-server/src/components/ui/spinner.tsx b/apps/dashboard/src/components/ui/spinner.tsx similarity index 100% rename from packages/stack-server/src/components/ui/spinner.tsx rename to apps/dashboard/src/components/ui/spinner.tsx diff --git a/packages/stack-server/src/components/ui/switch.tsx b/apps/dashboard/src/components/ui/switch.tsx similarity index 100% rename from packages/stack-server/src/components/ui/switch.tsx rename to apps/dashboard/src/components/ui/switch.tsx diff --git a/packages/stack-server/src/components/ui/table.tsx b/apps/dashboard/src/components/ui/table.tsx similarity index 100% rename from packages/stack-server/src/components/ui/table.tsx rename to apps/dashboard/src/components/ui/table.tsx diff --git a/packages/stack-server/src/components/ui/tabs.tsx b/apps/dashboard/src/components/ui/tabs.tsx similarity index 100% rename from packages/stack-server/src/components/ui/tabs.tsx rename to apps/dashboard/src/components/ui/tabs.tsx diff --git a/packages/stack-server/src/components/ui/textarea.tsx b/apps/dashboard/src/components/ui/textarea.tsx similarity index 100% rename from packages/stack-server/src/components/ui/textarea.tsx rename to apps/dashboard/src/components/ui/textarea.tsx diff --git a/packages/stack-server/src/components/ui/toast.tsx b/apps/dashboard/src/components/ui/toast.tsx similarity index 100% rename from packages/stack-server/src/components/ui/toast.tsx rename to apps/dashboard/src/components/ui/toast.tsx diff --git a/packages/stack-server/src/components/ui/toaster.tsx b/apps/dashboard/src/components/ui/toaster.tsx similarity index 100% rename from packages/stack-server/src/components/ui/toaster.tsx rename to apps/dashboard/src/components/ui/toaster.tsx diff --git a/packages/stack-server/src/components/ui/toggle-group.tsx b/apps/dashboard/src/components/ui/toggle-group.tsx similarity index 100% rename from packages/stack-server/src/components/ui/toggle-group.tsx rename to apps/dashboard/src/components/ui/toggle-group.tsx diff --git a/packages/stack-server/src/components/ui/toggle.tsx b/apps/dashboard/src/components/ui/toggle.tsx similarity index 100% rename from packages/stack-server/src/components/ui/toggle.tsx rename to apps/dashboard/src/components/ui/toggle.tsx diff --git a/packages/stack-server/src/components/ui/tooltip.tsx b/apps/dashboard/src/components/ui/tooltip.tsx similarity index 100% rename from packages/stack-server/src/components/ui/tooltip.tsx rename to apps/dashboard/src/components/ui/tooltip.tsx diff --git a/packages/stack-server/src/components/ui/typography.tsx b/apps/dashboard/src/components/ui/typography.tsx similarity index 100% rename from packages/stack-server/src/components/ui/typography.tsx rename to apps/dashboard/src/components/ui/typography.tsx diff --git a/packages/stack-server/src/components/ui/use-toast.ts b/apps/dashboard/src/components/ui/use-toast.ts similarity index 100% rename from packages/stack-server/src/components/ui/use-toast.ts rename to apps/dashboard/src/components/ui/use-toast.ts diff --git a/packages/stack-server/src/email/index.tsx b/apps/dashboard/src/email/index.tsx similarity index 100% rename from packages/stack-server/src/email/index.tsx rename to apps/dashboard/src/email/index.tsx diff --git a/packages/stack-server/src/email/templates/email-verification.tsx b/apps/dashboard/src/email/templates/email-verification.tsx similarity index 100% rename from packages/stack-server/src/email/templates/email-verification.tsx rename to apps/dashboard/src/email/templates/email-verification.tsx diff --git a/packages/stack-server/src/email/templates/empty.tsx b/apps/dashboard/src/email/templates/empty.tsx similarity index 100% rename from packages/stack-server/src/email/templates/empty.tsx rename to apps/dashboard/src/email/templates/empty.tsx diff --git a/packages/stack-server/src/email/templates/magic-link.tsx b/apps/dashboard/src/email/templates/magic-link.tsx similarity index 100% rename from packages/stack-server/src/email/templates/magic-link.tsx rename to apps/dashboard/src/email/templates/magic-link.tsx diff --git a/packages/stack-server/src/email/templates/password-reset.tsx b/apps/dashboard/src/email/templates/password-reset.tsx similarity index 100% rename from packages/stack-server/src/email/templates/password-reset.tsx rename to apps/dashboard/src/email/templates/password-reset.tsx diff --git a/packages/stack-server/src/email/utils.tsx b/apps/dashboard/src/email/utils.tsx similarity index 98% rename from packages/stack-server/src/email/utils.tsx rename to apps/dashboard/src/email/utils.tsx index 67a436173..25948ed60 100644 --- a/packages/stack-server/src/email/utils.tsx +++ b/apps/dashboard/src/email/utils.tsx @@ -6,7 +6,7 @@ import { magicLinkTemplate } from "./templates/magic-link"; import { render } from "@react-email/render"; import { Reader } from "@/components/email-editor/email-builder"; import { Body, Head, Html, Preview } from "@react-email/components"; -import * as Handlebars from 'handlebars'; +import * as Handlebars from 'handlebars/dist/handlebars.js'; import _ from 'lodash'; const userVars = [ @@ -173,4 +173,4 @@ export function renderEmailTemplate( const text = render(component, { plainText: true }); return { html, text, subject: mergedSubject }; -} \ No newline at end of file +} diff --git a/packages/stack-server/src/globals.d.ts b/apps/dashboard/src/globals.d.ts similarity index 60% rename from packages/stack-server/src/globals.d.ts rename to apps/dashboard/src/globals.d.ts index 167c99266..98d1e80dc 100644 --- a/packages/stack-server/src/globals.d.ts +++ b/apps/dashboard/src/globals.d.ts @@ -5,4 +5,9 @@ declare namespace React { interface HTMLAttributes { inert?: '', } -} \ No newline at end of file +} + +declare module "handlebars/dist/handlebars.js" { + import * as Handlebars from 'handlebars'; + export = Handlebars; +} diff --git a/packages/stack-server/src/hooks/use-animation-frame.tsx b/apps/dashboard/src/hooks/use-animation-frame.tsx similarity index 100% rename from packages/stack-server/src/hooks/use-animation-frame.tsx rename to apps/dashboard/src/hooks/use-animation-frame.tsx diff --git a/packages/stack-server/src/hooks/use-from-now.tsx b/apps/dashboard/src/hooks/use-from-now.tsx similarity index 100% rename from packages/stack-server/src/hooks/use-from-now.tsx rename to apps/dashboard/src/hooks/use-from-now.tsx diff --git a/packages/stack-server/src/hooks/use-is-hydrated.tsx b/apps/dashboard/src/hooks/use-is-hydrated.tsx similarity index 100% rename from packages/stack-server/src/hooks/use-is-hydrated.tsx rename to apps/dashboard/src/hooks/use-is-hydrated.tsx diff --git a/packages/stack-server/src/hooks/use-mutation-observer.tsx b/apps/dashboard/src/hooks/use-mutation-observer.tsx similarity index 100% rename from packages/stack-server/src/hooks/use-mutation-observer.tsx rename to apps/dashboard/src/hooks/use-mutation-observer.tsx diff --git a/packages/stack-server/src/lib/api-keys.tsx b/apps/dashboard/src/lib/api-keys.tsx similarity index 100% rename from packages/stack-server/src/lib/api-keys.tsx rename to apps/dashboard/src/lib/api-keys.tsx diff --git a/packages/stack-server/src/lib/email-templates.tsx b/apps/dashboard/src/lib/email-templates.tsx similarity index 100% rename from packages/stack-server/src/lib/email-templates.tsx rename to apps/dashboard/src/lib/email-templates.tsx diff --git a/apps/dashboard/src/lib/openapi.tsx b/apps/dashboard/src/lib/openapi.tsx new file mode 100644 index 000000000..2fe91a1cc --- /dev/null +++ b/apps/dashboard/src/lib/openapi.tsx @@ -0,0 +1,273 @@ +import { SmartRouteHandler } from '@/route-handlers/smart-route-handler'; +import { StackAssertionError } from '@stackframe/stack-shared/dist/utils/errors'; +import { HttpMethod } from '@stackframe/stack-shared/dist/utils/http'; +import { deindent } from '@stackframe/stack-shared/dist/utils/strings'; +import * as yup from 'yup'; + +export function parseOpenAPI(options: { + endpoints: Map>, + audience: 'client' | 'server' | 'admin', +}) { + return { + openapi: '3.1.0', + info: { + title: 'Stack REST API', + version: '1.0.0', + }, + servers: [{ + url: 'https://app.stack-auth.com/api/v1', + description: 'Stack REST API', + }], + paths: Object.fromEntries( + [...options.endpoints] + .map(([path, handlersByMethod]) => ( + [path, Object.fromEntries( + [...handlersByMethod] + .map(([method, handler]) => ( + [method.toLowerCase(), parseRouteHandler({ handler, method, path, audience: options.audience })] + )) + .filter(([_, handler]) => handler !== undefined) + )] + )) + .filter(([_, handlersByMethod]) => Object.keys(handlersByMethod).length > 0), + ), + }; +} + +const endpointMetadataSchema = yup.object({ + summary: yup.string().required(), + description: yup.string().required(), + hide: yup.boolean().optional(), + tags: yup.array(yup.string()).required(), +}); + +function undefinedIfMixed(value: yup.SchemaFieldDescription | undefined): yup.SchemaFieldDescription | undefined { + if (!value) return undefined; + return value.type === 'mixed' ? undefined : value; +} + +function isSchemaObjectDescription(value: yup.SchemaFieldDescription): value is yup.SchemaObjectDescription & { type: 'object' } { + return value.type === 'object'; +} + +function isSchemaMixedDescription(value: yup.SchemaFieldDescription): value is yup.SchemaDescription & { type: 'mixed' } { + return value.type === 'mixed'; +} + +function isSchemaArrayDescription(value: yup.SchemaFieldDescription): value is yup.SchemaInnerTypeDescription & { type: 'array', innerType: yup.SchemaInnerTypeDescription } { + return value.type === 'array'; +} + +function isSchemaTupleDescription(value: yup.SchemaFieldDescription): value is yup.SchemaInnerTypeDescription & { type: 'tuple', innerType: yup.SchemaInnerTypeDescription[] } { + return value.type === 'tuple'; +} + +function isMaybeRequestSchemaForAudience(requestDescribe: yup.SchemaObjectDescription, audience: 'client' | 'server' | 'admin') { + const schemaAuth = requestDescribe.fields.auth; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- yup types are wrong and claim that fields always exist + if (!schemaAuth) return true; + if (isSchemaMixedDescription(schemaAuth)) return true; + if (!isSchemaObjectDescription(schemaAuth)) return true; + const schemaAudience = schemaAuth.fields.type; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- same as above + if (!schemaAudience) return true; + if ("oneOf" in schemaAudience) { + return schemaAudience.oneOf.includes(audience); + } +} + + +function parseRouteHandler(options: { + handler: SmartRouteHandler, + path: string, + method: HttpMethod, + audience: 'client' | 'server' | 'admin', +}) { + let result: any = undefined; + + for (const overload of options.handler.overloads.values()) { + const requestDescribe = overload.request.describe(); + const responseDescribe = overload.response.describe(); + if (!isSchemaObjectDescription(requestDescribe)) throw new Error('Request schema must be a yup.ObjectSchema'); + if (!isSchemaObjectDescription(responseDescribe)) throw new Error('Response schema must be a yup.ObjectSchema'); + + // estimate whether this overload is the right one based on a heuristic + if (!isMaybeRequestSchemaForAudience(requestDescribe, options.audience)) { + // This overload is definitely not for the audience + continue; + } + + if (result) { + throw new StackAssertionError(deindent` + OpenAPI generator matched multiple overloads for audience ${options.audience} on endpoint ${options.method} ${options.path}. + + This does not necessarily mean there is a bug; the OpenAPI generator uses a heuristic to pick the allowed overloads, and may pick too many. Currently, this heuristic checks whether the request.auth.type property in the schema is a yup.string.oneOf(...) and matches it to the expected audience of the schema. If there are multiple overloads matching a single audience, for example because none of the overloads specify request.auth.type, the OpenAPI generator will not know which overload to generate specs for, and hence fails. + + Either specify request.auth.type on the schema of the specified endpoint or update the OpenAPI generator to support your use case. + `); + } + + result = parseOverload({ + metadata: overload.metadata ?? { + summary: `${options.method} ${options.path}`, + description: `No documentation available for this endpoint.`, + tags: ["Uncategorized"], + }, + path: options.path, + pathDesc: undefinedIfMixed(requestDescribe.fields.params), + parameterDesc: undefinedIfMixed(requestDescribe.fields.query), + requestBodyDesc: undefinedIfMixed(requestDescribe.fields.body), + responseDesc: undefinedIfMixed(responseDescribe.fields.body), + }); + } + + return result; +} + +function getFieldSchema(field: yup.SchemaFieldDescription): { type: string, items?: any } | undefined { + const meta = "meta" in field ? field.meta : {}; + if (meta?.openapi?.hide) { + return undefined; + } + + const openapiFieldExtra = { + example: meta?.openapi?.exampleValue, + description: meta?.openapi?.description, + }; + + switch (field.type) { + case 'string': + case 'number': + case 'boolean': { + return { type: field.type, ...openapiFieldExtra }; + } + case 'mixed': { + return { type: 'object', ...openapiFieldExtra }; + } + case 'object': { + return { type: 'object', ...openapiFieldExtra }; + } + case 'array': { + return { type: 'array', items: getFieldSchema((field as any).innerType), ...openapiFieldExtra }; + } + default: { + throw new Error(`Unsupported field type: ${field.type}`); + } + } +} + +function toParameters(description: yup.SchemaFieldDescription, path?: string) { + const pathParams: string[] = path ? path.match(/{[^}]+}/g) || [] : []; + if (!isSchemaObjectDescription(description)) { + throw new StackAssertionError('Parameters field must be an object schema', { actual: description }); + } + + return Object.entries(description.fields).map(([key, field]) => { + if (path && !pathParams.includes(`{${key}}`)) { + return { schema: null }; + } + return { + name: key, + in: path ? 'path' : 'query', + schema: getFieldSchema(field as any), + required: !(field as any).optional && !(field as any).nullable, + }; + }).filter((x) => x.schema !== null); +} + +function toSchema(description: yup.SchemaFieldDescription): any { + if (isSchemaObjectDescription(description)) { + return { + type: 'object', + properties: Object.fromEntries(Object.entries(description.fields).map(([key, field]) => { + return [key, getFieldSchema(field)]; + }, {})) + }; + } else if (isSchemaArrayDescription(description)) { + return { + type: 'array', + items: toSchema(description.innerType), + }; + } else { + throw new StackAssertionError(`Unsupported schema type: ${description.type}`, { actual: description }); + } +} + +function toRequired(description: yup.SchemaFieldDescription) { + let res: string[] = []; + if (isSchemaObjectDescription(description)) { + res = Object.entries(description.fields) + .filter(([_, field]) => !(field as any).optional && !(field as any).nullable) + .map(([key]) => key); + } else if (isSchemaArrayDescription(description)) { + res = []; + } else { + throw new StackAssertionError(`Unsupported schema type: ${description.type}`, { actual: description }); + } + if (res.length === 0) return undefined; + return res; +} + +function toExamples(description: yup.SchemaFieldDescription) { + if (!isSchemaObjectDescription(description)) { + throw new StackAssertionError('Examples field must be an object schema', { actual: description }); + } + + return Object.entries(description.fields).reduce((acc, [key, field]) => { + const schema = getFieldSchema(field); + if (!schema) return acc; + const example = "meta" in field ? field.meta?.openapi?.exampleValue : undefined; + return { ...acc, [key]: example }; + }, {}); +} + +export function parseOverload(options: { + metadata: yup.InferType, + path: string, + pathDesc?: yup.SchemaFieldDescription, + parameterDesc?: yup.SchemaFieldDescription, + requestBodyDesc?: yup.SchemaFieldDescription, + responseDesc?: yup.SchemaFieldDescription, +}) { + const pathParameters = options.pathDesc ? toParameters(options.pathDesc, options.path) : []; + const queryParameters = options.parameterDesc ? toParameters(options.parameterDesc) : []; + const responseSchema = options.responseDesc ? toSchema(options.responseDesc) : {}; + const responseRequired = options.responseDesc ? toRequired(options.responseDesc) : undefined; + + let requestBody; + if (options.requestBodyDesc) { + requestBody = { + required: true, + content: { + 'application/json': { + schema: { + ...toSchema(options.requestBodyDesc), + required: toRequired(options.requestBodyDesc), + example: toExamples(options.requestBodyDesc), + }, + }, + }, + }; + } + + return { + summary: options.metadata.summary, + description: options.metadata.description, + parameters: queryParameters.concat(pathParameters), + requestBody, + tags: options.metadata.tags, + responses: { + 200: { + description: 'Successful response', + content: { + 'application/json': { + schema: { + ...responseSchema, + required: responseRequired, + }, + }, + }, + }, + }, + }; +} diff --git a/packages/stack-server/src/lib/permissions.tsx b/apps/dashboard/src/lib/permissions.tsx similarity index 100% rename from packages/stack-server/src/lib/permissions.tsx rename to apps/dashboard/src/lib/permissions.tsx diff --git a/packages/stack-server/src/lib/projects.tsx b/apps/dashboard/src/lib/projects.tsx similarity index 100% rename from packages/stack-server/src/lib/projects.tsx rename to apps/dashboard/src/lib/projects.tsx diff --git a/packages/stack-server/src/lib/teams.tsx b/apps/dashboard/src/lib/teams.tsx similarity index 100% rename from packages/stack-server/src/lib/teams.tsx rename to apps/dashboard/src/lib/teams.tsx diff --git a/apps/dashboard/src/lib/tokens.tsx b/apps/dashboard/src/lib/tokens.tsx new file mode 100644 index 000000000..41c60524e --- /dev/null +++ b/apps/dashboard/src/lib/tokens.tsx @@ -0,0 +1,84 @@ +import * as yup from 'yup'; +import { JWTExpired, JOSEError } from 'jose/errors'; +import { decryptJWT, encryptJWT } from '@stackframe/stack-shared/dist/utils/jwt'; +import { KnownErrors } from '@stackframe/stack-shared'; +import { prismaClient } from '@/prisma-client'; +import { generateSecureRandomString } from '@stackframe/stack-shared/dist/utils/crypto'; + +export const authorizationHeaderSchema = yup.string().matches(/^StackSession [^ ]+$/); + +const accessTokenSchema = yup.object({ + projectId: yup.string().required(), + userId: yup.string().required(), + exp: yup.number().required(), +}); + +export const oauthCookieSchema = yup.object({ + projectId: yup.string().required(), + publishableClientKey: yup.string().required(), + innerCodeVerifier: yup.string().required(), + innerState: yup.string().required(), + redirectUri: yup.string().required(), + scope: yup.string().required(), + state: yup.string().required(), + grantType: yup.string().required(), + codeChallenge: yup.string().required(), + codeChallengeMethod: yup.string().required(), + responseType: yup.string().required(), + type: yup.string().oneOf(['authenticate', 'link']).required(), + projectUserId: yup.string().optional(), + providerScope: yup.string().optional(), + errorRedirectUrl: yup.string().optional(), + afterCallbackRedirectUrl: yup.string().optional(), +}); + + +export async function decodeAccessToken(accessToken: string) { + let decoded; + try { + decoded = await decryptJWT(accessToken); + } catch (error) { + if (error instanceof JWTExpired) { + throw new KnownErrors.AccessTokenExpired(); + } else if (error instanceof JOSEError) { + throw new KnownErrors.UnparsableAccessToken(); + } + throw error; + } + + return await accessTokenSchema.validate(decoded); +} + +export async function encodeAccessToken({ + projectId, + userId, +}: { + projectId: string, + userId: string, +}) { + return await encryptJWT({ projectId, userId }, process.env.STACK_ACCESS_TOKEN_EXPIRATION_TIME || '1h'); +} + +export async function createAuthTokens({ + projectId, + projectUserId, +}: { + projectId: string, + projectUserId: string, +}) { + const refreshToken = generateSecureRandomString(); + const accessToken = await encodeAccessToken({ + projectId, + userId: projectUserId, + }); + + await prismaClient.projectUserRefreshToken.create({ + data: { + projectId, + projectUserId, + refreshToken: refreshToken, + }, + }); + + return { refreshToken, accessToken }; +} diff --git a/packages/stack-server/src/lib/users.tsx b/apps/dashboard/src/lib/users.tsx similarity index 100% rename from packages/stack-server/src/lib/users.tsx rename to apps/dashboard/src/lib/users.tsx diff --git a/packages/stack-server/src/lib/utils.tsx b/apps/dashboard/src/lib/utils.tsx similarity index 100% rename from packages/stack-server/src/lib/utils.tsx rename to apps/dashboard/src/lib/utils.tsx diff --git a/apps/dashboard/src/middleware.tsx b/apps/dashboard/src/middleware.tsx new file mode 100644 index 000000000..35673a3ff --- /dev/null +++ b/apps/dashboard/src/middleware.tsx @@ -0,0 +1,63 @@ +import './polyfills'; + +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; + +const corsAllowedRequestHeaders = [ + // General + 'authorization', + 'content-type', + 'x-stack-project-id', + 'x-stack-override-error-status', + 'x-stack-random-nonce', // used to forcefully disable some caches + 'x-stack-client-version', + + // Project auth + 'x-stack-request-type', + 'x-stack-publishable-client-key', + 'x-stack-secret-server-key', + 'x-stack-super-secret-admin-key', + 'x-stack-admin-access-token', + + // User auth + 'x-stack-refresh-token', + 'x-stack-access-token', +]; + +const corsAllowedResponseHeaders = [ + 'content-type', + 'x-stack-actual-status', + 'x-stack-known-error', +]; + +// This function can be marked `async` if using `await` inside +export async function middleware(request: NextRequest) { + const url = new URL(request.url); + const isApiRequest = url.pathname.startsWith('/api/'); + + // default headers + const responseInit: ResponseInit = { + headers: { + // CORS headers + ...!isApiRequest ? {} : { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", + "Access-Control-Allow-Headers": corsAllowedRequestHeaders.join(', '), + "Access-Control-Expose-Headers": corsAllowedResponseHeaders.join(', '), + }, + }, + }; + + // we want to allow preflight requests to pass through + // even if the API route does not implement OPTIONS + if (request.method === 'OPTIONS' && isApiRequest) { + return new Response(null, responseInit); + } + + return NextResponse.next(responseInit); +} + +// See "Matching Paths" below to learn more +export const config = { + matcher: '/:path*', +}; diff --git a/apps/dashboard/src/oauth/index.tsx b/apps/dashboard/src/oauth/index.tsx new file mode 100644 index 000000000..b7bc4834b --- /dev/null +++ b/apps/dashboard/src/oauth/index.tsx @@ -0,0 +1,50 @@ +import OAuth2Server from "@node-oauth/oauth2-server"; +import { OAuthProviderConfigJson } from "@stackframe/stack-shared"; +import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; +import { GithubProvider } from "./providers/github"; +import { OAuthModel } from "./model"; +import { OAuthBaseProvider } from "./providers/base"; +import { GoogleProvider } from "./providers/google"; +import { FacebookProvider } from "./providers/facebook"; +import { MicrosoftProvider } from "./providers/microsoft"; +import { SpotifyProvider } from "./providers/spotify"; +import { SharedProvider, sharedProviders, toStandardProvider } from "@stackframe/stack-shared/dist/interface/clientInterface"; + +const _providers = { + github: GithubProvider, + google: GoogleProvider, + facebook: FacebookProvider, + microsoft: MicrosoftProvider, + spotify: SpotifyProvider, +} as const; + +const _getEnvForProvider = (provider: keyof typeof _providers) => { + return { + clientId: getEnvVariable(`${provider.toUpperCase()}_CLIENT_ID`), + clientSecret: getEnvVariable(`${provider.toUpperCase()}_CLIENT_SECRET`), + }; +}; + +const _isSharedProvider = (provider: OAuthProviderConfigJson): provider is OAuthProviderConfigJson & { type: SharedProvider } => { + return sharedProviders.includes(provider.type as any); +}; + +export function getProvider(provider: OAuthProviderConfigJson): OAuthBaseProvider { + if (_isSharedProvider(provider)) { + const providerName = toStandardProvider(provider.type); + return new _providers[providerName]({ + clientId: _getEnvForProvider(providerName).clientId, + clientSecret: _getEnvForProvider(providerName).clientSecret, + }); + } else { + return new _providers[provider.type]({ + clientId: provider.clientId, + clientSecret: provider.clientSecret, + }); + } +} + +export const oauthServer = new OAuth2Server({ + model: new OAuthModel(), + allowExtendedTokenAttributes: true, +}); diff --git a/packages/stack-server/src/oauth/model.tsx b/apps/dashboard/src/oauth/model.tsx similarity index 100% rename from packages/stack-server/src/oauth/model.tsx rename to apps/dashboard/src/oauth/model.tsx diff --git a/apps/dashboard/src/oauth/providers/base.tsx b/apps/dashboard/src/oauth/providers/base.tsx new file mode 100644 index 000000000..bc6f62b0d --- /dev/null +++ b/apps/dashboard/src/oauth/providers/base.tsx @@ -0,0 +1,91 @@ +import { Issuer, generators, CallbackParamsType, Client, TokenSet } from "openid-client"; +import { OAuthUserInfo } from "../utils"; +import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { mergeScopeStrings } from "@stackframe/stack-shared/dist/utils/strings"; + +export abstract class OAuthBaseProvider { + issuer: Issuer; + scope: string; + oauthClient: Client; + redirectUri: string; + + constructor(options: { + issuer: string, + authorizationEndpoint: string, + tokenEndpoint: string, + userinfoEndpoint?: string, + clientId: string, + clientSecret: string, + redirectUri: string, + baseScope: string, + }) { + this.issuer = new Issuer({ + issuer: options.issuer, + authorization_endpoint: options.authorizationEndpoint, + token_endpoint: options.tokenEndpoint, + userinfo_endpoint: options.userinfoEndpoint, + }); + this.oauthClient = new this.issuer.Client({ + client_id: options.clientId, + client_secret: options.clientSecret, + redirect_uri: options.redirectUri, + response_types: ["code"], + }); + + // facebook always return an id_token even in the OAuth2 flow, which is not supported by openid-client + const oldGrant = this.oauthClient.grant; + this.oauthClient.grant = async function (params) { + const grant = await oldGrant.call(this, params); + delete grant.id_token; + return grant; + }; + + this.redirectUri = options.redirectUri; + this.scope = options.baseScope; + } + + getAuthorizationUrl(options: { + codeVerifier: string, + state: string, + extraScope?: string, + }) { + return this.oauthClient.authorizationUrl({ + scope: mergeScopeStrings(this.scope, options.extraScope || ""), + code_challenge: generators.codeChallenge(options.codeVerifier), + code_challenge_method: "S256", + state: options.state, + response_type: "code", + access_type: "offline", + }); + } + + async getCallback(options: { + callbackParams: CallbackParamsType, + codeVerifier: string, + state: string, + }): Promise { + let tokenSet; + try { + const params = { + code_verifier: options.codeVerifier, + state: options.state, + }; + tokenSet = await this.oauthClient.oauthCallback(this.redirectUri, options.callbackParams, params); + } catch (error) { + throw new StackAssertionError("OAuth callback failed", undefined, { cause: error }); + } + if (!tokenSet.access_token) { + throw new StackAssertionError("No access token received", { tokenSet }); + } + return await this.postProcessUserInfo(tokenSet); + } + + async getAccessToken(options: { + refreshToken: string, + scope?: string, + }): Promise { + return await this.oauthClient.refresh(options.refreshToken, { exchangeBody: { scope: options.scope } }); + } + + abstract postProcessUserInfo(tokenSet: TokenSet): Promise; +} diff --git a/apps/dashboard/src/oauth/providers/facebook.tsx b/apps/dashboard/src/oauth/providers/facebook.tsx new file mode 100644 index 000000000..27d475d5a --- /dev/null +++ b/apps/dashboard/src/oauth/providers/facebook.tsx @@ -0,0 +1,35 @@ +import { TokenSet } from "openid-client"; +import { OAuthBaseProvider } from "./base"; +import { OAuthUserInfo, validateUserInfo } from "../utils"; + +export class FacebookProvider extends OAuthBaseProvider { + constructor(options: { + clientId: string, + clientSecret: string, + }) { + super({ + issuer: "https://www.facebook.com", + authorizationEndpoint: "https://facebook.com/v20.0/dialog/oauth/", + tokenEndpoint: "https://graph.facebook.com/v20.0/oauth/access_token", + redirectUri: process.env.NEXT_PUBLIC_STACK_URL + "/api/v1/auth/callback/facebook", + baseScope: "public_profile email", + ...options + }); + } + + async postProcessUserInfo(tokenSet: TokenSet): Promise { + const url = new URL('https://graph.facebook.com/v3.2/me'); + url.searchParams.append('access_token', tokenSet.access_token || ""); + url.searchParams.append('fields', 'id,name,email'); + const rawUserInfo = await fetch(url).then((res) => res.json()); + + return validateUserInfo({ + accountId: rawUserInfo.id, + displayName: rawUserInfo.name, + email: rawUserInfo.email, + profileImageUrl: `https://graph.facebook.com/v19.0/${rawUserInfo.id}/picture`, + accessToken: tokenSet.access_token, + refreshToken: tokenSet.refresh_token, + }); + } +} diff --git a/apps/dashboard/src/oauth/providers/github.tsx b/apps/dashboard/src/oauth/providers/github.tsx new file mode 100644 index 000000000..2b387990e --- /dev/null +++ b/apps/dashboard/src/oauth/providers/github.tsx @@ -0,0 +1,42 @@ +import { TokenSet } from "openid-client"; +import { OAuthBaseProvider } from "./base"; +import { OAuthUserInfo, validateUserInfo } from "../utils"; + +export class GithubProvider extends OAuthBaseProvider { + constructor(options: { + clientId: string, + clientSecret: string, + }) { + super({ + issuer: "https://github.com", + authorizationEndpoint: "https://github.com/login/oauth/authorize", + tokenEndpoint: "https://github.com/login/oauth/access_token", + userinfoEndpoint: "https://api.github.com/user", + redirectUri: process.env.NEXT_PUBLIC_STACK_URL + "/api/v1/auth/callback/github", + baseScope: "user:email", + ...options, + }); + } + + async postProcessUserInfo(tokenSet: TokenSet): Promise { + const rawUserInfo = await this.oauthClient.userinfo(tokenSet); + let email = rawUserInfo.email; + if (!email) { + const emails = await fetch("https://api.github.com/user/emails", { + headers: { + Authorization: `token ${tokenSet.access_token}`, + }, + }).then((res) => res.json()); + rawUserInfo.email = emails.find((e: any) => e.primary).email; + } + + return validateUserInfo({ + accountId: rawUserInfo.id?.toString(), + displayName: rawUserInfo.name, + email: rawUserInfo.email, + profileImageUrl: rawUserInfo.avatar_url, + accessToken: tokenSet.access_token, + refreshToken: tokenSet.refresh_token, + }); + } +} diff --git a/apps/dashboard/src/oauth/providers/google.tsx b/apps/dashboard/src/oauth/providers/google.tsx new file mode 100644 index 000000000..3034d0348 --- /dev/null +++ b/apps/dashboard/src/oauth/providers/google.tsx @@ -0,0 +1,32 @@ +import { TokenSet } from "openid-client"; +import { OAuthBaseProvider } from "./base"; +import { OAuthUserInfo, validateUserInfo } from "../utils"; + +export class GoogleProvider extends OAuthBaseProvider { + constructor(options: { + clientId: string, + clientSecret: string, + }) { + super({ + issuer: "https://accounts.google.com", + authorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth", + tokenEndpoint: "https://oauth2.googleapis.com/token", + userinfoEndpoint: "https://openidconnect.googleapis.com/v1/userinfo", + redirectUri: process.env.NEXT_PUBLIC_STACK_URL + "/api/v1/auth/callback/google", + baseScope: "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile", + ...options, + }); + } + + async postProcessUserInfo(tokenSet: TokenSet): Promise { + const rawUserInfo = await this.oauthClient.userinfo(tokenSet); + return validateUserInfo({ + accountId: rawUserInfo.sub, + displayName: rawUserInfo.name, + email: rawUserInfo.email, + profileImageUrl: rawUserInfo.picture, + accessToken: tokenSet.access_token, + refreshToken: tokenSet.refresh_token, + }); + } +} diff --git a/apps/dashboard/src/oauth/providers/microsoft.tsx b/apps/dashboard/src/oauth/providers/microsoft.tsx new file mode 100644 index 000000000..a6aaf005a --- /dev/null +++ b/apps/dashboard/src/oauth/providers/microsoft.tsx @@ -0,0 +1,39 @@ +import { TokenSet } from "openid-client"; +import { OAuthBaseProvider } from "./base"; +import { OAuthUserInfo, validateUserInfo } from "../utils"; + +export class MicrosoftProvider extends OAuthBaseProvider { + constructor(options: { + clientId: string, + clientSecret: string, + }) { + super({ + issuer: "https://login.microsoftonline.com", + authorizationEndpoint: "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize", + tokenEndpoint: "https://login.microsoftonline.com/consumers/oauth2/v2.0/token", + redirectUri: process.env.NEXT_PUBLIC_STACK_URL + "/api/v1/auth/callback/microsoft", + baseScope: "User.Read", + ...options, + }); + } + + async postProcessUserInfo(tokenSet: TokenSet): Promise { + const rawUserInfo = await fetch( + 'https://graph.microsoft.com/v1.0/me', + { + headers: { + Authorization: `Bearer ${tokenSet.access_token}`, + }, + } + ).then(res => res.json()); + + return validateUserInfo({ + accountId: rawUserInfo.id, + displayName: rawUserInfo.displayName, + email: rawUserInfo.mail || rawUserInfo.userPrincipalName, + profileImageUrl: undefined, // Microsoft Graph API does not return profile image URL + accessToken: tokenSet.access_token, + refreshToken: tokenSet.refresh_token, + }); + } +} diff --git a/apps/dashboard/src/oauth/providers/spotify.tsx b/apps/dashboard/src/oauth/providers/spotify.tsx new file mode 100644 index 000000000..bae5241d1 --- /dev/null +++ b/apps/dashboard/src/oauth/providers/spotify.tsx @@ -0,0 +1,36 @@ +import { TokenSet } from "openid-client"; +import { OAuthBaseProvider } from "./base"; +import { OAuthUserInfo, validateUserInfo } from "../utils"; + +export class SpotifyProvider extends OAuthBaseProvider { + constructor(options: { + clientId: string, + clientSecret: string, + }) { + super({ + issuer: "https://accounts.spotify.com", + authorizationEndpoint: "https://accounts.spotify.com/authorize", + tokenEndpoint: "https://accounts.spotify.com/api/token", + redirectUri: process.env.NEXT_PUBLIC_STACK_URL + "/api/v1/auth/callback/spotify", + baseScope: "user-read-email user-read-private", + ...options, + }); + } + + async postProcessUserInfo(tokenSet: TokenSet): Promise { + const info = await fetch("https://api.spotify.com/v1/me", { + headers: { + Authorization: `Bearer ${tokenSet.access_token}`, + }, + }).then((res) => res.json()); + + return validateUserInfo({ + accountId: info.id, + displayName: info.display_name, + email: info.email, + profileImageUrl: info.images?.[0]?.url, + accessToken: tokenSet.access_token, + refreshToken: tokenSet.refresh_token, + }); + } +} diff --git a/apps/dashboard/src/oauth/utils.tsx b/apps/dashboard/src/oauth/utils.tsx new file mode 100644 index 000000000..89fe6abb1 --- /dev/null +++ b/apps/dashboard/src/oauth/utils.tsx @@ -0,0 +1,16 @@ +import * as yup from 'yup'; + +export type OAuthUserInfo = yup.InferType; + +const OAuthUserInfoSchema = yup.object().shape({ + accountId: yup.string().required(), + displayName: yup.string().nullable().default(null), + email: yup.string().required(), + profileImageUrl: yup.string().nullable().default(null), + accessToken: yup.string().nullable().default(null), + refreshToken: yup.string().nullable().default(null), +}); + +export function validateUserInfo(userInfo: any): OAuthUserInfo { + return OAuthUserInfoSchema.validateSync(userInfo); +} diff --git a/apps/dashboard/src/polyfills.tsx b/apps/dashboard/src/polyfills.tsx new file mode 100644 index 000000000..6a974ffcd --- /dev/null +++ b/apps/dashboard/src/polyfills.tsx @@ -0,0 +1,12 @@ +import { registerErrorSink } from "@stackframe/stack-shared/dist/utils/errors"; +import * as Sentry from "@sentry/nextjs"; + +const sentryErrorSink = (location: string, error: unknown) => { + Sentry.captureException(error, { extra: { location } }); +}; + +export function ensurePolyfilled() { + registerErrorSink(sentryErrorSink); +} + +ensurePolyfilled(); diff --git a/apps/dashboard/src/prisma-client.tsx b/apps/dashboard/src/prisma-client.tsx new file mode 100644 index 000000000..e399f372f --- /dev/null +++ b/apps/dashboard/src/prisma-client.tsx @@ -0,0 +1,14 @@ + +import { PrismaClient } from '@prisma/client'; + +// In dev mode, fast refresh causes us to recreate many Prisma clients, eventually overloading the database. +// Therefore, only create one Prisma client in dev mode. +const globalForPrisma = global as unknown as { prisma: PrismaClient }; + +// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition +export const prismaClient = globalForPrisma.prisma || new PrismaClient(); + +if (process.env.NODE_ENV !== 'production') { + globalForPrisma.prisma = prismaClient; +} + diff --git a/packages/stack-server/src/route-handlers/crud-handler.tsx b/apps/dashboard/src/route-handlers/crud-handler.tsx similarity index 100% rename from packages/stack-server/src/route-handlers/crud-handler.tsx rename to apps/dashboard/src/route-handlers/crud-handler.tsx diff --git a/apps/dashboard/src/route-handlers/prisma-handler.tsx b/apps/dashboard/src/route-handlers/prisma-handler.tsx new file mode 100644 index 000000000..66b9c6ae2 --- /dev/null +++ b/apps/dashboard/src/route-handlers/prisma-handler.tsx @@ -0,0 +1,151 @@ +import { CrudSchema, CrudTypeOf } from "@stackframe/stack-shared/dist/crud"; +import { CrudHandlers, RouteHandlerMetadataMap, createCrudHandlers } from "./crud-handler"; +import { SmartRequestAuth } from "./smart-request"; +import { Prisma } from "@prisma/client"; +import { GetResult } from "@prisma/client/runtime/library"; +import { StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { prismaClient } from "@/prisma-client"; + +type AllPrismaModelNames = Prisma.TypeMap["meta"]["modelProps"]; +type WhereUnique = Prisma.TypeMap["model"][Capitalize]["operations"]["findUniqueOrThrow"]["args"]["where"]; +type WhereMany = Prisma.TypeMap["model"][Capitalize]["operations"]["findMany"]["args"]["where"]; +type Where = { [K in keyof WhereMany as (K extends keyof WhereUnique ? K : never)]: WhereMany[K] }; +type Include = (Prisma.TypeMap["model"][Capitalize]["operations"]["findMany"]["args"] & { include?: unknown })["include"]; +type BaseFields = Where & Partial>; +type PRead, I extends Include> = GetResult]["payload"], { where: W, include: I }, "findUniqueOrThrow">; +type PUpdate = Prisma.TypeMap["model"][Capitalize]["operations"]["update"]["args"]["data"]; +type PCreate = Prisma.TypeMap["model"][Capitalize]["operations"]["create"]["args"]["data"]; + +type Context = { + params: Record, + auth: SmartRequestAuth, +}; + +type CRead> = T extends { Admin: { Read: infer R } } ? R : never; +type CCreate> = T extends { Admin: { Create: infer R } } ? R : never; +type CUpdate> = T extends { Admin: { Update: infer R } } ? R : never; +type CEitherWrite> = CCreate | CUpdate; + +export type CrudHandlersFromCrudType> = CrudHandlers< + | ("Create" extends keyof T["Admin"] ? "Create" : never) + | ("Read" extends keyof T["Admin"] ? "Read" : never) + | ("Read" extends keyof T["Admin"] ? "List" : never) + | ("Update" extends keyof T["Admin"] ? "Update" : never) + | ("Delete" extends keyof T["Admin"] ? "Delete" : never) +>; + +export function createPrismaCrudHandlers< + S extends CrudSchema, + PrismaModelName extends AllPrismaModelNames, + ParamName extends string, + W extends Where, + I extends Include, + B extends BaseFields, +>( + crudSchema: S, + prismaModelName: PrismaModelName, + options: & { + paramNames: ParamName[], + baseFields: (context: Context) => Promise, + where?: (context: Context) => Promise, + whereUnique?: (context: Context) => Promise>, + include: (context: Context) => Promise, + crudToPrisma?: ((crud: CEitherWrite>, context: Context) => Promise | Omit, keyof B>>), + prismaToCrud?: (prisma: PRead, context: Context) => Promise>>, + fieldMapping?: any, + createNotFoundError?: (context: Context) => Error, + metadataMap?: RouteHandlerMetadataMap, + } + & ( + | { + crudToPrisma: {}, + prismaToCrud: {}, + fieldMapping?: void, + } + | { + crudToPrisma: void, + prismaToCrud: void, + fieldMapping: {}, + } + ), +): CrudHandlersFromCrudType> { + const wrapper = (func: (data: any, context: Context, queryBase: any) => Promise): (opts: { params: Record, data?: unknown, auth: SmartRequestAuth }) => Promise => { + return async (req) => { + const context: Context = { + params: req.params, + auth: req.auth, + }; + const whereBase = await options.where?.(context); + const includeBase = await options.include(context); + try { + return await func(req.data, context, { where: whereBase, include: includeBase }); + } catch (e) { + if ((e as any)?.code === 'P2025') { + throw (options.createNotFoundError ?? (() => new StatusError(StatusError.NotFound)))(context); + } + throw e; + } + }; + }; + + const prismaToCrud = options.prismaToCrud ?? throwErr("missing prismaToCrud is not yet implemented"); + const crudToPrisma = options.crudToPrisma ?? throwErr("missing crudToPrisma is not yet implemented"); + + return createCrudHandlers(crudSchema, { + paramNames: options.paramNames, + onRead: wrapper(async (data, context) => { + const prisma = await (prismaClient[prismaModelName].findUniqueOrThrow as any)({ + include: await options.include(context), + where: { + ...await options.baseFields(context), + ...await options.where?.(context), + ...await options.whereUnique?.(context), + }, + }); + return await prismaToCrud(prisma, context); + }), + onList: wrapper(async (data, context) => { + const prisma: any[] = await (prismaClient[prismaModelName].findMany as any)({ + include: await options.include(context), + where: { + ...await options.baseFields(context), + ...await options.where?.(context), + }, + }); + return await Promise.all(prisma.map((p) => prismaToCrud(p, context))); + }), + onCreate: wrapper(async (data, context) => { + const prisma = await (prismaClient[prismaModelName].create as any)({ + include: await options.include(context), + data: { + ...await options.baseFields(context), + ...await crudToPrisma(data, context), + }, + }); + return await prismaToCrud(prisma, context); + }), + onUpdate: wrapper(async (data, context) => { + const prisma = await (prismaClient[prismaModelName].update as any)({ + include: await options.include(context), + where: { + ...await options.baseFields(context), + ...await options.where?.(context), + ...await options.whereUnique?.(context), + }, + data: await crudToPrisma(data, context), + }); + return await prismaToCrud(prisma, context); + }), + onDelete: wrapper(async (data, context) => { + await (prismaClient[prismaModelName].delete as any)({ + include: await options.include(context), + where: { + ...await options.baseFields(context), + ...await options.where?.(context), + ...await options.whereUnique?.(context), + }, + }); + }), + metadataMap: options.metadataMap, + }); +} diff --git a/apps/dashboard/src/route-handlers/redirect-handler.tsx b/apps/dashboard/src/route-handlers/redirect-handler.tsx new file mode 100644 index 000000000..e5b101f95 --- /dev/null +++ b/apps/dashboard/src/route-handlers/redirect-handler.tsx @@ -0,0 +1,37 @@ +import "../polyfills"; + +import { NextRequest } from "next/server"; +import * as yup from "yup"; +import { createSmartRouteHandler } from "./smart-route-handler"; + +export function redirectHandler(redirectPath: string, statusCode: 301 | 302 | 303 | 307 | 308 = 307): (req: NextRequest, options: any) => Promise { + return createSmartRouteHandler({ + request: yup.object({ + url: yup.string().required(), + method: yup.string().oneOf(["GET"]).required(), + }), + response: yup.object({ + statusCode: yup.number().oneOf([statusCode]).required(), + headers: yup.object().shape({ + location: yup.array(yup.string().required()), + }), + bodyType: yup.string().oneOf(["text"]).required(), + body: yup.string().required(), + }), + async handler(req) { + const urlWithTrailingSlash = new URL(req.url); + if (!urlWithTrailingSlash.pathname.endsWith("/")) { + urlWithTrailingSlash.pathname += "/"; + } + const newUrl = new URL(redirectPath, urlWithTrailingSlash); + return { + statusCode, + headers: { + location: [newUrl.toString()], + }, + bodyType: "text", + body: "Redirecting...", + }; + }, + }); +} diff --git a/packages/stack-server/src/route-handlers/smart-request.tsx b/apps/dashboard/src/route-handlers/smart-request.tsx similarity index 99% rename from packages/stack-server/src/route-handlers/smart-request.tsx rename to apps/dashboard/src/route-handlers/smart-request.tsx index b188ac82e..1f9c6bcd4 100644 --- a/packages/stack-server/src/route-handlers/smart-request.tsx +++ b/apps/dashboard/src/route-handlers/smart-request.tsx @@ -158,21 +158,21 @@ async function parseAuth(req: NextRequest): Promise { case "client": { if (!publishableClientKey) throw new KnownErrors.ClientAuthenticationRequired(); const isValid = await checkApiKeySet(projectId, { publishableClientKey }); - if (!isValid) throw new KnownErrors.InvalidPublishableClientKey(); + if (!isValid) throw new KnownErrors.InvalidPublishableClientKey(projectId); projectAccessType = "key"; break; } case "server": { if (!secretServerKey) throw new KnownErrors.ServerAuthenticationRequired(); const isValid = await checkApiKeySet(projectId, { secretServerKey }); - if (!isValid) throw new KnownErrors.InvalidSecretServerKey(); + if (!isValid) throw new KnownErrors.InvalidSecretServerKey(projectId); projectAccessType = "key"; break; } case "admin": { if (!superSecretAdminKey) throw new KnownErrors.AdminAuthenticationRequired(); const isValid = await checkApiKeySet(projectId, { superSecretAdminKey }); - if (!isValid) throw new KnownErrors.InvalidSuperSecretAdminKey(); + if (!isValid) throw new KnownErrors.InvalidSuperSecretAdminKey(projectId); projectAccessType = "key"; break; } diff --git a/apps/dashboard/src/route-handlers/smart-response.tsx b/apps/dashboard/src/route-handlers/smart-response.tsx new file mode 100644 index 000000000..0e14bcac1 --- /dev/null +++ b/apps/dashboard/src/route-handlers/smart-response.tsx @@ -0,0 +1,110 @@ +import "../polyfills"; + +import { NextRequest } from "next/server"; +import * as yup from "yup"; +import { Json } from "@stackframe/stack-shared/dist/utils/json"; +import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; + +export type SmartResponse = { + statusCode: number, + headers?: Record, +} & ( + | { + bodyType?: undefined, + body?: ArrayBuffer | Json, + } + | { + bodyType: "text", + body?: string, + } + | { + bodyType: "json", + body?: Json, + } + | { + bodyType: "binary", + body?: ArrayBuffer, + } +); + +async function validate(req: NextRequest, obj: unknown, schema: yup.Schema): Promise { + try { + return await schema.validate(obj, { + abortEarly: false, + stripUnknown: true, + }); + } catch (error) { + throw new StackAssertionError(`Error occured during ${req.url} response validation: ${error}`, { obj, schema, error }, { cause: error }); + } +} + + +function isBinaryBody(body: unknown): body is BodyInit { + return body instanceof ArrayBuffer + || body instanceof SharedArrayBuffer + || body instanceof Blob + || ArrayBuffer.isView(body); +} + +export async function createResponse(req: NextRequest, requestId: string, obj: T, schema: yup.Schema): Promise { + const validated = await validate(req, obj, schema); + + let status = validated.statusCode; + const headers = new Map; + + let arrayBufferBody; + if (obj.body === undefined) { + arrayBufferBody = new ArrayBuffer(0); + } else { + const bodyType = validated.bodyType ?? (isBinaryBody(validated.body) ? "binary" : "json"); + switch (bodyType) { + case "json": { + headers.set("content-type", ["application/json; charset=utf-8"]); + arrayBufferBody = new TextEncoder().encode(JSON.stringify(validated.body)); + break; + } + case "text": { + headers.set("content-type", ["text/plain; charset=utf-8"]); + if (typeof validated.body !== "string") throw new Error(`Invalid body, expected string, got ${validated.body}`); + arrayBufferBody = new TextEncoder().encode(validated.body); + break; + } + case "binary": { + if (!isBinaryBody(validated.body)) throw new Error(`Invalid body, expected ArrayBuffer, got ${validated.body}`); + arrayBufferBody = validated.body; + break; + } + default: { + throw new Error(`Invalid body type: ${bodyType}`); + } + } + } + + + // Add the request ID to the response headers + headers.set("x-stack-request-id", [requestId]); + + + // Disable caching by default + headers.set("cache-control", ["no-store, max-age=0"]); + + + // If the x-stack-override-error-status header is given, override error statuses to 200 + if (req.headers.has("x-stack-override-error-status") && status >= 400 && status < 600) { + status = 200; + headers.set("x-stack-actual-status", [validated.statusCode.toString()]); + } + + return new Response( + arrayBufferBody, + { + status, + headers: [ + ...Object.entries({ + ...Object.fromEntries(headers), + ...validated.headers ?? {} + }).flatMap(([key, values]) => values.map(v => [key.toLowerCase(), v!] as [string, string])), + ], + }, + ); +} diff --git a/apps/dashboard/src/route-handlers/smart-route-handler.tsx b/apps/dashboard/src/route-handlers/smart-route-handler.tsx new file mode 100644 index 000000000..8022e48ed --- /dev/null +++ b/apps/dashboard/src/route-handlers/smart-route-handler.tsx @@ -0,0 +1,226 @@ +import "../polyfills"; + +import { NextRequest } from "next/server"; +import { StackAssertionError, StatusError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; +import * as yup from "yup"; +import { DeepPartial } from "@stackframe/stack-shared/dist/utils/objects"; +import { generateSecureRandomString } from "@stackframe/stack-shared/dist/utils/crypto"; +import { KnownErrors } from "@stackframe/stack-shared/dist/known-errors"; +import { runAsynchronously, wait } from "@stackframe/stack-shared/dist/utils/promises"; +import { MergeSmartRequest, SmartRequest, createLazyRequestParser } from "./smart-request"; +import { SmartResponse, createResponse } from "./smart-response"; + +class InternalServerError extends StatusError { + constructor() { + super(StatusError.InternalServerError); + } +} + +/** + * Known errors that are common and should not be logged with their stacktrace. + */ +const commonErrors = [ + KnownErrors.AccessTokenExpired, + InternalServerError, +]; + +/** + * Catches the given error, logs it if needed and returns it as a StatusError. Errors that are not actually errors + * (such as Next.js redirects) will be rethrown. + */ +function catchError(error: unknown): StatusError { + // catch some Next.js non-errors and rethrow them + if (error instanceof Error) { + const digest = (error as any)?.digest; + if (typeof digest === "string") { + if (["NEXT_REDIRECT", "DYNAMIC_SERVER_USAGE"].some(m => digest.startsWith(m))) { + throw error; + } + } + } + + if (error instanceof StatusError) return error; + captureError(`route-handler`, error); + return new InternalServerError(); +} + +/** + * Catches any errors thrown in the handler and returns a 500 response with the thrown error message. Also logs the + * request details. + */ +export function deprecatedSmartRouteHandler(handler: (req: NextRequest, options: any, requestId: string) => Promise): (req: NextRequest, options: any) => Promise { + return async (req: NextRequest, options: any) => { + const requestId = generateSecureRandomString(80); + let hasRequestFinished = false; + try { + // censor long query parameters because they might contain sensitive data + const censoredUrl = new URL(req.url); + for (const [key, value] of censoredUrl.searchParams.entries()) { + if (value.length <= 8) { + continue; + } + censoredUrl.searchParams.set(key, value.slice(0, 4) + "--REDACTED--" + value.slice(-4)); + } + + // request duration warning + const warnAfterSeconds = 12; + runAsynchronously(async () => { + await wait(warnAfterSeconds * 1000); + if (!hasRequestFinished) { + captureError("request-timeout-watcher", new Error(`Request with ID ${requestId} to endpoint ${req.nextUrl.pathname} has been running for ${warnAfterSeconds} seconds. Try to keep requests short. The request may be cancelled by the serverless provider if it takes too long.`)); + } + }); + + console.log(`[API REQ] [${requestId}] ${req.method} ${censoredUrl}`); + const timeStart = performance.now(); + const res = await handler(req, options, requestId); + const time = (performance.now() - timeStart); + console.log(`[ RES] [${requestId}] ${req.method} ${censoredUrl} (in ${time.toFixed(0)}ms)`); + return res; + } catch (e) { + let statusError: StatusError; + try { + statusError = catchError(e); + } catch (e) { + console.log(`[ EXC] [${requestId}] ${req.method} ${req.url}: Non-error caught (such as a redirect), will be rethrown. Digest: ${(e as any)?.digest}`); + throw e; + } + + console.log(`[ ERR] [${requestId}] ${req.method} ${req.url}: ${statusError.message}`); + if (!commonErrors.some(e => statusError instanceof e)) { + console.debug(`For the error above with request ID ${requestId}, the full error is:`, statusError); + } + + const res = await createResponse(req, requestId, { + statusCode: statusError.statusCode, + bodyType: "binary", + body: statusError.getBody(), + headers: { + ...statusError.getHeaders(), + }, + }, yup.mixed()); + return res; + } finally { + hasRequestFinished = true; + } + }; +}; + +export type SmartRouteHandlerOverloadMetadata = { + summary: string, + description: string, + tags: string[], +}; + +export type SmartRouteHandlerOverload< + Req extends DeepPartial, + Res extends SmartResponse, +> = { + metadata?: SmartRouteHandlerOverloadMetadata, + request: yup.Schema, + response: yup.Schema, + handler: (req: Req & MergeSmartRequest, fullReq: SmartRequest) => Promise, +}; + +export type SmartRouteHandlerOverloadGenerator< + OverloadParam, + Req extends DeepPartial, + Res extends SmartResponse, +> = (param: OverloadParam) => SmartRouteHandlerOverload; + +export type SmartRouteHandler< + OverloadParam = unknown, + Req extends DeepPartial = DeepPartial, + Res extends SmartResponse = SmartResponse, +> = ((req: NextRequest, options: any) => Promise) & { + overloads: Map>, +} + +const smartRouteHandlerSymbol = Symbol("smartRouteHandler"); + +export function isSmartRouteHandler(handler: any): handler is SmartRouteHandler { + return handler?.[smartRouteHandlerSymbol] === true; +} + +export function createSmartRouteHandler< + Req extends DeepPartial, + Res extends SmartResponse, +>( + handler: SmartRouteHandlerOverload, +): SmartRouteHandler +export function createSmartRouteHandler< + OverloadParam, + Req extends DeepPartial, + Res extends SmartResponse, +>( + overloadParams: readonly OverloadParam[], + overloadGenerator: SmartRouteHandlerOverloadGenerator +): SmartRouteHandler +export function createSmartRouteHandler< + Req extends DeepPartial, + Res extends SmartResponse, +>( + ...args: [readonly unknown[], SmartRouteHandlerOverloadGenerator] | [SmartRouteHandlerOverload] +): SmartRouteHandler { + const overloadParams = args.length > 1 ? args[0] as unknown[] : [undefined]; + const overloadGenerator = args.length > 1 ? args[1]! : () => (args[0] as SmartRouteHandlerOverload); + + const overloads = new Map(overloadParams.map((overloadParam) => [ + overloadParam, + overloadGenerator(overloadParam), + ])); + if (overloads.size !== overloadParams.length) { + throw new StackAssertionError("Duplicate overload parameters"); + } + + return Object.assign(deprecatedSmartRouteHandler(async (req, options, requestId) => { + const reqsParsed: [[Req, SmartRequest], SmartRouteHandlerOverload][] = []; + const reqsErrors: unknown[] = []; + const bodyBuffer = await req.arrayBuffer(); + for (const [overloadParam, handler] of overloads.entries()) { + const requestParser = await createLazyRequestParser(req, bodyBuffer, handler.request, options); + try { + const parserRes = await requestParser(); + reqsParsed.push([parserRes, handler]); + } catch (e) { + reqsErrors.push(e); + } + } + if (reqsParsed.length === 0) { + if (reqsErrors.length === 1) { + throw reqsErrors[0]; + } else { + const caughtErrors = reqsErrors.map(e => catchError(e)); + throw new KnownErrors.AllOverloadsFailed(caughtErrors.map(e => e.toHttpJson())); + } + } + + const smartReq = reqsParsed[0][0][0]; + const fullReq = reqsParsed[0][0][1]; + const handler = reqsParsed[0][1]; + + let smartRes = await handler.handler(smartReq as any, fullReq); + + return await createResponse(req, requestId, smartRes, handler.response); + }), { + [smartRouteHandlerSymbol]: true, + overloads, + }); +} + +/** + * needed in the multi-overload smartRouteHandler for weird TypeScript reasons that I don't understand + * + * if you can remove this wherever it's used without causing type errors, it's safe to remove + */ +export function routeHandlerTypeHelper, Res extends SmartResponse>(handler: { + request: yup.Schema, + response: yup.Schema, + handler: (req: Req & MergeSmartRequest, fullReq: SmartRequest) => Promise, +}): { + request: yup.Schema, + response: yup.Schema, + handler: (req: Req & MergeSmartRequest, fullReq: SmartRequest) => Promise, +} { + return handler; +} diff --git a/packages/stack-server/src/stack.tsx b/apps/dashboard/src/stack.tsx similarity index 88% rename from packages/stack-server/src/stack.tsx rename to apps/dashboard/src/stack.tsx index 1179f3ad2..6b2008a23 100644 --- a/packages/stack-server/src/stack.tsx +++ b/apps/dashboard/src/stack.tsx @@ -3,7 +3,7 @@ import './polyfills'; import { StackServerApp } from '@stackframe/stack'; if (process.env.NEXT_PUBLIC_STACK_PROJECT_ID !== "internal") { - throw new Error("This project is not configured correctly. stack-server must always use the internal project."); + throw new Error("This project is not configured correctly. stack-dashboard must always use the internal project."); } export const stackServerApp = new StackServerApp<"nextjs-cookie", true, 'internal'>({ diff --git a/packages/stack-server/tailwind.config.ts b/apps/dashboard/tailwind.config.ts similarity index 100% rename from packages/stack-server/tailwind.config.ts rename to apps/dashboard/tailwind.config.ts diff --git a/apps/dashboard/tsconfig.json b/apps/dashboard/tsconfig.json new file mode 100644 index 000000000..d39cb8dfc --- /dev/null +++ b/apps/dashboard/tsconfig.json @@ -0,0 +1,43 @@ +{ + "compilerOptions": { + "target": "es2015", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "noErrorTruncation": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./src/*" + ] + }, + "skipLibCheck": true + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + "**/*.?ts", + "**/*.?tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/apps/e2e/.env b/apps/e2e/.env index 3c175e37a..9f06c65a8 100644 --- a/apps/e2e/.env +++ b/apps/e2e/.env @@ -1,3 +1,4 @@ +SERVER_BASE_URL= INTERNAL_PROJECT_ID= INTERNAL_PROJECT_CLIENT_KEY= -SERVER_BASE_URL=http://localhost:8101 +INTERNAL_PROJECT_SERVER_KEY= diff --git a/apps/e2e/.env.development b/apps/e2e/.env.development index 22fa9f0ce..0eb3b2d17 100644 --- a/apps/e2e/.env.development +++ b/apps/e2e/.env.development @@ -1,6 +1,4 @@ -# Contains the credentials for the internal project of Stack's default development environment setup. -# Do not use in a production environment, instead replace it with actual values gathered from https://app.stack-auth.com. -NEXT_PUBLIC_STACK_URL=http://localhost:8101 -NEXT_PUBLIC_STACK_PROJECT_ID=internal -NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only -STACK_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only +SERVER_BASE_URL=http://localhost:8101 +INTERNAL_PROJECT_ID=internal +INTERNAL_PROJECT_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only +INTERNAL_PROJECT_SERVER_KEY=this-secret-server-key-is-for-local-development-only diff --git a/apps/e2e/package.json b/apps/e2e/package.json index 519c0ef27..68d76b347 100644 --- a/apps/e2e/package.json +++ b/apps/e2e/package.json @@ -4,8 +4,8 @@ "private": true, "type": "module", "scripts": { - "test": "dotenv -c -- vitest watch", - "test:ci": "vitest run" + "test:watch": "dotenv -c development -- vitest watch", + "test": "dotenv -c development -- vitest run" }, "dependencies": {}, "devDependencies": { diff --git a/apps/e2e/tests/stack-server/api/internal-project.test.ts b/apps/e2e/tests/api/internal-project.test.ts similarity index 97% rename from apps/e2e/tests/stack-server/api/internal-project.test.ts rename to apps/e2e/tests/api/internal-project.test.ts index 72d608f97..8e7f9e55b 100644 --- a/apps/e2e/tests/stack-server/api/internal-project.test.ts +++ b/apps/e2e/tests/api/internal-project.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "vitest"; import request from "supertest"; -import { BASE_URL, INTERNAL_PROJECT_CLIENT_KEY, INTERNAL_PROJECT_ID } from "../../helpers"; +import { BASE_URL, INTERNAL_PROJECT_CLIENT_KEY, INTERNAL_PROJECT_ID } from "../helpers"; import crypto from "crypto"; const AUTH_HEADER = { @@ -40,11 +40,13 @@ async function signInWithEmailPassword(email: string, password: string) { describe("Various internal project tests", () => { test("Main Page", async () => { const response = await request(BASE_URL).get("/"); + console.log(response); expect(response.status).toBe(307); }); test("API root (no authentication)", async () => { const response = await request(BASE_URL).get("/api/v1"); + console.log(response); expect(response.status).toBe(200); expect(response.text).contains("Stack API") expect(response.text).contains("Authentication: None") @@ -52,18 +54,21 @@ describe("Various internal project tests", () => { test("Credential sign up", async () => { const { response } = await signUpWithEmailPassword(); + console.log(response); expect(response.status).toBe(200); }); test("Credential sign in", async () => { const { email, password } = await signUpWithEmailPassword(); const { response } = await signInWithEmailPassword(email, password); + console.log(response); expect(response.status).toBe(200); }); test("No current user without authentication", async () => { const response = await request(BASE_URL).get("/api/v1/current-user").set(AUTH_HEADER); + console.log(response); expect(response.status).toBe(200); expect(response.body).toBe(null); }); diff --git a/dependencies.compose.yaml b/dependencies.compose.yaml index 926035c9a..772d9f1f2 100644 --- a/dependencies.compose.yaml +++ b/dependencies.compose.yaml @@ -13,7 +13,7 @@ services: image: inbucket/inbucket:latest ports: - 2500:2500 - - 9000:9000 + - 8105:9000 - 1100:1100 volumes: - inbucket-data:/data diff --git a/docs/package.json b/docs/package.json index 5eab46621..f1b31a859 100644 --- a/docs/package.json +++ b/docs/package.json @@ -14,7 +14,7 @@ "author": "", "dependencies": { "fern-api": "^0.30.7", - "@stackframe/stack-server": "workspace:*" + "@stackframe/stack-backend": "workspace:*" }, "devDependencies": { "rimraf": "^5.0.7" diff --git a/examples/partial-prerendering/package.json b/examples/partial-prerendering/package.json index 2fdfffd72..57b2730c6 100644 --- a/examples/partial-prerendering/package.json +++ b/examples/partial-prerendering/package.json @@ -3,9 +3,9 @@ "version": "2.4.27", "private": true, "scripts": { - "dev": "next dev --port 8105", + "dev": "next dev --port 8109", "build": "next build", - "start": "next start --port 8105", + "start": "next start --port 8109", "lint": "next lint" }, "dependencies": { diff --git a/package.json b/package.json index b58032b64..fcf577aca 100644 --- a/package.json +++ b/package.json @@ -7,23 +7,23 @@ "preinstall": "npx -y only-allow pnpm", "typecheck": "only-allow pnpm && turbo typecheck", "build": "only-allow pnpm && turbo build", - "build:server": "only-allow pnpm && turbo run build --no-cache --filter=@stackframe/stack-server...", + "build:backend": "only-allow pnpm && turbo run build --no-cache --filter=@stackframe/stack-backend...", + "build:dashboard": "only-allow pnpm && turbo run build --no-cache --filter=@stackframe/stack-dashboard...", "build:demo": "only-allow pnpm && turbo run build --no-cache --filter=demo-app...", "clean": "only-allow pnpm && turbo run clean --no-cache && rimraf --glob **/.next && rimraf --glob **/.turbo && rimraf --glob **/node_modules", "codegen": "only-allow pnpm && turbo run codegen --no-cache", - "psql:server": "only-allow pnpm && pnpm run --filter=@stackframe/stack-server psql", - "prisma:server": "only-allow pnpm && pnpm run --filter=@stackframe/stack-server prisma", + "psql": "only-allow pnpm && pnpm run --filter=@stackframe/stack-backend psql", + "prisma": "only-allow pnpm && pnpm run --filter=@stackframe/stack-backend prisma", "fern": "only-allow pnpm && pnpm run --filter=@stackframe/docs fern", "dev": "only-allow pnpm && turbo run dev --parallel --continue", - "dev:app": "only-allow pnpm && turbo run dev --continue --filter=@stackframe/dev-app...", - "dev:server": "only-allow pnpm && turbo run dev --continue --filter=@stackframe/stack-server...", - "dev:email": "only-allow pnpm && turbo run email --continue --filter=@stackframe/stack-server...", "start": "only-allow pnpm && turbo run start --parallel --continue", - "start:server": "only-allow pnpm && turbo run start --continue --filter=@stackframe/stack-server...", + "start:backend": "only-allow pnpm && turbo run start --continue --filter=@stackframe/stack-backend...", + "start:dashboard": "only-allow pnpm && turbo run start --continue --filter=@stackframe/stack-dashboard...", "lint": "only-allow pnpm && turbo run lint --no-cache -- --max-warnings=0", "release": "only-allow pnpm && release", "peek": "only-allow pnpm && pnpm release --peek", "changeset": "only-allow pnpm && changeset", + "test:watch": "only-allow pnpm && turbo run test:watch", "test": "only-allow pnpm && turbo run test", "generate-docs": "only-allow pnpm && turbo run generate-docs --no-cache", "generate-keys": "only-allow pnpm && turbo run generate-keys --no-cache" diff --git a/packages/stack-server/src/server.ts b/packages/stack-server/src/server.ts deleted file mode 100644 index b38520edf..000000000 --- a/packages/stack-server/src/server.ts +++ /dev/null @@ -1,25 +0,0 @@ -// server.js -import { Server, createServer } from 'http'; -import next from 'next'; - -const app = next({ dev: true }); -const handle = app.getRequestHandler(); - -let serverInstance: Server | null = null; - -export function startServer(port: number) { - return app.prepare().then(() => { - // eslint-disable-next-line @typescript-eslint/no-misused-promises - const server = createServer((req, res) => handle(req, res).catch((err) => console.error(err))); - serverInstance = server.listen(port); - console.log(`> Ready on http://localhost:${port}`); - return server; - }); -} - -export function stopServer() { - if (serverInstance) { - serverInstance.close(); - console.log('> Server stopped'); - } -} diff --git a/packages/stack-shared/src/known-errors.tsx b/packages/stack-shared/src/known-errors.tsx index 99d4ae58f..5e359cc43 100644 --- a/packages/stack-shared/src/known-errors.tsx +++ b/packages/stack-shared/src/known-errors.tsx @@ -41,6 +41,7 @@ export abstract class KnownError extends StatusError { code: this.errorCode, message: this.humanReadableMessage, details: this.details, + error: true, }, undefined, 2)); } @@ -240,31 +241,40 @@ const RequestTypeWithoutProjectId = createKnownErrorConstructor( const InvalidPublishableClientKey = createKnownErrorConstructor( InvalidProjectAuthentication, "INVALID_PUBLISHABLE_CLIENT_KEY", - () => [ + (projectId: string) => [ 401, - "The publishable key is not valid for the given project. Does the project and/or the key exist?", + `The publishable key is not valid for the project ${JSON.stringify(projectId)}. Does the project and/or the key exist?`, + { + projectId, + }, ] as const, - () => [] as const, + (json: any) => [json.projectId] as const, ); const InvalidSecretServerKey = createKnownErrorConstructor( InvalidProjectAuthentication, "INVALID_SECRET_SERVER_KEY", - () => [ + (projectId: string) => [ 401, - "The secret server key is not valid for the given project. Does the project and/or the key exist?", + `The secret server key is not valid for the project ${JSON.stringify(projectId)}. Does the project and/or the key exist?`, + { + projectId, + }, ] as const, - () => [] as const, + (json: any) => [json.projectId] as const, ); const InvalidSuperSecretAdminKey = createKnownErrorConstructor( InvalidProjectAuthentication, "INVALID_SUPER_SECRET_ADMIN_KEY", - () => [ + (projectId: string) => [ 401, - "The super secret admin key is not valid for the given project. Does the project and/or the key exist?", + `The super secret admin key is not valid for the project ${JSON.stringify(projectId)}. Does the project and/or the key exist?`, + { + projectId, + }, ] as const, - () => [] as const, + (json: any) => [json.projectId] as const, ); const InvalidAdminAccessToken = createKnownErrorConstructor( diff --git a/packages/stack/src/lib/stack-app.ts b/packages/stack/src/lib/stack-app.ts index 0b85337e5..e3fcd9600 100644 --- a/packages/stack/src/lib/stack-app.ts +++ b/packages/stack/src/lib/stack-app.ts @@ -183,7 +183,6 @@ function createEmptyTokenStore() { }); } -const loadingSentinel = Symbol("stackAppCacheLoadingSentinel"); const cachePromiseByComponentId = new Map>(); function useAsyncCache(cache: AsyncCache, dependencies: D, caller: string): T { // we explicitly don't want to run this hook in SSR diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3001d2f38..4fc338cf5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -85,6 +85,425 @@ importers: specifier: ^1.5.0 version: 1.6.0(@types/node@20.14.2)(jsdom@24.1.0)(terser@5.31.1) + apps/backend: + dependencies: + '@hookform/resolvers': + specifier: ^3.3.4 + version: 3.6.0(react-hook-form@7.52.0(react@18.3.1)) + '@next/bundle-analyzer': + specifier: ^14.0.3 + version: 14.2.4 + '@node-oauth/oauth2-server': + specifier: ^5.1.0 + version: 5.1.0 + '@prisma/client': + specifier: ^5.9.1 + version: 5.15.0(prisma@5.15.0) + '@react-email/components': + specifier: ^0.0.14 + version: 0.0.14(@types/react@18.3.3)(react@18.3.1) + '@react-email/render': + specifier: ^0.0.12 + version: 0.0.12 + '@react-email/tailwind': + specifier: ^0.0.14 + version: 0.0.14(react@18.3.1) + '@sentry/nextjs': + specifier: ^7.105.0 + version: 7.117.0(next@14.2.3(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.92.0(@swc/core@1.3.101)(esbuild@0.21.5)) + '@stackframe/stack-shared': + specifier: workspace:* + version: link:../../packages/stack-shared + '@vercel/analytics': + specifier: ^1.2.2 + version: 1.3.1(next@14.2.3(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + bcrypt: + specifier: ^5.1.1 + version: 5.1.1 + date-fns: + specifier: ^3.6.0 + version: 3.6.0 + dotenv-cli: + specifier: ^7.3.0 + version: 7.4.1 + handlebars: + specifier: ^4.7.8 + version: 4.7.8 + jose: + specifier: ^5.2.2 + version: 5.4.0 + lodash: + specifier: ^4.17.21 + version: 4.17.21 + next: + specifier: ^14.1 + version: 14.2.3(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + nodemailer: + specifier: ^6.9.10 + version: 6.9.13 + openid-client: + specifier: ^5.6.4 + version: 5.6.5 + pg: + specifier: ^8.11.3 + version: 8.12.0 + posthog-js: + specifier: ^1.138.1 + version: 1.139.2 + prettier: + specifier: ^3.2.5 + version: 3.3.2 + react: + specifier: ^18.2 + version: 18.3.1 + react-email: + specifier: 2.1.0 + version: 2.1.0(@babel/core@7.24.7)(@swc/helpers@0.5.11)(eslint@8.30.0) + server-only: + specifier: ^0.0.1 + version: 0.0.1 + sharp: + specifier: ^0.32.6 + version: 0.32.6 + yaml: + specifier: ^2.4.5 + version: 2.4.5 + yup: + specifier: ^1.4.0 + version: 1.4.0 + devDependencies: + '@types/bcrypt': + specifier: ^5.0.2 + version: 5.0.2 + '@types/lodash': + specifier: ^4.17.4 + version: 4.17.5 + '@types/node': + specifier: ^20.8.10 + version: 20.14.2 + '@types/nodemailer': + specifier: ^6.4.14 + version: 6.4.15 + '@types/react': + specifier: ^18.2.66 + version: 18.3.3 + glob: + specifier: ^10.4.1 + version: 10.4.1 + prisma: + specifier: ^5.9.1 + version: 5.15.0 + rimraf: + specifier: ^5.0.5 + version: 5.0.7 + tsx: + specifier: ^4.7.2 + version: 4.15.5 + + apps/dashboard: + dependencies: + '@hookform/resolvers': + specifier: ^3.3.4 + version: 3.6.0(react-hook-form@7.52.0(react@18.3.1)) + '@mdx-js/loader': + specifier: ^3 + version: 3.0.1(webpack@5.92.0(@swc/core@1.3.101)(esbuild@0.21.5)) + '@mdx-js/react': + specifier: ^3.0.0 + version: 3.0.1(@types/react@18.3.3)(react@18.3.1) + '@next/bundle-analyzer': + specifier: ^14.0.3 + version: 14.2.4 + '@next/mdx': + specifier: ^14 + version: 14.2.4(@mdx-js/loader@3.0.1(webpack@5.92.0(@swc/core@1.3.101)(esbuild@0.21.5)))(@mdx-js/react@3.0.1(@types/react@18.3.3)(react@18.3.1)) + '@node-oauth/oauth2-server': + specifier: ^5.1.0 + version: 5.1.0 + '@prisma/client': + specifier: ^5.9.1 + version: 5.15.0(prisma@5.15.0) + '@radix-ui/react-accordion': + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-alert-dialog': + specifier: ^1.0.5 + version: 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-aspect-ratio': + specifier: ^1.0.3 + version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-avatar': + specifier: ^1.0.4 + version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-checkbox': + specifier: ^1.0.4 + version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collapsible': + specifier: ^1.0.3 + version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-context-menu': + specifier: ^2.1.5 + version: 2.1.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dialog': + specifier: ^1.0.5 + version: 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dropdown-menu': + specifier: ^2.0.6 + version: 2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-hover-card': + specifier: ^1.0.7 + version: 1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-icons': + specifier: ^1.3.0 + version: 1.3.0(react@18.3.1) + '@radix-ui/react-label': + specifier: ^2.0.2 + version: 2.0.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-menubar': + specifier: ^1.0.4 + version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-navigation-menu': + specifier: ^1.1.4 + version: 1.1.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popover': + specifier: ^1.0.7 + version: 1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-progress': + specifier: ^1.0.3 + version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-radio-group': + specifier: ^1.1.3 + version: 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-scroll-area': + specifier: ^1.0.5 + version: 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-select': + specifier: ^2.0.0 + version: 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-separator': + specifier: ^1.0.3 + version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slider': + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': + specifier: ^1.0.2 + version: 1.0.2(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-switch': + specifier: ^1.0.3 + version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tabs': + specifier: ^1.0.4 + version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toast': + specifier: ^1.1.5 + version: 1.1.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toggle': + specifier: ^1.0.3 + version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toggle-group': + specifier: ^1.0.4 + version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tooltip': + specifier: ^1.0.7 + version: 1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-email/components': + specifier: ^0.0.14 + version: 0.0.14(@types/react@18.3.3)(react@18.3.1) + '@react-email/render': + specifier: ^0.0.12 + version: 0.0.12 + '@react-email/tailwind': + specifier: ^0.0.14 + version: 0.0.14(react@18.3.1) + '@sentry/nextjs': + specifier: ^7.105.0 + version: 7.117.0(next@14.2.3(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.92.0(@swc/core@1.3.101)(esbuild@0.21.5)) + '@stackframe/stack': + specifier: workspace:* + version: link:../../packages/stack + '@stackframe/stack-shared': + specifier: workspace:* + version: link:../../packages/stack-shared + '@tanstack/react-table': + specifier: ^8.17.0 + version: 8.17.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/mdx': + specifier: ^2 + version: 2.0.13 + '@vercel/analytics': + specifier: ^1.2.2 + version: 1.3.1(next@14.2.3(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + bcrypt: + specifier: ^5.1.1 + version: 5.1.1 + bright: + specifier: ^0.8.4 + version: 0.8.5(react@18.3.1) + canvas-confetti: + specifier: ^1.9.2 + version: 1.9.3 + class-variance-authority: + specifier: ^0.7.0 + version: 0.7.0 + clsx: + specifier: ^2.0.0 + version: 2.1.1 + cmdk: + specifier: ^1.0.0 + version: 1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + date-fns: + specifier: ^3.6.0 + version: 3.6.0 + dotenv-cli: + specifier: ^7.3.0 + version: 7.4.1 + geist: + specifier: ^1 + version: 1.3.0(next@14.2.3(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + handlebars: + specifier: ^4.7.8 + version: 4.7.8 + highlight.js: + specifier: ^11.9.0 + version: 11.9.0 + input-otp: + specifier: ^1.2.4 + version: 1.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + jose: + specifier: ^5.2.2 + version: 5.4.0 + lodash: + specifier: ^4.17.21 + version: 4.17.21 + lucide-react: + specifier: ^0.378.0 + version: 0.378.0(react@18.3.1) + next: + specifier: ^14.1 + version: 14.2.3(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next-themes: + specifier: ^0.2.1 + version: 0.2.1(next@14.2.3(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + nodemailer: + specifier: ^6.9.10 + version: 6.9.13 + openid-client: + specifier: ^5.6.4 + version: 5.6.5 + pg: + specifier: ^8.11.3 + version: 8.12.0 + posthog-js: + specifier: ^1.138.1 + version: 1.139.2 + prettier: + specifier: ^3.2.5 + version: 3.3.2 + react: + specifier: ^18.2 + version: 18.3.1 + react-colorful: + specifier: ^5.6.1 + version: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-day-picker: + specifier: ^8.10.1 + version: 8.10.1(date-fns@3.6.0)(react@18.3.1) + react-dom: + specifier: ^18 + version: 18.3.1(react@18.3.1) + react-email: + specifier: 2.1.0 + version: 2.1.0(@babel/core@7.24.7)(@swc/helpers@0.5.11)(eslint@8.30.0) + react-hook-form: + specifier: ^7.51.4 + version: 7.52.0(react@18.3.1) + react-icons: + specifier: ^5.0.1 + version: 5.2.1(react@18.3.1) + react-resizable-panels: + specifier: ^2.0.19 + version: 2.0.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rehype-katex: + specifier: ^7 + version: 7.0.0 + remark-gfm: + specifier: ^4 + version: 4.0.0 + remark-heading-id: + specifier: ^1.0.1 + version: 1.0.1 + remark-math: + specifier: ^6 + version: 6.0.0 + server-only: + specifier: ^0.0.1 + version: 0.0.1 + sharp: + specifier: ^0.32.6 + version: 0.32.6 + tailwind-merge: + specifier: ^2.3.0 + version: 2.3.0 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@3.4.4) + yaml: + specifier: ^2.4.5 + version: 2.4.5 + yup: + specifier: ^1.4.0 + version: 1.4.0 + zod: + specifier: ^3.23.8 + version: 3.23.8 + zustand: + specifier: ^4.5.2 + version: 4.5.2(@types/react@18.3.3)(react@18.3.1) + devDependencies: + '@types/bcrypt': + specifier: ^5.0.2 + version: 5.0.2 + '@types/canvas-confetti': + specifier: ^1.6.4 + version: 1.6.4 + '@types/lodash': + specifier: ^4.17.4 + version: 4.17.5 + '@types/node': + specifier: ^20.8.10 + version: 20.14.2 + '@types/nodemailer': + specifier: ^6.4.14 + version: 6.4.15 + '@types/react': + specifier: ^18.2.66 + version: 18.3.3 + '@types/react-dom': + specifier: ^18 + version: 18.3.0 + autoprefixer: + specifier: ^10.4.17 + version: 10.4.19(postcss@8.4.38) + glob: + specifier: ^10.4.1 + version: 10.4.1 + postcss: + specifier: ^8.4.38 + version: 8.4.38 + prisma: + specifier: ^5.9.1 + version: 5.15.0 + rimraf: + specifier: ^5.0.5 + version: 5.0.7 + tailwindcss: + specifier: ^3.4.1 + version: 3.4.4 + tsx: + specifier: ^4.7.2 + version: 4.15.5 + apps/e2e: devDependencies: dotenv-cli: @@ -93,9 +512,9 @@ importers: docs: dependencies: - '@stackframe/stack-server': + '@stackframe/stack-backend': specifier: workspace:* - version: link:../packages/stack-server + version: link:../apps/backend fern-api: specifier: ^0.30.7 version: 0.30.7 @@ -369,380 +788,76 @@ importers: '@stackframe/stack-shared': specifier: workspace:* version: link:../stack-shared - color: - specifier: ^4.2.3 - version: 4.2.3 - cookie: - specifier: ^0.6.0 - version: 0.6.0 - js-cookie: - specifier: ^3.0.5 - version: 3.0.5 - lucide-react: - specifier: ^0.378.0 - version: 0.378.0(react@18.3.1) - oauth4webapi: - specifier: ^2.10.3 - version: 2.10.4 - react-hook-form: - specifier: ^7.51.4 - version: 7.52.0(react@18.3.1) - rimraf: - specifier: ^5.0.5 - version: 5.0.7 - server-only: - specifier: ^0.0.1 - version: 0.0.1 - styled-components: - specifier: ^6.1.8 - version: 6.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - yup: - specifier: ^1.4.0 - version: 1.4.0 - devDependencies: - '@types/color': - specifier: ^3.0.6 - version: 3.0.6 - '@types/cookie': - specifier: ^0.6.0 - version: 0.6.0 - '@types/js-cookie': - specifier: ^3.0.6 - version: 3.0.6 - '@types/react': - specifier: ^18.2.66 - version: 18.3.3 - esbuild: - specifier: ^0.20.2 - version: 0.20.2 - next: - specifier: ^14.1.0 - version: 14.2.3(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react: - specifier: ^18.2.0 - version: 18.3.1 - tsup: - specifier: ^8.0.2 - version: 8.1.0(@swc/core@1.3.101)(postcss@8.4.38)(typescript@5.3.3) - - packages/stack-sc: - devDependencies: - '@types/react': - specifier: ^18.2.66 - version: 18.3.3 - next: - specifier: ^14.1.0 - version: 14.2.3(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react: - specifier: ^18.2.0 - version: 18.3.1 - rimraf: - specifier: ^5.0.5 - version: 5.0.7 - - packages/stack-server: - dependencies: - '@hookform/resolvers': - specifier: ^3.3.4 - version: 3.6.0(react-hook-form@7.52.0(react@18.3.1)) - '@mdx-js/loader': - specifier: ^3 - version: 3.0.1(webpack@5.92.0(@swc/core@1.3.101)(esbuild@0.21.5)) - '@mdx-js/react': - specifier: ^3.0.0 - version: 3.0.1(@types/react@18.3.3)(react@18.3.1) - '@next/bundle-analyzer': - specifier: ^14.0.3 - version: 14.2.4 - '@next/mdx': - specifier: ^14 - version: 14.2.4(@mdx-js/loader@3.0.1(webpack@5.92.0(@swc/core@1.3.101)(esbuild@0.21.5)))(@mdx-js/react@3.0.1(@types/react@18.3.3)(react@18.3.1)) - '@node-oauth/oauth2-server': - specifier: ^5.1.0 - version: 5.1.0 - '@prisma/client': - specifier: ^5.9.1 - version: 5.15.0(prisma@5.15.0) - '@radix-ui/react-accordion': - specifier: ^1.1.2 - version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-alert-dialog': - specifier: ^1.0.5 - version: 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-aspect-ratio': - specifier: ^1.0.3 - version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-avatar': - specifier: ^1.0.4 - version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-checkbox': - specifier: ^1.0.4 - version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-collapsible': - specifier: ^1.0.3 - version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-context-menu': - specifier: ^2.1.5 - version: 2.1.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-dialog': - specifier: ^1.0.5 - version: 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-dropdown-menu': - specifier: ^2.0.6 - version: 2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-hover-card': - specifier: ^1.0.7 - version: 1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-icons': - specifier: ^1.3.0 - version: 1.3.0(react@18.3.1) - '@radix-ui/react-label': - specifier: ^2.0.2 - version: 2.0.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-menubar': - specifier: ^1.0.4 - version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-navigation-menu': - specifier: ^1.1.4 - version: 1.1.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-popover': - specifier: ^1.0.7 - version: 1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-progress': - specifier: ^1.0.3 - version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-radio-group': - specifier: ^1.1.3 - version: 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-scroll-area': - specifier: ^1.0.5 - version: 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-select': - specifier: ^2.0.0 - version: 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-separator': - specifier: ^1.0.3 - version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slider': - specifier: ^1.1.2 - version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': - specifier: ^1.0.2 - version: 1.0.2(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-switch': - specifier: ^1.0.3 - version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-tabs': - specifier: ^1.0.4 - version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-toast': - specifier: ^1.1.5 - version: 1.1.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-toggle': - specifier: ^1.0.3 - version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-toggle-group': - specifier: ^1.0.4 - version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-tooltip': - specifier: ^1.0.7 - version: 1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-email/components': - specifier: ^0.0.14 - version: 0.0.14(@types/react@18.3.3)(react@18.3.1) - '@react-email/render': - specifier: ^0.0.12 - version: 0.0.12 - '@react-email/tailwind': - specifier: ^0.0.14 - version: 0.0.14(react@18.3.1) - '@sentry/nextjs': - specifier: ^7.105.0 - version: 7.117.0(next@14.2.3(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.92.0(@swc/core@1.3.101)(esbuild@0.21.5)) - '@stackframe/stack': - specifier: workspace:* - version: link:../stack - '@stackframe/stack-shared': - specifier: workspace:* - version: link:../stack-shared - '@tanstack/react-table': - specifier: ^8.17.0 - version: 8.17.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@types/mdx': - specifier: ^2 - version: 2.0.13 - '@vercel/analytics': - specifier: ^1.2.2 - version: 1.3.1(next@14.2.3(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) - bcrypt: - specifier: ^5.1.1 - version: 5.1.1 - bright: - specifier: ^0.8.4 - version: 0.8.5(react@18.3.1) - canvas-confetti: - specifier: ^1.9.2 - version: 1.9.3 - class-variance-authority: - specifier: ^0.7.0 - version: 0.7.0 - clsx: - specifier: ^2.0.0 - version: 2.1.1 - cmdk: - specifier: ^1.0.0 - version: 1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - date-fns: - specifier: ^3.6.0 - version: 3.6.0 - dotenv-cli: - specifier: ^7.3.0 - version: 7.4.1 - geist: - specifier: ^1 - version: 1.3.0(next@14.2.3(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) - handlebars: - specifier: ^4.7.8 - version: 4.7.8 - highlight.js: - specifier: ^11.9.0 - version: 11.9.0 - input-otp: - specifier: ^1.2.4 - version: 1.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - jose: - specifier: ^5.2.2 - version: 5.4.0 - lodash: - specifier: ^4.17.21 - version: 4.17.21 + color: + specifier: ^4.2.3 + version: 4.2.3 + cookie: + specifier: ^0.6.0 + version: 0.6.0 + js-cookie: + specifier: ^3.0.5 + version: 3.0.5 lucide-react: specifier: ^0.378.0 version: 0.378.0(react@18.3.1) - next: - specifier: ^14.1 - version: 14.2.3(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - next-themes: - specifier: ^0.2.1 - version: 0.2.1(next@14.2.3(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - nodemailer: - specifier: ^6.9.10 - version: 6.9.13 - openid-client: - specifier: ^5.6.4 - version: 5.6.5 - pg: - specifier: ^8.11.3 - version: 8.12.0 - posthog-js: - specifier: ^1.138.1 - version: 1.139.2 - prettier: - specifier: ^3.2.5 - version: 3.3.2 - react: - specifier: ^18.2 - version: 18.3.1 - react-colorful: - specifier: ^5.6.1 - version: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-day-picker: - specifier: ^8.10.1 - version: 8.10.1(date-fns@3.6.0)(react@18.3.1) - react-dom: - specifier: ^18 - version: 18.3.1(react@18.3.1) - react-email: - specifier: 2.1.0 - version: 2.1.0(@babel/core@7.24.7)(@swc/helpers@0.5.11)(eslint@8.30.0) + oauth4webapi: + specifier: ^2.10.3 + version: 2.10.4 react-hook-form: specifier: ^7.51.4 version: 7.52.0(react@18.3.1) - react-icons: - specifier: ^5.0.1 - version: 5.2.1(react@18.3.1) - react-resizable-panels: - specifier: ^2.0.19 - version: 2.0.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - rehype-katex: - specifier: ^7 - version: 7.0.0 - remark-gfm: - specifier: ^4 - version: 4.0.0 - remark-heading-id: - specifier: ^1.0.1 - version: 1.0.1 - remark-math: - specifier: ^6 - version: 6.0.0 + rimraf: + specifier: ^5.0.5 + version: 5.0.7 server-only: specifier: ^0.0.1 version: 0.0.1 - sharp: - specifier: ^0.32.6 - version: 0.32.6 - tailwind-merge: - specifier: ^2.3.0 - version: 2.3.0 - tailwindcss-animate: - specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.4) - yaml: - specifier: ^2.4.5 - version: 2.4.5 + styled-components: + specifier: ^6.1.8 + version: 6.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) yup: specifier: ^1.4.0 version: 1.4.0 - zod: - specifier: ^3.23.8 - version: 3.23.8 - zustand: - specifier: ^4.5.2 - version: 4.5.2(@types/react@18.3.3)(react@18.3.1) devDependencies: - '@types/bcrypt': - specifier: ^5.0.2 - version: 5.0.2 - '@types/canvas-confetti': - specifier: ^1.6.4 - version: 1.6.4 - '@types/lodash': - specifier: ^4.17.4 - version: 4.17.5 - '@types/node': - specifier: ^20.8.10 - version: 20.14.2 - '@types/nodemailer': - specifier: ^6.4.14 - version: 6.4.15 + '@types/color': + specifier: ^3.0.6 + version: 3.0.6 + '@types/cookie': + specifier: ^0.6.0 + version: 0.6.0 + '@types/js-cookie': + specifier: ^3.0.6 + version: 3.0.6 '@types/react': specifier: ^18.2.66 version: 18.3.3 - '@types/react-dom': - specifier: ^18 - version: 18.3.0 - autoprefixer: - specifier: ^10.4.17 - version: 10.4.19(postcss@8.4.38) - glob: - specifier: ^10.4.1 - version: 10.4.1 - postcss: - specifier: ^8.4.38 - version: 8.4.38 - prisma: - specifier: ^5.9.1 - version: 5.15.0 + esbuild: + specifier: ^0.20.2 + version: 0.20.2 + next: + specifier: ^14.1.0 + version: 14.2.3(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: + specifier: ^18.2.0 + version: 18.3.1 + tsup: + specifier: ^8.0.2 + version: 8.1.0(@swc/core@1.3.101)(postcss@8.4.38)(typescript@5.3.3) + + packages/stack-sc: + devDependencies: + '@types/react': + specifier: ^18.2.66 + version: 18.3.3 + next: + specifier: ^14.1.0 + version: 14.2.3(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: + specifier: ^18.2.0 + version: 18.3.1 rimraf: specifier: ^5.0.5 version: 5.0.7 - tailwindcss: - specifier: ^3.4.1 - version: 3.4.4 - tsx: - specifier: ^4.7.2 - version: 4.15.5 packages/stack-shared: dependencies: @@ -9903,7 +10018,7 @@ snapshots: rollup: 2.78.0 stacktrace-parser: 0.1.10 optionalDependencies: - webpack: 5.92.0(@swc/core@1.3.101(@swc/helpers@0.5.11))(esbuild@0.19.11) + webpack: 5.92.0(@swc/core@1.3.101)(esbuild@0.21.5) transitivePeerDependencies: - encoding - supports-color @@ -13836,7 +13951,7 @@ snapshots: dependencies: nanoid: 3.3.7 picocolors: 1.0.1 - source-map-js: 1.0.2 + source-map-js: 1.2.0 postcss@8.4.38: dependencies: @@ -14787,7 +14902,7 @@ snapshots: dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 - chokidar: 3.5.3 + chokidar: 3.6.0 didyoumean: 1.2.2 dlv: 1.1.3 fast-glob: 3.3.2 @@ -14891,6 +15006,19 @@ snapshots: '@swc/core': 1.3.101(@swc/helpers@0.5.11) esbuild: 0.19.11 + terser-webpack-plugin@5.3.10(@swc/core@1.3.101)(esbuild@0.21.5)(webpack@5.92.0(@swc/core@1.3.101)(esbuild@0.21.5)): + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + jest-worker: 27.5.1 + schema-utils: 3.3.0 + serialize-javascript: 6.0.2 + terser: 5.31.1 + webpack: 5.92.0(@swc/core@1.3.101)(esbuild@0.21.5) + optionalDependencies: + '@swc/core': 1.3.101(@swc/helpers@0.5.11) + esbuild: 0.21.5 + optional: true + terser@5.31.1: dependencies: '@jridgewell/source-map': 0.3.6 @@ -15388,6 +15516,38 @@ snapshots: - esbuild - uglify-js + webpack@5.92.0(@swc/core@1.3.101)(esbuild@0.21.5): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.5 + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/wasm-edit': 1.12.1 + '@webassemblyjs/wasm-parser': 1.12.1 + acorn: 8.12.0 + acorn-import-attributes: 1.9.5(acorn@8.12.0) + browserslist: 4.23.1 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.17.0 + es-module-lexer: 1.5.3 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.3.0 + tapable: 2.2.1 + terser-webpack-plugin: 5.3.10(@swc/core@1.3.101)(esbuild@0.21.5)(webpack@5.92.0(@swc/core@1.3.101)(esbuild@0.21.5)) + watchpack: 2.4.1 + webpack-sources: 3.2.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + optional: true + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 diff --git a/turbo.json b/turbo.json index 7569199b3..5057dc25b 100644 --- a/turbo.json +++ b/turbo.json @@ -39,6 +39,9 @@ "test": { "cache": false }, + "test:watch": { + "cache": false + }, "generate-docs": { "cache": false },