Files
webstudio/apps/builder/app/shared/sync/patch/patch-auth.server.ts

116 lines
2.9 KiB
TypeScript

import {
AuthorizationError,
authorizeProject,
type AppContext,
type AuthPermit,
} from "@webstudio-is/trpc-interface/index.server";
import { getRequiredPermitForBuildPatchTransaction } from "@webstudio-is/project/index.server";
import env from "~/env/env.server";
import { readAccessToken } from "~/services/token.server";
import type {
NormalizedPatchRequest,
PatchEntry,
} from "./patch-normalize.server";
export type AuthorizedPatchEntry = {
entry: PatchEntry;
context: AppContext;
};
export type RejectedPatchEntry = {
entry: PatchEntry;
errors: string;
};
export const createWriterContext = async (
context: AppContext,
authToken: string
) => {
if (authToken.length === 0 || authToken === "session") {
throw new AuthorizationError("Collab writer token is not authorized");
}
try {
const accessToken = await readAccessToken(
authToken,
env.AUTH_WS_CLIENT_SECRET
);
if (accessToken !== undefined) {
return {
...context,
authorization: {
type: "user" as const,
userId: accessToken.userId,
sessionCreatedAt: 0,
isLoggedInToBuilder: async (projectId: string) =>
projectId === accessToken.projectId,
},
};
}
} catch {
// Not a signed collab token; try the existing shared-link token flow.
}
return context.createTokenContext(authToken);
};
const resolveEntryWriterContext = async (
context: AppContext,
entry: PatchEntry
) => {
if (entry.writer.type === "context") {
return entry.writer.context;
}
return createWriterContext(context, entry.writer.authToken);
};
export const assertProjectPermit = async ({
context,
permit,
projectId,
}: {
context: AppContext;
permit: AuthPermit;
projectId: string;
}) => {
const allowed = await authorizeProject.hasProjectPermit(
{ projectId, permit },
context
);
if (allowed === false) {
throw new AuthorizationError(
`You don't have permission to ${permit === "build" ? "build" : "edit"} this project.`
);
}
};
export const authorizePatchEntries = async (
context: AppContext,
patch: NormalizedPatchRequest
) => {
const authorized: AuthorizedPatchEntry[] = [];
const rejected: RejectedPatchEntry[] = [];
for (const entry of patch.entries) {
try {
const writerContext = await resolveEntryWriterContext(context, entry);
await assertProjectPermit({
context: writerContext,
permit: getRequiredPermitForBuildPatchTransaction(entry.transaction),
projectId: patch.projectId,
});
authorized.push({ entry, context: writerContext });
} catch (error) {
if (error instanceof AuthorizationError) {
rejected.push({ entry, errors: error.message });
continue;
}
throw error;
}
}
if (authorized.length === 0 && rejected.length === 0) {
throw new Error("Transaction entries required");
}
return { authorized, rejected };
};