From 5d208c7ea9ee4147f32e34a59d180f5bf9c7002b Mon Sep 17 00:00:00 2001 From: masterchief-Dave Date: Thu, 8 Aug 2024 05:43:15 +0100 Subject: [PATCH 1/2] feat: get roles in organisation --- .gitignore | 3 +- src/controllers/OrgController.ts | 29 +++++- src/enums/permission-category.enum.ts | 9 ++ src/models/organization-member.ts | 27 ++++++ src/models/organization-role.entity.ts | 36 +++++++ src/models/organization.ts | 27 ++++-- src/models/permissions.entity.ts | 23 +++++ src/models/user.ts | 7 ++ src/routes/organisation.ts | 12 +++ src/services/org.services.ts | 43 ++++++++- src/services/user.services.ts | 9 +- src/test/blog.spec.ts | 2 + src/test/organisation.spec.ts | 125 +++++++++++++++++++------ src/test/users.spec.ts | 11 +-- 14 files changed, 309 insertions(+), 54 deletions(-) create mode 100644 src/enums/permission-category.enum.ts create mode 100644 src/models/organization-member.ts create mode 100644 src/models/organization-role.entity.ts create mode 100644 src/models/permissions.entity.ts diff --git a/.gitignore b/.gitignore index 28c12193..227e94c8 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,5 @@ package-lock.json .mail.ts.bk src/bk -swagger.json \ No newline at end of file +swagger.json +todo.txt \ No newline at end of file diff --git a/src/controllers/OrgController.ts b/src/controllers/OrgController.ts index 46eeb05e..83c3dea0 100644 --- a/src/controllers/OrgController.ts +++ b/src/controllers/OrgController.ts @@ -1,4 +1,4 @@ -import { Request, Response, NextFunction } from "express"; +import { NextFunction, Request, Response } from "express"; import { OrgService } from "../services/org.services"; import log from "../utils/logger"; @@ -1071,4 +1071,31 @@ export class OrgController { next(error); } } + + async getSingleRole(req: Request, res: Response, next: NextFunction) { + try { + const organizationId = req.params.org_id; + const roleId = req.params.role_id; + const response = await this.orgService.fetchSingleRole( + organizationId, + roleId, + ); + } catch (error) { + next(error); + } + } + + async getAllOrganizationRoles( + req: Request, + res: Response, + next: NextFunction, + ) { + try { + const organizationId = req.params.org_id; + const response = + await this.orgService.fetchAllRolesInOrganization(organizationId); + } catch (error) { + next(error); + } + } } diff --git a/src/enums/permission-category.enum.ts b/src/enums/permission-category.enum.ts new file mode 100644 index 00000000..27a37340 --- /dev/null +++ b/src/enums/permission-category.enum.ts @@ -0,0 +1,9 @@ +export enum PermissionCategory { + CanViewTransactions = "canViewTransactions", + CanViewRefunds = "canViewRefunds", + CanLogRefunds = "canLogRefunds", + CanViewUsers = "canViewUsers", + CanCreateUsers = "canCreateUsers", + CanEditUsers = "canEditUsers", + CanBlacklistWhitelistUsers = "canBlacklistWhitelistUsers", +} diff --git a/src/models/organization-member.ts b/src/models/organization-member.ts new file mode 100644 index 00000000..0a71ac07 --- /dev/null +++ b/src/models/organization-member.ts @@ -0,0 +1,27 @@ +import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; +import ExtendedBaseEntity from "./extended-base-entity"; +import { Organization } from "./organization"; +import { OrganizationRole } from "./organization-role.entity"; +import { Profile } from "./profile"; +import { User } from "./user"; + +@Entity() +export class OrganizationMember extends ExtendedBaseEntity { + @PrimaryGeneratedColumn("uuid") + id: string; + + @ManyToOne(() => User, (user) => user.organizationMembers) + user_id: User; + + @ManyToOne( + () => Organization, + (organization) => organization.organizationMembers, + ) + organization_id: Organization; + + @ManyToOne(() => OrganizationRole, (role) => role.organizationMembers) + role: OrganizationRole; + + @ManyToOne(() => Profile) + profile_id: Profile; +} diff --git a/src/models/organization-role.entity.ts b/src/models/organization-role.entity.ts new file mode 100644 index 00000000..0ca7ab3a --- /dev/null +++ b/src/models/organization-role.entity.ts @@ -0,0 +1,36 @@ +import { + Column, + Entity, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, +} from "typeorm"; +import ExtendedBaseEntity from "./extended-base-entity"; +import { Organization } from "./organization"; +import { OrganizationMember } from "./organization-member"; +import { Permissions } from "./permissions.entity"; + +@Entity("roles") +export class OrganizationRole extends ExtendedBaseEntity { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column({ length: 50, nullable: false }) + name: string; + + @Column({ length: 200, nullable: true }) + description: string; + + @OneToMany(() => Permissions, (permission) => permission.role, { + eager: false, + }) + permissions: Permissions[]; + + @ManyToOne(() => Organization, (organization) => organization.role, { + eager: false, + }) + organization: Organization; + + @OneToMany(() => OrganizationMember, (member) => member.role) + organizationMembers: OrganizationMember[]; +} diff --git a/src/models/organization.ts b/src/models/organization.ts index 21463b63..8847ddc7 100644 --- a/src/models/organization.ts +++ b/src/models/organization.ts @@ -1,19 +1,21 @@ import { - Entity, - PrimaryGeneratedColumn, + BeforeInsert, Column, - OneToMany, + Entity, ManyToMany, - BeforeInsert, + OneToMany, + PrimaryGeneratedColumn, UpdateDateColumn, } from "typeorm"; -import { User } from "."; import { v4 as uuidv4 } from "uuid"; -import { UserOrganization } from "./user-organisation"; -import ExtendedBaseEntity from "./extended-base-entity"; +import { User } from "."; import { BillingPlan } from "./billing-plan"; +import ExtendedBaseEntity from "./extended-base-entity"; +import { OrganizationMember } from "./organization-member"; +import { OrganizationRole } from "./organization-role.entity"; import { Payment } from "./payment"; import { Product } from "./product"; +import { UserOrganization } from "./user-organisation"; @Entity() export class Organization extends ExtendedBaseEntity { @@ -74,6 +76,17 @@ export class Organization extends ExtendedBaseEntity { @OneToMany(() => Product, (product) => product.org, { cascade: true }) products: Product[]; + @OneToMany(() => OrganizationRole, (role) => role.organization, { + eager: false, + }) + role: OrganizationRole; + + @OneToMany( + () => OrganizationMember, + (organizationMember) => organizationMember.organization_id, + ) + organizationMembers: OrganizationMember[]; + @BeforeInsert() generateSlug() { this.slug = uuidv4(); diff --git a/src/models/permissions.entity.ts b/src/models/permissions.entity.ts new file mode 100644 index 00000000..23118fed --- /dev/null +++ b/src/models/permissions.entity.ts @@ -0,0 +1,23 @@ +import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; +import { PermissionCategory } from "../enums/permission-category.enum"; +import ExtendedBaseEntity from "./extended-base-entity"; +import { OrganizationRole } from "./organization-role.entity"; + +@Entity() +export class Permissions extends ExtendedBaseEntity { + @PrimaryGeneratedColumn("uuid") + id: string; + @Column({ + type: "enum", + enum: PermissionCategory, + }) + category: PermissionCategory; + + @Column({ type: "boolean", nullable: false }) + permission_list: boolean; + + @ManyToOne(() => OrganizationRole, (role) => role.permissions, { + eager: false, + }) + role: OrganizationRole; +} diff --git a/src/models/user.ts b/src/models/user.ts index fa8602eb..c78cce37 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -19,6 +19,7 @@ import { UserRole } from "../enums/userRoles"; import { getIsInvalidMessage } from "../utils"; import ExtendedBaseEntity from "./extended-base-entity"; import { Like } from "./like"; +import { OrganizationMember } from "./organization-member"; import { UserOrganization } from "./user-organisation"; @Entity() @@ -105,6 +106,12 @@ export class User extends ExtendedBaseEntity { @Column({ nullable: true, type: "bigint" }) passwordResetExpires: number; + @OneToMany( + () => OrganizationMember, + (organizationMember) => organizationMember.organization_id, + ) + organizationMembers: OrganizationMember[]; + createPasswordResetToken(): string { const resetToken = crypto.randomBytes(32).toString("hex"); diff --git a/src/routes/organisation.ts b/src/routes/organisation.ts index c78263a4..e880e660 100644 --- a/src/routes/organisation.ts +++ b/src/routes/organisation.ts @@ -66,4 +66,16 @@ orgRouter.put( checkPermissions([UserRole.SUPER_ADMIN, UserRole.USER]), orgController.updateOrganisation.bind(orgController), ); + +orgRouter.get( + "/organizations/:org_id/roles/:role_id", + authMiddleware, + orgController.getSingleRole, +); + +orgRouter.get( + "/organizations/:org_id/roles", + authMiddleware, + orgController.getAllOrganizationRoles, +); export { orgRouter }; diff --git a/src/services/org.services.ts b/src/services/org.services.ts index 2773e304..6b6b1289 100644 --- a/src/services/org.services.ts +++ b/src/services/org.services.ts @@ -1,19 +1,27 @@ +import { Repository } from "typeorm"; +import { v4 as uuidv4 } from "uuid"; +import config from "../config/index"; import AppDataSource from "../data-source"; import { UserRole } from "../enums/userRoles"; import { BadRequest } from "../middleware"; +import { Conflict, ResourceNotFound } from "../middleware/error"; +import { Invitation, OrgInviteToken, UserOrganization } from "../models"; import { Organization } from "../models/organization"; +import { OrganizationRole } from "../models/organization-role.entity"; import { User } from "../models/user"; import { ICreateOrganisation, IOrgService } from "../types"; -import log from "../utils/logger"; -import { UserOrganization, Invitation, OrgInviteToken } from "../models"; -import { v4 as uuidv4 } from "uuid"; import { addEmailToQueue } from "../utils/queue"; import renderTemplate from "../views/email/renderTemplate"; -import { Conflict, ResourceNotFound } from "../middleware/error"; -import config from "../config/index"; const frontendBaseUrl = config.BASE_URL; export class OrgService implements IOrgService { + private organizationRepository: Repository; + private organizationRoleRepository: Repository; + constructor() { + this.organizationRepository = AppDataSource.getRepository(Organization); + this.organizationRoleRepository = + AppDataSource.getRepository(OrganizationRole); + } public async createOrganisation( payload: ICreateOrganisation, userId: string, @@ -340,4 +348,29 @@ export class OrgService implements IOrgService { return []; } + + public async fetchSingleRole(organizationId: string, roleId: string) { + // const orgRoles = await this. + } + + public async fetchAllRolesInOrganization(organizationId: string) { + try { + const organization = await this.organizationRepository.findOne({ + where: { id: organizationId }, + }); + + if (!organization) { + throw new ResourceNotFound("Organization not found"); + } + + const roles = await this.organizationRoleRepository.find({ + where: { organization: { id: organizationId } }, + select: ["id", "name", "description"], + }); + + return roles; + } catch (error) { + throw error; + } + } } diff --git a/src/services/user.services.ts b/src/services/user.services.ts index 02f7728c..249fad3a 100644 --- a/src/services/user.services.ts +++ b/src/services/user.services.ts @@ -1,11 +1,10 @@ // src/services/UserService.ts -import { User } from "../models/user"; -import { Profile } from "../models/profile"; -import { IUserService } from "../types"; -import { HttpError } from "../middleware"; import { Repository, UpdateResult } from "typeorm"; -import AppDataSource from "../data-source"; import { cloudinary } from "../config/multer"; +import AppDataSource from "../data-source"; +import { HttpError } from "../middleware"; +import { Profile } from "../models/profile"; +import { User } from "../models/user"; interface IUserProfileUpdate { first_name: string; diff --git a/src/test/blog.spec.ts b/src/test/blog.spec.ts index b5b90154..6231b9b1 100644 --- a/src/test/blog.spec.ts +++ b/src/test/blog.spec.ts @@ -14,6 +14,8 @@ jest.mock("../data-source", () => ({ }, })); +jest.mock("../models"); + describe("BlogService", () => { let blogService: BlogService; let blogRepositoryMock: jest.Mocked>; diff --git a/src/test/organisation.spec.ts b/src/test/organisation.spec.ts index 5340a0fa..f3c829b1 100644 --- a/src/test/organisation.spec.ts +++ b/src/test/organisation.spec.ts @@ -1,47 +1,50 @@ // @ts-nocheck -import { OrgService } from "../services"; -import { Organization, User, UserOrganization } from "../models"; -import AppDataSource from "../data-source"; -import { UserRole } from "../enums/userRoles"; -import { BadRequest } from "../middleware"; import jwt from "jsonwebtoken"; -import { AuthService } from "../services/index.ts"; +import AppDataSource from "../data-source"; +import { Organization, User } from "../models"; +import { OrgService } from "../services"; -import { authMiddleware } from "../middleware/auth.ts"; -import { OrgService } from "../services/org.services.ts"; +import { Repository } from "typeorm"; import { OrgController } from "../controllers/OrgController.ts"; +import { authMiddleware } from "../middleware/auth.ts"; +import { InvalidInput, ResourceNotFound } from "../middleware/error.ts"; import { validateOrgId } from "../middleware/organizationValidation.ts"; -import { InvalidInput } from "../middleware/error.ts"; -import { authMiddleware } from "../middleware"; -import { OrgService } from "../services/organisation.service"; - -jest.mock("../data-source", () => { - return { - AppDataSource: { - manager: { - save: jest.fn(), - findOne: jest.fn(), - }, - getRepository: jest.fn(), - initialize: jest.fn().mockResolvedValue(true), - }, - }; -}); -jest.mock("../models"); -jest.mock("jsonwebtoken"); +import { OrganizationRole } from "../models/organization-role.entity.ts"; + +jest.mock("../data-source", () => ({ + __esModule: true, + default: { + getRepository: jest.fn(), + initialize: jest.fn(), + isInitialized: false, + }, +})); describe("Organization Controller and Middleware", () => { - let orgService: OrgService; + let organizationService: OrgService; let orgController: OrgController; let mockManager; + let organizationRepositoryMock: jest.Mocked>; + let organizationRoleRepositoryMock: jest.Mocked>; beforeEach(() => { + jest.clearAllMocks(); orgController = new OrgController(); + mockManager = { findOne: jest.fn(), }; - AppDataSource.manager = mockManager; - AppDataSource.getRepository = jest.fn().mockReturnValue(mockManager); + organizationRepositoryMock = { + findOne: jest.fn(), + } as any; + organizationRoleRepositoryMock = { + find: jest.fn(), + } as any; + (AppDataSource.getRepository as jest.Mock).mockImplementation((entity) => { + if (entity === Organization) return organizationRepositoryMock; + if (entity === OrganizationRole) return organizationRoleRepositoryMock; + }); + organizationService = new OrgService(); }); it("check if user is authenticated", async () => { @@ -131,4 +134,68 @@ describe("Organization Controller and Middleware", () => { "Valid org_id must be provided", ); }); + + describe("fetchAllRolesInOrganization", () => { + it("should fetch all roles for an existing organization", async () => { + const organizationId = "org123"; + const mockOrganization = { id: organizationId, name: "Test Org" }; + const mockRoles = [ + { id: "role1", name: "Admin", description: "Administrator" }, + { id: "role2", name: "User", description: "Regular User" }, + ]; + + organizationRepositoryMock.findOne.mockResolvedValue(mockOrganization); + organizationRoleRepositoryMock.find.mockResolvedValue(mockRoles); + + const result = + await organizationService.fetchAllRolesInOrganization(organizationId); + + expect(result).toEqual(mockRoles); + expect(organizationRepositoryMock.findOne).toHaveBeenCalledWith({ + where: { id: organizationId }, + }); + expect(organizationRoleRepositoryMock.find).toHaveBeenCalledWith({ + where: { organization: { id: organizationId } }, + select: ["id", "name", "description"], + }); + }); + + it("should throw ResourceNotFound for non-existent organization", async () => { + const organizationId = "nonexistent123"; + + organizationRepositoryMock.findOne.mockResolvedValue(null); + + try { + await organizationService.fetchAllRolesInOrganization(organizationId); + fail("Expected ResourceNotFound to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(ResourceNotFound); + } + + expect(organizationRepositoryMock.findOne).toHaveBeenCalledWith({ + where: { id: organizationId }, + }); + expect(organizationRoleRepositoryMock.find).not.toHaveBeenCalled(); + }); + + it("should return an empty array when organization has no roles", async () => { + const organizationId = "org456"; + const mockOrganization = { id: organizationId, name: "Test Org" }; + + organizationRepositoryMock.findOne.mockResolvedValue(mockOrganization); + organizationRoleRepositoryMock.find.mockResolvedValue([]); + + const result = + await organizationService.fetchAllRolesInOrganization(organizationId); + + expect(result).toEqual([]); + expect(organizationRepositoryMock.findOne).toHaveBeenCalledWith({ + where: { id: organizationId }, + }); + expect(organizationRoleRepositoryMock.find).toHaveBeenCalledWith({ + where: { organization: { id: organizationId } }, + select: ["id", "name", "description"], + }); + }); + }); }); diff --git a/src/test/users.spec.ts b/src/test/users.spec.ts index eb02fe75..3a482a9d 100644 --- a/src/test/users.spec.ts +++ b/src/test/users.spec.ts @@ -1,12 +1,12 @@ // @ts-nocheck -import { UserService } from "../services"; -import { UserController } from "../controllers/UserController"; -import { User } from "../models"; +import { NextFunction, Request, Response } from "express"; import { Repository } from "typeorm"; +import { validate as mockUuidValidate } from "uuid"; +import { UserController } from "../controllers/UserController"; import AppDataSource from "../data-source"; import { HttpError } from "../middleware"; -import { validate as mockUuidValidate } from "uuid"; -import { Request, Response, NextFunction } from "express"; +import { User } from "../models"; +import { UserService } from "../services"; jest.mock("../data-source", () => ({ getRepository: jest.fn(), @@ -40,7 +40,6 @@ describe("UserService", () => { }; next = jest.fn(); mockUuidValidate.mockReturnValue(true); - (AppDataSource.getRepository as jest.Mock).mockReturnValue( userRepositoryMock, ); From 604860058970fa8b2de887715f9e8e0a47ab8cc9 Mon Sep 17 00:00:00 2001 From: masterchief-Dave Date: Thu, 8 Aug 2024 06:02:19 +0100 Subject: [PATCH 2/2] fix: response sent to user --- src/controllers/OrgController.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/controllers/OrgController.ts b/src/controllers/OrgController.ts index 83c3dea0..9af6e9c3 100644 --- a/src/controllers/OrgController.ts +++ b/src/controllers/OrgController.ts @@ -1,4 +1,5 @@ import { NextFunction, Request, Response } from "express"; +import { ResourceNotFound, ServerError } from "../middleware"; import { OrgService } from "../services/org.services"; import log from "../utils/logger"; @@ -1080,6 +1081,11 @@ export class OrgController { organizationId, roleId, ); + + return res.status(200).json({ + status_code: 200, + data: response, + }); } catch (error) { next(error); } @@ -1094,8 +1100,16 @@ export class OrgController { const organizationId = req.params.org_id; const response = await this.orgService.fetchAllRolesInOrganization(organizationId); + + return res.status(200).json({ + status_code: 200, + data: response, + }); } catch (error) { - next(error); + if (error instanceof ResourceNotFound) { + next(error); + } + next(new ServerError("Error fetching all roles in organization")); } } }