From 7e5c40918686a3ba74b9a117f4dedda9c0732e9f Mon Sep 17 00:00:00 2001 From: Dipendra Upreti Date: Tue, 9 Apr 2024 17:47:25 +0545 Subject: [PATCH 01/10] refactor(user): change role to supertokens role --- packages/user/src/index.ts | 6 +++--- .../src/model/{roles => supertokens-roles}/controller.ts | 0 .../{roles => supertokens-roles}/handlers/createRole.ts | 0 .../{roles => supertokens-roles}/handlers/deleteRole.ts | 0 .../{roles => supertokens-roles}/handlers/getPermissions.ts | 0 .../model/{roles => supertokens-roles}/handlers/getRoles.ts | 0 .../model/{roles => supertokens-roles}/handlers/index.ts | 0 .../handlers/updatePermissions.ts | 0 .../user/src/model/{roles => supertokens-roles}/resolver.ts | 0 .../user/src/model/{roles => supertokens-roles}/service.ts | 0 10 files changed, 3 insertions(+), 3 deletions(-) rename packages/user/src/model/{roles => supertokens-roles}/controller.ts (100%) rename packages/user/src/model/{roles => supertokens-roles}/handlers/createRole.ts (100%) rename packages/user/src/model/{roles => supertokens-roles}/handlers/deleteRole.ts (100%) rename packages/user/src/model/{roles => supertokens-roles}/handlers/getPermissions.ts (100%) rename packages/user/src/model/{roles => supertokens-roles}/handlers/getRoles.ts (100%) rename packages/user/src/model/{roles => supertokens-roles}/handlers/index.ts (100%) rename packages/user/src/model/{roles => supertokens-roles}/handlers/updatePermissions.ts (100%) rename packages/user/src/model/{roles => supertokens-roles}/resolver.ts (100%) rename packages/user/src/model/{roles => supertokens-roles}/service.ts (100%) diff --git a/packages/user/src/index.ts b/packages/user/src/index.ts index eda895c2d..430bbaf00 100644 --- a/packages/user/src/index.ts +++ b/packages/user/src/index.ts @@ -102,9 +102,9 @@ export { default as getInvitationService } from "./lib/getInvitationService"; export { default as invitationRoutes } from "./model/invitations/controller"; export { default as permissionResolver } from "./model/permissions/resolver"; export { default as permissionRoutes } from "./model/permissions/controller"; -export { default as RoleService } from "./model/roles/service"; -export { default as roleResolver } from "./model/roles/resolver"; -export { default as roleRoutes } from "./model/roles/controller"; +export { default as RoleService } from "./model/supertokens-roles/service"; +export { default as roleResolver } from "./model/supertokens-roles/resolver"; +export { default as roleRoutes } from "./model/supertokens-roles/controller"; // [DU 2023-AUG-07] use formatDate from "@dzangolab/fastify-slonik" package export { formatDate } from "@dzangolab/fastify-slonik"; export { default as computeInvitationExpiresAt } from "./lib/computeInvitationExpiresAt"; diff --git a/packages/user/src/model/roles/controller.ts b/packages/user/src/model/supertokens-roles/controller.ts similarity index 100% rename from packages/user/src/model/roles/controller.ts rename to packages/user/src/model/supertokens-roles/controller.ts diff --git a/packages/user/src/model/roles/handlers/createRole.ts b/packages/user/src/model/supertokens-roles/handlers/createRole.ts similarity index 100% rename from packages/user/src/model/roles/handlers/createRole.ts rename to packages/user/src/model/supertokens-roles/handlers/createRole.ts diff --git a/packages/user/src/model/roles/handlers/deleteRole.ts b/packages/user/src/model/supertokens-roles/handlers/deleteRole.ts similarity index 100% rename from packages/user/src/model/roles/handlers/deleteRole.ts rename to packages/user/src/model/supertokens-roles/handlers/deleteRole.ts diff --git a/packages/user/src/model/roles/handlers/getPermissions.ts b/packages/user/src/model/supertokens-roles/handlers/getPermissions.ts similarity index 100% rename from packages/user/src/model/roles/handlers/getPermissions.ts rename to packages/user/src/model/supertokens-roles/handlers/getPermissions.ts diff --git a/packages/user/src/model/roles/handlers/getRoles.ts b/packages/user/src/model/supertokens-roles/handlers/getRoles.ts similarity index 100% rename from packages/user/src/model/roles/handlers/getRoles.ts rename to packages/user/src/model/supertokens-roles/handlers/getRoles.ts diff --git a/packages/user/src/model/roles/handlers/index.ts b/packages/user/src/model/supertokens-roles/handlers/index.ts similarity index 100% rename from packages/user/src/model/roles/handlers/index.ts rename to packages/user/src/model/supertokens-roles/handlers/index.ts diff --git a/packages/user/src/model/roles/handlers/updatePermissions.ts b/packages/user/src/model/supertokens-roles/handlers/updatePermissions.ts similarity index 100% rename from packages/user/src/model/roles/handlers/updatePermissions.ts rename to packages/user/src/model/supertokens-roles/handlers/updatePermissions.ts diff --git a/packages/user/src/model/roles/resolver.ts b/packages/user/src/model/supertokens-roles/resolver.ts similarity index 100% rename from packages/user/src/model/roles/resolver.ts rename to packages/user/src/model/supertokens-roles/resolver.ts diff --git a/packages/user/src/model/roles/service.ts b/packages/user/src/model/supertokens-roles/service.ts similarity index 100% rename from packages/user/src/model/roles/service.ts rename to packages/user/src/model/supertokens-roles/service.ts From b72e70b4d2aef95a2c90785636ed89367223d8c2 Mon Sep 17 00:00:00 2001 From: Dipendra Upreti Date: Tue, 9 Apr 2024 18:36:34 +0545 Subject: [PATCH 02/10] feat(user): add roles service --- packages/user/src/index.ts | 1 + packages/user/src/model/roles/service.ts | 184 ++++++++++++++++++++ packages/user/src/model/roles/sqlFactory.ts | 125 +++++++++++++ packages/user/src/schemas/index.ts | 2 + packages/user/src/schemas/roles.ts | 10 ++ 5 files changed, 322 insertions(+) create mode 100644 packages/user/src/model/roles/service.ts create mode 100644 packages/user/src/model/roles/sqlFactory.ts create mode 100644 packages/user/src/schemas/roles.ts diff --git a/packages/user/src/index.ts b/packages/user/src/index.ts index 430bbaf00..302b11abc 100644 --- a/packages/user/src/index.ts +++ b/packages/user/src/index.ts @@ -118,6 +118,7 @@ export { default as areRolesExist } from "./supertokens/utils/areRolesExist"; export { default as validateEmail } from "./validator/email"; export { default as validatePassword } from "./validator/password"; export { default as hasUserPermission } from "./lib/hasUserPermission"; +export { default as CustomApiError } from "./customApiError"; export * from "./constants"; diff --git a/packages/user/src/model/roles/service.ts b/packages/user/src/model/roles/service.ts new file mode 100644 index 000000000..ba251ccc3 --- /dev/null +++ b/packages/user/src/model/roles/service.ts @@ -0,0 +1,184 @@ +import { BaseService } from "@dzangolab/fastify-slonik"; + +import RoleSqlFactory from "./sqlFactory"; +import CustomApiError from "../../customApiError"; +import { roleSchema } from "../../schemas"; + +import type { Service } from "@dzangolab/fastify-slonik"; +import type { QueryResultRow } from "slonik"; + +/* eslint-disable brace-style */ +class RoleService< + Role extends QueryResultRow, + RoleCreateInput extends QueryResultRow, + RoleUpdateInput extends QueryResultRow + > + extends BaseService + implements Service +{ + /* eslint-enabled */ + static readonly TABLE = "roles"; + + protected _validationSchema = roleSchema; + + create = async (data: RoleCreateInput) => { + const { permissions, ...dataInput } = data; + + const roleCount = await this.count({ + key: "role", + operator: "eq", + value: data.role as string, + }); + + if (roleCount !== 0) { + throw new CustomApiError({ + name: "ROLE_ALREADY_EXISTS", + message: "Unable to create role as it already exists", + statusCode: 422, + }); + } + + const query = this.factory.getCreateSql(dataInput as RoleCreateInput); + + const result = (await this.database.connect(async (connection) => { + return connection.query(query).then((data) => { + return data.rows[0]; + }); + })) as Role; + + if (permissions) { + try { + await this.setRolePermissions( + result.id as number, + permissions as string[] + ); + } catch (error) { + await this.delete(result.id as number); + + throw error; + } + } + + return { + ...result, + permissions, + }; + }; + + delete = async (id: number | string): Promise => { + const role = await this.findById(id); + + if (!role) { + throw new CustomApiError({ + name: "UNKNOWN_ROLE_ERROR", + message: `Invalid role`, + statusCode: 422, + }); + } + + const isRoleAssigned = await this.isRoleAssigned(id); + + if (isRoleAssigned) { + 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 query = this.factory.getDeleteSql(id); + + await this.database.connect((connection) => { + return connection.one(query); + }); + + return role; + }; + + isRoleAssigned = async (id: number | string): Promise => { + const query = this.factory.getIsRoleAssignedSql(id); + + const result = await this.database.connect((connection) => { + return connection.one(query).then((columns) => columns.isRoleAssigned); + }); + + return result as boolean; + }; + + setRolePermissions = async ( + roleId: string | number, + permissions: string[] + ) => { + const query = this.factory.getSetRolePermissionsSql(roleId, permissions); + + await this.database.connect((connection) => { + return connection.any(query); + }); + + return { roleId, permissions }; + }; + + update = async ( + id: number | string, + data: RoleUpdateInput + ): Promise => { + const { permissions, ...dataInput } = data; + + const role = await this.findById(id); + + if (!role) { + throw new CustomApiError({ + name: "UNKNOWN_ROLE_ERROR", + message: `Invalid role`, + statusCode: 422, + }); + } + + const result = await this.database.connect(async (connection) => { + return connection.transaction(async (transactionConnection) => { + await transactionConnection.query( + this.factory.getUpdateSql(id, dataInput as RoleUpdateInput) + ); + + if (permissions) { + await transactionConnection.query( + this.factory.getSetRolePermissionsSql(id, []) + ); + + await transactionConnection.query( + this.factory.getSetRolePermissionsSql(id, permissions as string[]) + ); + } + + return await transactionConnection.query( + this.factory.getFindByIdSql(id) + ); + }); + }); + + return result.rows[0] as Role; + }; + + get factory() { + if (!this.table) { + throw new Error(`Service table is not defined`); + } + + if (!this._factory) { + this._factory = new RoleSqlFactory< + Role, + RoleCreateInput, + RoleUpdateInput + >(this); + } + + return this._factory as RoleSqlFactory< + Role, + RoleCreateInput, + RoleUpdateInput + >; + } +} + +export default RoleService; diff --git a/packages/user/src/model/roles/sqlFactory.ts b/packages/user/src/model/roles/sqlFactory.ts new file mode 100644 index 000000000..52ab8417f --- /dev/null +++ b/packages/user/src/model/roles/sqlFactory.ts @@ -0,0 +1,125 @@ +import { + DefaultSqlFactory, + createFilterFragment, + createLimitFragment, + createSortFragment, + createTableIdentifier, +} from "@dzangolab/fastify-slonik"; +import { sql } from "slonik"; +import { z } from "zod"; + +import type { + FilterInput, + SortInput, + SqlFactory, +} from "@dzangolab/fastify-slonik"; +import type { QuerySqlToken, QueryResultRow } from "slonik"; + +/* eslint-disable brace-style */ +class RoleSqlFactory< + Role extends QueryResultRow, + RoleCreateInput extends QueryResultRow, + RoleUpdateInput extends QueryResultRow + > + extends DefaultSqlFactory + implements SqlFactory +{ + /* eslint-enabled */ + getListSql = ( + limit: number, + offset?: number, + filters?: FilterInput, + sort?: SortInput[] + ): QuerySqlToken => { + const tableIdentifier = createTableIdentifier(this.table, this.schema); + + const rolePermissionsIdentifier = createTableIdentifier( + "role_permissions", + this.schema + ); + + return sql.type(this.validationSchema)` + SELECT + ${this.getTableFragment()}.*, + COALESCE(role_permissions.permissions, '[]') AS permissions + FROM ${this.getTableFragment()} + LEFT JOIN LATERAL ( + SELECT jsonb_agg(rp.permission) AS permissions + FROM ${rolePermissionsIdentifier} as rp + WHERE rp.role_id = ${this.getTableFragment()}.id + ) AS role_permissions ON TRUE + ${createFilterFragment(filters, tableIdentifier)} + ${createSortFragment(tableIdentifier, this.getSortInput(sort))} + ${createLimitFragment(limit, offset)}; + `; + }; + + getFindByIdSql = (id: number | string): QuerySqlToken => { + const rolePermissionsIdentifier = createTableIdentifier( + "role_permissions", + this.schema + ); + + return sql.type(this.validationSchema)` + SELECT + ${this.getTableFragment()}.*, + COALESCE(role_permissions.permissions, '[]') AS permissions + FROM ${this.getTableFragment()} + LEFT JOIN LATERAL ( + SELECT jsonb_agg(rp.permission) AS permissions + FROM ${rolePermissionsIdentifier} as rp + WHERE rp.role_id = ${this.getTableFragment()}.id + ) AS role_permissions ON TRUE + WHERE id = ${id}; + `; + }; + + getIsRoleAssignedSql = (id: number | string) => { + const userRolesTableIdentifier = createTableIdentifier( + "user_roles", + this.schema + ); + + const schema = z.object({ + isRoleAssigned: z.boolean(), + }); + + return sql.type(schema)` + SELECT EXISTS ( + SELECT * + FROM ${userRolesTableIdentifier} + WHERE role_id = ${id} + ) AS is_role_assigned; + `; + }; + + getSetRolePermissionsSql = ( + roleId: string | number, + permissions: string[] + ) => { + const rolePermissionsIdentifier = createTableIdentifier( + "role_permissions", + this.schema + ); + + if (permissions.length === 0) { + return sql.unsafe` + DELETE FROM ${rolePermissionsIdentifier} + WHERE role_id = ${roleId}; + `; + } + + return sql.unsafe` + INSERT INTO ${rolePermissionsIdentifier} ("role_id", "permission") + SELECT * + FROM ${sql.unnest( + permissions.map((permission) => { + return [roleId, permission]; + }), + ["int4", "varchar"] + )} ON CONFLICT DO NOTHING; + `; + }; +} + +export default RoleSqlFactory; diff --git a/packages/user/src/schemas/index.ts b/packages/user/src/schemas/index.ts index b9a901b08..4b757b435 100644 --- a/packages/user/src/schemas/index.ts +++ b/packages/user/src/schemas/index.ts @@ -1,3 +1,5 @@ export { default as emailSchema } from "./email"; export { default as passwordSchema } from "./password"; + +export { default as roleSchema } from "./roles"; diff --git a/packages/user/src/schemas/roles.ts b/packages/user/src/schemas/roles.ts new file mode 100644 index 000000000..2f9ccba55 --- /dev/null +++ b/packages/user/src/schemas/roles.ts @@ -0,0 +1,10 @@ +import { z } from "zod"; + +const roleSchema = z.object({ + id: z.number(), + role: z.string().max(255), + permissions: z.optional(z.array(z.string().max(255))), + default: z.boolean(), +}); + +export default roleSchema; From b9fb06dabc6b280f819e0287231f2193908c6622 Mon Sep 17 00:00:00 2001 From: Dipendra Upreti Date: Wed, 10 Apr 2024 09:47:04 +0545 Subject: [PATCH 03/10] feat(user): add roles controller --- packages/user/src/index.ts | 3 +- packages/user/src/model/roles/controller.ts | 167 ++++++++++++++++++++ packages/user/src/types/index.ts | 2 + packages/user/src/types/roles.ts | 11 ++ 4 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 packages/user/src/model/roles/controller.ts create mode 100644 packages/user/src/types/roles.ts diff --git a/packages/user/src/index.ts b/packages/user/src/index.ts index 302b11abc..c53c5e416 100644 --- a/packages/user/src/index.ts +++ b/packages/user/src/index.ts @@ -103,7 +103,8 @@ export { default as invitationRoutes } from "./model/invitations/controller"; export { default as permissionResolver } from "./model/permissions/resolver"; export { default as permissionRoutes } from "./model/permissions/controller"; export { default as RoleService } from "./model/supertokens-roles/service"; -export { default as roleResolver } from "./model/supertokens-roles/resolver"; +export { default as supertokensRoleResolver } from "./model/supertokens-roles/resolver"; +export { default as roleResolver } from "./model/roles/controller"; export { default as roleRoutes } from "./model/supertokens-roles/controller"; // [DU 2023-AUG-07] use formatDate from "@dzangolab/fastify-slonik" package export { formatDate } from "@dzangolab/fastify-slonik"; diff --git a/packages/user/src/model/roles/controller.ts b/packages/user/src/model/roles/controller.ts new file mode 100644 index 000000000..48f91d160 --- /dev/null +++ b/packages/user/src/model/roles/controller.ts @@ -0,0 +1,167 @@ +import Service from "./service"; +import CustomApiError from "../../customApiError"; + +import type { RoleUpdateInput } from "../../types"; +import type { FastifyInstance, FastifyReply } from "fastify"; +import type { SessionRequest } from "supertokens-node/framework/fastify"; + +const plugin = async ( + fastify: FastifyInstance, + options: unknown, + done: () => void +) => { + fastify.get( + "/roles", + { + preHandler: fastify.verifySession(), + }, + async (request: SessionRequest, reply: FastifyReply) => { + const service = new Service(request.config, request.slonik); + + const { limit, offset, filters, sort } = request.query as { + limit: number; + offset?: number; + filters?: string; + sort?: string; + }; + + const data = await service.list( + limit, + offset, + filters ? JSON.parse(filters) : undefined, + sort ? JSON.parse(sort) : undefined + ); + + reply.send(data); + } + ); + + fastify.get( + "/roles/:id(^\\d+)", + { + preHandler: fastify.verifySession(), + }, + async (request: SessionRequest, reply) => { + const service = new Service(request.config, request.slonik); + + const { id } = request.params as { id: number }; + + const data = await service.findById(id); + + reply.send(data); + } + ); + + fastify.delete( + "/roles/:id(^\\d+)", + { + preHandler: fastify.verifySession(), + }, + async (request: SessionRequest, reply: FastifyReply) => { + const service = new Service(request.config, request.slonik); + + const { id } = request.params as { id: number }; + + try { + const data = await service.delete(id); + + reply.send(data); + } catch (error) { + if (error instanceof CustomApiError) { + reply.status(error.statusCode); + + return reply.send({ + message: error.message, + name: error.name, + statusCode: error.statusCode, + }); + } + + request.log.error(error); + reply.status(500); + + return reply.send({ + status: "ERROR", + message: "Oops! Something went wrong", + }); + } + } + ); + + fastify.post( + "/roles", + { + preHandler: fastify.verifySession(), + }, + async (request: SessionRequest, reply: FastifyReply) => { + const service = new Service(request.config, request.slonik); + const input = request.body as RoleUpdateInput; + + try { + const data = await service.create(input); + + return reply.send(data); + } catch (error) { + if (error instanceof CustomApiError) { + reply.status(error.statusCode); + + return reply.send({ + message: error.message, + name: error.name, + statusCode: error.statusCode, + }); + } + + request.log.error(error); + reply.status(500); + + return reply.send({ + status: "ERROR", + message: "Oops! Something went wrong", + }); + } + } + ); + + fastify.put( + "/roles/:id(^\\d+)", + { + preHandler: fastify.verifySession(), + }, + async (request: SessionRequest, reply: FastifyReply) => { + const service = new Service(request.config, request.slonik); + + const { id } = request.params as { id: number }; + + const input = request.body as RoleUpdateInput; + + try { + const data = await service.update(id, input); + + return reply.send(data); + } catch (error) { + if (error instanceof CustomApiError) { + reply.status(error.statusCode); + + return reply.send({ + message: error.message, + name: error.name, + statusCode: error.statusCode, + }); + } + + request.log.error(error); + reply.status(500); + + return reply.send({ + status: "ERROR", + message: "Oops! Something went wrong", + }); + } + } + ); + + done(); +}; + +export default plugin; diff --git a/packages/user/src/types/index.ts b/packages/user/src/types/index.ts index 88833db90..8103cc06a 100644 --- a/packages/user/src/types/index.ts +++ b/packages/user/src/types/index.ts @@ -67,3 +67,5 @@ export type { export type { IsEmailOptions } from "./isEmailOptions"; export type { StrongPasswordOptions } from "./strongPasswordOptions"; + +export type { Role, RoleCreateInput, RoleUpdateInput } from "./roles"; diff --git a/packages/user/src/types/roles.ts b/packages/user/src/types/roles.ts new file mode 100644 index 000000000..cbf5260b2 --- /dev/null +++ b/packages/user/src/types/roles.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; + +import { roleSchema } from "../schemas"; + +type Role = z.infer; + +type RoleCreateInput = Omit; + +type RoleUpdateInput = Partial>; + +export type { Role, RoleCreateInput, RoleUpdateInput }; From 04dab797e3756176a73b583abdf0f7edc26ae4c9 Mon Sep 17 00:00:00 2001 From: Dipendra Upreti Date: Wed, 10 Apr 2024 12:46:39 +0545 Subject: [PATCH 04/10] chore(api): add handlers for role --- packages/user/src/index.ts | 4 +- packages/user/src/model/roles/controller.ts | 128 +----------------- .../user/src/model/roles/handlers/create.ts | 42 ++++++ .../user/src/model/roles/handlers/delete.ts | 42 ++++++ .../user/src/model/roles/handlers/index.ts | 13 ++ .../user/src/model/roles/handlers/role.ts | 21 +++ .../user/src/model/roles/handlers/roles.ts | 33 +++++ .../user/src/model/roles/handlers/update.ts | 44 ++++++ 8 files changed, 204 insertions(+), 123 deletions(-) create mode 100644 packages/user/src/model/roles/handlers/create.ts create mode 100644 packages/user/src/model/roles/handlers/delete.ts create mode 100644 packages/user/src/model/roles/handlers/index.ts create mode 100644 packages/user/src/model/roles/handlers/role.ts create mode 100644 packages/user/src/model/roles/handlers/roles.ts create mode 100644 packages/user/src/model/roles/handlers/update.ts diff --git a/packages/user/src/index.ts b/packages/user/src/index.ts index c53c5e416..1f033e191 100644 --- a/packages/user/src/index.ts +++ b/packages/user/src/index.ts @@ -104,8 +104,8 @@ export { default as permissionResolver } from "./model/permissions/resolver"; export { default as permissionRoutes } from "./model/permissions/controller"; export { default as RoleService } from "./model/supertokens-roles/service"; export { default as supertokensRoleResolver } from "./model/supertokens-roles/resolver"; -export { default as roleResolver } from "./model/roles/controller"; -export { default as roleRoutes } from "./model/supertokens-roles/controller"; +export { default as supertokensRoleRoutes } from "./model/supertokens-roles/controller"; +export { default as roleRoutes } from "./model/roles/controller"; // [DU 2023-AUG-07] use formatDate from "@dzangolab/fastify-slonik" package export { formatDate } from "@dzangolab/fastify-slonik"; export { default as computeInvitationExpiresAt } from "./lib/computeInvitationExpiresAt"; diff --git a/packages/user/src/model/roles/controller.ts b/packages/user/src/model/roles/controller.ts index 48f91d160..1ffd1a413 100644 --- a/packages/user/src/model/roles/controller.ts +++ b/packages/user/src/model/roles/controller.ts @@ -1,9 +1,6 @@ -import Service from "./service"; -import CustomApiError from "../../customApiError"; +import handlers from "./handlers"; -import type { RoleUpdateInput } from "../../types"; -import type { FastifyInstance, FastifyReply } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; +import type { FastifyInstance } from "fastify"; const plugin = async ( fastify: FastifyInstance, @@ -15,25 +12,7 @@ const plugin = async ( { preHandler: fastify.verifySession(), }, - async (request: SessionRequest, reply: FastifyReply) => { - const service = new Service(request.config, request.slonik); - - const { limit, offset, filters, sort } = request.query as { - limit: number; - offset?: number; - filters?: string; - sort?: string; - }; - - const data = await service.list( - limit, - offset, - filters ? JSON.parse(filters) : undefined, - sort ? JSON.parse(sort) : undefined - ); - - reply.send(data); - } + handlers.roles ); fastify.get( @@ -41,15 +20,7 @@ const plugin = async ( { preHandler: fastify.verifySession(), }, - async (request: SessionRequest, reply) => { - const service = new Service(request.config, request.slonik); - - const { id } = request.params as { id: number }; - - const data = await service.findById(id); - - reply.send(data); - } + handlers.role ); fastify.delete( @@ -57,35 +28,7 @@ const plugin = async ( { preHandler: fastify.verifySession(), }, - async (request: SessionRequest, reply: FastifyReply) => { - const service = new Service(request.config, request.slonik); - - const { id } = request.params as { id: number }; - - try { - const data = await service.delete(id); - - reply.send(data); - } catch (error) { - if (error instanceof CustomApiError) { - reply.status(error.statusCode); - - return reply.send({ - message: error.message, - name: error.name, - statusCode: error.statusCode, - }); - } - - request.log.error(error); - reply.status(500); - - return reply.send({ - status: "ERROR", - message: "Oops! Something went wrong", - }); - } - } + handlers.deleteRole ); fastify.post( @@ -93,34 +36,7 @@ const plugin = async ( { preHandler: fastify.verifySession(), }, - async (request: SessionRequest, reply: FastifyReply) => { - const service = new Service(request.config, request.slonik); - const input = request.body as RoleUpdateInput; - - try { - const data = await service.create(input); - - return reply.send(data); - } catch (error) { - if (error instanceof CustomApiError) { - reply.status(error.statusCode); - - return reply.send({ - message: error.message, - name: error.name, - statusCode: error.statusCode, - }); - } - - request.log.error(error); - reply.status(500); - - return reply.send({ - status: "ERROR", - message: "Oops! Something went wrong", - }); - } - } + handlers.create ); fastify.put( @@ -128,37 +44,7 @@ const plugin = async ( { preHandler: fastify.verifySession(), }, - async (request: SessionRequest, reply: FastifyReply) => { - const service = new Service(request.config, request.slonik); - - const { id } = request.params as { id: number }; - - const input = request.body as RoleUpdateInput; - - try { - const data = await service.update(id, input); - - return reply.send(data); - } catch (error) { - if (error instanceof CustomApiError) { - reply.status(error.statusCode); - - return reply.send({ - message: error.message, - name: error.name, - statusCode: error.statusCode, - }); - } - - request.log.error(error); - reply.status(500); - - return reply.send({ - status: "ERROR", - message: "Oops! Something went wrong", - }); - } - } + handlers.update ); done(); diff --git a/packages/user/src/model/roles/handlers/create.ts b/packages/user/src/model/roles/handlers/create.ts new file mode 100644 index 000000000..3904b6b77 --- /dev/null +++ b/packages/user/src/model/roles/handlers/create.ts @@ -0,0 +1,42 @@ +import CustomApiError from "../../../customApiError"; +import Service from "../service"; + +import type { Role, RoleCreateInput, RoleUpdateInput } from "../../../types"; +import type { FastifyReply } from "fastify"; +import type { SessionRequest } from "supertokens-node/framework/fastify"; + +const create = async (request: SessionRequest, reply: FastifyReply) => { + const service = new Service( + request.config, + request.slonik, + request.dbSchema + ); + + const input = request.body as RoleCreateInput; + + try { + const data = await service.create(input); + + return reply.send(data); + } catch (error) { + if (error instanceof CustomApiError) { + reply.status(error.statusCode); + + return reply.send({ + message: error.message, + name: error.name, + statusCode: error.statusCode, + }); + } + + request.log.error(error); + reply.status(500); + + return reply.send({ + status: "ERROR", + message: "Oops! Something went wrong", + }); + } +}; + +export default create; diff --git a/packages/user/src/model/roles/handlers/delete.ts b/packages/user/src/model/roles/handlers/delete.ts new file mode 100644 index 000000000..d871da1d9 --- /dev/null +++ b/packages/user/src/model/roles/handlers/delete.ts @@ -0,0 +1,42 @@ +import CustomApiError from "../../../customApiError"; +import Service from "../service"; + +import type { Role, RoleCreateInput, RoleUpdateInput } from "../../../types"; +import type { FastifyReply } from "fastify"; +import type { SessionRequest } from "supertokens-node/framework/fastify"; + +const deleteRole = async (request: SessionRequest, reply: FastifyReply) => { + const service = new Service( + request.config, + request.slonik, + request.dbSchema + ); + + const { id } = request.params as { id: number }; + + try { + const data = await service.delete(id); + + reply.send(data); + } catch (error) { + if (error instanceof CustomApiError) { + reply.status(error.statusCode); + + return reply.send({ + message: error.message, + name: error.name, + statusCode: error.statusCode, + }); + } + + request.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/index.ts b/packages/user/src/model/roles/handlers/index.ts new file mode 100644 index 000000000..d6ee174a0 --- /dev/null +++ b/packages/user/src/model/roles/handlers/index.ts @@ -0,0 +1,13 @@ +import create from "./create"; +import deleteRole from "./delete"; +import role from "./role"; +import roles from "./roles"; +import update from "./update"; + +export default { + create, + deleteRole, + role, + roles, + update, +}; diff --git a/packages/user/src/model/roles/handlers/role.ts b/packages/user/src/model/roles/handlers/role.ts new file mode 100644 index 000000000..4ef44d2f3 --- /dev/null +++ b/packages/user/src/model/roles/handlers/role.ts @@ -0,0 +1,21 @@ +import Service from "../service"; + +import type { Role, RoleCreateInput, RoleUpdateInput } from "../../../types"; +import type { FastifyReply } from "fastify"; +import type { SessionRequest } from "supertokens-node/framework/fastify"; + +const role = async (request: SessionRequest, reply: FastifyReply) => { + const service = new Service( + request.config, + request.slonik, + request.dbSchema + ); + + const { id } = request.params as { id: number }; + + const data = await service.findById(id); + + reply.send(data); +}; + +export default role; diff --git a/packages/user/src/model/roles/handlers/roles.ts b/packages/user/src/model/roles/handlers/roles.ts new file mode 100644 index 000000000..4eb0fb2f6 --- /dev/null +++ b/packages/user/src/model/roles/handlers/roles.ts @@ -0,0 +1,33 @@ +import Service from "../service"; + +import type { Role, RoleCreateInput, RoleUpdateInput } from "../../../types"; +import type { FastifyReply } from "fastify"; +import type { SessionRequest } from "supertokens-node/framework/fastify"; + +const roles = async (request: SessionRequest, reply: FastifyReply) => { + const { config, dbSchema, query, slonik } = request; + + const service = new Service( + config, + slonik, + dbSchema + ); + + const { limit, offset, filters, sort } = query as { + limit: number; + offset?: number; + filters?: string; + sort?: string; + }; + + const data = await service.list( + limit, + offset, + filters ? JSON.parse(filters) : undefined, + sort ? JSON.parse(sort) : undefined + ); + + reply.send(data); +}; + +export default roles; diff --git a/packages/user/src/model/roles/handlers/update.ts b/packages/user/src/model/roles/handlers/update.ts new file mode 100644 index 000000000..ee87c91e7 --- /dev/null +++ b/packages/user/src/model/roles/handlers/update.ts @@ -0,0 +1,44 @@ +import CustomApiError from "../../../customApiError"; +import Service from "../service"; + +import type { Role, RoleCreateInput, RoleUpdateInput } from "../../../types"; +import type { FastifyReply } from "fastify"; +import type { SessionRequest } from "supertokens-node/framework/fastify"; + +const update = async (request: SessionRequest, reply: FastifyReply) => { + const service = new Service( + request.config, + request.slonik, + request.dbSchema + ); + + const { id } = request.params as { id: number }; + + const input = request.body as RoleUpdateInput; + + try { + const data = await service.update(id, input); + + return reply.send(data); + } catch (error) { + if (error instanceof CustomApiError) { + reply.status(error.statusCode); + + return reply.send({ + message: error.message, + name: error.name, + statusCode: error.statusCode, + }); + } + + request.log.error(error); + reply.status(500); + + return reply.send({ + status: "ERROR", + message: "Oops! Something went wrong", + }); + } +}; + +export default update; From 5c7666f22052dc95b9fdb2fa4d14e0b43916f399 Mon Sep 17 00:00:00 2001 From: Dipendra Upreti Date: Wed, 10 Apr 2024 13:41:43 +0545 Subject: [PATCH 05/10] chore(user): move roles table name to constants.ts file --- packages/user/src/constants.ts | 10 +++++++++- packages/user/src/model/roles/service.ts | 3 ++- packages/user/src/model/roles/sqlFactory.ts | 10 ++++++---- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/user/src/constants.ts b/packages/user/src/constants.ts index 2a0dd021c..d690cb2d6 100644 --- a/packages/user/src/constants.ts +++ b/packages/user/src/constants.ts @@ -20,7 +20,6 @@ const ROUTE_ME = "/me"; const ROUTE_USERS = "/users"; const ROUTE_USERS_DISABLE = "/users/:id/disable"; const ROUTE_USERS_ENABLE = "/users/:id/enable"; -const TABLE_USERS = "users"; // Roles const ROUTE_ROLES = "/roles"; @@ -29,6 +28,12 @@ const ROUTE_ROLES_PERMISSIONS = "/roles/permissions"; // Permissions const ROUTE_PERMISSIONS = "/permissions"; +// Tables +const TABLE_ROLES = "roles"; +const TABLE_USERS = "users"; +const TABLE_ROLE_PERMISSIONS = "role_permissions"; +const TABLE_USER_ROLES = "user_roles"; + // Email verification const EMAIL_VERIFICATION_MODE = "REQUIRED"; const EMAIL_VERIFICATION_PATH = "/verify-email"; @@ -75,4 +80,7 @@ export { ROUTE_USERS_ENABLE, TABLE_INVITATIONS, TABLE_USERS, + TABLE_ROLES, + TABLE_ROLE_PERMISSIONS, + TABLE_USER_ROLES, }; diff --git a/packages/user/src/model/roles/service.ts b/packages/user/src/model/roles/service.ts index ba251ccc3..ea414bae7 100644 --- a/packages/user/src/model/roles/service.ts +++ b/packages/user/src/model/roles/service.ts @@ -1,6 +1,7 @@ import { BaseService } from "@dzangolab/fastify-slonik"; import RoleSqlFactory from "./sqlFactory"; +import { TABLE_ROLES } from "../../constants"; import CustomApiError from "../../customApiError"; import { roleSchema } from "../../schemas"; @@ -17,7 +18,7 @@ class RoleService< implements Service { /* eslint-enabled */ - static readonly TABLE = "roles"; + static readonly TABLE = TABLE_ROLES; protected _validationSchema = roleSchema; diff --git a/packages/user/src/model/roles/sqlFactory.ts b/packages/user/src/model/roles/sqlFactory.ts index 52ab8417f..f6ef3ade2 100644 --- a/packages/user/src/model/roles/sqlFactory.ts +++ b/packages/user/src/model/roles/sqlFactory.ts @@ -8,6 +8,8 @@ import { import { sql } from "slonik"; import { z } from "zod"; +import { TABLE_ROLE_PERMISSIONS, TABLE_USER_ROLES } from "../../constants"; + import type { FilterInput, SortInput, @@ -34,7 +36,7 @@ class RoleSqlFactory< const tableIdentifier = createTableIdentifier(this.table, this.schema); const rolePermissionsIdentifier = createTableIdentifier( - "role_permissions", + TABLE_ROLE_PERMISSIONS, this.schema ); @@ -56,7 +58,7 @@ class RoleSqlFactory< getFindByIdSql = (id: number | string): QuerySqlToken => { const rolePermissionsIdentifier = createTableIdentifier( - "role_permissions", + TABLE_ROLE_PERMISSIONS, this.schema ); @@ -76,7 +78,7 @@ class RoleSqlFactory< getIsRoleAssignedSql = (id: number | string) => { const userRolesTableIdentifier = createTableIdentifier( - "user_roles", + TABLE_USER_ROLES, this.schema ); @@ -98,7 +100,7 @@ class RoleSqlFactory< permissions: string[] ) => { const rolePermissionsIdentifier = createTableIdentifier( - "role_permissions", + TABLE_ROLE_PERMISSIONS, this.schema ); From 75ac1575d272adb5ad621a215b0b407b8ec3b7ee Mon Sep 17 00:00:00 2001 From: Dipendra Upreti Date: Mon, 15 Apr 2024 15:02:55 +0545 Subject: [PATCH 06/10] feat(user): remove dbScheam for roles service --- packages/user/src/model/roles/handlers/create.ts | 3 +-- packages/user/src/model/roles/handlers/delete.ts | 3 +-- packages/user/src/model/roles/handlers/role.ts | 3 +-- packages/user/src/model/roles/handlers/roles.ts | 5 ++--- packages/user/src/model/roles/handlers/update.ts | 3 +-- 5 files changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/user/src/model/roles/handlers/create.ts b/packages/user/src/model/roles/handlers/create.ts index 3904b6b77..c6883f068 100644 --- a/packages/user/src/model/roles/handlers/create.ts +++ b/packages/user/src/model/roles/handlers/create.ts @@ -8,8 +8,7 @@ import type { SessionRequest } from "supertokens-node/framework/fastify"; const create = async (request: SessionRequest, reply: FastifyReply) => { const service = new Service( request.config, - request.slonik, - request.dbSchema + request.slonik ); const input = request.body as RoleCreateInput; diff --git a/packages/user/src/model/roles/handlers/delete.ts b/packages/user/src/model/roles/handlers/delete.ts index d871da1d9..a7bc19abc 100644 --- a/packages/user/src/model/roles/handlers/delete.ts +++ b/packages/user/src/model/roles/handlers/delete.ts @@ -8,8 +8,7 @@ import type { SessionRequest } from "supertokens-node/framework/fastify"; const deleteRole = async (request: SessionRequest, reply: FastifyReply) => { const service = new Service( request.config, - request.slonik, - request.dbSchema + request.slonik ); const { id } = request.params as { id: number }; diff --git a/packages/user/src/model/roles/handlers/role.ts b/packages/user/src/model/roles/handlers/role.ts index 4ef44d2f3..bda2933f5 100644 --- a/packages/user/src/model/roles/handlers/role.ts +++ b/packages/user/src/model/roles/handlers/role.ts @@ -7,8 +7,7 @@ import type { SessionRequest } from "supertokens-node/framework/fastify"; const role = async (request: SessionRequest, reply: FastifyReply) => { const service = new Service( request.config, - request.slonik, - request.dbSchema + request.slonik ); const { id } = request.params as { id: number }; diff --git a/packages/user/src/model/roles/handlers/roles.ts b/packages/user/src/model/roles/handlers/roles.ts index 4eb0fb2f6..d5afa6080 100644 --- a/packages/user/src/model/roles/handlers/roles.ts +++ b/packages/user/src/model/roles/handlers/roles.ts @@ -5,12 +5,11 @@ import type { FastifyReply } from "fastify"; import type { SessionRequest } from "supertokens-node/framework/fastify"; const roles = async (request: SessionRequest, reply: FastifyReply) => { - const { config, dbSchema, query, slonik } = request; + const { config, query, slonik } = request; const service = new Service( config, - slonik, - dbSchema + slonik ); const { limit, offset, filters, sort } = query as { diff --git a/packages/user/src/model/roles/handlers/update.ts b/packages/user/src/model/roles/handlers/update.ts index ee87c91e7..f587dbdbe 100644 --- a/packages/user/src/model/roles/handlers/update.ts +++ b/packages/user/src/model/roles/handlers/update.ts @@ -8,8 +8,7 @@ import type { SessionRequest } from "supertokens-node/framework/fastify"; const update = async (request: SessionRequest, reply: FastifyReply) => { const service = new Service( request.config, - request.slonik, - request.dbSchema + request.slonik ); const { id } = request.params as { id: number }; From 8956d736a535b8eb0c0bfe4231c4ba9c660d1feb Mon Sep 17 00:00:00 2001 From: Dipendra Upreti Date: Mon, 15 Apr 2024 15:16:05 +0545 Subject: [PATCH 07/10] chore(user): export roles schema and type --- packages/user/src/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/user/src/index.ts b/packages/user/src/index.ts index 1f033e191..d6011f035 100644 --- a/packages/user/src/index.ts +++ b/packages/user/src/index.ts @@ -120,6 +120,7 @@ export { default as validateEmail } from "./validator/email"; export { default as validatePassword } from "./validator/password"; export { default as hasUserPermission } from "./lib/hasUserPermission"; export { default as CustomApiError } from "./customApiError"; +export { emailSchema, passwordSchema, roleSchema } from "./schemas"; export * from "./constants"; @@ -129,6 +130,9 @@ export type { ThirdPartyEmailPasswordRecipe } from "./supertokens/types/thirdPar export type { AuthUser, ChangePasswordInput, + Role, + RoleCreateInput, + RoleUpdateInput, UserCreateInput, UserUpdateInput, User, From 564b405ce958417be5800c8d54879fd63db35e93 Mon Sep 17 00:00:00 2001 From: Dipendra Upreti Date: Tue, 23 Apr 2024 15:19:48 +0545 Subject: [PATCH 08/10] chore(user): remove supertokens role service --- packages/user/src/index.ts | 4 +- .../src/model/supertokens-roles/controller.ts | 54 ----- .../supertokens-roles/handlers/createRole.ts | 42 ---- .../supertokens-roles/handlers/deleteRole.ts | 61 ------ .../handlers/getPermissions.ts | 41 ---- .../supertokens-roles/handlers/getRoles.ts | 25 --- .../model/supertokens-roles/handlers/index.ts | 13 -- .../handlers/updatePermissions.ts | 47 ----- .../src/model/supertokens-roles/resolver.ts | 186 ------------------ .../src/model/supertokens-roles/service.ts | 123 ------------ 10 files changed, 1 insertion(+), 595 deletions(-) delete mode 100644 packages/user/src/model/supertokens-roles/controller.ts delete mode 100644 packages/user/src/model/supertokens-roles/handlers/createRole.ts delete mode 100644 packages/user/src/model/supertokens-roles/handlers/deleteRole.ts delete mode 100644 packages/user/src/model/supertokens-roles/handlers/getPermissions.ts delete mode 100644 packages/user/src/model/supertokens-roles/handlers/getRoles.ts delete mode 100644 packages/user/src/model/supertokens-roles/handlers/index.ts delete mode 100644 packages/user/src/model/supertokens-roles/handlers/updatePermissions.ts delete mode 100644 packages/user/src/model/supertokens-roles/resolver.ts delete mode 100644 packages/user/src/model/supertokens-roles/service.ts diff --git a/packages/user/src/index.ts b/packages/user/src/index.ts index d6011f035..428be7e8c 100644 --- a/packages/user/src/index.ts +++ b/packages/user/src/index.ts @@ -102,9 +102,7 @@ export { default as getInvitationService } from "./lib/getInvitationService"; export { default as invitationRoutes } from "./model/invitations/controller"; export { default as permissionResolver } from "./model/permissions/resolver"; export { default as permissionRoutes } from "./model/permissions/controller"; -export { default as RoleService } from "./model/supertokens-roles/service"; -export { default as supertokensRoleResolver } from "./model/supertokens-roles/resolver"; -export { default as supertokensRoleRoutes } from "./model/supertokens-roles/controller"; +export { default as RoleService } from "./model/roles/service"; export { default as roleRoutes } from "./model/roles/controller"; // [DU 2023-AUG-07] use formatDate from "@dzangolab/fastify-slonik" package export { formatDate } from "@dzangolab/fastify-slonik"; diff --git a/packages/user/src/model/supertokens-roles/controller.ts b/packages/user/src/model/supertokens-roles/controller.ts deleted file mode 100644 index 5140ae553..000000000 --- a/packages/user/src/model/supertokens-roles/controller.ts +++ /dev/null @@ -1,54 +0,0 @@ -import handlers from "./handlers"; -import { ROUTE_ROLES, ROUTE_ROLES_PERMISSIONS } from "../../constants"; - -import type { FastifyInstance } from "fastify"; - -const plugin = async ( - fastify: FastifyInstance, - options: unknown, - done: () => void -) => { - fastify.delete( - ROUTE_ROLES, - { - preHandler: [fastify.verifySession()], - }, - handlers.deleteRole - ); - - fastify.get( - ROUTE_ROLES, - { - preHandler: [fastify.verifySession()], - }, - handlers.getRoles - ); - - fastify.get( - ROUTE_ROLES_PERMISSIONS, - { - preHandler: [fastify.verifySession()], - }, - handlers.getPermissions - ); - - fastify.post( - ROUTE_ROLES, - { - preHandler: [fastify.verifySession()], - }, - handlers.createRole - ); - - fastify.put( - ROUTE_ROLES_PERMISSIONS, - { - preHandler: [fastify.verifySession()], - }, - handlers.updatePermissions - ); - - done(); -}; - -export default plugin; diff --git a/packages/user/src/model/supertokens-roles/handlers/createRole.ts b/packages/user/src/model/supertokens-roles/handlers/createRole.ts deleted file mode 100644 index 78ccfe45b..000000000 --- a/packages/user/src/model/supertokens-roles/handlers/createRole.ts +++ /dev/null @@ -1,42 +0,0 @@ -import CustomApiError from "../../../customApiError"; -import RoleService from "../service"; - -import type { FastifyReply } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; - -const createRole = async (request: SessionRequest, reply: FastifyReply) => { - const { body, log } = request; - - const { role, permissions } = body as { - role: string; - permissions: string[]; - }; - - try { - const service = new RoleService(); - - const createResponse = await service.createRole(role, permissions); - - return reply.send(createResponse); - } 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 createRole; diff --git a/packages/user/src/model/supertokens-roles/handlers/deleteRole.ts b/packages/user/src/model/supertokens-roles/handlers/deleteRole.ts deleted file mode 100644 index 79a137523..000000000 --- a/packages/user/src/model/supertokens-roles/handlers/deleteRole.ts +++ /dev/null @@ -1,61 +0,0 @@ -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/supertokens-roles/handlers/getPermissions.ts b/packages/user/src/model/supertokens-roles/handlers/getPermissions.ts deleted file mode 100644 index dcda6ae2f..000000000 --- a/packages/user/src/model/supertokens-roles/handlers/getPermissions.ts +++ /dev/null @@ -1,41 +0,0 @@ -import RoleService from "../service"; - -import type { FastifyReply } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; - -const getPermissions = async (request: SessionRequest, reply: FastifyReply) => { - const { log, query } = request; - let permissions: string[] = []; - - try { - 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); - } - - return reply.send({ permissions }); - } catch (error) { - log.error(error); - reply.status(500); - - return reply.send({ - status: "ERROR", - message: "Oops! Something went wrong", - }); - } -}; - -export default getPermissions; diff --git a/packages/user/src/model/supertokens-roles/handlers/getRoles.ts b/packages/user/src/model/supertokens-roles/handlers/getRoles.ts deleted file mode 100644 index 7429c097d..000000000 --- a/packages/user/src/model/supertokens-roles/handlers/getRoles.ts +++ /dev/null @@ -1,25 +0,0 @@ -import RoleService from "../service"; - -import type { FastifyReply } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; - -const getRoles = async (request: SessionRequest, reply: FastifyReply) => { - const { log } = request; - - try { - const service = new RoleService(); - const roles = await service.getRoles(); - - return reply.send({ roles }); - } catch (error) { - log.error(error); - reply.status(500); - - return reply.send({ - status: "ERROR", - message: "Oops! Something went wrong", - }); - } -}; - -export default getRoles; diff --git a/packages/user/src/model/supertokens-roles/handlers/index.ts b/packages/user/src/model/supertokens-roles/handlers/index.ts deleted file mode 100644 index 9176aedec..000000000 --- a/packages/user/src/model/supertokens-roles/handlers/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -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, - updatePermissions, -}; diff --git a/packages/user/src/model/supertokens-roles/handlers/updatePermissions.ts b/packages/user/src/model/supertokens-roles/handlers/updatePermissions.ts deleted file mode 100644 index 2bb89c5c9..000000000 --- a/packages/user/src/model/supertokens-roles/handlers/updatePermissions.ts +++ /dev/null @@ -1,47 +0,0 @@ -import CustomApiError from "../../../customApiError"; -import RoleService from "../service"; - -import type { FastifyReply } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; - -const updatePermissions = async ( - request: SessionRequest, - reply: FastifyReply -) => { - const { log, body } = request; - - try { - const { role, permissions } = body as { - role: string; - permissions: string[]; - }; - - const service = new RoleService(); - const updatedPermissionsResponse = await service.updateRolePermissions( - role, - permissions - ); - - 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); - - return reply.send({ - status: "ERROR", - message: "Oops! Something went wrong", - }); - } -}; - -export default updatePermissions; diff --git a/packages/user/src/model/supertokens-roles/resolver.ts b/packages/user/src/model/supertokens-roles/resolver.ts deleted file mode 100644 index 491136be7..000000000 --- a/packages/user/src/model/supertokens-roles/resolver.ts +++ /dev/null @@ -1,186 +0,0 @@ -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) { - 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( - "Oops, Something went wrong" - ); - - mercuriusError.statusCode = 500; - - return mercuriusError; - } - }, - - deleteRole: async ( - parent: unknown, - arguments_: { - role: string; - }, - context: MercuriusContext - ) => { - const { app } = context; - - try { - const service = new RoleService(); - - 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( - "Oops, Something went wrong" - ); - - mercuriusError.statusCode = 500; - - return mercuriusError; - } - }, - - updateRolePermissions: async ( - parent: unknown, - arguments_: { - role: string; - permissions: string[]; - }, - context: MercuriusContext - ) => { - const { app } = context; - const { permissions, role } = arguments_; - - try { - const service = new RoleService(); - const updatedPermissionsResponse = await service.updateRolePermissions( - role, - permissions - ); - - 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( - "Oops, Something went wrong" - ); - - mercuriusError.statusCode = 500; - - return mercuriusError; - } - }, -}; - -const Query = { - roles: async ( - parent: unknown, - arguments_: Record, - context: MercuriusContext - ) => { - const { app } = context; - - try { - const service = new RoleService(); - const roles = await service.getRoles(); - - return roles; - } catch (error) { - app.log.error(error); - - const mercuriusError = new mercurius.ErrorWithProps( - "Oops, Something went wrong" - ); - - mercuriusError.statusCode = 500; - - return mercuriusError; - } - }, - rolePermissions: async ( - parent: unknown, - arguments_: { - role: string; - }, - context: MercuriusContext - ) => { - const { app } = context; - const { role } = arguments_; - let permissions: string[] = []; - - try { - if (role) { - const service = new RoleService(); - - permissions = await service.getPermissionsForRole(role); - } - - return permissions; - } catch (error) { - app.log.error(error); - - const mercuriusError = new mercurius.ErrorWithProps( - "Oops, Something went wrong" - ); - - mercuriusError.statusCode = 500; - - return mercuriusError; - } - }, -}; - -export default { Mutation, Query }; diff --git a/packages/user/src/model/supertokens-roles/service.ts b/packages/user/src/model/supertokens-roles/service.ts deleted file mode 100644 index 3b9de3ba1..000000000 --- a/packages/user/src/model/supertokens-roles/service.ts +++ /dev/null @@ -1,123 +0,0 @@ -import UserRoles from "supertokens-node/recipe/userroles"; - -import CustomApiError from "../../customApiError"; - -class RoleService { - createRole = async ( - role: string, - permissions?: string[] - ): Promise<{ status: "OK" }> => { - const { roles } = await UserRoles.getAllRoles(role); - - if (roles.includes(role)) { - throw new CustomApiError({ - name: "ROLE_ALREADY_EXISTS", - message: "Unable to create role as it already exists", - statusCode: 422, - }); - } - - const createRoleResponse = await UserRoles.createNewRoleOrAddPermissions( - role, - permissions || [] - ); - - return { status: createRoleResponse.status }; - }; - - deleteRole = async (role: string): Promise<{ status: "OK" }> => { - 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 { status: deleteRoleResponse.status }; - }; - - getPermissionsForRole = async (role: string): Promise => { - let permissions: string[] = []; - - const response = await UserRoles.getPermissionsForRole(role); - - if (response.status === "OK") { - permissions = response.permissions; - } - - return permissions; - }; - - getRoles = async (): Promise<{ role: string; permissions: string[] }[]> => { - let roles: { role: string; permissions: string[] }[] = []; - - const response = await UserRoles.getAllRoles(); - - if (response.status === "OK") { - // [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; - }; - - updateRolePermissions = async ( - role: string, - permissions: string[] - ): Promise<{ status: "OK"; permissions: string[] }> => { - const response = await UserRoles.getPermissionsForRole(role); - - if (response.status === "UNKNOWN_ROLE_ERROR") { - throw new CustomApiError({ - name: "UNKNOWN_ROLE_ERROR", - message: `Invalid role`, - statusCode: 422, - }); - } - - const rolePermissions = response.permissions; - - const newPermissions = permissions.filter( - (permission) => !rolePermissions.includes(permission) - ); - - const removedPermissions = rolePermissions.filter( - (permission) => !permissions.includes(permission) - ); - - await UserRoles.removePermissionsFromRole(role, removedPermissions); - await UserRoles.createNewRoleOrAddPermissions(role, newPermissions); - - const permissionsResponse = await this.getPermissionsForRole(role); - - return { - status: "OK", - permissions: permissionsResponse, - }; - }; -} - -export default RoleService; From ee20347bbfb7ff26b8f1b8a148ebfff47cd1ca08 Mon Sep 17 00:00:00 2001 From: Dipendra Upreti Date: Tue, 23 Apr 2024 16:44:21 +0545 Subject: [PATCH 09/10] chore(user): roles service all method support filterInput --- packages/user/src/model/roles/service.ts | 20 +++++++++++++- packages/user/src/model/roles/sqlFactory.ts | 30 +++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/packages/user/src/model/roles/service.ts b/packages/user/src/model/roles/service.ts index ea414bae7..2bd5db4c7 100644 --- a/packages/user/src/model/roles/service.ts +++ b/packages/user/src/model/roles/service.ts @@ -5,7 +5,11 @@ import { TABLE_ROLES } from "../../constants"; import CustomApiError from "../../customApiError"; import { roleSchema } from "../../schemas"; -import type { Service } from "@dzangolab/fastify-slonik"; +import type { + FilterInput, + Service, + SortInput, +} from "@dzangolab/fastify-slonik"; import type { QueryResultRow } from "slonik"; /* eslint-disable brace-style */ @@ -22,6 +26,20 @@ class RoleService< protected _validationSchema = roleSchema; + all = async ( + fields: string[], + sort?: SortInput[], + filterInput?: FilterInput + ): Promise> => { + const query = this.factory.getAllSql(fields, sort, filterInput); + + const result = await this.database.connect((connection) => { + return connection.any(query); + }); + + return result as Partial; + }; + create = async (data: RoleCreateInput) => { const { permissions, ...dataInput } = data; diff --git a/packages/user/src/model/roles/sqlFactory.ts b/packages/user/src/model/roles/sqlFactory.ts index f6ef3ade2..cceec1dde 100644 --- a/packages/user/src/model/roles/sqlFactory.ts +++ b/packages/user/src/model/roles/sqlFactory.ts @@ -5,6 +5,7 @@ import { createSortFragment, createTableIdentifier, } from "@dzangolab/fastify-slonik"; +import humps from "humps"; import { sql } from "slonik"; import { z } from "zod"; @@ -27,6 +28,35 @@ class RoleSqlFactory< implements SqlFactory { /* eslint-enabled */ + getAllSql = ( + fields: string[], + sort?: SortInput[], + filters?: FilterInput + ): QuerySqlToken => { + const identifiers = []; + + const fieldsObject: Record = {}; + + for (const field of fields) { + identifiers.push(sql.identifier([humps.decamelize(field)])); + fieldsObject[humps.camelize(field)] = true; + } + + const tableIdentifier = createTableIdentifier(this.table, this.schema); + + const allSchema = + this.validationSchema._def.typeName === "ZodObject" + ? (this.validationSchema as z.AnyZodObject).pick(fieldsObject) + : z.any(); + + return sql.type(allSchema)` + SELECT ${sql.join(identifiers, sql.fragment`, `)} + FROM ${this.getTableFragment()} + ${createFilterFragment(filters, tableIdentifier)} + ${createSortFragment(tableIdentifier, this.getSortInput(sort))} + `; + }; + getListSql = ( limit: number, offset?: number, From c458399e8c8c02c120e12ccb31d8f8034151aa7e Mon Sep 17 00:00:00 2001 From: Dipendra Upreti Date: Wed, 24 Apr 2024 09:28:59 +0545 Subject: [PATCH 10/10] chore(user): support permission by roles all method --- packages/user/src/model/roles/sqlFactory.ts | 25 ++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/user/src/model/roles/sqlFactory.ts b/packages/user/src/model/roles/sqlFactory.ts index cceec1dde..36a21174e 100644 --- a/packages/user/src/model/roles/sqlFactory.ts +++ b/packages/user/src/model/roles/sqlFactory.ts @@ -38,11 +38,23 @@ class RoleSqlFactory< const fieldsObject: Record = {}; for (const field of fields) { - identifiers.push(sql.identifier([humps.decamelize(field)])); - fieldsObject[humps.camelize(field)] = true; + if (field != "permissions") { + identifiers.push(sql.identifier([humps.decamelize(field)])); + fieldsObject[humps.camelize(field)] = true; + } } const tableIdentifier = createTableIdentifier(this.table, this.schema); + const rolePermissionsIdentifier = createTableIdentifier( + TABLE_ROLE_PERMISSIONS, + this.schema + ); + + const permissionsFragment = fields.includes("permissions") + ? sql.fragment`, + COALESCE(role_permissions.permissions, '[]') AS permissions + ` + : sql.fragment``; const allSchema = this.validationSchema._def.typeName === "ZodObject" @@ -50,8 +62,15 @@ class RoleSqlFactory< : z.any(); return sql.type(allSchema)` - SELECT ${sql.join(identifiers, sql.fragment`, `)} + SELECT + ${sql.join(identifiers, sql.fragment`, `)} + ${permissionsFragment} FROM ${this.getTableFragment()} + LEFT JOIN LATERAL ( + SELECT jsonb_agg(rp.permission) AS permissions + FROM ${rolePermissionsIdentifier} as rp + WHERE rp.role_id = ${this.getTableFragment()}.id + ) AS role_permissions ON TRUE ${createFilterFragment(filters, tableIdentifier)} ${createSortFragment(tableIdentifier, this.getSortInput(sort))} `;