From 56e1684e2eabad9e05c8d234d0c7582c1f81a462 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 31 Mar 2025 16:21:01 -0400 Subject: [PATCH] Update api endpoints for new association --- server/auth/actions.ts | 1 + server/routers/client/createClient.ts | 55 ++++----- server/routers/client/deleteClient.ts | 16 ++- server/routers/client/index.ts | 1 + server/routers/client/pickClientDefaults.ts | 95 --------------- server/routers/client/updateClient.ts | 124 ++++++++++++++++++++ server/routers/external.ts | 13 +- 7 files changed, 172 insertions(+), 133 deletions(-) create mode 100644 server/routers/client/updateClient.ts diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 26242b4a..815fa137 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -64,6 +64,7 @@ export enum ActionsEnum { updateResourceRule = "updateResourceRule", createClient = "createClient", deleteClient = "deleteClient", + updateClient = "updateClient", listClients = "listClients", listOrgDomains = "listOrgDomains", } diff --git a/server/routers/client/createClient.ts b/server/routers/client/createClient.ts index 19b41b66..8bc2af49 100644 --- a/server/routers/client/createClient.ts +++ b/server/routers/client/createClient.ts @@ -3,15 +3,12 @@ import { z } from "zod"; import { db } from "@server/db"; import { roles, - userSites, - sites, - roleSites, - Site, Client, clients, roleClients, userClients, - olms + olms, + clientSites } from "@server/db/schema"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -21,21 +18,19 @@ import { eq, and } from "drizzle-orm"; import { fromError } from "zod-validation-error"; import moment from "moment"; import { hashPassword } from "@server/auth/password"; +import { getNextAvailableClientSubnet } from "@server/lib/ip"; +import config from "@server/lib/config"; const createClientParamsSchema = z .object({ - siteId: z - .string() - .transform((val) => parseInt(val)) - .pipe(z.number()) + orgId: z.string() }) .strict(); const createClientSchema = z .object({ name: z.string().min(1).max(255), - siteId: z.number().int().positive(), - subnet: z.string(), + siteIds: z.array(z.string().transform(Number).pipe(z.number())), olmId: z.string(), secret: z.string(), type: z.enum(["olm"]) @@ -62,7 +57,7 @@ export async function createClient( ); } - const { name, type, siteId, subnet, olmId, secret } = + const { name, type, siteIds, olmId, secret } = parsedBody.data; const parsedParams = createClientParamsSchema.safeParse(req.params); @@ -75,16 +70,7 @@ export async function createClient( ); } - const { siteId: paramSiteId } = parsedParams.data; - - if (siteId != paramSiteId) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Site ID in body does not match site ID in URL" - ) - ); - } + const { orgId } = parsedParams.data; if (!req.userOrgRoleId) { return next( @@ -92,21 +78,16 @@ export async function createClient( ); } - const [site] = await db - .select() - .from(sites) - .where(eq(sites.siteId, siteId)); + const newSubnet = await getNextAvailableClientSubnet(orgId); - if (!site) { - return next(createHttpError(HttpCode.NOT_FOUND, "Site not found")); - } + const subnet = `${newSubnet.split("/")[0]}/${config.getRawConfig().orgs.block_size}` // we want the block size of the whole org await db.transaction(async (trx) => { const adminRole = await trx .select() .from(roles) .where( - and(eq(roles.isAdmin, true), eq(roles.orgId, site.orgId)) + and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)) ) .limit(1); @@ -120,7 +101,7 @@ export async function createClient( const [newClient] = await trx .insert(clients) .values({ - orgId: site.orgId, + orgId, name, subnet, type @@ -140,6 +121,16 @@ export async function createClient( }); } + // Create site to client associations + if (siteIds && siteIds.length > 0) { + await trx.insert(clientSites).values( + siteIds.map(siteId => ({ + clientId: newClient.clientId, + siteId + })) + ); + } + const secretHash = await hashPassword(secret); await trx.insert(olms).values({ @@ -163,4 +154,4 @@ export async function createClient( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } -} +} \ No newline at end of file diff --git a/server/routers/client/deleteClient.ts b/server/routers/client/deleteClient.ts index 9f1fb93e..f99f6fee 100644 --- a/server/routers/client/deleteClient.ts +++ b/server/routers/client/deleteClient.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { clients, sites } from "@server/db/schema"; +import { clients, clientSites } from "@server/db/schema"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -48,7 +48,17 @@ export async function deleteClient( ); } - await db.delete(clients).where(eq(clients.clientId, clientId)); + await db.transaction(async (trx) => { + // Delete the client-site associations first + await trx + .delete(clientSites) + .where(eq(clientSites.clientId, clientId)); + + // Then delete the client itself + await trx + .delete(clients) + .where(eq(clients.clientId, clientId)); + }); return response(res, { data: null, @@ -63,4 +73,4 @@ export async function deleteClient( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } -} +} \ No newline at end of file diff --git a/server/routers/client/index.ts b/server/routers/client/index.ts index 686d08e9..144957ed 100644 --- a/server/routers/client/index.ts +++ b/server/routers/client/index.ts @@ -2,3 +2,4 @@ export * from "./pickClientDefaults"; export * from "./createClient"; export * from "./deleteClient"; export * from "./listClients"; +export * from "./updateClient"; \ No newline at end of file diff --git a/server/routers/client/pickClientDefaults.ts b/server/routers/client/pickClientDefaults.ts index b56a4bf4..231bc409 100644 --- a/server/routers/client/pickClientDefaults.ts +++ b/server/routers/client/pickClientDefaults.ts @@ -1,32 +1,11 @@ import { Request, Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { clients, exitNodes, olms, sites } from "@server/db/schema"; -import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; -import { findNextAvailableCidr, getNextAvailableClientSubnet } from "@server/lib/ip"; import { generateId } from "@server/auth/sessions/app"; -import config from "@server/lib/config"; -import { z } from "zod"; -import { fromError } from "zod-validation-error"; - -const getSiteSchema = z - .object({ - siteId: z.string().transform(Number).pipe(z.number()) - }) - .strict(); export type PickClientDefaultsResponse = { - exitNodeId: number; - siteId: number; - address: string; - publicKey: string; - name: string; - listenPort: number; - endpoint: string; - subnet: string; olmId: string; olmSecret: string; }; @@ -37,85 +16,11 @@ export async function pickClientDefaults( next: NextFunction ): Promise { try { - const parsedParams = getSiteSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const { siteId } = parsedParams.data; - - const [site] = await db - .select() - .from(sites) - .where(eq(sites.siteId, siteId)); - - if (!site) { - return next(createHttpError(HttpCode.NOT_FOUND, "Site not found")); - } - - if (site.type !== "newt") { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Site is not a newt site" - ) - ); - } - - // TODO: more intelligent way to pick the exit node - - // make sure there is an exit node by counting the exit nodes table - const nodes = await db.select().from(exitNodes); - if (nodes.length === 0) { - return next( - createHttpError(HttpCode.NOT_FOUND, "No exit nodes available") - ); - } - - // get the first exit node - const exitNode = nodes[0]; - - // make sure all the required fields are present - const sitesRequiredFields = z.object({ - address: z.string(), - publicKey: z.string(), - listenPort: z.number(), - endpoint: z.string() - }); - - const parsedSite = sitesRequiredFields.safeParse(site); - if (!parsedSite.success) { - logger.error("Unable to pick client defaults because: " + fromError(parsedSite.error).toString()); - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Site is not configured to accept client connectivity" - ) - ); - } - - const { address, publicKey, listenPort, endpoint } = parsedSite.data; - - const newSubnet = await getNextAvailableClientSubnet(site.orgId); - const olmId = generateId(15); const secret = generateId(48); return response(res, { data: { - exitNodeId: exitNode.exitNodeId, - siteId: site.siteId, - address: address, - publicKey: publicKey, - name: site.name, - listenPort: listenPort, - endpoint: endpoint, - subnet: `${newSubnet.split("/")[0]}/${config.getRawConfig().orgs.block_size}`, // we want the block size of the whole org olmId: olmId, olmSecret: secret }, diff --git a/server/routers/client/updateClient.ts b/server/routers/client/updateClient.ts new file mode 100644 index 00000000..9bdd4295 --- /dev/null +++ b/server/routers/client/updateClient.ts @@ -0,0 +1,124 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { + clients, + clientSites +} from "@server/db/schema"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { eq, and } from "drizzle-orm"; +import { fromError } from "zod-validation-error"; + +const updateClientParamsSchema = z + .object({ + clientId: z.string().transform(Number).pipe(z.number().int().positive()) + }) + .strict(); + +const updateClientSchema = z + .object({ + name: z.string().min(1).max(255).optional(), + siteIds: z.array(z.string().transform(Number).pipe(z.number())).optional() + }) + .strict(); + +export type UpdateClientBody = z.infer; + +export async function updateClient( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedBody = updateClientSchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { name, siteIds } = parsedBody.data; + + const parsedParams = updateClientParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { clientId } = parsedParams.data; + + // Fetch the client to make sure it exists and the user has access to it + const [client] = await db + .select() + .from(clients) + .where(eq(clients.clientId, clientId)) + .limit(1); + + if (!client) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Client with ID ${clientId} not found` + ) + ); + } + + await db.transaction(async (trx) => { + // Update client name if provided + if (name) { + await trx + .update(clients) + .set({ name }) + .where(eq(clients.clientId, clientId)); + } + + // Update site associations if provided + if (siteIds) { + // Delete existing site associations + await trx + .delete(clientSites) + .where(eq(clientSites.clientId, clientId)); + + // Create new site associations + if (siteIds.length > 0) { + await trx.insert(clientSites).values( + siteIds.map(siteId => ({ + clientId, + siteId + })) + ); + } + } + + // Fetch the updated client + const [updatedClient] = await trx + .select() + .from(clients) + .where(eq(clients.clientId, clientId)) + .limit(1); + + return response(res, { + data: updatedClient, + success: true, + error: false, + message: "Client updated successfully", + status: HttpCode.OK + }); + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} \ No newline at end of file diff --git a/server/routers/external.ts b/server/routers/external.ts index 6b01aacc..5001daf5 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -102,7 +102,7 @@ authenticated.get( ); authenticated.get( - "/site/:siteId/pick-client-defaults", + "/pick-client-defaults", verifySiteAccess, verifyUserHasAction(ActionsEnum.createClient), client.pickClientDefaults @@ -116,8 +116,8 @@ authenticated.get( ); authenticated.put( - "/site/:siteId/client", - verifySiteAccess, + "/org/:orgId/client", + verifyOrgAccess, verifyUserHasAction(ActionsEnum.createClient), client.createClient ); @@ -129,6 +129,13 @@ authenticated.delete( client.deleteClient ); +authenticated.post( + "/client/:clientId", + verifyClientAccess, // this will check if the user has access to the client + verifyUserHasAction(ActionsEnum.updateClient), // this will check if the user has permission to update the client + client.updateClient +); + // authenticated.get( // "/site/:siteId/roles", // verifySiteAccess,