From 259cea1c42efe748bd3c0aa800598d7e35fb9b14 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 22 Oct 2025 23:49:43 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20add=20API=20endpoint=20for=20listin?= =?UTF-8?q?g=20blueprints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/auth/actions.ts | 4 +- server/routers/blueprints/index.ts | 1 + server/routers/blueprints/listBluePrints.ts | 129 ++++++++++++++++++++ server/routers/external.ts | 10 +- 4 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 server/routers/blueprints/index.ts create mode 100644 server/routers/blueprints/listBluePrints.ts diff --git a/server/auth/actions.ts b/server/auth/actions.ts index e48bc502..132eec7b 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -116,6 +116,9 @@ export enum ActionsEnum { updateLoginPage = "updateLoginPage", getLoginPage = "getLoginPage", deleteLoginPage = "deleteLoginPage", + + // blueprints + listBlueprints = "listBlueprints", applyBlueprint = "applyBlueprint" } @@ -193,7 +196,6 @@ export async function checkUserActionPermission( .limit(1); return roleActionPermission.length > 0; - } catch (error) { console.error("Error checking user action permission:", error); throw createHttpError( diff --git a/server/routers/blueprints/index.ts b/server/routers/blueprints/index.ts new file mode 100644 index 00000000..7182d5f9 --- /dev/null +++ b/server/routers/blueprints/index.ts @@ -0,0 +1 @@ +export * from "./listBluePrints"; diff --git a/server/routers/blueprints/listBluePrints.ts b/server/routers/blueprints/listBluePrints.ts new file mode 100644 index 00000000..531ced8a --- /dev/null +++ b/server/routers/blueprints/listBluePrints.ts @@ -0,0 +1,129 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, blueprints, orgs } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { sql, eq, or, inArray, and, count } from "drizzle-orm"; +import logger from "@server/logger"; +import { fromZodError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; +import { warn } from "console"; + +const listBluePrintsParamsSchema = z + .object({ + orgId: z.string() + }) + .strict(); + +const listBluePrintsSchema = z + .object({ + limit: z + .string() + .optional() + .default("1000") + .transform(Number) + .pipe(z.number().int().nonnegative()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.number().int().nonnegative()) + }) + .strict(); + +async function queryBlueprints(orgId: string, limit: number, offset: number) { + const res = await db + .select({ + blueprintId: blueprints.blueprintId, + name: blueprints.name, + source: blueprints.source, + succeeded: blueprints.succeeded + }) + .from(blueprints) + .leftJoin(orgs, eq(blueprints.orgId, orgs.orgId)) + .limit(limit) + .offset(offset); + return res; +} + +export type ListBlueprintsResponse = { + domains: NonNullable>>; + pagination: { total: number; limit: number; offset: number }; +}; + +registry.registerPath({ + method: "get", + path: "/org/{orgId}/blueprints", + description: "List all blueprints for a organization.", + tags: [OpenAPITags.Org], + request: { + params: z.object({ + orgId: z.string() + }), + query: listBluePrintsSchema + }, + responses: {} +}); + +export async function listBlueprints( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedQuery = listBluePrintsSchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromZodError(parsedQuery.error) + ) + ); + } + const { limit, offset } = parsedQuery.data; + + const parsedParams = listBluePrintsParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromZodError(parsedParams.error) + ) + ); + } + + const { orgId } = parsedParams.data; + + const blueprintsList = await queryBlueprints( + orgId.toString(), + limit, + offset + ); + + const [{ count }] = await db + .select({ count: sql`count(*)` }) + .from(blueprints); + + return response(res, { + data: { + domains: blueprintsList, + pagination: { + total: count, + limit, + offset + } + }, + success: true, + error: false, + message: "Blueprints retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/external.ts b/server/routers/external.ts index 8bd72f62..a5ef3ba3 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -13,6 +13,7 @@ import * as siteResource from "./siteResource"; import * as supporterKey from "./supporterKey"; import * as accessToken from "./accessToken"; import * as idp from "./idp"; +import * as blueprints from "./blueprints"; import * as apiKeys from "./apiKeys"; import HttpCode from "@server/types/HttpCode"; import { @@ -675,8 +676,6 @@ authenticated.post( idp.updateOidcIdp ); - - authenticated.delete("/idp/:idpId", verifyUserIsServerAdmin, idp.deleteIdp); authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp); @@ -705,7 +704,6 @@ authenticated.get( idp.listIdpOrgPolicies ); - authenticated.get("/idp", idp.listIdps); // anyone can see this; it's just a list of idp names and ids authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp); @@ -814,6 +812,12 @@ authenticated.delete( domain.deleteAccountDomain ); +authenticated.get( + "/org/:orgId/blueprints", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.listBlueprints), + blueprints.listBlueprints +); // Auth routes export const authRouter = Router(); unauthenticated.use("/auth", authRouter);