Skip to content

Commit c3619bf

Browse files
committed
feat(webapp): extend admin workers endpoint and add PAT admin helper
Adds a GET loader to the admin worker groups endpoint, exposes type, hidden, workloadType, cloudProvider, location, staticIPs, and enableFastPath on creation, and introduces authenticateAdminRequest and requireAdminApiRequest helpers in personalAccessToken.server.ts. The generic authenticateAdminRequest returns a discriminated result so callers can shape failures to fit their context; requireAdminApiRequest is the Remix loader/action wrapper that throws a Response.
1 parent 7c95207 commit c3619bf

File tree

4 files changed

+136
-28
lines changed

4 files changed

+136
-28
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
Admin worker groups API: add GET loader and expose more fields on POST.

apps/webapp/app/routes/admin.api.v1.workers.ts

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
1+
import {
2+
type ActionFunctionArgs,
3+
type LoaderFunctionArgs,
4+
json,
5+
} from "@remix-run/server-runtime";
26
import { tryCatch } from "@trigger.dev/core";
3-
import { type Project } from "@trigger.dev/database";
7+
import { type Project, WorkerInstanceGroupType, WorkloadType } from "@trigger.dev/database";
48
import { z } from "zod";
59
import { prisma } from "~/db.server";
6-
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
10+
import { requireAdminApiRequest } from "~/services/personalAccessToken.server";
711
import { WorkerGroupService } from "~/v3/services/worker/workerGroupService.server";
812

913
const RequestBodySchema = z.object({
@@ -12,34 +16,44 @@ const RequestBodySchema = z.object({
1216
projectId: z.string().optional(),
1317
makeDefaultForProject: z.boolean().default(false),
1418
removeDefaultFromProject: z.boolean().default(false),
19+
type: z.nativeEnum(WorkerInstanceGroupType).optional(),
20+
hidden: z.boolean().optional(),
21+
workloadType: z.nativeEnum(WorkloadType).optional(),
22+
cloudProvider: z.string().optional(),
23+
location: z.string().optional(),
24+
staticIPs: z.string().optional(),
25+
enableFastPath: z.boolean().optional(),
1526
});
1627

17-
export async function action({ request }: ActionFunctionArgs) {
18-
// Next authenticate the request
19-
const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request);
20-
21-
if (!authenticationResult) {
22-
return json({ error: "Invalid or Missing API key" }, { status: 401 });
23-
}
28+
export async function loader({ request }: LoaderFunctionArgs) {
29+
await requireAdminApiRequest(request);
2430

25-
const user = await prisma.user.findFirst({
26-
where: {
27-
id: authenticationResult.userId,
28-
},
31+
const workerGroups = await prisma.workerInstanceGroup.findMany({
32+
orderBy: { createdAt: "asc" },
2933
});
3034

31-
if (!user) {
32-
return json({ error: "Invalid or Missing API key" }, { status: 401 });
33-
}
35+
return json({ workerGroups });
36+
}
3437

35-
if (!user.admin) {
36-
return json({ error: "You must be an admin to perform this action" }, { status: 403 });
37-
}
38+
export async function action({ request }: ActionFunctionArgs) {
39+
await requireAdminApiRequest(request);
3840

3941
try {
4042
const rawBody = await request.json();
41-
const { name, description, projectId, makeDefaultForProject, removeDefaultFromProject } =
42-
RequestBodySchema.parse(rawBody ?? {});
43+
const {
44+
name,
45+
description,
46+
projectId,
47+
makeDefaultForProject,
48+
removeDefaultFromProject,
49+
type,
50+
hidden,
51+
workloadType,
52+
cloudProvider,
53+
location,
54+
staticIPs,
55+
enableFastPath,
56+
} = RequestBodySchema.parse(rawBody ?? {});
4357

4458
if (removeDefaultFromProject) {
4559
if (!projectId) {
@@ -74,7 +88,17 @@ export async function action({ request }: ActionFunctionArgs) {
7488
});
7589

7690
if (!existingWorkerGroup) {
77-
const { workerGroup, token } = await createWorkerGroup(name, description);
91+
const { workerGroup, token } = await createWorkerGroup({
92+
name,
93+
description,
94+
type,
95+
hidden,
96+
workloadType,
97+
cloudProvider,
98+
location,
99+
staticIPs,
100+
enableFastPath,
101+
});
78102

79103
if (!makeDefaultForProject) {
80104
return json({
@@ -150,9 +174,11 @@ export async function action({ request }: ActionFunctionArgs) {
150174
}
151175
}
152176

153-
async function createWorkerGroup(name: string | undefined, description: string | undefined) {
177+
async function createWorkerGroup(
178+
options: Parameters<WorkerGroupService["createWorkerGroup"]>[0]
179+
) {
154180
const service = new WorkerGroupService();
155-
return await service.createWorkerGroup({ name, description });
181+
return await service.createWorkerGroup(options);
156182
}
157183

158184
async function removeDefaultWorkerGroupFromProject(projectId: string) {

apps/webapp/app/services/personalAccessToken.server.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type PersonalAccessToken } from "@trigger.dev/database";
1+
import { type PersonalAccessToken, type User } from "@trigger.dev/database";
22
import { customAlphabet, nanoid } from "nanoid";
33
import { z } from "zod";
44
import { prisma } from "~/db.server";
@@ -118,6 +118,59 @@ export async function authenticateApiRequestWithPersonalAccessToken(
118118
return authenticatePersonalAccessToken(token);
119119
}
120120

121+
export type AdminAuthenticationResult =
122+
| { ok: true; user: User }
123+
| { ok: false; status: 401 | 403; message: string };
124+
125+
/**
126+
* Authenticates a request via personal access token and checks the user is
127+
* an admin. Returns a discriminated result so callers can shape the failure
128+
* (throw a Response, wrap in neverthrow, return JSON, etc.) to fit their
129+
* context. See `requireAdminApiRequest` for the Remix loader/action wrapper.
130+
*/
131+
export async function authenticateAdminRequest(
132+
request: Request
133+
): Promise<AdminAuthenticationResult> {
134+
const authResult = await authenticateApiRequestWithPersonalAccessToken(request);
135+
136+
if (!authResult) {
137+
return { ok: false, status: 401, message: "Invalid or Missing API key" };
138+
}
139+
140+
const user = await prisma.user.findFirst({
141+
where: { id: authResult.userId },
142+
});
143+
144+
if (!user) {
145+
return { ok: false, status: 401, message: "Invalid or Missing API key" };
146+
}
147+
148+
if (!user.admin) {
149+
return { ok: false, status: 403, message: "You must be an admin to perform this action" };
150+
}
151+
152+
return { ok: true, user };
153+
}
154+
155+
/**
156+
* Remix loader/action wrapper around `authenticateAdminRequest` that throws
157+
* a Response on failure so routes can `await` without handling the error
158+
* branch. Uses `new Response` directly to avoid coupling this module to
159+
* `@remix-run/server-runtime`.
160+
*/
161+
export async function requireAdminApiRequest(request: Request): Promise<User> {
162+
const result = await authenticateAdminRequest(request);
163+
164+
if (!result.ok) {
165+
throw new Response(JSON.stringify({ error: result.message }), {
166+
status: result.status,
167+
headers: { "Content-Type": "application/json" },
168+
});
169+
}
170+
171+
return result.user;
172+
}
173+
121174
function getPersonalAccessTokenFromRequest(request: Request) {
122175
const rawAuthorization = request.headers.get("Authorization");
123176

apps/webapp/app/v3/services/worker/workerGroupService.server.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { WorkerInstanceGroup, WorkerInstanceGroupType } from "@trigger.dev/database";
1+
import { WorkerInstanceGroup, WorkerInstanceGroupType, WorkloadType } from "@trigger.dev/database";
22
import { WithRunEngine } from "../baseService.server";
33
import { WorkerGroupTokenService } from "./workerGroupTokenService.server";
44
import { logger } from "~/services/logger.server";
@@ -14,11 +14,25 @@ export class WorkerGroupService extends WithRunEngine {
1414
organizationId,
1515
name,
1616
description,
17+
type,
18+
hidden,
19+
workloadType,
20+
cloudProvider,
21+
location,
22+
staticIPs,
23+
enableFastPath,
1724
}: {
1825
projectId?: string;
1926
organizationId?: string;
2027
name?: string;
2128
description?: string;
29+
type?: WorkerInstanceGroupType;
30+
hidden?: boolean;
31+
workloadType?: WorkloadType;
32+
cloudProvider?: string;
33+
location?: string;
34+
staticIPs?: string;
35+
enableFastPath?: boolean;
2236
}) {
2337
if (!name) {
2438
name = await this.generateWorkerName({ projectId });
@@ -30,15 +44,24 @@ export class WorkerGroupService extends WithRunEngine {
3044
});
3145
const token = await tokenService.createToken();
3246

47+
const resolvedType =
48+
type ?? (projectId ? WorkerInstanceGroupType.UNMANAGED : WorkerInstanceGroupType.MANAGED);
49+
3350
const workerGroup = await this._prisma.workerInstanceGroup.create({
3451
data: {
3552
projectId,
3653
organizationId,
37-
type: projectId ? WorkerInstanceGroupType.UNMANAGED : WorkerInstanceGroupType.MANAGED,
54+
type: resolvedType,
3855
masterQueue: this.generateMasterQueueName({ projectId, name }),
3956
tokenId: token.id,
4057
description,
4158
name,
59+
hidden,
60+
workloadType,
61+
cloudProvider,
62+
location,
63+
staticIPs,
64+
enableFastPath,
4265
},
4366
});
4467

0 commit comments

Comments
 (0)