diff --git a/apps/builder/app/builder/features/topbar/domains.tsx b/apps/builder/app/builder/features/topbar/domains.tsx index f87f23abdd68..d439466698dd 100644 --- a/apps/builder/app/builder/features/topbar/domains.tsx +++ b/apps/builder/app/builder/features/topbar/domains.tsx @@ -19,7 +19,6 @@ import { ExternalLinkIcon, CopyIcon, } from "@webstudio-is/icons"; -import type { DomainStatus } from "@webstudio-is/prisma-client"; import { CollapsibleDomainSection } from "./collapsible-domain-section"; import { startTransition, @@ -39,6 +38,7 @@ import DomainCheckbox from "./domain-checkbox"; import { CopyToClipboard } from "~/builder/shared/copy-to-clipboard"; export type Domain = Project["domainsVirtual"][number]; +type DomainStatus = Project["domainsVirtual"][number]["status"]; const InputEllipsis = styled(InputField, { "&>input": { diff --git a/apps/builder/app/dashboard/dashboard.stories.tsx b/apps/builder/app/dashboard/dashboard.stories.tsx index 0235e9b535b5..492f517371c4 100644 --- a/apps/builder/app/dashboard/dashboard.stories.tsx +++ b/apps/builder/app/dashboard/dashboard.stories.tsx @@ -16,6 +16,7 @@ const user = { image: null, username: "Taylor", teamId: null, + provider: "github", }; const createRouter = (element: JSX.Element) => diff --git a/apps/builder/app/routes/rest.build.$buildId.tsx b/apps/builder/app/routes/rest.build.$buildId.tsx index 89fd71f4660f..afbb76a4281c 100644 --- a/apps/builder/app/routes/rest.build.$buildId.tsx +++ b/apps/builder/app/routes/rest.build.$buildId.tsx @@ -54,7 +54,7 @@ export const loader = async ({ const user = project === null || project.userId === null ? undefined - : await getUserById(project.userId); + : await getUserById(context, project.userId); return { ...pagesCanvasData, diff --git a/apps/builder/app/routes/rest.current.products.ts b/apps/builder/app/routes/rest.current.products.ts deleted file mode 100644 index 02dc07a36842..000000000000 --- a/apps/builder/app/routes/rest.current.products.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { json, type LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { findAuthenticatedUser } from "~/services/auth.server"; -import { loginPath } from "~/shared/router-utils"; -import { prisma } from "@webstudio-is/prisma-client"; -import { preventCrossOriginCookie } from "~/services/no-cross-origin-cookie"; -import { redirect } from "~/services/no-store-redirect"; -import { checkCsrf } from "~/services/csrf-session.server"; -import { allowedDestinations } from "~/services/destinations.server"; - -/** - * Created for ebugging purposes, to check that payments and products are working - * Showing current user's checkouts with products. - */ -export const loader = async ({ request }: LoaderFunctionArgs) => { - preventCrossOriginCookie(request); - allowedDestinations(request, ["document", "empty"]); - await checkCsrf(request); - - const user = await findAuthenticatedUser(request); - - if (user === null) { - const url = new URL(request.url); - throw redirect( - loginPath({ - returnTo: `${url.pathname}?${url.searchParams.toString()}`, - }) - ); - } - - const userCheckoutsWithProducts = await prisma.user.findUnique({ - where: { id: user.id }, - select: { - id: true, - - username: true, - products: { - select: { - product: { - select: { - id: true, - name: true, - description: true, - features: true, - meta: true, - images: true, - }, - }, - }, - }, - checkout: { - select: { - eventId: true, - createdAt: true, - - product: { - select: { - id: true, - name: true, - description: true, - features: true, - meta: true, - images: true, - }, - }, - }, - }, - }, - }); - - return json(userCheckoutsWithProducts); -}; diff --git a/apps/builder/app/services/auth.server.ts b/apps/builder/app/services/auth.server.ts index c7f4139e39c6..93fe6cbea773 100644 --- a/apps/builder/app/services/auth.server.ts +++ b/apps/builder/app/services/auth.server.ts @@ -11,6 +11,7 @@ import env from "~/env/env.server"; import { builderAuthenticator } from "./builder-auth.server"; import { staticEnv } from "~/env/env.static.server"; import type { SessionData } from "./auth.server.utils"; +import { createContext } from "~/shared/context.server"; const transformRefToAlias = (input: string) => { const rawAlias = input.endsWith(".staging") ? input.slice(0, -8) : input; @@ -32,11 +33,15 @@ export const callbackOrigin = const strategyCallback = async ({ profile, + request, }: { profile: GitHubProfile | GoogleProfile; + request: Request; }) => { + const context = await createContext(request); + try { - const user = await db.user.createOrLoginWithOAuth(profile); + const user = await db.user.createOrLoginWithOAuth(context, profile); return { userId: user.id, createdAt: Date.now() }; } catch (error) { if (error instanceof Error) { @@ -83,7 +88,7 @@ if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) { if (env.DEV_LOGIN === "true") { authenticator.use( - new FormStrategy(async ({ form }) => { + new FormStrategy(async ({ form, request }) => { const secretValue = form.get("secret"); if (secretValue == null) { @@ -96,7 +101,9 @@ if (env.DEV_LOGIN === "true") { if (secret === env.AUTH_SECRET?.slice(0, 4)) { try { - const user = await db.user.createOrLoginWithDev(email); + const context = await createContext(request); + + const user = await db.user.createOrLoginWithDev(context, email); return { userId: user.id, createdAt: Date.now(), @@ -128,9 +135,10 @@ export const findAuthenticatedUser = async (request: Request) => { if (user == null) { return null; } + const context = await createContext(request); try { - return await getUserById(user.userId); + return await getUserById(context, user.userId); } catch (error) { return null; } diff --git a/apps/builder/app/shared/$resources/sitemap.xml.server.ts b/apps/builder/app/shared/$resources/sitemap.xml.server.ts index 963d7db7028a..2ca651dabd35 100644 --- a/apps/builder/app/shared/$resources/sitemap.xml.server.ts +++ b/apps/builder/app/shared/$resources/sitemap.xml.server.ts @@ -1,9 +1,9 @@ import { json } from "@remix-run/server-runtime"; -import { prisma } from "@webstudio-is/prisma-client"; import { parsePages } from "@webstudio-is/project-build/index.server"; import { getStaticSiteMapXml } from "@webstudio-is/sdk"; import { parseBuilderUrl } from "@webstudio-is/http-client"; import { isBuilder } from "../router-utils"; +import { createContext } from "../context.server"; /** * This should be a route in SvelteKit, as it can be fetched server-side without an actual HTTP request. @@ -22,25 +22,24 @@ export const loader = async ({ request }: { request: Request }) => { throw new Error("projectId is required"); } - // get pages from the database - const build = await prisma.build.findFirst({ - where: { - projectId, - deployment: null, - }, - select: { - pages: true, - updatedAt: true, - }, - }); - - if (build === null) { - throw json({ message: "Build not found" }, { status: 404 }); + const context = await createContext(request); + + const buildResult = await context.postgrest.client + .from("Build") + .select("pages, updatedAt") + .eq("projectId", projectId) + .is("deployment", null) + .single(); + + if (buildResult.error) { + throw json({ message: buildResult.error.message }, { status: 404 }); } + const build = buildResult.data; + const pages = parsePages(build.pages); - const siteMap = getStaticSiteMapXml(pages, build.updatedAt.toISOString()); + const siteMap = getStaticSiteMapXml(pages, build.updatedAt); return json(siteMap); }; diff --git a/apps/builder/app/shared/context.server.ts b/apps/builder/app/shared/context.server.ts index 5c5dfb508693..341b6795a046 100644 --- a/apps/builder/app/shared/context.server.ts +++ b/apps/builder/app/shared/context.server.ts @@ -7,7 +7,6 @@ import { entryApi } from "./entri/entri-api.server"; import { getUserPlanFeatures } from "./db/user-plan-features.server"; import { staticEnv } from "~/env/env.static.server"; import { createClient } from "@webstudio-is/postrest/index.server"; -import { prisma } from "@webstudio-is/prisma-client"; import { builderAuthenticator } from "~/services/builder-auth.server"; import { readLoginSessionBloomFilter } from "~/services/session.server"; import type { BloomFilter } from "~/services/bloom-filter.server"; @@ -40,29 +39,24 @@ export const extractAuthFromRequest = async (request: Request) => { }; }; -const createTokenAuthorizationContext = async (authToken: string) => { - const projectOwnerIdByToken = await prisma.authorizationToken.findUnique({ - where: { - token: authToken, - }, - select: { - project: { - select: { - id: true, - userId: true, - }, - }, - }, - }); +const createTokenAuthorizationContext = async ( + authToken: string, + postgrest: AppContext["postgrest"] +) => { + const projectOwnerIdByToken = await postgrest.client + .from("AuthorizationToken") + .select("project:Project(id, userId)") + .eq("token", authToken) + .single(); - if (projectOwnerIdByToken === null) { + if (projectOwnerIdByToken.error) { throw new Error(`Project owner can't be found for token ${authToken}`); } - const ownerId = projectOwnerIdByToken.project.userId; + const ownerId = projectOwnerIdByToken.data.project?.userId ?? null; if (ownerId === null) { throw new Error( - `Project ${projectOwnerIdByToken.project.id} has null userId` + `Project ${projectOwnerIdByToken.data.project?.id} has null userId` ); } @@ -74,7 +68,8 @@ const createTokenAuthorizationContext = async (authToken: string) => { }; const createAuthorizationContext = async ( - request: Request + request: Request, + postgrest: AppContext["postgrest"] ): Promise => { const { authToken, isServiceCall, sessionData } = await extractAuthFromRequest(request); @@ -87,7 +82,7 @@ const createAuthorizationContext = async ( } if (authToken != null) { - return await createTokenAuthorizationContext(authToken); + return await createTokenAuthorizationContext(authToken, postgrest); } if (sessionData?.userId != null) { @@ -155,7 +150,8 @@ const createEntriContext = () => { }; const createUserPlanContext = async ( - authorization: AppContext["authorization"] + authorization: AppContext["authorization"], + postgrest: AppContext["postgrest"] ) => { const ownerId = authorization.type === "token" @@ -164,7 +160,9 @@ const createUserPlanContext = async ( ? authorization.userId : undefined; - const planFeatures = ownerId ? await getUserPlanFeatures(ownerId) : undefined; + const planFeatures = ownerId + ? await getUserPlanFeatures(ownerId, postgrest) + : undefined; return planFeatures; }; @@ -193,18 +191,27 @@ export const createPostrestContext = () => { * argument buildEnv==="prod" only if we are loading project with production build */ export const createContext = async (request: Request): Promise => { - const authorization = await createAuthorizationContext(request); + const postgrest = createPostrestContext(); + const authorization = await createAuthorizationContext(request, postgrest); const domain = createDomainContext(); const deployment = createDeploymentContext(getRequestOrigin(request.url)); const entri = createEntriContext(); - const userPlanFeatures = await createUserPlanContext(authorization); + const userPlanFeatures = await createUserPlanContext( + authorization, + postgrest + ); const trpcCache = createTrpcCache(); - const postgrest = createPostrestContext(); const createTokenContext = async (authToken: string) => { - const authorization = await createTokenAuthorizationContext(authToken); - const userPlanFeatures = await createUserPlanContext(authorization); + const authorization = await createTokenAuthorizationContext( + authToken, + postgrest + ); + const userPlanFeatures = await createUserPlanContext( + authorization, + postgrest + ); return { authorization, diff --git a/apps/builder/app/shared/db/canvas.server.ts b/apps/builder/app/shared/db/canvas.server.ts index f98ede7183d7..8345c1563c69 100644 --- a/apps/builder/app/shared/db/canvas.server.ts +++ b/apps/builder/app/shared/db/canvas.server.ts @@ -3,7 +3,6 @@ import { loadBuildById } from "@webstudio-is/project-build/index.server"; import { loadAssetsByProject } from "@webstudio-is/asset-uploader/index.server"; import type { AppContext } from "@webstudio-is/trpc-interface/index.server"; import { findPageByIdOrPath, getStyleDeclKey } from "@webstudio-is/sdk"; -import type { Build } from "@webstudio-is/prisma-client"; import { db as projectDb } from "@webstudio-is/project/index.server"; const getPair = (item: Item): [string, Item] => [ @@ -12,7 +11,7 @@ const getPair = (item: Item): [string, Item] => [ ]; export const loadProductionCanvasData = async ( - buildId: Build["id"], + buildId: string, context: AppContext ): Promise => { const build = await loadBuildById(context, buildId); diff --git a/apps/builder/app/shared/db/user-plan-features.server.ts b/apps/builder/app/shared/db/user-plan-features.server.ts index e4bd8ee9c730..a671dc29f7c4 100644 --- a/apps/builder/app/shared/db/user-plan-features.server.ts +++ b/apps/builder/app/shared/db/user-plan-features.server.ts @@ -1,29 +1,38 @@ -import { prisma } from "@webstudio-is/prisma-client"; import type { AppContext } from "@webstudio-is/trpc-interface/index.server"; import env from "~/env/env.server"; export type UserPlanFeatures = NonNullable; export const getUserPlanFeatures = async ( - userId: string + userId: string, + postgrest: AppContext["postgrest"] ): Promise => { - const userProducts = await prisma.userProduct.findMany({ - where: { userId }, - select: { - customerId: true, - subscriptionId: true, - product: { - select: { - id: true, - name: true, - description: true, - features: true, - meta: true, - images: true, - }, - }, - }, - }); + const userProductsResult = await postgrest.client + .from("UserProduct") + .select("customerId, subscriptionId, productId") + .eq("userId", userId); + + if (userProductsResult.error) { + console.error(userProductsResult.error); + throw new Error("Failed to fetch user products"); + } + + const userProducts = userProductsResult.data; + + const productsResult = await postgrest.client + .from("Product") + .select("name") + .in( + "id", + userProducts.map(({ productId }) => productId) + ); + + if (productsResult.error) { + console.error(productsResult.error); + throw new Error("Failed to fetch products"); + } + + const products = productsResult.data; // This is fast and dirty implementation // @todo: implement this using products meta, custom table with aggregated transaction info @@ -39,7 +48,7 @@ export const getUserPlanFeatures = async ( maxDomainsAllowedPerUser: Number.MAX_SAFE_INTEGER, hasSubscription, hasProPlan: true, - planName: userProducts[0].product.name, + planName: products[0].name, }; } diff --git a/apps/builder/app/shared/db/user.server.ts b/apps/builder/app/shared/db/user.server.ts index 4450108f7009..fb74d02d07f8 100644 --- a/apps/builder/app/shared/db/user.server.ts +++ b/apps/builder/app/shared/db/user.server.ts @@ -1,72 +1,69 @@ +import type { Database } from "@webstudio-is/postrest/index.server"; +import type { AppContext } from "@webstudio-is/trpc-interface/index.server"; import type { GitHubProfile } from "remix-auth-github"; import type { GoogleProfile } from "remix-auth-google"; -import { prisma } from "@webstudio-is/prisma-client"; -import { z } from "zod"; -const User = z.object({ - id: z.string(), - email: z.string().nullable(), - image: z.string().nullable(), - username: z.string().nullable(), - createdAt: z.date().transform((date) => date.toISOString()), - teamId: z.string().nullable(), -}); +export type User = Database["public"]["Tables"]["User"]["Row"]; -export type User = z.infer; +export const getUserById = async (context: AppContext, id: User["id"]) => { + const dbUser = await context.postgrest.client + .from("User") + .select() + .eq("id", id) + .single(); -export const getUserById = async (id: User["id"]) => { - const dbUser = await prisma.user.findUnique({ - where: { id }, - }); - return User.parse(dbUser); -}; + if (dbUser.error) { + console.error(dbUser.error); + throw new Error("User not found"); + } -const genericCreateAccount = async (userData: { - email: string; - username: string; - image: string; - provider: string; -}): Promise => { - const dbUser = await prisma.user.findUnique({ - where: { - email: userData.email, - }, - }); + return dbUser.data; +}; - if (dbUser) { - const user = User.parse(dbUser); +const genericCreateAccount = async ( + context: AppContext, + userData: { + email: string; + username: string; + image: string; + provider: string; + } +): Promise => { + const dbUser = await context.postgrest.client + .from("User") + .select() + .eq("email", userData.email) + .single(); - if (user.teamId) { - return user; - } - await prisma.team.create({ - data: { - users: { - connect: { - id: user.id, - }, - }, - }, - }); + if (dbUser.error == null) { + return dbUser.data; + } - return user; + // https://github.com/PostgREST/postgrest/blob/bfbd033c6e9f38cfbc8b1cfe19ee009a9379e3dd/docs/references/errors.rst#L234 + if (dbUser.error.code !== "PGRST116") { + console.error(dbUser.error); + throw new Error("User not found"); } - const newTeam = await prisma.team.create({ - data: { - users: { - create: userData, - }, - }, - include: { - users: true, - }, - }); + const newUser = await context.postgrest.client + .from("User") + .insert({ + id: crypto.randomUUID(), + ...userData, + }) + .select() + .single(); + + if (newUser.error) { + console.error(newUser.error); + throw new Error("Failed to create user"); + } - return User.parse(newTeam.users[0]); + return newUser.data; }; export const createOrLoginWithOAuth = async ( + context: AppContext, profile: GoogleProfile | GitHubProfile ): Promise => { const userData = { @@ -75,11 +72,14 @@ export const createOrLoginWithOAuth = async ( image: (profile.photos ?? [])[0]?.value, provider: profile.provider, }; - const newUser = await genericCreateAccount(userData); + const newUser = await genericCreateAccount(context, userData); return newUser; }; -export const createOrLoginWithDev = async (email: string): Promise => { +export const createOrLoginWithDev = async ( + context: AppContext, + email: string +): Promise => { const userData = { email, username: "admin", @@ -87,6 +87,6 @@ export const createOrLoginWithDev = async (email: string): Promise => { provider: "dev", }; - const newUser = await genericCreateAccount(userData); + const newUser = await genericCreateAccount(context, userData); return newUser; }; diff --git a/apps/builder/package.json b/apps/builder/package.json index 5f7039e97a0b..dbd40fa39a95 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -65,7 +65,6 @@ "@webstudio-is/icons": "workspace:*", "@webstudio-is/image": "workspace:*", "@webstudio-is/postrest": "workspace:*", - "@webstudio-is/prisma-client": "workspace:*", "@webstudio-is/project": "workspace:*", "@webstudio-is/project-build": "workspace:*", "@webstudio-is/react-sdk": "workspace:*", diff --git a/apps/builder/vite.config.ts b/apps/builder/vite.config.ts index 846dca4679e4..54f1f98f3f21 100644 --- a/apps/builder/vite.config.ts +++ b/apps/builder/vite.config.ts @@ -65,9 +65,6 @@ export default defineConfig(({ mode }) => { define: { "process.env.NODE_ENV": JSON.stringify(mode), }, - ssr: { - external: ["@webstudio-is/prisma-client"], - }, server: { // Service-to-service OAuth token call requires a specified host for the wstd.dev domain host: "wstd.dev", diff --git a/package.json b/package.json index 42169ff8005d..a78736aaa76e 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "build": "pnpm -r --filter='!./fixtures/*' build", "dts": "pnpm -r dts", - "dev": "pnpm --filter='@webstudio-is/builder' --filter='@webstudio-is/prisma-client' --parallel --recursive dev", + "dev": "pnpm --filter='@webstudio-is/builder' dev", "lint": "eslint \"**/*.{ts,tsx}\" --max-warnings 0", "checks": "pnpm -r test && pnpm -r typecheck && pnpm lint", "format": "prettier --write \"**/*.{ts,tsx,md}\"", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e85be35236f7..7d6e4a5da5c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -268,9 +268,6 @@ importers: '@webstudio-is/postrest': specifier: workspace:* version: link:../../packages/postgrest - '@webstudio-is/prisma-client': - specifier: workspace:* - version: link:../../packages/prisma-client '@webstudio-is/project': specifier: workspace:* version: link:../../packages/project