From 36c2e089b272b45e95b7b9b8a09782bc6e60b490 Mon Sep 17 00:00:00 2001 From: Bharath Date: Wed, 11 Dec 2024 13:01:54 +0530 Subject: [PATCH 01/23] FEAT: Multi-tenancy support --- package-lock.json | 10 +- src/authentication/authentication.guard.ts | 2 + src/authentication/authentication.helper.ts | 3 +- .../entity/abstract.tenant.entity.ts | 9 + src/authorization/entity/entity.entity.ts | 4 +- .../entity/entityPermission.entity.ts | 4 +- src/authorization/entity/group.entity.ts | 4 +- .../entity/groupPermission.entity.ts | 4 +- src/authorization/entity/groupRole.entity.ts | 4 +- src/authorization/entity/role.entity.ts | 4 +- .../entity/rolePermission.entity.ts | 4 +- src/authorization/entity/tenant.entity.ts | 14 ++ src/authorization/entity/user.entity.ts | 4 +- src/authorization/entity/userGroup.entity.ts | 5 +- .../entity/userPermission.entity.ts | 4 +- src/authorization/service/entity.service.ts | 4 +- src/authorization/service/group.service.ts | 10 +- src/authorization/service/role.service.ts | 4 +- src/authorization/service/user.service.ts | 8 +- src/config/migration.config.ts | 2 +- .../1733833844028-MultiTenantFeature.ts | 219 ++++++++++++++++++ src/util/execution.manager.ts | 29 +++ 22 files changed, 320 insertions(+), 35 deletions(-) create mode 100644 src/authorization/entity/abstract.tenant.entity.ts create mode 100644 src/authorization/entity/tenant.entity.ts create mode 100644 src/migrations/1733833844028-MultiTenantFeature.ts create mode 100644 src/util/execution.manager.ts diff --git a/package-lock.json b/package-lock.json index 940a51f..f2fdcaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5005,7 +5005,7 @@ "any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" }, "anymatch": { "version": "3.1.1", @@ -6640,7 +6640,7 @@ "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, "content-disposition": { "version": "0.5.3", @@ -6952,7 +6952,7 @@ "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, "denque": { "version": "1.5.0", @@ -8669,7 +8669,7 @@ "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, "has-value": { "version": "1.0.0", @@ -14460,7 +14460,7 @@ "thenify-all": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", "requires": { "thenify": ">= 3.1.0 < 4" } diff --git a/src/authentication/authentication.guard.ts b/src/authentication/authentication.guard.ts index 3b0bda5..0936864 100644 --- a/src/authentication/authentication.guard.ts +++ b/src/authentication/authentication.guard.ts @@ -1,6 +1,7 @@ import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { GqlExecutionContext } from '@nestjs/graphql'; import { AuthenticationHelper } from './authentication.helper'; +import { ExecutionManager } from '../util/execution.manager'; @Injectable() export class AuthGuard implements CanActivate { @@ -13,6 +14,7 @@ export class AuthGuard implements CanActivate { if (token) { const reqAuthToken = token.split(' ')[1]; ctx.user = this.authenticationHelper.validateAuthToken(reqAuthToken); + ExecutionManager.setTenantId(ctx.user.tenantId); return true; } } diff --git a/src/authentication/authentication.helper.ts b/src/authentication/authentication.helper.ts index 163e3e5..7af8415 100644 --- a/src/authentication/authentication.helper.ts +++ b/src/authentication/authentication.helper.ts @@ -13,9 +13,10 @@ export class AuthenticationHelper { this.configService.get('JWT_TOKEN_EXPTIME') * 1 || 60 * 60; const secret = this.configService.get('JWT_SECRET') as string; const username = userDetails.email || userDetails.phone; - + const tenantId = userDetails.tenantId; const dataStoredInToken = { username: username, + tenantId, sub: userDetails.id, env: this.configService.get('ENV') || 'local', }; diff --git a/src/authorization/entity/abstract.tenant.entity.ts b/src/authorization/entity/abstract.tenant.entity.ts new file mode 100644 index 0000000..fe4f89e --- /dev/null +++ b/src/authorization/entity/abstract.tenant.entity.ts @@ -0,0 +1,9 @@ +import { PrimaryColumn } from 'typeorm'; +import BaseEntity from './base.entity'; + +class AbstractTenantEntity extends BaseEntity { + @PrimaryColumn({ type: 'uuid' }) + public tenantId!: string; +} + +export default AbstractTenantEntity; diff --git a/src/authorization/entity/entity.entity.ts b/src/authorization/entity/entity.entity.ts index 51749e4..6c70a9a 100644 --- a/src/authorization/entity/entity.entity.ts +++ b/src/authorization/entity/entity.entity.ts @@ -1,9 +1,9 @@ import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; -import BaseEntity from './base.entity'; +import AbstractTenantEntity from './abstract.tenant.entity'; @Entity() @Index('entity_name_unique_idx', { synchronize: false }) -class EntityModel extends BaseEntity { +class EntityModel extends AbstractTenantEntity { @PrimaryGeneratedColumn('uuid') public id!: string; diff --git a/src/authorization/entity/entityPermission.entity.ts b/src/authorization/entity/entityPermission.entity.ts index a3f08ed..fabdcaf 100644 --- a/src/authorization/entity/entityPermission.entity.ts +++ b/src/authorization/entity/entityPermission.entity.ts @@ -1,8 +1,8 @@ import { Entity, PrimaryColumn } from 'typeorm'; -import BaseEntity from './base.entity'; +import AbstractTenantEntity from './abstract.tenant.entity'; @Entity() -class EntityPermission extends BaseEntity { +class EntityPermission extends AbstractTenantEntity { @PrimaryColumn({ type: 'uuid' }) public permissionId!: string; diff --git a/src/authorization/entity/group.entity.ts b/src/authorization/entity/group.entity.ts index 4e6ab36..efa31cf 100644 --- a/src/authorization/entity/group.entity.ts +++ b/src/authorization/entity/group.entity.ts @@ -1,9 +1,9 @@ import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; -import BaseEntity from './base.entity'; +import AbstractTenantEntity from './abstract.tenant.entity'; @Entity() @Index('group_name_unique_idx', { synchronize: false }) -class Group extends BaseEntity { +class Group extends AbstractTenantEntity { @PrimaryGeneratedColumn('uuid') public id!: string; diff --git a/src/authorization/entity/groupPermission.entity.ts b/src/authorization/entity/groupPermission.entity.ts index 93b9f3b..7481125 100644 --- a/src/authorization/entity/groupPermission.entity.ts +++ b/src/authorization/entity/groupPermission.entity.ts @@ -1,8 +1,8 @@ import { Entity, PrimaryColumn } from 'typeorm'; -import BaseEntity from './base.entity'; +import AbstractTenantEntity from './abstract.tenant.entity'; @Entity() -class GroupPermission extends BaseEntity { +class GroupPermission extends AbstractTenantEntity { @PrimaryColumn({ type: 'uuid' }) public permissionId!: string; diff --git a/src/authorization/entity/groupRole.entity.ts b/src/authorization/entity/groupRole.entity.ts index d0dd1ed..8104d22 100644 --- a/src/authorization/entity/groupRole.entity.ts +++ b/src/authorization/entity/groupRole.entity.ts @@ -1,8 +1,8 @@ import { Entity, PrimaryColumn } from 'typeorm'; -import BaseEntity from './base.entity'; +import AbstractTenantEntity from './abstract.tenant.entity'; @Entity() -class GroupRole extends BaseEntity { +class GroupRole extends AbstractTenantEntity { @PrimaryColumn({ type: 'uuid' }) public roleId!: string; diff --git a/src/authorization/entity/role.entity.ts b/src/authorization/entity/role.entity.ts index 6e62937..e34bd2c 100644 --- a/src/authorization/entity/role.entity.ts +++ b/src/authorization/entity/role.entity.ts @@ -1,9 +1,9 @@ import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; -import BaseEntity from './base.entity'; +import AbstractTenantEntity from './abstract.tenant.entity'; @Entity() @Index('role_name_unique_idx', { synchronize: false }) -class Role extends BaseEntity { +class Role extends AbstractTenantEntity { @PrimaryGeneratedColumn('uuid') public id!: string; diff --git a/src/authorization/entity/rolePermission.entity.ts b/src/authorization/entity/rolePermission.entity.ts index adc75d6..7ae285d 100644 --- a/src/authorization/entity/rolePermission.entity.ts +++ b/src/authorization/entity/rolePermission.entity.ts @@ -1,8 +1,8 @@ import { Entity, PrimaryColumn } from 'typeorm'; -import BaseEntity from './base.entity'; +import AbstractTenantEntity from './abstract.tenant.entity'; @Entity() -class RolePermission extends BaseEntity { +class RolePermission extends AbstractTenantEntity { @PrimaryColumn({ type: 'uuid' }) public permissionId!: string; diff --git a/src/authorization/entity/tenant.entity.ts b/src/authorization/entity/tenant.entity.ts new file mode 100644 index 0000000..be0b646 --- /dev/null +++ b/src/authorization/entity/tenant.entity.ts @@ -0,0 +1,14 @@ +import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; +import BaseEntity from './base.entity'; + +@Entity() +@Index('tenant_name_unique_idx', { synchronize: false }) +class Tenant extends BaseEntity { + @PrimaryGeneratedColumn('uuid') + public id!: string; + + @Column() + public name!: string; +} + +export default Tenant; diff --git a/src/authorization/entity/user.entity.ts b/src/authorization/entity/user.entity.ts index 0da6209..89ea1ed 100644 --- a/src/authorization/entity/user.entity.ts +++ b/src/authorization/entity/user.entity.ts @@ -1,6 +1,6 @@ import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; import { Status } from '../../schema/graphql.schema'; -import BaseEntity from './base.entity'; +import AbstractTenantEntity from './abstract.tenant.entity'; @Entity() @Index('user_phone_unique_idx', ['phone'], { @@ -8,7 +8,7 @@ import BaseEntity from './base.entity'; where: '"deleted_at" IS NULL', }) @Index('user_email_unique_idx', { synchronize: false }) -class User extends BaseEntity { +class User extends AbstractTenantEntity { @PrimaryGeneratedColumn('uuid') public id!: string; diff --git a/src/authorization/entity/userGroup.entity.ts b/src/authorization/entity/userGroup.entity.ts index 41fef17..c88608a 100644 --- a/src/authorization/entity/userGroup.entity.ts +++ b/src/authorization/entity/userGroup.entity.ts @@ -1,7 +1,8 @@ import { Entity, PrimaryColumn } from 'typeorm'; -import BaseEntity from './base.entity'; +import AbstractTenantEntity from './abstract.tenant.entity'; + @Entity() -class UserGroup extends BaseEntity { +class UserGroup extends AbstractTenantEntity { @PrimaryColumn({ type: 'uuid' }) public groupId!: string; diff --git a/src/authorization/entity/userPermission.entity.ts b/src/authorization/entity/userPermission.entity.ts index 97788a3..bebea6d 100644 --- a/src/authorization/entity/userPermission.entity.ts +++ b/src/authorization/entity/userPermission.entity.ts @@ -1,8 +1,8 @@ import { Entity, PrimaryColumn } from 'typeorm'; -import BaseEntity from './base.entity'; +import AbstractTenantEntity from './abstract.tenant.entity'; @Entity() -class UserPermission extends BaseEntity { +class UserPermission extends AbstractTenantEntity { @PrimaryColumn({ type: 'uuid' }) public permissionId!: string; diff --git a/src/authorization/service/entity.service.ts b/src/authorization/service/entity.service.ts index d5327f7..b22e3bc 100644 --- a/src/authorization/service/entity.service.ts +++ b/src/authorization/service/entity.service.ts @@ -14,6 +14,7 @@ import { EntityModelRepository } from '../repository/entity.repository'; import { EntityPermissionRepository } from '../repository/entityPermission.repository'; import { PermissionRepository } from '../repository/permission.repository'; import { EntityServiceInterface } from './entity.service.interface'; +import { ExecutionManager } from '../../util/execution.manager'; @Injectable() export class EntityService implements EntityServiceInterface { @@ -100,9 +101,10 @@ export class EntityService implements EntityServiceInterface { ); } + const tenantId = ExecutionManager.getTenantId(); const permissionsToBeRemovedFromEntity: EntityPermission[] = existingPermissionsOfEntity .filter((p) => !validPermissionsInRequest.has(p.id)) - .map((p) => ({ permissionId: p.id, entityId: id })); + .map((p) => ({ permissionId: p.id, entityId: id, tenantId })); const entityPermission = this.entityPermissionRepository.create( request.permissions.map((permission) => ({ diff --git a/src/authorization/service/group.service.ts b/src/authorization/service/group.service.ts index fc062ea..0b57db1 100644 --- a/src/authorization/service/group.service.ts +++ b/src/authorization/service/group.service.ts @@ -41,6 +41,7 @@ import SearchService from './search.service'; import { UserCacheServiceInterface } from './usercache.service.interface'; import { DUPLICATE_ERROR_CODE } from '../../constants/db.error.constants'; import { LoggerService } from '../../logger/logger.service'; +import { ExecutionManager } from '../../util/execution.manager'; @Injectable() export class GroupService implements GroupServiceInterface { @@ -209,9 +210,10 @@ export class GroupService implements GroupServiceInterface { ); } + const tenantId = ExecutionManager.getTenantId(); const permissionsToBeRemovedFromGroup: GroupPermission[] = existingPermissionsOfGroup .filter((p) => !validPermissionsInRequest.has(p.id)) - .map((p) => ({ permissionId: p.id, groupId: id })); + .map((p) => ({ permissionId: p.id, groupId: id, tenantId })); const groupPermission = this.groupPermissionRepository.create( request.permissions.map((permission) => ({ @@ -241,9 +243,10 @@ export class GroupService implements GroupServiceInterface { const validUsersInRequest = await this.validateUsers(userIds); const existingUsersOfGroup = await this.getGroupUsers(id); + const tenantId = ExecutionManager.getTenantId(); const usersToBeRemovedFromGroup: UserGroup[] = existingUsersOfGroup .filter((user) => !validUsersInRequest.has(user.id)) - .map((user) => ({ userId: user.id, groupId: id })); + .map((user) => ({ userId: user.id, groupId: id, tenantId })); const userGroups = this.userGroupRepository.create( userIds.map((userId) => ({ userId: userId, groupId: id })), ); @@ -306,9 +309,10 @@ export class GroupService implements GroupServiceInterface { ); } + const tenantId = ExecutionManager.getTenantId(); const rolesToBeRemovedFromGroup: GroupRole[] = existingRolesOfGroup .filter((p) => !validRolesInRequest.has(p.id)) - .map((r) => ({ groupId: id, roleId: r.id })); + .map((r) => ({ groupId: id, roleId: r.id, tenantId })); const groupRoles = this.groupRoleRepository.create( request.roles.map((role) => ({ diff --git a/src/authorization/service/role.service.ts b/src/authorization/service/role.service.ts index bc6bfce..72ce4eb 100644 --- a/src/authorization/service/role.service.ts +++ b/src/authorization/service/role.service.ts @@ -23,6 +23,7 @@ import { RolePermissionRepository } from '../repository/rolePermission.repositor import { RoleServiceInterface } from './role.service.interface'; import { RoleCacheServiceInterface } from './rolecache.service.interface'; import SearchService from './search.service'; +import { ExecutionManager } from '../../util/execution.manager'; @Injectable() export class RoleService implements RoleServiceInterface { @@ -142,9 +143,10 @@ export class RoleService implements RoleServiceInterface { ); } + const tenantId = ExecutionManager.getTenantId(); const permissionsToBeRemovedFromRole: RolePermission[] = existingPermissionsOfRole .filter((p) => !validPermissionsInRequest.has(p.id)) - .map((p) => ({ permissionId: p.id, roleId: id })); + .map((p) => ({ permissionId: p.id, roleId: id, tenantId })); const rolePermissions = this.rolePermissionRepository.create( request.permissions.map((permission) => ({ diff --git a/src/authorization/service/user.service.ts b/src/authorization/service/user.service.ts index a974230..315f2d4 100644 --- a/src/authorization/service/user.service.ts +++ b/src/authorization/service/user.service.ts @@ -32,6 +32,7 @@ import { RoleCacheServiceInterface } from './rolecache.service.interface'; import SearchService from './search.service'; import { UserServiceInterface } from './user.service.interface'; import { UserCacheServiceInterface } from './usercache.service.interface'; +import { ExecutionManager } from '../../util/execution.manager'; @Injectable() export class UserService implements UserServiceInterface { @@ -147,9 +148,10 @@ export class UserService implements UserServiceInterface { ); } + const tenantId = ExecutionManager.getTenantId(); const groupsToBeRemovedFromUser: UserGroup[] = existingGroupsOfUser .filter((p) => !validGroupsInRequest.has(p.id)) - .map((g) => ({ userId: id, groupId: g.id })); + .map((g) => ({ userId: id, groupId: g.id, tenantId })); const userGroups = this.userGroupRepository.create( user.groups.map((group) => ({ userId: id, groupId: group })), ); @@ -186,10 +188,10 @@ export class UserService implements UserServiceInterface { request.permissions.filter((p) => !validPermissions.has(p)).toString(), ); } - + const tenantId = ExecutionManager.getTenantId(); const userPermissionsToBeRemoved: UserPermission[] = existingUserPermissions .filter((p) => !validPermissions.has(p.id)) - .map((p) => ({ userId: id, permissionId: p.id })); + .map((p) => ({ userId: id, permissionId: p.id, tenantId })); this.userPermissionRepository.remove(userPermissionsToBeRemoved); const userPermissionsCreated = this.userPermissionRepository.create( diff --git a/src/config/migration.config.ts b/src/config/migration.config.ts index 34b3a79..bb6934c 100644 --- a/src/config/migration.config.ts +++ b/src/config/migration.config.ts @@ -1,6 +1,6 @@ import { DataSource } from 'typeorm'; import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; -import dotenv from 'dotenv'; +import * as dotenv from 'dotenv'; dotenv.config(); /** * Uses env params to configure TypeORM database library diff --git a/src/migrations/1733833844028-MultiTenantFeature.ts b/src/migrations/1733833844028-MultiTenantFeature.ts new file mode 100644 index 0000000..a894d56 --- /dev/null +++ b/src/migrations/1733833844028-MultiTenantFeature.ts @@ -0,0 +1,219 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class MultiTenantFeature1733833844028 implements MigrationInterface { + name = 'MultiTenantFeature1733833844028'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "tenant" ("deleted_at" TIMESTAMP, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, CONSTRAINT "PK_da8c6efd67bb301e810e56ac139" PRIMARY KEY ("id"))`, + ); + const tenants = await queryRunner.query( + `INSERT INTO "tenant" ("name") VALUES ('Default Tenant') RETURNING id`, + ); + const tenantId = tenants[0].id; + await queryRunner.query( + `ALTER TABLE "entity_model" ADD "tenant_id" uuid NOT NULL DEFAULT '${tenantId}'`, + ); + await queryRunner.query( + `ALTER TABLE "entity_model" ALTER COLUMN "tenant_id" DROP DEFAULT`, + ); + await queryRunner.query( + `ALTER TABLE "entity_model" DROP CONSTRAINT "PK_ea7e5d0ca6a0d6221f78cea499a"`, + ); + await queryRunner.query( + `ALTER TABLE "entity_model" ADD CONSTRAINT "PK_8136db3bf8c7a328973a0feb096" PRIMARY KEY ("id", "tenant_id")`, + ); + await queryRunner.query( + `ALTER TABLE "entity_permission" ADD "tenant_id" uuid NOT NULL DEFAULT '${tenantId}'`, + ); + await queryRunner.query( + `ALTER TABLE "entity_permission" ALTER COLUMN "tenant_id" DROP DEFAULT`, + ); + await queryRunner.query( + `ALTER TABLE "entity_permission" DROP CONSTRAINT "PK_22d409e099ab8a6bc3fc7b7b8a1"`, + ); + await queryRunner.query( + `ALTER TABLE "entity_permission" ADD CONSTRAINT "PK_456866ab8087e8d3a3d616dbe0a" PRIMARY KEY ("permission_id", "entity_id", "tenant_id")`, + ); + await queryRunner.query( + `ALTER TABLE "group" ADD "tenant_id" uuid NOT NULL DEFAULT '${tenantId}'`, + ); + await queryRunner.query( + `ALTER TABLE "group" ALTER COLUMN "tenant_id" DROP DEFAULT`, + ); + await queryRunner.query( + `ALTER TABLE "group" DROP CONSTRAINT "PK_256aa0fda9b1de1a73ee0b7106b"`, + ); + await queryRunner.query( + `ALTER TABLE "group" ADD CONSTRAINT "PK_3ba4cfb3cab75ac2ea4b0a9536b" PRIMARY KEY ("id", "tenant_id")`, + ); + await queryRunner.query( + `ALTER TABLE "group_permission" ADD "tenant_id" uuid NOT NULL DEFAULT '${tenantId}'`, + ); + await queryRunner.query( + `ALTER TABLE "group_permission" ALTER COLUMN "tenant_id" DROP DEFAULT`, + ); + await queryRunner.query( + `ALTER TABLE "group_permission" DROP CONSTRAINT "PK_5aadf555f3ea93c95bc952f1547"`, + ); + await queryRunner.query( + `ALTER TABLE "group_permission" ADD CONSTRAINT "PK_97edbfb9e685755fa455480f98c" PRIMARY KEY ("permission_id", "group_id", "tenant_id")`, + ); + await queryRunner.query( + `ALTER TABLE "group_role" ADD "tenant_id" uuid NOT NULL DEFAULT '${tenantId}'`, + ); + await queryRunner.query( + `ALTER TABLE "group_role" ALTER COLUMN "tenant_id" DROP DEFAULT`, + ); + await queryRunner.query( + `ALTER TABLE "group_role" DROP CONSTRAINT "PK_34b9a049ae09a85e87e7f18787b"`, + ); + await queryRunner.query( + `ALTER TABLE "group_role" ADD CONSTRAINT "PK_3a8315bbdf7bfd962bf0e7a40e5" PRIMARY KEY ("role_id", "group_id", "tenant_id")`, + ); + await queryRunner.query( + `ALTER TABLE "role" ADD "tenant_id" uuid NOT NULL DEFAULT '${tenantId}'`, + ); + await queryRunner.query( + `ALTER TABLE "role" ALTER COLUMN "tenant_id" DROP DEFAULT`, + ); + await queryRunner.query( + `ALTER TABLE "role" DROP CONSTRAINT "PK_b36bcfe02fc8de3c57a8b2391c2"`, + ); + await queryRunner.query( + `ALTER TABLE "role" ADD CONSTRAINT "PK_2b394366739f89a92b09a90aea4" PRIMARY KEY ("id", "tenant_id")`, + ); + await queryRunner.query( + `ALTER TABLE "role_permission" ADD "tenant_id" uuid NOT NULL DEFAULT '${tenantId}'`, + ); + await queryRunner.query( + `ALTER TABLE "role_permission" ALTER COLUMN "tenant_id" DROP DEFAULT`, + ); + await queryRunner.query( + `ALTER TABLE "role_permission" DROP CONSTRAINT "PK_19a94c31d4960ded0dcd0397759"`, + ); + await queryRunner.query( + `ALTER TABLE "role_permission" ADD CONSTRAINT "PK_96cb2e2da566ba65ae7f24cece2" PRIMARY KEY ("permission_id", "role_id", "tenant_id")`, + ); + await queryRunner.query( + `ALTER TABLE "user" ADD "tenant_id" uuid NOT NULL DEFAULT '${tenantId}'`, + ); + await queryRunner.query( + `ALTER TABLE "user" ALTER COLUMN "tenant_id" DROP DEFAULT`, + ); + await queryRunner.query( + `ALTER TABLE "user" DROP CONSTRAINT "PK_cace4a159ff9f2512dd42373760"`, + ); + await queryRunner.query( + `ALTER TABLE "user" ADD CONSTRAINT "PK_9a56c21022d9a4dc056d9c37575" PRIMARY KEY ("id", "tenant_id")`, + ); + await queryRunner.query( + `ALTER TABLE "user_group" ADD "tenant_id" uuid NOT NULL DEFAULT '${tenantId}'`, + ); + await queryRunner.query( + `ALTER TABLE "user_group" ALTER COLUMN "tenant_id" DROP DEFAULT`, + ); + await queryRunner.query( + `ALTER TABLE "user_group" DROP CONSTRAINT "PK_bd332ba499e012f8d20905f8061"`, + ); + await queryRunner.query( + `ALTER TABLE "user_group" ADD CONSTRAINT "PK_fe9b93596a9c69d45a04226cc40" PRIMARY KEY ("group_id", "user_id", "tenant_id")`, + ); + await queryRunner.query( + `ALTER TABLE "user_permission" ADD "tenant_id" uuid NOT NULL DEFAULT '${tenantId}'`, + ); + await queryRunner.query( + `ALTER TABLE "user_permission" ALTER COLUMN "tenant_id" DROP DEFAULT`, + ); + await queryRunner.query( + `ALTER TABLE "user_permission" DROP CONSTRAINT "PK_e55fe6295b438912cb42bce1baa"`, + ); + await queryRunner.query( + `ALTER TABLE "user_permission" ADD CONSTRAINT "PK_f63ef18a89058a5d95c171b7823" PRIMARY KEY ("permission_id", "user_id", "tenant_id")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user_permission" DROP CONSTRAINT "PK_f63ef18a89058a5d95c171b7823"`, + ); + await queryRunner.query( + `ALTER TABLE "user_permission" ADD CONSTRAINT "PK_e55fe6295b438912cb42bce1baa" PRIMARY KEY ("permission_id", "user_id")`, + ); + await queryRunner.query( + `ALTER TABLE "user_permission" DROP COLUMN "tenant_id"`, + ); + await queryRunner.query( + `ALTER TABLE "user_group" DROP CONSTRAINT "PK_fe9b93596a9c69d45a04226cc40"`, + ); + await queryRunner.query( + `ALTER TABLE "user_group" ADD CONSTRAINT "PK_bd332ba499e012f8d20905f8061" PRIMARY KEY ("group_id", "user_id")`, + ); + await queryRunner.query(`ALTER TABLE "user_group" DROP COLUMN "tenant_id"`); + await queryRunner.query( + `ALTER TABLE "user" DROP CONSTRAINT "PK_9a56c21022d9a4dc056d9c37575"`, + ); + await queryRunner.query( + `ALTER TABLE "user" ADD CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id")`, + ); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "tenant_id"`); + await queryRunner.query( + `ALTER TABLE "role_permission" DROP CONSTRAINT "PK_96cb2e2da566ba65ae7f24cece2"`, + ); + await queryRunner.query( + `ALTER TABLE "role_permission" ADD CONSTRAINT "PK_19a94c31d4960ded0dcd0397759" PRIMARY KEY ("permission_id", "role_id")`, + ); + await queryRunner.query( + `ALTER TABLE "role_permission" DROP COLUMN "tenant_id"`, + ); + await queryRunner.query( + `ALTER TABLE "role" DROP CONSTRAINT "PK_2b394366739f89a92b09a90aea4"`, + ); + await queryRunner.query( + `ALTER TABLE "role" ADD CONSTRAINT "PK_b36bcfe02fc8de3c57a8b2391c2" PRIMARY KEY ("id")`, + ); + await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "tenant_id"`); + await queryRunner.query( + `ALTER TABLE "group_role" DROP CONSTRAINT "PK_3a8315bbdf7bfd962bf0e7a40e5"`, + ); + await queryRunner.query( + `ALTER TABLE "group_role" ADD CONSTRAINT "PK_34b9a049ae09a85e87e7f18787b" PRIMARY KEY ("role_id", "group_id")`, + ); + await queryRunner.query(`ALTER TABLE "group_role" DROP COLUMN "tenant_id"`); + await queryRunner.query( + `ALTER TABLE "group_permission" DROP CONSTRAINT "PK_97edbfb9e685755fa455480f98c"`, + ); + await queryRunner.query( + `ALTER TABLE "group_permission" ADD CONSTRAINT "PK_5aadf555f3ea93c95bc952f1547" PRIMARY KEY ("permission_id", "group_id")`, + ); + await queryRunner.query( + `ALTER TABLE "group_permission" DROP COLUMN "tenant_id"`, + ); + await queryRunner.query( + `ALTER TABLE "group" DROP CONSTRAINT "PK_3ba4cfb3cab75ac2ea4b0a9536b"`, + ); + await queryRunner.query( + `ALTER TABLE "group" ADD CONSTRAINT "PK_256aa0fda9b1de1a73ee0b7106b" PRIMARY KEY ("id")`, + ); + await queryRunner.query(`ALTER TABLE "group" DROP COLUMN "tenant_id"`); + await queryRunner.query( + `ALTER TABLE "entity_permission" DROP CONSTRAINT "PK_456866ab8087e8d3a3d616dbe0a"`, + ); + await queryRunner.query( + `ALTER TABLE "entity_permission" ADD CONSTRAINT "PK_22d409e099ab8a6bc3fc7b7b8a1" PRIMARY KEY ("permission_id", "entity_id")`, + ); + await queryRunner.query( + `ALTER TABLE "entity_permission" DROP COLUMN "tenant_id"`, + ); + await queryRunner.query( + `ALTER TABLE "entity_model" DROP CONSTRAINT "PK_8136db3bf8c7a328973a0feb096"`, + ); + await queryRunner.query( + `ALTER TABLE "entity_model" ADD CONSTRAINT "PK_ea7e5d0ca6a0d6221f78cea499a" PRIMARY KEY ("id")`, + ); + await queryRunner.query( + `ALTER TABLE "entity_model" DROP COLUMN "tenant_id"`, + ); + await queryRunner.query(`DROP TABLE "tenant"`); + } +} diff --git a/src/util/execution.manager.ts b/src/util/execution.manager.ts new file mode 100644 index 0000000..6b382ff --- /dev/null +++ b/src/util/execution.manager.ts @@ -0,0 +1,29 @@ +import { AsyncLocalStorage } from 'async_hooks'; + +export class ExecutionManager { + private static asl: AsyncLocalStorage>; + + static init() { + ExecutionManager.asl = new AsyncLocalStorage(); + } + + private static getExecutionContext() { + return ExecutionManager.asl; + } + + public static getFromContext(key: string) { + return ExecutionManager.getExecutionContext()?.getStore()?.get(key) as T; + } + + public static setInContext(key: string, value: any) { + ExecutionManager.getExecutionContext()?.getStore()?.set(key, value); + } + + static setTenantId(tenantId: string) { + ExecutionManager.setInContext('tenantId', tenantId); + } + + static getTenantId() { + return ExecutionManager.getFromContext('tenantId'); + } +} From 66d5966368938a0b3afc47098eefa26261c23188 Mon Sep 17 00:00:00 2001 From: Bharath Date: Wed, 11 Dec 2024 15:40:49 +0530 Subject: [PATCH 02/23] TEST: multi-tenent test fixes --- src/authorization/graphql/entity.graphql | 1 + src/authorization/graphql/group.graphql | 2 ++ src/authorization/graphql/role.graphql | 1 + src/authorization/graphql/user.graphql | 2 ++ src/schema/graphql.schema.ts | 6 +++++ .../resolver/userauth.resolver.test.ts | 12 ++++++--- .../service/otpauth.service.test.ts | 7 +++++ .../service/passwordauth.service.test.ts | 2 ++ .../service/token.service.test.ts | 5 ++++ .../repository/entity.repository.test.ts | 1 + .../repository/group.repository.test.ts | 1 + .../groupPermission.repository.test.ts | 1 + .../repository/role.repository.test.ts | 1 + .../rolePermission.repository.test.ts | 2 ++ .../repository/user.repository.test.ts | 1 + .../repository/userGroup.repository.test.ts | 1 + .../userPermission.repository.test.ts | 1 + .../resolver/entity.resolver.test.ts | 15 +++++++---- .../resolver/group.resolver.test.ts | 26 ++++++++++++++----- .../resolver/permission.resolver.test.ts | 1 + .../resolver/role.resolver.test.ts | 23 +++++++++++----- .../resolver/user.resolver.test.ts | 6 ++++- .../service/group.service.test.ts | 2 ++ .../service/role.service.test.ts | 2 ++ .../service/user.service.test.ts | 3 +++ 25 files changed, 103 insertions(+), 22 deletions(-) diff --git a/src/authorization/graphql/entity.graphql b/src/authorization/graphql/entity.graphql index 6dd456e..55e4405 100644 --- a/src/authorization/graphql/entity.graphql +++ b/src/authorization/graphql/entity.graphql @@ -1,6 +1,7 @@ type Entity { id: ID! name: String! + tenantId: String! permissions: [Permission] } diff --git a/src/authorization/graphql/group.graphql b/src/authorization/graphql/group.graphql index a0aa248..36a6a17 100644 --- a/src/authorization/graphql/group.graphql +++ b/src/authorization/graphql/group.graphql @@ -1,6 +1,7 @@ type Group { id: ID! name: String! + tenantId: String! users: [User] roles: [Role] permissions: [Permission] @@ -27,6 +28,7 @@ input UpdateGroupRoleInput { type GroupRole { id: ID!, name: String! + tenantId: String! } input GroupInputFilter { diff --git a/src/authorization/graphql/role.graphql b/src/authorization/graphql/role.graphql index 1243d89..e90aa40 100644 --- a/src/authorization/graphql/role.graphql +++ b/src/authorization/graphql/role.graphql @@ -1,6 +1,7 @@ type Role { id: ID! name: String! + tenantId: String! permissions: [Permission] } diff --git a/src/authorization/graphql/user.graphql b/src/authorization/graphql/user.graphql index f9cb8f5..2a8d145 100644 --- a/src/authorization/graphql/user.graphql +++ b/src/authorization/graphql/user.graphql @@ -20,6 +20,7 @@ type User { groups: [Group] permissions: [Permission] inviteToken: String + tenantId: String! } enum OperationType { @@ -44,6 +45,7 @@ input UpdateUserGroupInput { type UserGroupResponse { id: ID!, name: String! + tenantId: String! } enum OperationType { diff --git a/src/schema/graphql.schema.ts b/src/schema/graphql.schema.ts index f3d4a9a..cfc9b1c 100644 --- a/src/schema/graphql.schema.ts +++ b/src/schema/graphql.schema.ts @@ -291,6 +291,7 @@ export interface InviteTokenResponse { export interface Entity { id: string; name: string; + tenantId: string; permissions?: Permission[]; } @@ -318,6 +319,7 @@ export interface IQuery { export interface Group { id: string; name: string; + tenantId: string; users?: User[]; roles?: Role[]; permissions?: Permission[]; @@ -327,6 +329,7 @@ export interface Group { export interface GroupRole { id: string; name: string; + tenantId: string; } export interface GroupPaginated extends Paginated { @@ -343,6 +346,7 @@ export interface Permission { export interface Role { id: string; name: string; + tenantId: string; permissions?: Permission[]; } @@ -367,9 +371,11 @@ export interface User { groups?: Group[]; permissions?: Permission[]; inviteToken?: string; + tenantId: string; } export interface UserGroupResponse { id: string; name: string; + tenantId: string; } diff --git a/test/authentication/resolver/userauth.resolver.test.ts b/test/authentication/resolver/userauth.resolver.test.ts index cf71957..a39f83d 100644 --- a/test/authentication/resolver/userauth.resolver.test.ts +++ b/test/authentication/resolver/userauth.resolver.test.ts @@ -35,6 +35,7 @@ const users: User[] = [ lastName: 'User', origin: 'simple', status: Status.ACTIVE, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; @@ -94,6 +95,7 @@ describe('Userauth Module', () => { firstName: users[0].firstName, lastName: users[0].lastName, status: users[0].status, + tenantId: users[0].tenantId, }; const tokenResponse = { accessToken: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. @@ -114,7 +116,7 @@ describe('Userauth Module', () => { .post(gql) .send({ query: - 'mutation { passwordLogin(input: { username: "user@test.com" password: "s3cr3t1234567890" }) { accessToken, refreshToken, user{ id, email, phone, firstName, lastName, status } }}', + 'mutation { passwordLogin(input: { username: "user@test.com" password: "s3cr3t1234567890" }) { accessToken, refreshToken, user{ id, email, phone, firstName, lastName, status tenantId} }}', }) .expect(200) .expect((res) => { @@ -184,6 +186,7 @@ describe('Userauth Module', () => { inviteToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Inh5ekBrZXl2YWx1ZS5zeXN0ZW1zIiwiaWF0IjoxNjIxNTI1NTE1LCJleHAiOjE2MjE1MjkxMTV9.t8z7rBZKkog-1jirScYU6HE7KVTzatKWjZw8lVz3xLo', status: Status.INVITED, + tenantId: users[0].tenantId, }, }; configService.get('JWT_SECRET').returns('s3cr3t1234567890'); @@ -199,7 +202,7 @@ describe('Userauth Module', () => { .set('Authorization', `Bearer ${token}`) .send({ query: `mutation { inviteTokenSignup(input: { email: "test@gmail.com" - phone: "9947849200" firstName: "Test" lastName: "Name" }) { inviteToken tokenExpiryTime user{id firstName lastName inviteToken status}}}`, + phone: "9947849200" firstName: "Test" lastName: "Name" }) { inviteToken tokenExpiryTime user{id firstName lastName inviteToken status tenantId}}}`, }) .expect(200) .expect((res) => { @@ -283,6 +286,7 @@ describe('Userauth Module', () => { inviteToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Inh5ekBrZXl2YWx1ZS5zeXN0ZW1zIiwiaWF0IjoxNjIxNTI1NTE1LCJleHAiOjE2MjE1MjkxMTV9.t8z7rBZKkog-1jirScYU6HE7KVTzatKWjZw8lVz3xLo', status: Status.INVITED, + tenantId: users[0].tenantId, }, }; tokenService @@ -303,6 +307,7 @@ describe('Userauth Module', () => { lastName inviteToken status + tenantId } } }`, @@ -340,6 +345,7 @@ describe('Userauth Module', () => { firstName: users[0].firstName, lastName: users[0].lastName, status: users[0].status, + tenantId: users[0].tenantId, }; const tokenResponse: TokenResponse = { ...token, user: user }; tokenService @@ -349,7 +355,7 @@ describe('Userauth Module', () => { return request(app.getHttpServer()) .post(gql) .send({ - query: `mutation { refresh(input: { refreshToken: "${token.refreshToken}"}) { accessToken refreshToken user { id, email, phone, firstName, lastName, status} }}`, + query: `mutation { refresh(input: { refreshToken: "${token.refreshToken}"}) { accessToken refreshToken user { id, email, phone, firstName, lastName, status tenantId} }}`, }) .expect(200) .expect((res) => { diff --git a/test/authentication/service/otpauth.service.test.ts b/test/authentication/service/otpauth.service.test.ts index 97f66c2..612d2be 100644 --- a/test/authentication/service/otpauth.service.test.ts +++ b/test/authentication/service/otpauth.service.test.ts @@ -28,6 +28,7 @@ let users: User[] = [ lastName: 'Test2', origin: 'simple', status: Status.ACTIVE, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; @@ -72,6 +73,7 @@ describe('test OTPAuthService', () => { refreshToken: token.refreshToken, origin: 'simple', status: Status.ACTIVE, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; }); @@ -88,6 +90,7 @@ describe('test OTPAuthService', () => { lastName: 'Test2', origin: 'simple', status: Status.ACTIVE, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; @@ -126,6 +129,7 @@ describe('test OTPAuthService', () => { lastName: 'Test2', origin: 'simple', status: Status.ACTIVE, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; const input: UserOTPLoginInput = { @@ -152,6 +156,7 @@ describe('test OTPAuthService', () => { lastName: users[0].lastName, origin: 'simple', status: Status.ACTIVE, + tenantId: users[0].tenantId, }, ]; const userSignup: UserOTPSignupInput = { @@ -179,6 +184,7 @@ describe('test OTPAuthService', () => { lastName: resp.lastName, origin: 'simple', status: Status.ACTIVE, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }; const expectedUser = users[0]; @@ -195,6 +201,7 @@ describe('test OTPAuthService', () => { lastName: 'Test2', origin: 'simple', status: Status.ACTIVE, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; const userSignup: UserOTPSignupInput = { diff --git a/test/authentication/service/passwordauth.service.test.ts b/test/authentication/service/passwordauth.service.test.ts index 89ca024..47d42aa 100644 --- a/test/authentication/service/passwordauth.service.test.ts +++ b/test/authentication/service/passwordauth.service.test.ts @@ -19,6 +19,7 @@ let users: User[] = [ lastName: 'Test2', origin: 'simple', status: Status.ACTIVE, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; @@ -74,6 +75,7 @@ describe('test PasswordAuthService', () => { refreshToken: token.refreshToken, origin: 'simple', status: Status.ACTIVE, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; }); diff --git a/test/authentication/service/token.service.test.ts b/test/authentication/service/token.service.test.ts index a9590fa..28bbf9a 100644 --- a/test/authentication/service/token.service.test.ts +++ b/test/authentication/service/token.service.test.ts @@ -47,6 +47,7 @@ describe('test TokenService', () => { lastName: 'Test2', origin: 'simple', status: Status.ACTIVE, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; const token = authenticationHelper.generateTokenForUser(users[0]); @@ -71,6 +72,7 @@ describe('test TokenService', () => { lastName: 'Test2', origin: 'simple', status: Status.ACTIVE, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; const refreshInviteToken = authenticationHelper.generateInvitationToken( @@ -103,6 +105,7 @@ describe('test TokenService', () => { lastName: 'Test2', origin: 'simple', status: Status.ACTIVE, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; userService @@ -125,6 +128,7 @@ describe('test TokenService', () => { lastName: 'Test2', origin: 'simple', status: Status.ACTIVE, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; userService @@ -144,6 +148,7 @@ describe('test TokenService', () => { lastName: 'Test2', origin: 'simple', status: Status.ACTIVE, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; users[0].inviteToken = ''; diff --git a/test/authorization/repository/entity.repository.test.ts b/test/authorization/repository/entity.repository.test.ts index 27dab75..1b167bf 100644 --- a/test/authorization/repository/entity.repository.test.ts +++ b/test/authorization/repository/entity.repository.test.ts @@ -8,6 +8,7 @@ const VALID_ENTITY_ID = 'ae032b1b-cc3c-4e44-9197-276ca877a7f8'; const entity: Entity = { id: VALID_ENTITY_ID, name: 'Test Entity 1', + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }; const updateResult: UpdateResult = { diff --git a/test/authorization/repository/group.repository.test.ts b/test/authorization/repository/group.repository.test.ts index f584df9..56cd70c 100644 --- a/test/authorization/repository/group.repository.test.ts +++ b/test/authorization/repository/group.repository.test.ts @@ -11,6 +11,7 @@ const VALID_GROUP_ID = 'ae032b1b-cc3c-4e44-9197-276ca877a7f8'; const group: Group = { id: VALID_GROUP_ID, name: 'Test Group 1', + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }; const updateResult: UpdateResult = { diff --git a/test/authorization/repository/groupPermission.repository.test.ts b/test/authorization/repository/groupPermission.repository.test.ts index 24430e2..03fc4b8 100644 --- a/test/authorization/repository/groupPermission.repository.test.ts +++ b/test/authorization/repository/groupPermission.repository.test.ts @@ -10,6 +10,7 @@ const groupPermissions: GroupPermission[] = [ { groupId: VALID_GROUP_ID, permissionId: VALID_PERMISSION_ID, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; diff --git a/test/authorization/repository/role.repository.test.ts b/test/authorization/repository/role.repository.test.ts index 48949fa..cbd168d 100644 --- a/test/authorization/repository/role.repository.test.ts +++ b/test/authorization/repository/role.repository.test.ts @@ -10,6 +10,7 @@ const VALID_ROLE_ID = 'ae032b1b-cc3c-4e44-9197-276ca877a7f8'; const role: Role = { id: VALID_ROLE_ID, name: 'Test Role 1', + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }; const updateResult: UpdateResult = { diff --git a/test/authorization/repository/rolePermission.repository.test.ts b/test/authorization/repository/rolePermission.repository.test.ts index 70ab8ae..95b6b32 100644 --- a/test/authorization/repository/rolePermission.repository.test.ts +++ b/test/authorization/repository/rolePermission.repository.test.ts @@ -15,10 +15,12 @@ describe('test RolePermission repository', () => { { permissionId: '2b33268a-7ff5-4cac-a87a-6bfc4430d34c', roleId: VALID_ROLE_ID, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, { permissionId: '09f7f119-c14b-4c37-ac1f-aae57d7bdbe5', roleId: VALID_ROLE_ID, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; diff --git a/test/authorization/repository/user.repository.test.ts b/test/authorization/repository/user.repository.test.ts index f0072e2..5f89e37 100644 --- a/test/authorization/repository/user.repository.test.ts +++ b/test/authorization/repository/user.repository.test.ts @@ -17,6 +17,7 @@ const users: User[] = [ origin: 'simple', status: Status.ACTIVE, email: VALID_EMAIL, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; diff --git a/test/authorization/repository/userGroup.repository.test.ts b/test/authorization/repository/userGroup.repository.test.ts index 815132b..156d102 100644 --- a/test/authorization/repository/userGroup.repository.test.ts +++ b/test/authorization/repository/userGroup.repository.test.ts @@ -10,6 +10,7 @@ const userGroups: UserGroup[] = [ { userId: VALID_USER_ID, groupId: VALID_GROUP_ID, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; diff --git a/test/authorization/repository/userPermission.repository.test.ts b/test/authorization/repository/userPermission.repository.test.ts index b9983d0..ede2a41 100644 --- a/test/authorization/repository/userPermission.repository.test.ts +++ b/test/authorization/repository/userPermission.repository.test.ts @@ -9,6 +9,7 @@ const userPermissions: UserPermission[] = [ { permissionId: VALID_PERMISSION_ID, userId: 'ccecef4f-58d3-477b-87ee-847ee22efe4d', + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; diff --git a/test/authorization/resolver/entity.resolver.test.ts b/test/authorization/resolver/entity.resolver.test.ts index ade13d8..5be7188 100644 --- a/test/authorization/resolver/entity.resolver.test.ts +++ b/test/authorization/resolver/entity.resolver.test.ts @@ -30,6 +30,7 @@ const users: User[] = [ lastName: 'Test2', origin: 'simple', status: Status.ACTIVE, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; @@ -45,6 +46,7 @@ const allEntities: Entity[] = [ id: '2b33268a-7ff5-4cac-a87a-6bfc4430d34c', name: 'Customers', permissions: permissions, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; @@ -52,6 +54,7 @@ const entities: Entity[] = [ { id: '2b33268a-7ff5-4cac-a87a-6bfc4430d34c', name: 'Customers', + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; @@ -98,7 +101,9 @@ describe('Entity Module', () => { return request(app.getHttpServer()) .post(gql) .set('Authorization', `Bearer ${token}`) - .send({ query: '{getEntities {id name permissions { id name} }}' }) + .send({ + query: '{getEntities {id name permissions { id name} tenantId}}', + }) .expect(200) .expect((res) => { expect(res.body.data.getEntities).toEqual(allEntities); @@ -114,7 +119,7 @@ describe('Entity Module', () => { .set('Authorization', `Bearer ${token}`) .send({ query: - '{getEntity(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8") {id name }}', + '{getEntity(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8") {id name tenantId}}', }) .expect(200) .expect((res) => { @@ -135,7 +140,7 @@ describe('Entity Module', () => { .set('Authorization', `Bearer ${token}`) .send({ query: - 'mutation { createEntity(input: {name: "Test1"}) {id name }}', + 'mutation { createEntity(input: {name: "Test1"}) {id name tenantId}}', }) .expect(200) .expect((res) => { @@ -159,7 +164,7 @@ describe('Entity Module', () => { .set('Authorization', `Bearer ${token}`) .send({ query: - 'mutation { updateEntity(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8", input: {name: "Test1"}) {id name }}', + 'mutation { updateEntity(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8", input: {name: "Test1"}) {id name tenantId}}', }) .expect(200) .expect((res) => { @@ -176,7 +181,7 @@ describe('Entity Module', () => { .set('Authorization', `Bearer ${token}`) .send({ query: - 'mutation { deleteEntity(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8") {id name }}', + 'mutation { deleteEntity(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8") {id name tenantId}}', }) .expect(200) .expect((res) => { diff --git a/test/authorization/resolver/group.resolver.test.ts b/test/authorization/resolver/group.resolver.test.ts index 9329ed5..5efd314 100644 --- a/test/authorization/resolver/group.resolver.test.ts +++ b/test/authorization/resolver/group.resolver.test.ts @@ -32,6 +32,7 @@ const users: User[] = [ lastName: 'Test2', origin: 'simple', status: GqlSchema.Status.ACTIVE, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; @@ -39,6 +40,7 @@ const groups: Group[] = [ { id: '2b33268a-7ff5-4cac-a87a-6bfc4430d34c', name: 'Customers', + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; @@ -91,6 +93,7 @@ describe('Group Module', () => { { id: 'f56bc83b-b163-4fa0-a685-c0fa0926614c', name: 'Test Group Role', + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; const allPermissions: Permission[] = [ @@ -113,6 +116,7 @@ describe('Group Module', () => { { id: 'f56bc83b-b163-4fa0-a685-c0fa0926614c', name: 'Test Group Role', + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ], permissions: [ @@ -131,6 +135,7 @@ describe('Group Module', () => { name: 'Create-Roles', }, ], + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ], }; @@ -150,7 +155,7 @@ describe('Group Module', () => { .set('Authorization', `Bearer ${token}`) .send({ query: - '{getGroups { totalCount results { id name permissions{id name} roles{ id name } allPermissions{ id name }}}}', + '{getGroups { totalCount results { id name permissions{id name} roles{ id name tenantId} allPermissions{ id name } tenantId}}}', }) .expect(200) .expect((res) => { @@ -163,6 +168,7 @@ describe('Group Module', () => { const group: Group = { id: '4a3c33a9-983e-44c0-ad22-bdc5a84c2c75', name: 'Customers', + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }; const permissions: Permission[] = [ { @@ -174,6 +180,7 @@ describe('Group Module', () => { { id: '9942109f-026b-4f2f-a26f-5ceb5f911ba6', name: 'Test Group Role', + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; const allPermissions: Permission[] = [ @@ -193,6 +200,7 @@ describe('Group Module', () => { { id: '9942109f-026b-4f2f-a26f-5ceb5f911ba6', name: 'Test Group Role', + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ], permissions: [ @@ -211,6 +219,7 @@ describe('Group Module', () => { name: 'Create-Roles', }, ], + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }; groupService.getGroupById(group.id).returns(Promise.resolve(group)); groupService @@ -227,7 +236,7 @@ describe('Group Module', () => { .set('Authorization', `Bearer ${token}`) .send({ query: - '{getGroup(id: "4a3c33a9-983e-44c0-ad22-bdc5a84c2c75") {id name permissions{ id name } roles{ id name } allPermissions{ id name }}}', + '{getGroup(id: "4a3c33a9-983e-44c0-ad22-bdc5a84c2c75") {id name permissions{ id name } roles{ id name tenantId} allPermissions{ id name } tenantId}}', }) .expect(200) .expect((res) => { @@ -249,7 +258,8 @@ describe('Group Module', () => { .post(gql) .set('Authorization', `Bearer ${token}`) .send({ - query: 'mutation { createGroup(input: {name: "Test1"}) {id name }}', + query: + 'mutation { createGroup(input: {name: "Test1"}) {id name tenantId}}', }) .expect(200) .expect((res) => { @@ -275,7 +285,7 @@ describe('Group Module', () => { .set('Authorization', `Bearer ${token}`) .send({ query: - 'mutation { updateGroup(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8", input: {name: "Test1"}) {id name }}', + 'mutation { updateGroup(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8", input: {name: "Test1"}) {id name tenantId}}', }) .expect(200) .expect((res) => { @@ -294,7 +304,7 @@ describe('Group Module', () => { .set('Authorization', `Bearer ${token}`) .send({ query: - 'mutation { deleteGroup(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8") {id name }}', + 'mutation { deleteGroup(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8") {id name tenantId}}', }) .expect(200) .expect((res) => { @@ -341,18 +351,21 @@ describe('Group Module', () => { lastName: 'Test2', origin: 'simple', status: GqlSchema.Status.ACTIVE, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; const groupInPayload: Group[] = [ { id: '836cccce-8ff6-40e9-9fc7-2dd5cba3f514', name: 'HR', + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; const roles: Role[] = [ { id: 'fcd858c6-26c5-462b-8c53-4b544830dca8', name: 'Customers', + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; const groups: GqlSchema.Group[] = [ @@ -360,6 +373,7 @@ describe('Group Module', () => { id: '836cccce-8ff6-40e9-9fc7-2dd5cba3f514', name: 'HR', roles: roles, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; const token = authenticationHelper.generateAccessToken(users[0]); @@ -375,7 +389,7 @@ describe('Group Module', () => { .set('Authorization', `Bearer ${token}`) .send({ query: - ' { getGroup(id: "836cccce-8ff6-40e9-9fc7-2dd5cba3f514") {id name roles{ id name }}}', + ' { getGroup(id: "836cccce-8ff6-40e9-9fc7-2dd5cba3f514") {id name tenantId roles{ id name tenantId }}}', }) .expect(200) .expect((res) => { diff --git a/test/authorization/resolver/permission.resolver.test.ts b/test/authorization/resolver/permission.resolver.test.ts index 0780f43..93d2a0e 100644 --- a/test/authorization/resolver/permission.resolver.test.ts +++ b/test/authorization/resolver/permission.resolver.test.ts @@ -28,6 +28,7 @@ const users: User[] = [ lastName: 'Test2', origin: 'simple', status: Status.ACTIVE, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; const permissions: Permission[] = [ diff --git a/test/authorization/resolver/role.resolver.test.ts b/test/authorization/resolver/role.resolver.test.ts index f90e884..65d1f40 100644 --- a/test/authorization/resolver/role.resolver.test.ts +++ b/test/authorization/resolver/role.resolver.test.ts @@ -31,6 +31,7 @@ const users: User[] = [ lastName: 'Test2', origin: 'simple', status: GqlSchema.Status.ACTIVE, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; @@ -38,6 +39,7 @@ const roles: Role[] = [ { id: '2b33268a-7ff5-4cac-a87a-6bfc4430d34c', name: 'Customers', + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; @@ -92,6 +94,7 @@ describe('Role Module', () => { name: 'Customers', }, ], + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ], }; @@ -106,7 +109,7 @@ describe('Role Module', () => { .set('Authorization', `Bearer ${token}`) .send({ query: - '{getRoles { totalCount results { id name permissions { id name }}}}', + '{getRoles { totalCount results { id name permissions { id name} tenantId}}}', }) .expect(200) .expect((res) => { @@ -125,7 +128,7 @@ describe('Role Module', () => { .set('Authorization', `Bearer ${token}`) .send({ query: - '{getRole(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8") {id name }}', + '{getRole(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8") {id name tenantId}}', }) .expect(200) .expect((res) => { @@ -147,7 +150,8 @@ describe('Role Module', () => { .post(gql) .set('Authorization', `Bearer ${token}`) .send({ - query: 'mutation { createRole(input: {name: "Test1"}) {id name }}', + query: + 'mutation { createRole(input: {name: "Test1"}) {id name tenantId}}', }) .expect(200) .expect((res) => { @@ -173,7 +177,7 @@ describe('Role Module', () => { .set('Authorization', `Bearer ${token}`) .send({ query: - 'mutation { updateRole(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8", input: {name: "Test1"}) {id name }}', + 'mutation { updateRole(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8", input: {name: "Test1"}) {id name tenantId}}', }) .expect(200) .expect((res) => { @@ -192,7 +196,7 @@ describe('Role Module', () => { .set('Authorization', `Bearer ${token}`) .send({ query: - 'mutation { deleteRole(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8") {id name }}', + 'mutation { deleteRole(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8") {id name tenantId}}', }) .expect(200) .expect((res) => { @@ -238,11 +242,13 @@ describe('Role Module', () => { lastName: 'Test2', origin: 'simple', status: GqlSchema.Status.ACTIVE, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; const roleInPayload: Role = { id: 'cf525c1d-e64d-462c-8cdc-e55eb9234b9e', name: 'Role1', + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }; const permissions: Permission[] = [ { @@ -259,6 +265,7 @@ describe('Role Module', () => { id: 'cf525c1d-e64d-462c-8cdc-e55eb9234b9e', name: 'Role1', permissions: permissions, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; const token = authenticationHelper.generateAccessToken(users[0]); @@ -273,7 +280,7 @@ describe('Role Module', () => { .set('Authorization', `Bearer ${token}`) .send({ query: - '{getRole(id: "cf525c1d-e64d-462c-8cdc-e55eb9234b9e") {id name permissions{ id name }}}', + '{getRole(id: "cf525c1d-e64d-462c-8cdc-e55eb9234b9e") {id name permissions{ id name } tenantId}}', }) .expect(200) .expect((res) => { @@ -291,6 +298,7 @@ describe('Role Module', () => { lastName: 'Test2', origin: 'simple', status: GqlSchema.Status.ACTIVE, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; const permissions: Permission[] = [ @@ -310,6 +318,7 @@ describe('Role Module', () => { id: '2b33268a-7ff5-4cac-a87a-6bfc4430d34c', name: 'Customers', permissions: permissions, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ], }; @@ -319,7 +328,7 @@ describe('Role Module', () => { .set('Authorization', `Bearer ${token}`) .send({ query: - '{getRoles { totalCount results { id name permissions{ id name }}}}', + '{getRoles { totalCount results { id name permissions{ id name } tenantId}}}', }) .expect(200) .expect((res) => { diff --git a/test/authorization/resolver/user.resolver.test.ts b/test/authorization/resolver/user.resolver.test.ts index be670cd..b4a81e9 100644 --- a/test/authorization/resolver/user.resolver.test.ts +++ b/test/authorization/resolver/user.resolver.test.ts @@ -28,6 +28,7 @@ const users: User[] = [ lastName: 'User', origin: 'simple', status: GqlSchema.Status.ACTIVE, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; @@ -57,6 +58,7 @@ const groups: Group[] = [ { id: '2b33268a-7ff5-4cac-a87a-6bfc4430d34c', name: 'Customers', + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; @@ -138,6 +140,7 @@ describe('User Module', () => { lastName: users[0].lastName, origin: 'simple', status: GqlSchema.Status.ACTIVE, + tenantId: users[0].tenantId, }; const finalResponse: GqlSchema.User = { @@ -148,6 +151,7 @@ describe('User Module', () => { lastName: users[0].lastName, permissions: permissions, status: GqlSchema.Status.ACTIVE, + tenantId: users[0].tenantId, }; const token = authenticationHelper.generateAccessToken(userInPayload); @@ -165,7 +169,7 @@ describe('User Module', () => { .set('Authorization', `Bearer ${token}`) .send({ query: - '{getUser(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f9") { id email phone firstName lastName status permissions { id name } }}', + '{getUser(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f9") { id email phone firstName lastName status permissions { id name } tenantId}}', }) .expect(200) .expect((res) => { diff --git a/test/authorization/service/group.service.test.ts b/test/authorization/service/group.service.test.ts index 280add0..63da8bc 100644 --- a/test/authorization/service/group.service.test.ts +++ b/test/authorization/service/group.service.test.ts @@ -32,6 +32,7 @@ const groups: Group[] = [ { id: VALID_GROUP_ID, name: 'Test1', + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; @@ -53,6 +54,7 @@ const users: User[] = [ lastName: 'Test2', origin: 'simple', status: Status.ACTIVE, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; diff --git a/test/authorization/service/role.service.test.ts b/test/authorization/service/role.service.test.ts index 2c63953..77f437f 100644 --- a/test/authorization/service/role.service.test.ts +++ b/test/authorization/service/role.service.test.ts @@ -31,6 +31,7 @@ const roles: Role[] = [ { id: VALID_ROLE_ID, name: 'Test1', + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; const permissions: Permission[] = [ @@ -199,6 +200,7 @@ describe('test Role Service', () => { expect(result).toEqual({ id: VALID_ROLE_ID, name: 'Test1', + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }); }); }); diff --git a/test/authorization/service/user.service.test.ts b/test/authorization/service/user.service.test.ts index 5899a49..95fa0d2 100644 --- a/test/authorization/service/user.service.test.ts +++ b/test/authorization/service/user.service.test.ts @@ -34,6 +34,7 @@ const users: User[] = [ lastName: 'Test2', origin: 'simple', status: Status.ACTIVE, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; @@ -49,6 +50,7 @@ const groups: Group[] = [ { id: '39d338b9-02bd-4971-a24e-b39a3f475580', name: 'Customers', + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; @@ -213,6 +215,7 @@ describe('test UserService', () => { lastName: 'Test2', origin: 'simple', status: Status.ACTIVE, + tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }; saveMock.mockResolvedValue(users[0]); From 1e4d149c7d959f601701c3f4e48c4960d76ea849 Mon Sep 17 00:00:00 2001 From: Bharath Date: Wed, 11 Dec 2024 17:17:05 +0530 Subject: [PATCH 03/23] FEAT: save tenantId in execution context --- package-lock.json | 24 +++- package.json | 2 + src/app.module.ts | 9 +- src/authentication/authentication.helper.ts | 3 +- .../service/password.auth.service.ts | 4 + .../entity/abstract.tenant.entity.ts | 4 +- src/authorization/resolver/entity.resolver.ts | 2 +- src/main.ts | 3 + src/middleware/executionId.middleware.ts | 10 ++ .../1733833844028-MultiTenantFeature.ts | 120 ------------------ src/util/execution.manager.ts | 11 ++ 11 files changed, 62 insertions(+), 130 deletions(-) create mode 100644 src/middleware/executionId.middleware.ts diff --git a/package-lock.json b/package-lock.json index f2fdcaf..2a8350a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4473,6 +4473,12 @@ "integrity": "sha512-2BaZbcYqJ8jQZoIYChf/sVSdKvQOw1pqesmdUKGgwb+TBfVbFFx5mBdfL6SRPs8FBa64xep9Yp5CQDHas5b/4Q==", "dev": true }, + "@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true + }, "@types/validator": { "version": "13.1.3", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.1.3.tgz", @@ -5207,6 +5213,11 @@ } } }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, "ws": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", @@ -11824,6 +11835,13 @@ "is-docker": "^2.0.0" } }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "optional": true + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -15342,9 +15360,9 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", - "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==" + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, "v8-compile-cache": { "version": "2.3.0", diff --git a/package.json b/package.json index 7664161..be14bc4 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "twilio": "^3.67.1", "typeorm": "^0.3.11", "typeorm-naming-strategies": "^2.0.0", + "uuid": "^8.3.2", "winston": "^3.3.3" }, "devDependencies": { @@ -86,6 +87,7 @@ "@types/speakeasy": "^2.0.6", "@types/supertest": "^2.0.10", "@types/totp-generator": "0.0.2", + "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^4.19.0", "@typescript-eslint/parser": "^4.19.0", "eslint": "^7.22.0", diff --git a/src/app.module.ts b/src/app.module.ts index cdae947..aadaf94 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { MiddlewareConsumer, Module } from '@nestjs/common'; import * as Joi from '@hapi/joi'; import { ConfigModule } from '@nestjs/config'; @@ -7,6 +7,7 @@ import { AppGraphQLModule } from './graphql/graphql.module'; import { UserAuthModule } from './authentication/authentication.module'; import { AuthorizationModule } from './authorization/authorization.module'; import { HealthModule } from './health/health.module'; +import { ExecutionContextBinder } from './middleware/executionId.middleware'; @Module({ imports: [ @@ -30,4 +31,8 @@ import { HealthModule } from './health/health.module'; controllers: [], providers: [], }) -export class AppModule {} +export class AppModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(ExecutionContextBinder).forRoutes('*'); + } +} diff --git a/src/authentication/authentication.helper.ts b/src/authentication/authentication.helper.ts index 7af8415..a3f1144 100644 --- a/src/authentication/authentication.helper.ts +++ b/src/authentication/authentication.helper.ts @@ -13,10 +13,9 @@ export class AuthenticationHelper { this.configService.get('JWT_TOKEN_EXPTIME') * 1 || 60 * 60; const secret = this.configService.get('JWT_SECRET') as string; const username = userDetails.email || userDetails.phone; - const tenantId = userDetails.tenantId; const dataStoredInToken = { username: username, - tenantId, + tenantId: userDetails.tenantId, sub: userDetails.id, env: this.configService.get('ENV') || 'local', }; diff --git a/src/authentication/service/password.auth.service.ts b/src/authentication/service/password.auth.service.ts index 6dd99da..bdc1764 100644 --- a/src/authentication/service/password.auth.service.ts +++ b/src/authentication/service/password.auth.service.ts @@ -25,6 +25,7 @@ import { } from '../exception/userauth.exception'; import { Authenticatable } from '../interfaces/authenticatable'; import { TokenService } from './token.service'; +import { ExecutionManager } from '../../util/execution.manager'; @Injectable() export default class PasswordAuthService implements Authenticatable { @@ -68,6 +69,7 @@ export default class PasswordAuthService implements Authenticatable { async inviteTokenSignup( userDetails: UserInviteTokenSignupInput, ): Promise { + const tenantId = ExecutionManager.getTenantId(); const verifyUser = await this.userService.verifyDuplicateUser( userDetails.email, userDetails.phone, @@ -85,6 +87,7 @@ export default class PasswordAuthService implements Authenticatable { userFromInput.middleName = userDetails.middleName; userFromInput.lastName = userDetails.lastName; userFromInput.status = Status.INVITED; + userFromInput.tenantId = tenantId; let invitationToken: { token: any; tokenExpiryTime?: any }; const transaction = await this.dataSource.manager.transaction(async () => { const savedUser = await this.userService.createUser(userFromInput); @@ -104,6 +107,7 @@ export default class PasswordAuthService implements Authenticatable { lastName: user.lastName, inviteToken: user?.inviteToken, status: user.status, + tenantId: user.tenantId, }; return { inviteToken: invitationToken.token, diff --git a/src/authorization/entity/abstract.tenant.entity.ts b/src/authorization/entity/abstract.tenant.entity.ts index fe4f89e..29625be 100644 --- a/src/authorization/entity/abstract.tenant.entity.ts +++ b/src/authorization/entity/abstract.tenant.entity.ts @@ -1,8 +1,8 @@ -import { PrimaryColumn } from 'typeorm'; +import { Column } from 'typeorm'; import BaseEntity from './base.entity'; class AbstractTenantEntity extends BaseEntity { - @PrimaryColumn({ type: 'uuid' }) + @Column({ type: 'uuid' }) public tenantId!: string; } diff --git a/src/authorization/resolver/entity.resolver.ts b/src/authorization/resolver/entity.resolver.ts index f465e7f..1e423aa 100644 --- a/src/authorization/resolver/entity.resolver.ts +++ b/src/authorization/resolver/entity.resolver.ts @@ -52,7 +52,7 @@ export class EntityResolver { async updateEntityPermissions( @Args('id', ParseUUIDPipe) id: string, @Args('input') entityInput: UpdateEntityPermissionInput, - ): Promise { + ): Promise { return this.entityService.updateEntityPermissions(id, entityInput); } diff --git a/src/main.ts b/src/main.ts index 6303653..5789d72 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,7 @@ import { ValidationPipe } from '@nestjs/common'; import { CustomExceptionsFilter } from './exception/exception.filter'; import { ConfigService } from '@nestjs/config'; import { LoggerService } from './logger/logger.service'; +import { ExecutionManager } from './util/execution.manager'; async function bootstrap() { const appOptions = { cors: true }; @@ -15,6 +16,8 @@ async function bootstrap() { app.useGlobalPipes(new ValidationPipe()); app.useGlobalFilters(new CustomExceptionsFilter()); + ExecutionManager.init(); + await app.listen(configService.get('PORT') || 4000); logger.info(`Application is running on: ${await app.getUrl()}`); diff --git a/src/middleware/executionId.middleware.ts b/src/middleware/executionId.middleware.ts new file mode 100644 index 0000000..354ce42 --- /dev/null +++ b/src/middleware/executionId.middleware.ts @@ -0,0 +1,10 @@ +import { NestMiddleware } from '@nestjs/common'; +import { NextFunction, Request, Response } from 'express'; + +import { ExecutionManager } from '../util/execution.manager'; + +export class ExecutionContextBinder implements NestMiddleware { + async use(req: Request, res: Response, next: NextFunction) { + ExecutionManager.runWithContext(next); + } +} diff --git a/src/migrations/1733833844028-MultiTenantFeature.ts b/src/migrations/1733833844028-MultiTenantFeature.ts index a894d56..09638a2 100644 --- a/src/migrations/1733833844028-MultiTenantFeature.ts +++ b/src/migrations/1733833844028-MultiTenantFeature.ts @@ -17,200 +17,80 @@ export class MultiTenantFeature1733833844028 implements MigrationInterface { await queryRunner.query( `ALTER TABLE "entity_model" ALTER COLUMN "tenant_id" DROP DEFAULT`, ); - await queryRunner.query( - `ALTER TABLE "entity_model" DROP CONSTRAINT "PK_ea7e5d0ca6a0d6221f78cea499a"`, - ); - await queryRunner.query( - `ALTER TABLE "entity_model" ADD CONSTRAINT "PK_8136db3bf8c7a328973a0feb096" PRIMARY KEY ("id", "tenant_id")`, - ); await queryRunner.query( `ALTER TABLE "entity_permission" ADD "tenant_id" uuid NOT NULL DEFAULT '${tenantId}'`, ); await queryRunner.query( `ALTER TABLE "entity_permission" ALTER COLUMN "tenant_id" DROP DEFAULT`, ); - await queryRunner.query( - `ALTER TABLE "entity_permission" DROP CONSTRAINT "PK_22d409e099ab8a6bc3fc7b7b8a1"`, - ); - await queryRunner.query( - `ALTER TABLE "entity_permission" ADD CONSTRAINT "PK_456866ab8087e8d3a3d616dbe0a" PRIMARY KEY ("permission_id", "entity_id", "tenant_id")`, - ); await queryRunner.query( `ALTER TABLE "group" ADD "tenant_id" uuid NOT NULL DEFAULT '${tenantId}'`, ); await queryRunner.query( `ALTER TABLE "group" ALTER COLUMN "tenant_id" DROP DEFAULT`, ); - await queryRunner.query( - `ALTER TABLE "group" DROP CONSTRAINT "PK_256aa0fda9b1de1a73ee0b7106b"`, - ); - await queryRunner.query( - `ALTER TABLE "group" ADD CONSTRAINT "PK_3ba4cfb3cab75ac2ea4b0a9536b" PRIMARY KEY ("id", "tenant_id")`, - ); await queryRunner.query( `ALTER TABLE "group_permission" ADD "tenant_id" uuid NOT NULL DEFAULT '${tenantId}'`, ); await queryRunner.query( `ALTER TABLE "group_permission" ALTER COLUMN "tenant_id" DROP DEFAULT`, ); - await queryRunner.query( - `ALTER TABLE "group_permission" DROP CONSTRAINT "PK_5aadf555f3ea93c95bc952f1547"`, - ); - await queryRunner.query( - `ALTER TABLE "group_permission" ADD CONSTRAINT "PK_97edbfb9e685755fa455480f98c" PRIMARY KEY ("permission_id", "group_id", "tenant_id")`, - ); await queryRunner.query( `ALTER TABLE "group_role" ADD "tenant_id" uuid NOT NULL DEFAULT '${tenantId}'`, ); await queryRunner.query( `ALTER TABLE "group_role" ALTER COLUMN "tenant_id" DROP DEFAULT`, ); - await queryRunner.query( - `ALTER TABLE "group_role" DROP CONSTRAINT "PK_34b9a049ae09a85e87e7f18787b"`, - ); - await queryRunner.query( - `ALTER TABLE "group_role" ADD CONSTRAINT "PK_3a8315bbdf7bfd962bf0e7a40e5" PRIMARY KEY ("role_id", "group_id", "tenant_id")`, - ); await queryRunner.query( `ALTER TABLE "role" ADD "tenant_id" uuid NOT NULL DEFAULT '${tenantId}'`, ); await queryRunner.query( `ALTER TABLE "role" ALTER COLUMN "tenant_id" DROP DEFAULT`, ); - await queryRunner.query( - `ALTER TABLE "role" DROP CONSTRAINT "PK_b36bcfe02fc8de3c57a8b2391c2"`, - ); - await queryRunner.query( - `ALTER TABLE "role" ADD CONSTRAINT "PK_2b394366739f89a92b09a90aea4" PRIMARY KEY ("id", "tenant_id")`, - ); await queryRunner.query( `ALTER TABLE "role_permission" ADD "tenant_id" uuid NOT NULL DEFAULT '${tenantId}'`, ); await queryRunner.query( `ALTER TABLE "role_permission" ALTER COLUMN "tenant_id" DROP DEFAULT`, ); - await queryRunner.query( - `ALTER TABLE "role_permission" DROP CONSTRAINT "PK_19a94c31d4960ded0dcd0397759"`, - ); - await queryRunner.query( - `ALTER TABLE "role_permission" ADD CONSTRAINT "PK_96cb2e2da566ba65ae7f24cece2" PRIMARY KEY ("permission_id", "role_id", "tenant_id")`, - ); await queryRunner.query( `ALTER TABLE "user" ADD "tenant_id" uuid NOT NULL DEFAULT '${tenantId}'`, ); await queryRunner.query( `ALTER TABLE "user" ALTER COLUMN "tenant_id" DROP DEFAULT`, ); - await queryRunner.query( - `ALTER TABLE "user" DROP CONSTRAINT "PK_cace4a159ff9f2512dd42373760"`, - ); - await queryRunner.query( - `ALTER TABLE "user" ADD CONSTRAINT "PK_9a56c21022d9a4dc056d9c37575" PRIMARY KEY ("id", "tenant_id")`, - ); await queryRunner.query( `ALTER TABLE "user_group" ADD "tenant_id" uuid NOT NULL DEFAULT '${tenantId}'`, ); await queryRunner.query( `ALTER TABLE "user_group" ALTER COLUMN "tenant_id" DROP DEFAULT`, ); - await queryRunner.query( - `ALTER TABLE "user_group" DROP CONSTRAINT "PK_bd332ba499e012f8d20905f8061"`, - ); - await queryRunner.query( - `ALTER TABLE "user_group" ADD CONSTRAINT "PK_fe9b93596a9c69d45a04226cc40" PRIMARY KEY ("group_id", "user_id", "tenant_id")`, - ); await queryRunner.query( `ALTER TABLE "user_permission" ADD "tenant_id" uuid NOT NULL DEFAULT '${tenantId}'`, ); await queryRunner.query( `ALTER TABLE "user_permission" ALTER COLUMN "tenant_id" DROP DEFAULT`, ); - await queryRunner.query( - `ALTER TABLE "user_permission" DROP CONSTRAINT "PK_e55fe6295b438912cb42bce1baa"`, - ); - await queryRunner.query( - `ALTER TABLE "user_permission" ADD CONSTRAINT "PK_f63ef18a89058a5d95c171b7823" PRIMARY KEY ("permission_id", "user_id", "tenant_id")`, - ); } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "user_permission" DROP CONSTRAINT "PK_f63ef18a89058a5d95c171b7823"`, - ); - await queryRunner.query( - `ALTER TABLE "user_permission" ADD CONSTRAINT "PK_e55fe6295b438912cb42bce1baa" PRIMARY KEY ("permission_id", "user_id")`, - ); await queryRunner.query( `ALTER TABLE "user_permission" DROP COLUMN "tenant_id"`, ); - await queryRunner.query( - `ALTER TABLE "user_group" DROP CONSTRAINT "PK_fe9b93596a9c69d45a04226cc40"`, - ); - await queryRunner.query( - `ALTER TABLE "user_group" ADD CONSTRAINT "PK_bd332ba499e012f8d20905f8061" PRIMARY KEY ("group_id", "user_id")`, - ); await queryRunner.query(`ALTER TABLE "user_group" DROP COLUMN "tenant_id"`); - await queryRunner.query( - `ALTER TABLE "user" DROP CONSTRAINT "PK_9a56c21022d9a4dc056d9c37575"`, - ); - await queryRunner.query( - `ALTER TABLE "user" ADD CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id")`, - ); await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "tenant_id"`); - await queryRunner.query( - `ALTER TABLE "role_permission" DROP CONSTRAINT "PK_96cb2e2da566ba65ae7f24cece2"`, - ); - await queryRunner.query( - `ALTER TABLE "role_permission" ADD CONSTRAINT "PK_19a94c31d4960ded0dcd0397759" PRIMARY KEY ("permission_id", "role_id")`, - ); await queryRunner.query( `ALTER TABLE "role_permission" DROP COLUMN "tenant_id"`, ); - await queryRunner.query( - `ALTER TABLE "role" DROP CONSTRAINT "PK_2b394366739f89a92b09a90aea4"`, - ); - await queryRunner.query( - `ALTER TABLE "role" ADD CONSTRAINT "PK_b36bcfe02fc8de3c57a8b2391c2" PRIMARY KEY ("id")`, - ); await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "tenant_id"`); - await queryRunner.query( - `ALTER TABLE "group_role" DROP CONSTRAINT "PK_3a8315bbdf7bfd962bf0e7a40e5"`, - ); - await queryRunner.query( - `ALTER TABLE "group_role" ADD CONSTRAINT "PK_34b9a049ae09a85e87e7f18787b" PRIMARY KEY ("role_id", "group_id")`, - ); await queryRunner.query(`ALTER TABLE "group_role" DROP COLUMN "tenant_id"`); - await queryRunner.query( - `ALTER TABLE "group_permission" DROP CONSTRAINT "PK_97edbfb9e685755fa455480f98c"`, - ); - await queryRunner.query( - `ALTER TABLE "group_permission" ADD CONSTRAINT "PK_5aadf555f3ea93c95bc952f1547" PRIMARY KEY ("permission_id", "group_id")`, - ); await queryRunner.query( `ALTER TABLE "group_permission" DROP COLUMN "tenant_id"`, ); - await queryRunner.query( - `ALTER TABLE "group" DROP CONSTRAINT "PK_3ba4cfb3cab75ac2ea4b0a9536b"`, - ); - await queryRunner.query( - `ALTER TABLE "group" ADD CONSTRAINT "PK_256aa0fda9b1de1a73ee0b7106b" PRIMARY KEY ("id")`, - ); await queryRunner.query(`ALTER TABLE "group" DROP COLUMN "tenant_id"`); - await queryRunner.query( - `ALTER TABLE "entity_permission" DROP CONSTRAINT "PK_456866ab8087e8d3a3d616dbe0a"`, - ); - await queryRunner.query( - `ALTER TABLE "entity_permission" ADD CONSTRAINT "PK_22d409e099ab8a6bc3fc7b7b8a1" PRIMARY KEY ("permission_id", "entity_id")`, - ); await queryRunner.query( `ALTER TABLE "entity_permission" DROP COLUMN "tenant_id"`, ); - await queryRunner.query( - `ALTER TABLE "entity_model" DROP CONSTRAINT "PK_8136db3bf8c7a328973a0feb096"`, - ); - await queryRunner.query( - `ALTER TABLE "entity_model" ADD CONSTRAINT "PK_ea7e5d0ca6a0d6221f78cea499a" PRIMARY KEY ("id")`, - ); await queryRunner.query( `ALTER TABLE "entity_model" DROP COLUMN "tenant_id"`, ); diff --git a/src/util/execution.manager.ts b/src/util/execution.manager.ts index 6b382ff..d92f6c7 100644 --- a/src/util/execution.manager.ts +++ b/src/util/execution.manager.ts @@ -1,3 +1,4 @@ +import { v4 as UUIDV4 } from 'uuid'; import { AsyncLocalStorage } from 'async_hooks'; export class ExecutionManager { @@ -7,6 +8,16 @@ export class ExecutionManager { ExecutionManager.asl = new AsyncLocalStorage(); } + static runWithContext(callback: (...args: any[]) => R): R { + return ExecutionManager.getExecutionContext().run( + new Map([ + ['id', UUIDV4()], + ['startTime', new Date().toISOString()], + ]), + () => callback.apply(this), + ); + } + private static getExecutionContext() { return ExecutionManager.asl; } From 2900db22dca63473f465aff6b2122906b0204b0a Mon Sep 17 00:00:00 2001 From: Sruthy M L Date: Wed, 11 Dec 2024 18:21:35 +0530 Subject: [PATCH 04/23] FEAT: add postgres rls for tenant isolation --- src/authentication/authentication.guard.ts | 2 - .../entity/abstract.tenant.entity.ts | 5 +- src/database/database.module.ts | 13 +++++ src/middleware/executionId.middleware.ts | 11 +++- .../1733833844028-MultiTenantFeature.ts | 55 +++++++++++++++++++ 5 files changed, 82 insertions(+), 4 deletions(-) diff --git a/src/authentication/authentication.guard.ts b/src/authentication/authentication.guard.ts index 0936864..3b0bda5 100644 --- a/src/authentication/authentication.guard.ts +++ b/src/authentication/authentication.guard.ts @@ -1,7 +1,6 @@ import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { GqlExecutionContext } from '@nestjs/graphql'; import { AuthenticationHelper } from './authentication.helper'; -import { ExecutionManager } from '../util/execution.manager'; @Injectable() export class AuthGuard implements CanActivate { @@ -14,7 +13,6 @@ export class AuthGuard implements CanActivate { if (token) { const reqAuthToken = token.split(' ')[1]; ctx.user = this.authenticationHelper.validateAuthToken(reqAuthToken); - ExecutionManager.setTenantId(ctx.user.tenantId); return true; } } diff --git a/src/authorization/entity/abstract.tenant.entity.ts b/src/authorization/entity/abstract.tenant.entity.ts index 29625be..ad07d1d 100644 --- a/src/authorization/entity/abstract.tenant.entity.ts +++ b/src/authorization/entity/abstract.tenant.entity.ts @@ -2,7 +2,10 @@ import { Column } from 'typeorm'; import BaseEntity from './base.entity'; class AbstractTenantEntity extends BaseEntity { - @Column({ type: 'uuid' }) + @Column({ + type: 'uuid', + default: () => "current_setting('app.current_tenant')", + }) public tenantId!: string; } diff --git a/src/database/database.module.ts b/src/database/database.module.ts index 153dfe2..34e6ef6 100644 --- a/src/database/database.module.ts +++ b/src/database/database.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; +import { ExecutionManager } from '../util/execution.manager'; @Module({ imports: [ @@ -22,6 +23,18 @@ import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; namingStrategy: new SnakeNamingStrategy(), synchronize: false, logging: true, + extra: { + poolMiddleware: async (query: string, params: any[]) => { + const tenantId = ExecutionManager.getTenantId(); + if (tenantId) { + return [ + `SELECT set_config('app.tenant_id', ${tenantId}, false) ${query}`, + params, + ]; + } + return [query, params]; + }, + }, }), }), ], diff --git a/src/middleware/executionId.middleware.ts b/src/middleware/executionId.middleware.ts index 354ce42..0250864 100644 --- a/src/middleware/executionId.middleware.ts +++ b/src/middleware/executionId.middleware.ts @@ -2,9 +2,18 @@ import { NestMiddleware } from '@nestjs/common'; import { NextFunction, Request, Response } from 'express'; import { ExecutionManager } from '../util/execution.manager'; +import { AuthenticationHelper } from '../authentication/authentication.helper'; export class ExecutionContextBinder implements NestMiddleware { + constructor(private readonly auth: AuthenticationHelper) {} async use(req: Request, res: Response, next: NextFunction) { - ExecutionManager.runWithContext(next); + ExecutionManager.runWithContext(async () => { + const token = req.headers.authorization?.split(' ')[1]; + if (token) { + const user = this.auth.validateAuthToken(token); + ExecutionManager.setTenantId(user.tenantId); + } + next(); + }); } } diff --git a/src/migrations/1733833844028-MultiTenantFeature.ts b/src/migrations/1733833844028-MultiTenantFeature.ts index 09638a2..4d34550 100644 --- a/src/migrations/1733833844028-MultiTenantFeature.ts +++ b/src/migrations/1733833844028-MultiTenantFeature.ts @@ -71,9 +71,64 @@ export class MultiTenantFeature1733833844028 implements MigrationInterface { await queryRunner.query( `ALTER TABLE "user_permission" ALTER COLUMN "tenant_id" DROP DEFAULT`, ); + await queryRunner.query(` + ALTER TABLE "user" ENABLE ROW LEVEL SECURITY; + ALTER TABLE "role" ENABLE ROW LEVEL SECURITY; + ALTER TABLE "group" ENABLE ROW LEVEL SECURITY; + ALTER TABLE "entity_model" ENABLE ROW LEVEL SECURITY; + ALTER TABLE "user_group" ENABLE ROW LEVEL SECURITY; + ALTER TABLE "user_permission" ENABLE ROW LEVEL SECURITY; + ALTER TABLE "group_role" ENABLE ROW LEVEL SECURITY; + ALTER TABLE "group_permission" ENABLE ROW LEVEL SECURITY; + ALTER TABLE "role_permission" ENABLE ROW LEVEL SECURITY; + ALTER TABLE "entity_permission" ENABLE ROW LEVEL SECURITY; + + CREATE POLICY tenant_isolation_policy ON "user" + USING (tenant_id = current_setting('app.tenant_id')::uuid); + CREATE POLICY tenant_isolation_policy ON "role" + USING (tenant_id = current_setting('app.tenant_id')::uuid); + CREATE POLICY tenant_isolation_policy ON "group" + USING (tenant_id = current_setting('app.tenant_id')::uuid); + CREATE POLICY tenant_isolation_policy ON "entity_model" + USING (tenant_id = current_setting('app.tenant_id')::uuid); + CREATE POLICY tenant_isolation_policy ON "user_group" + USING (tenant_id = current_setting('app.tenant_id')::uuid); + CREATE POLICY tenant_isolation_policy ON "user_permission" + USING (tenant_id = current_setting('app.tenant_id')::uuid); + CREATE POLICY tenant_isolation_policy ON "group_role" + USING (tenant_id = current_setting('app.tenant_id')::uuid); + CREATE POLICY tenant_isolation_policy ON "group_permission" + USING (tenant_id = current_setting('app.tenant_id')::uuid); + CREATE POLICY tenant_isolation_policy ON "role_permission" + USING (tenant_id = current_setting('app.tenant_id')::uuid); + CREATE POLICY tenant_isolation_policy ON "entity_permission" + USING (tenant_id = current_setting('app.tenant_id')::uuid); + `); } public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DROP POLICY tenant_isolation_policy ON "user"; + DROP POLICY tenant_isolation_policy ON "role"; + DROP POLICY tenant_isolation_policy ON "group"; + DROP POLICY tenant_isolation_policy ON "entity_model"; + DROP POLICY tenant_isolation_policy ON "user_group"; + DROP POLICY tenant_isolation_policy ON "user_permission"; + DROP POLICY tenant_isolation_policy ON "group_role"; + DROP POLICY tenant_isolation_policy ON "group_permission"; + DROP POLICY tenant_isolation_policy ON "role_permission"; + DROP POLICY tenant_isolation_policy ON "entity_permission"; + ALTER TABLE "user" DISABLE ROW LEVEL SECURITY; + ALTER TABLE "role" DISABLE ROW LEVEL SECURITY; + ALTER TABLE "group" DISABLE ROW LEVEL SECURITY; + ALTER TABLE "entity_model" DISABLE ROW LEVEL SECURITY; + ALTER TABLE "user_group" DISABLE ROW LEVEL SECURITY; + ALTER TABLE "user_permission" DISABLE ROW LEVEL SECURITY; + ALTER TABLE "group_role" DISABLE ROW LEVEL SECURITY; + ALTER TABLE "group_permission" DISABLE ROW LEVEL SECURITY; + ALTER TABLE "role_permission" DISABLE ROW LEVEL SECURITY; + ALTER TABLE "entity_permission" DISABLE ROW LEVEL SECURITY; + `); await queryRunner.query( `ALTER TABLE "user_permission" DROP COLUMN "tenant_id"`, ); From 67f613470184c625028bd2068c2c3b3928421785 Mon Sep 17 00:00:00 2001 From: Sruthy M L Date: Wed, 11 Dec 2024 18:41:39 +0530 Subject: [PATCH 05/23] FIX: make execution context binder injectable --- src/authentication/authentication.module.ts | 1 + src/middleware/executionId.middleware.ts | 22 ++++++++++++--------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/authentication/authentication.module.ts b/src/authentication/authentication.module.ts index 3deb67b..2ff6b16 100644 --- a/src/authentication/authentication.module.ts +++ b/src/authentication/authentication.module.ts @@ -77,5 +77,6 @@ const providers: Provider[] = [ ], providers, controllers: [GoogleAuthController], + exports: [AuthenticationHelper], }) export class UserAuthModule {} diff --git a/src/middleware/executionId.middleware.ts b/src/middleware/executionId.middleware.ts index 0250864..32578ca 100644 --- a/src/middleware/executionId.middleware.ts +++ b/src/middleware/executionId.middleware.ts @@ -1,19 +1,23 @@ -import { NestMiddleware } from '@nestjs/common'; -import { NextFunction, Request, Response } from 'express'; - -import { ExecutionManager } from '../util/execution.manager'; +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; import { AuthenticationHelper } from '../authentication/authentication.helper'; +import { ExecutionManager } from '../util/execution.manager'; +@Injectable() export class ExecutionContextBinder implements NestMiddleware { constructor(private readonly auth: AuthenticationHelper) {} async use(req: Request, res: Response, next: NextFunction) { ExecutionManager.runWithContext(async () => { - const token = req.headers.authorization?.split(' ')[1]; - if (token) { - const user = this.auth.validateAuthToken(token); - ExecutionManager.setTenantId(user.tenantId); + try { + const token = req.headers.authorization?.split(' ')[1]; + if (token) { + const user = this.auth.validateAuthToken(token); + ExecutionManager.setTenantId(user.tenantId); + } + next(); + } catch (error) { + next(error); } - next(); }); } } From 65513d851ed7569c6ad1acea7b289794157b8ab1 Mon Sep 17 00:00:00 2001 From: Sruthy M L Date: Thu, 12 Dec 2024 14:48:58 +0530 Subject: [PATCH 06/23] FEAT: use new database connection per tenant --- .../entity/abstract.tenant.entity.ts | 2 +- .../repository/base.repository.ts | 49 +++++++++++++ src/authorization/service/entity.service.ts | 5 +- src/database/database.module.ts | 13 ---- src/middleware/executionId.middleware.ts | 2 +- .../1733833844028-MultiTenantFeature.ts | 24 +++++++ src/util/database.connection.ts | 68 +++++++++++++++++++ 7 files changed, 146 insertions(+), 17 deletions(-) create mode 100644 src/util/database.connection.ts diff --git a/src/authorization/entity/abstract.tenant.entity.ts b/src/authorization/entity/abstract.tenant.entity.ts index ad07d1d..7525038 100644 --- a/src/authorization/entity/abstract.tenant.entity.ts +++ b/src/authorization/entity/abstract.tenant.entity.ts @@ -4,7 +4,7 @@ import BaseEntity from './base.entity'; class AbstractTenantEntity extends BaseEntity { @Column({ type: 'uuid', - default: () => "current_setting('app.current_tenant')", + default: () => "current_setting('app.tenant_id')::uuid", }) public tenantId!: string; } diff --git a/src/authorization/repository/base.repository.ts b/src/authorization/repository/base.repository.ts index acbab45..8efb138 100644 --- a/src/authorization/repository/base.repository.ts +++ b/src/authorization/repository/base.repository.ts @@ -1,7 +1,56 @@ import { DataSource, EntityTarget, ObjectLiteral, Repository } from 'typeorm'; +import { getConnection } from '../../util/database.connection'; export class BaseRepository extends Repository { + private entityClass: any; + constructor(entity: EntityTarget, dataSource: DataSource) { super(entity, dataSource.createEntityManager()); + this.entityClass = entity; + } + + protected async getDynamicRepository(): Promise> { + const connection = await getConnection(); + return connection.getRepository(this.entityClass.name); + } + + async find(options?: any): Promise { + const repository = await this.getDynamicRepository(); + return repository.find(options); + } + + async findOne(options: any): Promise { + const repository = await this.getDynamicRepository(); + return repository.findOne(options); + } + + async findOneOrFail(options: any): Promise { + const repository = await this.getDynamicRepository(); + return repository.findOneOrFail(options); + } + + async findOneBy(options: any): Promise { + const repository = await this.getDynamicRepository(); + return repository.findOneBy(options); + } + + async update(criteria: any, partialEntity: any): Promise { + const repository = await this.getDynamicRepository(); + return repository.update(criteria, partialEntity); + } + + async delete(criteria: any): Promise { + const repository = await this.getDynamicRepository(); + return repository.delete(criteria); + } + + async softDelete(criteria: any): Promise { + const repository = await this.getDynamicRepository(); + return repository.softDelete(criteria); + } + + async count(options?: any): Promise { + const repository = await this.getDynamicRepository(); + return repository.count(options); } } diff --git a/src/authorization/service/entity.service.ts b/src/authorization/service/entity.service.ts index b22e3bc..634d794 100644 --- a/src/authorization/service/entity.service.ts +++ b/src/authorization/service/entity.service.ts @@ -15,6 +15,7 @@ import { EntityPermissionRepository } from '../repository/entityPermission.repos import { PermissionRepository } from '../repository/permission.repository'; import { EntityServiceInterface } from './entity.service.interface'; import { ExecutionManager } from '../../util/execution.manager'; +import { getConnection } from '../../util/database.connection'; @Injectable() export class EntityService implements EntityServiceInterface { @@ -61,8 +62,8 @@ export class EntityService implements EntityServiceInterface { if (!existingEntity) { throw new EntityNotFoundException(id); } - - await this.dataSource.manager.transaction(async (entityManager) => { + const entityManager = (await getConnection()).manager; + await entityManager.transaction(async (entityManager) => { const entityRepo = entityManager.getRepository(EntityModel); const entityPermissionRepo = entityManager.getRepository( EntityPermission, diff --git a/src/database/database.module.ts b/src/database/database.module.ts index 34e6ef6..153dfe2 100644 --- a/src/database/database.module.ts +++ b/src/database/database.module.ts @@ -2,7 +2,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; -import { ExecutionManager } from '../util/execution.manager'; @Module({ imports: [ @@ -23,18 +22,6 @@ import { ExecutionManager } from '../util/execution.manager'; namingStrategy: new SnakeNamingStrategy(), synchronize: false, logging: true, - extra: { - poolMiddleware: async (query: string, params: any[]) => { - const tenantId = ExecutionManager.getTenantId(); - if (tenantId) { - return [ - `SELECT set_config('app.tenant_id', ${tenantId}, false) ${query}`, - params, - ]; - } - return [query, params]; - }, - }, }), }), ], diff --git a/src/middleware/executionId.middleware.ts b/src/middleware/executionId.middleware.ts index 32578ca..a3a4341 100644 --- a/src/middleware/executionId.middleware.ts +++ b/src/middleware/executionId.middleware.ts @@ -16,7 +16,7 @@ export class ExecutionContextBinder implements NestMiddleware { } next(); } catch (error) { - next(error); + next(); } }); } diff --git a/src/migrations/1733833844028-MultiTenantFeature.ts b/src/migrations/1733833844028-MultiTenantFeature.ts index 4d34550..c851f67 100644 --- a/src/migrations/1733833844028-MultiTenantFeature.ts +++ b/src/migrations/1733833844028-MultiTenantFeature.ts @@ -104,9 +104,33 @@ export class MultiTenantFeature1733833844028 implements MigrationInterface { CREATE POLICY tenant_isolation_policy ON "entity_permission" USING (tenant_id = current_setting('app.tenant_id')::uuid); `); + await queryRunner.query(` + ALTER TABLE "entity_permission" ALTER COLUMN "tenant_id" SET DEFAULT current_setting('app.tenant_id')::uuid; + ALTER TABLE "group" ALTER COLUMN "tenant_id" SET DEFAULT current_setting('app.tenant_id')::uuid; + ALTER TABLE "group_permission" ALTER COLUMN "tenant_id" SET DEFAULT current_setting('app.tenant_id')::uuid; + ALTER TABLE "entity_model" ALTER COLUMN "tenant_id" SET DEFAULT current_setting('app.tenant_id')::uuid; + ALTER TABLE "role" ALTER COLUMN "tenant_id" SET DEFAULT current_setting('app.tenant_id')::uuid; + ALTER TABLE "group_role" ALTER COLUMN "tenant_id" SET DEFAULT current_setting('app.tenant_id')::uuid; + ALTER TABLE "role_permission" ALTER COLUMN "tenant_id" SET DEFAULT current_setting('app.tenant_id')::uuid; + ALTER TABLE "user" ALTER COLUMN "tenant_id" SET DEFAULT current_setting('app.tenant_id')::uuid; + ALTER TABLE "user_group" ALTER COLUMN "tenant_id" SET DEFAULT current_setting('app.tenant_id')::uuid; + ALTER TABLE "user_permission" ALTER COLUMN "tenant_id" SET DEFAULT current_setting('app.tenant_id')::uuid; + `); } public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "user_permission" ALTER COLUMN "tenant_id" DROP DEFAULT; + ALTER TABLE "user_group" ALTER COLUMN "tenant_id" DROP DEFAULT; + ALTER TABLE "user" ALTER COLUMN "tenant_id" DROP DEFAULT; + ALTER TABLE "role_permission" ALTER COLUMN "tenant_id" DROP DEFAULT; + ALTER TABLE "group_role" ALTER COLUMN "tenant_id" DROP DEFAULT; + ALTER TABLE "role" ALTER COLUMN "tenant_id" DROP DEFAULT; + ALTER TABLE "entity_model" ALTER COLUMN "tenant_id" DROP DEFAULT; + ALTER TABLE "group_permission" ALTER COLUMN "tenant_id" DROP DEFAULT; + ALTER TABLE "group" ALTER COLUMN "tenant_id" DROP DEFAULT; + ALTER TABLE "entity_permission" ALTER COLUMN "tenant_id" DROP DEFAULT; + `); await queryRunner.query(` DROP POLICY tenant_isolation_policy ON "user"; DROP POLICY tenant_isolation_policy ON "role"; diff --git a/src/util/database.connection.ts b/src/util/database.connection.ts new file mode 100644 index 0000000..b84c94d --- /dev/null +++ b/src/util/database.connection.ts @@ -0,0 +1,68 @@ +import { getConnectionManager, DataSource } from 'typeorm'; +import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; +import { LoggerService } from '../logger/logger.service'; +import { ExecutionManager } from './execution.manager'; + +/** + * Get connection based on the logged in tenant + * @returns connection + */ + +export async function getConnection(): Promise { + const tenantName = ExecutionManager.getTenantId(); + return getConnectionForTenant(tenantName); +} + +export async function getConnectionForTenant( + tenantId: string, +): Promise { + if (getConnectionManager().has(tenantId)) { + const con = getConnectionManager().get(tenantId); + const existingConnection = await Promise.resolve( + con.isConnected ? con : con.connect(), + ); + await switchToTenant(tenantId, existingConnection); + + return existingConnection; + } + + const newConnection: DataSource = await new DataSource({ + name: tenantId, + type: 'postgres', + host: process.env.POSTGRES_HOST, + port: Number(process.env.POSTGRES_PORT), + username: process.env.POSTGRES_USER, + password: process.env.POSTGRES_PASSWORD, + database: process.env.POSTGRES_DB, + entities: [ + __dirname + '/../**/*.entity.ts', + __dirname + '/../**/*.entity.js', + ], + synchronize: false, + logging: ['error'], + namingStrategy: new SnakeNamingStrategy(), + extra: { max: process.env.POSTGRES_TENANT_MAX_CONNECTION_LIMIT }, + }).initialize(); + + await switchToTenant(tenantId, newConnection); + + return newConnection; +} + +const switchToTenant = async ( + tenantId: string, + connection: DataSource, +): Promise => { + try { + await connection.query(`select set_config('app.tenant_id', $1, false)`, [ + tenantId, + ]); + } catch (error) { + const logger = LoggerService.getInstance('bootstrap()'); + logger.error( + `Failed to switch to tenant: ${tenantId}, error: ${JSON.stringify( + error, + )}]`, + ); + } +}; From 7572a60b67835bc752dca5ec19d9ce93a578223b Mon Sep 17 00:00:00 2001 From: Bharath Date: Thu, 12 Dec 2024 19:25:20 +0530 Subject: [PATCH 07/23] FEAT: add tenant creation api --- src/authentication/authKey.guard.ts | 20 ++++++++++++++++ src/authorization/authorization.module.ts | 12 ++++++++++ src/authorization/entity/tenant.entity.ts | 6 +++-- .../exception/tenant.exception.ts | 7 ++++++ src/authorization/graphql/tenant.graphql | 14 +++++++++++ .../repository/tenant.repository.ts | 16 +++++++++++++ src/authorization/resolver/tenant.resolver.ts | 22 ++++++++++++++++++ .../service/tenant.service.interface.ts | 10 ++++++++ src/authorization/service/tenant.service.ts | 23 +++++++++++++++++++ .../1733833844028-MultiTenantFeature.ts | 4 ++-- src/schema/graphql.schema.ts | 12 ++++++++++ 11 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 src/authentication/authKey.guard.ts create mode 100644 src/authorization/exception/tenant.exception.ts create mode 100644 src/authorization/graphql/tenant.graphql create mode 100644 src/authorization/repository/tenant.repository.ts create mode 100644 src/authorization/resolver/tenant.resolver.ts create mode 100644 src/authorization/service/tenant.service.interface.ts create mode 100644 src/authorization/service/tenant.service.ts diff --git a/src/authentication/authKey.guard.ts b/src/authentication/authKey.guard.ts new file mode 100644 index 0000000..4ddbf9c --- /dev/null +++ b/src/authentication/authKey.guard.ts @@ -0,0 +1,20 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { GqlExecutionContext } from '@nestjs/graphql'; + +@Injectable() +export class AuthKeyGuard implements CanActivate { + constructor(private configService: ConfigService) {} + + canActivate(context: ExecutionContext): boolean { + const ctx = GqlExecutionContext.create(context).getContext(); + if (ctx) { + const authKeyInHeader = ctx.headers['x-api-key']; + if (authKeyInHeader) { + const secretKey = this.configService.get('AUTH_KEY') as string; + return secretKey === authKeyInHeader; + } + } + return false; + } +} diff --git a/src/authorization/authorization.module.ts b/src/authorization/authorization.module.ts index 99f89df..f8ab0bf 100644 --- a/src/authorization/authorization.module.ts +++ b/src/authorization/authorization.module.ts @@ -51,6 +51,11 @@ import { UserService } from './service/user.service'; import { UserServiceInterface } from './service/user.service.interface'; import { UserCacheService } from './service/usercache.service'; import { UserCacheServiceInterface } from './service/usercache.service.interface'; +import Tenant from './entity/tenant.entity'; +import { TenantResolver } from './resolver/tenant.resolver'; +import TenantService from './service/tenant.service'; +import { TenantRepository } from './repository/tenant.repository'; +import { TenantServiceInterface } from './service/tenant.service.interface'; @Module({ imports: [ @@ -66,6 +71,7 @@ import { UserCacheServiceInterface } from './service/usercache.service.interface Role, GroupRole, RolePermission, + Tenant, ]), RedisCacheModule, ], @@ -91,6 +97,8 @@ import { UserCacheServiceInterface } from './service/usercache.service.interface UserGroupRepository, EntityPermissionRepository, LoggerService, + TenantResolver, + TenantRepository, { provide: EntityServiceInterface, useClass: EntityService, @@ -127,6 +135,10 @@ import { UserCacheServiceInterface } from './service/usercache.service.interface provide: UserCacheServiceInterface, useClass: UserCacheService, }, + { + provide: TenantServiceInterface, + useClass: TenantService, + }, ], exports: [ { diff --git a/src/authorization/entity/tenant.entity.ts b/src/authorization/entity/tenant.entity.ts index be0b646..526f829 100644 --- a/src/authorization/entity/tenant.entity.ts +++ b/src/authorization/entity/tenant.entity.ts @@ -1,14 +1,16 @@ -import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; import BaseEntity from './base.entity'; @Entity() -@Index('tenant_name_unique_idx', { synchronize: false }) class Tenant extends BaseEntity { @PrimaryGeneratedColumn('uuid') public id!: string; @Column() public name!: string; + + @Column({ unique: true }) + public domain!: string; } export default Tenant; diff --git a/src/authorization/exception/tenant.exception.ts b/src/authorization/exception/tenant.exception.ts new file mode 100644 index 0000000..829924e --- /dev/null +++ b/src/authorization/exception/tenant.exception.ts @@ -0,0 +1,7 @@ +import { NotFoundException } from '@nestjs/common'; + +export class TenantNotFoundException extends NotFoundException { + constructor(tenantDomain: string) { + super(`Tenant with domain ${tenantDomain} not found`); + } +} diff --git a/src/authorization/graphql/tenant.graphql b/src/authorization/graphql/tenant.graphql new file mode 100644 index 0000000..a362b0f --- /dev/null +++ b/src/authorization/graphql/tenant.graphql @@ -0,0 +1,14 @@ +input NewTenantInput { + name: String! + domain: String! +} + +type Tenant { + id: ID! + name: String! + domain: String! +} + +type Mutation { + createTenant(input: NewTenantInput!): Tenant +} diff --git a/src/authorization/repository/tenant.repository.ts b/src/authorization/repository/tenant.repository.ts new file mode 100644 index 0000000..5f3a81c --- /dev/null +++ b/src/authorization/repository/tenant.repository.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; + +import { BaseRepository } from './base.repository'; +import Tenant from '../entity/tenant.entity'; + +@Injectable() +export class TenantRepository extends BaseRepository { + constructor(private dataSource: DataSource) { + super(Tenant, dataSource); + } + + async getTenantByDomain(domain: string) { + return this.findOneBy({ domain }); + } +} diff --git a/src/authorization/resolver/tenant.resolver.ts b/src/authorization/resolver/tenant.resolver.ts new file mode 100644 index 0000000..f29af9d --- /dev/null +++ b/src/authorization/resolver/tenant.resolver.ts @@ -0,0 +1,22 @@ +import { Args, Mutation, Resolver } from '@nestjs/graphql'; +import { NewTenantInput } from '../../schema/graphql.schema'; +import Tenant from '../entity/tenant.entity'; +import { Inject, UseGuards } from '@nestjs/common'; +import { AuthKeyGuard } from '../../authentication/authKey.guard'; +import { TenantServiceInterface } from '../service/tenant.service.interface'; + +@Resolver('Tenant') +export class TenantResolver { + constructor( + @Inject(TenantServiceInterface) + private tenantService: TenantServiceInterface, + ) {} + + @UseGuards(AuthKeyGuard) + @Mutation() + async createTenant( + @Args('input') tenantInput: NewTenantInput, + ): Promise { + return this.tenantService.createTenant(tenantInput); + } +} diff --git a/src/authorization/service/tenant.service.interface.ts b/src/authorization/service/tenant.service.interface.ts new file mode 100644 index 0000000..a3f83b0 --- /dev/null +++ b/src/authorization/service/tenant.service.interface.ts @@ -0,0 +1,10 @@ +import { NewTenantInput } from 'src/schema/graphql.schema'; +import Tenant from '../entity/tenant.entity'; + +export interface TenantServiceInterface { + getTenantByDomain(domain: string): Promise; + + createTenant(tenant: NewTenantInput): Promise; +} + +export const TenantServiceInterface = Symbol('TenantServiceInterface'); diff --git a/src/authorization/service/tenant.service.ts b/src/authorization/service/tenant.service.ts new file mode 100644 index 0000000..bd34559 --- /dev/null +++ b/src/authorization/service/tenant.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; + +import Tenant from '../entity/tenant.entity'; +import { TenantNotFoundException } from '../exception/tenant.exception'; +import { TenantRepository } from '../repository/tenant.repository'; +import { NewTenantInput } from '../../schema/graphql.schema'; + +@Injectable() +export default class TenantService { + constructor(private tenantRepository: TenantRepository) {} + + async getTenantByDomain(domain: string): Promise { + const tenant = await this.tenantRepository.getTenantByDomain(domain); + if (!tenant) { + throw new TenantNotFoundException(domain); + } + return tenant; + } + + async createTenant(tenant: NewTenantInput): Promise { + return this.tenantRepository.save(tenant); + } +} diff --git a/src/migrations/1733833844028-MultiTenantFeature.ts b/src/migrations/1733833844028-MultiTenantFeature.ts index c851f67..491b09a 100644 --- a/src/migrations/1733833844028-MultiTenantFeature.ts +++ b/src/migrations/1733833844028-MultiTenantFeature.ts @@ -5,10 +5,10 @@ export class MultiTenantFeature1733833844028 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( - `CREATE TABLE "tenant" ("deleted_at" TIMESTAMP, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, CONSTRAINT "PK_da8c6efd67bb301e810e56ac139" PRIMARY KEY ("id"))`, + `CREATE TABLE "tenant" ("deleted_at" TIMESTAMP, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "domain" character varying NOT NULL, CONSTRAINT "UQ_97b9c4dae58b30f5bd875f241ab" UNIQUE ("domain"), CONSTRAINT "PK_da8c6efd67bb301e810e56ac139" PRIMARY KEY ("id"))`, ); const tenants = await queryRunner.query( - `INSERT INTO "tenant" ("name") VALUES ('Default Tenant') RETURNING id`, + `INSERT INTO "tenant" ("name", "domain") VALUES ('Default Tenant', 'default.domain') RETURNING id`, ); const tenantId = tenants[0].id; await queryRunner.query( diff --git a/src/schema/graphql.schema.ts b/src/schema/graphql.schema.ts index cfc9b1c..96ca830 100644 --- a/src/schema/graphql.schema.ts +++ b/src/schema/graphql.schema.ts @@ -166,6 +166,11 @@ export interface RoleSearchCondition { name?: StringSearchCondition; } +export interface NewTenantInput { + name: string; + domain: string; +} + export interface UpdateUserInput { firstName?: string; middleName?: string; @@ -261,6 +266,7 @@ export interface IMutation { updateRole(id: string, input: UpdateRoleInput): Role | Promise; deleteRole(id: string): Role | Promise; updateRolePermissions(id: string, input: UpdateRolePermissionInput): Permission[] | Promise; + createTenant(input: NewTenantInput): Tenant | Promise; updateUser(id: string, input: UpdateUserInput): User | Promise; deleteUser(id: string): User | Promise; updateUserPermissions(id: string, input: UpdateUserPermissionInput): Permission[] | Promise; @@ -355,6 +361,12 @@ export interface RolePaginated extends Paginated { results?: Role[]; } +export interface Tenant { + id: string; + name: string; + domain: string; +} + export interface UserPaginated extends Paginated { totalCount?: number; results?: User[]; From 5e0b2a6385b5072066ad17c365f6908ec286121e Mon Sep 17 00:00:00 2001 From: Bharath Date: Thu, 12 Dec 2024 20:06:55 +0530 Subject: [PATCH 08/23] REFACTOR: remove tenantId in response --- .../service/password.auth.service.ts | 4 --- src/authorization/graphql/entity.graphql | 1 - src/authorization/graphql/group.graphql | 2 -- src/authorization/graphql/role.graphql | 1 - src/authorization/graphql/user.graphql | 2 -- src/schema/graphql.schema.ts | 6 ---- .../resolver/userauth.resolver.test.ts | 11 ++---- .../resolver/entity.resolver.test.ts | 26 +++++++------- .../resolver/group.resolver.test.ts | 36 ++++++++----------- .../resolver/role.resolver.test.ts | 33 ++++++++--------- .../resolver/user.resolver.test.ts | 3 +- 11 files changed, 47 insertions(+), 78 deletions(-) diff --git a/src/authentication/service/password.auth.service.ts b/src/authentication/service/password.auth.service.ts index bdc1764..6dd99da 100644 --- a/src/authentication/service/password.auth.service.ts +++ b/src/authentication/service/password.auth.service.ts @@ -25,7 +25,6 @@ import { } from '../exception/userauth.exception'; import { Authenticatable } from '../interfaces/authenticatable'; import { TokenService } from './token.service'; -import { ExecutionManager } from '../../util/execution.manager'; @Injectable() export default class PasswordAuthService implements Authenticatable { @@ -69,7 +68,6 @@ export default class PasswordAuthService implements Authenticatable { async inviteTokenSignup( userDetails: UserInviteTokenSignupInput, ): Promise { - const tenantId = ExecutionManager.getTenantId(); const verifyUser = await this.userService.verifyDuplicateUser( userDetails.email, userDetails.phone, @@ -87,7 +85,6 @@ export default class PasswordAuthService implements Authenticatable { userFromInput.middleName = userDetails.middleName; userFromInput.lastName = userDetails.lastName; userFromInput.status = Status.INVITED; - userFromInput.tenantId = tenantId; let invitationToken: { token: any; tokenExpiryTime?: any }; const transaction = await this.dataSource.manager.transaction(async () => { const savedUser = await this.userService.createUser(userFromInput); @@ -107,7 +104,6 @@ export default class PasswordAuthService implements Authenticatable { lastName: user.lastName, inviteToken: user?.inviteToken, status: user.status, - tenantId: user.tenantId, }; return { inviteToken: invitationToken.token, diff --git a/src/authorization/graphql/entity.graphql b/src/authorization/graphql/entity.graphql index 55e4405..6dd456e 100644 --- a/src/authorization/graphql/entity.graphql +++ b/src/authorization/graphql/entity.graphql @@ -1,7 +1,6 @@ type Entity { id: ID! name: String! - tenantId: String! permissions: [Permission] } diff --git a/src/authorization/graphql/group.graphql b/src/authorization/graphql/group.graphql index 36a6a17..a0aa248 100644 --- a/src/authorization/graphql/group.graphql +++ b/src/authorization/graphql/group.graphql @@ -1,7 +1,6 @@ type Group { id: ID! name: String! - tenantId: String! users: [User] roles: [Role] permissions: [Permission] @@ -28,7 +27,6 @@ input UpdateGroupRoleInput { type GroupRole { id: ID!, name: String! - tenantId: String! } input GroupInputFilter { diff --git a/src/authorization/graphql/role.graphql b/src/authorization/graphql/role.graphql index e90aa40..1243d89 100644 --- a/src/authorization/graphql/role.graphql +++ b/src/authorization/graphql/role.graphql @@ -1,7 +1,6 @@ type Role { id: ID! name: String! - tenantId: String! permissions: [Permission] } diff --git a/src/authorization/graphql/user.graphql b/src/authorization/graphql/user.graphql index 2a8d145..f9cb8f5 100644 --- a/src/authorization/graphql/user.graphql +++ b/src/authorization/graphql/user.graphql @@ -20,7 +20,6 @@ type User { groups: [Group] permissions: [Permission] inviteToken: String - tenantId: String! } enum OperationType { @@ -45,7 +44,6 @@ input UpdateUserGroupInput { type UserGroupResponse { id: ID!, name: String! - tenantId: String! } enum OperationType { diff --git a/src/schema/graphql.schema.ts b/src/schema/graphql.schema.ts index 96ca830..f73494d 100644 --- a/src/schema/graphql.schema.ts +++ b/src/schema/graphql.schema.ts @@ -297,7 +297,6 @@ export interface InviteTokenResponse { export interface Entity { id: string; name: string; - tenantId: string; permissions?: Permission[]; } @@ -325,7 +324,6 @@ export interface IQuery { export interface Group { id: string; name: string; - tenantId: string; users?: User[]; roles?: Role[]; permissions?: Permission[]; @@ -335,7 +333,6 @@ export interface Group { export interface GroupRole { id: string; name: string; - tenantId: string; } export interface GroupPaginated extends Paginated { @@ -352,7 +349,6 @@ export interface Permission { export interface Role { id: string; name: string; - tenantId: string; permissions?: Permission[]; } @@ -383,11 +379,9 @@ export interface User { groups?: Group[]; permissions?: Permission[]; inviteToken?: string; - tenantId: string; } export interface UserGroupResponse { id: string; name: string; - tenantId: string; } diff --git a/test/authentication/resolver/userauth.resolver.test.ts b/test/authentication/resolver/userauth.resolver.test.ts index a39f83d..43f3bb0 100644 --- a/test/authentication/resolver/userauth.resolver.test.ts +++ b/test/authentication/resolver/userauth.resolver.test.ts @@ -95,7 +95,6 @@ describe('Userauth Module', () => { firstName: users[0].firstName, lastName: users[0].lastName, status: users[0].status, - tenantId: users[0].tenantId, }; const tokenResponse = { accessToken: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. @@ -116,7 +115,7 @@ describe('Userauth Module', () => { .post(gql) .send({ query: - 'mutation { passwordLogin(input: { username: "user@test.com" password: "s3cr3t1234567890" }) { accessToken, refreshToken, user{ id, email, phone, firstName, lastName, status tenantId} }}', + 'mutation { passwordLogin(input: { username: "user@test.com" password: "s3cr3t1234567890" }) { accessToken, refreshToken, user{ id, email, phone, firstName, lastName, status}}}', }) .expect(200) .expect((res) => { @@ -186,7 +185,6 @@ describe('Userauth Module', () => { inviteToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Inh5ekBrZXl2YWx1ZS5zeXN0ZW1zIiwiaWF0IjoxNjIxNTI1NTE1LCJleHAiOjE2MjE1MjkxMTV9.t8z7rBZKkog-1jirScYU6HE7KVTzatKWjZw8lVz3xLo', status: Status.INVITED, - tenantId: users[0].tenantId, }, }; configService.get('JWT_SECRET').returns('s3cr3t1234567890'); @@ -202,7 +200,7 @@ describe('Userauth Module', () => { .set('Authorization', `Bearer ${token}`) .send({ query: `mutation { inviteTokenSignup(input: { email: "test@gmail.com" - phone: "9947849200" firstName: "Test" lastName: "Name" }) { inviteToken tokenExpiryTime user{id firstName lastName inviteToken status tenantId}}}`, + phone: "9947849200" firstName: "Test" lastName: "Name" }) { inviteToken tokenExpiryTime user{id firstName lastName inviteToken status}}}`, }) .expect(200) .expect((res) => { @@ -286,7 +284,6 @@ describe('Userauth Module', () => { inviteToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Inh5ekBrZXl2YWx1ZS5zeXN0ZW1zIiwiaWF0IjoxNjIxNTI1NTE1LCJleHAiOjE2MjE1MjkxMTV9.t8z7rBZKkog-1jirScYU6HE7KVTzatKWjZw8lVz3xLo', status: Status.INVITED, - tenantId: users[0].tenantId, }, }; tokenService @@ -307,7 +304,6 @@ describe('Userauth Module', () => { lastName inviteToken status - tenantId } } }`, @@ -345,7 +341,6 @@ describe('Userauth Module', () => { firstName: users[0].firstName, lastName: users[0].lastName, status: users[0].status, - tenantId: users[0].tenantId, }; const tokenResponse: TokenResponse = { ...token, user: user }; tokenService @@ -355,7 +350,7 @@ describe('Userauth Module', () => { return request(app.getHttpServer()) .post(gql) .send({ - query: `mutation { refresh(input: { refreshToken: "${token.refreshToken}"}) { accessToken refreshToken user { id, email, phone, firstName, lastName, status tenantId} }}`, + query: `mutation { refresh(input: { refreshToken: "${token.refreshToken}"}) { accessToken refreshToken user { id, email, phone, firstName, lastName, status}}}`, }) .expect(200) .expect((res) => { diff --git a/test/authorization/resolver/entity.resolver.test.ts b/test/authorization/resolver/entity.resolver.test.ts index 5be7188..1f04675 100644 --- a/test/authorization/resolver/entity.resolver.test.ts +++ b/test/authorization/resolver/entity.resolver.test.ts @@ -17,6 +17,7 @@ import { UpdateEntityPermissionInput, } from '../../../src/schema/graphql.schema'; import { mockedConfigService } from '../../utils/mocks/config.service'; +import EntityModel from '../../../src/authorization/entity/entity.entity'; const gql = '/graphql'; @@ -46,7 +47,6 @@ const allEntities: Entity[] = [ id: '2b33268a-7ff5-4cac-a87a-6bfc4430d34c', name: 'Customers', permissions: permissions, - tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; @@ -54,7 +54,6 @@ const entities: Entity[] = [ { id: '2b33268a-7ff5-4cac-a87a-6bfc4430d34c', name: 'Customers', - tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; @@ -94,7 +93,9 @@ describe('Entity Module', () => { describe(gql, () => { describe('entities', () => { it('should get the entities', () => { - entityService.getAllEntities().returns(Promise.resolve(entities)); + entityService + .getAllEntities() + .returns(Promise.resolve(entities as EntityModel[])); entityService .getEntityPermissions('2b33268a-7ff5-4cac-a87a-6bfc4430d34c') .returns(Promise.resolve(permissions)); @@ -102,7 +103,7 @@ describe('Entity Module', () => { .post(gql) .set('Authorization', `Bearer ${token}`) .send({ - query: '{getEntities {id name permissions { id name} tenantId}}', + query: '{getEntities {id name permissions { id name}}}', }) .expect(200) .expect((res) => { @@ -113,13 +114,13 @@ describe('Entity Module', () => { it('should get single entity', () => { entityService .getEntityById('ae032b1b-cc3c-4e44-9197-276ca877a7f8') - .returns(Promise.resolve(entities[0])); + .returns(Promise.resolve(entities[0] as EntityModel)); return request(app.getHttpServer()) .post(gql) .set('Authorization', `Bearer ${token}`) .send({ query: - '{getEntity(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8") {id name tenantId}}', + '{getEntity(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8") {id name}}', }) .expect(200) .expect((res) => { @@ -134,13 +135,12 @@ describe('Entity Module', () => { const obj = Object.create(null); entityService .createEntity(Object.assign(obj, input)) - .returns(Promise.resolve(entities[0])); + .returns(Promise.resolve(entities[0] as EntityModel)); return request(app.getHttpServer()) .post(gql) .set('Authorization', `Bearer ${token}`) .send({ - query: - 'mutation { createEntity(input: {name: "Test1"}) {id name tenantId}}', + query: 'mutation { createEntity(input: {name: "Test1"}) {id name}}', }) .expect(200) .expect((res) => { @@ -158,13 +158,13 @@ describe('Entity Module', () => { 'ae032b1b-cc3c-4e44-9197-276ca877a7f8', Object.assign(obj, input), ) - .returns(Promise.resolve(entities[0])); + .returns(Promise.resolve(entities[0] as EntityModel)); return request(app.getHttpServer()) .post(gql) .set('Authorization', `Bearer ${token}`) .send({ query: - 'mutation { updateEntity(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8", input: {name: "Test1"}) {id name tenantId}}', + 'mutation { updateEntity(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8", input: {name: "Test1"}) {id name}}', }) .expect(200) .expect((res) => { @@ -175,13 +175,13 @@ describe('Entity Module', () => { it('should delete a entity', () => { entityService .deleteEntity('ae032b1b-cc3c-4e44-9197-276ca877a7f8') - .returns(Promise.resolve(entities[0])); + .returns(Promise.resolve(entities[0] as EntityModel)); return request(app.getHttpServer()) .post(gql) .set('Authorization', `Bearer ${token}`) .send({ query: - 'mutation { deleteEntity(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8") {id name tenantId}}', + 'mutation { deleteEntity(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8") {id name}}', }) .expect(200) .expect((res) => { diff --git a/test/authorization/resolver/group.resolver.test.ts b/test/authorization/resolver/group.resolver.test.ts index 5efd314..116f03c 100644 --- a/test/authorization/resolver/group.resolver.test.ts +++ b/test/authorization/resolver/group.resolver.test.ts @@ -36,11 +36,10 @@ const users: User[] = [ }, ]; -const groups: Group[] = [ +const groups = [ { id: '2b33268a-7ff5-4cac-a87a-6bfc4430d34c', name: 'Customers', - tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; @@ -116,7 +115,6 @@ describe('Group Module', () => { { id: 'f56bc83b-b163-4fa0-a685-c0fa0926614c', name: 'Test Group Role', - tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ], permissions: [ @@ -135,7 +133,6 @@ describe('Group Module', () => { name: 'Create-Roles', }, ], - tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ], }; @@ -149,13 +146,15 @@ describe('Group Module', () => { groupService .getAllGroupPermissions(groups[0].id) .returns(Promise.resolve(allPermissions)); - groupService.getAllGroups().returns(Promise.resolve([groups, 1])); + groupService + .getAllGroups() + .returns(Promise.resolve([groups as Group[], 1])); return request(app.getHttpServer()) .post(gql) .set('Authorization', `Bearer ${token}`) .send({ query: - '{getGroups { totalCount results { id name permissions{id name} roles{ id name tenantId} allPermissions{ id name } tenantId}}}', + '{getGroups { totalCount results { id name permissions{id name} roles{ id name } allPermissions{ id name }}}}', }) .expect(200) .expect((res) => { @@ -200,7 +199,6 @@ describe('Group Module', () => { { id: '9942109f-026b-4f2f-a26f-5ceb5f911ba6', name: 'Test Group Role', - tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ], permissions: [ @@ -219,7 +217,6 @@ describe('Group Module', () => { name: 'Create-Roles', }, ], - tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }; groupService.getGroupById(group.id).returns(Promise.resolve(group)); groupService @@ -236,7 +233,7 @@ describe('Group Module', () => { .set('Authorization', `Bearer ${token}`) .send({ query: - '{getGroup(id: "4a3c33a9-983e-44c0-ad22-bdc5a84c2c75") {id name permissions{ id name } roles{ id name tenantId} allPermissions{ id name } tenantId}}', + '{getGroup(id: "4a3c33a9-983e-44c0-ad22-bdc5a84c2c75") {id name permissions{ id name } roles{ id name } allPermissions{ id name }}}', }) .expect(200) .expect((res) => { @@ -253,13 +250,12 @@ describe('Group Module', () => { groupService .createGroup(Object.assign(obj, input)) - .returns(Promise.resolve(groups[0])); + .returns(Promise.resolve(groups[0] as Group)); return request(app.getHttpServer()) .post(gql) .set('Authorization', `Bearer ${token}`) .send({ - query: - 'mutation { createGroup(input: {name: "Test1"}) {id name tenantId}}', + query: 'mutation { createGroup(input: {name: "Test1"}) {id name}}', }) .expect(200) .expect((res) => { @@ -279,13 +275,13 @@ describe('Group Module', () => { 'ae032b1b-cc3c-4e44-9197-276ca877a7f8', Object.assign(obj, input), ) - .returns(Promise.resolve(groups[0])); + .returns(Promise.resolve(groups[0] as Group)); return request(app.getHttpServer()) .post(gql) .set('Authorization', `Bearer ${token}`) .send({ query: - 'mutation { updateGroup(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8", input: {name: "Test1"}) {id name tenantId}}', + 'mutation { updateGroup(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8", input: {name: "Test1"}) {id name}}', }) .expect(200) .expect((res) => { @@ -298,13 +294,13 @@ describe('Group Module', () => { groupService .deleteGroup('ae032b1b-cc3c-4e44-9197-276ca877a7f8') - .returns(Promise.resolve(groups[0])); + .returns(Promise.resolve(groups[0] as Group)); return request(app.getHttpServer()) .post(gql) .set('Authorization', `Bearer ${token}`) .send({ query: - 'mutation { deleteGroup(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8") {id name tenantId}}', + 'mutation { deleteGroup(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8") {id name}}', }) .expect(200) .expect((res) => { @@ -361,11 +357,10 @@ describe('Group Module', () => { tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; - const roles: Role[] = [ + const roles = [ { id: 'fcd858c6-26c5-462b-8c53-4b544830dca8', name: 'Customers', - tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; const groups: GqlSchema.Group[] = [ @@ -373,7 +368,6 @@ describe('Group Module', () => { id: '836cccce-8ff6-40e9-9fc7-2dd5cba3f514', name: 'HR', roles: roles, - tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; const token = authenticationHelper.generateAccessToken(users[0]); @@ -383,13 +377,13 @@ describe('Group Module', () => { .returns(Promise.resolve(groupInPayload[0])); groupService .getGroupRoles('836cccce-8ff6-40e9-9fc7-2dd5cba3f514') - .returns(Promise.resolve(roles)); + .returns(Promise.resolve(roles as Role[])); return request(app.getHttpServer()) .post(gql) .set('Authorization', `Bearer ${token}`) .send({ query: - ' { getGroup(id: "836cccce-8ff6-40e9-9fc7-2dd5cba3f514") {id name tenantId roles{ id name tenantId }}}', + ' { getGroup(id: "836cccce-8ff6-40e9-9fc7-2dd5cba3f514") {id name roles{ id name }}}', }) .expect(200) .expect((res) => { diff --git a/test/authorization/resolver/role.resolver.test.ts b/test/authorization/resolver/role.resolver.test.ts index 65d1f40..27488bd 100644 --- a/test/authorization/resolver/role.resolver.test.ts +++ b/test/authorization/resolver/role.resolver.test.ts @@ -35,11 +35,10 @@ const users: User[] = [ }, ]; -const roles: Role[] = [ +const roles = [ { id: '2b33268a-7ff5-4cac-a87a-6bfc4430d34c', name: 'Customers', - tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; @@ -94,13 +93,14 @@ describe('Role Module', () => { name: 'Customers', }, ], - tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ], }; const token = authenticationHelper.generateAccessToken(users[0]); - roleService.getAllRoles().returns(Promise.resolve([roles, 1])); + roleService + .getAllRoles() + .returns(Promise.resolve([roles as Role[], 1])); roleService .getRolePermissions(roles[0].id) .returns(Promise.resolve(permissions)); @@ -109,7 +109,7 @@ describe('Role Module', () => { .set('Authorization', `Bearer ${token}`) .send({ query: - '{getRoles { totalCount results { id name permissions { id name} tenantId}}}', + '{getRoles { totalCount results { id name permissions { id name}}}}', }) .expect(200) .expect((res) => { @@ -122,13 +122,13 @@ describe('Role Module', () => { roleService .getRoleById('ae032b1b-cc3c-4e44-9197-276ca877a7f8') - .returns(Promise.resolve(roles[0])); + .returns(Promise.resolve(roles[0] as Role)); return request(app.getHttpServer()) .post(gql) .set('Authorization', `Bearer ${token}`) .send({ query: - '{getRole(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8") {id name tenantId}}', + '{getRole(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8") {id name}}', }) .expect(200) .expect((res) => { @@ -145,13 +145,12 @@ describe('Role Module', () => { roleService .createRole(Object.assign(obj, input)) - .returns(Promise.resolve(roles[0])); + .returns(Promise.resolve(roles[0] as Role)); return request(app.getHttpServer()) .post(gql) .set('Authorization', `Bearer ${token}`) .send({ - query: - 'mutation { createRole(input: {name: "Test1"}) {id name tenantId}}', + query: 'mutation { createRole(input: {name: "Test1"}) {id name}}', }) .expect(200) .expect((res) => { @@ -171,13 +170,13 @@ describe('Role Module', () => { 'ae032b1b-cc3c-4e44-9197-276ca877a7f8', Object.assign(obj, input), ) - .returns(Promise.resolve(roles[0])); + .returns(Promise.resolve(roles[0] as Role)); return request(app.getHttpServer()) .post(gql) .set('Authorization', `Bearer ${token}`) .send({ query: - 'mutation { updateRole(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8", input: {name: "Test1"}) {id name tenantId}}', + 'mutation { updateRole(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8", input: {name: "Test1"}) {id name}}', }) .expect(200) .expect((res) => { @@ -190,13 +189,13 @@ describe('Role Module', () => { roleService .deleteRole('ae032b1b-cc3c-4e44-9197-276ca877a7f8') - .returns(Promise.resolve(roles[0])); + .returns(Promise.resolve(roles[0] as Role)); return request(app.getHttpServer()) .post(gql) .set('Authorization', `Bearer ${token}`) .send({ query: - 'mutation { deleteRole(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8") {id name tenantId}}', + 'mutation { deleteRole(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f8") {id name}}', }) .expect(200) .expect((res) => { @@ -265,7 +264,6 @@ describe('Role Module', () => { id: 'cf525c1d-e64d-462c-8cdc-e55eb9234b9e', name: 'Role1', permissions: permissions, - tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ]; const token = authenticationHelper.generateAccessToken(users[0]); @@ -280,7 +278,7 @@ describe('Role Module', () => { .set('Authorization', `Bearer ${token}`) .send({ query: - '{getRole(id: "cf525c1d-e64d-462c-8cdc-e55eb9234b9e") {id name permissions{ id name } tenantId}}', + '{getRole(id: "cf525c1d-e64d-462c-8cdc-e55eb9234b9e") {id name permissions{ id name }}}', }) .expect(200) .expect((res) => { @@ -318,7 +316,6 @@ describe('Role Module', () => { id: '2b33268a-7ff5-4cac-a87a-6bfc4430d34c', name: 'Customers', permissions: permissions, - tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }, ], }; @@ -328,7 +325,7 @@ describe('Role Module', () => { .set('Authorization', `Bearer ${token}`) .send({ query: - '{getRoles { totalCount results { id name permissions{ id name } tenantId}}}', + '{getRoles { totalCount results { id name permissions{ id name }}}}', }) .expect(200) .expect((res) => { diff --git a/test/authorization/resolver/user.resolver.test.ts b/test/authorization/resolver/user.resolver.test.ts index b4a81e9..4dbafe5 100644 --- a/test/authorization/resolver/user.resolver.test.ts +++ b/test/authorization/resolver/user.resolver.test.ts @@ -151,7 +151,6 @@ describe('User Module', () => { lastName: users[0].lastName, permissions: permissions, status: GqlSchema.Status.ACTIVE, - tenantId: users[0].tenantId, }; const token = authenticationHelper.generateAccessToken(userInPayload); @@ -169,7 +168,7 @@ describe('User Module', () => { .set('Authorization', `Bearer ${token}`) .send({ query: - '{getUser(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f9") { id email phone firstName lastName status permissions { id name } tenantId}}', + '{getUser(id: "ae032b1b-cc3c-4e44-9197-276ca877a7f9") { id email phone firstName lastName status permissions { id name }}}', }) .expect(200) .expect((res) => { From 9d10fea6597c3d1879a392c9ccd73c7010402a75 Mon Sep 17 00:00:00 2001 From: Sruthy M L Date: Thu, 12 Dec 2024 20:56:41 +0530 Subject: [PATCH 09/23] FEAT: modified usages of db queries to use tenant specific connection --- .../service/password.auth.service.ts | 54 ++++++++++--------- .../repository/base.repository.ts | 24 ++++++++- .../repository/group.repository.ts | 2 +- .../repository/groupRole.repository.ts | 2 +- .../repository/permission.repository.ts | 10 ++-- .../repository/role.repository.ts | 2 +- .../repository/user.repository.ts | 6 +-- src/authorization/service/entity.service.ts | 5 +- src/authorization/service/group.service.ts | 11 ++-- src/authorization/service/role.service.ts | 7 +-- src/authorization/service/user.service.ts | 13 ++--- .../repository/group.repository.test.ts | 2 +- .../repository/groupRole.repository.test.ts | 2 +- .../repository/permission.repository.test.ts | 2 +- .../repository/role.repository.test.ts | 2 +- .../repository/user.repository.test.ts | 2 +- .../service/group.service.test.ts | 2 +- .../service/role.service.test.ts | 2 +- .../service/user.service.test.ts | 2 +- 19 files changed, 91 insertions(+), 61 deletions(-) diff --git a/src/authentication/service/password.auth.service.ts b/src/authentication/service/password.auth.service.ts index 6dd99da..89eddbc 100644 --- a/src/authentication/service/password.auth.service.ts +++ b/src/authentication/service/password.auth.service.ts @@ -25,6 +25,7 @@ import { } from '../exception/userauth.exception'; import { Authenticatable } from '../interfaces/authenticatable'; import { TokenService } from './token.service'; +import { getConnection } from '../../util/database.connection'; @Injectable() export default class PasswordAuthService implements Authenticatable { @@ -86,31 +87,34 @@ export default class PasswordAuthService implements Authenticatable { userFromInput.lastName = userDetails.lastName; userFromInput.status = Status.INVITED; let invitationToken: { token: any; tokenExpiryTime?: any }; - const transaction = await this.dataSource.manager.transaction(async () => { - const savedUser = await this.userService.createUser(userFromInput); - invitationToken = this.authenticationHelper.generateInvitationToken( - { id: savedUser.id }, - this.configService.get('INVITATION_TOKEN_EXPTIME'), - ); - await this.userService.updateField( - savedUser.id, - 'inviteToken', - invitationToken.token, - ); - const user = await this.userService.getUserById(savedUser.id); - const userResponse = { - id: user.id, - firstName: user.firstName, - lastName: user.lastName, - inviteToken: user?.inviteToken, - status: user.status, - }; - return { - inviteToken: invitationToken.token, - tokenExpiryTime: invitationToken.tokenExpiryTime, - user: userResponse, - }; - }); + const transaction = await (await getConnection()).manager.transaction( + async () => { + const savedUser = await this.userService.createUser(userFromInput); + invitationToken = this.authenticationHelper.generateInvitationToken( + { id: savedUser.id }, + this.configService.get('INVITATION_TOKEN_EXPTIME'), + ); + await this.userService.updateField( + savedUser.id, + 'inviteToken', + invitationToken.token, + ); + const user = await this.userService.getUserById(savedUser.id); + const userResponse = { + id: user.id, + firstName: user.firstName, + lastName: user.lastName, + inviteToken: user?.inviteToken, + status: user.status, + tenantId: user.tenantId, + }; + return { + inviteToken: invitationToken.token, + tokenExpiryTime: invitationToken.tokenExpiryTime, + user: userResponse, + }; + }, + ); return transaction; } diff --git a/src/authorization/repository/base.repository.ts b/src/authorization/repository/base.repository.ts index 8efb138..92c2f9d 100644 --- a/src/authorization/repository/base.repository.ts +++ b/src/authorization/repository/base.repository.ts @@ -1,4 +1,13 @@ -import { DataSource, EntityTarget, ObjectLiteral, Repository } from 'typeorm'; +import { + DataSource, + EntityTarget, + ObjectLiteral, + Repository, + SelectQueryBuilder, + QueryRunner, + SaveOptions, + DeepPartial, +} from 'typeorm'; import { getConnection } from '../../util/database.connection'; export class BaseRepository extends Repository { @@ -53,4 +62,17 @@ export class BaseRepository extends Repository { const repository = await this.getDynamicRepository(); return repository.count(options); } + + async getQueryBuilder( + alias?: string, + queryRunner?: QueryRunner, + ): Promise> { + const repository = await this.getDynamicRepository(); + return repository.createQueryBuilder(alias, queryRunner); + } + + async save(entity: any, options?: SaveOptions): Promise { + const repository = await this.getDynamicRepository(); + return repository.save(entity, options); + } } diff --git a/src/authorization/repository/group.repository.ts b/src/authorization/repository/group.repository.ts index 0e7293e..8027748 100644 --- a/src/authorization/repository/group.repository.ts +++ b/src/authorization/repository/group.repository.ts @@ -29,7 +29,7 @@ export class GroupRepository extends BaseRepository { } async getGroupsForUserId(userId: string): Promise { - return this.createQueryBuilder('group') + return (await this.getQueryBuilder('group')) .leftJoinAndSelect(UserGroup, 'userGroup', 'group.id = userGroup.groupId') .where('userGroup.userId = :userId', { userId }) .getMany(); diff --git a/src/authorization/repository/groupRole.repository.ts b/src/authorization/repository/groupRole.repository.ts index a05e46f..24b3028 100644 --- a/src/authorization/repository/groupRole.repository.ts +++ b/src/authorization/repository/groupRole.repository.ts @@ -11,7 +11,7 @@ export class GroupRoleRepository extends BaseRepository { } async getGroupCountForRoleId(roleId: string): Promise { - return this.createQueryBuilder('groupRole') + return (await this.getQueryBuilder('groupRole')) .innerJoinAndSelect(Group, 'group', 'group.id = groupRole.groupId') .where('groupRole.roleId= :roleId', { roleId }) .getCount(); diff --git a/src/authorization/repository/permission.repository.ts b/src/authorization/repository/permission.repository.ts index ac69c41..d80392f 100644 --- a/src/authorization/repository/permission.repository.ts +++ b/src/authorization/repository/permission.repository.ts @@ -50,7 +50,7 @@ export class PermissionRepository extends BaseRepository { } async getPermissionsByRoleId(roleId: string): Promise { - return this.createQueryBuilder('permission') + return (await this.getQueryBuilder('permission')) .leftJoinAndSelect( RolePermission, 'rolePermission', @@ -61,7 +61,7 @@ export class PermissionRepository extends BaseRepository { } async getPermissionsByGroupId(groupId: string): Promise { - return this.createQueryBuilder('permission') + return (await this.getQueryBuilder('permission')) .leftJoinAndSelect( GroupPermission, 'groupPermission', @@ -72,7 +72,7 @@ export class PermissionRepository extends BaseRepository { } async getPermissionsByEntityId(entityId: string): Promise { - return this.createQueryBuilder('permission') + return (await this.getQueryBuilder('permission')) .leftJoinAndSelect( EntityPermission, 'entityPermission', @@ -85,7 +85,7 @@ export class PermissionRepository extends BaseRepository { } async getPermissionsByUserId(userId: string): Promise { - return this.createQueryBuilder('permission') + return (await this.getQueryBuilder('permission')) .leftJoinAndSelect( UserPermission, 'userPermission', @@ -98,7 +98,7 @@ export class PermissionRepository extends BaseRepository { async getGroupRolePermissionsByGroupId( groupId: string, ): Promise { - return this.createQueryBuilder('permission') + return (await this.getQueryBuilder('permission')) .innerJoin( RolePermission, 'rolePermission', diff --git a/src/authorization/repository/role.repository.ts b/src/authorization/repository/role.repository.ts index 0b38ec1..60c355b 100644 --- a/src/authorization/repository/role.repository.ts +++ b/src/authorization/repository/role.repository.ts @@ -29,7 +29,7 @@ export class RoleRepository extends BaseRepository { } async getRolesForGroupId(groupId: string): Promise { - return this.createQueryBuilder('role') + return (await this.getQueryBuilder('role')) .leftJoinAndSelect(GroupRole, 'groupRole', 'role.id = groupRole.roleId') .where('groupRole.groupId = :groupId', { groupId }) .getMany(); diff --git a/src/authorization/repository/user.repository.ts b/src/authorization/repository/user.repository.ts index 0a2d3a6..ed699fe 100644 --- a/src/authorization/repository/user.repository.ts +++ b/src/authorization/repository/user.repository.ts @@ -16,7 +16,7 @@ export class UserRepository extends BaseRepository { } async getUserByEmail(email: string) { - return this.createQueryBuilder('user') + return (await this.getQueryBuilder('user')) .where('lower(user.email) = lower(:email)', { email }) .getOne(); } @@ -42,14 +42,14 @@ export class UserRepository extends BaseRepository { } async getUsersByGroupId(groupId: string): Promise { - return this.createQueryBuilder('user') + return (await this.getQueryBuilder('user')) .leftJoinAndSelect(UserGroup, 'userGroup', 'userGroup.userId = user.id') .where('userGroup.groupId = :groupId', { groupId }) .getMany(); } async getUserCountForGroupId(groupId: string): Promise { - return this.createQueryBuilder('user') + return (await this.getQueryBuilder('user')) .innerJoinAndSelect(UserGroup, 'userGroup', 'userGroup.userId = user.id') .where('userGroup.groupId = :groupId', { groupId }) .getCount(); diff --git a/src/authorization/service/entity.service.ts b/src/authorization/service/entity.service.ts index 634d794..648b91c 100644 --- a/src/authorization/service/entity.service.ts +++ b/src/authorization/service/entity.service.ts @@ -14,8 +14,8 @@ import { EntityModelRepository } from '../repository/entity.repository'; import { EntityPermissionRepository } from '../repository/entityPermission.repository'; import { PermissionRepository } from '../repository/permission.repository'; import { EntityServiceInterface } from './entity.service.interface'; -import { ExecutionManager } from '../../util/execution.manager'; import { getConnection } from '../../util/database.connection'; +import { ExecutionManager } from '../../util/execution.manager'; @Injectable() export class EntityService implements EntityServiceInterface { @@ -114,7 +114,8 @@ export class EntityService implements EntityServiceInterface { })), ); - await this.dataSource.manager.transaction(async (entityManager) => { + const entityManager = (await getConnection()).manager; + await entityManager.transaction(async (entityManager) => { const entityPermissionsRepo = entityManager.getRepository( EntityPermission, ); diff --git a/src/authorization/service/group.service.ts b/src/authorization/service/group.service.ts index 0b57db1..1e43f2f 100644 --- a/src/authorization/service/group.service.ts +++ b/src/authorization/service/group.service.ts @@ -42,6 +42,7 @@ import { UserCacheServiceInterface } from './usercache.service.interface'; import { DUPLICATE_ERROR_CODE } from '../../constants/db.error.constants'; import { LoggerService } from '../../logger/logger.service'; import { ExecutionManager } from '../../util/execution.manager'; +import { getConnection } from '../../util/database.connection'; @Injectable() export class GroupService implements GroupServiceInterface { @@ -74,7 +75,7 @@ export class GroupService implements GroupServiceInterface { ['name', 'group.name'], ['updatedAt', 'group.updated_at'], ]); - let queryBuilder = this.groupRepository.createQueryBuilder('group'); + let queryBuilder = await this.groupRepository.getQueryBuilder('group'); if (input?.search) { queryBuilder = this.searchService.generateSearchTermForEntity( @@ -170,7 +171,7 @@ export class GroupService implements GroupServiceInterface { throw new GroupDeleteNotAllowedException(); } - await this.dataSource.manager.transaction(async (entityManager) => { + await (await getConnection()).manager.transaction(async (entityManager) => { const groupRepo = entityManager.getRepository(Group); const groupRoleRepo = entityManager.getRepository(GroupRole); const groupPermissionRepo = entityManager.getRepository(GroupPermission); @@ -222,7 +223,7 @@ export class GroupService implements GroupServiceInterface { })), ); - await this.dataSource.manager.transaction(async (entityManager) => { + await (await getConnection()).manager.transaction(async (entityManager) => { const groupPermissionsRepo = entityManager.getRepository(GroupPermission); await groupPermissionsRepo.remove(permissionsToBeRemovedFromGroup); await groupPermissionsRepo.save(groupPermission); @@ -251,7 +252,7 @@ export class GroupService implements GroupServiceInterface { userIds.map((userId) => ({ userId: userId, groupId: id })), ); - await this.dataSource.manager.transaction(async (entityManager) => { + await (await getConnection()).manager.transaction(async (entityManager) => { const userGroupsRepo = entityManager.getRepository(UserGroup); await userGroupsRepo.remove(usersToBeRemovedFromGroup); await userGroupsRepo.save(userGroups); @@ -321,7 +322,7 @@ export class GroupService implements GroupServiceInterface { })), ); - await this.dataSource.manager.transaction(async (entityManager) => { + await (await getConnection()).manager.transaction(async (entityManager) => { const groupRolesRepo = entityManager.getRepository(GroupRole); await groupRolesRepo.remove(rolesToBeRemovedFromGroup); await groupRolesRepo.save(groupRoles); diff --git a/src/authorization/service/role.service.ts b/src/authorization/service/role.service.ts index 72ce4eb..62de6e9 100644 --- a/src/authorization/service/role.service.ts +++ b/src/authorization/service/role.service.ts @@ -24,6 +24,7 @@ import { RoleServiceInterface } from './role.service.interface'; import { RoleCacheServiceInterface } from './rolecache.service.interface'; import SearchService from './search.service'; import { ExecutionManager } from '../../util/execution.manager'; +import { getConnection } from '../../util/database.connection'; @Injectable() export class RoleService implements RoleServiceInterface { @@ -43,7 +44,7 @@ export class RoleService implements RoleServiceInterface { ['name', 'role.name'], ['updatedAt', 'role.updated_at'], ]); - let queryBuilder = this.rolesRepository.createQueryBuilder('role'); + let queryBuilder = await this.rolesRepository.getQueryBuilder('role'); if (input?.search) { queryBuilder = this.searchService.generateSearchTermForEntity( queryBuilder, @@ -106,7 +107,7 @@ export class RoleService implements RoleServiceInterface { throw new RoleDeleteNotAllowedException(); } - await this.dataSource.manager.transaction(async (entityManager) => { + await (await getConnection()).manager.transaction(async (entityManager) => { const rolePermissionsRepo = entityManager.getRepository(RolePermission); const roleRepo = entityManager.getRepository(Role); await rolePermissionsRepo.softDelete({ roleId: id }); @@ -155,7 +156,7 @@ export class RoleService implements RoleServiceInterface { })), ); - await this.dataSource.manager.transaction(async (entityManager) => { + await (await getConnection()).manager.transaction(async (entityManager) => { const rolePermissionsRepo = entityManager.getRepository(RolePermission); await rolePermissionsRepo.remove(permissionsToBeRemovedFromRole); await rolePermissionsRepo.save(rolePermissions); diff --git a/src/authorization/service/user.service.ts b/src/authorization/service/user.service.ts index 315f2d4..aff3763 100644 --- a/src/authorization/service/user.service.ts +++ b/src/authorization/service/user.service.ts @@ -1,5 +1,5 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { DataSource, SelectQueryBuilder } from 'typeorm'; +import { DataSource, EntityManager, SelectQueryBuilder } from 'typeorm'; import { UserNotAuthorized } from '../../authentication/exception/userauth.exception'; import { FilterBuilder } from '../../common/filter.builder'; import { SearchEntity } from '../../constants/search.entity.enum'; @@ -33,6 +33,7 @@ import SearchService from './search.service'; import { UserServiceInterface } from './user.service.interface'; import { UserCacheServiceInterface } from './usercache.service.interface'; import { ExecutionManager } from '../../util/execution.manager'; +import { getConnection } from '../../util/database.connection'; @Injectable() export class UserService implements UserServiceInterface { @@ -54,7 +55,7 @@ export class UserService implements UserServiceInterface { private roleCacheService: RoleCacheServiceInterface, ) {} - getAllUsers(input?: UserInputFilter): Promise<[User[], number]> { + async getAllUsers(input?: UserInputFilter): Promise<[User[], number]> { const SortFieldMapping = new Map([ ['firstName', 'user.firstName'], ['updatedAt', 'user.updated_at'], @@ -74,7 +75,7 @@ export class UserService implements UserServiceInterface { ); } }; - const qb = this.userRepository.createQueryBuilder('user'); + const qb = await this.userRepository.getQueryBuilder('user'); if (input?.search) { this.searchService.generateSearchTermForEntity( qb, @@ -156,7 +157,7 @@ export class UserService implements UserServiceInterface { user.groups.map((group) => ({ userId: id, groupId: group })), ); - await this.dataSource.manager.transaction(async (entityManager) => { + await (await getConnection()).manager.transaction(async (entityManager) => { const userGroupsRepo = entityManager.getRepository(UserGroup); await userGroupsRepo.remove(groupsToBeRemovedFromUser); await userGroupsRepo.save(userGroups); @@ -227,7 +228,7 @@ export class UserService implements UserServiceInterface { throw new UserNotFoundException(id); } - await this.dataSource.manager.transaction(async (entityManager) => { + await (await getConnection()).manager.transaction(async (entityManager) => { const userRepo = entityManager.getRepository(User); const userGroupRepo = entityManager.getRepository(UserGroup); const userPermissionRepo = entityManager.getRepository(UserPermission); @@ -370,7 +371,7 @@ export class UserService implements UserServiceInterface { 'Username should be provided with email or phone', ); } - let query = this.userRepository.createQueryBuilder('user'); + let query = await this.userRepository.getQueryBuilder('user'); if (email) { query = query.orWhere('lower(user.email) = lower(:email)', { email: nullCheckedEmail, diff --git a/test/authorization/repository/group.repository.test.ts b/test/authorization/repository/group.repository.test.ts index 56cd70c..cf4eb69 100644 --- a/test/authorization/repository/group.repository.test.ts +++ b/test/authorization/repository/group.repository.test.ts @@ -48,7 +48,7 @@ describe('test Group repository', () => { updateMock = groupRepository.update = jest.fn(); findMock = groupRepository.find = jest.fn(); - createQueryBuilderMock = groupRepository.createQueryBuilder = jest + createQueryBuilderMock = groupRepository.getQueryBuilder = jest .fn() .mockReturnValue({ leftJoinAndSelect: jest.fn().mockReturnThis(), diff --git a/test/authorization/repository/groupRole.repository.test.ts b/test/authorization/repository/groupRole.repository.test.ts index 99ccd12..1748f7a 100644 --- a/test/authorization/repository/groupRole.repository.test.ts +++ b/test/authorization/repository/groupRole.repository.test.ts @@ -26,7 +26,7 @@ describe('test GroupRole repository', () => { groupRoleRepository = moduleRef.get(GroupRoleRepository); - createQueryBuilderMock = groupRoleRepository.createQueryBuilder = jest + createQueryBuilderMock = groupRoleRepository.getQueryBuilder = jest .fn() .mockReturnValue({ innerJoinAndSelect: jest.fn().mockReturnThis(), diff --git a/test/authorization/repository/permission.repository.test.ts b/test/authorization/repository/permission.repository.test.ts index 579cc93..0b8f859 100644 --- a/test/authorization/repository/permission.repository.test.ts +++ b/test/authorization/repository/permission.repository.test.ts @@ -60,7 +60,7 @@ describe('test Permission repository', () => { updateMock = permissionRepository.update = jest.fn(); softDeleteMock = permissionRepository.softDelete = jest.fn(); - createQueryBuilderMock = permissionRepository.createQueryBuilder = jest + createQueryBuilderMock = permissionRepository.getQueryBuilder = jest .fn() .mockReturnValue({ innerJoin: jest.fn().mockReturnThis(), diff --git a/test/authorization/repository/role.repository.test.ts b/test/authorization/repository/role.repository.test.ts index cbd168d..2a6e28c 100644 --- a/test/authorization/repository/role.repository.test.ts +++ b/test/authorization/repository/role.repository.test.ts @@ -47,7 +47,7 @@ describe('test Role repository', () => { updateMock = roleRepository.update = jest.fn(); findMock = roleRepository.find = jest.fn(); - createQueryBuilderMock = roleRepository.createQueryBuilder = jest + createQueryBuilderMock = roleRepository.getQueryBuilder = jest .fn() .mockReturnValue({ leftJoinAndSelect: jest.fn().mockReturnThis(), diff --git a/test/authorization/repository/user.repository.test.ts b/test/authorization/repository/user.repository.test.ts index 5f89e37..f1801b7 100644 --- a/test/authorization/repository/user.repository.test.ts +++ b/test/authorization/repository/user.repository.test.ts @@ -59,7 +59,7 @@ describe('test User repository', () => { updateMock = userRepository.update = jest.fn(); findMock = userRepository.find = jest.fn(); - createQueryBuilderMock = userRepository.createQueryBuilder = jest + createQueryBuilderMock = userRepository.getQueryBuilder = jest .fn() .mockReturnValue({ leftJoinAndSelect: jest.fn().mockReturnThis(), diff --git a/test/authorization/service/group.service.test.ts b/test/authorization/service/group.service.test.ts index 63da8bc..bdb57fb 100644 --- a/test/authorization/service/group.service.test.ts +++ b/test/authorization/service/group.service.test.ts @@ -175,7 +175,7 @@ describe('test Group Service', () => { userGroupRepository = moduleRef.get(UserGroupRepository); dataSource = moduleRef.get(DataSource); - createQueryBuilderMock = groupRepository.createQueryBuilder = jest + createQueryBuilderMock = groupRepository.getQueryBuilder = jest .fn() .mockReturnValue({ leftJoinAndSelect: jest.fn().mockReturnThis(), diff --git a/test/authorization/service/role.service.test.ts b/test/authorization/service/role.service.test.ts index 77f437f..7981072 100644 --- a/test/authorization/service/role.service.test.ts +++ b/test/authorization/service/role.service.test.ts @@ -103,7 +103,7 @@ describe('test Role Service', () => { roleRepository = moduleRef.get(RoleRepository); permissionRepository = moduleRef.get(PermissionRepository); - createQueryBuilderMock = roleRepository.createQueryBuilder = jest + createQueryBuilderMock = roleRepository.getQueryBuilder = jest .fn() .mockReturnValue({ leftJoinAndSelect: jest.fn().mockReturnThis(), diff --git a/test/authorization/service/user.service.test.ts b/test/authorization/service/user.service.test.ts index 95fa0d2..54510f5 100644 --- a/test/authorization/service/user.service.test.ts +++ b/test/authorization/service/user.service.test.ts @@ -151,7 +151,7 @@ describe('test UserService', () => { saveMock = userRepository.save = jest.fn(); updateUserByIdMock = userRepository.updateUserById = jest.fn(); - createQueryBuilderMock = userRepository.createQueryBuilder = jest + createQueryBuilderMock = userRepository.getQueryBuilder = jest .fn() .mockReturnValue({ leftJoinAndSelect: jest.fn().mockReturnThis(), From 001b8bb47f73267accd7a012c242fc49c024d927 Mon Sep 17 00:00:00 2001 From: Sruthy M L Date: Fri, 13 Dec 2024 10:53:22 +0530 Subject: [PATCH 10/23] FIX: add configurable max connection limit per tenant if needed --- env.sample | 1 + src/util/database.connection.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/env.sample b/env.sample index 18f559c..64ae071 100644 --- a/env.sample +++ b/env.sample @@ -3,6 +3,7 @@ POSTGRES_PORT=5432 POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres POSTGRES_DB=authentication-service +POSTGRES_TENANT_MAX_CONNECTION_LIMIT=10 REDIS_HOST=localhost REDIS_PORT=6379 REDIS_CACHE_TTL=3600 diff --git a/src/util/database.connection.ts b/src/util/database.connection.ts index b84c94d..9e491bf 100644 --- a/src/util/database.connection.ts +++ b/src/util/database.connection.ts @@ -41,7 +41,9 @@ export async function getConnectionForTenant( synchronize: false, logging: ['error'], namingStrategy: new SnakeNamingStrategy(), - extra: { max: process.env.POSTGRES_TENANT_MAX_CONNECTION_LIMIT }, + ...(process.env.POSTGRES_TENANT_MAX_CONNECTION_LIMIT + ? { extra: { max: process.env.POSTGRES_TENANT_MAX_CONNECTION_LIMIT } } + : {}), }).initialize(); await switchToTenant(tenantId, newConnection); From ea0526f843b755a03878e1a49e2e156cc1048c49 Mon Sep 17 00:00:00 2001 From: Sruthy M L Date: Fri, 13 Dec 2024 10:58:57 +0530 Subject: [PATCH 11/23] REFACTOR: rename executionId.middleware.ts to executionContext.middleware.ts --- src/app.module.ts | 2 +- ...executionId.middleware.ts => executionContext.middleware.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/middleware/{executionId.middleware.ts => executionContext.middleware.ts} (100%) diff --git a/src/app.module.ts b/src/app.module.ts index aadaf94..4219a0f 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -7,7 +7,7 @@ import { AppGraphQLModule } from './graphql/graphql.module'; import { UserAuthModule } from './authentication/authentication.module'; import { AuthorizationModule } from './authorization/authorization.module'; import { HealthModule } from './health/health.module'; -import { ExecutionContextBinder } from './middleware/executionId.middleware'; +import { ExecutionContextBinder } from './middleware/executionContext.middleware'; @Module({ imports: [ diff --git a/src/middleware/executionId.middleware.ts b/src/middleware/executionContext.middleware.ts similarity index 100% rename from src/middleware/executionId.middleware.ts rename to src/middleware/executionContext.middleware.ts From 93da280586a82c1b7921a5ac92b035d8d31f90f8 Mon Sep 17 00:00:00 2001 From: Sruthy M L Date: Fri, 13 Dec 2024 11:21:07 +0530 Subject: [PATCH 12/23] FEAT: use sepearate db users for migrations and tenant operations --- env.sample | 8 ++++++-- src/config/migration.config.ts | 4 ++-- src/util/database.connection.ts | 31 +++++++++++++++++++++++++++++-- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/env.sample b/env.sample index 64ae071..bc06835 100644 --- a/env.sample +++ b/env.sample @@ -1,7 +1,11 @@ +# Superuser for migrations +POSTGRES_ADMIN_USER=postgres +POSTGRES_ADMIN_PASSWORD=postgres +# Minimal user with restricted access +POSTGRES_TENANT_USER=tenant +POSTGRES_TENANT_PASSWORD=tenant POSTGRES_HOST=localhost POSTGRES_PORT=5432 -POSTGRES_USER=postgres -POSTGRES_PASSWORD=postgres POSTGRES_DB=authentication-service POSTGRES_TENANT_MAX_CONNECTION_LIMIT=10 REDIS_HOST=localhost diff --git a/src/config/migration.config.ts b/src/config/migration.config.ts index bb6934c..6b338ce 100644 --- a/src/config/migration.config.ts +++ b/src/config/migration.config.ts @@ -10,8 +10,8 @@ const dataSource = new DataSource({ host: process.env.POSTGRES_HOST, port: parseInt(process.env.POSTGRES_PORT as string), database: process.env.POSTGRES_DB, - username: process.env.POSTGRES_USER, - password: process.env.POSTGRES_PASSWORD, + username: process.env.POSTGRES_ADMIN_USER, + password: process.env.POSTGRES_ADMIN_PASSWORD, synchronize: false, logging: false, namingStrategy: new SnakeNamingStrategy(), diff --git a/src/util/database.connection.ts b/src/util/database.connection.ts index 9e491bf..294e821 100644 --- a/src/util/database.connection.ts +++ b/src/util/database.connection.ts @@ -31,8 +31,8 @@ export async function getConnectionForTenant( type: 'postgres', host: process.env.POSTGRES_HOST, port: Number(process.env.POSTGRES_PORT), - username: process.env.POSTGRES_USER, - password: process.env.POSTGRES_PASSWORD, + username: process.env.POSTGRES_TENANT_USER, + password: process.env.POSTGRES_TENANT_PASSWORD, database: process.env.POSTGRES_DB, entities: [ __dirname + '/../**/*.entity.ts', @@ -68,3 +68,30 @@ const switchToTenant = async ( ); } }; + +export async function getConnectionAsOwner(): Promise { + if (!getConnectionManager().has('Owner')) + return await new DataSource({ + name: 'Owner', + type: 'postgres', + host: process.env.POSTGRES_HOST, + port: Number(process.env.POSTGRES_PORT), + username: process.env.POSTGRES_ADMIN_USER, + password: process.env.POSTGRES_ADMIN_PASSWORD, + database: process.env.POSTGRES_DB, + entities: [__dirname + '/../**/*.entity.js'], + synchronize: false, + logging: ['error'], + namingStrategy: new SnakeNamingStrategy(), + migrationsTableName: 'migrations', + migrations: [__dirname + '/../migrations/*.js'], + ...(process.env.POSTGRES_ADMIN_MAX_CONNECTION_LIMIT + ? { extra: { max: process.env.POSTGRES_ADMIN_MAX_CONNECTION_LIMIT } } + : {}), + }).initialize(); + const con = getConnectionManager().get('Owner'); + const existingConnection = await Promise.resolve( + con.isConnected ? con : con.connect(), + ); + return existingConnection; +} From 29168b2feccc2b76477e429a67e85206a2911463 Mon Sep 17 00:00:00 2001 From: Sruthy M L Date: Fri, 13 Dec 2024 11:33:31 +0530 Subject: [PATCH 13/23] FIX: use dynamic connection for user permission updation --- src/authorization/repository/base.repository.ts | 7 ++++++- src/authorization/service/user.service.ts | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/authorization/repository/base.repository.ts b/src/authorization/repository/base.repository.ts index 92c2f9d..c6be59f 100644 --- a/src/authorization/repository/base.repository.ts +++ b/src/authorization/repository/base.repository.ts @@ -6,7 +6,7 @@ import { SelectQueryBuilder, QueryRunner, SaveOptions, - DeepPartial, + RemoveOptions, } from 'typeorm'; import { getConnection } from '../../util/database.connection'; @@ -75,4 +75,9 @@ export class BaseRepository extends Repository { const repository = await this.getDynamicRepository(); return repository.save(entity, options); } + + async remove(entity: any, options?: RemoveOptions): Promise { + const repository = await this.getDynamicRepository(); + return repository.remove(entity, options); + } } diff --git a/src/authorization/service/user.service.ts b/src/authorization/service/user.service.ts index aff3763..00dd66e 100644 --- a/src/authorization/service/user.service.ts +++ b/src/authorization/service/user.service.ts @@ -202,7 +202,7 @@ export class UserService implements UserServiceInterface { })), ); - const userPermissionsUpdated = await this.dataSource.transaction( + const userPermissionsUpdated = await (await getConnection()).transaction( async (entityManager) => { const userPermissionsRepo = entityManager.getRepository(UserPermission); await userPermissionsRepo.remove(userPermissionsToBeRemoved); From f9c3033dbdc1c98c72efecd7a6a4597e325ac0ae Mon Sep 17 00:00:00 2001 From: Sruthy M L Date: Fri, 13 Dec 2024 12:00:06 +0530 Subject: [PATCH 14/23] FIX: use admin user for db connection setup --- env.sample | 1 + src/database/database.module.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/env.sample b/env.sample index bc06835..d187ffa 100644 --- a/env.sample +++ b/env.sample @@ -37,3 +37,4 @@ MIN_RECAPTCHA_SCORE=.5 RECAPTCHA_VERIFY_URL=https://www.google.com/recaptcha/api/siteverify DEFAULT_ADMIN_PASSWORD=adminpassword INVITATION_TOKEN_EXPTIME = 7d +AUTH_KEY= diff --git a/src/database/database.module.ts b/src/database/database.module.ts index 153dfe2..085a794 100644 --- a/src/database/database.module.ts +++ b/src/database/database.module.ts @@ -12,8 +12,8 @@ import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; type: 'postgres', host: configService.get('POSTGRES_HOST'), port: configService.get('POSTGRES_PORT'), - username: configService.get('POSTGRES_USER'), - password: configService.get('POSTGRES_PASSWORD'), + username: configService.get('POSTGRES_ADMIN_USER'), + password: configService.get('POSTGRES_ADMIN_PASSWORD'), database: configService.get('POSTGRES_DB'), entities: [ __dirname + '/../**/*.entity.ts', From f0c0d754b7db9a4ada7369622dbfef8e1bb8d0d7 Mon Sep 17 00:00:00 2001 From: Sruthy M L Date: Fri, 13 Dec 2024 12:06:11 +0530 Subject: [PATCH 15/23] FEAT: add env validations for new postgres user variables --- src/app.module.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index 4219a0f..68fd3d2 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -15,8 +15,10 @@ import { ExecutionContextBinder } from './middleware/executionContext.middleware validationSchema: Joi.object({ POSTGRES_HOST: Joi.string().required(), POSTGRES_PORT: Joi.number().required(), - POSTGRES_USER: Joi.string().required(), - POSTGRES_PASSWORD: Joi.string().required(), + POSTGRES_ADMIN_USER: Joi.string().required(), + POSTGRES_ADMIN_PASSWORD: Joi.string().required(), + POSTGRES_TENANT_USER: Joi.string().required(), + POSTGRES_TENANT_PASSWORD: Joi.string().required(), POSTGRES_DB: Joi.string().required(), PORT: Joi.number(), JWT_SECRET: Joi.string().required().min(10), From c0ec4c5f849a8b8d6ef6be9f1ec1287af449b2dd Mon Sep 17 00:00:00 2001 From: Sruthy M L Date: Fri, 13 Dec 2024 12:39:47 +0530 Subject: [PATCH 16/23] DOCS: update README with PostgresSQL admin and tenant user setup --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e435fb0..57216c9 100644 --- a/README.md +++ b/README.md @@ -55,14 +55,17 @@ Developers can customise this as per their requirement. - Clone the repo and execute command `npm install` - Create a copy of the env.sample file and rename it as .env - Install postgres and redis +- Create a restricted tenant user that cannot bypass RLS policies - Provide postgres, redis secrets and default user details in .env file as mentioned below | Database configuration(Required) | | |--|--| |POSTGRES_HOST | localhost | |POSTGRES_PORT | 5432| -|POSTGRES_USER | postgres | -|POSTGRES_PASSWORD | postgres | +|POSTGRES_ADMIN_USER | postgres | +|POSTGRES_ADMIN_PASSWORD | postgres | +|POSTGRES_TENANT_USER | tenant | +|POSTGRES_TENANT_PASSWORD | tenant | |POSTGRES_DB | auth_service |   From d0f032c9a32eb1ea2d31cd4815f72a9c560cebe1 Mon Sep 17 00:00:00 2001 From: Bharath Date: Fri, 13 Dec 2024 19:15:41 +0530 Subject: [PATCH 17/23] FEAT: add tenant module --- src/authorization/authorization.module.ts | 14 ++-------- src/schema/graphql.schema.ts | 24 ++++++++-------- .../entity/tenant.entity.ts | 2 +- .../exception/tenant.exception.ts | 0 .../graphql/tenant.graphql | 0 .../repository/tenant.repository.ts | 2 +- .../resolver/tenant.resolver.ts | 0 .../service/tenant.service.interface.ts | 0 .../service/tenant.service.ts | 3 +- src/tenant/tenant.module.ts | 28 +++++++++++++++++++ 10 files changed, 46 insertions(+), 27 deletions(-) rename src/{authorization => tenant}/entity/tenant.entity.ts (81%) rename src/{authorization => tenant}/exception/tenant.exception.ts (100%) rename src/{authorization => tenant}/graphql/tenant.graphql (100%) rename src/{authorization => tenant}/repository/tenant.repository.ts (82%) rename src/{authorization => tenant}/resolver/tenant.resolver.ts (100%) rename src/{authorization => tenant}/service/tenant.service.interface.ts (100%) rename src/{authorization => tenant}/service/tenant.service.ts (83%) create mode 100644 src/tenant/tenant.module.ts diff --git a/src/authorization/authorization.module.ts b/src/authorization/authorization.module.ts index f8ab0bf..b145f47 100644 --- a/src/authorization/authorization.module.ts +++ b/src/authorization/authorization.module.ts @@ -51,11 +51,7 @@ import { UserService } from './service/user.service'; import { UserServiceInterface } from './service/user.service.interface'; import { UserCacheService } from './service/usercache.service'; import { UserCacheServiceInterface } from './service/usercache.service.interface'; -import Tenant from './entity/tenant.entity'; -import { TenantResolver } from './resolver/tenant.resolver'; -import TenantService from './service/tenant.service'; -import { TenantRepository } from './repository/tenant.repository'; -import { TenantServiceInterface } from './service/tenant.service.interface'; +import { TenantModule } from '../tenant/tenant.module'; @Module({ imports: [ @@ -71,8 +67,8 @@ import { TenantServiceInterface } from './service/tenant.service.interface'; Role, GroupRole, RolePermission, - Tenant, ]), + TenantModule, RedisCacheModule, ], providers: [ @@ -97,8 +93,6 @@ import { TenantServiceInterface } from './service/tenant.service.interface'; UserGroupRepository, EntityPermissionRepository, LoggerService, - TenantResolver, - TenantRepository, { provide: EntityServiceInterface, useClass: EntityService, @@ -135,10 +129,6 @@ import { TenantServiceInterface } from './service/tenant.service.interface'; provide: UserCacheServiceInterface, useClass: UserCacheService, }, - { - provide: TenantServiceInterface, - useClass: TenantService, - }, ], exports: [ { diff --git a/src/schema/graphql.schema.ts b/src/schema/graphql.schema.ts index f73494d..237f4de 100644 --- a/src/schema/graphql.schema.ts +++ b/src/schema/graphql.schema.ts @@ -166,11 +166,6 @@ export interface RoleSearchCondition { name?: StringSearchCondition; } -export interface NewTenantInput { - name: string; - domain: string; -} - export interface UpdateUserInput { firstName?: string; middleName?: string; @@ -233,6 +228,11 @@ export interface PaginationInput { offset?: number; } +export interface NewTenantInput { + name: string; + domain: string; +} + export interface Paginated { totalCount?: number; } @@ -266,11 +266,11 @@ export interface IMutation { updateRole(id: string, input: UpdateRoleInput): Role | Promise; deleteRole(id: string): Role | Promise; updateRolePermissions(id: string, input: UpdateRolePermissionInput): Permission[] | Promise; - createTenant(input: NewTenantInput): Tenant | Promise; updateUser(id: string, input: UpdateUserInput): User | Promise; deleteUser(id: string): User | Promise; updateUserPermissions(id: string, input: UpdateUserPermissionInput): Permission[] | Promise; updateUserGroups(id: string, input: UpdateUserGroupInput): UserGroupResponse[] | Promise; + createTenant(input: NewTenantInput): Tenant | Promise; } export interface TokenResponse { @@ -357,12 +357,6 @@ export interface RolePaginated extends Paginated { results?: Role[]; } -export interface Tenant { - id: string; - name: string; - domain: string; -} - export interface UserPaginated extends Paginated { totalCount?: number; results?: User[]; @@ -385,3 +379,9 @@ export interface UserGroupResponse { id: string; name: string; } + +export interface Tenant { + id: string; + name: string; + domain: string; +} diff --git a/src/authorization/entity/tenant.entity.ts b/src/tenant/entity/tenant.entity.ts similarity index 81% rename from src/authorization/entity/tenant.entity.ts rename to src/tenant/entity/tenant.entity.ts index 526f829..04f136a 100644 --- a/src/authorization/entity/tenant.entity.ts +++ b/src/tenant/entity/tenant.entity.ts @@ -1,5 +1,5 @@ import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; -import BaseEntity from './base.entity'; +import BaseEntity from '../../authorization/entity/base.entity'; @Entity() class Tenant extends BaseEntity { diff --git a/src/authorization/exception/tenant.exception.ts b/src/tenant/exception/tenant.exception.ts similarity index 100% rename from src/authorization/exception/tenant.exception.ts rename to src/tenant/exception/tenant.exception.ts diff --git a/src/authorization/graphql/tenant.graphql b/src/tenant/graphql/tenant.graphql similarity index 100% rename from src/authorization/graphql/tenant.graphql rename to src/tenant/graphql/tenant.graphql diff --git a/src/authorization/repository/tenant.repository.ts b/src/tenant/repository/tenant.repository.ts similarity index 82% rename from src/authorization/repository/tenant.repository.ts rename to src/tenant/repository/tenant.repository.ts index 5f3a81c..800b01e 100644 --- a/src/authorization/repository/tenant.repository.ts +++ b/src/tenant/repository/tenant.repository.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import { BaseRepository } from './base.repository'; +import { BaseRepository } from '../../authorization/repository/base.repository'; import Tenant from '../entity/tenant.entity'; @Injectable() diff --git a/src/authorization/resolver/tenant.resolver.ts b/src/tenant/resolver/tenant.resolver.ts similarity index 100% rename from src/authorization/resolver/tenant.resolver.ts rename to src/tenant/resolver/tenant.resolver.ts diff --git a/src/authorization/service/tenant.service.interface.ts b/src/tenant/service/tenant.service.interface.ts similarity index 100% rename from src/authorization/service/tenant.service.interface.ts rename to src/tenant/service/tenant.service.interface.ts diff --git a/src/authorization/service/tenant.service.ts b/src/tenant/service/tenant.service.ts similarity index 83% rename from src/authorization/service/tenant.service.ts rename to src/tenant/service/tenant.service.ts index bd34559..c3081e2 100644 --- a/src/authorization/service/tenant.service.ts +++ b/src/tenant/service/tenant.service.ts @@ -4,9 +4,10 @@ import Tenant from '../entity/tenant.entity'; import { TenantNotFoundException } from '../exception/tenant.exception'; import { TenantRepository } from '../repository/tenant.repository'; import { NewTenantInput } from '../../schema/graphql.schema'; +import { TenantServiceInterface } from './tenant.service.interface'; @Injectable() -export default class TenantService { +export default class TenantService implements TenantServiceInterface { constructor(private tenantRepository: TenantRepository) {} async getTenantByDomain(domain: string): Promise { diff --git a/src/tenant/tenant.module.ts b/src/tenant/tenant.module.ts new file mode 100644 index 0000000..27dd1d6 --- /dev/null +++ b/src/tenant/tenant.module.ts @@ -0,0 +1,28 @@ +import { Module } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import Tenant from '../tenant/entity/tenant.entity'; +import { TenantResolver } from '../tenant/resolver/tenant.resolver'; +import TenantService from '../tenant/service/tenant.service'; +import { TenantRepository } from '../tenant/repository/tenant.repository'; +import { TenantServiceInterface } from '../tenant/service/tenant.service.interface'; + +@Module({ + imports: [TypeOrmModule.forFeature([Tenant])], + providers: [ + ConfigService, + TenantResolver, + TenantRepository, + { + provide: TenantServiceInterface, + useClass: TenantService, + }, + ], + exports: [ + { + provide: TenantServiceInterface, + useClass: TenantService, + }, + ], +}) +export class TenantModule {} From c985e3799d7895c07561e98edc98bbe04c241bf8 Mon Sep 17 00:00:00 2001 From: Sruthy M L Date: Tue, 17 Dec 2024 10:38:48 +0530 Subject: [PATCH 18/23] REFACTOR: use NestJS dependency injection in the request scope to obtain a connection from the connection pool --- src/authentication/authentication.module.ts | 2 + .../service/password.auth.service.ts | 57 ++++++----- src/authorization/authorization.module.ts | 2 + .../repository/base.repository.ts | 78 +-------------- .../repository/entity.repository.ts | 8 +- .../repository/group.repository.ts | 10 +- .../repository/groupRole.repository.ts | 10 +- .../repository/permission.repository.ts | 10 +- .../repository/role.repository.ts | 10 +- .../repository/user.repository.ts | 14 ++- .../repository/userPermission.repository.ts | 8 +- src/authorization/service/entity.service.ts | 11 +-- src/authorization/service/group.service.ts | 13 +-- src/authorization/service/role.service.ts | 10 +- src/authorization/service/user.service.ts | 15 +-- src/database/database.constants.ts | 1 + src/database/database.module.ts | 3 + src/database/database.provider.ts | 68 +++++++++++++ src/util/database.connection.ts | 97 ------------------- .../service/passwordauth.service.test.ts | 5 + .../repository/entity.repository.test.ts | 6 +- .../repository/group.repository.test.ts | 7 +- .../groupPermission.repository.test.ts | 5 + .../repository/groupRole.repository.test.ts | 7 +- .../repository/permission.repository.test.ts | 2 +- .../repository/role.repository.test.ts | 7 +- .../rolePermission.repository.test.ts | 5 + .../repository/user.repository.test.ts | 7 +- .../repository/userGroup.repository.test.ts | 5 + .../userPermission.repository.test.ts | 5 + .../service/group.service.test.ts | 7 +- .../service/permission.service.test.ts | 5 + .../service/role.service.test.ts | 7 +- .../service/user.service.test.ts | 7 +- 34 files changed, 255 insertions(+), 259 deletions(-) create mode 100644 src/database/database.constants.ts create mode 100644 src/database/database.provider.ts delete mode 100644 src/util/database.connection.ts diff --git a/src/authentication/authentication.module.ts b/src/authentication/authentication.module.ts index 2ff6b16..a9c5e47 100644 --- a/src/authentication/authentication.module.ts +++ b/src/authentication/authentication.module.ts @@ -33,6 +33,7 @@ import PasswordAuthService from './service/password.auth.service'; import { RecaptchaService } from './service/recaptcha.service'; import { TokenService } from './service/token.service'; import TwilioOTPService from './service/twilio.otp.service'; +import { DatabaseModule } from '../database/database.module'; const providers: Provider[] = [ UserAuthResolver, @@ -74,6 +75,7 @@ const providers: Provider[] = [ AuthorizationModule, TwilioImplModule, HttpModule, + DatabaseModule, ], providers, controllers: [GoogleAuthController], diff --git a/src/authentication/service/password.auth.service.ts b/src/authentication/service/password.auth.service.ts index 89eddbc..44e7d79 100644 --- a/src/authentication/service/password.auth.service.ts +++ b/src/authentication/service/password.auth.service.ts @@ -25,7 +25,7 @@ import { } from '../exception/userauth.exception'; import { Authenticatable } from '../interfaces/authenticatable'; import { TokenService } from './token.service'; -import { getConnection } from '../../util/database.connection'; +import { TENANT_CONNECTION } from '../../database/database.constants'; @Injectable() export default class PasswordAuthService implements Authenticatable { @@ -33,6 +33,7 @@ export default class PasswordAuthService implements Authenticatable { @Inject(UserServiceInterface) private userService: UserServiceInterface, private tokenService: TokenService, private authenticationHelper: AuthenticationHelper, + @Inject(TENANT_CONNECTION) private dataSource: DataSource, private configService: ConfigService, ) {} @@ -87,34 +88,32 @@ export default class PasswordAuthService implements Authenticatable { userFromInput.lastName = userDetails.lastName; userFromInput.status = Status.INVITED; let invitationToken: { token: any; tokenExpiryTime?: any }; - const transaction = await (await getConnection()).manager.transaction( - async () => { - const savedUser = await this.userService.createUser(userFromInput); - invitationToken = this.authenticationHelper.generateInvitationToken( - { id: savedUser.id }, - this.configService.get('INVITATION_TOKEN_EXPTIME'), - ); - await this.userService.updateField( - savedUser.id, - 'inviteToken', - invitationToken.token, - ); - const user = await this.userService.getUserById(savedUser.id); - const userResponse = { - id: user.id, - firstName: user.firstName, - lastName: user.lastName, - inviteToken: user?.inviteToken, - status: user.status, - tenantId: user.tenantId, - }; - return { - inviteToken: invitationToken.token, - tokenExpiryTime: invitationToken.tokenExpiryTime, - user: userResponse, - }; - }, - ); + const transaction = await this.dataSource.manager.transaction(async () => { + const savedUser = await this.userService.createUser(userFromInput); + invitationToken = this.authenticationHelper.generateInvitationToken( + { id: savedUser.id }, + this.configService.get('INVITATION_TOKEN_EXPTIME'), + ); + await this.userService.updateField( + savedUser.id, + 'inviteToken', + invitationToken.token, + ); + const user = await this.userService.getUserById(savedUser.id); + const userResponse = { + id: user.id, + firstName: user.firstName, + lastName: user.lastName, + inviteToken: user?.inviteToken, + status: user.status, + tenantId: user.tenantId, + }; + return { + inviteToken: invitationToken.token, + tokenExpiryTime: invitationToken.tokenExpiryTime, + user: userResponse, + }; + }); return transaction; } diff --git a/src/authorization/authorization.module.ts b/src/authorization/authorization.module.ts index b145f47..77e7019 100644 --- a/src/authorization/authorization.module.ts +++ b/src/authorization/authorization.module.ts @@ -52,6 +52,7 @@ import { UserServiceInterface } from './service/user.service.interface'; import { UserCacheService } from './service/usercache.service'; import { UserCacheServiceInterface } from './service/usercache.service.interface'; import { TenantModule } from '../tenant/tenant.module'; +import { DatabaseModule } from '../database/database.module'; @Module({ imports: [ @@ -70,6 +71,7 @@ import { TenantModule } from '../tenant/tenant.module'; ]), TenantModule, RedisCacheModule, + DatabaseModule, ], providers: [ GroupResolver, diff --git a/src/authorization/repository/base.repository.ts b/src/authorization/repository/base.repository.ts index c6be59f..acbab45 100644 --- a/src/authorization/repository/base.repository.ts +++ b/src/authorization/repository/base.repository.ts @@ -1,83 +1,7 @@ -import { - DataSource, - EntityTarget, - ObjectLiteral, - Repository, - SelectQueryBuilder, - QueryRunner, - SaveOptions, - RemoveOptions, -} from 'typeorm'; -import { getConnection } from '../../util/database.connection'; +import { DataSource, EntityTarget, ObjectLiteral, Repository } from 'typeorm'; export class BaseRepository extends Repository { - private entityClass: any; - constructor(entity: EntityTarget, dataSource: DataSource) { super(entity, dataSource.createEntityManager()); - this.entityClass = entity; - } - - protected async getDynamicRepository(): Promise> { - const connection = await getConnection(); - return connection.getRepository(this.entityClass.name); - } - - async find(options?: any): Promise { - const repository = await this.getDynamicRepository(); - return repository.find(options); - } - - async findOne(options: any): Promise { - const repository = await this.getDynamicRepository(); - return repository.findOne(options); - } - - async findOneOrFail(options: any): Promise { - const repository = await this.getDynamicRepository(); - return repository.findOneOrFail(options); - } - - async findOneBy(options: any): Promise { - const repository = await this.getDynamicRepository(); - return repository.findOneBy(options); - } - - async update(criteria: any, partialEntity: any): Promise { - const repository = await this.getDynamicRepository(); - return repository.update(criteria, partialEntity); - } - - async delete(criteria: any): Promise { - const repository = await this.getDynamicRepository(); - return repository.delete(criteria); - } - - async softDelete(criteria: any): Promise { - const repository = await this.getDynamicRepository(); - return repository.softDelete(criteria); - } - - async count(options?: any): Promise { - const repository = await this.getDynamicRepository(); - return repository.count(options); - } - - async getQueryBuilder( - alias?: string, - queryRunner?: QueryRunner, - ): Promise> { - const repository = await this.getDynamicRepository(); - return repository.createQueryBuilder(alias, queryRunner); - } - - async save(entity: any, options?: SaveOptions): Promise { - const repository = await this.getDynamicRepository(); - return repository.save(entity, options); - } - - async remove(entity: any, options?: RemoveOptions): Promise { - const repository = await this.getDynamicRepository(); - return repository.remove(entity, options); } } diff --git a/src/authorization/repository/entity.repository.ts b/src/authorization/repository/entity.repository.ts index d050de7..21bd8a9 100644 --- a/src/authorization/repository/entity.repository.ts +++ b/src/authorization/repository/entity.repository.ts @@ -1,12 +1,16 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { UpdateEntityInput } from 'src/schema/graphql.schema'; import { DataSource } from 'typeorm'; import EntityModel from '../entity/entity.entity'; import { BaseRepository } from './base.repository'; +import { TENANT_CONNECTION } from '../../database/database.constants'; @Injectable() export class EntityModelRepository extends BaseRepository { - constructor(private dataSource: DataSource) { + constructor( + @Inject(TENANT_CONNECTION) + private dataSource: DataSource, + ) { super(EntityModel, dataSource); } diff --git a/src/authorization/repository/group.repository.ts b/src/authorization/repository/group.repository.ts index 8027748..814f67b 100644 --- a/src/authorization/repository/group.repository.ts +++ b/src/authorization/repository/group.repository.ts @@ -1,12 +1,16 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { DataSource, In } from 'typeorm'; import Group from '../entity/group.entity'; import UserGroup from '../entity/userGroup.entity'; import { BaseRepository } from './base.repository'; +import { TENANT_CONNECTION } from '../../database/database.constants'; @Injectable() export class GroupRepository extends BaseRepository { - constructor(private dataSource: DataSource) { + constructor( + @Inject(TENANT_CONNECTION) + private dataSource: DataSource, + ) { super(Group, dataSource); } @@ -29,7 +33,7 @@ export class GroupRepository extends BaseRepository { } async getGroupsForUserId(userId: string): Promise { - return (await this.getQueryBuilder('group')) + return this.createQueryBuilder('group') .leftJoinAndSelect(UserGroup, 'userGroup', 'group.id = userGroup.groupId') .where('userGroup.userId = :userId', { userId }) .getMany(); diff --git a/src/authorization/repository/groupRole.repository.ts b/src/authorization/repository/groupRole.repository.ts index 24b3028..c5d4a9d 100644 --- a/src/authorization/repository/groupRole.repository.ts +++ b/src/authorization/repository/groupRole.repository.ts @@ -1,17 +1,21 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; import Group from '../entity/group.entity'; import GroupRole from '../entity/groupRole.entity'; import { BaseRepository } from './base.repository'; +import { TENANT_CONNECTION } from '../../database/database.constants'; @Injectable() export class GroupRoleRepository extends BaseRepository { - constructor(private dataSource: DataSource) { + constructor( + @Inject(TENANT_CONNECTION) + private dataSource: DataSource, + ) { super(GroupRole, dataSource); } async getGroupCountForRoleId(roleId: string): Promise { - return (await this.getQueryBuilder('groupRole')) + return this.createQueryBuilder('groupRole') .innerJoinAndSelect(Group, 'group', 'group.id = groupRole.groupId') .where('groupRole.roleId= :roleId', { roleId }) .getCount(); diff --git a/src/authorization/repository/permission.repository.ts b/src/authorization/repository/permission.repository.ts index d80392f..ac69c41 100644 --- a/src/authorization/repository/permission.repository.ts +++ b/src/authorization/repository/permission.repository.ts @@ -50,7 +50,7 @@ export class PermissionRepository extends BaseRepository { } async getPermissionsByRoleId(roleId: string): Promise { - return (await this.getQueryBuilder('permission')) + return this.createQueryBuilder('permission') .leftJoinAndSelect( RolePermission, 'rolePermission', @@ -61,7 +61,7 @@ export class PermissionRepository extends BaseRepository { } async getPermissionsByGroupId(groupId: string): Promise { - return (await this.getQueryBuilder('permission')) + return this.createQueryBuilder('permission') .leftJoinAndSelect( GroupPermission, 'groupPermission', @@ -72,7 +72,7 @@ export class PermissionRepository extends BaseRepository { } async getPermissionsByEntityId(entityId: string): Promise { - return (await this.getQueryBuilder('permission')) + return this.createQueryBuilder('permission') .leftJoinAndSelect( EntityPermission, 'entityPermission', @@ -85,7 +85,7 @@ export class PermissionRepository extends BaseRepository { } async getPermissionsByUserId(userId: string): Promise { - return (await this.getQueryBuilder('permission')) + return this.createQueryBuilder('permission') .leftJoinAndSelect( UserPermission, 'userPermission', @@ -98,7 +98,7 @@ export class PermissionRepository extends BaseRepository { async getGroupRolePermissionsByGroupId( groupId: string, ): Promise { - return (await this.getQueryBuilder('permission')) + return this.createQueryBuilder('permission') .innerJoin( RolePermission, 'rolePermission', diff --git a/src/authorization/repository/role.repository.ts b/src/authorization/repository/role.repository.ts index 60c355b..3d9e002 100644 --- a/src/authorization/repository/role.repository.ts +++ b/src/authorization/repository/role.repository.ts @@ -1,13 +1,17 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { UpdateRoleInput } from 'src/schema/graphql.schema'; import { DataSource, In } from 'typeorm'; import GroupRole from '../entity/groupRole.entity'; import Role from '../entity/role.entity'; import { BaseRepository } from './base.repository'; +import { TENANT_CONNECTION } from '../../database/database.constants'; @Injectable() export class RoleRepository extends BaseRepository { - constructor(private dataSource: DataSource) { + constructor( + @Inject(TENANT_CONNECTION) + private dataSource: DataSource, + ) { super(Role, dataSource); } @@ -29,7 +33,7 @@ export class RoleRepository extends BaseRepository { } async getRolesForGroupId(groupId: string): Promise { - return (await this.getQueryBuilder('role')) + return this.createQueryBuilder('role') .leftJoinAndSelect(GroupRole, 'groupRole', 'role.id = groupRole.roleId') .where('groupRole.groupId = :groupId', { groupId }) .getMany(); diff --git a/src/authorization/repository/user.repository.ts b/src/authorization/repository/user.repository.ts index ed699fe..8769721 100644 --- a/src/authorization/repository/user.repository.ts +++ b/src/authorization/repository/user.repository.ts @@ -1,13 +1,17 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { UpdateUserInput } from 'src/schema/graphql.schema'; import { DataSource, In } from 'typeorm'; import User from '../entity/user.entity'; import UserGroup from '../entity/userGroup.entity'; import { BaseRepository } from './base.repository'; +import { TENANT_CONNECTION } from '../../database/database.constants'; @Injectable() export class UserRepository extends BaseRepository { - constructor(private dataSource: DataSource) { + constructor( + @Inject(TENANT_CONNECTION) + private dataSource: DataSource, + ) { super(User, dataSource); } @@ -16,7 +20,7 @@ export class UserRepository extends BaseRepository { } async getUserByEmail(email: string) { - return (await this.getQueryBuilder('user')) + return this.createQueryBuilder('user') .where('lower(user.email) = lower(:email)', { email }) .getOne(); } @@ -42,14 +46,14 @@ export class UserRepository extends BaseRepository { } async getUsersByGroupId(groupId: string): Promise { - return (await this.getQueryBuilder('user')) + return this.createQueryBuilder('user') .leftJoinAndSelect(UserGroup, 'userGroup', 'userGroup.userId = user.id') .where('userGroup.groupId = :groupId', { groupId }) .getMany(); } async getUserCountForGroupId(groupId: string): Promise { - return (await this.getQueryBuilder('user')) + return this.createQueryBuilder('user') .innerJoinAndSelect(UserGroup, 'userGroup', 'userGroup.userId = user.id') .where('userGroup.groupId = :groupId', { groupId }) .getCount(); diff --git a/src/authorization/repository/userPermission.repository.ts b/src/authorization/repository/userPermission.repository.ts index c882b9b..cbaa187 100644 --- a/src/authorization/repository/userPermission.repository.ts +++ b/src/authorization/repository/userPermission.repository.ts @@ -1,11 +1,15 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; import UserPermission from '../entity/userPermission.entity'; import { BaseRepository } from './base.repository'; +import { TENANT_CONNECTION } from '../../database/database.constants'; @Injectable() export class UserPermissionRepository extends BaseRepository { - constructor(private dataSource: DataSource) { + constructor( + @Inject(TENANT_CONNECTION) + private dataSource: DataSource, + ) { super(UserPermission, dataSource); } diff --git a/src/authorization/service/entity.service.ts b/src/authorization/service/entity.service.ts index 648b91c..9344b52 100644 --- a/src/authorization/service/entity.service.ts +++ b/src/authorization/service/entity.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { NewEntityInput, @@ -14,8 +14,8 @@ import { EntityModelRepository } from '../repository/entity.repository'; import { EntityPermissionRepository } from '../repository/entityPermission.repository'; import { PermissionRepository } from '../repository/permission.repository'; import { EntityServiceInterface } from './entity.service.interface'; -import { getConnection } from '../../util/database.connection'; import { ExecutionManager } from '../../util/execution.manager'; +import { TENANT_CONNECTION } from '../../database/database.constants'; @Injectable() export class EntityService implements EntityServiceInterface { @@ -23,6 +23,7 @@ export class EntityService implements EntityServiceInterface { private entityRepository: EntityModelRepository, private entityPermissionRepository: EntityPermissionRepository, private permissionRepository: PermissionRepository, + @Inject(TENANT_CONNECTION) private dataSource: DataSource, ) {} @@ -62,8 +63,7 @@ export class EntityService implements EntityServiceInterface { if (!existingEntity) { throw new EntityNotFoundException(id); } - const entityManager = (await getConnection()).manager; - await entityManager.transaction(async (entityManager) => { + await this.dataSource.manager.transaction(async (entityManager) => { const entityRepo = entityManager.getRepository(EntityModel); const entityPermissionRepo = entityManager.getRepository( EntityPermission, @@ -114,8 +114,7 @@ export class EntityService implements EntityServiceInterface { })), ); - const entityManager = (await getConnection()).manager; - await entityManager.transaction(async (entityManager) => { + await this.dataSource.manager.transaction(async (entityManager) => { const entityPermissionsRepo = entityManager.getRepository( EntityPermission, ); diff --git a/src/authorization/service/group.service.ts b/src/authorization/service/group.service.ts index 1e43f2f..0f9be29 100644 --- a/src/authorization/service/group.service.ts +++ b/src/authorization/service/group.service.ts @@ -42,7 +42,7 @@ import { UserCacheServiceInterface } from './usercache.service.interface'; import { DUPLICATE_ERROR_CODE } from '../../constants/db.error.constants'; import { LoggerService } from '../../logger/logger.service'; import { ExecutionManager } from '../../util/execution.manager'; -import { getConnection } from '../../util/database.connection'; +import { TENANT_CONNECTION } from '../../database/database.constants'; @Injectable() export class GroupService implements GroupServiceInterface { @@ -54,6 +54,7 @@ export class GroupService implements GroupServiceInterface { private groupRoleRepository: GroupRoleRepository, private rolesRepository: RoleRepository, private userRepository: UserRepository, + @Inject(TENANT_CONNECTION) private dataSource: DataSource, @Inject(GroupCacheServiceInterface) private groupCacheService: GroupCacheServiceInterface, @@ -75,7 +76,7 @@ export class GroupService implements GroupServiceInterface { ['name', 'group.name'], ['updatedAt', 'group.updated_at'], ]); - let queryBuilder = await this.groupRepository.getQueryBuilder('group'); + let queryBuilder = this.groupRepository.createQueryBuilder('group'); if (input?.search) { queryBuilder = this.searchService.generateSearchTermForEntity( @@ -171,7 +172,7 @@ export class GroupService implements GroupServiceInterface { throw new GroupDeleteNotAllowedException(); } - await (await getConnection()).manager.transaction(async (entityManager) => { + await this.dataSource.manager.transaction(async (entityManager) => { const groupRepo = entityManager.getRepository(Group); const groupRoleRepo = entityManager.getRepository(GroupRole); const groupPermissionRepo = entityManager.getRepository(GroupPermission); @@ -223,7 +224,7 @@ export class GroupService implements GroupServiceInterface { })), ); - await (await getConnection()).manager.transaction(async (entityManager) => { + await this.dataSource.manager.transaction(async (entityManager) => { const groupPermissionsRepo = entityManager.getRepository(GroupPermission); await groupPermissionsRepo.remove(permissionsToBeRemovedFromGroup); await groupPermissionsRepo.save(groupPermission); @@ -252,7 +253,7 @@ export class GroupService implements GroupServiceInterface { userIds.map((userId) => ({ userId: userId, groupId: id })), ); - await (await getConnection()).manager.transaction(async (entityManager) => { + await this.dataSource.manager.transaction(async (entityManager) => { const userGroupsRepo = entityManager.getRepository(UserGroup); await userGroupsRepo.remove(usersToBeRemovedFromGroup); await userGroupsRepo.save(userGroups); @@ -322,7 +323,7 @@ export class GroupService implements GroupServiceInterface { })), ); - await (await getConnection()).manager.transaction(async (entityManager) => { + await this.dataSource.manager.transaction(async (entityManager) => { const groupRolesRepo = entityManager.getRepository(GroupRole); await groupRolesRepo.remove(rolesToBeRemovedFromGroup); await groupRolesRepo.save(groupRoles); diff --git a/src/authorization/service/role.service.ts b/src/authorization/service/role.service.ts index 62de6e9..d0dec5e 100644 --- a/src/authorization/service/role.service.ts +++ b/src/authorization/service/role.service.ts @@ -24,8 +24,7 @@ import { RoleServiceInterface } from './role.service.interface'; import { RoleCacheServiceInterface } from './rolecache.service.interface'; import SearchService from './search.service'; import { ExecutionManager } from '../../util/execution.manager'; -import { getConnection } from '../../util/database.connection'; - +import { TENANT_CONNECTION } from '../../database/database.constants'; @Injectable() export class RoleService implements RoleServiceInterface { constructor( @@ -35,6 +34,7 @@ export class RoleService implements RoleServiceInterface { private groupRoleRepository: GroupRoleRepository, private rolePermissionRepository: RolePermissionRepository, private permissionRepository: PermissionRepository, + @Inject(TENANT_CONNECTION) private dataSource: DataSource, private searchService: SearchService, ) {} @@ -44,7 +44,7 @@ export class RoleService implements RoleServiceInterface { ['name', 'role.name'], ['updatedAt', 'role.updated_at'], ]); - let queryBuilder = await this.rolesRepository.getQueryBuilder('role'); + let queryBuilder = this.rolesRepository.createQueryBuilder('role'); if (input?.search) { queryBuilder = this.searchService.generateSearchTermForEntity( queryBuilder, @@ -107,7 +107,7 @@ export class RoleService implements RoleServiceInterface { throw new RoleDeleteNotAllowedException(); } - await (await getConnection()).manager.transaction(async (entityManager) => { + await this.dataSource.manager.transaction(async (entityManager) => { const rolePermissionsRepo = entityManager.getRepository(RolePermission); const roleRepo = entityManager.getRepository(Role); await rolePermissionsRepo.softDelete({ roleId: id }); @@ -156,7 +156,7 @@ export class RoleService implements RoleServiceInterface { })), ); - await (await getConnection()).manager.transaction(async (entityManager) => { + await this.dataSource.manager.transaction(async (entityManager) => { const rolePermissionsRepo = entityManager.getRepository(RolePermission); await rolePermissionsRepo.remove(permissionsToBeRemovedFromRole); await rolePermissionsRepo.save(rolePermissions); diff --git a/src/authorization/service/user.service.ts b/src/authorization/service/user.service.ts index 00dd66e..e7984bb 100644 --- a/src/authorization/service/user.service.ts +++ b/src/authorization/service/user.service.ts @@ -1,5 +1,5 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { DataSource, EntityManager, SelectQueryBuilder } from 'typeorm'; +import { DataSource, SelectQueryBuilder } from 'typeorm'; import { UserNotAuthorized } from '../../authentication/exception/userauth.exception'; import { FilterBuilder } from '../../common/filter.builder'; import { SearchEntity } from '../../constants/search.entity.enum'; @@ -33,7 +33,7 @@ import SearchService from './search.service'; import { UserServiceInterface } from './user.service.interface'; import { UserCacheServiceInterface } from './usercache.service.interface'; import { ExecutionManager } from '../../util/execution.manager'; -import { getConnection } from '../../util/database.connection'; +import { TENANT_CONNECTION } from '../../database/database.constants'; @Injectable() export class UserService implements UserServiceInterface { @@ -49,6 +49,7 @@ export class UserService implements UserServiceInterface { private groupCacheService: GroupCacheServiceInterface, @Inject(PermissionCacheServiceInterface) private permissionCacheService: PermissionCacheServiceInterface, + @Inject(TENANT_CONNECTION) private dataSource: DataSource, private searchService: SearchService, @Inject(RoleCacheServiceInterface) @@ -75,7 +76,7 @@ export class UserService implements UserServiceInterface { ); } }; - const qb = await this.userRepository.getQueryBuilder('user'); + const qb = this.userRepository.createQueryBuilder('user'); if (input?.search) { this.searchService.generateSearchTermForEntity( qb, @@ -157,7 +158,7 @@ export class UserService implements UserServiceInterface { user.groups.map((group) => ({ userId: id, groupId: group })), ); - await (await getConnection()).manager.transaction(async (entityManager) => { + await this.dataSource.manager.transaction(async (entityManager) => { const userGroupsRepo = entityManager.getRepository(UserGroup); await userGroupsRepo.remove(groupsToBeRemovedFromUser); await userGroupsRepo.save(userGroups); @@ -202,7 +203,7 @@ export class UserService implements UserServiceInterface { })), ); - const userPermissionsUpdated = await (await getConnection()).transaction( + const userPermissionsUpdated = await this.dataSource.manager.transaction( async (entityManager) => { const userPermissionsRepo = entityManager.getRepository(UserPermission); await userPermissionsRepo.remove(userPermissionsToBeRemoved); @@ -228,7 +229,7 @@ export class UserService implements UserServiceInterface { throw new UserNotFoundException(id); } - await (await getConnection()).manager.transaction(async (entityManager) => { + await this.dataSource.manager.transaction(async (entityManager) => { const userRepo = entityManager.getRepository(User); const userGroupRepo = entityManager.getRepository(UserGroup); const userPermissionRepo = entityManager.getRepository(UserPermission); @@ -371,7 +372,7 @@ export class UserService implements UserServiceInterface { 'Username should be provided with email or phone', ); } - let query = await this.userRepository.getQueryBuilder('user'); + let query = this.userRepository.createQueryBuilder('user'); if (email) { query = query.orWhere('lower(user.email) = lower(:email)', { email: nullCheckedEmail, diff --git a/src/database/database.constants.ts b/src/database/database.constants.ts new file mode 100644 index 0000000..03a06c4 --- /dev/null +++ b/src/database/database.constants.ts @@ -0,0 +1 @@ +export const TENANT_CONNECTION = 'TENANT_CONNECTION'; diff --git a/src/database/database.module.ts b/src/database/database.module.ts index 085a794..ec48018 100644 --- a/src/database/database.module.ts +++ b/src/database/database.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; +import { databaseProvider } from './database.provider'; @Module({ imports: [ @@ -25,5 +26,7 @@ import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; }), }), ], + exports: [databaseProvider], + providers: [databaseProvider], }) export class DatabaseModule {} diff --git a/src/database/database.provider.ts b/src/database/database.provider.ts new file mode 100644 index 0000000..c90a6a2 --- /dev/null +++ b/src/database/database.provider.ts @@ -0,0 +1,68 @@ +import { Scope } from '@nestjs/common'; +import { REQUEST } from '@nestjs/core'; +import { DataSource, getConnectionManager } from 'typeorm'; +import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; +import { ExecutionManager } from '../util/execution.manager'; +import { TENANT_CONNECTION } from './database.constants'; +import { LoggerService } from 'src/logger/logger.service'; + +export const databaseProvider = { + provide: TENANT_CONNECTION, + scope: Scope.REQUEST, + useFactory: async () => { + const tenantId = ExecutionManager.getTenantId(); + + if (getConnectionManager().has(tenantId)) { + const con = getConnectionManager().get(tenantId); + const existingConnection = await Promise.resolve( + con.isConnected ? con : con.connect(), + ); + await switchToTenant(tenantId, existingConnection); + + return existingConnection; + } + + const newConnection: DataSource = await new DataSource({ + name: tenantId, + type: 'postgres', + host: process.env.POSTGRES_HOST, + port: Number(process.env.POSTGRES_PORT), + username: process.env.POSTGRES_TENANT_USER, + password: process.env.POSTGRES_TENANT_PASSWORD, + database: process.env.POSTGRES_DB, + entities: [ + __dirname + '/../**/*.entity.ts', + __dirname + '/../**/*.entity.js', + ], + synchronize: false, + logging: ['error'], + namingStrategy: new SnakeNamingStrategy(), + ...(process.env.POSTGRES_TENANT_MAX_CONNECTION_LIMIT + ? { extra: { max: process.env.POSTGRES_TENANT_MAX_CONNECTION_LIMIT } } + : {}), + }).initialize(); + + await switchToTenant(tenantId, newConnection); + + return newConnection; + }, + inject: [REQUEST], +}; + +const switchToTenant = async ( + tenantId: string, + connection: DataSource, +): Promise => { + try { + await connection.query(`select set_config('app.tenant_id', $1, false)`, [ + tenantId, + ]); + } catch (error) { + const logger = LoggerService.getInstance('bootstrap()'); + logger.error( + `Failed to switch to tenant: ${tenantId}, error: ${JSON.stringify( + error, + )}]`, + ); + } +}; diff --git a/src/util/database.connection.ts b/src/util/database.connection.ts deleted file mode 100644 index 294e821..0000000 --- a/src/util/database.connection.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { getConnectionManager, DataSource } from 'typeorm'; -import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; -import { LoggerService } from '../logger/logger.service'; -import { ExecutionManager } from './execution.manager'; - -/** - * Get connection based on the logged in tenant - * @returns connection - */ - -export async function getConnection(): Promise { - const tenantName = ExecutionManager.getTenantId(); - return getConnectionForTenant(tenantName); -} - -export async function getConnectionForTenant( - tenantId: string, -): Promise { - if (getConnectionManager().has(tenantId)) { - const con = getConnectionManager().get(tenantId); - const existingConnection = await Promise.resolve( - con.isConnected ? con : con.connect(), - ); - await switchToTenant(tenantId, existingConnection); - - return existingConnection; - } - - const newConnection: DataSource = await new DataSource({ - name: tenantId, - type: 'postgres', - host: process.env.POSTGRES_HOST, - port: Number(process.env.POSTGRES_PORT), - username: process.env.POSTGRES_TENANT_USER, - password: process.env.POSTGRES_TENANT_PASSWORD, - database: process.env.POSTGRES_DB, - entities: [ - __dirname + '/../**/*.entity.ts', - __dirname + '/../**/*.entity.js', - ], - synchronize: false, - logging: ['error'], - namingStrategy: new SnakeNamingStrategy(), - ...(process.env.POSTGRES_TENANT_MAX_CONNECTION_LIMIT - ? { extra: { max: process.env.POSTGRES_TENANT_MAX_CONNECTION_LIMIT } } - : {}), - }).initialize(); - - await switchToTenant(tenantId, newConnection); - - return newConnection; -} - -const switchToTenant = async ( - tenantId: string, - connection: DataSource, -): Promise => { - try { - await connection.query(`select set_config('app.tenant_id', $1, false)`, [ - tenantId, - ]); - } catch (error) { - const logger = LoggerService.getInstance('bootstrap()'); - logger.error( - `Failed to switch to tenant: ${tenantId}, error: ${JSON.stringify( - error, - )}]`, - ); - } -}; - -export async function getConnectionAsOwner(): Promise { - if (!getConnectionManager().has('Owner')) - return await new DataSource({ - name: 'Owner', - type: 'postgres', - host: process.env.POSTGRES_HOST, - port: Number(process.env.POSTGRES_PORT), - username: process.env.POSTGRES_ADMIN_USER, - password: process.env.POSTGRES_ADMIN_PASSWORD, - database: process.env.POSTGRES_DB, - entities: [__dirname + '/../**/*.entity.js'], - synchronize: false, - logging: ['error'], - namingStrategy: new SnakeNamingStrategy(), - migrationsTableName: 'migrations', - migrations: [__dirname + '/../migrations/*.js'], - ...(process.env.POSTGRES_ADMIN_MAX_CONNECTION_LIMIT - ? { extra: { max: process.env.POSTGRES_ADMIN_MAX_CONNECTION_LIMIT } } - : {}), - }).initialize(); - const con = getConnectionManager().get('Owner'); - const existingConnection = await Promise.resolve( - con.isConnected ? con : con.connect(), - ); - return existingConnection; -} diff --git a/test/authentication/service/passwordauth.service.test.ts b/test/authentication/service/passwordauth.service.test.ts index 47d42aa..d23c052 100644 --- a/test/authentication/service/passwordauth.service.test.ts +++ b/test/authentication/service/passwordauth.service.test.ts @@ -8,6 +8,7 @@ import { TokenService } from '../../../src/authentication/service/token.service' import User from '../../../src/authorization/entity/user.entity'; import { UserServiceInterface } from '../../../src/authorization/service/user.service.interface'; import { Status } from '../../../src/schema/graphql.schema'; +import { TENANT_CONNECTION } from '../../../src/database/database.constants'; let users: User[] = [ { @@ -51,6 +52,10 @@ describe('test PasswordAuthService', () => { provide: DataSource, useValue: mockDataSource, }, + { + provide: TENANT_CONNECTION, + useValue: mockDataSource, + }, PasswordAuthService, AuthenticationHelper, ], diff --git a/test/authorization/repository/entity.repository.test.ts b/test/authorization/repository/entity.repository.test.ts index 1b167bf..fdd3d18 100644 --- a/test/authorization/repository/entity.repository.test.ts +++ b/test/authorization/repository/entity.repository.test.ts @@ -2,13 +2,13 @@ import { Test } from '@nestjs/testing'; import { Entity, UpdateEntityInput } from 'src/schema/graphql.schema'; import { DataSource, UpdateResult } from 'typeorm'; import { EntityModelRepository } from '../../../src/authorization/repository/entity.repository'; +import { TENANT_CONNECTION } from '../../../src/database/database.constants'; const VALID_ENTITY_ID = 'ae032b1b-cc3c-4e44-9197-276ca877a7f8'; const entity: Entity = { id: VALID_ENTITY_ID, name: 'Test Entity 1', - tenantId: '1ef2a357-d4b7-4a30-88ca-d1cc627f2994', }; const updateResult: UpdateResult = { @@ -34,6 +34,10 @@ describe('test Entity model repository', () => { provide: DataSource, useValue: mockDataSource, }, + { + provide: TENANT_CONNECTION, + useValue: mockDataSource, + }, ], }).compile(); diff --git a/test/authorization/repository/group.repository.test.ts b/test/authorization/repository/group.repository.test.ts index cf4eb69..6eac46e 100644 --- a/test/authorization/repository/group.repository.test.ts +++ b/test/authorization/repository/group.repository.test.ts @@ -5,6 +5,7 @@ import * as typeorm from 'typeorm'; import { DataSource, UpdateResult } from 'typeorm'; import UserGroup from '../../../src/authorization/entity/userGroup.entity'; import { GroupRepository } from '../../../src/authorization/repository/group.repository'; +import { TENANT_CONNECTION } from '../../../src/database/database.constants'; const VALID_GROUP_ID = 'ae032b1b-cc3c-4e44-9197-276ca877a7f8'; @@ -39,6 +40,10 @@ describe('test Group repository', () => { provide: DataSource, useValue: mockDataSource, }, + { + provide: TENANT_CONNECTION, + useValue: mockDataSource, + }, ], }).compile(); @@ -48,7 +53,7 @@ describe('test Group repository', () => { updateMock = groupRepository.update = jest.fn(); findMock = groupRepository.find = jest.fn(); - createQueryBuilderMock = groupRepository.getQueryBuilder = jest + createQueryBuilderMock = groupRepository.createQueryBuilder = jest .fn() .mockReturnValue({ leftJoinAndSelect: jest.fn().mockReturnThis(), diff --git a/test/authorization/repository/groupPermission.repository.test.ts b/test/authorization/repository/groupPermission.repository.test.ts index 03fc4b8..e684e85 100644 --- a/test/authorization/repository/groupPermission.repository.test.ts +++ b/test/authorization/repository/groupPermission.repository.test.ts @@ -2,6 +2,7 @@ import { Test } from '@nestjs/testing'; import GroupPermission from 'src/authorization/entity/groupPermission.entity'; import { DataSource } from 'typeorm'; import { GroupPermissionRepository } from '../../../src/authorization/repository/groupPermission.repository'; +import { TENANT_CONNECTION } from '../../../src/database/database.constants'; const VALID_PERMISSION_ID = 'ae032b1b-cc3c-4e44-9197-276ca877a7f8'; const VALID_GROUP_ID = '3282163d-fd5a-4026-b912-1a9cc5eefc98'; @@ -30,6 +31,10 @@ describe('test GroupPermission repository', () => { provide: DataSource, useValue: mockDataSource, }, + { + provide: TENANT_CONNECTION, + useValue: mockDataSource, + }, ], }).compile(); diff --git a/test/authorization/repository/groupRole.repository.test.ts b/test/authorization/repository/groupRole.repository.test.ts index 1748f7a..204ea5c 100644 --- a/test/authorization/repository/groupRole.repository.test.ts +++ b/test/authorization/repository/groupRole.repository.test.ts @@ -2,6 +2,7 @@ import { Test } from '@nestjs/testing'; import { DataSource } from 'typeorm'; import Group from '../../../src/authorization/entity/group.entity'; import { GroupRoleRepository } from '../../../src/authorization/repository/groupRole.repository'; +import { TENANT_CONNECTION } from '../../../src/database/database.constants'; const VALID_ROLE_ID = 'ae032b1b-cc3c-4e44-9197-276ca877a7f8'; @@ -21,12 +22,16 @@ describe('test GroupRole repository', () => { provide: DataSource, useValue: mockDataSource, }, + { + provide: TENANT_CONNECTION, + useValue: mockDataSource, + }, ], }).compile(); groupRoleRepository = moduleRef.get(GroupRoleRepository); - createQueryBuilderMock = groupRoleRepository.getQueryBuilder = jest + createQueryBuilderMock = groupRoleRepository.createQueryBuilder = jest .fn() .mockReturnValue({ innerJoinAndSelect: jest.fn().mockReturnThis(), diff --git a/test/authorization/repository/permission.repository.test.ts b/test/authorization/repository/permission.repository.test.ts index 0b8f859..579cc93 100644 --- a/test/authorization/repository/permission.repository.test.ts +++ b/test/authorization/repository/permission.repository.test.ts @@ -60,7 +60,7 @@ describe('test Permission repository', () => { updateMock = permissionRepository.update = jest.fn(); softDeleteMock = permissionRepository.softDelete = jest.fn(); - createQueryBuilderMock = permissionRepository.getQueryBuilder = jest + createQueryBuilderMock = permissionRepository.createQueryBuilder = jest .fn() .mockReturnValue({ innerJoin: jest.fn().mockReturnThis(), diff --git a/test/authorization/repository/role.repository.test.ts b/test/authorization/repository/role.repository.test.ts index 2a6e28c..c4abed5 100644 --- a/test/authorization/repository/role.repository.test.ts +++ b/test/authorization/repository/role.repository.test.ts @@ -4,6 +4,7 @@ import { UpdateRoleInput } from 'src/schema/graphql.schema'; import { DataSource, In, UpdateResult } from 'typeorm'; import GroupRole from '../../../src/authorization/entity/groupRole.entity'; import { RoleRepository } from '../../../src/authorization/repository/role.repository'; +import { TENANT_CONNECTION } from '../../../src/database/database.constants'; const VALID_ROLE_ID = 'ae032b1b-cc3c-4e44-9197-276ca877a7f8'; @@ -38,6 +39,10 @@ describe('test Role repository', () => { provide: DataSource, useValue: mockDataSource, }, + { + provide: TENANT_CONNECTION, + useValue: mockDataSource, + }, ], }).compile(); @@ -47,7 +52,7 @@ describe('test Role repository', () => { updateMock = roleRepository.update = jest.fn(); findMock = roleRepository.find = jest.fn(); - createQueryBuilderMock = roleRepository.getQueryBuilder = jest + createQueryBuilderMock = roleRepository.createQueryBuilder = jest .fn() .mockReturnValue({ leftJoinAndSelect: jest.fn().mockReturnThis(), diff --git a/test/authorization/repository/rolePermission.repository.test.ts b/test/authorization/repository/rolePermission.repository.test.ts index 95b6b32..6088673 100644 --- a/test/authorization/repository/rolePermission.repository.test.ts +++ b/test/authorization/repository/rolePermission.repository.test.ts @@ -2,6 +2,7 @@ import { Test } from '@nestjs/testing'; import RolePermission from 'src/authorization/entity/rolePermission.entity'; import { DataSource } from 'typeorm'; import { RolePermissionRepository } from '../../../src/authorization/repository/rolePermission.repository'; +import { TENANT_CONNECTION } from '../../../src/database/database.constants'; describe('test RolePermission repository', () => { let rolePermissionRepository: RolePermissionRepository; @@ -32,6 +33,10 @@ describe('test RolePermission repository', () => { provide: DataSource, useValue: mockDataSource, }, + { + provide: TENANT_CONNECTION, + useValue: mockDataSource, + }, ], }).compile(); diff --git a/test/authorization/repository/user.repository.test.ts b/test/authorization/repository/user.repository.test.ts index f1801b7..6b967b7 100644 --- a/test/authorization/repository/user.repository.test.ts +++ b/test/authorization/repository/user.repository.test.ts @@ -4,6 +4,7 @@ import { DataSource, In, UpdateResult } from 'typeorm'; import UserGroup from '../../../src/authorization/entity/userGroup.entity'; import { UserRepository } from '../../../src/authorization/repository/user.repository'; import { Status, UpdateUserInput } from '../../../src/schema/graphql.schema'; +import { TENANT_CONNECTION } from '../../../src/database/database.constants'; const VALID_USER_ID = 'ae032b1b-cc3c-4e44-9197-276ca877a7f8'; const VALID_EMAIL = 'test@valid.com'; @@ -49,6 +50,10 @@ describe('test User repository', () => { provide: DataSource, useValue: mockDataSource, }, + { + provide: TENANT_CONNECTION, + useValue: mockDataSource, + }, ], }).compile(); @@ -59,7 +64,7 @@ describe('test User repository', () => { updateMock = userRepository.update = jest.fn(); findMock = userRepository.find = jest.fn(); - createQueryBuilderMock = userRepository.getQueryBuilder = jest + createQueryBuilderMock = userRepository.createQueryBuilder = jest .fn() .mockReturnValue({ leftJoinAndSelect: jest.fn().mockReturnThis(), diff --git a/test/authorization/repository/userGroup.repository.test.ts b/test/authorization/repository/userGroup.repository.test.ts index 156d102..6a1c4bc 100644 --- a/test/authorization/repository/userGroup.repository.test.ts +++ b/test/authorization/repository/userGroup.repository.test.ts @@ -2,6 +2,7 @@ import { Test } from '@nestjs/testing'; import UserGroup from 'src/authorization/entity/userGroup.entity'; import { DataSource } from 'typeorm'; import { UserGroupRepository } from '../../../src/authorization/repository/userGroup.repository'; +import { TENANT_CONNECTION } from '../../../src/database/database.constants'; const VALID_USER_ID = 'ccecef4f-58d3-477b-87ee-847ee22efe4d'; const VALID_GROUP_ID = '3282163d-fd5a-4026-b912-1a9cc5eefc98'; @@ -29,6 +30,10 @@ describe('test UserGroup repository', () => { provide: DataSource, useValue: mockDataSource, }, + { + provide: TENANT_CONNECTION, + useValue: mockDataSource, + }, ], }).compile(); diff --git a/test/authorization/repository/userPermission.repository.test.ts b/test/authorization/repository/userPermission.repository.test.ts index ede2a41..1f09c8e 100644 --- a/test/authorization/repository/userPermission.repository.test.ts +++ b/test/authorization/repository/userPermission.repository.test.ts @@ -2,6 +2,7 @@ import { Test } from '@nestjs/testing'; import UserPermission from 'src/authorization/entity/userPermission.entity'; import { DataSource } from 'typeorm'; import { UserPermissionRepository } from '../../../src/authorization/repository/userPermission.repository'; +import { TENANT_CONNECTION } from '../../../src/database/database.constants'; const VALID_PERMISSION_ID = 'ae032b1b-cc3c-4e44-9197-276ca877a7f8'; @@ -29,6 +30,10 @@ describe('test UserPermission repository', () => { provide: DataSource, useValue: mockDataSource, }, + { + provide: TENANT_CONNECTION, + useValue: mockDataSource, + }, ], }).compile(); diff --git a/test/authorization/service/group.service.test.ts b/test/authorization/service/group.service.test.ts index bdb57fb..310ec58 100644 --- a/test/authorization/service/group.service.test.ts +++ b/test/authorization/service/group.service.test.ts @@ -26,6 +26,7 @@ import { Status, UpdateGroupInput, } from '../../../src/schema/graphql.schema'; +import { TENANT_CONNECTION } from '../../../src/database/database.constants'; const VALID_GROUP_ID = '3282163d-fd5a-4026-b912-1a9cc5eefc98'; const groups: Group[] = [ @@ -162,6 +163,10 @@ describe('test Group Service', () => { provide: DataSource, useValue: mockDataSource, }, + { + provide: TENANT_CONNECTION, + useValue: mockDataSource, + }, // { // provide: DataSource, // useFactory: dataSourceMockFactory, @@ -175,7 +180,7 @@ describe('test Group Service', () => { userGroupRepository = moduleRef.get(UserGroupRepository); dataSource = moduleRef.get(DataSource); - createQueryBuilderMock = groupRepository.getQueryBuilder = jest + createQueryBuilderMock = groupRepository.createQueryBuilder = jest .fn() .mockReturnValue({ leftJoinAndSelect: jest.fn().mockReturnThis(), diff --git a/test/authorization/service/permission.service.test.ts b/test/authorization/service/permission.service.test.ts index 9bcbb8e..9993988 100644 --- a/test/authorization/service/permission.service.test.ts +++ b/test/authorization/service/permission.service.test.ts @@ -15,6 +15,7 @@ import { NewPermissionInput, UpdatePermissionInput, } from '../../../src/schema/graphql.schema'; +import { TENANT_CONNECTION } from '../../../src/database/database.constants'; const VALID_PERMISSION_ID = 'ae032b1b-cc3c-4e44-9197-276ca877a7f8'; const INVALID_PERMISSION_ID = 'ae032b1b-cc3c-4e44-9197-276ca877a7f9'; @@ -59,6 +60,10 @@ describe('test Permission service', () => { provide: PermissionCacheServiceInterface, useValue: mockPermissionCacheService, }, + { + provide: TENANT_CONNECTION, + useValue: mockDataSource, + }, ], }).compile(); diff --git a/test/authorization/service/role.service.test.ts b/test/authorization/service/role.service.test.ts index 7981072..45a0b51 100644 --- a/test/authorization/service/role.service.test.ts +++ b/test/authorization/service/role.service.test.ts @@ -24,6 +24,7 @@ import { NewRoleInput, UpdateRoleInput, } from '../../../src/schema/graphql.schema'; +import { TENANT_CONNECTION } from '../../../src/database/database.constants'; const VALID_ROLE_ID = 'ae032b1b-cc3c-4e44-9197-276ca877a7f8'; const INVALID_ROLE_ID = 'ae032b1b-cc3c-4e44-9197-276ca877a7f9'; @@ -96,6 +97,10 @@ describe('test Role Service', () => { provide: DataSource, useValue: mockDataSource, }, + { + provide: TENANT_CONNECTION, + useValue: mockDataSource, + }, ], }).compile(); @@ -103,7 +108,7 @@ describe('test Role Service', () => { roleRepository = moduleRef.get(RoleRepository); permissionRepository = moduleRef.get(PermissionRepository); - createQueryBuilderMock = roleRepository.getQueryBuilder = jest + createQueryBuilderMock = roleRepository.createQueryBuilder = jest .fn() .mockReturnValue({ leftJoinAndSelect: jest.fn().mockReturnThis(), diff --git a/test/authorization/service/user.service.test.ts b/test/authorization/service/user.service.test.ts index 54510f5..eda2ba4 100644 --- a/test/authorization/service/user.service.test.ts +++ b/test/authorization/service/user.service.test.ts @@ -23,6 +23,7 @@ import { UserServiceInterface } from '../../../src/authorization/service/user.se import { UserCacheServiceInterface } from '../../../src/authorization/service/usercache.service.interface'; import { RedisCacheService } from '../../../src/cache/redis-cache/redis-cache.service'; import { Status } from '../../../src/schema/graphql.schema'; +import { TENANT_CONNECTION } from '../../../src/database/database.constants'; const users: User[] = [ { @@ -142,6 +143,10 @@ describe('test UserService', () => { provide: DataSource, useValue: mockDataSource, }, + { + provide: TENANT_CONNECTION, + useValue: mockDataSource, + }, ], }).compile(); userService = moduleRef.get(UserService); @@ -151,7 +156,7 @@ describe('test UserService', () => { saveMock = userRepository.save = jest.fn(); updateUserByIdMock = userRepository.updateUserById = jest.fn(); - createQueryBuilderMock = userRepository.getQueryBuilder = jest + createQueryBuilderMock = userRepository.createQueryBuilder = jest .fn() .mockReturnValue({ leftJoinAndSelect: jest.fn().mockReturnThis(), From 12e0a8eff3732bfc4f2007aa83164e6c20140ebb Mon Sep 17 00:00:00 2001 From: Bharath Date: Tue, 17 Dec 2024 12:16:17 +0530 Subject: [PATCH 19/23] REFACTOR: add util function for extracting token --- src/authentication/authentication.guard.ts | 3 ++- src/authentication/rest.authentication.guard.ts | 6 +++--- src/authentication/util/token.util.ts | 5 +++++ src/middleware/executionContext.middleware.ts | 6 ++++-- 4 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 src/authentication/util/token.util.ts diff --git a/src/authentication/authentication.guard.ts b/src/authentication/authentication.guard.ts index 3b0bda5..c91d8da 100644 --- a/src/authentication/authentication.guard.ts +++ b/src/authentication/authentication.guard.ts @@ -1,6 +1,7 @@ import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { GqlExecutionContext } from '@nestjs/graphql'; import { AuthenticationHelper } from './authentication.helper'; +import { TokenUtil } from './util/token.util'; @Injectable() export class AuthGuard implements CanActivate { @@ -11,7 +12,7 @@ export class AuthGuard implements CanActivate { if (ctx) { const token = ctx.headers.authorization; if (token) { - const reqAuthToken = token.split(' ')[1]; + const reqAuthToken = TokenUtil.extractToken(token); ctx.user = this.authenticationHelper.validateAuthToken(reqAuthToken); return true; } diff --git a/src/authentication/rest.authentication.guard.ts b/src/authentication/rest.authentication.guard.ts index 7f2d045..7c6e4f2 100644 --- a/src/authentication/rest.authentication.guard.ts +++ b/src/authentication/rest.authentication.guard.ts @@ -5,6 +5,7 @@ import { UnauthorizedException, } from '@nestjs/common'; import { AuthenticationHelper } from './authentication.helper'; +import { TokenUtil } from './util/token.util'; @Injectable() export class RestAuthGuard implements CanActivate { @@ -12,9 +13,8 @@ export class RestAuthGuard implements CanActivate { public async canActivate(context: ExecutionContext): Promise { const ctx = context.switchToHttp().getRequest(); const res = context.switchToHttp().getResponse(); - const req = await this.authenticationHelper.validateAuthToken( - ctx.headers.authorization.split(' ')[1], - ); + const reqAuthToken = TokenUtil.extractToken(ctx.headers.authorization); + const req = await this.authenticationHelper.validateAuthToken(reqAuthToken); if (!(req instanceof UnauthorizedException)) { return true; } else { diff --git a/src/authentication/util/token.util.ts b/src/authentication/util/token.util.ts new file mode 100644 index 0000000..32ed5af --- /dev/null +++ b/src/authentication/util/token.util.ts @@ -0,0 +1,5 @@ +export class TokenUtil { + public static extractToken(authorization: string): string { + return authorization?.split(' ')[1]; + } +} diff --git a/src/middleware/executionContext.middleware.ts b/src/middleware/executionContext.middleware.ts index a3a4341..18e99b7 100644 --- a/src/middleware/executionContext.middleware.ts +++ b/src/middleware/executionContext.middleware.ts @@ -2,6 +2,7 @@ import { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; import { AuthenticationHelper } from '../authentication/authentication.helper'; import { ExecutionManager } from '../util/execution.manager'; +import { TokenUtil } from '../authentication/util/token.util'; @Injectable() export class ExecutionContextBinder implements NestMiddleware { @@ -9,9 +10,10 @@ export class ExecutionContextBinder implements NestMiddleware { async use(req: Request, res: Response, next: NextFunction) { ExecutionManager.runWithContext(async () => { try { - const token = req.headers.authorization?.split(' ')[1]; + const token = req.headers.authorization; if (token) { - const user = this.auth.validateAuthToken(token); + const reqAuthToken = TokenUtil.extractToken(token); + const user = this.auth.validateAuthToken(reqAuthToken); ExecutionManager.setTenantId(user.tenantId); } next(); From 1e7837bc70ac59d0cac7c63fbdcdf794815b5251 Mon Sep 17 00:00:00 2001 From: Sruthy M L Date: Wed, 18 Dec 2024 14:49:08 +0530 Subject: [PATCH 20/23] BLD: Add db init script for creation of tenant user and db --- docker-compose.yml | 7 +++++++ docker.env.sample | 2 ++ init-db.sh | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 init-db.sh diff --git a/docker-compose.yml b/docker-compose.yml index 1c1acbe..9b5d107 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,12 +19,19 @@ services: postgres: container_name: postgres image: postgres:12 + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_ADMIN_USER: ${POSTGRES_USER} + PGPASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_TENANT_USER: ${POSTGRES_TENANT_USER} + POSTGRES_TENANT_PASSWORD: ${POSTGRES_TENANT_PASSWORD} expose: - '5432' ports: - '5432:5432' volumes: - /data/postgres:/var/lib/postgresql/data + - ./init-db.sh:/docker-entrypoint-initdb.d/init-db.sh env_file: - docker.env networks: diff --git a/docker.env.sample b/docker.env.sample index 27eca62..f6d2bfb 100644 --- a/docker.env.sample +++ b/docker.env.sample @@ -1,5 +1,7 @@ POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres POSTGRES_DB=authentication-service +POSTGRES_TENANT_USER=tenant +POSTGRES_TENANT_PASSWORD=tenant PGADMIN_DEFAULT_EMAIL= PGADMIN_DEFAULT_PASSWORD= \ No newline at end of file diff --git a/init-db.sh b/init-db.sh new file mode 100644 index 0000000..b58a0c9 --- /dev/null +++ b/init-db.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -e + +# Create the database if it doesn't exist +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + DO \$\$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = '$POSTGRES_DB') THEN + CREATE DATABASE "$POSTGRES_DB" + WITH + OWNER = postgres + ENCODING = 'UTF8'; + END IF; + END + \$\$; +EOSQL + +# Create tenant user if it doesn't exist +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + DO \$\$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_user WHERE usename = '$POSTGRES_TENANT_USER') THEN + CREATE USER $POSTGRES_TENANT_USER WITH PASSWORD '$POSTGRES_TENANT_PASSWORD'; + END IF; + END + \$\$; + + GRANT CONNECT ON DATABASE "$POSTGRES_DB" TO "$POSTGRES_TENANT_USER"; + ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "$POSTGRES_TENANT_USER"; + GRANT USAGE ON SCHEMA public TO "$POSTGRES_TENANT_USER"; +EOSQL + +echo "Database setup complete" From cf1df6662b2b592ea39b560d519be598c3f65828 Mon Sep 17 00:00:00 2001 From: Sruthy M L Date: Wed, 18 Dec 2024 14:50:51 +0530 Subject: [PATCH 21/23] DOC: Added description for multi tenancy support --- README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 57216c9..d3adfe4 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ Developers can customise this as per their requirement. |JWT_TOKEN_EXPTIME|3600 | |JWT_REFRESH_TOKEN_EXP_TIME| 36000 | |ENV | local| +|AUTH_KEY| Required authentication key for tenant creation |   | Other Configuration(Required) | | @@ -134,4 +135,18 @@ GraphQL endpoint http://localhost:${PORT}/auth/api/graphql -[API Documentation](https://documenter.getpostman.com/view/10091423/U16ev8cG) \ No newline at end of file +[API Documentation](https://documenter.getpostman.com/view/10091423/U16ev8cG) + +## Multi-tenancy Support + +This service supports multi-tenancy with complete data isolation between tenants at the database level using PostgreSQL row-level security (RLS). Each tenant's data is isolated using a tenant_id column and RLS policies. + +### How it Works + +1. Every tenant specific entities in the system has a `tenant_id` column +2. PostgreSQL Row Level Security (RLS) policies are enabled on all tables +3. The `app.tenant_id` configuration parameter is set for each request +4. Database queries are automatically filtered by the `tenant_id` through RLS policies + +##### Setting up Database User for Multi-tenancy +Create a database user with restricted access (can be done using the provided init-db.sh) \ No newline at end of file From 6fef4dae94521ecd2e18a04fb317db3d3feb4924 Mon Sep 17 00:00:00 2001 From: Bharath Date: Wed, 18 Dec 2024 15:18:54 +0530 Subject: [PATCH 22/23] FEAT: Handle login for multi-tenancy --- env.sample | 2 + src/authentication/authentication.graphql | 35 +++++++----- src/authentication/authentication.module.ts | 2 + .../resolver/user.auth.resolver.ts | 2 +- src/authentication/service/google.service.ts | 56 +++++++++++-------- .../service/otp.auth.service.ts | 18 ++++-- .../service/password.auth.service.ts | 15 ++++- .../validation/user.auth.schema.validation.ts | 27 +++++++++ src/schema/graphql.schema.ts | 5 ++ .../service/tenant.service.interface.ts | 2 + src/tenant/service/tenant.service.ts | 32 ++++++++++- .../resolver/userauth.resolver.test.ts | 3 +- .../service/otpauth.service.test.ts | 5 +- .../service/passwordauth.service.test.ts | 3 + 14 files changed, 157 insertions(+), 50 deletions(-) diff --git a/env.sample b/env.sample index d187ffa..0bde3bc 100644 --- a/env.sample +++ b/env.sample @@ -38,3 +38,5 @@ RECAPTCHA_VERIFY_URL=https://www.google.com/recaptcha/api/siteverify DEFAULT_ADMIN_PASSWORD=adminpassword INVITATION_TOKEN_EXPTIME = 7d AUTH_KEY= +MULTI_TENANCY_ENABLED= +DEFAULT_TENANT_ID= diff --git a/src/authentication/authentication.graphql b/src/authentication/authentication.graphql index f60b274..7998213 100644 --- a/src/authentication/authentication.graphql +++ b/src/authentication/authentication.graphql @@ -1,16 +1,16 @@ type Mutation { - passwordLogin(input: UserPasswordLoginInput!): TokenResponse - passwordSignup(input: UserPasswordSignupInput!): UserSignupResponse - inviteTokenSignup(input: UserInviteTokenSignupInput): InviteTokenResponse - refreshInviteToken(id: ID!):InviteTokenResponse - setPasswordForInvite(input: UserPasswordForInviteInput): UserSignupResponse - revokeInviteToken(id: ID!): Boolean - otpLogin(input: UserOTPLoginInput!): TokenResponse - otpSignup(input: UserOTPSignupInput!): UserSignupResponse - changePassword(input: UserPasswordInput!): User - refresh(input: RefreshTokenInput!): TokenResponse - logout: String - generateOtp(input: GenerateOtpInput): String + passwordLogin(input: UserPasswordLoginInput!): TokenResponse + passwordSignup(input: UserPasswordSignupInput!): UserSignupResponse + inviteTokenSignup(input: UserInviteTokenSignupInput): InviteTokenResponse + refreshInviteToken(id: ID!): InviteTokenResponse + setPasswordForInvite(input: UserPasswordForInviteInput): UserSignupResponse + revokeInviteToken(id: ID!): Boolean + otpLogin(input: UserOTPLoginInput!): TokenResponse + otpSignup(input: UserOTPSignupInput!): UserSignupResponse + changePassword(input: UserPasswordInput!): User + refresh(input: RefreshTokenInput!): TokenResponse + logout: String + generateOtp(input: GenerateOtpInput): String } input UserPasswordSignupInput { @@ -20,6 +20,7 @@ input UserPasswordSignupInput { firstName: String! middleName: String lastName: String! + tenantDomain: String } input UserOTPSignupInput { @@ -28,16 +29,19 @@ input UserOTPSignupInput { firstName: String! middleName: String lastName: String! + tenantDomain: String } input UserPasswordLoginInput { username: String! password: String! + tenantDomain: String } input UserOTPLoginInput { username: String! otp: String! + tenantDomain: String } type TokenResponse { @@ -65,11 +69,12 @@ input RefreshTokenInput { } input GenerateOtpInput { - phone: String! + phone: String! + tenantDomain: String } -input Enable2FAInput{ - code: String! +input Enable2FAInput { + code: String! } input UserInviteTokenSignupInput { diff --git a/src/authentication/authentication.module.ts b/src/authentication/authentication.module.ts index a9c5e47..b2b1002 100644 --- a/src/authentication/authentication.module.ts +++ b/src/authentication/authentication.module.ts @@ -34,6 +34,7 @@ import { RecaptchaService } from './service/recaptcha.service'; import { TokenService } from './service/token.service'; import TwilioOTPService from './service/twilio.otp.service'; import { DatabaseModule } from '../database/database.module'; +import { TenantModule } from '../tenant/tenant.module'; const providers: Provider[] = [ UserAuthResolver, @@ -76,6 +77,7 @@ const providers: Provider[] = [ TwilioImplModule, HttpModule, DatabaseModule, + TenantModule, ], providers, controllers: [GoogleAuthController], diff --git a/src/authentication/resolver/user.auth.resolver.ts b/src/authentication/resolver/user.auth.resolver.ts index bf02343..a69c588 100644 --- a/src/authentication/resolver/user.auth.resolver.ts +++ b/src/authentication/resolver/user.auth.resolver.ts @@ -126,7 +126,7 @@ export default class UserAuthResolver { @Mutation('generateOtp') @UsePipes(new ValidationPipe(GenerateOtpInputSchema)) async generateOtp(@Args('input') request: GenerateOtpInput) { - return this.otpAuthService.sendOTP(request.phone); + return this.otpAuthService.sendOTP(request); } // Its commented as of now. This should be part of Two factor authentication. // Implementation is not yet completed. diff --git a/src/authentication/service/google.service.ts b/src/authentication/service/google.service.ts index bf0e978..d7ad74d 100644 --- a/src/authentication/service/google.service.ts +++ b/src/authentication/service/google.service.ts @@ -5,11 +5,15 @@ import { GoogleUserSchema } from '../validation/user.auth.schema.validation'; import { UserServiceInterface } from '../../authorization/service/user.service.interface'; import { AuthenticationHelper } from '../authentication.helper'; import { GoogleLoginUser } from '../passport/googleStrategy'; +import { TenantServiceInterface } from '../../tenant/service/tenant.service.interface'; @Injectable() export class GoogleAuthService { constructor( - @Inject(UserServiceInterface) private userService: UserServiceInterface, + @Inject(UserServiceInterface) + private userService: UserServiceInterface, + @Inject(TenantServiceInterface) + private tenantService: TenantServiceInterface, private authenticationHelper: AuthenticationHelper, ) {} private async validateInput( @@ -26,28 +30,32 @@ export class GoogleAuthService { } async googleLogin(googleLoginInput: GoogleLoginUser) { - return this.validateInput(googleLoginInput) - .then((googleUser) => { - return this.userService.getUserDetailsByEmailOrPhone(googleUser.email); - }) - .then((existingUserDetails) => { - if (!existingUserDetails) { - const userFromInput = new User(); - return this.userService.createUser({ - ...userFromInput, - ...googleLoginInput, - origin: 'google', - }); - } else return existingUserDetails; - }) - .then((user) => { - const token = this.authenticationHelper.generateTokenForUser(user); - this.userService.updateField( - user.id, - 'refreshToken', - token.refreshToken, - ); - return token; - }); + try { + const googleUser = await this.validateInput(googleLoginInput); + await this.tenantService.setTenantIdInContext(googleUser); + + let user = await this.userService.getUserDetailsByEmailOrPhone( + googleUser.email, + ); + if (!user) { + user = await this.userService.createUser({ + ...new User(), + ...googleLoginInput, + origin: 'google', + }); + } + + const token = this.authenticationHelper.generateTokenForUser(user); + await this.userService.updateField( + user.id, + 'refreshToken', + token.refreshToken, + ); + + return token; + } catch (error) { + console.error('Google login failed:', error.message); + throw new Error('Google login failed. Please try again.'); + } } } diff --git a/src/authentication/service/otp.auth.service.ts b/src/authentication/service/otp.auth.service.ts index 5725f54..7e23531 100644 --- a/src/authentication/service/otp.auth.service.ts +++ b/src/authentication/service/otp.auth.service.ts @@ -3,6 +3,7 @@ import User from '../../authorization/entity/user.entity'; import { UserNotFoundException } from '../../authorization/exception/user.exception'; import { UserServiceInterface } from '../../authorization/service/user.service.interface'; import { + GenerateOtpInput, Status, TokenResponse, UserOTPLoginInput, @@ -16,11 +17,15 @@ import { import { Authenticatable } from '../interfaces/authenticatable'; import { OTPVerifiable } from '../interfaces/otp.verifiable'; import { TokenService } from './token.service'; +import { TenantServiceInterface } from '../../tenant/service/tenant.service.interface'; @Injectable() export default class OTPAuthService implements Authenticatable { constructor( - @Inject(UserServiceInterface) private userService: UserServiceInterface, + @Inject(UserServiceInterface) + private userService: UserServiceInterface, + @Inject(TenantServiceInterface) + private tenantService: TenantServiceInterface, private tokenService: TokenService, private otpService: OTPVerifiable, ) {} @@ -28,6 +33,7 @@ export default class OTPAuthService implements Authenticatable { async userSignup( userDetails: UserOTPSignupInput, ): Promise { + await this.tenantService.setTenantIdInContext(userDetails); const verifyObj = await this.userService.verifyDuplicateUser( userDetails.email, userDetails.phone, @@ -51,6 +57,7 @@ export default class OTPAuthService implements Authenticatable { } async userLogin(userDetails: UserOTPLoginInput): Promise { + await this.tenantService.setTenantIdInContext(userDetails); const userRecord = await this.userService.getUserDetailsByUsername( userDetails.username, userDetails.username, @@ -69,13 +76,16 @@ export default class OTPAuthService implements Authenticatable { return tokenResponse; } - async sendOTP(phoneNumber: string): Promise { - const user = await this.userService.getActiveUserByPhoneNumber(phoneNumber); + async sendOTP(generateOtpInput: GenerateOtpInput): Promise { + await this.tenantService.setTenantIdInContext(generateOtpInput); + const user = await this.userService.getActiveUserByPhoneNumber( + generateOtpInput.phone, + ); if (user && user.phone) { //Found an active user, generating OTP and sending the message to the user await this.otpService.sendOTP(user); } else { - throw new UserNotFoundException(phoneNumber); + throw new UserNotFoundException(generateOtpInput.phone); } } diff --git a/src/authentication/service/password.auth.service.ts b/src/authentication/service/password.auth.service.ts index 44e7d79..8e7c772 100644 --- a/src/authentication/service/password.auth.service.ts +++ b/src/authentication/service/password.auth.service.ts @@ -26,11 +26,16 @@ import { import { Authenticatable } from '../interfaces/authenticatable'; import { TokenService } from './token.service'; import { TENANT_CONNECTION } from '../../database/database.constants'; +import { TenantServiceInterface } from '../../tenant/service/tenant.service.interface'; +import { ExecutionManager } from '../../util/execution.manager'; @Injectable() export default class PasswordAuthService implements Authenticatable { constructor( - @Inject(UserServiceInterface) private userService: UserServiceInterface, + @Inject(UserServiceInterface) + private userService: UserServiceInterface, + @Inject(TenantServiceInterface) + private tenantService: TenantServiceInterface, private tokenService: TokenService, private authenticationHelper: AuthenticationHelper, @Inject(TENANT_CONNECTION) @@ -41,6 +46,7 @@ export default class PasswordAuthService implements Authenticatable { async userSignup( userDetails: UserPasswordSignupInput, ): Promise { + await this.tenantService.setTenantIdInContext(userDetails); const verifyObj = await this.userService.verifyDuplicateUser( userDetails.email, userDetails.phone, @@ -91,7 +97,7 @@ export default class PasswordAuthService implements Authenticatable { const transaction = await this.dataSource.manager.transaction(async () => { const savedUser = await this.userService.createUser(userFromInput); invitationToken = this.authenticationHelper.generateInvitationToken( - { id: savedUser.id }, + { id: savedUser.id, tenantId: ExecutionManager.getTenantId() }, this.configService.get('INVITATION_TOKEN_EXPTIME'), ); await this.userService.updateField( @@ -106,7 +112,6 @@ export default class PasswordAuthService implements Authenticatable { lastName: user.lastName, inviteToken: user?.inviteToken, status: user.status, - tenantId: user.tenantId, }; return { inviteToken: invitationToken.token, @@ -123,6 +128,9 @@ export default class PasswordAuthService implements Authenticatable { const verificationResponse: any = this.authenticationHelper.validateInvitationToken( passwordDetails.inviteToken, ); + await this.tenantService.setTenantIdInContext({ + tenantId: verificationResponse.tenantId, + }); const user = await this.userService.getUserById(verificationResponse.id); if (user.password) { throw new PasswordAlreadySetException(user.id); @@ -138,6 +146,7 @@ export default class PasswordAuthService implements Authenticatable { } async userLogin(userDetails: UserPasswordLoginInput): Promise { + await this.tenantService.setTenantIdInContext(userDetails); const userRecord = await this.userService.getUserDetailsByUsername( userDetails.username, userDetails.username, diff --git a/src/authentication/validation/user.auth.schema.validation.ts b/src/authentication/validation/user.auth.schema.validation.ts index 18a7fd5..915bca2 100644 --- a/src/authentication/validation/user.auth.schema.validation.ts +++ b/src/authentication/validation/user.auth.schema.validation.ts @@ -1,4 +1,8 @@ import * as Joi from '@hapi/joi'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +const multiTenancyEnabled = Boolean(process.env.MULTI_TENANCY_ENABLED); export const UserPasswordSignupInputSchema = Joi.object({ email: Joi.string().email({ tlds: { allow: false } }), @@ -13,6 +17,13 @@ export const UserPasswordSignupInputSchema = Joi.object({ lastName: Joi.string() .regex(/^[a-zA-Z ]*$/) .required(), + tenantDomain: Joi.string().when('email', { + is: Joi.exist(), + then: Joi.optional(), + otherwise: multiTenancyEnabled + ? Joi.string().required() + : Joi.string().optional(), + }), }) .options({ abortEarly: false }) .or('email', 'phone'); @@ -27,16 +38,29 @@ export const UserOTPSignupInputSchema = Joi.object({ lastName: Joi.string() .regex(/^[a-zA-Z ]*$/) .required(), + tenantDomain: Joi.string().when('email', { + is: Joi.exist(), + then: Joi.optional(), + otherwise: multiTenancyEnabled + ? Joi.string().required() + : Joi.string().optional(), + }), }).options({ abortEarly: false }); export const UserPasswordLoginInputSchema = Joi.object({ username: Joi.string().required(), password: Joi.string().min(10).required(), + tenantDomain: multiTenancyEnabled + ? Joi.string().required() + : Joi.string().optional(), }).options({ abortEarly: false }); export const UserOTPLoginInputSchema = Joi.object({ username: Joi.string().required(), otp: Joi.string().required(), + tenantDomain: multiTenancyEnabled + ? Joi.string().required() + : Joi.string().optional(), }).options({ abortEarly: false }); export const UserSendOTPInputSchema = Joi.object({ @@ -67,6 +91,9 @@ export const GoogleUserSchema = Joi.object({ export const GenerateOtpInputSchema = Joi.object({ phone: Joi.string().trim().required(), + tenantDomain: multiTenancyEnabled + ? Joi.string().required() + : Joi.string().optional(), }); export const Enable2FAInputSchema = Joi.object({ diff --git a/src/schema/graphql.schema.ts b/src/schema/graphql.schema.ts index 237f4de..a108901 100644 --- a/src/schema/graphql.schema.ts +++ b/src/schema/graphql.schema.ts @@ -35,6 +35,7 @@ export interface UserPasswordSignupInput { firstName: string; middleName?: string; lastName: string; + tenantDomain?: string; } export interface UserOTPSignupInput { @@ -43,16 +44,19 @@ export interface UserOTPSignupInput { firstName: string; middleName?: string; lastName: string; + tenantDomain?: string; } export interface UserPasswordLoginInput { username: string; password: string; + tenantDomain?: string; } export interface UserOTPLoginInput { username: string; otp: string; + tenantDomain?: string; } export interface UserPasswordInput { @@ -66,6 +70,7 @@ export interface RefreshTokenInput { export interface GenerateOtpInput { phone: string; + tenantDomain?: string; } export interface Enable2FAInput { diff --git a/src/tenant/service/tenant.service.interface.ts b/src/tenant/service/tenant.service.interface.ts index a3f83b0..85be7c0 100644 --- a/src/tenant/service/tenant.service.interface.ts +++ b/src/tenant/service/tenant.service.interface.ts @@ -5,6 +5,8 @@ export interface TenantServiceInterface { getTenantByDomain(domain: string): Promise; createTenant(tenant: NewTenantInput): Promise; + + setTenantIdInContext(userDetails: any): Promise; } export const TenantServiceInterface = Symbol('TenantServiceInterface'); diff --git a/src/tenant/service/tenant.service.ts b/src/tenant/service/tenant.service.ts index c3081e2..ea05514 100644 --- a/src/tenant/service/tenant.service.ts +++ b/src/tenant/service/tenant.service.ts @@ -4,11 +4,24 @@ import Tenant from '../entity/tenant.entity'; import { TenantNotFoundException } from '../exception/tenant.exception'; import { TenantRepository } from '../repository/tenant.repository'; import { NewTenantInput } from '../../schema/graphql.schema'; +import { ConfigService } from '@nestjs/config'; +import { ExecutionManager } from '../../util/execution.manager'; import { TenantServiceInterface } from './tenant.service.interface'; @Injectable() export default class TenantService implements TenantServiceInterface { - constructor(private tenantRepository: TenantRepository) {} + private readonly multiTenancyEnabled: boolean; + private readonly defaultTenantId: string; + constructor( + private tenantRepository: TenantRepository, + private configService: ConfigService, + ) { + this.multiTenancyEnabled = + this.configService.get('MULTI_TENANCY_ENABLED') === 'true'; + this.defaultTenantId = this.configService.get( + 'DEFAULT_TENANT_ID', + ) as string; + } async getTenantByDomain(domain: string): Promise { const tenant = await this.tenantRepository.getTenantByDomain(domain); @@ -21,4 +34,21 @@ export default class TenantService implements TenantServiceInterface { async createTenant(tenant: NewTenantInput): Promise { return this.tenantRepository.save(tenant); } + + async setTenantIdInContext(userDetails: any): Promise { + let tenantId: string; + if (this.multiTenancyEnabled) { + const domain = + userDetails.tenantDomain || + userDetails.email?.split('@')[1] || + userDetails.username?.split('@')[1]; + const tenant = userDetails.tenantId + ? { id: userDetails.tenantId } + : await this.getTenantByDomain(domain); + tenantId = tenant.id; + } else { + tenantId = this.defaultTenantId; + } + ExecutionManager.setTenantId(tenantId); + } } diff --git a/test/authentication/resolver/userauth.resolver.test.ts b/test/authentication/resolver/userauth.resolver.test.ts index 43f3bb0..dd17806 100644 --- a/test/authentication/resolver/userauth.resolver.test.ts +++ b/test/authentication/resolver/userauth.resolver.test.ts @@ -87,6 +87,7 @@ describe('Userauth Module', () => { const input: UserPasswordLoginInput = { username: 'user@test.com', password: 's3cr3t1234567890', + tenantDomain: 'domain.com', }; const user = { id: users[0].id, @@ -115,7 +116,7 @@ describe('Userauth Module', () => { .post(gql) .send({ query: - 'mutation { passwordLogin(input: { username: "user@test.com" password: "s3cr3t1234567890" }) { accessToken, refreshToken, user{ id, email, phone, firstName, lastName, status}}}', + 'mutation { passwordLogin(input: { username: "user@test.com" password: "s3cr3t1234567890" tenantDomain: "domain.com" }) { accessToken, refreshToken, user{ id, email, phone, firstName, lastName, status}}}', }) .expect(200) .expect((res) => { diff --git a/test/authentication/service/otpauth.service.test.ts b/test/authentication/service/otpauth.service.test.ts index 612d2be..d1af984 100644 --- a/test/authentication/service/otpauth.service.test.ts +++ b/test/authentication/service/otpauth.service.test.ts @@ -17,6 +17,7 @@ import { UserOTPLoginInput, UserOTPSignupInput, } from '../../../src/schema/graphql.schema'; +import { TenantServiceInterface } from '../../../src/tenant/service/tenant.service.interface'; let users: User[] = [ { @@ -36,6 +37,7 @@ describe('test OTPAuthService', () => { let otpAuthService: OTPAuthService; let authenticationHelper: AuthenticationHelper; const userService = Substitute.for(); + const tenantService = Substitute.for(); const configService = Substitute.for(); const otpService = Substitute.for(); const tokenService = Substitute.for(); @@ -48,6 +50,7 @@ describe('test OTPAuthService', () => { controllers: [], providers: [ { provide: UserServiceInterface, useValue: userService }, + { provide: TenantServiceInterface, useValue: tenantService }, { provide: ConfigService, useValue: configService }, { provide: TokenService, useValue: tokenService }, { provide: 'OTPVerifiable', useValue: otpService }, @@ -235,7 +238,7 @@ describe('test OTPAuthService', () => { .getActiveUserByPhoneNumber('91234567980') .returns(Promise.resolve(users[0])); - const resp = await otpAuthService.sendOTP('91234567980'); + const resp = await otpAuthService.sendOTP({ phone: '91234567980' }); expect(resp).toHaveReturned; }); }); diff --git a/test/authentication/service/passwordauth.service.test.ts b/test/authentication/service/passwordauth.service.test.ts index d23c052..dd6f0bf 100644 --- a/test/authentication/service/passwordauth.service.test.ts +++ b/test/authentication/service/passwordauth.service.test.ts @@ -9,6 +9,7 @@ import User from '../../../src/authorization/entity/user.entity'; import { UserServiceInterface } from '../../../src/authorization/service/user.service.interface'; import { Status } from '../../../src/schema/graphql.schema'; import { TENANT_CONNECTION } from '../../../src/database/database.constants'; +import { TenantServiceInterface } from '../../../src/tenant/service/tenant.service.interface'; let users: User[] = [ { @@ -28,6 +29,7 @@ describe('test PasswordAuthService', () => { let passwordAuthService: PasswordAuthService; let authenticationHelper: AuthenticationHelper; const userService = Substitute.for(); + const tenantService = Substitute.for(); const configService = Substitute.for(); const tokenService = Substitute.for(); configService.get('ENV').returns('local'); @@ -46,6 +48,7 @@ describe('test PasswordAuthService', () => { controllers: [], providers: [ { provide: UserServiceInterface, useValue: userService }, + { provide: TenantServiceInterface, useValue: tenantService }, { provide: ConfigService, useValue: configService }, { provide: TokenService, useValue: tokenService }, { From 28d64e2541eddbd9989176ac05f2241d755bb43f Mon Sep 17 00:00:00 2001 From: Bharath Date: Wed, 18 Dec 2024 15:30:07 +0530 Subject: [PATCH 23/23] DOC: Add description for env variables used for handling multi-tenancy login --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index d3adfe4..0205b66 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,12 @@ Developers can customise this as per their requirement. | OTP_WINDOW | 300 | | OTP_STEP | 1 | +   +|Multi-Tenancy Configuration(Optional) | | +|--|--| +|MULTI_TENANCY_ENABLED | A boolean that indicates if multi-tenancy is enabled, used for handling user login | +|DEFAULT_TENANT_ID | Default tenant id to be used when multi-tenancy is disabled | + - Run `npm run migration:run` - Run `npm run start` - Service should be up and running in http://localhost:${PORT}.