From 343d04505895206f154ed3ac4843509f59b403c4 Mon Sep 17 00:00:00 2001 From: Bernt Christian Egeland Date: Thu, 29 Aug 2024 16:23:07 +0000 Subject: [PATCH 1/6] api wrapper --- .../v1/org/[orgid]/network/[nwid]/index.ts | 89 ++++++------------- src/utils/apiAuth.ts | 87 ++++++++++++++++++ 2 files changed, 113 insertions(+), 63 deletions(-) create mode 100644 src/utils/apiAuth.ts diff --git a/src/pages/api/v1/org/[orgid]/network/[nwid]/index.ts b/src/pages/api/v1/org/[orgid]/network/[nwid]/index.ts index c753466a..481442a5 100644 --- a/src/pages/api/v1/org/[orgid]/network/[nwid]/index.ts +++ b/src/pages/api/v1/org/[orgid]/network/[nwid]/index.ts @@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { appRouter } from "~/server/api/root"; import { prisma } from "~/server/db"; import { AuthorizationType } from "~/types/apiTypes"; +import { SecuredOrganizationApiRoute } from "~/utils/apiAuth"; import { decryptAndVerifyToken } from "~/utils/encryption"; import { handleApiErrors } from "~/utils/errors"; import rateLimit from "~/utils/rateLimit"; @@ -204,70 +205,32 @@ export const POST_network = async (req: NextApiRequest, res: NextApiResponse) => return handleApiErrors(cause, res); } }; -export const GET_network = async (req: NextApiRequest, res: NextApiResponse) => { - const apiKey = req.headers["x-ztnet-auth"] as string; - - // network id - const networkId = req.query?.nwid as string; - - // organization id - const orgid = req.query?.orgid as string; - - try { - if (!apiKey) { - return res.status(400).json({ error: "API Key is required" }); - } - - if (!orgid) { - return res.status(400).json({ error: "Organization ID is required" }); - } - - if (!networkId) { - return res.status(400).json({ error: "Network ID is required" }); - } - const decryptedData: { userId: string; name?: string } = await decryptAndVerifyToken({ - apiKey, - apiAuthorizationType: AuthorizationType.ORGANIZATION, - }); - - // assemble the context object - const ctx = { - session: { - user: { - id: decryptedData.userId as string, - }, - }, - prisma, - }; - // Check if the user is an organization admin - // TODO This might be redundant as the caller.createOrgNetwork will check for the same thing. Keeping it for now - await checkUserOrganizationRole({ - ctx, - organizationId: orgid, - minimumRequiredRole: Role.READ_ONLY, - }); +export const GET_network = SecuredOrganizationApiRoute( + { requiredRole: Role.READ_ONLY, requireNetworkId: true }, + async (_req, res, { networkId, ctx }) => { + try { + const network = await prisma.network.findUnique({ + where: { nwid: networkId }, + select: { authorId: true, description: true }, + }); - const network = await prisma.network.findUnique({ - where: { nwid: networkId }, - select: { authorId: true, description: true }, - }); + if (!network) { + return res.status(401).json({ error: "Network not found or access denied." }); + } - if (!network) { - return res.status(401).json({ error: "Network not found or access denied." }); + const ztControllerResponse = await ztController.local_network_detail( + //@ts-expect-error + ctx, + networkId, + false, + ); + return res.status(200).json({ + ...network, + ...ztControllerResponse?.network, + }); + } catch (cause) { + return handleApiErrors(cause, res); } - - const ztControllerResponse = await ztController.local_network_detail( - //@ts-expect-error - ctx, - networkId, - false, - ); - return res.status(200).json({ - ...network, - ...ztControllerResponse?.network, - }); - } catch (cause) { - return handleApiErrors(cause, res); - } -}; + }, +); diff --git a/src/utils/apiAuth.ts b/src/utils/apiAuth.ts new file mode 100644 index 00000000..598736e5 --- /dev/null +++ b/src/utils/apiAuth.ts @@ -0,0 +1,87 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { handleApiErrors } from "~/utils/errors"; +import { checkUserOrganizationRole } from "~/utils/role"; +import { Role } from "@prisma/client"; +import { prisma } from "~/server/db"; +import { decryptAndVerifyToken } from "./encryption"; +import { AuthorizationType } from "~/types/apiTypes"; + +/** + * Organization API handler wrapper for apir routes that require authentication + */ +type ApiHandler = ( + req: NextApiRequest, + res: NextApiResponse, + context: { + userId: string; + orgId: string; + networkId?: string; + ctx: { + prisma: typeof prisma; + session: { + user: { + id: string; + }; + }; + }; + }, +) => Promise; + +export const SecuredOrganizationApiRoute = ( + options: { + requiredRole?: Role; + requireNetworkId?: boolean; + }, + handler: ApiHandler, +) => { + return async (req: NextApiRequest, res: NextApiResponse) => { + const apiKey = req.headers["x-ztnet-auth"] as string; + const orgId = req.query?.orgid as string; + const networkId = req.query?.nwid as string; + + try { + if (!apiKey) { + return res.status(400).json({ error: "API Key is required" }); + } + + if (!orgId) { + return res.status(400).json({ error: "Organization ID is required" }); + } + + if (options.requireNetworkId && !networkId) { + return res.status(400).json({ error: "Network ID is required" }); + } + + const decryptedData = await decryptAndVerifyToken({ + apiKey, + apiAuthorizationType: AuthorizationType.ORGANIZATION, + }); + + const ctx = { + session: { + user: { + id: decryptedData.userId as string, + }, + }, + prisma, + }; + + if (options.requiredRole) { + await checkUserOrganizationRole({ + ctx, + organizationId: orgId, + minimumRequiredRole: options.requiredRole, + }); + } + + await handler(req, res, { + userId: decryptedData.userId, + orgId, + networkId, + ctx, + }); + } catch (cause) { + return handleApiErrors(cause, res); + } + }; +}; From ccbf1135cfcc8872a8726b9ab98aa79fcaaf53de Mon Sep 17 00:00:00 2001 From: Bernt Christian Egeland Date: Fri, 30 Aug 2024 07:09:23 +0000 Subject: [PATCH 2/6] org api endpoint wrapper --- src/pages/api/v1/org/[orgid]/index.ts | 96 ++--- .../v1/org/[orgid]/network/[nwid]/index.ts | 229 +++++----- .../network/[nwid]/member/[memberId]/index.ts | 392 +++++++----------- .../[orgid]/network/[nwid]/member/index.ts | 82 +--- src/pages/api/v1/org/[orgid]/network/index.ts | 171 +++----- src/pages/api/v1/org/[orgid]/user/index.ts | 79 ++-- src/pages/api/v1/org/index.ts | 99 ++--- src/utils/apiAuth.ts | 32 +- 8 files changed, 429 insertions(+), 751 deletions(-) diff --git a/src/pages/api/v1/org/[orgid]/index.ts b/src/pages/api/v1/org/[orgid]/index.ts index b18b90b0..85bb6a12 100644 --- a/src/pages/api/v1/org/[orgid]/index.ts +++ b/src/pages/api/v1/org/[orgid]/index.ts @@ -1,12 +1,9 @@ import { Role } from "@prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; import { appRouter } from "~/server/api/root"; -import { prisma } from "~/server/db"; -import { AuthorizationType } from "~/types/apiTypes"; -import { decryptAndVerifyToken } from "~/utils/encryption"; +import { SecuredOrganizationApiRoute } from "~/utils/apiAuth"; import { handleApiErrors } from "~/utils/errors"; import rateLimit from "~/utils/rateLimit"; -import { checkUserOrganizationRole } from "~/utils/role"; // Number of allowed requests per minute const limiter = rateLimit({ @@ -37,66 +34,35 @@ export default async function apiNetworkHandler( } } -export const GET_orgById = async (req: NextApiRequest, res: NextApiResponse) => { - try { - const apiKey = req.headers["x-ztnet-auth"] as string; - const orgid = req.query?.orgid as string; - - if (!apiKey) { - return res.status(400).json({ error: "API Key is required" }); - } +export const GET_orgById = SecuredOrganizationApiRoute( + { requiredRole: Role.READ_ONLY }, + async (_req, res, { orgId, ctx }) => { + try { + //@ts-expect-error + const caller = appRouter.createCaller(ctx); + const organization = await caller.org + .getOrgById({ + organizationId: orgId, + }) + // modify the response to only inlude certain fields + .then((org) => { + return { + id: org.id, + name: org.orgName, + createdAt: org.createdAt, + ownerId: org.ownerId, + networks: org.networks.map((network) => { + return { + nwid: network.nwid, + name: network.name, + }; + }), + }; + }); - if (!orgid) { - return res.status(400).json({ error: "Organization ID is required" }); + return res.status(200).json(organization); + } catch (cause) { + return handleApiErrors(cause, res); } - - const decryptedData: { userId: string; name?: string } = await decryptAndVerifyToken({ - apiKey, - apiAuthorizationType: AuthorizationType.ORGANIZATION, - }); - - // Mock context - const ctx = { - session: { - user: { - id: decryptedData.userId as string, - }, - }, - prisma, - }; - - // Check if the user is an organization admin - // TODO This might be redundant as the caller.getOrgById will check for the same thing - await checkUserOrganizationRole({ - ctx, - organizationId: orgid, - minimumRequiredRole: Role.READ_ONLY, - }); - - //@ts-expect-error - const caller = appRouter.createCaller(ctx); - const organization = await caller.org - .getOrgById({ - organizationId: orgid, - }) - // modify the response to only inlude certain fields - .then((org) => { - return { - id: org.id, - name: org.orgName, - createdAt: org.createdAt, - ownerId: org.ownerId, - networks: org.networks.map((network) => { - return { - nwid: network.nwid, - name: network.name, - }; - }), - }; - }); - - return res.status(200).json(organization); - } catch (cause) { - return handleApiErrors(cause, res); - } -}; + }, +); diff --git a/src/pages/api/v1/org/[orgid]/network/[nwid]/index.ts b/src/pages/api/v1/org/[orgid]/network/[nwid]/index.ts index 481442a5..39f696fb 100644 --- a/src/pages/api/v1/org/[orgid]/network/[nwid]/index.ts +++ b/src/pages/api/v1/org/[orgid]/network/[nwid]/index.ts @@ -2,12 +2,9 @@ import { Role, network } from "@prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; import { appRouter } from "~/server/api/root"; import { prisma } from "~/server/db"; -import { AuthorizationType } from "~/types/apiTypes"; import { SecuredOrganizationApiRoute } from "~/utils/apiAuth"; -import { decryptAndVerifyToken } from "~/utils/encryption"; import { handleApiErrors } from "~/utils/errors"; import rateLimit from "~/utils/rateLimit"; -import { checkUserOrganizationRole } from "~/utils/role"; import * as ztController from "~/utils/ztApi"; // Number of allowed requests per minute @@ -60,151 +57,111 @@ export default async function apiNetworkByIdHandler( break; } } -export const POST_network = async (req: NextApiRequest, res: NextApiResponse) => { - const apiKey = req.headers["x-ztnet-auth"] as string; - // network id - const networkId = req.query?.nwid as string; - - // organization id - const orgid = req.query?.orgid as string; - const requestBody = req.body; - - try { - if (!apiKey) { - return res.status(400).json({ error: "API Key is required" }); - } +export const POST_network = SecuredOrganizationApiRoute( + { requiredRole: Role.READ_ONLY, requireNetworkId: true }, + async (_req, res, { networkId, ctx, body }) => { + try { + // structure of the updateableFields object: + const updateableFields = { + name: { type: "string", destinations: ["controller", "database"] }, + description: { type: "string", destinations: ["database"] }, + flowRule: { type: "string", destinations: ["custom"] }, + mtu: { type: "string", destinations: ["controller"] }, + private: { type: "boolean", destinations: ["controller"] }, + // capabilities: { type: "array", destinations: ["controller"] }, + dns: { type: "array", destinations: ["controller"] }, + ipAssignmentPools: { type: "array", destinations: ["controller"] }, + routes: { type: "array", destinations: ["controller"] }, + // rules: { type: "array", destinations: ["controller"] }, + // tags: { type: "array", destinations: ["controller"] }, + v4AssignMode: { type: "object", destinations: ["controller"] }, + v6AssignMode: { type: "object", destinations: ["controller"] }, + }; + + const databasePayload: Partial = {}; + const controllerPayload: Partial = {}; + + // @ts-expect-error + const caller = appRouter.createCaller(ctx); + + // Iterate over keys in the request body + for (const key in body) { + // Check if the key is not in updateableFields + if (!(key in updateableFields)) { + return res.status(400).json({ error: `Invalid field: ${key}` }); + } - if (!orgid) { - return res.status(400).json({ error: "Organization ID is required" }); - } + try { + const parsedValue = parseField(key, body[key], updateableFields[key].type); + // if custom and flowRule call the caller.setFlowRule + if (key === "flowRule") { + // @ts-expect-error + const caller = appRouter.createCaller(ctx); + await caller.network.setFlowRule({ + nwid: networkId, + updateParams: { + flowRoute: parsedValue, + }, + }); + } + if (updateableFields[key].destinations.includes("database")) { + databasePayload[key] = parsedValue; + } + if (updateableFields[key].destinations.includes("controller")) { + controllerPayload[key] = parsedValue; + } + } catch (error) { + return res.status(400).json({ error: error.message }); + } + } - if (!networkId) { - return res.status(400).json({ error: "Network ID is required" }); - } + const network = await caller.network + .getNetworkById({ nwid: networkId }) + .then(async (res) => { + return res.network || null; + }); - const decryptedData: { userId: string; name?: string } = await decryptAndVerifyToken({ - apiKey, - apiAuthorizationType: AuthorizationType.ORGANIZATION, - }); - - // assemble the context object - const ctx = { - session: { - user: { - id: decryptedData.userId as string, - }, - }, - prisma, - }; - - // Check if the user is an organization admin - // TODO This might be redundant as the caller.createOrgNetwork will check for the same thing. Keeping it for now - await checkUserOrganizationRole({ - ctx, - organizationId: orgid, - minimumRequiredRole: Role.READ_ONLY, - }); - - // structure of the updateableFields object: - const updateableFields = { - name: { type: "string", destinations: ["controller", "database"] }, - description: { type: "string", destinations: ["database"] }, - flowRule: { type: "string", destinations: ["custom"] }, - mtu: { type: "string", destinations: ["controller"] }, - private: { type: "boolean", destinations: ["controller"] }, - // capabilities: { type: "array", destinations: ["controller"] }, - dns: { type: "array", destinations: ["controller"] }, - ipAssignmentPools: { type: "array", destinations: ["controller"] }, - routes: { type: "array", destinations: ["controller"] }, - // rules: { type: "array", destinations: ["controller"] }, - // tags: { type: "array", destinations: ["controller"] }, - v4AssignMode: { type: "object", destinations: ["controller"] }, - v6AssignMode: { type: "object", destinations: ["controller"] }, - }; - - const databasePayload: Partial = {}; - const controllerPayload: Partial = {}; - - // @ts-expect-error - const caller = appRouter.createCaller(ctx); - - // Iterate over keys in the request body - for (const key in requestBody) { - // Check if the key is not in updateableFields - if (!(key in updateableFields)) { - return res.status(400).json({ error: `Invalid field: ${key}` }); + if (!network) { + return res.status(401).json({ error: "Network not found or access denied." }); } - try { - const parsedValue = parseField(key, requestBody[key], updateableFields[key].type); - // if custom and flowRule call the caller.setFlowRule - if (key === "flowRule") { + /** + * Update the network in the controller + */ + if (Object.keys(controllerPayload).length > 0) { + await ztController.network_update({ // @ts-expect-error - const caller = appRouter.createCaller(ctx); - await caller.network.setFlowRule({ - nwid: networkId, - updateParams: { - flowRoute: parsedValue, - }, - }); - } - if (updateableFields[key].destinations.includes("database")) { - databasePayload[key] = parsedValue; - } - if (updateableFields[key].destinations.includes("controller")) { - controllerPayload[key] = parsedValue; - } - } catch (error) { - return res.status(400).json({ error: error.message }); + ctx, + nwid: networkId, + // @ts-expect-error + updateParams: controllerPayload, + }); } - } - - const network = await caller.network - .getNetworkById({ nwid: networkId }) - .then(async (res) => { - return res.network || null; - }); - if (!network) { - return res.status(401).json({ error: "Network not found or access denied." }); - } + if (Object.keys(databasePayload).length > 0) { + await ctx.prisma.network.update({ + where: { + nwid: networkId, + }, + data: { + ...databasePayload, + }, + }); + } - /** - * Update the network in the controller - */ - if (Object.keys(controllerPayload).length > 0) { - await ztController.network_update({ - // @ts-expect-error + const ztControllerResponse = await ztController.local_network_detail( + //@ts-expect-error ctx, - nwid: networkId, - // @ts-expect-error - updateParams: controllerPayload, - }); - } - - if (Object.keys(databasePayload).length > 0) { - await ctx.prisma.network.update({ - where: { - nwid: networkId, - }, - data: { - ...databasePayload, - }, - }); + networkId, + false, + ); + return res.status(200).json(ztControllerResponse?.network); + } catch (cause) { + return handleApiErrors(cause, res); } - - const ztControllerResponse = await ztController.local_network_detail( - //@ts-expect-error - ctx, - networkId, - false, - ); - return res.status(200).json(ztControllerResponse?.network); - } catch (cause) { - return handleApiErrors(cause, res); - } -}; + }, +); export const GET_network = SecuredOrganizationApiRoute( { requiredRole: Role.READ_ONLY, requireNetworkId: true }, diff --git a/src/pages/api/v1/org/[orgid]/network/[nwid]/member/[memberId]/index.ts b/src/pages/api/v1/org/[orgid]/network/[nwid]/member/[memberId]/index.ts index 68d741cf..b15a117a 100644 --- a/src/pages/api/v1/org/[orgid]/network/[nwid]/member/[memberId]/index.ts +++ b/src/pages/api/v1/org/[orgid]/network/[nwid]/member/[memberId]/index.ts @@ -2,8 +2,7 @@ import { Role, network_members } from "@prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; import { appRouter } from "~/server/api/root"; import { prisma } from "~/server/db"; -import { AuthorizationType } from "~/types/apiTypes"; -import { decryptAndVerifyToken } from "~/utils/encryption"; +import { SecuredOrganizationApiRoute } from "~/utils/apiAuth"; import { handleApiErrors } from "~/utils/errors"; import rateLimit from "~/utils/rateLimit"; import { checkUserOrganizationRole } from "~/utils/role"; @@ -66,152 +65,128 @@ export default async function apiNetworkUpdateMembersHandler( * @param res - The NextApiResponse object. * @returns A JSON response indicating the success or failure of the update operation. */ -export const POST_orgUpdateNetworkMember = async ( - req: NextApiRequest, - res: NextApiResponse, -) => { - const apiKey = req.headers["x-ztnet-auth"] as string; - const networkId = req.query?.nwid as string; - const memberId = req.query?.memberId as string; - const requestBody = req.body; - // organization id - const orgid = req.query?.orgid as string; - - if (!apiKey) { - return res.status(400).json({ error: "API Key is required" }); - } - - if (!networkId) { - return res.status(400).json({ error: "Network ID is required" }); - } - - if (!orgid) { - return res.status(400).json({ error: "Organization ID is required" }); - } - - try { - const decryptedData: { userId: string; name?: string } = await decryptAndVerifyToken({ - apiKey, - apiAuthorizationType: AuthorizationType.ORGANIZATION, - }); - - // structure of the updateableFields object: - const updateableFields = { - name: { type: "string", destinations: ["database"] }, - authorized: { type: "boolean", destinations: ["controller"] }, - }; - - const databasePayload: Partial = {}; - const controllerPayload: Partial = {}; - - // Iterate over keys in the request body - for (const key in requestBody) { - // Check if the key is not in updateableFields - if (!(key in updateableFields)) { - return res.status(400).json({ error: `Invalid field: ${key}` }); - } - - try { - const parsedValue = parseField(key, requestBody[key], updateableFields[key].type); - if (updateableFields[key].destinations.includes("database")) { - databasePayload[key] = parsedValue; +export const POST_orgUpdateNetworkMember = SecuredOrganizationApiRoute( + { requiredRole: Role.USER, requireNetworkId: true }, + async (_req, res, { networkId, orgId, body, userId, memberId }) => { + try { + // structure of the updateableFields object: + const updateableFields = { + name: { type: "string", destinations: ["database"] }, + authorized: { type: "boolean", destinations: ["controller"] }, + }; + + const databasePayload: Partial = {}; + const controllerPayload: Partial = {}; + + // Iterate over keys in the request body + for (const key in body) { + // Check if the key is not in updateableFields + if (!(key in updateableFields)) { + return res.status(400).json({ error: `Invalid field: ${key}` }); } - if (updateableFields[key].destinations.includes("controller")) { - controllerPayload[key] = parsedValue; + + try { + const parsedValue = parseField(key, body[key], updateableFields[key].type); + if (updateableFields[key].destinations.includes("database")) { + databasePayload[key] = parsedValue; + } + if (updateableFields[key].destinations.includes("controller")) { + controllerPayload[key] = parsedValue; + } + } catch (error) { + return res.status(400).json({ error: error.message }); } - } catch (error) { - return res.status(400).json({ error: error.message }); } - } - // assemble the context object - const ctx = { - session: { - user: { - id: decryptedData.userId as string, + // assemble the context object + const ctx = { + session: { + user: { + id: userId as string, + }, }, - }, - prisma, - wss: null, - }; + prisma, + wss: null, + }; - // Check if the user is an organization admin - await checkUserOrganizationRole({ - ctx, - organizationId: orgid, - minimumRequiredRole: Role.USER, - }); + // Check if the user is an organization admin + await checkUserOrganizationRole({ + ctx, + organizationId: orgId, + minimumRequiredRole: Role.USER, + }); - // make sure the member is valid - const network = await prisma.network.findUnique({ - where: { nwid: networkId, organizationId: orgid }, - include: { - networkMembers: { - where: { id: memberId }, + // make sure the member is valid + const network = await prisma.network.findUnique({ + where: { nwid: networkId, organizationId: orgId }, + include: { + networkMembers: { + where: { id: memberId }, + }, }, - }, - }); + }); - if (!network?.networkMembers || network.networkMembers.length === 0) { - return res - .status(401) - .json({ error: "Member or Network not found or access denied." }); - } + if (!network?.networkMembers || network.networkMembers.length === 0) { + return res + .status(401) + .json({ error: "Member or Network not found or access denied." }); + } - if (Object.keys(databasePayload).length > 0) { - // if users click the re-generate icon on IP address - await ctx.prisma.network.update({ - where: { - nwid: networkId, - }, - data: { - networkMembers: { - update: { - where: { - id_nwid: { - id: memberId, - nwid: networkId, // this should be the value of `nwid` you are looking for + if (Object.keys(databasePayload).length > 0) { + // if users click the re-generate icon on IP address + await ctx.prisma.network.update({ + where: { + nwid: networkId, + }, + data: { + networkMembers: { + update: { + where: { + id_nwid: { + id: memberId, + nwid: networkId, // this should be the value of `nwid` you are looking for + }, + }, + data: { + ...databasePayload, }, - }, - data: { - ...databasePayload, }, }, }, - }, - select: { - networkMembers: { - where: { - id: memberId, + select: { + networkMembers: { + where: { + id: memberId, + }, }, }, - }, - }); - } + }); + } - if (Object.keys(controllerPayload).length > 0) { - await ztController.member_update({ - // @ts-expect-error - ctx, + if (Object.keys(controllerPayload).length > 0) { + await ztController.member_update({ + // @ts-expect-error + ctx, + nwid: networkId, + memberId: memberId, + // @ts-expect-error + updateParams: controllerPayload, + }); + } + + // @ts-expect-error + const caller = appRouter.createCaller(ctx); + const networkAndMembers = await caller.networkMember.getMemberById({ nwid: networkId, - memberId: memberId, - // @ts-expect-error - updateParams: controllerPayload, + id: memberId, }); - } - - // @ts-expect-error - const caller = appRouter.createCaller(ctx); - const networkAndMembers = await caller.networkMember.getMemberById({ - nwid: networkId, - id: memberId, - }); - return res.status(200).json(networkAndMembers); - } catch (cause) { - return handleApiErrors(cause, res); - } -}; + return res.status(200).json(networkAndMembers); + } catch (cause) { + return handleApiErrors(cause, res); + } + }, +); /** * Handles the HTTP DELETE request to delete a member from a network. @@ -220,128 +195,49 @@ export const POST_orgUpdateNetworkMember = async ( * @param res - The NextApiResponse object representing the outgoing response. * @returns A JSON response indicating the success or failure of the operation. */ -export const DELETE_orgStashNetworkMember = async ( - req: NextApiRequest, - res: NextApiResponse, -) => { - const apiKey = req.headers["x-ztnet-auth"] as string; - const networkId = req.query?.nwid as string; - const memberId = req.query?.memberId as string; - - // organization id - const orgid = req.query?.orgid as string; - - try { - const decryptedData: { userId: string; name?: string } = await decryptAndVerifyToken({ - apiKey, - apiAuthorizationType: AuthorizationType.ORGANIZATION, - }); - - // Check if the networkId exists - if (!networkId) { - return res.status(400).json({ error: "Network ID is required" }); - } - - // Check if the networkId exists - if (!memberId) { - return res.status(400).json({ error: "Member ID is required" }); - } +export const DELETE_orgStashNetworkMember = SecuredOrganizationApiRoute( + { requiredRole: Role.USER, requireNetworkId: true }, + async (_req, res, { networkId, orgId, memberId, ctx }) => { + try { + // @ts-expect-error + const caller = appRouter.createCaller(ctx); + const networkAndMembers = await caller.networkMember.stash({ + nwid: networkId, + id: memberId, + organizationId: orgId, + }); - // Check if the organizationId exists - if (!orgid) { - return res.status(400).json({ error: "Organization ID is required" }); + return res.status(200).json(networkAndMembers); + } catch (cause) { + return handleApiErrors(cause, res); } + }, +); - // assemble the context object - const ctx = { - session: { - user: { - id: decryptedData.userId as string, - }, - }, - prisma, - wss: null, - }; - - // Check if the user is an organization admin - // TODO This might be redundant as the caller.stash will check for the same thing. Keeping it for now - await checkUserOrganizationRole({ - ctx, - organizationId: orgid, - minimumRequiredRole: Role.USER, - }); - - // @ts-expect-error - const caller = appRouter.createCaller(ctx); - const networkAndMembers = await caller.networkMember.stash({ - nwid: networkId, - id: memberId, - organizationId: orgid, - }); - - return res.status(200).json(networkAndMembers); - } catch (cause) { - return handleApiErrors(cause, res); - } -}; - -export const GET_orgNetworkMemberById = async ( - req: NextApiRequest, - res: NextApiResponse, -) => { - const apiKey = req.headers["x-ztnet-auth"] as string; - const networkId = req.query?.nwid as string; - const memberId = req.query?.memberId as string; - - // organization id - const orgid = req.query?.orgid as string; - - try { - const decryptedData: { userId: string; name?: string } = await decryptAndVerifyToken({ - apiKey, - apiAuthorizationType: AuthorizationType.ORGANIZATION, - }); - // check if orgid is present - if (!orgid) { - return res.status(400).json({ error: "Organization ID is required" }); - } - // Check if the networkId exists - if (!networkId) { - return res.status(400).json({ error: "Network ID is required" }); - } +/** + * Retrieves a network member by their ID. + * + * @param _req - The request object. + * @param res - The response object. + * @param networkId - The ID of the network. + * @param memberId - The ID of the member. + * @param ctx - The context object. + * @returns The network member and associated network information. + */ +export const GET_orgNetworkMemberById = SecuredOrganizationApiRoute( + { requiredRole: Role.USER, requireNetworkId: true }, + async (_req, res, { networkId, memberId, ctx }) => { + try { + // @ts-expect-error + const caller = appRouter.createCaller(ctx); + const networkAndMembers = await caller.networkMember.getMemberById({ + nwid: networkId, + id: memberId, + }); - // Check if the networkId exists - if (!memberId) { - return res.status(400).json({ error: "Member ID is required" }); + return res.status(200).json(networkAndMembers); + } catch (cause) { + return handleApiErrors(cause, res); } - - // assemble the context object - const ctx = { - session: { - user: { - id: decryptedData.userId as string, - }, - }, - prisma, - wss: null, - }; - - // Check if the user is an organization admin - await checkUserOrganizationRole({ - ctx, - organizationId: orgid, - minimumRequiredRole: Role.USER, - }); - - // @ts-expect-error - const caller = appRouter.createCaller(ctx); - const networkAndMembers = await caller.networkMember.getMemberById({ - nwid: networkId, - id: memberId, - }); - - return res.status(200).json(networkAndMembers); - } catch (cause) { - return handleApiErrors(cause, res); - } -}; + }, +); diff --git a/src/pages/api/v1/org/[orgid]/network/[nwid]/member/index.ts b/src/pages/api/v1/org/[orgid]/network/[nwid]/member/index.ts index f3f8c830..f3a363e9 100644 --- a/src/pages/api/v1/org/[orgid]/network/[nwid]/member/index.ts +++ b/src/pages/api/v1/org/[orgid]/network/[nwid]/member/index.ts @@ -1,9 +1,7 @@ import { Role } from "@prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; import { appRouter } from "~/server/api/root"; -import { prisma } from "~/server/db"; -import { AuthorizationType } from "~/types/apiTypes"; -import { decryptAndVerifyToken } from "~/utils/encryption"; +import { SecuredOrganizationApiRoute } from "~/utils/apiAuth"; import { handleApiErrors } from "~/utils/errors"; import rateLimit from "~/utils/rateLimit"; import { checkUserOrganizationRole } from "~/utils/role"; @@ -41,63 +39,29 @@ export default async function apiNetworkMembersHandler( } } -export const GET_orgNetworkMembers = async ( - req: NextApiRequest, - res: NextApiResponse, -) => { - const apiKey = req.headers["x-ztnet-auth"] as string; - // network id - const networkId = req.query?.nwid as string; - - // organization id - const orgid = req.query?.orgid as string; - - if (!apiKey) { - return res.status(400).json({ error: "API Key is required" }); - } - - if (!networkId) { - return res.status(400).json({ error: "Network ID is required" }); - } +export const GET_orgNetworkMembers = SecuredOrganizationApiRoute( + { requiredRole: Role.USER, requireNetworkId: true }, + async (_req, res, { networkId, orgId, ctx }) => { + try { + // Check if the user is an organization admin + // TODO This might be redundant as the caller.createOrgNetwork will check for the same thing. Keeping it for now + await checkUserOrganizationRole({ + ctx, + organizationId: orgId, + minimumRequiredRole: Role.USER, + }); - if (!orgid) { - return res.status(400).json({ error: "Organization ID is required" }); - } - - try { - const decryptedData: { userId: string; name?: string } = await decryptAndVerifyToken({ - apiKey, - apiAuthorizationType: AuthorizationType.ORGANIZATION, - }); - - // assemble the context object - const ctx = { - session: { - user: { - id: decryptedData.userId as string, - }, - }, - prisma, - }; + // @ts-expect-error + const caller = appRouter.createCaller(ctx); + const response = await caller.network.getNetworkById({ nwid: networkId }); - // Check if the user is an organization admin - // TODO This might be redundant as the caller.createOrgNetwork will check for the same thing. Keeping it for now - await checkUserOrganizationRole({ - ctx, - organizationId: orgid, - minimumRequiredRole: Role.USER, - }); + if (!response?.network) { + return res.status(401).json({ error: "Network not found or access denied." }); + } - // @ts-expect-error - const caller = appRouter.createCaller(ctx); - const response = await caller.network.getNetworkById({ nwid: networkId }); - - if (!response?.network) { - return res.status(401).json({ error: "Network not found or access denied." }); + return res.status(200).json(response?.members); + } catch (cause) { + return handleApiErrors(cause, res); } - - return res.status(200).json(response?.members); - } catch (cause) { - return handleApiErrors(cause, res); - } -}; + }, +); diff --git a/src/pages/api/v1/org/[orgid]/network/index.ts b/src/pages/api/v1/org/[orgid]/network/index.ts index e4edf3ba..ed59271b 100644 --- a/src/pages/api/v1/org/[orgid]/network/index.ts +++ b/src/pages/api/v1/org/[orgid]/network/index.ts @@ -1,12 +1,9 @@ import { Role } from "@prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; import { appRouter } from "~/server/api/root"; -import { prisma } from "~/server/db"; -import { AuthorizationType } from "~/types/apiTypes"; -import { decryptAndVerifyToken } from "~/utils/encryption"; +import { SecuredOrganizationApiRoute } from "~/utils/apiAuth"; import { handleApiErrors } from "~/utils/errors"; import rateLimit from "~/utils/rateLimit"; -import { checkUserOrganizationRole } from "~/utils/role"; import * as ztController from "~/utils/ztApi"; // Number of allowed requests per minute @@ -41,124 +38,54 @@ export default async function apiNetworkHandler( } } -export const POST_orgCreateNewNetwork = async ( - req: NextApiRequest, - res: NextApiResponse, -) => { - try { - // API Key - const apiKey = req.headers["x-ztnet-auth"] as string; - - // organization id - const orgid = req.query?.orgid as string; - - // organization name - const { name } = req.body; - - if (!apiKey) { - return res.status(400).json({ error: "API Key is required" }); - } +export const POST_orgCreateNewNetwork = SecuredOrganizationApiRoute( + { requiredRole: Role.USER }, + async (_req, res, { body, orgId, ctx }) => { + try { + // organization name + const { name } = body; + + // @ts-expect-error + const caller = appRouter.createCaller(ctx); + const networkAndMembers = await caller.org.createOrgNetwork({ + organizationId: orgId, + networkName: name, + orgName: "Created by Rest API", + }); - if (!orgid) { - return res.status(400).json({ error: "Organization ID is required" }); + return res.status(200).json(networkAndMembers); + } catch (cause) { + return handleApiErrors(cause, res); } - - // If there are users, verify the API key - const decryptedData: { userId: string; name?: string } = await decryptAndVerifyToken({ - apiKey, - apiAuthorizationType: AuthorizationType.ORGANIZATION, - }); - - // Mock context - const ctx = { - session: { - user: { - id: decryptedData.userId as string, - }, - }, - prisma, - }; - - // Check if the user is an organization admin - // TODO This might be redundant as the caller.createOrgNetwork will check for the same thing. Keeping it for now - await checkUserOrganizationRole({ - ctx, - organizationId: orgid, - minimumRequiredRole: Role.USER, - }); - - if (!orgid) { - return res.status(400).json({ error: "Organization ID is required" }); + }, +); + +export const GET_orgUserNetworks = SecuredOrganizationApiRoute( + { requiredRole: Role.USER }, + async (_req, res, { orgId, ctx }) => { + try { + // @ts-expect-error + const caller = appRouter.createCaller(ctx); + const organization = await caller.org + .getOrgById({ organizationId: orgId }) + .then(async (org) => { + // Use Promise.all to wait for all network detail fetches to complete + const networksDetails = await Promise.all( + org.networks.map(async (network) => { + const controller = await ztController.local_network_detail( + //@ts-expect-error ctx is mocked + ctx, + network.nwid, + ); + return controller.network; + }), + ); + return networksDetails; + }); + + return res.status(200).json(organization); + } catch (cause) { + return handleApiErrors(cause, res); } - - // @ts-expect-error - const caller = appRouter.createCaller(ctx); - const networkAndMembers = await caller.org.createOrgNetwork({ - organizationId: orgid, - networkName: name, - orgName: "Created by Rest API", - }); - - return res.status(200).json(networkAndMembers); - } catch (cause) { - return handleApiErrors(cause, res); - } -}; - -export const GET_orgUserNetworks = async (req: NextApiRequest, res: NextApiResponse) => { - const apiKey = req.headers["x-ztnet-auth"] as string; - const orgid = req.query?.orgid as string; - - if (!apiKey) { - return res.status(400).json({ error: "API Key is required" }); - } - - if (!orgid) { - return res.status(400).json({ error: "Organization ID is required" }); - } - - try { - const decryptedData: { userId: string; name?: string } = await decryptAndVerifyToken({ - apiKey, - apiAuthorizationType: AuthorizationType.ORGANIZATION, - }); - - // If there are users, verify the API key - const ctx = { - session: { - user: { - id: decryptedData.userId as string, - }, - }, - prisma, - }; - // Check if the user is an organization admin - // TODO This might be redundant as the caller.createOrgNetwork will check for the same thing. Keeping it for now - await checkUserOrganizationRole({ - ctx, - organizationId: orgid, - minimumRequiredRole: Role.USER, - }); - - // @ts-expect-error - const caller = appRouter.createCaller(ctx); - const organization = await caller.org - .getOrgById({ organizationId: orgid }) - .then(async (org) => { - // Make sure to use `async` here to allow await inside - // Use Promise.all to wait for all network detail fetches to complete - const networksDetails = await Promise.all( - org.networks.map(async (network) => { - //@ts-expect-error ctx is mocked - const controller = await ztController.local_network_detail(ctx, network.nwid); - return controller.network; - }), - ); - return networksDetails; - }); - - return res.status(200).json(organization); - } catch (cause) { - return handleApiErrors(cause, res); - } -}; + }, +); diff --git a/src/pages/api/v1/org/[orgid]/user/index.ts b/src/pages/api/v1/org/[orgid]/user/index.ts index eec555b0..2f76f99b 100644 --- a/src/pages/api/v1/org/[orgid]/user/index.ts +++ b/src/pages/api/v1/org/[orgid]/user/index.ts @@ -1,12 +1,9 @@ import { Role } from "@prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; import { appRouter } from "~/server/api/root"; -import { prisma } from "~/server/db"; -import { AuthorizationType } from "~/types/apiTypes"; -import { decryptAndVerifyToken } from "~/utils/encryption"; +import { SecuredOrganizationApiRoute } from "~/utils/apiAuth"; import { handleApiErrors } from "~/utils/errors"; import rateLimit from "~/utils/rateLimit"; -import { checkUserOrganizationRole } from "~/utils/role"; // Number of allowed requests per minute const limiter = rateLimit({ @@ -37,51 +34,29 @@ export default async function apiNetworkHandler( } } -const GET_organizationUsers = async (req: NextApiRequest, res: NextApiResponse) => { - const apiKey = req.headers["x-ztnet-auth"] as string; - const orgid = req.query?.orgid as string; - - try { - const decryptedData: { userId: string; name?: string } = await decryptAndVerifyToken({ - apiKey, - apiAuthorizationType: AuthorizationType.ORGANIZATION, - }); - - // Mock context - const ctx = { - session: { - user: { - id: decryptedData.userId as string, - }, - }, - prisma, - }; - // Check if the user is an organization admin - // TODO This might be redundant as the caller.getOrgUsers will check for the same thing - await checkUserOrganizationRole({ - ctx, - organizationId: orgid, - minimumRequiredRole: Role.READ_ONLY, - }); - - // @ts-expect-error ctx is not a valid parameter - const caller = appRouter.createCaller(ctx); - const orgUsers = await caller.org - .getOrgUsers({ - organizationId: orgid, - }) - .then((users) => { - return users.map((user) => ({ - orgId: orgid, - userId: user.id, - name: user.name, - email: user.email, - role: user.role, - })); - }); - - return res.status(200).json(orgUsers); - } catch (cause) { - return handleApiErrors(cause, res); - } -}; +const GET_organizationUsers = SecuredOrganizationApiRoute( + { requiredRole: Role.USER }, + async (_req, res, { orgId, ctx }) => { + try { + // @ts-expect-error ctx is not a valid parameter + const caller = appRouter.createCaller(ctx); + const orgUsers = await caller.org + .getOrgUsers({ + organizationId: orgId, + }) + .then((users) => { + return users.map((user) => ({ + orgId: orgId, + userId: user.id, + name: user.name, + email: user.email, + role: user.role, + })); + }); + + return res.status(200).json(orgUsers); + } catch (cause) { + return handleApiErrors(cause, res); + } + }, +); diff --git a/src/pages/api/v1/org/index.ts b/src/pages/api/v1/org/index.ts index 42e54042..51a57440 100644 --- a/src/pages/api/v1/org/index.ts +++ b/src/pages/api/v1/org/index.ts @@ -1,8 +1,7 @@ import { Role } from "@prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; import { prisma } from "~/server/db"; -import { AuthorizationType } from "~/types/apiTypes"; -import { decryptAndVerifyToken } from "~/utils/encryption"; +import { SecuredOrganizationApiRoute } from "~/utils/apiAuth"; import { handleApiErrors } from "~/utils/errors"; import rateLimit from "~/utils/rateLimit"; @@ -35,68 +34,50 @@ export default async function apiOrganizationHandler( } } -export const GET_userOrganization = async (req: NextApiRequest, res: NextApiResponse) => { - const apiKey = req.headers["x-ztnet-auth"] as string; - - try { - const decryptedData: { userId: string; name: string } = await decryptAndVerifyToken({ - apiKey, - apiAuthorizationType: AuthorizationType.ORGANIZATION, - }); - - const orgUserRole = await prisma.userOrganizationRole.findFirst({ - where: { - userId: decryptedData.userId, - }, - select: { - role: true, - }, - }); - - // If the user is not part of the organization or the role is not in the Role enum - if (!orgUserRole || orgUserRole.role in Role === false) { - return res.status(403).json({ error: "Unauthorized" }); - } - - // get all organizations the user is part of. - const organizations = await prisma.organization - .findMany({ - where: { - users: { - some: { - id: decryptedData.userId, +export const GET_userOrganization = SecuredOrganizationApiRoute( + { requiredRole: Role.READ_ONLY, requireOrgId: false }, + async (_req, res, { userId }) => { + try { + // get all organizations the user is part of. + const organizations = await prisma.organization + .findMany({ + where: { + users: { + some: { + id: userId, + }, }, }, - }, - select: { - id: true, - orgName: true, - ownerId: true, - description: true, - createdAt: true, - users: { - select: { - id: true, - name: true, - email: true, - organizationRoles: { - select: { - role: true, + select: { + id: true, + orgName: true, + ownerId: true, + description: true, + createdAt: true, + users: { + select: { + id: true, + name: true, + email: true, + organizationRoles: { + select: { + role: true, + }, }, }, }, }, - }, - }) - .then((orgs) => { - return orgs.filter((org) => { - org.users = undefined; - return org; + }) + .then((orgs) => { + return orgs.filter((org) => { + org.users = undefined; + return org; + }); }); - }); - return res.status(200).json(organizations); - } catch (cause) { - return handleApiErrors(cause, res); - } -}; + return res.status(200).json(organizations); + } catch (cause) { + return handleApiErrors(cause, res); + } + }, +); diff --git a/src/utils/apiAuth.ts b/src/utils/apiAuth.ts index 598736e5..769e75ca 100644 --- a/src/utils/apiAuth.ts +++ b/src/utils/apiAuth.ts @@ -13,9 +13,12 @@ type ApiHandler = ( req: NextApiRequest, res: NextApiResponse, context: { + // biome-ignore lint/suspicious/noExplicitAny: + body: any; userId: string; orgId: string; networkId?: string; + memberId?: string; ctx: { prisma: typeof prisma; session: { @@ -29,8 +32,9 @@ type ApiHandler = ( export const SecuredOrganizationApiRoute = ( options: { - requiredRole?: Role; + requiredRole: Role; requireNetworkId?: boolean; + requireOrgId?: boolean; }, handler: ApiHandler, ) => { @@ -38,17 +42,25 @@ export const SecuredOrganizationApiRoute = ( const apiKey = req.headers["x-ztnet-auth"] as string; const orgId = req.query?.orgid as string; const networkId = req.query?.nwid as string; + const memberId = req.query?.memberId as string; + const body = req.body; + + const mergedOptions = { + // Set orgid as required by default + requireOrgId: true, + ...options, + }; try { if (!apiKey) { return res.status(400).json({ error: "API Key is required" }); } - if (!orgId) { + if (mergedOptions.requireOrgId && !orgId) { return res.status(400).json({ error: "Organization ID is required" }); } - if (options.requireNetworkId && !networkId) { + if (mergedOptions.requireNetworkId && !networkId) { return res.status(400).json({ error: "Network ID is required" }); } @@ -66,18 +78,18 @@ export const SecuredOrganizationApiRoute = ( prisma, }; - if (options.requiredRole) { - await checkUserOrganizationRole({ - ctx, - organizationId: orgId, - minimumRequiredRole: options.requiredRole, - }); - } + await checkUserOrganizationRole({ + ctx, + organizationId: orgId, + minimumRequiredRole: mergedOptions.requiredRole, + }); await handler(req, res, { + body, userId: decryptedData.userId, orgId, networkId, + memberId, ctx, }); } catch (cause) { From a89dab46398709967cf4a68f1893b9bc2eafadfb Mon Sep 17 00:00:00 2001 From: Bernt Christian Egeland Date: Fri, 30 Aug 2024 07:10:23 +0000 Subject: [PATCH 3/6] import path --- src/pages/api/v1/org/[orgid]/index.ts | 2 +- src/pages/api/v1/org/[orgid]/network/[nwid]/index.ts | 2 +- .../v1/org/[orgid]/network/[nwid]/member/[memberId]/index.ts | 2 +- src/pages/api/v1/org/[orgid]/network/[nwid]/member/index.ts | 2 +- src/pages/api/v1/org/[orgid]/network/index.ts | 2 +- src/pages/api/v1/org/[orgid]/user/index.ts | 2 +- src/pages/api/v1/org/index.ts | 2 +- src/utils/{apiAuth.ts => apiRouteAuth.ts} | 0 8 files changed, 7 insertions(+), 7 deletions(-) rename src/utils/{apiAuth.ts => apiRouteAuth.ts} (100%) diff --git a/src/pages/api/v1/org/[orgid]/index.ts b/src/pages/api/v1/org/[orgid]/index.ts index 85bb6a12..f678e9c8 100644 --- a/src/pages/api/v1/org/[orgid]/index.ts +++ b/src/pages/api/v1/org/[orgid]/index.ts @@ -1,7 +1,7 @@ import { Role } from "@prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; import { appRouter } from "~/server/api/root"; -import { SecuredOrganizationApiRoute } from "~/utils/apiAuth"; +import { SecuredOrganizationApiRoute } from "~/utils/apiRouteAuth"; import { handleApiErrors } from "~/utils/errors"; import rateLimit from "~/utils/rateLimit"; diff --git a/src/pages/api/v1/org/[orgid]/network/[nwid]/index.ts b/src/pages/api/v1/org/[orgid]/network/[nwid]/index.ts index 39f696fb..7fdc5691 100644 --- a/src/pages/api/v1/org/[orgid]/network/[nwid]/index.ts +++ b/src/pages/api/v1/org/[orgid]/network/[nwid]/index.ts @@ -2,7 +2,7 @@ import { Role, network } from "@prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; import { appRouter } from "~/server/api/root"; import { prisma } from "~/server/db"; -import { SecuredOrganizationApiRoute } from "~/utils/apiAuth"; +import { SecuredOrganizationApiRoute } from "~/utils/apiRouteAuth"; import { handleApiErrors } from "~/utils/errors"; import rateLimit from "~/utils/rateLimit"; import * as ztController from "~/utils/ztApi"; diff --git a/src/pages/api/v1/org/[orgid]/network/[nwid]/member/[memberId]/index.ts b/src/pages/api/v1/org/[orgid]/network/[nwid]/member/[memberId]/index.ts index b15a117a..98a1b507 100644 --- a/src/pages/api/v1/org/[orgid]/network/[nwid]/member/[memberId]/index.ts +++ b/src/pages/api/v1/org/[orgid]/network/[nwid]/member/[memberId]/index.ts @@ -2,7 +2,7 @@ import { Role, network_members } from "@prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; import { appRouter } from "~/server/api/root"; import { prisma } from "~/server/db"; -import { SecuredOrganizationApiRoute } from "~/utils/apiAuth"; +import { SecuredOrganizationApiRoute } from "~/utils/apiRouteAuth"; import { handleApiErrors } from "~/utils/errors"; import rateLimit from "~/utils/rateLimit"; import { checkUserOrganizationRole } from "~/utils/role"; diff --git a/src/pages/api/v1/org/[orgid]/network/[nwid]/member/index.ts b/src/pages/api/v1/org/[orgid]/network/[nwid]/member/index.ts index f3a363e9..e8ec567e 100644 --- a/src/pages/api/v1/org/[orgid]/network/[nwid]/member/index.ts +++ b/src/pages/api/v1/org/[orgid]/network/[nwid]/member/index.ts @@ -1,7 +1,7 @@ import { Role } from "@prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; import { appRouter } from "~/server/api/root"; -import { SecuredOrganizationApiRoute } from "~/utils/apiAuth"; +import { SecuredOrganizationApiRoute } from "~/utils/apiRouteAuth"; import { handleApiErrors } from "~/utils/errors"; import rateLimit from "~/utils/rateLimit"; import { checkUserOrganizationRole } from "~/utils/role"; diff --git a/src/pages/api/v1/org/[orgid]/network/index.ts b/src/pages/api/v1/org/[orgid]/network/index.ts index ed59271b..d2360489 100644 --- a/src/pages/api/v1/org/[orgid]/network/index.ts +++ b/src/pages/api/v1/org/[orgid]/network/index.ts @@ -1,7 +1,7 @@ import { Role } from "@prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; import { appRouter } from "~/server/api/root"; -import { SecuredOrganizationApiRoute } from "~/utils/apiAuth"; +import { SecuredOrganizationApiRoute } from "~/utils/apiRouteAuth"; import { handleApiErrors } from "~/utils/errors"; import rateLimit from "~/utils/rateLimit"; import * as ztController from "~/utils/ztApi"; diff --git a/src/pages/api/v1/org/[orgid]/user/index.ts b/src/pages/api/v1/org/[orgid]/user/index.ts index 2f76f99b..e0277bac 100644 --- a/src/pages/api/v1/org/[orgid]/user/index.ts +++ b/src/pages/api/v1/org/[orgid]/user/index.ts @@ -1,7 +1,7 @@ import { Role } from "@prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; import { appRouter } from "~/server/api/root"; -import { SecuredOrganizationApiRoute } from "~/utils/apiAuth"; +import { SecuredOrganizationApiRoute } from "~/utils/apiRouteAuth"; import { handleApiErrors } from "~/utils/errors"; import rateLimit from "~/utils/rateLimit"; diff --git a/src/pages/api/v1/org/index.ts b/src/pages/api/v1/org/index.ts index 51a57440..a6ed2f76 100644 --- a/src/pages/api/v1/org/index.ts +++ b/src/pages/api/v1/org/index.ts @@ -1,7 +1,7 @@ import { Role } from "@prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; import { prisma } from "~/server/db"; -import { SecuredOrganizationApiRoute } from "~/utils/apiAuth"; +import { SecuredOrganizationApiRoute } from "~/utils/apiRouteAuth"; import { handleApiErrors } from "~/utils/errors"; import rateLimit from "~/utils/rateLimit"; diff --git a/src/utils/apiAuth.ts b/src/utils/apiRouteAuth.ts similarity index 100% rename from src/utils/apiAuth.ts rename to src/utils/apiRouteAuth.ts From 973e8c3b6e0a5864ea096cb7a6c243b1ce0bc436 Mon Sep 17 00:00:00 2001 From: Bernt Christian Egeland Date: Fri, 30 Aug 2024 09:32:08 +0000 Subject: [PATCH 4/6] apiNetworkUpdateMembersHandler --- .../__tests__/v1/network/networkById.test.ts | 310 ++++++++++-------- .../v1/networkMembers/updateMember.test.ts | 37 ++- src/pages/api/v1/network/[id]/index.ts | 78 ++--- .../network/[id]/member/[memberId]/index.ts | 304 +++++++---------- src/pages/api/v1/network/[id]/member/index.ts | 89 ++--- src/pages/api/v1/network/index.ts | 124 +++---- src/utils/apiRouteAuth.ts | 99 +++++- src/utils/encryption.ts | 1 + 8 files changed, 528 insertions(+), 514 deletions(-) diff --git a/src/pages/api/__tests__/v1/network/networkById.test.ts b/src/pages/api/__tests__/v1/network/networkById.test.ts index 63d044d0..6a1187e8 100644 --- a/src/pages/api/__tests__/v1/network/networkById.test.ts +++ b/src/pages/api/__tests__/v1/network/networkById.test.ts @@ -32,141 +32,183 @@ jest.mock("~/utils/rateLimit", () => ({ })), })); -it("should respond 200 when network is found", async () => { - // Mock the decryption to return a valid user ID - (encryptionModule.decryptAndVerifyToken as jest.Mock).mockResolvedValue({ - userId: "userId", +describe("/api/getNetworkById", () => { + // Reset the mocks + beforeEach(() => { + jest.clearAllMocks(); }); - - // Mock the database to return a network - prisma.network.findUnique = jest.fn().mockResolvedValue({ - nwid: "test_nw_id", - nwname: "credent_second", - authorId: 1, - }); - - // Mock the ztController to return a network detail - (ztController.local_network_detail as jest.Mock).mockResolvedValue({ - network: { id: "networkId", name: "networkName" }, - }); - - const req = { - method: "GET", - headers: { "x-ztnet-auth": "validApiKey" }, - query: { id: "networkId" }, - } as unknown as NextApiRequest; - - const res = { - status: jest.fn().mockReturnThis(), - json: jest.fn(), - end: jest.fn(), - setHeader: jest.fn(), // Mock `setHeader` rate limiter uses it - } as unknown as NextApiResponse; - - await apiNetworkByIdHandler(req, res); - - expect(res.status).toHaveBeenCalledWith(200); - expect(res.json).toHaveBeenCalledWith({ - id: "networkId", - name: "networkName", - authorId: 1, - nwid: "test_nw_id", - nwname: "credent_second", - }); -}); - -it("should respond 401 when network is not found", async () => { - // Mock the decryption to return a valid user ID - (encryptionModule.decryptAndVerifyToken as jest.Mock).mockResolvedValue({ - userId: "userId", - }); - - // Mock the database to return a network - prisma.network.findUnique = jest.fn().mockResolvedValue(null); - - const req = { - method: "GET", - headers: { "x-ztnet-auth": "validApiKey" }, - query: { id: "networkId" }, - } as unknown as NextApiRequest; - - const res = { - status: jest.fn().mockReturnThis(), - json: jest.fn(), - end: jest.fn(), - setHeader: jest.fn(), // Mock `setHeader` rate limiter uses it - } as unknown as NextApiResponse; - - await apiNetworkByIdHandler(req, res); - - expect(res.status).toHaveBeenCalledWith(401); - expect(res.json).toHaveBeenCalledWith({ error: "Network not found or access denied." }); -}); - -it("should respond with an error when ztController throws an error", async () => { - // Mock the decryption to return a valid user ID - (encryptionModule.decryptAndVerifyToken as jest.Mock).mockResolvedValue({ - userId: "ztnetUserId", - }); - - // Mock the database to return a network - prisma.network.findUnique = jest.fn().mockResolvedValue({ - nwid: "networkId", - name: "networkName", - authorId: 1, - }); - - // Mock the ztController to throw an error - const error = new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Internal server error", + // it("should respond 200 when network is found", async () => { + // // Mock the decryption to return a valid user ID + // (encryptionModule.decryptAndVerifyToken as jest.Mock).mockResolvedValue({ + // userId: "userId", + // }); + + // // Mock the database to return a network + // prisma.network.findUnique = jest.fn().mockResolvedValue({ + // nwid: "test_nw_id", + // nwname: "credent_second", + // authorId: "userId", + // }); + + // // Mock the ztController to return a network detail + // (ztController.local_network_detail as jest.Mock).mockResolvedValue({ + // network: { id: "networkId", name: "networkName" }, + // }); + + // const req = { + // method: "GET", + // headers: { "x-ztnet-auth": "validApiKey" }, + // query: { id: "networkId" }, + // } as unknown as NextApiRequest; + + // const res = { + // status: jest.fn().mockReturnThis(), + // json: jest.fn(), + // end: jest.fn(), + // setHeader: jest.fn(), // Mock `setHeader` rate limiter uses it + // } as unknown as NextApiResponse; + + // await apiNetworkByIdHandler(req, res); + + // expect(res.status).toHaveBeenCalledWith(200); + // expect(res.json).toHaveBeenCalledWith({ + // id: "networkId", + // name: "networkName", + // authorId: "userId", + // nwid: "test_nw_id", + // nwname: "credent_second", + // }); + // }); + + // it("should respond 401 when network is not found", async () => { + // // Mock the decryption to return a valid user ID + // (encryptionModule.decryptAndVerifyToken as jest.Mock).mockResolvedValue({ + // userId: "userId", + // }); + + // // Mock the database to return a network + // prisma.network.findUnique = jest.fn().mockResolvedValue(null); + + // const req = { + // method: "GET", + // headers: { "x-ztnet-auth": "validApiKey" }, + // query: { id: "networkId" }, + // } as unknown as NextApiRequest; + + // const res = { + // status: jest.fn().mockReturnThis(), + // json: jest.fn(), + // end: jest.fn(), + // setHeader: jest.fn(), // Mock `setHeader` rate limiter uses it + // } as unknown as NextApiResponse; + + // await apiNetworkByIdHandler(req, res); + + // expect(res.status).toHaveBeenCalledWith(401); + // expect(res.json).toHaveBeenCalledWith({ + // error: "Network not found or access denied.", + // }); + // }); + + it("should respond with an error when ztController throws an error", async () => { + // Mock the decryption to return a valid user ID + (encryptionModule.decryptAndVerifyToken as jest.Mock).mockResolvedValue({ + userId: "ztnetUserId", + }); + + // Mock the database to return a network + prisma.network.findUnique = jest.fn().mockResolvedValue({ + nwid: "networkId", + name: "networkName", + authorId: "ztnetUserId", + }); + + // Mock the ztController to throw an error + const error = new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Internal server error", + }); + + (ztController.local_network_detail as jest.Mock).mockRejectedValue(error); + + const req = { + method: "GET", + headers: { "x-ztnet-auth": "validApiKey" }, + query: { id: "networkId" }, + } as unknown as NextApiRequest; + + const res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + end: jest.fn(), + setHeader: jest.fn(), // Mock `setHeader` rate limiter uses it + } as unknown as NextApiResponse; + + await apiNetworkByIdHandler(req, res); + + const httpCode = getHTTPStatusCodeFromError(error); + expect(res.status).toHaveBeenCalledWith(httpCode); + expect(res.json).toHaveBeenCalledWith({ error: error.message }); }); - (ztController.local_network_detail as jest.Mock).mockRejectedValue(error); - - const req = { - method: "GET", - headers: { "x-ztnet-auth": "validApiKey" }, - query: { id: "networkId" }, - } as unknown as NextApiRequest; - - const res = { - status: jest.fn().mockReturnThis(), - json: jest.fn(), - end: jest.fn(), - setHeader: jest.fn(), // Mock `setHeader` rate limiter uses it - } as unknown as NextApiResponse; - - await apiNetworkByIdHandler(req, res); - - const httpCode = getHTTPStatusCodeFromError(error); - expect(res.status).toHaveBeenCalledWith(httpCode); - expect(res.json).toHaveBeenCalledWith({ error: error.message }); -}); - -it("should respond 401 when decryptAndVerifyToken fails", async () => { - // Mock the decryption to fail - (encryptionModule.decryptAndVerifyToken as jest.Mock).mockRejectedValue( - new Error("Invalid token"), - ); - - const req = { - method: "GET", - headers: { "x-ztnet-auth": "invalidApiKey" }, - query: { id: "networkId" }, - } as unknown as NextApiRequest; - - const res = { - status: jest.fn().mockReturnThis(), - json: jest.fn(), - end: jest.fn(), - setHeader: jest.fn(), // Mock `setHeader` if rate limiter uses it - } as unknown as NextApiResponse; - - await apiNetworkByIdHandler(req, res); - - expect(res.status).toHaveBeenCalledWith(401); - expect(res.json).toHaveBeenCalledWith( - expect.objectContaining({ error: expect.any(String) }), - ); + // it("should respond 401 when decryptAndVerifyToken fails", async () => { + // // Mock the decryption to fail + // (encryptionModule.decryptAndVerifyToken as jest.Mock).mockRejectedValue( + // new Error("Invalid token"), + // ); + + // const req = { + // method: "GET", + // headers: { "x-ztnet-auth": "invalidApiKey" }, + // query: { id: "networkId" }, + // } as unknown as NextApiRequest; + + // const res = { + // status: jest.fn().mockReturnThis(), + // json: jest.fn(), + // end: jest.fn(), + // setHeader: jest.fn(), // Mock `setHeader` if rate limiter uses it + // } as unknown as NextApiResponse; + + // await apiNetworkByIdHandler(req, res); + + // expect(res.status).toHaveBeenCalledWith(401); + // expect(res.json).toHaveBeenCalledWith( + // expect.objectContaining({ error: expect.any(String) }), + // ); + // }); + + // it("should respond 401 when user is not the author of the network", async () => { + // // Mock the decryption to return a valid user ID + // (encryptionModule.decryptAndVerifyToken as jest.Mock).mockResolvedValue({ + // userId: "userId", + // }); + + // const req = { + // method: "GET", + // headers: { "x-ztnet-auth": "validApiKey" }, + // query: { id: "networkIdThatUserDoesNotOwn" }, + // } as unknown as NextApiRequest; + + // const res = { + // status: jest.fn().mockReturnThis(), + // end: jest.fn(), + // json: jest.fn().mockReturnThis(), + // setHeader: jest.fn(), // Mock `setHeader` rate limiter uses it + // } as unknown as NextApiResponse; + + // // Mock the database to return a network + // prisma.network.findUnique = jest.fn().mockResolvedValue({ + // nwid: "networkIdThatUserDoesNotOwn", + // nwname: "Some Network", + // authorId: "anotherUserId", + // }); + + // await apiNetworkByIdHandler(req, res); + + // expect(res.status).toHaveBeenCalledWith(401); + // expect(res.json).toHaveBeenCalledWith({ + // error: "Network not found or access denied.", + // }); + // }); }); diff --git a/src/pages/api/__tests__/v1/networkMembers/updateMember.test.ts b/src/pages/api/__tests__/v1/networkMembers/updateMember.test.ts index 785b31c7..04d40008 100644 --- a/src/pages/api/__tests__/v1/networkMembers/updateMember.test.ts +++ b/src/pages/api/__tests__/v1/networkMembers/updateMember.test.ts @@ -52,14 +52,6 @@ describe("Update Network Members", () => { authorId: 1, }); - // Mock the database to return a network - prisma.network.findUnique = jest.fn().mockResolvedValue({ - nwid: "test_nw_id", - nwname: "credent_second", - authorId: 1, - networkMembers: [{ id: "memberId" }], - }); - const mockRegister = jest.fn().mockResolvedValue({ id: "memberId" }); appRouter.createCaller = jest .fn() @@ -103,7 +95,21 @@ describe("Update Network Members", () => { // Assertions expect(res.status).toHaveBeenCalledWith(401); }); + it("should respond 200 when member is successfully updated", async () => { + // Mock the database to return a network + prisma.network.findUnique = jest.fn().mockResolvedValue({ + nwid: "test_nw_id", + nwname: "credent_second", + authorId: "userId", + networkMembers: [{ id: "memberId" }], + }); + + // mock the token + prisma.aPIToken.findUnique = jest.fn().mockResolvedValue({ + expiresAt: new Date(), + }); + const req = { method: "POST", headers: { "x-ztnet-auth": "validApiKey" }, @@ -119,8 +125,15 @@ describe("Update Network Members", () => { // Assertions expect(res.status).toHaveBeenCalledWith(200); }); - // Example for a 400 response - it("should respond 400 for invalid input", async () => { + + it("should respond 401 for invalid input", async () => { + // Mock the database to return a network + prisma.network.findUnique = jest.fn().mockResolvedValue({ + nwid: "test_nw_id", + nwname: "credent_second", + authorId: 1, + networkMembers: [{ id: "memberId" }], + }); const req = { method: "POST", headers: { "x-ztnet-auth": "validApiKey" }, @@ -134,7 +147,7 @@ describe("Update Network Members", () => { await apiNetworkUpdateMembersHandler(req, res); // Assertions - expect(res.status).toHaveBeenCalledWith(400); + expect(res.status).toHaveBeenCalledWith(401); }); it("should respond 401 when decryptAndVerifyToken fails", async () => { @@ -146,7 +159,7 @@ describe("Update Network Members", () => { const req = { method: "POST", headers: { "x-ztnet-auth": "invalidApiKey" }, - query: { id: "networkId" }, + query: { id: "networkId", memberId: "memberId" }, body: { name: "New Name", authorized: "true" }, } as unknown as NextApiRequest; diff --git a/src/pages/api/v1/network/[id]/index.ts b/src/pages/api/v1/network/[id]/index.ts index 5d03ccb9..e345b1b4 100644 --- a/src/pages/api/v1/network/[id]/index.ts +++ b/src/pages/api/v1/network/[id]/index.ts @@ -1,7 +1,6 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { prisma } from "~/server/db"; -import { AuthorizationType } from "~/types/apiTypes"; -import { decryptAndVerifyToken } from "~/utils/encryption"; +import { SecuredPrivateApiRoute } from "~/utils/apiRouteAuth"; import { handleApiErrors } from "~/utils/errors"; import rateLimit from "~/utils/rateLimit"; import * as ztController from "~/utils/ztApi"; @@ -35,55 +34,30 @@ export default async function apiNetworkByIdHandler( } } -const GET_network = async (req: NextApiRequest, res: NextApiResponse) => { - const apiKey = req.headers["x-ztnet-auth"] as string; - const networkId = req.query?.id as string; - - // Check if the networkId exists - if (!networkId) { - return res.status(400).json({ error: "Network ID is required" }); - } - - let decryptedData: { userId: string; name?: string }; - try { - decryptedData = await decryptAndVerifyToken({ - apiKey, - apiAuthorizationType: AuthorizationType.PERSONAL, +const GET_network = SecuredPrivateApiRoute( + { + requireNetworkId: true, + }, + async (_req, res, { networkId, ctx, userId }) => { + // get the network details + const network = await prisma.network.findUnique({ + where: { nwid: networkId, authorId: userId }, + select: { authorId: true, description: true }, }); - } catch (error) { - return res.status(401).json({ error: error.message }); - } - // assemble the context object - const ctx = { - session: { - user: { - id: decryptedData.userId as string, - }, - }, - prisma, - }; - // make sure user has access to the network - const network = await prisma.network.findUnique({ - where: { nwid: networkId, authorId: decryptedData.userId }, - select: { authorId: true, description: true }, - }); - - if (!network) { - return res.status(401).json({ error: "Network not found or access denied." }); - } - try { - const ztControllerResponse = await ztController.local_network_detail( - //@ts-expect-error - ctx, - networkId, - false, - ); - return res.status(200).json({ - ...network, - ...ztControllerResponse?.network, - }); - } catch (cause) { - return handleApiErrors(cause, res); - } -}; + try { + const ztControllerResponse = await ztController.local_network_detail( + //@ts-expect-error + ctx, + networkId, + false, + ); + return res.status(200).json({ + ...network, + ...ztControllerResponse?.network, + }); + } catch (cause) { + return handleApiErrors(cause, res); + } + }, +); diff --git a/src/pages/api/v1/network/[id]/member/[memberId]/index.ts b/src/pages/api/v1/network/[id]/member/[memberId]/index.ts index 4b302ce9..3c0ede6a 100644 --- a/src/pages/api/v1/network/[id]/member/[memberId]/index.ts +++ b/src/pages/api/v1/network/[id]/member/[memberId]/index.ts @@ -2,8 +2,7 @@ import { network_members } from "@prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; import { appRouter } from "~/server/api/root"; import { prisma } from "~/server/db"; -import { AuthorizationType } from "~/types/apiTypes"; -import { decryptAndVerifyToken } from "~/utils/encryption"; +import { SecuredPrivateApiRoute } from "~/utils/apiRouteAuth"; import { handleApiErrors } from "~/utils/errors"; import rateLimit from "~/utils/rateLimit"; import * as ztController from "~/utils/ztApi"; @@ -62,147 +61,116 @@ export default async function apiNetworkUpdateMembersHandler( * @param res - The NextApiResponse object. * @returns A JSON response indicating the success or failure of the update operation. */ -const POST_updateNetworkMember = async (req: NextApiRequest, res: NextApiResponse) => { - const apiKey = req.headers["x-ztnet-auth"] as string; - const networkId = req.query?.id as string; - const memberId = req.query?.memberId as string; - const requestBody = req.body; - - if (Object.keys(requestBody).length === 0) { - return res.status(400).json({ error: "No data provided for update" }); - } - - let decryptedData: { userId: string; name?: string }; - try { - decryptedData = await decryptAndVerifyToken({ - apiKey, - apiAuthorizationType: AuthorizationType.PERSONAL, - }); - } catch (error) { - return res.status(401).json({ error: error.message }); - } - - // Check if the networkId exists - if (!networkId) { - return res.status(400).json({ error: "Network ID is required" }); - } - - // Check if the networkId exists - if (!memberId) { - return res.status(400).json({ error: "Member ID is required" }); - } +const POST_updateNetworkMember = SecuredPrivateApiRoute( + { + requireNetworkId: true, + requireMemberId: true, + }, + async (_req, res, { body, userId, networkId, memberId, ctx }) => { + if (Object.keys(body).length === 0) { + return res.status(400).json({ error: "No data provided for update" }); + } - // structure of the updateableFields object: - const updateableFields = { - name: { type: "string", destinations: ["database"] }, - authorized: { type: "boolean", destinations: ["controller"] }, - }; + // structure of the updateableFields object: + const updateableFields = { + name: { type: "string", destinations: ["database"] }, + authorized: { type: "boolean", destinations: ["controller"] }, + }; - const databasePayload: Partial = {}; - const controllerPayload: Partial = {}; + const databasePayload: Partial = {}; + const controllerPayload: Partial = {}; - // Iterate over keys in the request body - for (const key in requestBody) { - // Check if the key is not in updateableFields - if (!(key in updateableFields)) { - return res.status(400).json({ error: `Invalid field: ${key}` }); - } - - try { - const parsedValue = parseField(key, requestBody[key], updateableFields[key].type); - if (updateableFields[key].destinations.includes("database")) { - databasePayload[key] = parsedValue; + // Iterate over keys in the request body + for (const key in body) { + // Check if the key is not in updateableFields + if (!(key in updateableFields)) { + return res.status(400).json({ error: `Invalid field: ${key}` }); } - if (updateableFields[key].destinations.includes("controller")) { - controllerPayload[key] = parsedValue; + + try { + const parsedValue = parseField(key, body[key], updateableFields[key].type); + if (updateableFields[key].destinations.includes("database")) { + databasePayload[key] = parsedValue; + } + if (updateableFields[key].destinations.includes("controller")) { + controllerPayload[key] = parsedValue; + } + } catch (error) { + return res.status(400).json({ error: error.message }); } - } catch (error) { - return res.status(400).json({ error: error.message }); } - } - - // assemble the context object - const ctx = { - session: { - user: { - id: decryptedData.userId as string, - }, - }, - prisma, - wss: null, - }; - - try { - // make sure the member is valid - const network = await prisma.network.findUnique({ - where: { nwid: networkId, authorId: decryptedData.userId }, - include: { - networkMembers: { - where: { id: memberId }, + try { + // make sure the member is valid + const network = await prisma.network.findUnique({ + where: { nwid: networkId, authorId: userId }, + include: { + networkMembers: { + where: { id: memberId }, + }, }, - }, - }); + }); - if (!network?.networkMembers || network.networkMembers.length === 0) { - return res - .status(401) - .json({ error: "Member or Network not found or access denied." }); - } + if (!network?.networkMembers || network.networkMembers.length === 0) { + return res + .status(401) + .json({ error: "Member or Network not found or access denied." }); + } - if (Object.keys(databasePayload).length > 0) { - // if users click the re-generate icon on IP address - await ctx.prisma.network.update({ - where: { - nwid: networkId, - }, - data: { - networkMembers: { - update: { - where: { - id_nwid: { - id: memberId, - nwid: networkId, // this should be the value of `nwid` you are looking for + if (Object.keys(databasePayload).length > 0) { + // if users click the re-generate icon on IP address + await ctx.prisma.network.update({ + where: { + nwid: networkId, + }, + data: { + networkMembers: { + update: { + where: { + id_nwid: { + id: memberId, + nwid: networkId, // this should be the value of `nwid` you are looking for + }, + }, + data: { + ...databasePayload, }, - }, - data: { - ...databasePayload, }, }, }, - }, - select: { - networkMembers: { - where: { - id: memberId, + select: { + networkMembers: { + where: { + id: memberId, + }, }, }, - }, - }); - } + }); + } - if (Object.keys(controllerPayload).length > 0) { - await ztController.member_update({ - // @ts-expect-error - ctx, + if (Object.keys(controllerPayload).length > 0) { + await ztController.member_update({ + // @ts-expect-error + ctx, + nwid: networkId, + memberId: memberId, + // @ts-expect-error + updateParams: controllerPayload, + }); + } + + // @ts-expect-error + const caller = appRouter.createCaller(ctx); + const networkAndMembers = await caller.networkMember.getMemberById({ nwid: networkId, - memberId: memberId, - // @ts-expect-error - updateParams: controllerPayload, + id: memberId, }); - } - // @ts-expect-error - const caller = appRouter.createCaller(ctx); - const networkAndMembers = await caller.networkMember.getMemberById({ - nwid: networkId, - id: memberId, - }); - - return res.status(200).json(networkAndMembers); - } catch (cause) { - return handleApiErrors(cause, res); - } -}; + return res.status(200).json(networkAndMembers); + } catch (cause) { + return handleApiErrors(cause, res); + } + }, +); /** * Handles the HTTP DELETE request to delete a member from a network. @@ -211,63 +179,39 @@ const POST_updateNetworkMember = async (req: NextApiRequest, res: NextApiRespons * @param res - The NextApiResponse object representing the outgoing response. * @returns A JSON response indicating the success or failure of the operation. */ -const DELETE_deleteNetworkMember = async (req: NextApiRequest, res: NextApiResponse) => { - const apiKey = req.headers["x-ztnet-auth"] as string; - const networkId = req.query?.id as string; - const memberId = req.query?.memberId as string; - - try { - const decryptedData: { userId: string; name?: string } = await decryptAndVerifyToken({ - apiKey, - apiAuthorizationType: AuthorizationType.PERSONAL, - }); - - // Check if the networkId exists - if (!networkId) { - return res.status(400).json({ error: "Network ID is required" }); - } - - // Check if the networkId exists - if (!memberId) { - return res.status(400).json({ error: "Member ID is required" }); - } - - // assemble the context object - const ctx = { - session: { - user: { - id: decryptedData.userId as string, - }, - }, - prisma, - wss: null, - }; - - // make sure the member is valid - const network = await prisma.network.findUnique({ - where: { nwid: networkId, authorId: decryptedData.userId }, - include: { - networkMembers: { - where: { id: memberId }, +const DELETE_deleteNetworkMember = SecuredPrivateApiRoute( + { + requireNetworkId: true, + requireMemberId: true, + }, + async (_req, res, { userId, networkId, memberId, ctx }) => { + try { + // make sure the member is valid + const network = await prisma.network.findUnique({ + where: { nwid: networkId, authorId: userId }, + include: { + networkMembers: { + where: { id: memberId }, + }, }, - }, - }); + }); - if (!network?.networkMembers || network.networkMembers.length === 0) { - return res - .status(401) - .json({ error: "Member or Network not found or access denied." }); - } + if (!network?.networkMembers || network.networkMembers.length === 0) { + return res + .status(401) + .json({ error: "Member or Network not found or access denied." }); + } - // @ts-expect-error - const caller = appRouter.createCaller(ctx); - const networkAndMembers = await caller.networkMember.stash({ - nwid: networkId, - id: memberId, - }); + // @ts-expect-error + const caller = appRouter.createCaller(ctx); + const networkAndMembers = await caller.networkMember.stash({ + nwid: networkId, + id: memberId, + }); - return res.status(200).json(networkAndMembers); - } catch (cause) { - return handleApiErrors(cause, res); - } -}; + return res.status(200).json(networkAndMembers); + } catch (cause) { + return handleApiErrors(cause, res); + } + }, +); diff --git a/src/pages/api/v1/network/[id]/member/index.ts b/src/pages/api/v1/network/[id]/member/index.ts index 4b01a2a1..2e88f70f 100644 --- a/src/pages/api/v1/network/[id]/member/index.ts +++ b/src/pages/api/v1/network/[id]/member/index.ts @@ -1,7 +1,6 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { prisma } from "~/server/db"; -import { AuthorizationType } from "~/types/apiTypes"; -import { decryptAndVerifyToken } from "~/utils/encryption"; +import { SecuredPrivateApiRoute } from "~/utils/apiRouteAuth"; import { handleApiErrors } from "~/utils/errors"; import rateLimit from "~/utils/rateLimit"; import * as ztController from "~/utils/ztApi"; @@ -35,64 +34,36 @@ export default async function apiNetworkMembersHandler( } } -const GET_networkMembers = async (req: NextApiRequest, res: NextApiResponse) => { - const apiKey = req.headers["x-ztnet-auth"] as string; - const networkId = req.query?.id as string; - - // Check if the networkId exists - if (!networkId) { - return res.status(400).json({ error: "Network ID is required" }); - } - - try { - const decryptedData: { userId: string; name?: string } = await decryptAndVerifyToken({ - apiKey, - apiAuthorizationType: AuthorizationType.PERSONAL, - }); - - // assemble the context object - const ctx = { - session: { - user: { - id: decryptedData.userId as string, +const GET_networkMembers = SecuredPrivateApiRoute( + { + requireNetworkId: true, + }, + async (_req, res, { networkId, ctx }) => { + try { + const arr = []; + const networks = await prisma.network.findUnique({ + where: { + nwid: networkId, }, - }, - prisma, - }; - - // make sure user has access to the network - const network = await prisma.network.findUnique({ - where: { nwid: networkId, authorId: decryptedData.userId }, - select: { nwid: true, name: true, authorId: true }, - }); - - if (!network) { - return res.status(401).json({ error: "Network not found or access denied." }); - } + include: { + networkMembers: true, + }, + }); - const arr = []; - const networks = await prisma.network.findUnique({ - where: { - nwid: networkId, - }, - include: { - networkMembers: true, - }, - }); + for (const member of networks.networkMembers) { + const controllerMember = await ztController.member_details( + //@ts-expect-error + ctx, + networkId, + member.id, + false, + ); + arr.push({ ...member, ...controllerMember }); + } - for (const member of networks.networkMembers) { - const controllerMember = await ztController.member_details( - //@ts-expect-error - ctx, - networkId, - member.id, - false, - ); - arr.push({ ...member, ...controllerMember }); + return res.status(200).json(arr); + } catch (cause) { + return handleApiErrors(cause, res); } - - return res.status(200).json(arr); - } catch (cause) { - return handleApiErrors(cause, res); - } -}; + }, +); diff --git a/src/pages/api/v1/network/index.ts b/src/pages/api/v1/network/index.ts index 4d3a3a25..db7fa8a6 100644 --- a/src/pages/api/v1/network/index.ts +++ b/src/pages/api/v1/network/index.ts @@ -1,8 +1,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { networkProvisioningFactory } from "~/server/api/services/networkService"; import { prisma } from "~/server/db"; -import { AuthorizationType } from "~/types/apiTypes"; -import { decryptAndVerifyToken } from "~/utils/encryption"; +import { SecuredPrivateApiRoute } from "~/utils/apiRouteAuth"; import { handleApiErrors } from "~/utils/errors"; import rateLimit from "~/utils/rateLimit"; import * as ztController from "~/utils/ztApi"; @@ -39,81 +38,56 @@ export default async function apiNetworkHandler( } } -const POST_createNewNetwork = async (req: NextApiRequest, res: NextApiResponse) => { - const apiKey = req.headers["x-ztnet-auth"] as string; - // If there are users, verify the API key - try { - const decryptedData: { userId: string; name: string } = await decryptAndVerifyToken({ - apiKey, - apiAuthorizationType: AuthorizationType.PERSONAL, - }); - - const { name } = req.body; - - const ctx = { - session: { - user: { - id: decryptedData.userId as string, - }, - }, - prisma, - }; - const newNetworkId = await networkProvisioningFactory({ - ctx, - input: { central: false, name }, - }); +const POST_createNewNetwork = SecuredPrivateApiRoute( + { + requireNetworkId: false, + }, + async (_req, res, { body, ctx }) => { + // If there are users, verify the API key + try { + const { name } = body; - return res.status(200).json(newNetworkId); - } catch (cause) { - return handleApiErrors(cause, res); - } -}; + const newNetworkId = await networkProvisioningFactory({ + ctx, + input: { central: false, name }, + }); -const GET_userNetworks = async (req: NextApiRequest, res: NextApiResponse) => { - const apiKey = req.headers["x-ztnet-auth"] as string; + return res.status(200).json(newNetworkId); + } catch (cause) { + return handleApiErrors(cause, res); + } + }, +); - let decryptedData: { userId: string; name?: string }; - try { - decryptedData = await decryptAndVerifyToken({ - apiKey, - apiAuthorizationType: AuthorizationType.PERSONAL, - }); - } catch (error) { - return res.status(401).json({ error: error.message }); - } +const GET_userNetworks = SecuredPrivateApiRoute( + { + requireNetworkId: false, + }, + async (_req, res, { ctx, userId }) => { + try { + const networks = await prisma.user.findFirst({ + where: { + id: userId, + }, + select: { + network: true, + }, + }); + const arr = []; + // biome-ignore lint/correctness/noUnsafeOptionalChaining: + for (const network of networks?.network) { + const ztControllerResponse = await ztController.local_network_detail( + //@ts-expect-error + ctx, + network.nwid, + false, + ); + arr.push(ztControllerResponse.network); + } - // If there are users, verify the API key - const ctx = { - session: { - user: { - id: decryptedData.userId as string, - }, - }, - prisma, - }; - try { - const networks = await prisma.user.findFirst({ - where: { - id: decryptedData.userId, - }, - select: { - network: true, - }, - }); - const arr = []; - // biome-ignore lint/correctness/noUnsafeOptionalChaining: - for (const network of networks?.network) { - const ztControllerResponse = await ztController.local_network_detail( - //@ts-expect-error - ctx, - network.nwid, - false, - ); - arr.push(ztControllerResponse.network); + return res.status(200).json(arr); + } catch (cause) { + return handleApiErrors(cause, res); } - - return res.status(200).json(arr); - } catch (cause) { - return handleApiErrors(cause, res); - } -}; + }, +); diff --git a/src/utils/apiRouteAuth.ts b/src/utils/apiRouteAuth.ts index 769e75ca..e5900acc 100644 --- a/src/utils/apiRouteAuth.ts +++ b/src/utils/apiRouteAuth.ts @@ -9,7 +9,7 @@ import { AuthorizationType } from "~/types/apiTypes"; /** * Organization API handler wrapper for apir routes that require authentication */ -type ApiHandler = ( +type OrgApiHandler = ( req: NextApiRequest, res: NextApiResponse, context: { @@ -36,7 +36,7 @@ export const SecuredOrganizationApiRoute = ( requireNetworkId?: boolean; requireOrgId?: boolean; }, - handler: ApiHandler, + handler: OrgApiHandler, ) => { return async (req: NextApiRequest, res: NextApiResponse) => { const apiKey = req.headers["x-ztnet-auth"] as string; @@ -97,3 +97,98 @@ export const SecuredOrganizationApiRoute = ( } }; }; + +type UserApiHandler = ( + req: NextApiRequest, + res: NextApiResponse, + context: { + // biome-ignore lint/suspicious/noExplicitAny: + body: any; + userId: string; + networkId?: string; + memberId?: string; + ctx: { + session: { + user: { + id: string; + }; + }; + prisma: typeof prisma; + }; + }, +) => Promise; + +export const SecuredPrivateApiRoute = ( + options: { + requireNetworkId?: boolean; + requireMemberId?: boolean; + }, + handler: UserApiHandler, +) => { + return async (req: NextApiRequest, res: NextApiResponse) => { + const apiKey = req.headers["x-ztnet-auth"] as string; + const networkId = req.query?.id as string; + const memberId = req.query?.memberId as string; + const body = req.body; + + const mergedOptions = { + // Set networkId as required by default + requireNetworkId: true, + ...options, + }; + + try { + if (!apiKey) { + return res.status(400).json({ error: "API Key is required" }); + } + + if (mergedOptions.requireNetworkId && !networkId) { + return res.status(400).json({ error: "Network ID is required" }); + } + + if (mergedOptions.requireMemberId && !memberId) { + return res.status(400).json({ error: "Member ID is required" }); + } + + const decryptedData = await decryptAndVerifyToken({ + apiKey, + apiAuthorizationType: AuthorizationType.PERSONAL, + }); + + if (mergedOptions.requireNetworkId) { + // make sure the user is the owner of the network + const userIsAuthor = await prisma.network.findUnique({ + where: { nwid: networkId, authorId: decryptedData.userId }, + select: { authorId: true, description: true }, + }); + + if ( + (networkId && !userIsAuthor) || + userIsAuthor.authorId !== decryptedData.userId + ) { + return res.status(401).json({ error: "Network not found or access denied." }); + } + } + + const ctx = { + session: { + user: { + id: decryptedData.userId as string, + }, + }, + prisma, + }; + + await handler(req, res, { + body, + userId: decryptedData.userId, + networkId, + memberId, + ctx, + }); + } catch (cause) { + console.error("catch cause", cause); + return handleApiErrors(cause, res); + } + }; +}; diff --git a/src/utils/encryption.ts b/src/utils/encryption.ts index 4cabfcb5..d9e89ee2 100644 --- a/src/utils/encryption.ts +++ b/src/utils/encryption.ts @@ -96,6 +96,7 @@ export async function decryptAndVerifyToken({ } catch (_error) { throw new Error("Invalid token"); } + // Validate the decrypted data structure (add more validations as necessary) if ( !decryptedData.userId || From a2e0c44e208cf53b79ae7a7e44318f1dbc0db85b Mon Sep 17 00:00:00 2001 From: Bernt Christian Egeland Date: Fri, 30 Aug 2024 09:40:10 +0000 Subject: [PATCH 5/6] SecuredPrivateApiRoute --- src/pages/api/v1/stats/index.ts | 125 +++++++++++++++----------------- src/utils/apiRouteAuth.ts | 2 + 2 files changed, 62 insertions(+), 65 deletions(-) diff --git a/src/pages/api/v1/stats/index.ts b/src/pages/api/v1/stats/index.ts index a1d95238..2458039c 100644 --- a/src/pages/api/v1/stats/index.ts +++ b/src/pages/api/v1/stats/index.ts @@ -1,7 +1,6 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { prisma } from "~/server/db"; -import { AuthorizationType } from "~/types/apiTypes"; -import { decryptAndVerifyToken } from "~/utils/encryption"; +import { SecuredPrivateApiRoute } from "~/utils/apiRouteAuth"; import { handleApiErrors } from "~/utils/errors"; import { globalSiteVersion } from "~/utils/global"; import rateLimit from "~/utils/rateLimit"; @@ -35,72 +34,68 @@ export default async function createStatsHandler( } } -export const GET_stats = async (req: NextApiRequest, res: NextApiResponse) => { - const apiKey = req.headers["x-ztnet-auth"] as string; - - const requireAdmin = true; - - try { - await decryptAndVerifyToken({ - apiKey, - apiAuthorizationType: AuthorizationType.PERSONAL, - requireAdmin, - }); - - // get number of users - const users = await prisma.user.count(); - - // get number of networks - const networks = await prisma.network.count(); - - // get number of members - const networkMembers = await prisma.network_members.count(); - - // get application version - const appVersion = globalSiteVersion; - - // get logins last 24 hours - const loginsLast24h = await prisma.user.count({ - where: { - lastLogin: { - gte: new Date(new Date().getTime() - 24 * 60 * 60 * 1000), +export const GET_stats = SecuredPrivateApiRoute( + { + requireNetworkId: false, + requireAdmin: true, + }, + async (_req, res) => { + try { + // get number of users + const users = await prisma.user.count(); + + // get number of networks + const networks = await prisma.network.count(); + + // get number of members + const networkMembers = await prisma.network_members.count(); + + // get application version + const appVersion = globalSiteVersion; + + // get logins last 24 hours + const loginsLast24h = await prisma.user.count({ + where: { + lastLogin: { + gte: new Date(new Date().getTime() - 24 * 60 * 60 * 1000), + }, }, - }, - }); - - // get UserInvitation - const pendingUserInvitations = await prisma.invitation.count(); + }); - // get pending Webhook - const activeWebhooks = await prisma.webhook.count(); + // get UserInvitation + const pendingUserInvitations = await prisma.invitation.count(); - // get uptime - const ztnetUptime = process.uptime(); + // get pending Webhook + const activeWebhooks = await prisma.webhook.count(); - // get if customPlanetUsed is used - const rootServer = await prisma.planet.count(); + // get uptime + const ztnetUptime = process.uptime(); - // get global options - const globalOptions = await prisma.globalOptions.findFirst({ - where: { - id: 1, - }, - }); + // get if customPlanetUsed is used + const rootServer = await prisma.planet.count(); - // return all json - return res.status(200).json({ - users, - networks, - networkMembers, - appVersion, - loginsLast24h, - pendingUserInvitations, - activeWebhooks, - ztnetUptime, - registrationEnabled: globalOptions?.enableRegistration || false, - hasPrivatRoot: !!rootServer, - }); - } catch (cause) { - return handleApiErrors(cause, res); - } -}; + // get global options + const globalOptions = await prisma.globalOptions.findFirst({ + where: { + id: 1, + }, + }); + + // return all json + return res.status(200).json({ + users, + networks, + networkMembers, + appVersion, + loginsLast24h, + pendingUserInvitations, + activeWebhooks, + ztnetUptime, + registrationEnabled: globalOptions?.enableRegistration || false, + hasPrivatRoot: !!rootServer, + }); + } catch (cause) { + return handleApiErrors(cause, res); + } + }, +); diff --git a/src/utils/apiRouteAuth.ts b/src/utils/apiRouteAuth.ts index e5900acc..7a5fb5e9 100644 --- a/src/utils/apiRouteAuth.ts +++ b/src/utils/apiRouteAuth.ts @@ -122,6 +122,7 @@ export const SecuredPrivateApiRoute = ( options: { requireNetworkId?: boolean; requireMemberId?: boolean; + requireAdmin?: boolean; }, handler: UserApiHandler, ) => { @@ -153,6 +154,7 @@ export const SecuredPrivateApiRoute = ( const decryptedData = await decryptAndVerifyToken({ apiKey, apiAuthorizationType: AuthorizationType.PERSONAL, + requireAdmin: mergedOptions.requireAdmin, }); if (mergedOptions.requireNetworkId) { From a9d083d69b09d1fbb8754d4a9a53290da0e009e7 Mon Sep 17 00:00:00 2001 From: Bernt Christian Egeland Date: Fri, 30 Aug 2024 09:44:03 +0000 Subject: [PATCH 6/6] tests --- .../__tests__/v1/network/networkById.test.ts | 264 +++++++++--------- 1 file changed, 132 insertions(+), 132 deletions(-) diff --git a/src/pages/api/__tests__/v1/network/networkById.test.ts b/src/pages/api/__tests__/v1/network/networkById.test.ts index 6a1187e8..31846e07 100644 --- a/src/pages/api/__tests__/v1/network/networkById.test.ts +++ b/src/pages/api/__tests__/v1/network/networkById.test.ts @@ -37,78 +37,78 @@ describe("/api/getNetworkById", () => { beforeEach(() => { jest.clearAllMocks(); }); - // it("should respond 200 when network is found", async () => { - // // Mock the decryption to return a valid user ID - // (encryptionModule.decryptAndVerifyToken as jest.Mock).mockResolvedValue({ - // userId: "userId", - // }); - - // // Mock the database to return a network - // prisma.network.findUnique = jest.fn().mockResolvedValue({ - // nwid: "test_nw_id", - // nwname: "credent_second", - // authorId: "userId", - // }); - - // // Mock the ztController to return a network detail - // (ztController.local_network_detail as jest.Mock).mockResolvedValue({ - // network: { id: "networkId", name: "networkName" }, - // }); - - // const req = { - // method: "GET", - // headers: { "x-ztnet-auth": "validApiKey" }, - // query: { id: "networkId" }, - // } as unknown as NextApiRequest; - - // const res = { - // status: jest.fn().mockReturnThis(), - // json: jest.fn(), - // end: jest.fn(), - // setHeader: jest.fn(), // Mock `setHeader` rate limiter uses it - // } as unknown as NextApiResponse; - - // await apiNetworkByIdHandler(req, res); - - // expect(res.status).toHaveBeenCalledWith(200); - // expect(res.json).toHaveBeenCalledWith({ - // id: "networkId", - // name: "networkName", - // authorId: "userId", - // nwid: "test_nw_id", - // nwname: "credent_second", - // }); - // }); - - // it("should respond 401 when network is not found", async () => { - // // Mock the decryption to return a valid user ID - // (encryptionModule.decryptAndVerifyToken as jest.Mock).mockResolvedValue({ - // userId: "userId", - // }); - - // // Mock the database to return a network - // prisma.network.findUnique = jest.fn().mockResolvedValue(null); - - // const req = { - // method: "GET", - // headers: { "x-ztnet-auth": "validApiKey" }, - // query: { id: "networkId" }, - // } as unknown as NextApiRequest; - - // const res = { - // status: jest.fn().mockReturnThis(), - // json: jest.fn(), - // end: jest.fn(), - // setHeader: jest.fn(), // Mock `setHeader` rate limiter uses it - // } as unknown as NextApiResponse; - - // await apiNetworkByIdHandler(req, res); - - // expect(res.status).toHaveBeenCalledWith(401); - // expect(res.json).toHaveBeenCalledWith({ - // error: "Network not found or access denied.", - // }); - // }); + it("should respond 200 when network is found", async () => { + // Mock the decryption to return a valid user ID + (encryptionModule.decryptAndVerifyToken as jest.Mock).mockResolvedValue({ + userId: "userId", + }); + + // Mock the database to return a network + prisma.network.findUnique = jest.fn().mockResolvedValue({ + nwid: "test_nw_id", + nwname: "credent_second", + authorId: "userId", + }); + + // Mock the ztController to return a network detail + (ztController.local_network_detail as jest.Mock).mockResolvedValue({ + network: { id: "networkId", name: "networkName" }, + }); + + const req = { + method: "GET", + headers: { "x-ztnet-auth": "validApiKey" }, + query: { id: "networkId" }, + } as unknown as NextApiRequest; + + const res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + end: jest.fn(), + setHeader: jest.fn(), // Mock `setHeader` rate limiter uses it + } as unknown as NextApiResponse; + + await apiNetworkByIdHandler(req, res); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + id: "networkId", + name: "networkName", + authorId: "userId", + nwid: "test_nw_id", + nwname: "credent_second", + }); + }); + + it("should respond 401 when network is not found", async () => { + // Mock the decryption to return a valid user ID + (encryptionModule.decryptAndVerifyToken as jest.Mock).mockResolvedValue({ + userId: "userId", + }); + + // Mock the database to return a network + prisma.network.findUnique = jest.fn().mockResolvedValue(null); + + const req = { + method: "GET", + headers: { "x-ztnet-auth": "validApiKey" }, + query: { id: "networkId" }, + } as unknown as NextApiRequest; + + const res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + end: jest.fn(), + setHeader: jest.fn(), // Mock `setHeader` rate limiter uses it + } as unknown as NextApiResponse; + + await apiNetworkByIdHandler(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ + error: "Network not found or access denied.", + }); + }); it("should respond with an error when ztController throws an error", async () => { // Mock the decryption to return a valid user ID @@ -151,64 +151,64 @@ describe("/api/getNetworkById", () => { expect(res.json).toHaveBeenCalledWith({ error: error.message }); }); - // it("should respond 401 when decryptAndVerifyToken fails", async () => { - // // Mock the decryption to fail - // (encryptionModule.decryptAndVerifyToken as jest.Mock).mockRejectedValue( - // new Error("Invalid token"), - // ); - - // const req = { - // method: "GET", - // headers: { "x-ztnet-auth": "invalidApiKey" }, - // query: { id: "networkId" }, - // } as unknown as NextApiRequest; - - // const res = { - // status: jest.fn().mockReturnThis(), - // json: jest.fn(), - // end: jest.fn(), - // setHeader: jest.fn(), // Mock `setHeader` if rate limiter uses it - // } as unknown as NextApiResponse; - - // await apiNetworkByIdHandler(req, res); - - // expect(res.status).toHaveBeenCalledWith(401); - // expect(res.json).toHaveBeenCalledWith( - // expect.objectContaining({ error: expect.any(String) }), - // ); - // }); - - // it("should respond 401 when user is not the author of the network", async () => { - // // Mock the decryption to return a valid user ID - // (encryptionModule.decryptAndVerifyToken as jest.Mock).mockResolvedValue({ - // userId: "userId", - // }); - - // const req = { - // method: "GET", - // headers: { "x-ztnet-auth": "validApiKey" }, - // query: { id: "networkIdThatUserDoesNotOwn" }, - // } as unknown as NextApiRequest; - - // const res = { - // status: jest.fn().mockReturnThis(), - // end: jest.fn(), - // json: jest.fn().mockReturnThis(), - // setHeader: jest.fn(), // Mock `setHeader` rate limiter uses it - // } as unknown as NextApiResponse; - - // // Mock the database to return a network - // prisma.network.findUnique = jest.fn().mockResolvedValue({ - // nwid: "networkIdThatUserDoesNotOwn", - // nwname: "Some Network", - // authorId: "anotherUserId", - // }); - - // await apiNetworkByIdHandler(req, res); - - // expect(res.status).toHaveBeenCalledWith(401); - // expect(res.json).toHaveBeenCalledWith({ - // error: "Network not found or access denied.", - // }); - // }); + it("should respond 401 when decryptAndVerifyToken fails", async () => { + // Mock the decryption to fail + (encryptionModule.decryptAndVerifyToken as jest.Mock).mockRejectedValue( + new Error("Invalid token"), + ); + + const req = { + method: "GET", + headers: { "x-ztnet-auth": "invalidApiKey" }, + query: { id: "networkId" }, + } as unknown as NextApiRequest; + + const res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + end: jest.fn(), + setHeader: jest.fn(), // Mock `setHeader` if rate limiter uses it + } as unknown as NextApiResponse; + + await apiNetworkByIdHandler(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ error: expect.any(String) }), + ); + }); + + it("should respond 401 when user is not the author of the network", async () => { + // Mock the decryption to return a valid user ID + (encryptionModule.decryptAndVerifyToken as jest.Mock).mockResolvedValue({ + userId: "userId", + }); + + const req = { + method: "GET", + headers: { "x-ztnet-auth": "validApiKey" }, + query: { id: "networkIdThatUserDoesNotOwn" }, + } as unknown as NextApiRequest; + + const res = { + status: jest.fn().mockReturnThis(), + end: jest.fn(), + json: jest.fn().mockReturnThis(), + setHeader: jest.fn(), // Mock `setHeader` rate limiter uses it + } as unknown as NextApiResponse; + + // Mock the database to return a network + prisma.network.findUnique = jest.fn().mockResolvedValue({ + nwid: "networkIdThatUserDoesNotOwn", + nwname: "Some Network", + authorId: "anotherUserId", + }); + + await apiNetworkByIdHandler(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ + error: "Network not found or access denied.", + }); + }); });