From cd93947a87c5e17647ca284eb913337f2b3344c9 Mon Sep 17 00:00:00 2001 From: myftija Date: Wed, 17 Sep 2025 14:00:49 +0200 Subject: [PATCH] feat(api): accept OATs in preview branch related endpoints Adjusts the authentication in a couple of endpoints to accept OATs too. --- .../route.tsx | 5 ++- ...1.projects.$projectRef.branches.archive.ts | 37 +++++++++++------ .../api.v1.projects.$projectRef.branches.ts | 41 ++++++++++++------- .../app/routes/resources.branches.archive.tsx | 7 +++- .../app/services/archiveBranch.server.ts | 32 +++++++++++---- .../app/services/upsertBranch.server.ts | 28 +++++++++---- 6 files changed, 106 insertions(+), 44 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.branches/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.branches/route.tsx index 7726e6a9b1..f356ee6c81 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.branches/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.branches/route.tsx @@ -130,7 +130,10 @@ export async function action({ request }: ActionFunctionArgs) { } const upsertBranchService = new UpsertBranchService(); - const result = await upsertBranchService.call(userId, submission.value); + const result = await upsertBranchService.call( + { type: "userMembership", userId }, + submission.value + ); if (result.success) { if (result.alreadyExisted) { diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.branches.archive.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.branches.archive.ts index 76147979c0..64119b5a41 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.branches.archive.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.branches.archive.ts @@ -2,9 +2,9 @@ import { type ActionFunctionArgs, json } from "@remix-run/server-runtime"; import { tryCatch } from "@trigger.dev/core"; import { z } from "zod"; import { prisma } from "~/db.server"; +import { authenticateRequest } from "~/services/apiAuth.server"; import { ArchiveBranchService } from "~/services/archiveBranch.server"; import { logger } from "~/services/logger.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; const ParamsSchema = z.object({ projectRef: z.string(), @@ -21,7 +21,12 @@ export async function action({ request, params }: ActionFunctionArgs) { logger.info("Archive branch", { url: request.url, params }); - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); + const authenticationResult = await authenticateRequest(request, { + personalAccessToken: true, + organizationAccessToken: true, + apiKey: false, + }); + if (!authenticationResult) { return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); } @@ -50,13 +55,16 @@ export async function action({ request, params }: ActionFunctionArgs) { archivedAt: true, }, where: { - organization: { - members: { - some: { - userId: authenticationResult.userId, - }, - }, - }, + organization: + authenticationResult.type === "organizationAccessToken" + ? { id: authenticationResult.result.organizationId } + : { + members: { + some: { + userId: authenticationResult.result.userId, + }, + }, + }, project: { externalRef: projectRef, }, @@ -74,9 +82,14 @@ export async function action({ request, params }: ActionFunctionArgs) { } const service = new ArchiveBranchService(); - const result = await service.call(authenticationResult.userId, { - environmentId: environment.id, - }); + const result = await service.call( + authenticationResult.type === "organizationAccessToken" + ? { type: "orgId", organizationId: authenticationResult.result.organizationId } + : { type: "userMembership", userId: authenticationResult.result.userId }, + { + environmentId: environment.id, + } + ); if (result.success) { return json(result); diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.branches.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.branches.ts index 6ae6a133e9..8678ef1f9d 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.branches.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.branches.ts @@ -1,7 +1,8 @@ -import { json, LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/server-runtime"; +import { json, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/server-runtime"; import { tryCatch, UpsertBranchRequestBody } from "@trigger.dev/core/v3"; import { z } from "zod"; import { prisma } from "~/db.server"; +import { authenticateRequest } from "~/services/apiAuth.server"; import { logger } from "~/services/logger.server"; import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; import { UpsertBranchService } from "~/services/upsertBranch.server"; @@ -19,7 +20,11 @@ export async function action({ request, params }: ActionFunctionArgs) { logger.info("project upsert branch", { url: request.url }); - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); + const authenticationResult = await authenticateRequest(request, { + personalAccessToken: true, + organizationAccessToken: true, + apiKey: false, + }); if (!authenticationResult) { return json({ error: "Invalid or Missing Access Token" }, { status: 401 }); } @@ -38,13 +43,16 @@ export async function action({ request, params }: ActionFunctionArgs) { }, where: { externalRef: projectRef, - organization: { - members: { - some: { - userId: authenticationResult.userId, - }, - }, - }, + organization: + authenticationResult.type === "organizationAccessToken" + ? { id: authenticationResult.result.organizationId } + : { + members: { + some: { + userId: authenticationResult.result.userId, + }, + }, + }, }, }); if (!project) { @@ -81,11 +89,16 @@ export async function action({ request, params }: ActionFunctionArgs) { const { branch, env, git } = parsed.data; const service = new UpsertBranchService(); - const result = await service.call(authenticationResult.userId, { - branchName: branch, - parentEnvironmentId: previewEnvironment.id, - git, - }); + const result = await service.call( + authenticationResult.type === "organizationAccessToken" + ? { type: "orgId", organizationId: authenticationResult.result.organizationId } + : { type: "userMembership", userId: authenticationResult.result.userId }, + { + branchName: branch, + parentEnvironmentId: previewEnvironment.id, + git, + } + ); if (!result.success) { return json({ error: result.error }, { status: 400 }); diff --git a/apps/webapp/app/routes/resources.branches.archive.tsx b/apps/webapp/app/routes/resources.branches.archive.tsx index 6658738ce0..57ba061bf0 100644 --- a/apps/webapp/app/routes/resources.branches.archive.tsx +++ b/apps/webapp/app/routes/resources.branches.archive.tsx @@ -37,7 +37,12 @@ export async function action({ request }: ActionFunctionArgs) { const archiveBranchService = new ArchiveBranchService(); - const result = await archiveBranchService.call(userId, submission.value); + const result = await archiveBranchService.call( + { type: "userMembership", userId }, + { + environmentId: submission.value.environmentId, + } + ); if (result.success) { return redirectWithSuccessMessage( diff --git a/apps/webapp/app/services/archiveBranch.server.ts b/apps/webapp/app/services/archiveBranch.server.ts index e0ff0e1174..9d7897f32c 100644 --- a/apps/webapp/app/services/archiveBranch.server.ts +++ b/apps/webapp/app/services/archiveBranch.server.ts @@ -10,18 +10,34 @@ export class ArchiveBranchService { this.#prismaClient = prismaClient; } - public async call(userId: string, { environmentId }: { environmentId: string }) { + public async call( + // The orgFilter approach is not ideal but we need to keep it this way for now because of how the service is used in routes and api endpoints. + // Currently authorization checks are spread across the controller/route layer and the service layer. Often we check in multiple places for org/project membership. + // Ideally we would take care of both the authentication and authorization checks in the controllers and routes. + // That would unify how we handle authorization and org/project membership checks. Also it would make the service layer queries simpler. + orgFilter: + | { type: "userMembership"; userId: string } + | { type: "orgId"; organizationId: string }, + { + environmentId, + }: { + environmentId: string; + } + ) { try { const environment = await this.#prismaClient.runtimeEnvironment.findFirstOrThrow({ where: { id: environmentId, - organization: { - members: { - some: { - userId: userId, - }, - }, - }, + organization: + orgFilter.type === "userMembership" + ? { + members: { + some: { + userId: orgFilter.userId, + }, + }, + } + : { id: orgFilter.organizationId }, }, include: { organization: { diff --git a/apps/webapp/app/services/upsertBranch.server.ts b/apps/webapp/app/services/upsertBranch.server.ts index 5f53ae2b98..a11fdc350a 100644 --- a/apps/webapp/app/services/upsertBranch.server.ts +++ b/apps/webapp/app/services/upsertBranch.server.ts @@ -14,7 +14,16 @@ export class UpsertBranchService { this.#prismaClient = prismaClient; } - public async call(userId: string, { parentEnvironmentId, branchName, git }: CreateBranchOptions) { + public async call( + // The orgFilter approach is not ideal but we need to keep it this way for now because of how the service is used in routes and api endpoints. + // Currently authorization checks are spread across the controller/route layer and the service layer. Often we check in multiple places for org/project membership. + // Ideally we would take care of both the authentication and authorization checks in the controllers and routes. + // That would unify how we handle authorization and org/project membership checks. Also it would make the service layer queries simpler. + orgFilter: + | { type: "userMembership"; userId: string } + | { type: "orgId"; organizationId: string }, + { parentEnvironmentId, branchName, git }: CreateBranchOptions + ) { const sanitizedBranchName = sanitizeBranchName(branchName); if (!sanitizedBranchName) { return { @@ -34,13 +43,16 @@ export class UpsertBranchService { const parentEnvironment = await this.#prismaClient.runtimeEnvironment.findFirst({ where: { id: parentEnvironmentId, - organization: { - members: { - some: { - userId: userId, - }, - }, - }, + organization: + orgFilter.type === "userMembership" + ? { + members: { + some: { + userId: orgFilter.userId, + }, + }, + } + : { id: orgFilter.organizationId }, }, include: { organization: {