diff --git a/.server-changes/admin-workers-endpoint.md b/.server-changes/admin-workers-endpoint.md new file mode 100644 index 0000000000..34cd6ad70e --- /dev/null +++ b/.server-changes/admin-workers-endpoint.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: improvement +--- + +Admin worker groups API: add GET loader and expose more fields on POST. diff --git a/apps/webapp/app/routes/admin.api.v1.environments.$environmentId.engine.repair-queues.ts b/apps/webapp/app/routes/admin.api.v1.environments.$environmentId.engine.repair-queues.ts index 30d60197f9..a3fd61546e 100644 --- a/apps/webapp/app/routes/admin.api.v1.environments.$environmentId.engine.repair-queues.ts +++ b/apps/webapp/app/routes/admin.api.v1.environments.$environmentId.engine.repair-queues.ts @@ -2,7 +2,7 @@ import { ActionFunctionArgs, json } from "@remix-run/server-runtime"; import pMap from "p-map"; import { z } from "zod"; import { $replica, prisma } from "~/db.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; +import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; import { determineEngineVersion } from "~/v3/engineVersion.server"; import { engine } from "~/v3/runEngine.server"; @@ -16,26 +16,7 @@ const BodySchema = z.object({ }); export async function action({ request, params }: ActionFunctionArgs) { - // Next authenticate the request - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const user = await prisma.user.findUnique({ - where: { - id: authenticationResult.userId, - }, - }); - - if (!user) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - if (!user.admin) { - return json({ error: "You must be an admin to perform this action" }, { status: 403 }); - } + await requireAdminApiRequest(request); const parsedParams = ParamsSchema.parse(params); diff --git a/apps/webapp/app/routes/admin.api.v1.environments.$environmentId.engine.report.ts b/apps/webapp/app/routes/admin.api.v1.environments.$environmentId.engine.report.ts index 3ea9576899..e37553ea23 100644 --- a/apps/webapp/app/routes/admin.api.v1.environments.$environmentId.engine.report.ts +++ b/apps/webapp/app/routes/admin.api.v1.environments.$environmentId.engine.report.ts @@ -1,7 +1,7 @@ import { json, LoaderFunctionArgs } from "@remix-run/server-runtime"; import { z } from "zod"; import { $replica, prisma } from "~/db.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; +import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; import { determineEngineVersion } from "~/v3/engineVersion.server"; import { engine } from "~/v3/runEngine.server"; @@ -16,26 +16,7 @@ const SearchParamsSchema = z.object({ }); export async function loader({ request, params }: LoaderFunctionArgs) { - // Next authenticate the request - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const user = await prisma.user.findUnique({ - where: { - id: authenticationResult.userId, - }, - }); - - if (!user) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - if (!user.admin) { - return json({ error: "You must be an admin to perform this action" }, { status: 403 }); - } + await requireAdminApiRequest(request); const parsedParams = ParamsSchema.parse(params); diff --git a/apps/webapp/app/routes/admin.api.v1.environments.$environmentId.schedules.recover.ts b/apps/webapp/app/routes/admin.api.v1.environments.$environmentId.schedules.recover.ts index a9ada56085..33c1581a94 100644 --- a/apps/webapp/app/routes/admin.api.v1.environments.$environmentId.schedules.recover.ts +++ b/apps/webapp/app/routes/admin.api.v1.environments.$environmentId.schedules.recover.ts @@ -1,7 +1,7 @@ import { ActionFunctionArgs, json, LoaderFunctionArgs } from "@remix-run/server-runtime"; import { z } from "zod"; import { prisma } from "~/db.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; +import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; import { scheduleEngine } from "~/v3/scheduleEngine.server"; const ParamsSchema = z.object({ @@ -9,26 +9,7 @@ const ParamsSchema = z.object({ }); export async function action({ request, params }: ActionFunctionArgs) { - // Next authenticate the request - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const user = await prisma.user.findUnique({ - where: { - id: authenticationResult.userId, - }, - }); - - if (!user) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - if (!user.admin) { - return json({ error: "You must be an admin to perform this action" }, { status: 403 }); - } + await requireAdminApiRequest(request); const parsedParams = ParamsSchema.parse(params); diff --git a/apps/webapp/app/routes/admin.api.v1.environments.$environmentId.ts b/apps/webapp/app/routes/admin.api.v1.environments.$environmentId.ts index f448c5b5ac..34ea14f9da 100644 --- a/apps/webapp/app/routes/admin.api.v1.environments.$environmentId.ts +++ b/apps/webapp/app/routes/admin.api.v1.environments.$environmentId.ts @@ -1,7 +1,7 @@ import { ActionFunctionArgs, json, LoaderFunctionArgs } from "@remix-run/server-runtime"; import { z } from "zod"; import { prisma } from "~/db.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; +import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; import { engine } from "~/v3/runEngine.server"; import { updateEnvConcurrencyLimits } from "~/v3/runQueue.server"; @@ -15,26 +15,7 @@ const RequestBodySchema = z.object({ }); export async function action({ request, params }: ActionFunctionArgs) { - // Next authenticate the request - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const user = await prisma.user.findUnique({ - where: { - id: authenticationResult.userId, - }, - }); - - if (!user) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - if (!user.admin) { - return json({ error: "You must be an admin to perform this action" }, { status: 403 }); - } + await requireAdminApiRequest(request); const parsedParams = ParamsSchema.parse(params); @@ -71,26 +52,7 @@ const SearchParamsSchema = z.object({ }); export async function loader({ request, params }: LoaderFunctionArgs) { - // Next authenticate the request - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const user = await prisma.user.findUnique({ - where: { - id: authenticationResult.userId, - }, - }); - - if (!user) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - if (!user.admin) { - return json({ error: "You must be an admin to get this endpoint" }, { status: 403 }); - } + await requireAdminApiRequest(request); const parsedParams = ParamsSchema.parse(params); diff --git a/apps/webapp/app/routes/admin.api.v1.feature-flags.ts b/apps/webapp/app/routes/admin.api.v1.feature-flags.ts index b10c983b14..92debd43a6 100644 --- a/apps/webapp/app/routes/admin.api.v1.feature-flags.ts +++ b/apps/webapp/app/routes/admin.api.v1.feature-flags.ts @@ -1,30 +1,11 @@ import { ActionFunctionArgs, json } from "@remix-run/server-runtime"; import { prisma } from "~/db.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; +import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; import { makeSetMultipleFlags } from "~/v3/featureFlags.server"; import { validatePartialFeatureFlags } from "~/v3/featureFlags"; export async function action({ request }: ActionFunctionArgs) { - // Next authenticate the request - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const user = await prisma.user.findFirst({ - where: { - id: authenticationResult.userId, - }, - }); - - if (!user) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - if (!user.admin) { - return json({ error: "You must be an admin to perform this action" }, { status: 403 }); - } + await requireAdminApiRequest(request); try { // Parse the request body diff --git a/apps/webapp/app/routes/admin.api.v1.gc.ts b/apps/webapp/app/routes/admin.api.v1.gc.ts index ea63a264ac..fbb5f4c900 100644 --- a/apps/webapp/app/routes/admin.api.v1.gc.ts +++ b/apps/webapp/app/routes/admin.api.v1.gc.ts @@ -2,8 +2,7 @@ import { type DataFunctionArgs } from "@remix-run/node"; import { PerformanceObserver } from "node:perf_hooks"; import { runInNewContext } from "node:vm"; import v8 from "v8"; -import { prisma } from "~/db.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; +import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; async function waitTillGcFinishes() { let resolver: (value: PerformanceEntry) => void; @@ -36,21 +35,7 @@ async function waitTillGcFinishes() { } export async function loader({ request }: DataFunctionArgs) { - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - - if (!authenticationResult) { - throw new Response("You must be an admin to perform this action", { status: 403 }); - } - - const user = await prisma.user.findFirst({ - where: { - id: authenticationResult.userId, - }, - }); - - if (!user?.admin) { - throw new Response("You must be an admin to perform this action", { status: 403 }); - } + await requireAdminApiRequest(request); const entry = await waitTillGcFinishes(); diff --git a/apps/webapp/app/routes/admin.api.v1.llm-models.$modelId.ts b/apps/webapp/app/routes/admin.api.v1.llm-models.$modelId.ts index 2556dc8267..e473c2c727 100644 --- a/apps/webapp/app/routes/admin.api.v1.llm-models.$modelId.ts +++ b/apps/webapp/app/routes/admin.api.v1.llm-models.$modelId.ts @@ -1,24 +1,10 @@ import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; import { z } from "zod"; import { prisma } from "~/db.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; - -async function requireAdmin(request: Request) { - const authResult = await authenticateApiRequestWithPersonalAccessToken(request); - if (!authResult) { - throw json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const user = await prisma.user.findUnique({ where: { id: authResult.userId } }); - if (!user?.admin) { - throw json({ error: "You must be an admin to perform this action" }, { status: 403 }); - } - - return user; -} +import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; export async function loader({ request, params }: LoaderFunctionArgs) { - await requireAdmin(request); + await requireAdminApiRequest(request); const model = await prisma.llmModel.findUnique({ where: { id: params.modelId }, @@ -69,7 +55,7 @@ const UpdateModelSchema = z.object({ }); export async function action({ request, params }: ActionFunctionArgs) { - await requireAdmin(request); + await requireAdminApiRequest(request); const modelId = params.modelId!; diff --git a/apps/webapp/app/routes/admin.api.v1.llm-models.missing.ts b/apps/webapp/app/routes/admin.api.v1.llm-models.missing.ts index 5ca7077e1c..9e1e7dcb43 100644 --- a/apps/webapp/app/routes/admin.api.v1.llm-models.missing.ts +++ b/apps/webapp/app/routes/admin.api.v1.llm-models.missing.ts @@ -1,24 +1,9 @@ import { type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; -import { prisma } from "~/db.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; +import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; import { getMissingLlmModels } from "~/services/admin/missingLlmModels.server"; -async function requireAdmin(request: Request) { - const authResult = await authenticateApiRequestWithPersonalAccessToken(request); - if (!authResult) { - throw json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const user = await prisma.user.findUnique({ where: { id: authResult.userId } }); - if (!user?.admin) { - throw json({ error: "You must be an admin to perform this action" }, { status: 403 }); - } - - return user; -} - export async function loader({ request }: LoaderFunctionArgs) { - await requireAdmin(request); + await requireAdminApiRequest(request); const url = new URL(request.url); const lookbackHours = parseInt(url.searchParams.get("lookbackHours") ?? "24", 10); diff --git a/apps/webapp/app/routes/admin.api.v1.llm-models.reload.ts b/apps/webapp/app/routes/admin.api.v1.llm-models.reload.ts index 747722b352..26eee6d843 100644 --- a/apps/webapp/app/routes/admin.api.v1.llm-models.reload.ts +++ b/apps/webapp/app/routes/admin.api.v1.llm-models.reload.ts @@ -1,18 +1,9 @@ import { type ActionFunctionArgs, json } from "@remix-run/server-runtime"; -import { prisma } from "~/db.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; +import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; import { llmPricingRegistry } from "~/v3/llmPricingRegistry.server"; export async function action({ request }: ActionFunctionArgs) { - const authResult = await authenticateApiRequestWithPersonalAccessToken(request); - if (!authResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const user = await prisma.user.findUnique({ where: { id: authResult.userId } }); - if (!user?.admin) { - return json({ error: "You must be an admin to perform this action" }, { status: 403 }); - } + await requireAdminApiRequest(request); if (!llmPricingRegistry) { return json({ error: "LLM cost tracking is disabled" }, { status: 400 }); diff --git a/apps/webapp/app/routes/admin.api.v1.llm-models.seed.ts b/apps/webapp/app/routes/admin.api.v1.llm-models.seed.ts index 32e780d9fb..ef85d45873 100644 --- a/apps/webapp/app/routes/admin.api.v1.llm-models.seed.ts +++ b/apps/webapp/app/routes/admin.api.v1.llm-models.seed.ts @@ -1,19 +1,11 @@ import { type ActionFunctionArgs, json } from "@remix-run/server-runtime"; import { seedLlmPricing, syncLlmCatalog } from "@internal/llm-model-catalog"; import { prisma } from "~/db.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; +import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; import { llmPricingRegistry } from "~/v3/llmPricingRegistry.server"; export async function action({ request }: ActionFunctionArgs) { - const authResult = await authenticateApiRequestWithPersonalAccessToken(request); - if (!authResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const user = await prisma.user.findUnique({ where: { id: authResult.userId } }); - if (!user?.admin) { - return json({ error: "You must be an admin to perform this action" }, { status: 403 }); - } + await requireAdminApiRequest(request); const url = new URL(request.url); const action = url.searchParams.get("action") ?? "seed"; diff --git a/apps/webapp/app/routes/admin.api.v1.llm-models.ts b/apps/webapp/app/routes/admin.api.v1.llm-models.ts index 4e3cc39f47..7c8a3d8caa 100644 --- a/apps/webapp/app/routes/admin.api.v1.llm-models.ts +++ b/apps/webapp/app/routes/admin.api.v1.llm-models.ts @@ -1,25 +1,11 @@ import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; import { z } from "zod"; import { prisma } from "~/db.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; +import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; import { generateFriendlyId } from "~/v3/friendlyIdentifiers"; -async function requireAdmin(request: Request) { - const authResult = await authenticateApiRequestWithPersonalAccessToken(request); - if (!authResult) { - throw json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const user = await prisma.user.findUnique({ where: { id: authResult.userId } }); - if (!user?.admin) { - throw json({ error: "You must be an admin to perform this action" }, { status: 403 }); - } - - return user; -} - export async function loader({ request }: LoaderFunctionArgs) { - await requireAdmin(request); + await requireAdminApiRequest(request); const url = new URL(request.url); const page = parseInt(url.searchParams.get("page") ?? "1"); @@ -75,7 +61,7 @@ const CreateModelSchema = z.object({ }); export async function action({ request }: ActionFunctionArgs) { - await requireAdmin(request); + await requireAdminApiRequest(request); if (request.method !== "POST") { return json({ error: "Method not allowed" }, { status: 405 }); diff --git a/apps/webapp/app/routes/admin.api.v1.migrate-legacy-master-queues.ts b/apps/webapp/app/routes/admin.api.v1.migrate-legacy-master-queues.ts index b960287c92..ad3575fa39 100644 --- a/apps/webapp/app/routes/admin.api.v1.migrate-legacy-master-queues.ts +++ b/apps/webapp/app/routes/admin.api.v1.migrate-legacy-master-queues.ts @@ -1,29 +1,9 @@ import { ActionFunctionArgs, json } from "@remix-run/server-runtime"; -import { prisma } from "~/db.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; +import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; import { engine } from "~/v3/runEngine.server"; export async function action({ request }: ActionFunctionArgs) { - // Next authenticate the request - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const user = await prisma.user.findUnique({ - where: { - id: authenticationResult.userId, - }, - }); - - if (!user) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - if (!user.admin) { - return json({ error: "You must be an admin to perform this action" }, { status: 403 }); - } + await requireAdminApiRequest(request); try { await engine.migrateLegacyMasterQueues(); diff --git a/apps/webapp/app/routes/admin.api.v1.orgs.$organizationId.concurrency.ts b/apps/webapp/app/routes/admin.api.v1.orgs.$organizationId.concurrency.ts index d6eee08f37..f5346a6907 100644 --- a/apps/webapp/app/routes/admin.api.v1.orgs.$organizationId.concurrency.ts +++ b/apps/webapp/app/routes/admin.api.v1.orgs.$organizationId.concurrency.ts @@ -1,7 +1,7 @@ import { ActionFunctionArgs, json } from "@remix-run/server-runtime"; import { z } from "zod"; import { prisma } from "~/db.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; +import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; import { marqs } from "~/v3/marqs/index.server"; import { updateEnvConcurrencyLimits } from "~/v3/runQueue.server"; @@ -17,26 +17,7 @@ const RequestBodySchema = z.object({ }); export async function action({ request, params }: ActionFunctionArgs) { - // Next authenticate the request - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const user = await prisma.user.findUnique({ - where: { - id: authenticationResult.userId, - }, - }); - - if (!user) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - if (!user.admin) { - return json({ error: "You must be an admin to perform this action" }, { status: 403 }); - } + await requireAdminApiRequest(request); const { organizationId } = ParamsSchema.parse(params); diff --git a/apps/webapp/app/routes/admin.api.v1.orgs.$organizationId.environments.staging.ts b/apps/webapp/app/routes/admin.api.v1.orgs.$organizationId.environments.staging.ts index 6a8628f752..f3c215bfd4 100644 --- a/apps/webapp/app/routes/admin.api.v1.orgs.$organizationId.environments.staging.ts +++ b/apps/webapp/app/routes/admin.api.v1.orgs.$organizationId.environments.staging.ts @@ -8,7 +8,7 @@ import { import { z } from "zod"; import { prisma } from "~/db.server"; import { createEnvironment } from "~/models/organization.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; +import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; import { updateEnvConcurrencyLimits } from "~/v3/runQueue.server"; const ParamsSchema = z.object({ @@ -19,26 +19,7 @@ const ParamsSchema = z.object({ * It will create a staging environment for all the projects where there isn't one already */ export async function action({ request, params }: ActionFunctionArgs) { - // Next authenticate the request - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const user = await prisma.user.findUnique({ - where: { - id: authenticationResult.userId, - }, - }); - - if (!user) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - if (!user.admin) { - return json({ error: "You must be an admin to perform this action" }, { status: 403 }); - } + await requireAdminApiRequest(request); const { organizationId } = ParamsSchema.parse(params); diff --git a/apps/webapp/app/routes/admin.api.v1.orgs.$organizationId.feature-flags.ts b/apps/webapp/app/routes/admin.api.v1.orgs.$organizationId.feature-flags.ts index bb0671355b..513616470a 100644 --- a/apps/webapp/app/routes/admin.api.v1.orgs.$organizationId.feature-flags.ts +++ b/apps/webapp/app/routes/admin.api.v1.orgs.$organizationId.feature-flags.ts @@ -1,43 +1,15 @@ import { ActionFunctionArgs, LoaderFunctionArgs, json } from "@remix-run/server-runtime"; import { z } from "zod"; import { prisma } from "~/db.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; +import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; import { validatePartialFeatureFlags } from "~/v3/featureFlags"; const ParamsSchema = z.object({ organizationId: z.string(), }); -async function authenticateAdmin(request: Request) { - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - - if (!authenticationResult) { - return { error: json({ error: "Invalid or Missing API key" }, { status: 401 }) }; - } - - const user = await prisma.user.findFirst({ - where: { - id: authenticationResult.userId, - }, - }); - - if (!user) { - return { error: json({ error: "Invalid or Missing API key" }, { status: 401 }) }; - } - - if (!user.admin) { - return { error: json({ error: "You must be an admin to perform this action" }, { status: 403 }) }; - } - - return { user }; -} - export async function loader({ request, params }: LoaderFunctionArgs) { - const authResult = await authenticateAdmin(request); - - if ("error" in authResult) { - return authResult.error; - } + await requireAdminApiRequest(request); const { organizationId } = ParamsSchema.parse(params); @@ -70,11 +42,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } export async function action({ request, params }: ActionFunctionArgs) { - const authResult = await authenticateAdmin(request); - - if ("error" in authResult) { - return authResult.error; - } + await requireAdminApiRequest(request); const { organizationId } = ParamsSchema.parse(params); diff --git a/apps/webapp/app/routes/admin.api.v1.orgs.$organizationId.runs.enable.ts b/apps/webapp/app/routes/admin.api.v1.orgs.$organizationId.runs.enable.ts index 6b1cf2d993..d60754f046 100644 --- a/apps/webapp/app/routes/admin.api.v1.orgs.$organizationId.runs.enable.ts +++ b/apps/webapp/app/routes/admin.api.v1.orgs.$organizationId.runs.enable.ts @@ -8,7 +8,7 @@ import { import { z } from "zod"; import { prisma } from "~/db.server"; import { createEnvironment } from "~/models/organization.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; +import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; import { updateEnvConcurrencyLimits } from "~/v3/runQueue.server"; import { PauseEnvironmentService } from "~/v3/services/pauseEnvironment.server"; @@ -24,26 +24,7 @@ const BodySchema = z.object({ * It will enabled/disable runs */ export async function action({ request, params }: ActionFunctionArgs) { - // Next authenticate the request - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const user = await prisma.user.findUnique({ - where: { - id: authenticationResult.userId, - }, - }); - - if (!user) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - if (!user.admin) { - return json({ error: "You must be an admin to perform this action" }, { status: 403 }); - } + await requireAdminApiRequest(request); const { organizationId } = ParamsSchema.parse(params); const body = BodySchema.safeParse(await request.json()); diff --git a/apps/webapp/app/routes/admin.api.v1.platform-notifications.ts b/apps/webapp/app/routes/admin.api.v1.platform-notifications.ts index 093104bcb0..3798d9fa73 100644 --- a/apps/webapp/app/routes/admin.api.v1.platform-notifications.ts +++ b/apps/webapp/app/routes/admin.api.v1.platform-notifications.ts @@ -1,7 +1,6 @@ import { type ActionFunctionArgs, json } from "@remix-run/server-runtime"; import { err, ok, type Result } from "neverthrow"; -import { prisma } from "~/db.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; +import { authenticateAdminRequest } from "~/services/personalAccessToken.server"; import { createPlatformNotification, type CreatePlatformNotificationInput, @@ -11,24 +10,10 @@ type AdminUser = { id: string; admin: boolean }; type AuthError = { status: number; message: string }; async function authenticateAdmin(request: Request): Promise> { - const authResult = await authenticateApiRequestWithPersonalAccessToken(request); - if (!authResult) { - return err({ status: 401, message: "Invalid or Missing API key" }); - } - - const user = await prisma.user.findUnique({ - where: { id: authResult.userId }, - select: { id: true, admin: true }, - }); - - if (!user?.admin) { - return err({ - status: user ? 403 : 401, - message: user ? "You must be an admin to perform this action" : "Invalid or Missing API key", - }); - } - - return ok(user); + const result = await authenticateAdminRequest(request); + return result.ok + ? ok({ id: result.user.id, admin: result.user.admin }) + : err({ status: result.status, message: result.message }); } export async function action({ request }: ActionFunctionArgs) { diff --git a/apps/webapp/app/routes/admin.api.v1.runs-replication.$batchId.backfill.ts b/apps/webapp/app/routes/admin.api.v1.runs-replication.$batchId.backfill.ts index 105fcaa408..1842779531 100644 --- a/apps/webapp/app/routes/admin.api.v1.runs-replication.$batchId.backfill.ts +++ b/apps/webapp/app/routes/admin.api.v1.runs-replication.$batchId.backfill.ts @@ -1,7 +1,6 @@ import { type ActionFunctionArgs, json } from "@remix-run/server-runtime"; import { z } from "zod"; -import { prisma } from "~/db.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; +import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; import { adminWorker } from "~/v3/services/adminWorker.server"; const Body = z.object({ @@ -19,26 +18,7 @@ const DEFAULT_BATCH_SIZE = 500; const DEFAULT_DELAY_INTERVAL_MS = 1000; export async function action({ request, params }: ActionFunctionArgs) { - // Next authenticate the request - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const user = await prisma.user.findUnique({ - where: { - id: authenticationResult.userId, - }, - }); - - if (!user) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - if (!user.admin) { - return json({ error: "You must be an admin to perform this action" }, { status: 403 }); - } + await requireAdminApiRequest(request); const { batchId } = Params.parse(params); diff --git a/apps/webapp/app/routes/admin.api.v1.runs-replication.$batchId.cancel.ts b/apps/webapp/app/routes/admin.api.v1.runs-replication.$batchId.cancel.ts index 8dfcf9fb85..b80bfba6b5 100644 --- a/apps/webapp/app/routes/admin.api.v1.runs-replication.$batchId.cancel.ts +++ b/apps/webapp/app/routes/admin.api.v1.runs-replication.$batchId.cancel.ts @@ -1,7 +1,6 @@ import { type ActionFunctionArgs, json } from "@remix-run/server-runtime"; import { z } from "zod"; -import { prisma } from "~/db.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; +import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; import { adminWorker } from "~/v3/services/adminWorker.server"; const Params = z.object({ @@ -9,26 +8,7 @@ const Params = z.object({ }); export async function action({ request, params }: ActionFunctionArgs) { - // Next authenticate the request - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const user = await prisma.user.findUnique({ - where: { - id: authenticationResult.userId, - }, - }); - - if (!user) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - if (!user.admin) { - return json({ error: "You must be an admin to perform this action" }, { status: 403 }); - } + await requireAdminApiRequest(request); const { batchId } = Params.parse(params); diff --git a/apps/webapp/app/routes/admin.api.v1.runs-replication.backfill.ts b/apps/webapp/app/routes/admin.api.v1.runs-replication.backfill.ts index 0897c30c21..c4d17ba875 100644 --- a/apps/webapp/app/routes/admin.api.v1.runs-replication.backfill.ts +++ b/apps/webapp/app/routes/admin.api.v1.runs-replication.backfill.ts @@ -3,7 +3,7 @@ import { type TaskRun } from "@trigger.dev/database"; import { z } from "zod"; import { prisma } from "~/db.server"; import { logger } from "~/services/logger.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; +import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; import { runsReplicationInstance } from "~/services/runsReplicationInstance.server"; import { FINAL_RUN_STATUSES } from "~/v3/taskStatus"; @@ -14,26 +14,7 @@ const Body = z.object({ const MAX_BATCH_SIZE = 50; export async function action({ request }: ActionFunctionArgs) { - // Next authenticate the request - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const user = await prisma.user.findUnique({ - where: { - id: authenticationResult.userId, - }, - }); - - if (!user) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - if (!user.admin) { - return json({ error: "You must be an admin to perform this action" }, { status: 403 }); - } + await requireAdminApiRequest(request); try { const body = await request.json(); diff --git a/apps/webapp/app/routes/admin.api.v1.runs-replication.create.ts b/apps/webapp/app/routes/admin.api.v1.runs-replication.create.ts index 483c2d219a..9cac56b65c 100644 --- a/apps/webapp/app/routes/admin.api.v1.runs-replication.create.ts +++ b/apps/webapp/app/routes/admin.api.v1.runs-replication.create.ts @@ -1,6 +1,5 @@ import { ActionFunctionArgs, json } from "@remix-run/server-runtime"; -import { prisma } from "~/db.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; +import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; import { z } from "zod"; import { ClickHouse } from "@internal/clickhouse"; import { env } from "~/env.server"; @@ -29,26 +28,7 @@ const CreateRunReplicationServiceParams = z.object({ type CreateRunReplicationServiceParams = z.infer; export async function action({ request }: ActionFunctionArgs) { - // Next authenticate the request - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const user = await prisma.user.findUnique({ - where: { - id: authenticationResult.userId, - }, - }); - - if (!user) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - if (!user.admin) { - return json({ error: "You must be an admin to perform this action" }, { status: 403 }); - } + await requireAdminApiRequest(request); try { const globalService = getRunsReplicationGlobal(); diff --git a/apps/webapp/app/routes/admin.api.v1.runs-replication.start.ts b/apps/webapp/app/routes/admin.api.v1.runs-replication.start.ts index a700c4d4f1..e0c603f532 100644 --- a/apps/webapp/app/routes/admin.api.v1.runs-replication.start.ts +++ b/apps/webapp/app/routes/admin.api.v1.runs-replication.start.ts @@ -1,30 +1,10 @@ import { ActionFunctionArgs, json } from "@remix-run/server-runtime"; -import { prisma } from "~/db.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; +import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; import { getRunsReplicationGlobal } from "~/services/runsReplicationGlobal.server"; import { runsReplicationInstance } from "~/services/runsReplicationInstance.server"; export async function action({ request }: ActionFunctionArgs) { - // Next authenticate the request - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const user = await prisma.user.findUnique({ - where: { - id: authenticationResult.userId, - }, - }); - - if (!user) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - if (!user.admin) { - return json({ error: "You must be an admin to perform this action" }, { status: 403 }); - } + await requireAdminApiRequest(request); try { const globalService = getRunsReplicationGlobal(); diff --git a/apps/webapp/app/routes/admin.api.v1.runs-replication.stop.ts b/apps/webapp/app/routes/admin.api.v1.runs-replication.stop.ts index 1dc53833d8..410a9aeaab 100644 --- a/apps/webapp/app/routes/admin.api.v1.runs-replication.stop.ts +++ b/apps/webapp/app/routes/admin.api.v1.runs-replication.stop.ts @@ -1,30 +1,10 @@ import { ActionFunctionArgs, json } from "@remix-run/server-runtime"; -import { prisma } from "~/db.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; +import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; import { getRunsReplicationGlobal } from "~/services/runsReplicationGlobal.server"; import { runsReplicationInstance } from "~/services/runsReplicationInstance.server"; export async function action({ request }: ActionFunctionArgs) { - // Next authenticate the request - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const user = await prisma.user.findUnique({ - where: { - id: authenticationResult.userId, - }, - }); - - if (!user) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - if (!user.admin) { - return json({ error: "You must be an admin to perform this action" }, { status: 403 }); - } + await requireAdminApiRequest(request); try { const globalService = getRunsReplicationGlobal(); diff --git a/apps/webapp/app/routes/admin.api.v1.runs-replication.teardown.ts b/apps/webapp/app/routes/admin.api.v1.runs-replication.teardown.ts index f4a1223dfc..8bcf760e72 100644 --- a/apps/webapp/app/routes/admin.api.v1.runs-replication.teardown.ts +++ b/apps/webapp/app/routes/admin.api.v1.runs-replication.teardown.ts @@ -1,6 +1,5 @@ import { ActionFunctionArgs, json } from "@remix-run/server-runtime"; -import { prisma } from "~/db.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; +import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; import { getRunsReplicationGlobal, unregisterRunsReplicationGlobal, @@ -8,26 +7,7 @@ import { import { runsReplicationInstance } from "~/services/runsReplicationInstance.server"; export async function action({ request }: ActionFunctionArgs) { - // Next authenticate the request - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - const user = await prisma.user.findUnique({ - where: { - id: authenticationResult.userId, - }, - }); - - if (!user) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } - - if (!user.admin) { - return json({ error: "You must be an admin to perform this action" }, { status: 403 }); - } + await requireAdminApiRequest(request); try { const globalService = getRunsReplicationGlobal(); diff --git a/apps/webapp/app/routes/admin.api.v1.snapshot.ts b/apps/webapp/app/routes/admin.api.v1.snapshot.ts index 3b345978f5..daba88f2a4 100644 --- a/apps/webapp/app/routes/admin.api.v1.snapshot.ts +++ b/apps/webapp/app/routes/admin.api.v1.snapshot.ts @@ -4,8 +4,7 @@ import os from "os"; import path from "path"; import { PassThrough } from "stream"; import v8 from "v8"; -import { prisma } from "~/db.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; +import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; // Format date as yyyy-MM-dd HH_mm_ss_SSS function formatDate(date: Date) { @@ -25,21 +24,7 @@ function formatDate(date: Date) { } export async function loader({ request }: DataFunctionArgs) { - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - - if (!authenticationResult) { - throw new Response("You must be an admin to perform this action", { status: 403 }); - } - - const user = await prisma.user.findFirst({ - where: { - id: authenticationResult.userId, - }, - }); - - if (!user?.admin) { - throw new Response("You must be an admin to perform this action", { status: 403 }); - } + await requireAdminApiRequest(request); const tempDir = os.tmpdir(); const filepath = path.join( diff --git a/apps/webapp/app/routes/admin.api.v1.workers.ts b/apps/webapp/app/routes/admin.api.v1.workers.ts index b215d8ce22..caa36e5217 100644 --- a/apps/webapp/app/routes/admin.api.v1.workers.ts +++ b/apps/webapp/app/routes/admin.api.v1.workers.ts @@ -1,9 +1,13 @@ -import { type ActionFunctionArgs, json } from "@remix-run/server-runtime"; +import { + type ActionFunctionArgs, + type LoaderFunctionArgs, + json, +} from "@remix-run/server-runtime"; import { tryCatch } from "@trigger.dev/core"; -import { type Project } from "@trigger.dev/database"; +import { type Project, WorkerInstanceGroupType, WorkloadType } from "@trigger.dev/database"; import { z } from "zod"; import { prisma } from "~/db.server"; -import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; +import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; import { WorkerGroupService } from "~/v3/services/worker/workerGroupService.server"; const RequestBodySchema = z.object({ @@ -12,34 +16,44 @@ const RequestBodySchema = z.object({ projectId: z.string().optional(), makeDefaultForProject: z.boolean().default(false), removeDefaultFromProject: z.boolean().default(false), + type: z.nativeEnum(WorkerInstanceGroupType).optional(), + hidden: z.boolean().optional(), + workloadType: z.nativeEnum(WorkloadType).optional(), + cloudProvider: z.string().optional(), + location: z.string().optional(), + staticIPs: z.string().optional(), + enableFastPath: z.boolean().optional(), }); -export async function action({ request }: ActionFunctionArgs) { - // Next authenticate the request - const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - - if (!authenticationResult) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } +export async function loader({ request }: LoaderFunctionArgs) { + await requireAdminApiRequest(request); - const user = await prisma.user.findFirst({ - where: { - id: authenticationResult.userId, - }, + const workerGroups = await prisma.workerInstanceGroup.findMany({ + orderBy: { createdAt: "asc" }, }); - if (!user) { - return json({ error: "Invalid or Missing API key" }, { status: 401 }); - } + return json({ workerGroups }); +} - if (!user.admin) { - return json({ error: "You must be an admin to perform this action" }, { status: 403 }); - } +export async function action({ request }: ActionFunctionArgs) { + await requireAdminApiRequest(request); try { const rawBody = await request.json(); - const { name, description, projectId, makeDefaultForProject, removeDefaultFromProject } = - RequestBodySchema.parse(rawBody ?? {}); + const { + name, + description, + projectId, + makeDefaultForProject, + removeDefaultFromProject, + type, + hidden, + workloadType, + cloudProvider, + location, + staticIPs, + enableFastPath, + } = RequestBodySchema.parse(rawBody ?? {}); if (removeDefaultFromProject) { if (!projectId) { @@ -74,7 +88,17 @@ export async function action({ request }: ActionFunctionArgs) { }); if (!existingWorkerGroup) { - const { workerGroup, token } = await createWorkerGroup(name, description); + const { workerGroup, token } = await createWorkerGroup({ + name, + description, + type, + hidden, + workloadType, + cloudProvider, + location, + staticIPs, + enableFastPath, + }); if (!makeDefaultForProject) { return json({ @@ -150,9 +174,11 @@ export async function action({ request }: ActionFunctionArgs) { } } -async function createWorkerGroup(name: string | undefined, description: string | undefined) { +async function createWorkerGroup( + options: Parameters[0] +) { const service = new WorkerGroupService(); - return await service.createWorkerGroup({ name, description }); + return await service.createWorkerGroup(options); } async function removeDefaultWorkerGroupFromProject(projectId: string) { diff --git a/apps/webapp/app/services/personalAccessToken.server.ts b/apps/webapp/app/services/personalAccessToken.server.ts index ebe8bc31ff..cceb576c9d 100644 --- a/apps/webapp/app/services/personalAccessToken.server.ts +++ b/apps/webapp/app/services/personalAccessToken.server.ts @@ -1,4 +1,4 @@ -import { type PersonalAccessToken } from "@trigger.dev/database"; +import { type PersonalAccessToken, type User } from "@trigger.dev/database"; import { customAlphabet, nanoid } from "nanoid"; import { z } from "zod"; import { prisma } from "~/db.server"; @@ -118,6 +118,59 @@ export async function authenticateApiRequestWithPersonalAccessToken( return authenticatePersonalAccessToken(token); } +export type AdminAuthenticationResult = + | { ok: true; user: User } + | { ok: false; status: 401 | 403; message: string }; + +/** + * Authenticates a request via personal access token and checks the user is + * an admin. Returns a discriminated result so callers can shape the failure + * (throw a Response, wrap in neverthrow, return JSON, etc.) to fit their + * context. See `requireAdminApiRequest` for the Remix loader/action wrapper. + */ +export async function authenticateAdminRequest( + request: Request +): Promise { + const authResult = await authenticateApiRequestWithPersonalAccessToken(request); + + if (!authResult) { + return { ok: false, status: 401, message: "Invalid or Missing API key" }; + } + + const user = await prisma.user.findFirst({ + where: { id: authResult.userId }, + }); + + if (!user) { + return { ok: false, status: 401, message: "Invalid or Missing API key" }; + } + + if (!user.admin) { + return { ok: false, status: 403, message: "You must be an admin to perform this action" }; + } + + return { ok: true, user }; +} + +/** + * Remix loader/action wrapper around `authenticateAdminRequest` that throws + * a Response on failure so routes can `await` without handling the error + * branch. Uses `new Response` directly to avoid coupling this module to + * `@remix-run/server-runtime`. + */ +export async function requireAdminApiRequest(request: Request): Promise { + const result = await authenticateAdminRequest(request); + + if (!result.ok) { + throw new Response(JSON.stringify({ error: result.message }), { + status: result.status, + headers: { "Content-Type": "application/json" }, + }); + } + + return result.user; +} + function getPersonalAccessTokenFromRequest(request: Request) { const rawAuthorization = request.headers.get("Authorization"); diff --git a/apps/webapp/app/v3/services/worker/workerGroupService.server.ts b/apps/webapp/app/v3/services/worker/workerGroupService.server.ts index 6a2c19cf24..fc280e8165 100644 --- a/apps/webapp/app/v3/services/worker/workerGroupService.server.ts +++ b/apps/webapp/app/v3/services/worker/workerGroupService.server.ts @@ -1,4 +1,4 @@ -import { WorkerInstanceGroup, WorkerInstanceGroupType } from "@trigger.dev/database"; +import { WorkerInstanceGroup, WorkerInstanceGroupType, WorkloadType } from "@trigger.dev/database"; import { WithRunEngine } from "../baseService.server"; import { WorkerGroupTokenService } from "./workerGroupTokenService.server"; import { logger } from "~/services/logger.server"; @@ -14,11 +14,25 @@ export class WorkerGroupService extends WithRunEngine { organizationId, name, description, + type, + hidden, + workloadType, + cloudProvider, + location, + staticIPs, + enableFastPath, }: { projectId?: string; organizationId?: string; name?: string; description?: string; + type?: WorkerInstanceGroupType; + hidden?: boolean; + workloadType?: WorkloadType; + cloudProvider?: string; + location?: string; + staticIPs?: string; + enableFastPath?: boolean; }) { if (!name) { name = await this.generateWorkerName({ projectId }); @@ -30,15 +44,24 @@ export class WorkerGroupService extends WithRunEngine { }); const token = await tokenService.createToken(); + const resolvedType = + type ?? (projectId ? WorkerInstanceGroupType.UNMANAGED : WorkerInstanceGroupType.MANAGED); + const workerGroup = await this._prisma.workerInstanceGroup.create({ data: { projectId, organizationId, - type: projectId ? WorkerInstanceGroupType.UNMANAGED : WorkerInstanceGroupType.MANAGED, + type: resolvedType, masterQueue: this.generateMasterQueueName({ projectId, name }), tokenId: token.id, description, name, + hidden, + workloadType, + cloudProvider, + location, + staticIPs, + enableFastPath, }, });