From 7536c03f63f623a3b248a5dd47284a0af4f5762c Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sun, 19 Oct 2025 12:04:20 -0700 Subject: [PATCH] add int api routes for add/remote email to resource email whitelist --- server/routers/integration.ts | 14 ++ .../resource/addEmailToResourceWhitelist.ts | 147 +++++++++++++++++ server/routers/resource/index.ts | 2 + .../removeEmailFromResourceWhitelist.ts | 150 ++++++++++++++++++ 4 files changed, 313 insertions(+) create mode 100644 server/routers/resource/addEmailToResourceWhitelist.ts create mode 100644 server/routers/resource/removeEmailFromResourceWhitelist.ts diff --git a/server/routers/integration.ts b/server/routers/integration.ts index d0c7c5d5..8808c931 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -414,6 +414,20 @@ authenticated.post( resource.setResourceWhitelist ); +authenticated.get( + `/resource/:resourceId/whitelist/add`, + verifyApiKeyResourceAccess, + verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist), + resource.addEmailToResourceWhitelist +); + +authenticated.get( + `/resource/:resourceId/whitelist/remove`, + verifyApiKeyResourceAccess, + verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist), + resource.removeEmailFromResourceWhitelist +); + authenticated.get( `/resource/:resourceId/whitelist`, verifyApiKeyResourceAccess, diff --git a/server/routers/resource/addEmailToResourceWhitelist.ts b/server/routers/resource/addEmailToResourceWhitelist.ts new file mode 100644 index 00000000..c0d80468 --- /dev/null +++ b/server/routers/resource/addEmailToResourceWhitelist.ts @@ -0,0 +1,147 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { resources, resourceWhitelist } 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 { fromError } from "zod-validation-error"; +import { and, eq } from "drizzle-orm"; +import { OpenAPITags, registry } from "@server/openApi"; + +const addEmailToResourceWhitelistBodySchema = z + .object({ + email: z + .string() + .email() + .or( + z.string().regex(/^\*@[\w.-]+\.[a-zA-Z]{2,}$/, { + message: + "Invalid email address. Wildcard (*) must be the entire local part." + }) + ) + .transform((v) => v.toLowerCase()) + }) + .strict(); + +const addEmailToResourceWhitelistParamsSchema = z + .object({ + resourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()) + }) + .strict(); + +registry.registerPath({ + method: "post", + path: "/resource/{resourceId}/whitelist/add", + description: "Add a single email to the resource whitelist.", + tags: [OpenAPITags.Resource], + request: { + params: addEmailToResourceWhitelistParamsSchema, + body: { + content: { + "application/json": { + schema: addEmailToResourceWhitelistBodySchema + } + } + } + }, + responses: {} +}); + +export async function addEmailToResourceWhitelist( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedBody = addEmailToResourceWhitelistBodySchema.safeParse( + req.body + ); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { email } = parsedBody.data; + + const parsedParams = addEmailToResourceWhitelistParamsSchema.safeParse( + req.params + ); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { resourceId } = parsedParams.data; + + const [resource] = await db + .select() + .from(resources) + .where(eq(resources.resourceId, resourceId)); + + if (!resource) { + return next( + createHttpError(HttpCode.NOT_FOUND, "Resource not found") + ); + } + + if (!resource.emailWhitelistEnabled) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Email whitelist is not enabled for this resource" + ) + ); + } + + // Check if email already exists in whitelist + const existingEntry = await db + .select() + .from(resourceWhitelist) + .where( + and( + eq(resourceWhitelist.resourceId, resourceId), + eq(resourceWhitelist.email, email) + ) + ); + + if (existingEntry.length > 0) { + return next( + createHttpError( + HttpCode.CONFLICT, + "Email already exists in whitelist" + ) + ); + } + + await db.insert(resourceWhitelist).values({ + email, + resourceId + }); + + return response(res, { + data: {}, + success: true, + error: false, + message: "Email added to whitelist successfully", + status: HttpCode.CREATED + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/resource/index.ts b/server/routers/resource/index.ts index 60938342..d1c7011d 100644 --- a/server/routers/resource/index.ts +++ b/server/routers/resource/index.ts @@ -23,3 +23,5 @@ export * from "./listResourceRules"; export * from "./updateResourceRule"; export * from "./getUserResources"; export * from "./setResourceHeaderAuth"; +export * from "./addEmailToResourceWhitelist"; +export * from "./removeEmailFromResourceWhitelist"; diff --git a/server/routers/resource/removeEmailFromResourceWhitelist.ts b/server/routers/resource/removeEmailFromResourceWhitelist.ts new file mode 100644 index 00000000..7667bf28 --- /dev/null +++ b/server/routers/resource/removeEmailFromResourceWhitelist.ts @@ -0,0 +1,150 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { resources, resourceWhitelist } 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 { fromError } from "zod-validation-error"; +import { and, eq } from "drizzle-orm"; +import { OpenAPITags, registry } from "@server/openApi"; + +const removeEmailFromResourceWhitelistBodySchema = z + .object({ + email: z + .string() + .email() + .or( + z.string().regex(/^\*@[\w.-]+\.[a-zA-Z]{2,}$/, { + message: + "Invalid email address. Wildcard (*) must be the entire local part." + }) + ) + .transform((v) => v.toLowerCase()) + }) + .strict(); + +const removeEmailFromResourceWhitelistParamsSchema = z + .object({ + resourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()) + }) + .strict(); + +registry.registerPath({ + method: "post", + path: "/resource/{resourceId}/whitelist/remove", + description: "Remove a single email from the resource whitelist.", + tags: [OpenAPITags.Resource], + request: { + params: removeEmailFromResourceWhitelistParamsSchema, + body: { + content: { + "application/json": { + schema: removeEmailFromResourceWhitelistBodySchema + } + } + } + }, + responses: {} +}); + +export async function removeEmailFromResourceWhitelist( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedBody = removeEmailFromResourceWhitelistBodySchema.safeParse( + req.body + ); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { email } = parsedBody.data; + + const parsedParams = + removeEmailFromResourceWhitelistParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { resourceId } = parsedParams.data; + + const [resource] = await db + .select() + .from(resources) + .where(eq(resources.resourceId, resourceId)); + + if (!resource) { + return next( + createHttpError(HttpCode.NOT_FOUND, "Resource not found") + ); + } + + if (!resource.emailWhitelistEnabled) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Email whitelist is not enabled for this resource" + ) + ); + } + + // Check if email exists in whitelist + const existingEntry = await db + .select() + .from(resourceWhitelist) + .where( + and( + eq(resourceWhitelist.resourceId, resourceId), + eq(resourceWhitelist.email, email) + ) + ); + + if (existingEntry.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Email not found in whitelist" + ) + ); + } + + await db + .delete(resourceWhitelist) + .where( + and( + eq(resourceWhitelist.resourceId, resourceId), + eq(resourceWhitelist.email, email) + ) + ); + + return response(res, { + data: {}, + success: true, + error: false, + message: "Email removed from whitelist successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +}