diff --git a/apps/builder/app/routes/_ui.(builder).tsx b/apps/builder/app/routes/_ui.(builder).tsx index f8882463ba80..454fe64cde9f 100644 --- a/apps/builder/app/routes/_ui.(builder).tsx +++ b/apps/builder/app/routes/_ui.(builder).tsx @@ -134,7 +134,7 @@ export const loader = async (loaderArgs: LoaderFunctionArgs) => { projectId: project.id, // At this point we already knew that if project loaded we have at least "view" permit // having that getProjectPermit is heavy operation we can skip check "view" permit - permits: ["own", "admin", "build"] as const, + permits: ["own", "admin", "build", "edit"] as const, }, context )) ?? "view"; diff --git a/apps/builder/app/shared/share-project/share-project.tsx b/apps/builder/app/shared/share-project/share-project.tsx index 4dc93fdef7f8..18bcf19ff788 100644 --- a/apps/builder/app/shared/share-project/share-project.tsx +++ b/apps/builder/app/shared/share-project/share-project.tsx @@ -32,6 +32,8 @@ import { import { Fragment, useState, type ComponentProps, type ReactNode } from "react"; import { useIds } from "../form-utils"; import { CopyToClipboard } from "~/builder/shared/copy-to-clipboard"; +import { isFeatureEnabled } from "@webstudio-is/feature-flags"; +import type { BuilderMode } from "../nano-states"; const Item = (props: ComponentProps) => ( { + {isFeatureEnabled("contentEditableMode") && ( + + Recipients can edit content only, such as text, images, and + predefined components. + {hasProPlan !== true && ( + <> +
+
+ Upgrade to a Pro account to share with Content Edit + permissions. +

+ + Upgrade + + + )} +
+ } + /> + )} + void; onDelete: () => void; - builderUrl: (props: { - authToken: string; - mode: "preview" | "design"; - }) => string; + builderUrl: (props: { authToken: string; mode: BuilderMode }) => string; hasProPlan: boolean; }; +const relationToMode: Record = { + viewers: "preview", + editors: "content", + builders: "design", + administrators: "design", +}; + const SharedLinkItem = ({ value, onChange, @@ -323,7 +362,7 @@ const SharedLinkItem = ({ diff --git a/packages/authorization-token/package.json b/packages/authorization-token/package.json index 1b9ff7535aa7..83e9e41cad4f 100644 --- a/packages/authorization-token/package.json +++ b/packages/authorization-token/package.json @@ -15,7 +15,8 @@ }, "devDependencies": { "@webstudio-is/tsconfig": "workspace:*", - "typescript": "5.6.3" + "typescript": "5.6.3", + "type-fest": "^4.26.1" }, "exports": { ".": { diff --git a/packages/authorization-token/src/trpc/authorization-tokens-router.ts b/packages/authorization-token/src/trpc/authorization-tokens-router.ts index 7f6a24cf51a4..8e8f75805c51 100644 --- a/packages/authorization-token/src/trpc/authorization-tokens-router.ts +++ b/packages/authorization-token/src/trpc/authorization-tokens-router.ts @@ -1,6 +1,11 @@ import { z } from "zod"; import { router, procedure } from "@webstudio-is/trpc-interface/index.server"; import { db } from "../db"; +import type { IsEqual } from "type-fest"; +import type { Database } from "@webstudio-is/postrest/index.server"; + +type Relation = + Database["public"]["Tables"]["AuthorizationToken"]["Row"]["relation"]; const TokenProjectRelation = z.enum([ "viewers", @@ -9,6 +14,10 @@ const TokenProjectRelation = z.enum([ "administrators", ]); +// Check DB types are compatible with zod types +type TokenRelation = z.infer; +true satisfies IsEqual; + export const authorizationTokenRouter = router({ findMany: procedure .input( diff --git a/packages/trpc-interface/src/authorize/project.server.ts b/packages/trpc-interface/src/authorize/project.server.ts index 5fcc4be70e02..30e5b5e7ed39 100644 --- a/packages/trpc-interface/src/authorize/project.server.ts +++ b/packages/trpc-interface/src/authorize/project.server.ts @@ -1,8 +1,14 @@ import type { AppContext } from "../context/context.server"; +import type { Database } from "@webstudio-is/postrest/index.server"; import memoize from "memoize"; +type Relation = + Database["public"]["Tables"]["AuthorizationToken"]["Row"]["relation"]; + export type AuthPermit = "view" | "edit" | "build" | "admin" | "own"; +type TokenAuthPermit = Exclude; + type CheckInput = { namespace: "Project"; id: string; @@ -36,28 +42,33 @@ const check = async ( return { allowed: row.data !== null }; } - const permitToRelationRewrite = { + if (input.permit === "own") { + return { allowed: false }; + } + + if (subjectSet.namespace !== "Token") { + return { allowed: false }; + } + + const permitToRelationRewrite: Record = { view: ["viewers", "editors", "builders", "administrators"], edit: ["editors", "builders", "administrators"], build: ["builders", "administrators"], admin: ["administrators"], - } as const; + }; - if (subjectSet.namespace === "Token" && input.permit !== "own") { - const row = await postgrestClient - .from("AuthorizationToken") - .select("token") - .eq("token", subjectSet.id) - .in("relation", [...permitToRelationRewrite[input.permit]]) - .maybeSingle(); - if (row.error) { - throw row.error; - } + const row = await postgrestClient + .from("AuthorizationToken") + .select("token") + .eq("token", subjectSet.id) + .in("relation", [...permitToRelationRewrite[input.permit]]) + .maybeSingle(); - return { allowed: row.data !== null }; + if (row.error) { + throw row.error; } - return { allowed: false }; + return { allowed: row.data !== null }; }; // doesn't work in cloudflare workers @@ -196,13 +207,13 @@ export const hasProjectPermit = async ( * @todo think about caching to authorizeTrpc.check.query * batching check queries would help too https://github.com/ory/keto/issues/812 */ -export const getProjectPermit = async ( +export const getProjectPermit = async ( props: { projectId: string; - permits: readonly T[]; + permits: readonly AuthPermit[]; }, context: AppContext -): Promise => { +): Promise => { const permitToCheck = props.permits; const permits = await Promise.allSettled( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d90efcd95333..4075be9c950b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1014,6 +1014,9 @@ importers: '@webstudio-is/tsconfig': specifier: workspace:* version: link:../tsconfig + type-fest: + specifier: ^4.26.1 + version: 4.26.1 typescript: specifier: 5.6.3 version: 5.6.3