From d38656e026fbaa9b751c3beeaabcb92a02f91032 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 30 Jul 2025 21:31:16 -0700 Subject: [PATCH] add clients to int api --- docker-compose.yml | 3 +- messages/en-US.json | 5 ++ server/middlewares/integration/index.ts | 1 + .../integration/verifyApiKeyClientAccess.ts | 86 +++++++++++++++++++ server/routers/client/getClient.ts | 18 ++-- server/routers/client/pickClientDefaults.ts | 2 +- server/routers/integration.ts | 47 +++++++++- .../[orgId]/settings/clients/create/page.tsx | 2 +- src/components/PermissionsSelectBox.tsx | 8 ++ 9 files changed, 158 insertions(+), 14 deletions(-) create mode 100644 server/middlewares/integration/verifyApiKeyClientAccess.ts diff --git a/docker-compose.yml b/docker-compose.yml index 49713379..09b150d7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,7 @@ services: - "3000:3000" - "3001:3001" - "3002:3002" + - "3003:3003" environment: - NODE_ENV=development - ENVIRONMENT=dev @@ -26,4 +27,4 @@ services: - ./postcss.config.mjs:/app/postcss.config.mjs - ./eslint.config.js:/app/eslint.config.js - ./config:/app/config - restart: no \ No newline at end of file + restart: no diff --git a/messages/en-US.json b/messages/en-US.json index e69e2b46..9986c5fd 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1022,6 +1022,11 @@ "actionDeleteIdpOrg": "Delete IDP Org Policy", "actionListIdpOrgs": "List IDP Orgs", "actionUpdateIdpOrg": "Update IDP Org", + "actionCreateClient": "Create Client", + "actionDeleteClient": "Delete Client", + "actionUpdateClient": "Update Client", + "actionListClients": "List Clients", + "actionGetClient": "Get Client", "noneSelected": "None selected", "orgNotFound2": "No organizations found.", "searchProgress": "Search...", diff --git a/server/middlewares/integration/index.ts b/server/middlewares/integration/index.ts index 19bf128e..4caf017b 100644 --- a/server/middlewares/integration/index.ts +++ b/server/middlewares/integration/index.ts @@ -10,3 +10,4 @@ export * from "./verifyApiKeySetResourceUsers"; export * from "./verifyAccessTokenAccess"; export * from "./verifyApiKeyIsRoot"; export * from "./verifyApiKeyApiKeyAccess"; +export * from "./verifyApiKeyClientAccess"; diff --git a/server/middlewares/integration/verifyApiKeyClientAccess.ts b/server/middlewares/integration/verifyApiKeyClientAccess.ts new file mode 100644 index 00000000..3583ef4d --- /dev/null +++ b/server/middlewares/integration/verifyApiKeyClientAccess.ts @@ -0,0 +1,86 @@ +import { Request, Response, NextFunction } from "express"; +import { clients, db } from "@server/db"; +import { apiKeyOrg } from "@server/db"; +import { and, eq } from "drizzle-orm"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; + +export async function verifyApiKeyClientAccess( + req: Request, + res: Response, + next: NextFunction +) { + try { + const apiKey = req.apiKey; + const clientId = parseInt( + req.params.clientId || req.body.clientId || req.query.clientId + ); + + if (!apiKey) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") + ); + } + + if (isNaN(clientId)) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid client ID") + ); + } + + const client = await db + .select() + .from(clients) + .where(eq(clients.clientId, clientId)) + .limit(1); + + if (client.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Client with ID ${clientId} not found` + ) + ); + } + + if (!client[0].orgId) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + `Client with ID ${clientId} does not have an organization ID` + ) + ); + } + + if (!req.apiKeyOrg) { + const apiKeyOrgRes = await db + .select() + .from(apiKeyOrg) + .where( + and( + eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), + eq(apiKeyOrg.orgId, client[0].orgId) + ) + ); + req.apiKeyOrg = apiKeyOrgRes[0]; + } + + if (!req.apiKeyOrg) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Key does not have access to this organization" + ) + ); + } + + return next(); + } catch (error) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error verifying site access" + ) + ); + } +} diff --git a/server/routers/client/getClient.ts b/server/routers/client/getClient.ts index 46f31b8c..6700d97c 100644 --- a/server/routers/client/getClient.ts +++ b/server/routers/client/getClient.ts @@ -18,28 +18,28 @@ const getClientSchema = z }) .strict(); -async function query(clientId: number) { +async function query(clientId: number, orgId: string) { // Get the client const [client] = await db .select() .from(clients) - .where(eq(clients.clientId, clientId)) + .where(and(eq(clients.clientId, clientId), eq(clients.orgId, orgId))) .limit(1); - + if (!client) { return null; } - + // Get the siteIds associated with this client const sites = await db .select({ siteId: clientSites.siteId }) .from(clientSites) .where(eq(clientSites.clientId, clientId)); - + // Add the siteIds to the client object return { ...client, - siteIds: sites.map(site => site.siteId) + siteIds: sites.map((site) => site.siteId) }; } @@ -75,9 +75,9 @@ export async function getClient( ); } - const { clientId } = parsedParams.data; + const { clientId, orgId } = parsedParams.data; - const client = await query(clientId); + const client = await query(clientId, orgId); if (!client) { return next( @@ -98,4 +98,4 @@ export async function getClient( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } -} \ No newline at end of file +} diff --git a/server/routers/client/pickClientDefaults.ts b/server/routers/client/pickClientDefaults.ts index b1459400..6f452142 100644 --- a/server/routers/client/pickClientDefaults.ts +++ b/server/routers/client/pickClientDefaults.ts @@ -23,7 +23,7 @@ const pickClientDefaultsSchema = z registry.registerPath({ method: "get", - path: "/site/{siteId}/pick-client-defaults", + path: "/org/{orgId}/pick-client-defaults", description: "Return pre-requisite data for creating a client.", tags: [OpenAPITags.Client, OpenAPITags.Site], request: { diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 51604a11..64675676 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -5,7 +5,7 @@ import * as domain from "./domain"; import * as target from "./target"; import * as user from "./user"; import * as role from "./role"; -// import * as client from "./client"; +import * as client from "./client"; import * as accessToken from "./accessToken"; import * as apiKeys from "./apiKeys"; import * as idp from "./idp"; @@ -20,7 +20,8 @@ import { verifyApiKeyUserAccess, verifyApiKeySetResourceUsers, verifyApiKeyAccessTokenAccess, - verifyApiKeyIsRoot + verifyApiKeyIsRoot, + verifyApiKeyClientAccess } from "@server/middlewares"; import HttpCode from "@server/types/HttpCode"; import { Router } from "express"; @@ -513,3 +514,45 @@ authenticated.get( verifyApiKeyHasAction(ActionsEnum.listIdpOrgs), idp.listIdpOrgPolicies ); + +authenticated.get( + "/org/:orgId/pick-client-defaults", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.createClient), + client.pickClientDefaults +); + +authenticated.get( + "/org/:orgId/clients", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.listClients), + client.listClients +); + +authenticated.get( + "/org/:orgId/client/:clientId", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.getClient), + client.getClient +); + +authenticated.put( + "/org/:orgId/client", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.createClient), + client.createClient +); + +authenticated.delete( + "/client/:clientId", + verifyApiKeyClientAccess, + verifyApiKeyHasAction(ActionsEnum.deleteClient), + client.deleteClient +); + +authenticated.post( + "/client/:clientId", + verifyApiKeyClientAccess, + verifyApiKeyHasAction(ActionsEnum.updateClient), + client.updateClient +); diff --git a/src/app/[orgId]/settings/clients/create/page.tsx b/src/app/[orgId]/settings/clients/create/page.tsx index 00b6b34c..2497d3f8 100644 --- a/src/app/[orgId]/settings/clients/create/page.tsx +++ b/src/app/[orgId]/settings/clients/create/page.tsx @@ -705,4 +705,4 @@ export default function Page() { )} ); -} \ No newline at end of file +} diff --git a/src/components/PermissionsSelectBox.tsx b/src/components/PermissionsSelectBox.tsx index 6f11d98e..848d116c 100644 --- a/src/components/PermissionsSelectBox.tsx +++ b/src/components/PermissionsSelectBox.tsx @@ -82,6 +82,14 @@ function getActionsCategories(root: boolean) { [t('actionDeleteResourceRule')]: "deleteResourceRule", [t('actionListResourceRules')]: "listResourceRules", [t('actionUpdateResourceRule')]: "updateResourceRule" + }, + + "Client": { + [t('actionCreateClient')]: "createClient", + [t('actionDeleteClient')]: "deleteClient", + [t('actionUpdateClient')]: "updateClient", + [t('actionListClients')]: "listClients", + [t('actionGetClient')]: "getClient" } };