From 14dd76db8bca87edb132814b260e960b75378803 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 10 Sep 2025 17:28:00 -0700 Subject: [PATCH] Apply blueprint over api call --- messages/en-US.json | 1 + server/auth/actions.ts | 3 +- server/routers/integration.ts | 7 ++ server/routers/org/applyBlueprint.ts | 122 ++++++++++++++++++++++++ server/routers/org/index.ts | 1 + src/components/PermissionsSelectBox.tsx | 3 +- 6 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 server/routers/org/applyBlueprint.ts diff --git a/messages/en-US.json b/messages/en-US.json index 24448e49..24601474 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -997,6 +997,7 @@ "actionDeleteSite": "Delete Site", "actionGetSite": "Get Site", "actionListSites": "List Sites", + "actionApplyBlueprint": "Apply Blueprint", "setupToken": "Setup Token", "setupTokenDescription": "Enter the setup token from the server console.", "setupTokenRequired": "Setup token is required", diff --git a/server/auth/actions.ts b/server/auth/actions.ts index ecbbd058..6b6c9bf4 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -101,7 +101,8 @@ export enum ActionsEnum { createOrgDomain = "createOrgDomain", deleteOrgDomain = "deleteOrgDomain", restartOrgDomain = "restartOrgDomain", - updateOrgUser = "updateOrgUser" + updateOrgUser = "updateOrgUser", + applyBlueprint = "applyBlueprint" } export async function checkUserActionPermission( diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 69bdbb42..6a43aaa7 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -644,3 +644,10 @@ authenticated.post( verifyApiKeyHasAction(ActionsEnum.updateClient), client.updateClient ); + +authenticated.put( + "/org/:orgId/blueprint", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.applyBlueprint), + org.applyBlueprint +); \ No newline at end of file diff --git a/server/routers/org/applyBlueprint.ts b/server/routers/org/applyBlueprint.ts new file mode 100644 index 00000000..7fba3f3b --- /dev/null +++ b/server/routers/org/applyBlueprint.ts @@ -0,0 +1,122 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { eq } from "drizzle-orm"; +import { + apiKeyOrg, + apiKeys, + domains, + Org, + orgDomains, + orgs, + roleActions, + roles, + userOrgs, + users, + actions +} from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import config from "@server/lib/config"; +import { fromError } from "zod-validation-error"; +import { defaultRoleAllowedActions } from "../role"; +import { OpenAPITags, registry } from "@server/openApi"; +import { isValidCIDR } from "@server/lib/validators"; +import { applyBlueprint as applyBlueprintFunc } from "@server/lib/blueprints/applyBlueprint"; + +const applyBlueprintSchema = z + .object({ + blueprint: z.string() + }) + .strict(); + +const applyBlueprintParamsSchema = z + .object({ + orgId: z.string() + }) + .strict(); + +registry.registerPath({ + method: "put", + path: "/org/{orgId}/blueprint", + description: "Apply a blueprint to an organization", + tags: [OpenAPITags.Org], + request: { + body: { + content: { + "application/json": { + schema: applyBlueprintSchema + } + } + } + }, + responses: {} +}); + +export async function applyBlueprint( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = applyBlueprintParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId } = parsedParams.data; + + const parsedBody = applyBlueprintSchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { blueprint } = parsedBody.data; + + if (!blueprint) { + logger.warn("No blueprint provided"); + return; + } + + logger.debug(`Received blueprint: ${blueprint}`); + + try { + const blueprintParsed = JSON.parse(blueprint); + // Update the blueprint in the database + await applyBlueprintFunc(orgId, blueprintParsed); + } catch (error) { + logger.error(`Failed to update database from config: ${error}`); + return next( + createHttpError( + HttpCode.BAD_REQUEST, + `Failed to update database from config: ${error}` + ) + ); + } + + return response(res, { + data: null, + success: true, + error: false, + message: "Blueprint applied successfully", + status: HttpCode.CREATED + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/org/index.ts b/server/routers/org/index.ts index c9a44d8d..754def66 100644 --- a/server/routers/org/index.ts +++ b/server/routers/org/index.ts @@ -7,3 +7,4 @@ export * from "./checkId"; export * from "./getOrgOverview"; export * from "./listOrgs"; export * from "./pickOrgDefaults"; +export * from "./applyBlueprint"; \ No newline at end of file diff --git a/src/components/PermissionsSelectBox.tsx b/src/components/PermissionsSelectBox.tsx index 3334cca5..4760aeef 100644 --- a/src/components/PermissionsSelectBox.tsx +++ b/src/components/PermissionsSelectBox.tsx @@ -29,7 +29,8 @@ function getActionsCategories(root: boolean) { [t('actionListUsers')]: "listUsers", [t('actionListOrgDomains')]: "listOrgDomains", [t('updateOrgUser')]: "updateOrgUser", - [t('createOrgUser')]: "createOrgUser" + [t('createOrgUser')]: "createOrgUser", + [t('actionApplyBlueprint')]: "applyBlueprint", }, Site: {