From 5023a5151a210b4a6d71b83be53e08c16e2a4cd3 Mon Sep 17 00:00:00 2001 From: Dipendra Upreti <91712746+dipendraupreti@users.noreply.github.com> Date: Sat, 23 Mar 2024 10:15:34 +0545 Subject: [PATCH] feat(user): add permissions while creating role (#622) --- packages/user/src/customApiError.ts | 20 ++++++ packages/user/src/model/roles/controller.ts | 8 +++ .../src/model/roles/handlers/createRole.ts | 8 ++- .../src/model/roles/handlers/deleteRole.ts | 61 ++++++++++++++++ .../model/roles/handlers/getPermissions.ts | 12 +++- .../user/src/model/roles/handlers/index.ts | 2 + .../model/roles/handlers/updatePermissions.ts | 15 +++- packages/user/src/model/roles/resolver.ts | 61 ++++++++++++++-- packages/user/src/model/roles/service.ts | 69 ++++++++++++++++--- 9 files changed, 238 insertions(+), 18 deletions(-) create mode 100644 packages/user/src/customApiError.ts create mode 100644 packages/user/src/model/roles/handlers/deleteRole.ts diff --git a/packages/user/src/customApiError.ts b/packages/user/src/customApiError.ts new file mode 100644 index 000000000..750b32949 --- /dev/null +++ b/packages/user/src/customApiError.ts @@ -0,0 +1,20 @@ +interface CustomApiErrorType { + message: string; + name: string; + statusCode: number; +} + +class CustomApiError extends Error { + public statusCode: number; + + constructor({ message, name, statusCode }: CustomApiErrorType) { + super(message); + this.message = message; + this.name = name; + this.statusCode = statusCode; + } +} + +export default CustomApiError; + +export type { CustomApiErrorType }; diff --git a/packages/user/src/model/roles/controller.ts b/packages/user/src/model/roles/controller.ts index 964ad9fd8..5140ae553 100644 --- a/packages/user/src/model/roles/controller.ts +++ b/packages/user/src/model/roles/controller.ts @@ -8,6 +8,14 @@ const plugin = async ( options: unknown, done: () => void ) => { + fastify.delete( + ROUTE_ROLES, + { + preHandler: [fastify.verifySession()], + }, + handlers.deleteRole + ); + fastify.get( ROUTE_ROLES, { diff --git a/packages/user/src/model/roles/handlers/createRole.ts b/packages/user/src/model/roles/handlers/createRole.ts index 528ac25a0..415558fc5 100644 --- a/packages/user/src/model/roles/handlers/createRole.ts +++ b/packages/user/src/model/roles/handlers/createRole.ts @@ -6,15 +6,17 @@ import type { SessionRequest } from "supertokens-node/framework/fastify"; const createRole = async (request: SessionRequest, reply: FastifyReply) => { const { body, log } = request; - const { role } = body as { + const { role, permissions } = body as { role: string; + permissions: string[]; }; try { const service = new RoleService(); - await service.createRole(role); - return reply.send({ role }); + const createResponse = await service.createRole(role, permissions); + + return reply.send(createResponse); } catch (error) { log.error(error); reply.status(500); diff --git a/packages/user/src/model/roles/handlers/deleteRole.ts b/packages/user/src/model/roles/handlers/deleteRole.ts new file mode 100644 index 000000000..79a137523 --- /dev/null +++ b/packages/user/src/model/roles/handlers/deleteRole.ts @@ -0,0 +1,61 @@ +import CustomApiError from "../../../customApiError"; +import RoleService from "../service"; + +import type { FastifyReply } from "fastify"; +import type { SessionRequest } from "supertokens-node/framework/fastify"; + +const deleteRole = async (request: SessionRequest, reply: FastifyReply) => { + const { log, query } = request; + + try { + let { role } = query as { role?: string }; + + if (role) { + try { + role = JSON.parse(role) as string; + } catch { + /* empty */ + } + + if (typeof role != "string") { + throw new CustomApiError({ + name: "UNKNOWN_ROLE_ERROR", + message: `Invalid role`, + statusCode: 422, + }); + } + + const service = new RoleService(); + + const deleteResponse = await service.deleteRole(role); + + return reply.send(deleteResponse); + } + + throw new CustomApiError({ + name: "UNKNOWN_ROLE_ERROR", + message: `Invalid role`, + statusCode: 422, + }); + } catch (error) { + if (error instanceof CustomApiError) { + reply.status(error.statusCode); + + return reply.send({ + message: error.message, + name: error.name, + statusCode: error.statusCode, + }); + } + + log.error(error); + reply.status(500); + + return reply.send({ + status: "ERROR", + message: "Oops! Something went wrong", + }); + } +}; + +export default deleteRole; diff --git a/packages/user/src/model/roles/handlers/getPermissions.ts b/packages/user/src/model/roles/handlers/getPermissions.ts index e70ccac79..dcda6ae2f 100644 --- a/packages/user/src/model/roles/handlers/getPermissions.ts +++ b/packages/user/src/model/roles/handlers/getPermissions.ts @@ -8,9 +8,19 @@ const getPermissions = async (request: SessionRequest, reply: FastifyReply) => { let permissions: string[] = []; try { - const { role } = query as { role?: string }; + let { role } = query as { role?: string }; if (role) { + try { + role = JSON.parse(role) as string; + } catch { + /* empty */ + } + + if (typeof role != "string") { + return reply.send({ permissions }); + } + const service = new RoleService(); permissions = await service.getPermissionsForRole(role); diff --git a/packages/user/src/model/roles/handlers/index.ts b/packages/user/src/model/roles/handlers/index.ts index 990facb3b..9176aedec 100644 --- a/packages/user/src/model/roles/handlers/index.ts +++ b/packages/user/src/model/roles/handlers/index.ts @@ -1,9 +1,11 @@ import createRole from "./createRole"; +import deleteRole from "./deleteRole"; import getPermissions from "./getPermissions"; import getRoles from "./getRoles"; import updatePermissions from "./updatePermissions"; export default { + deleteRole, createRole, getRoles, getPermissions, diff --git a/packages/user/src/model/roles/handlers/updatePermissions.ts b/packages/user/src/model/roles/handlers/updatePermissions.ts index 97bc2f083..2bb89c5c9 100644 --- a/packages/user/src/model/roles/handlers/updatePermissions.ts +++ b/packages/user/src/model/roles/handlers/updatePermissions.ts @@ -1,3 +1,4 @@ +import CustomApiError from "../../../customApiError"; import RoleService from "../service"; import type { FastifyReply } from "fastify"; @@ -16,13 +17,23 @@ const updatePermissions = async ( }; const service = new RoleService(); - const updatedPermissions = await service.updateRolePermissions( + const updatedPermissionsResponse = await service.updateRolePermissions( role, permissions ); - return reply.send({ permissions: updatedPermissions }); + return reply.send(updatedPermissionsResponse); } catch (error) { + if (error instanceof CustomApiError) { + reply.status(error.statusCode); + + return reply.send({ + message: error.message, + name: error.name, + statusCode: error.statusCode, + }); + } + log.error(error); reply.status(500); diff --git a/packages/user/src/model/roles/resolver.ts b/packages/user/src/model/roles/resolver.ts index 543ee7a5b..ba1b2ab28 100644 --- a/packages/user/src/model/roles/resolver.ts +++ b/packages/user/src/model/roles/resolver.ts @@ -1,11 +1,44 @@ import mercurius from "mercurius"; import RoleService from "./service"; +import CustomApiError from "../../customApiError"; import type { MercuriusContext } from "mercurius"; const Mutation = { createRole: async ( + parent: unknown, + arguments_: { + role: string; + permissions: string[]; + }, + context: MercuriusContext + ) => { + const { app } = context; + + try { + const service = new RoleService(); + + const createResponse = await service.createRole( + arguments_.role, + arguments_.permissions + ); + + return createResponse; + } catch (error) { + app.log.error(error); + + const mercuriusError = new mercurius.ErrorWithProps( + "Oops, Something went wrong" + ); + + mercuriusError.statusCode = 500; + + return mercuriusError; + } + }, + + deleteRole: async ( parent: unknown, arguments_: { role: string; @@ -16,10 +49,21 @@ const Mutation = { try { const service = new RoleService(); - await service.createRole(arguments_.role); - return arguments_.role; + const { role } = arguments_; + + const deleteResponse = await service.deleteRole(role); + + return deleteResponse; } catch (error) { + if (error instanceof CustomApiError) { + const mercuriusError = new mercurius.ErrorWithProps(error.name); + + mercuriusError.statusCode = error.statusCode; + + return mercuriusError; + } + app.log.error(error); const mercuriusError = new mercurius.ErrorWithProps( @@ -31,6 +75,7 @@ const Mutation = { return mercuriusError; } }, + updateRolePermissions: async ( parent: unknown, arguments_: { @@ -44,13 +89,21 @@ const Mutation = { try { const service = new RoleService(); - const updatedPermissions = await service.updateRolePermissions( + const updatedPermissionsResponse = await service.updateRolePermissions( role, permissions ); - return updatedPermissions; + return updatedPermissionsResponse; } catch (error) { + if (error instanceof CustomApiError) { + const mercuriusError = new mercurius.ErrorWithProps(error.name); + + mercuriusError.statusCode = error.statusCode; + + return mercuriusError; + } + app.log.error(error); const mercuriusError = new mercurius.ErrorWithProps( diff --git a/packages/user/src/model/roles/service.ts b/packages/user/src/model/roles/service.ts index a1ba2ed6f..29b2c22bc 100644 --- a/packages/user/src/model/roles/service.ts +++ b/packages/user/src/model/roles/service.ts @@ -1,8 +1,42 @@ import UserRoles from "supertokens-node/recipe/userroles"; +import CustomApiError from "../../customApiError"; + class RoleService { - createRole = async (role: string) => { - await UserRoles.createNewRoleOrAddPermissions(role, []); + createRole = async (role: string, permissions?: string[]) => { + const createRoleResponse = await UserRoles.createNewRoleOrAddPermissions( + role, + permissions || [] + ); + + return createRoleResponse; + }; + + deleteRole = async ( + role: string + ): Promise<{ status: "OK"; didRoleExist: boolean }> => { + const response = await UserRoles.getUsersThatHaveRole(role); + + if (response.status === "UNKNOWN_ROLE_ERROR") { + throw new CustomApiError({ + name: response.status, + message: `Invalid role`, + statusCode: 422, + }); + } + + if (response.users.length > 0) { + throw new CustomApiError({ + name: "ROLE_IN_USE", + message: + "The role is currently assigned to one or more users and cannot be deleted", + statusCode: 422, + }); + } + + const deleteRoleResponse = await UserRoles.deleteRole(role); + + return deleteRoleResponse; }; getPermissionsForRole = async (role: string): Promise => { @@ -17,13 +51,23 @@ class RoleService { return permissions; }; - getRoles = async (): Promise => { - let roles: string[] = []; + getRoles = async (): Promise<{ role: string; permissions: string[] }[]> => { + let roles: { role: string; permissions: string[] }[] = []; const response = await UserRoles.getAllRoles(); if (response.status === "OK") { - roles = response.roles; + // [DU 2024-MAR-20] This is N+1 problem + roles = await Promise.all( + response.roles.map(async (role) => { + const response = await UserRoles.getPermissionsForRole(role); + + return { + role, + permissions: response.status === "OK" ? response.permissions : [], + }; + }) + ); } return roles; @@ -32,11 +76,15 @@ class RoleService { updateRolePermissions = async ( role: string, permissions: string[] - ): Promise => { + ): Promise<{ status: "OK"; permissions: string[] }> => { const response = await UserRoles.getPermissionsForRole(role); if (response.status === "UNKNOWN_ROLE_ERROR") { - throw new Error("UNKNOWN_ROLE_ERROR"); + throw new CustomApiError({ + name: "UNKNOWN_ROLE_ERROR", + message: `Invalid role`, + statusCode: 422, + }); } const rolePermissions = response.permissions; @@ -52,7 +100,12 @@ class RoleService { await UserRoles.removePermissionsFromRole(role, removedPermissions); await UserRoles.createNewRoleOrAddPermissions(role, newPermissions); - return this.getPermissionsForRole(role); + const permissionsResponse = await this.getPermissionsForRole(role); + + return { + status: "OK", + permissions: permissionsResponse, + }; }; }