From 35ccdd30145b8c9307d81c582563b144fc90eef3 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Fri, 21 Feb 2025 14:39:10 -0500 Subject: [PATCH] add createClient for testing --- server/db/schema.ts | 4 + server/routers/client/createClient.ts | 171 ++++++++++++++++++++++++++ server/routers/client/deleteClient.ts | 94 ++++++++++++++ server/routers/client/index.ts | 2 + server/routers/external.ts | 7 ++ server/routers/site/createSite.ts | 2 +- 6 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 server/routers/client/createClient.ts create mode 100644 server/routers/client/deleteClient.ts diff --git a/server/db/schema.ts b/server/db/schema.ts index 931bf6a4..02602e32 100644 --- a/server/db/schema.ts +++ b/server/db/schema.ts @@ -121,6 +121,10 @@ export const clients = sqliteTable("clients", { siteId: integer("siteId").references(() => sites.siteId, { onDelete: "cascade" }), + orgId: text("orgId").references(() => orgs.orgId, { + onDelete: "cascade" + }), + name: text("name").notNull(), pubKey: text("pubKey"), subnet: text("subnet").notNull(), megabytesIn: integer("bytesIn"), diff --git a/server/routers/client/createClient.ts b/server/routers/client/createClient.ts new file mode 100644 index 00000000..85f1735e --- /dev/null +++ b/server/routers/client/createClient.ts @@ -0,0 +1,171 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { + roles, + userSites, + sites, + roleSites, + Site, + Client, + clients, + roleClients, + userClients, + olms +} 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 { addPeer } from "../gerbil/peers"; +import { fromError } from "zod-validation-error"; +import { newts } from "@server/db/schema"; +import moment from "moment"; +import { hashPassword } from "@server/auth/password"; + +const createClientParamsSchema = z + .object({ + siteId: z + .string() + .transform((val) => parseInt(val)) + .pipe(z.number()) + }) + .strict(); + +const createClientSchema = z + .object({ + name: z.string().min(1).max(255), + siteId: z.number().int().positive(), + pubKey: z.string(), + subnet: z.string(), + olmId: z.string(), + secret: z.string(), + type: z.enum(["olm"]) + }) + .strict(); + +export type CreateClientBody = z.infer; + +export type CreateClientResponse = Client; + +export async function createClient( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedBody = createClientSchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { name, type, siteId, pubKey, subnet, olmId, secret } = + parsedBody.data; + + const parsedParams = createClientParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + 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" + ) + ); + } + + if (!req.userOrgRoleId) { + return next( + createHttpError(HttpCode.FORBIDDEN, "User does not have a role") + ); + } + + const [site] = await db + .select() + .from(sites) + .where(eq(sites.siteId, siteId)); + + if (!site) { + return next(createHttpError(HttpCode.NOT_FOUND, "Site not found")); + } + + await db.transaction(async (trx) => { + const adminRole = await trx + .select() + .from(roles) + .where( + and(eq(roles.isAdmin, true), eq(roles.orgId, site.orgId)) + ) + .limit(1); + + if (adminRole.length === 0) { + trx.rollback(); + return next( + createHttpError(HttpCode.NOT_FOUND, `Admin role not found`) + ); + } + + const [newClient] = await trx + .insert(clients) + .values({ + siteId, + orgId: site.orgId, + name, + pubKey, + subnet, + type + }) + .returning(); + + await trx.insert(roleClients).values({ + roleId: adminRole[0].roleId, + clientId: newClient.clientId + }); + + if (req.userOrgRoleId != adminRole[0].roleId) { + // make sure the user can access the site + trx.insert(userClients).values({ + userId: req.user?.userId!, + clientId: newClient.clientId + }); + } + + const secretHash = await hashPassword(secret); + + await trx.insert(olms).values({ + olmId, + secretHash, + clientId: newClient.clientId, + dateCreated: moment().toISOString() + }); + + return response(res, { + data: newClient, + success: true, + error: false, + message: "Site created successfully", + status: HttpCode.CREATED + }); + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/client/deleteClient.ts b/server/routers/client/deleteClient.ts new file mode 100644 index 00000000..6ee12734 --- /dev/null +++ b/server/routers/client/deleteClient.ts @@ -0,0 +1,94 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { newts, newtSessions, 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 { deletePeer } from "../gerbil/peers"; +import { fromError } from "zod-validation-error"; +import { sendToClient } from "../ws"; + +const deleteClientSchema = z + .object({ + siteId: z.string().transform(Number).pipe(z.number().int().positive()) + }) + .strict(); + +export async function deleteSite( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = deleteClientSchema.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)) + .limit(1); + + if (!site) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Site with ID ${siteId} not found` + ) + ); + } + + await db.transaction(async (trx) => { + if (site.pubKey) { + if (site.type == "wireguard") { + await deletePeer(site.exitNodeId!, site.pubKey); + } else if (site.type == "newt") { + // get the newt on the site by querying the newt table for siteId + const [deletedNewt] = await trx + .delete(newts) + .where(eq(newts.siteId, siteId)) + .returning(); + if (deletedNewt) { + const payload = { + type: `newt/terminate`, + data: {} + }; + sendToClient(deletedNewt.newtId, payload); + + // delete all of the sessions for the newt + await trx + .delete(newtSessions) + .where(eq(newtSessions.newtId, deletedNewt.newtId)); + } + } + } + + await trx.delete(sites).where(eq(sites.siteId, siteId)); + }); + + return response(res, { + data: null, + success: true, + error: false, + message: "Site deleted successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/client/index.ts b/server/routers/client/index.ts index 5b493724..98abfe06 100644 --- a/server/routers/client/index.ts +++ b/server/routers/client/index.ts @@ -1 +1,3 @@ export * from "./pickClientDefaults"; +export * from "./createClient"; +export * from "./deleteClient"; diff --git a/server/routers/external.ts b/server/routers/external.ts index 778bf288..d2e680ec 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -103,6 +103,13 @@ authenticated.get( client.pickClientDefaults ); +authenticated.put( + "/site/:siteId/client", + verifySiteAccess, + verifyUserHasAction(ActionsEnum.createClient), + client.createClient +); + // authenticated.get( // "/site/:siteId/roles", // verifySiteAccess, diff --git a/server/routers/site/createSite.ts b/server/routers/site/createSite.ts index 9a5e6f46..1b30b5c8 100644 --- a/server/routers/site/createSite.ts +++ b/server/routers/site/createSite.ts @@ -35,7 +35,7 @@ const createSiteSchema = z subnet: z.string().optional(), newtId: z.string().optional(), secret: z.string().optional(), - type: z.string() + type: z.enum(["newt", "wireguard"]) }) .strict();