Skip to content

Commit

Permalink
Add editor mode
Browse files Browse the repository at this point in the history
  • Loading branch information
istarkov committed Nov 14, 2024
1 parent 36ba02d commit 0c44281
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 24 deletions.
2 changes: 1 addition & 1 deletion apps/builder/app/routes/_ui.(builder).tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
49 changes: 44 additions & 5 deletions apps/builder/app/shared/share-project/share-project.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof Flex>) => (
<Flex
Expand Down Expand Up @@ -225,6 +227,39 @@ const Menu = ({ name, hasProPlan, value, onChange, onDelete }: MenuProps) => {
</Grid>
</Grid>

{isFeatureEnabled("contentEditableMode") && (
<Permission
disabled={hasProPlan !== true}
onCheckedChange={handleCheckedChange("editors")}
checked={value.relation === "editors"}
title="Content"
info={
<Flex direction="column">
Recipients can edit content only, such as text, images, and
predefined components.
{hasProPlan !== true && (
<>
<br />
<br />
Upgrade to a Pro account to share with Content Edit
permissions.
<br /> <br />
<Link
className={buttonStyle({ color: "gradient" })}
color="contrast"
underline="none"
href="https://webstudio.is/pricing"
target="_blank"
>
Upgrade
</Link>
</>
)}
</Flex>
}
/>
)}

<Permission
onCheckedChange={handleCheckedChange("builders")}
checked={value.relation === "builders"}
Expand Down Expand Up @@ -301,13 +336,17 @@ type SharedLinkItemType = {
value: LinkOptions;
onChange: (value: LinkOptions) => void;
onDelete: () => void;
builderUrl: (props: {
authToken: string;
mode: "preview" | "design";
}) => string;
builderUrl: (props: { authToken: string; mode: BuilderMode }) => string;
hasProPlan: boolean;
};

const relationToMode: Record<Relation, BuilderMode> = {
viewers: "preview",
editors: "content",
builders: "design",
administrators: "design",
};

const SharedLinkItem = ({
value,
onChange,
Expand All @@ -323,7 +362,7 @@ const SharedLinkItem = ({
<CopyToClipboard
text={builderUrl({
authToken: value.token,
mode: value.relation === "viewers" ? "preview" : "design",
mode: relationToMode[value.relation],
})}
copyText="Copy link"
>
Expand Down
3 changes: 2 additions & 1 deletion packages/authorization-token/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
},
"devDependencies": {
"@webstudio-is/tsconfig": "workspace:*",
"typescript": "5.6.3"
"typescript": "5.6.3",
"type-fest": "^4.26.1"
},
"exports": {
".": {
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -9,6 +14,10 @@ const TokenProjectRelation = z.enum([
"administrators",
]);

// Check DB types are compatible with zod types
type TokenRelation = z.infer<typeof TokenProjectRelation>;
true satisfies IsEqual<TokenRelation, Relation>;

export const authorizationTokenRouter = router({
findMany: procedure
.input(
Expand Down
45 changes: 28 additions & 17 deletions packages/trpc-interface/src/authorize/project.server.ts
Original file line number Diff line number Diff line change
@@ -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<AuthPermit, "own">;

type CheckInput = {
namespace: "Project";
id: string;
Expand Down Expand Up @@ -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<TokenAuthPermit, Relation[]> = {
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
Expand Down Expand Up @@ -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 <T extends AuthPermit>(
export const getProjectPermit = async (
props: {
projectId: string;
permits: readonly T[];
permits: readonly AuthPermit[];
},
context: AppContext
): Promise<T | undefined> => {
): Promise<AuthPermit | undefined> => {
const permitToCheck = props.permits;

const permits = await Promise.allSettled(
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 0c44281

Please sign in to comment.