From e752de2a71a773654e897fd559624a5182e198b8 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 21 Mar 2025 17:05:04 -0400 Subject: [PATCH] Add admin user api interfaces --- server/middlewares/index.ts | 1 + server/middlewares/verifyUserIsServerAdmin.ts | 37 ++++++++ server/routers/external.ts | 10 +- server/routers/user/adminListUsers.ts | 92 +++++++++++++++++++ server/routers/user/adminRemoveUser.ts | 61 ++++++++++++ server/routers/user/index.ts | 4 +- server/routers/user/removeUserOrg.ts | 2 +- 7 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 server/middlewares/verifyUserIsServerAdmin.ts create mode 100644 server/routers/user/adminListUsers.ts create mode 100644 server/routers/user/adminRemoveUser.ts diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts index 03de18cb..b02f5b18 100644 --- a/server/middlewares/index.ts +++ b/server/middlewares/index.ts @@ -14,3 +14,4 @@ export * from "./verifyAdmin"; export * from "./verifySetResourceUsers"; export * from "./verifyUserInRole"; export * from "./verifyAccessTokenAccess"; +export * from "./verifyUserIsServerAdmin"; \ No newline at end of file diff --git a/server/middlewares/verifyUserIsServerAdmin.ts b/server/middlewares/verifyUserIsServerAdmin.ts new file mode 100644 index 00000000..9088a425 --- /dev/null +++ b/server/middlewares/verifyUserIsServerAdmin.ts @@ -0,0 +1,37 @@ +import { Request, Response, NextFunction } from "express"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; + +export async function verifyUserIsServerAdmin( + req: Request, + res: Response, + next: NextFunction +) { + const userId = req.user!.userId; + + if (!userId) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") + ); + } + + try { + if (!req.user?.serverAdmin) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "User is not a server admin" + ) + ); + } + + return next(); + } catch (e) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error verifying organization access" + ) + ); + } +} diff --git a/server/routers/external.ts b/server/routers/external.ts index c3f584c4..2eeae9de 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -23,7 +23,8 @@ import { verifyRoleAccess, verifySetResourceUsers, verifyUserAccess, - getUserOrgs + getUserOrgs, + verifyUserIsServerAdmin } from "@server/middlewares"; import { verifyUserHasAction } from "../middlewares/verifyUserHasAction"; import { ActionsEnum } from "@server/auth/actions"; @@ -418,6 +419,13 @@ unauthenticated.get("/resource/:resourceId/auth", resource.getResourceAuthInfo); unauthenticated.get("/user", verifySessionMiddleware, user.getUser); +authenticated.get("/users", verifyUserIsServerAdmin, user.adminListUsers); +authenticated.delete( + "/user/:userId", + verifyUserIsServerAdmin, + user.adminRemoveUser +); + authenticated.get("/org/:orgId/user/:userId", verifyOrgAccess, user.getOrgUser); authenticated.get( "/org/:orgId/users", diff --git a/server/routers/user/adminListUsers.ts b/server/routers/user/adminListUsers.ts new file mode 100644 index 00000000..e4d5c206 --- /dev/null +++ b/server/routers/user/adminListUsers.ts @@ -0,0 +1,92 @@ +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 { sql, eq } from "drizzle-orm"; +import logger from "@server/logger"; +import { users } from "@server/db/schema"; + +const listUsersSchema = 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 queryUsers(limit: number, offset: number) { + return await db + .select({ + id: users.userId, + email: users.email, + dateCreated: users.dateCreated, + }) + .from(users) + .where(eq(users.serverAdmin, false)) + .limit(limit) + .offset(offset); +} + +export type AdminListUsersResponse = { + users: NonNullable>>; + pagination: { total: number; limit: number; offset: number }; +}; + +export async function adminListUsers( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedQuery = listUsersSchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedQuery.error.errors.map((e) => e.message).join(", ") + ) + ); + } + const { limit, offset } = parsedQuery.data; + + const allUsers = await queryUsers( + limit, + offset + ); + + const [{ count }] = await db + .select({ count: sql`count(*)` }) + .from(users); + + return response(res, { + data: { + users: allUsers, + 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/server/routers/user/adminRemoveUser.ts b/server/routers/user/adminRemoveUser.ts new file mode 100644 index 00000000..a8128900 --- /dev/null +++ b/server/routers/user/adminRemoveUser.ts @@ -0,0 +1,61 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { userOrgs, users } from "@server/db/schema"; +import { and, 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"; + +const removeUserSchema = z + .object({ + userId: z.string() + }) + .strict(); + +export async function adminRemoveUser( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = removeUserSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { userId } = parsedParams.data; + + // get the user first + const user = await db + .select() + .from(userOrgs) + .where(eq(userOrgs.userId, userId)); + + if (!user || user.length === 0) { + return next(createHttpError(HttpCode.NOT_FOUND, "User not found")); + } + + await db.delete(users).where(eq(users.userId, userId)); + + return response(res, { + data: null, + success: true, + error: false, + message: "User removed successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/user/index.ts b/server/routers/user/index.ts index 1bd7faa4..22e06c58 100644 --- a/server/routers/user/index.ts +++ b/server/routers/user/index.ts @@ -4,4 +4,6 @@ export * from "./listUsers"; export * from "./addUserRole"; export * from "./inviteUser"; export * from "./acceptInvite"; -export * from "./getOrgUser"; \ No newline at end of file +export * from "./getOrgUser"; +export * from "./adminListUsers"; +export * from "./adminRemoveUser"; \ No newline at end of file diff --git a/server/routers/user/removeUserOrg.ts b/server/routers/user/removeUserOrg.ts index a608b895..d7fe2bd2 100644 --- a/server/routers/user/removeUserOrg.ts +++ b/server/routers/user/removeUserOrg.ts @@ -71,7 +71,7 @@ export async function removeUserOrg( data: null, success: true, error: false, - message: "User remove from org successfully", + message: "User removed from org successfully", status: HttpCode.OK }); } catch (error) {