diff --git a/server/routers/external.ts b/server/routers/external.ts index 6ad48c26..df44c599 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -499,7 +499,25 @@ authenticated.put( verifyUserIsServerAdmin, // verifyUserHasAction(ActionsEnum.createIdp), idp.createOidcIdp -) +); + +authenticated.delete( + "/idp/:idpId", + verifyUserIsServerAdmin, + idp.deleteIdp +); + +authenticated.get( + "/idp", + verifyUserIsServerAdmin, + idp.listIdps +); + +authenticated.get( + "/idp/:idpId", + verifyUserIsServerAdmin, + idp.getIdp +); // Auth routes export const authRouter = Router(); @@ -597,9 +615,9 @@ authRouter.post( authRouter.post( "/idp/:idpId/oidc/generate-url", idp.generateOidcUrl -) +); authRouter.post( "/idp/:idpId/oidc/validate-callback", idp.validateOidcCallback -) +); diff --git a/server/routers/idp/createOidcIdp.ts b/server/routers/idp/createOidcIdp.ts index b07d2872..1090b060 100644 --- a/server/routers/idp/createOidcIdp.ts +++ b/server/routers/idp/createOidcIdp.ts @@ -36,9 +36,9 @@ export type CreateIdpResponse = { registry.registerPath({ method: "put", - path: "/org/{orgId}/idp/oidc", - description: "Create an OIDC IdP for an organization.", - tags: [OpenAPITags.Org, OpenAPITags.Idp], + path: "/idp/oidc", + description: "Create an OIDC IdP.", + tags: [OpenAPITags.Idp], request: { body: { content: { diff --git a/server/routers/idp/deleteIdp.ts b/server/routers/idp/deleteIdp.ts new file mode 100644 index 00000000..e7878ed5 --- /dev/null +++ b/server/routers/idp/deleteIdp.ts @@ -0,0 +1,100 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { idp, idpOidcConfig, idpOrg } from "@server/db/schemas"; +import { eq } from "drizzle-orm"; +import { OpenAPITags, registry } from "@server/openApi"; + +const paramsSchema = z + .object({ + idpId: z.coerce.number() + }) + .strict(); + +registry.registerPath({ + method: "delete", + path: "/org/{orgId}/idp/oidc", + description: "Create an OIDC IdP for an organization.", + tags: [OpenAPITags.Org, OpenAPITags.Idp], + request: { + body: { + content: { + "application/json": { + schema: bodySchema + } + } + } + }, + responses: {} +}); + +export async function deleteIdp( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { idpId } = parsedParams.data; + + // Check if IDP exists + const [existingIdp] = await db + .select() + .from(idp) + .where(eq(idp.idpId, idpId)); + + if (!existingIdp) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "IdP not found" + ) + ); + } + + // Delete the IDP and its related records in a transaction + await db.transaction(async (trx) => { + // Delete OIDC config if it exists + await trx + .delete(idpOidcConfig) + .where(eq(idpOidcConfig.idpId, idpId)); + + // Delete IDP-org mappings + await trx + .delete(idpOrg) + .where(eq(idpOrg.idpId, idpId)); + + // Delete the IDP itself + await trx + .delete(idp) + .where(eq(idp.idpId, idpId)); + }); + + return response(res, { + data: null, + success: true, + error: false, + message: "IdP deleted successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/idp/getIdp.ts b/server/routers/idp/getIdp.ts new file mode 100644 index 00000000..6598b542 --- /dev/null +++ b/server/routers/idp/getIdp.ts @@ -0,0 +1,79 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { idp, idpOidcConfig } from "@server/db/schemas"; +import { eq } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; + +const paramsSchema = z + .object({ + idpId: z.coerce.number() + }) + .strict(); + +async function query(idpId: number) { + const [res] = await db + .select() + .from(idp) + .where(eq(idp.idpId, idpId)) + .leftJoin(idpOidcConfig, eq(idpOidcConfig.idpId, idp.idpId)) + .limit(1); + return res; +} + +export type GetIdpResponse = NonNullable>>; + +registry.registerPath({ + method: "get", + path: "/idp/{idpId}", + description: "Get an IDP by its IDP ID.", + tags: [OpenAPITags.Idp], + request: { + params: paramsSchema + }, + responses: {} +}); + +export async function getIdp( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { idpId } = parsedParams.data; + + const idpRes = await query(idpId); + + if (!idpRes) { + return next(createHttpError(HttpCode.NOT_FOUND, "Idp not found")); + } + + return response(res, { + data: idpRes, + success: true, + error: false, + message: "Idp retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/idp/index.ts b/server/routers/idp/index.ts index 70fbbcae..f9dfc5fc 100644 --- a/server/routers/idp/index.ts +++ b/server/routers/idp/index.ts @@ -1,3 +1,6 @@ export * from "./createOidcIdp"; +export * from "./deleteIdp"; +export * from "./listIdps"; export * from "./generateOidcUrl"; export * from "./validateOidcCallback"; +export * from "./getIdp"; diff --git a/server/routers/idp/listIdps.ts b/server/routers/idp/listIdps.ts new file mode 100644 index 00000000..0a7c5e4c --- /dev/null +++ b/server/routers/idp/listIdps.ts @@ -0,0 +1,94 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { domains, orgDomains, users } from "@server/db/schemas"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { sql } from "drizzle-orm"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; + +const querySchema = z + .object({ + limit: z + .string() + .optional() + .default("1000") + .transform(Number) + .pipe(z.number().int().nonnegative()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.number().int().nonnegative()) + }) + .strict(); + +async function query(limit: number, offset: number) { + const res = await db.select().from(orgDomains).limit(limit).offset(offset); + return res; +} + +export type ListIdpResponse = { + idps: NonNullable>>; + pagination: { total: number; limit: number; offset: number }; +}; + +registry.registerPath({ + method: "get", + path: "/idp", + description: "List all IDP in the system.", + tags: [OpenAPITags.Idp], + request: { + query: querySchema + }, + responses: {} +}); + +export async function listIdps( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedQuery = querySchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedQuery.error).toString() + ) + ); + } + const { limit, offset } = parsedQuery.data; + + const list = await query(limit, offset); + + const [{ count }] = await db + .select({ count: sql`count(*)` }) + .from(domains); + + return response(res, { + data: { + idps: list, + pagination: { + total: count, + limit, + offset + } + }, + success: true, + error: false, + message: "Users retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/src/app/[orgId]/settings/access/users/UsersTable.tsx b/src/app/[orgId]/settings/access/users/UsersTable.tsx index 515bd2c2..ea642800 100644 --- a/src/app/[orgId]/settings/access/users/UsersTable.tsx +++ b/src/app/[orgId]/settings/access/users/UsersTable.tsx @@ -88,8 +88,8 @@ export default function UsersTable({ users: u }: UsersTableProps) { Manage User - {userRow.username !== - user?.username && ( + {`${userRow.username}-${userRow.idpId}` !== + `${user?.username}-${userRow.idpId}` && ( { setIsDeleteModalOpen(