From 436cb23f73a215a45418769ea79dcb04933fca76 Mon Sep 17 00:00:00 2001 From: Dipendra Upreti Date: Fri, 5 Apr 2024 14:40:49 +0545 Subject: [PATCH 1/3] refactor(user): add own-roles --- packages/user/src/constants.ts | 8 + packages/user/src/index.ts | 1 + .../user/src/model/own-roles/controller.ts | 57 +++++++ .../model/own-roles/handlers/createRole.ts | 42 ++++++ .../model/own-roles/handlers/deleteRole.ts | 61 ++++++++ .../own-roles/handlers/getPermissions.ts | 39 +++++ .../src/model/own-roles/handlers/getRoles.ts | 26 ++++ .../src/model/own-roles/handlers/index.ts | 13 ++ .../own-roles/handlers/updatePermissions.ts | 48 ++++++ packages/user/src/model/own-roles/service.ts | 138 +++++++++++++++++ packages/user/src/model/own-roles/sql.ts | 63 ++++++++ .../user/src/model/own-roles/sqlFactory.ts | 140 ++++++++++++++++++ packages/user/src/types/roles/index.ts | 22 +++ packages/user/src/types/roles/service.ts | 56 +++++++ packages/user/src/types/roles/sqlFactory.ts | 34 +++++ 15 files changed, 748 insertions(+) create mode 100644 packages/user/src/model/own-roles/controller.ts create mode 100644 packages/user/src/model/own-roles/handlers/createRole.ts create mode 100644 packages/user/src/model/own-roles/handlers/deleteRole.ts create mode 100644 packages/user/src/model/own-roles/handlers/getPermissions.ts create mode 100644 packages/user/src/model/own-roles/handlers/getRoles.ts create mode 100644 packages/user/src/model/own-roles/handlers/index.ts create mode 100644 packages/user/src/model/own-roles/handlers/updatePermissions.ts create mode 100644 packages/user/src/model/own-roles/service.ts create mode 100644 packages/user/src/model/own-roles/sql.ts create mode 100644 packages/user/src/model/own-roles/sqlFactory.ts create mode 100644 packages/user/src/types/roles/index.ts create mode 100644 packages/user/src/types/roles/service.ts create mode 100644 packages/user/src/types/roles/sqlFactory.ts diff --git a/packages/user/src/constants.ts b/packages/user/src/constants.ts index 2a0dd021c..192e7aa57 100644 --- a/packages/user/src/constants.ts +++ b/packages/user/src/constants.ts @@ -20,7 +20,12 @@ const ROUTE_ME = "/me"; const ROUTE_USERS = "/users"; const ROUTE_USERS_DISABLE = "/users/:id/disable"; const ROUTE_USERS_ENABLE = "/users/:id/enable"; + +// Tables +const TABLE_ROLES = "roles"; +const TABLE_ROLE_PERMISSIONS = "role_permissions"; const TABLE_USERS = "users"; +const TABLE_USER_ROLES = "user_roles"; // Roles const ROUTE_ROLES = "/roles"; @@ -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/index.ts b/packages/user/src/index.ts index eda895c2d..c7e1c88a3 100644 --- a/packages/user/src/index.ts +++ b/packages/user/src/index.ts @@ -105,6 +105,7 @@ 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 ownRoleRoutes } from "./model/own-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/own-roles/controller.ts b/packages/user/src/model/own-roles/controller.ts new file mode 100644 index 000000000..b0966358b --- /dev/null +++ b/packages/user/src/model/own-roles/controller.ts @@ -0,0 +1,57 @@ +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 +) => { + const OWN_ROUTE_ROLES = `own-${ROUTE_ROLES}`; + const OWN_ROUTE_ROLES_PERMISSIONS = `own-${ROUTE_ROLES_PERMISSIONS}`; + + fastify.delete( + OWN_ROUTE_ROLES, + { + preHandler: [fastify.verifySession()], + }, + handlers.deleteRole + ); + + fastify.get( + OWN_ROUTE_ROLES, + { + preHandler: [fastify.verifySession()], + }, + handlers.getRoles + ); + + fastify.get( + OWN_ROUTE_ROLES_PERMISSIONS, + { + preHandler: [fastify.verifySession()], + }, + handlers.getPermissions + ); + + fastify.post( + OWN_ROUTE_ROLES, + { + preHandler: [fastify.verifySession()], + }, + handlers.createRole + ); + + fastify.put( + OWN_ROUTE_ROLES_PERMISSIONS, + { + preHandler: [fastify.verifySession()], + }, + handlers.updatePermissions + ); + + done(); +}; + +export default plugin; diff --git a/packages/user/src/model/own-roles/handlers/createRole.ts b/packages/user/src/model/own-roles/handlers/createRole.ts new file mode 100644 index 000000000..5500797d4 --- /dev/null +++ b/packages/user/src/model/own-roles/handlers/createRole.ts @@ -0,0 +1,42 @@ +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, dbSchema, config, slonik } = request; + + const { role, permissions } = body as { + role: string; + permissions: string[]; + }; + + try { + const service = new RoleService(config, slonik, dbSchema); + + const createResponse = await service.create({ 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/own-roles/handlers/deleteRole.ts b/packages/user/src/model/own-roles/handlers/deleteRole.ts new file mode 100644 index 000000000..b7f1b13c5 --- /dev/null +++ b/packages/user/src/model/own-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 { config, slonik, log, query, dbSchema } = 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(config, slonik, dbSchema); + + const deleteResponse = await service.delete(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/own-roles/handlers/getPermissions.ts b/packages/user/src/model/own-roles/handlers/getPermissions.ts new file mode 100644 index 000000000..456fc05e7 --- /dev/null +++ b/packages/user/src/model/own-roles/handlers/getPermissions.ts @@ -0,0 +1,39 @@ +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 { config, dbSchema, slonik, log, query } = request; + try { + let { role } = query as { role?: number }; + + if (role) { + try { + role = JSON.parse(role as unknown as string) as number; + } catch { + /* empty */ + } + + if (typeof role != "number") { + throw new TypeError("Invalid input"); + } + + const service = new RoleService(config, slonik, dbSchema); + + const permissions = await service.getPermissionsForRole(role); + + return 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/own-roles/handlers/getRoles.ts b/packages/user/src/model/own-roles/handlers/getRoles.ts new file mode 100644 index 000000000..612e8cd1b --- /dev/null +++ b/packages/user/src/model/own-roles/handlers/getRoles.ts @@ -0,0 +1,26 @@ +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 { config, dbSchema, log, slonik } = request; + + try { + const service = new RoleService(config, slonik, dbSchema); + + 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/own-roles/handlers/index.ts b/packages/user/src/model/own-roles/handlers/index.ts new file mode 100644 index 000000000..9176aedec --- /dev/null +++ b/packages/user/src/model/own-roles/handlers/index.ts @@ -0,0 +1,13 @@ +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/own-roles/handlers/updatePermissions.ts b/packages/user/src/model/own-roles/handlers/updatePermissions.ts new file mode 100644 index 000000000..0ae8d5172 --- /dev/null +++ b/packages/user/src/model/own-roles/handlers/updatePermissions.ts @@ -0,0 +1,48 @@ +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 { body, config, dbSchema, log, slonik } = request; + + try { + const { roleId, permissions } = body as { + roleId: number; + permissions: string[]; + }; + + const service = new RoleService(config, slonik, dbSchema); + + const updatedPermissionsResponse = await service.updateRolePermissions( + roleId, + 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/own-roles/service.ts b/packages/user/src/model/own-roles/service.ts new file mode 100644 index 000000000..c0cd5663a --- /dev/null +++ b/packages/user/src/model/own-roles/service.ts @@ -0,0 +1,138 @@ +import { BaseService } from "@dzangolab/fastify-slonik"; + +import RoleSqlFactory from "./sqlFactory"; +import { TABLE_ROLES } from "../../constants"; +import CustomApiError from "../../customApiError"; + +import type { RolePermission, RoleWithPermissions } from "../../types/roles"; +import type { Service } from "../../types/roles/service"; +import type { QueryResultRow } from "slonik"; + +class RoleService< + Role extends QueryResultRow, + RoleCreateInput extends QueryResultRow, + RoleUpdateInput extends QueryResultRow + > + extends BaseService + // eslint-disable-next-line prettier/prettier + implements Service { + static readonly TABLE = TABLE_ROLES; + + create = async (data: RoleCreateInput) => { + const query = this.factory.getCreateSql({ + role: data.role, + default: data.default, + } as unknown as RoleCreateInput); + + const result = (await this.database.connect(async (connection) => { + return connection.query(query).then((data) => { + return data.rows[0]; + }); + })) as Role; + + const permissions = await this.addRolePermissions( + result.id as number, + data.permissions as string[] + ); + + return { + ...result, + permissions: permissions.map( + (permission: RolePermission) => permission.permission + ), + }; + }; + + addRolePermissions = async (roleId: number, permissions: string[]) => { + const query = this.factory.getAddRolePermissionSql(roleId, permissions); + + const result = (await this.database.connect(async (connection) => { + return connection.query(query).then((data) => { + return data.rows; + }); + })) as RolePermission[]; + + return result; + }; + + getPermissionsForRole = async (id: number): Promise => { + const query = this.factory.getPermissionsForRoleSql(id); + + const result = await this.database.connect((connection) => { + return connection.any(query); + }); + + return result as RolePermission[]; + }; + + getRoles = async (): Promise => { + const query = this.factory.getRolesSql(); + + const result = await this.database.connect((connection) => { + return connection.any(query); + }); + + return result as RoleWithPermissions[]; + }; + + removePermissionsFromRole = async (roleId: number, permissions: string[]) => { + const query = this.factory.getRemovePermissionsSql(roleId, permissions); + + const result = await this.database.connect((connection) => { + return connection.any(query); + }); + + return result; + }; + + updateRolePermissions = async (roleId: number, permissions: string[]) => { + const response = await this.findById(roleId); + + if (!response) { + throw new CustomApiError({ + name: "UNKNOWN_ROLE_ERROR", + message: `Invalid role`, + statusCode: 422, + }); + } + + const rolePermissions = response.permissions as string[]; + + const newPermissions = permissions.filter( + (permission) => !rolePermissions.includes(permission) + ); + + const removedPermissions = rolePermissions.filter( + (permission) => !permissions.includes(permission) + ); + + await this.removePermissionsFromRole(roleId, removedPermissions); + await this.addRolePermissions(roleId, newPermissions); + + const permissionsResponse = await this.getPermissionsForRole(roleId); + + return permissionsResponse; + }; + + 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/own-roles/sql.ts b/packages/user/src/model/own-roles/sql.ts new file mode 100644 index 000000000..a6267abe5 --- /dev/null +++ b/packages/user/src/model/own-roles/sql.ts @@ -0,0 +1,63 @@ +import humps from "humps"; +import { sql } from "slonik"; + +import type { SortInput } from "@dzangolab/fastify-slonik"; +import type { IdentifierSqlToken } from "slonik"; + +const createSortFragment = ( + tableIdentifier: IdentifierSqlToken, + sort?: SortInput[] +) => { + if (sort && sort.length > 0) { + const arraySort = []; + + for (const data of sort) { + const direction = + data.direction === "ASC" ? sql.fragment`ASC` : sql.fragment`DESC`; + + let roleFragment; + + if (data.key === "roles") { + roleFragment = sql.fragment`user_role.role ->> 0`; + } + + const sortIdentifier = sql.identifier([ + ...tableIdentifier.names, + humps.decamelize(data.key), + ]); + + arraySort.push( + sql.fragment`${roleFragment ?? sortIdentifier} ${direction}` + ); + } + + return sql.fragment`ORDER BY ${sql.join(arraySort, sql.fragment`,`)}`; + } + + return sql.fragment``; +}; + +const createSortRoleFragment = ( + identifier: IdentifierSqlToken, + sort?: SortInput[] +) => { + let direction = sql.fragment`ASC`; + + if (!Array.isArray(sort)) { + sort = []; + } + + sort.some((sortItem) => { + if (sortItem.key === "roles" && sortItem.direction != "ASC") { + direction = sql.fragment`DESC`; + + return true; + } + + return false; + }); + + return sql.fragment`ORDER BY ${identifier} ${direction}`; +}; + +export { createSortFragment, createSortRoleFragment }; diff --git a/packages/user/src/model/own-roles/sqlFactory.ts b/packages/user/src/model/own-roles/sqlFactory.ts new file mode 100644 index 000000000..3afb79d2e --- /dev/null +++ b/packages/user/src/model/own-roles/sqlFactory.ts @@ -0,0 +1,140 @@ +import { + DefaultSqlFactory, + createLimitFragment, + createFilterFragment, + createTableIdentifier, +} from "@dzangolab/fastify-slonik"; +import humps from "humps"; +import { QueryResultRow, QuerySqlToken, sql } from "slonik"; +import * as zod from "zod"; + +import { createSortFragment, createSortRoleFragment } from "./sql"; +import { TABLE_ROLE_PERMISSIONS } from "../../constants"; + +import type { Service } from "../../types/roles/service"; +import type { SqlFactory } from "../../types/roles/sqlFactory"; +import type { FilterInput, SortInput } from "@dzangolab/fastify-slonik"; + +/* eslint-disable brace-style */ +class RoleSqlFactory< + Role extends QueryResultRow, + RoleCreateInput extends QueryResultRow, + RoleUpdateInput extends QueryResultRow + > + extends DefaultSqlFactory + implements SqlFactory +{ + /* eslint-enabled */ + protected declare _service: Service; + + getAddRolePermissionSql = ( + roleId: number, + permissions: string[] + ): QuerySqlToken => { + const permissionsTable = createTableIdentifier( + TABLE_ROLE_PERMISSIONS, + this.schema + ); + + return sql.type(this.validationSchema)` + INSERT INTO ${permissionsTable} ("role_id", "permission") + SELECT * + FROM ${sql.unnest( + [permissions.map((permission) => [roleId, permission])], + ["integer", "varchar"] + )} + RETURNING *; + `; + }; + + getPermissionsForRoleSql = (id: number): QuerySqlToken => { + const permissionsIdentifier = createTableIdentifier( + TABLE_ROLE_PERMISSIONS, + this.schema + ); + + const filters: FilterInput = { + key: "rowId", + operator: "eq", + value: `${id}`, + }; + + return sql.unsafe` + SELECT * + FROM ${permissionsIdentifier} + ${createFilterFragment(filters, permissionsIdentifier)}; + `; + }; + + getRolesSql = (): QuerySqlToken => { + const rolePermissionsIdentifier = createTableIdentifier( + TABLE_ROLE_PERMISSIONS, + this.schema + ); + + return sql.unsafe` + SELECT + ${this.getTableFragment()}.*, + COALESCE(user_permissions.permission, '[]') 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 + `; + }; + + getFindByIdSql = (id: number | string): QuerySqlToken => { + const rolePermissionsIdentifier = createTableIdentifier( + TABLE_ROLE_PERMISSIONS, + this.schema + ); + + return sql.unsafe` + SELECT + ${this.getTableFragment()}.*, + COALESCE(user_permissions.permission, '[]') 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} + `; + }; + + getRemovePermissionsSql = ( + roleId: number, + permissions: string[] + ): QuerySqlToken => { + const rolePermissionsIdentifier = createTableIdentifier( + TABLE_ROLE_PERMISSIONS, + this.schema + ); + + const filters: FilterInput = { + OR: permissions.map((permission) => { + return { + AND: [ + { key: "roleId", operator: "eq", value: roleId }, + { key: "permission", operator: "eq", value: permission }, + ], + }; + }), + } as FilterInput; + + return sql.unsafe` + DELETE FROM ${rolePermissionsIdentifier} + ${createFilterFragment(filters, rolePermissionsIdentifier)} + RETURNING *; + `; + }; + + get service() { + return this._service; + } +} + +export default RoleSqlFactory; diff --git a/packages/user/src/types/roles/index.ts b/packages/user/src/types/roles/index.ts new file mode 100644 index 000000000..90fa78117 --- /dev/null +++ b/packages/user/src/types/roles/index.ts @@ -0,0 +1,22 @@ +interface Role { + id: number; + role: string; + default: string; +} + +interface RoleWithPermissions extends Role { + permissions: string[]; +} + +interface RoleCreateInput { + role: string; + permissions: string[]; +} + +interface RolePermission { + id: string; + roleId: number; + permission: string; +} + +export type { Role, RoleCreateInput, RolePermission, RoleWithPermissions }; diff --git a/packages/user/src/types/roles/service.ts b/packages/user/src/types/roles/service.ts new file mode 100644 index 000000000..401204ae8 --- /dev/null +++ b/packages/user/src/types/roles/service.ts @@ -0,0 +1,56 @@ +import type { Role, RolePermission, RoleWithPermissions } from "."; +import type { ApiConfig } from "@dzangolab/fastify-config"; +import type { + Database, + FilterInput, + SortDirection, + SortInput, +} from "@dzangolab/fastify-slonik"; +import type { z } from "zod"; + +interface Service { + config: ApiConfig; + database: Database; + sortDirection: SortDirection; + sortKey: string; + schema: "public" | string; + table: string; + validationSchema: z.ZodTypeAny; + + all(fields: string[]): Promise>; + create(data: C): Promise; + delete(id: number | string): Promise; + findById(id: number | string): Promise; + getLimitDefault(): number; + getLimitMax(): number; + list( + limit?: number, + offset?: number, + filters?: FilterInput, + sort?: SortInput[] + ): Promise>; + count(filters?: FilterInput): Promise; + update(id: number | string, data: U): Promise; + addRolePermissions( + id: number, + permission: string[] + ): Promise; + getPermissionsForRole(id: number): Promise; + getRoles(): Promise; + updateRolePermissions( + roleId: number, + permission: string[] + ): Promise; + removePermissionsFromRole( + roleId: number, + permission: string[] + ): Promise; +} + +type PaginatedList = { + totalCount: number; + filteredCount: number; + data: readonly T[]; +}; + +export type { PaginatedList, Service }; diff --git a/packages/user/src/types/roles/sqlFactory.ts b/packages/user/src/types/roles/sqlFactory.ts new file mode 100644 index 000000000..2b1af6ff4 --- /dev/null +++ b/packages/user/src/types/roles/sqlFactory.ts @@ -0,0 +1,34 @@ +import type { Service } from "./service"; +import type { ApiConfig } from "@dzangolab/fastify-config"; +import type { FilterInput, SortInput } from "@dzangolab/fastify-slonik"; +import type { FragmentSqlToken, QueryResultRow, QuerySqlToken } from "slonik"; + +interface SqlFactory< + T extends QueryResultRow, + C extends QueryResultRow, + U extends QueryResultRow +> { + config: ApiConfig; + service: Service; + + getAllSql(fields: string[], sort?: SortInput[]): QuerySqlToken; + getCreateSql(data: C): QuerySqlToken; + getDeleteSql(id: number | string): QuerySqlToken; + getFindByIdSql(id: number | string): QuerySqlToken; + getListSql( + limit: number, + offset?: number, + filters?: FilterInput, + sort?: SortInput[] + ): QuerySqlToken; + getSortInput(sort?: SortInput[]): SortInput[]; + getTableFragment(): FragmentSqlToken; + getUpdateSql(id: number | string, data: U): QuerySqlToken; + getCountSql(filters?: FilterInput): QuerySqlToken; + getAddRolePermissionSql(roleId: number, permission: string[]): QuerySqlToken; + getPermissionsForRoleSql(id: number): QuerySqlToken; + getRolesSql(): QuerySqlToken; + getRemovePermissionsSql(roleId: number, permissions: string[]): QuerySqlToken; +} + +export type { SqlFactory }; From ef73d587dec6be185c5813c4c1ceb3a3f71b06ea Mon Sep 17 00:00:00 2001 From: Dipendra Upreti Date: Fri, 5 Apr 2024 16:21:36 +0545 Subject: [PATCH 2/3] fix(user): fix create role with permission --- packages/user/src/model/own-roles/controller.ts | 5 +++-- .../src/model/own-roles/handlers/createRole.ts | 10 +++------- packages/user/src/model/own-roles/service.ts | 14 ++++++++++++++ packages/user/src/model/own-roles/sqlFactory.ts | 6 ++++-- packages/user/src/types/roles/index.ts | 1 + 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/packages/user/src/model/own-roles/controller.ts b/packages/user/src/model/own-roles/controller.ts index b0966358b..89b375a77 100644 --- a/packages/user/src/model/own-roles/controller.ts +++ b/packages/user/src/model/own-roles/controller.ts @@ -8,8 +8,9 @@ const plugin = async ( options: unknown, done: () => void ) => { - const OWN_ROUTE_ROLES = `own-${ROUTE_ROLES}`; - const OWN_ROUTE_ROLES_PERMISSIONS = `own-${ROUTE_ROLES_PERMISSIONS}`; + const OWN_ROUTE_ROLES = `/own${ROUTE_ROLES}`; + + const OWN_ROUTE_ROLES_PERMISSIONS = `/own${ROUTE_ROLES_PERMISSIONS}`; fastify.delete( OWN_ROUTE_ROLES, diff --git a/packages/user/src/model/own-roles/handlers/createRole.ts b/packages/user/src/model/own-roles/handlers/createRole.ts index 5500797d4..b988e58b1 100644 --- a/packages/user/src/model/own-roles/handlers/createRole.ts +++ b/packages/user/src/model/own-roles/handlers/createRole.ts @@ -2,22 +2,18 @@ import CustomApiError from "../../../customApiError"; import RoleService from "../service"; import type { FastifyReply } from "fastify"; +import type { QueryResultRow } from "slonik"; import type { SessionRequest } from "supertokens-node/framework/fastify"; const createRole = async (request: SessionRequest, reply: FastifyReply) => { const { body, log, dbSchema, config, slonik } = request; - const { role, permissions } = body as { - role: string; - permissions: string[]; - }; - try { const service = new RoleService(config, slonik, dbSchema); - const createResponse = await service.create({ role, permissions }); + const response = await service.create(body as QueryResultRow); - return reply.send(createResponse); + return reply.send(response); } catch (error) { if (error instanceof CustomApiError) { reply.status(error.statusCode); diff --git a/packages/user/src/model/own-roles/service.ts b/packages/user/src/model/own-roles/service.ts index c0cd5663a..ec9abadcf 100644 --- a/packages/user/src/model/own-roles/service.ts +++ b/packages/user/src/model/own-roles/service.ts @@ -19,6 +19,20 @@ class RoleService< static readonly TABLE = TABLE_ROLES; create = async (data: RoleCreateInput) => { + const count = await this.count({ + key: "role", + operator: "eq", + value: data.role as string, + }); + + if (count != 0) { + throw new CustomApiError({ + name: "ROLE_ALREADY_EXISTS", + message: "Unable to create role as it already exists", + statusCode: 422, + }); + } + const query = this.factory.getCreateSql({ role: data.role, default: data.default, diff --git a/packages/user/src/model/own-roles/sqlFactory.ts b/packages/user/src/model/own-roles/sqlFactory.ts index 3afb79d2e..ed8c47f76 100644 --- a/packages/user/src/model/own-roles/sqlFactory.ts +++ b/packages/user/src/model/own-roles/sqlFactory.ts @@ -40,8 +40,10 @@ class RoleSqlFactory< INSERT INTO ${permissionsTable} ("role_id", "permission") SELECT * FROM ${sql.unnest( - [permissions.map((permission) => [roleId, permission])], - ["integer", "varchar"] + permissions.map((permission) => { + return [roleId, permission]; + }), + ["int4", "varchar"] )} RETURNING *; `; diff --git a/packages/user/src/types/roles/index.ts b/packages/user/src/types/roles/index.ts index 90fa78117..8100fad51 100644 --- a/packages/user/src/types/roles/index.ts +++ b/packages/user/src/types/roles/index.ts @@ -10,6 +10,7 @@ interface RoleWithPermissions extends Role { interface RoleCreateInput { role: string; + default: boolean; permissions: string[]; } From 3e5392eaff4e68ee9cf8b875021dc5992cabbb64 Mon Sep 17 00:00:00 2001 From: Dipendra Upreti Date: Fri, 5 Apr 2024 17:10:36 +0545 Subject: [PATCH 3/3] fix(user): fix getRolesSql --- packages/user/src/model/own-roles/sqlFactory.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/user/src/model/own-roles/sqlFactory.ts b/packages/user/src/model/own-roles/sqlFactory.ts index ed8c47f76..221643f94 100644 --- a/packages/user/src/model/own-roles/sqlFactory.ts +++ b/packages/user/src/model/own-roles/sqlFactory.ts @@ -77,13 +77,13 @@ class RoleSqlFactory< return sql.unsafe` SELECT ${this.getTableFragment()}.*, - COALESCE(user_permissions.permission, '[]') AS permissions + 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 + ) AS role_permissions ON TRUE; `; };