@flusys/nestjs-auth 6.0.1 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/cjs/controllers/authentication.controller.spec.js +219 -0
  2. package/cjs/controllers/branch.controller.spec.js +134 -0
  3. package/cjs/controllers/company-selection.controller.spec.js +84 -0
  4. package/cjs/controllers/company.controller.spec.js +131 -0
  5. package/cjs/controllers/email-verification.controller.spec.js +142 -0
  6. package/cjs/controllers/user-permission.controller.spec.js +138 -0
  7. package/cjs/controllers/user.controller.spec.js +204 -0
  8. package/cjs/docs/auth-swagger.config.spec.js +130 -0
  9. package/cjs/entities/company-branch.entity.js +2 -3
  10. package/cjs/entities/index.spec.js +73 -0
  11. package/cjs/helpers/auth-response.mapper.js +46 -0
  12. package/cjs/helpers/auth-response.mapper.spec.js +139 -0
  13. package/cjs/helpers/index.js +19 -0
  14. package/cjs/helpers/token.helper.js +58 -0
  15. package/cjs/helpers/token.helper.spec.js +86 -0
  16. package/cjs/index.js +1 -0
  17. package/cjs/interceptors/clear-token.interceptor.spec.js +107 -0
  18. package/cjs/interceptors/set-token.interceptor.spec.js +165 -0
  19. package/cjs/modules/auth.module.spec.js +243 -0
  20. package/cjs/services/auth-config.service.spec.js +97 -0
  21. package/cjs/services/auth-datasource.provider.spec.js +161 -0
  22. package/cjs/services/auth-email.service.spec.js +404 -0
  23. package/cjs/services/authentication.service.js +37 -109
  24. package/cjs/services/authentication.service.spec.js +904 -0
  25. package/cjs/services/branch.service.js +30 -3
  26. package/cjs/services/branch.service.spec.js +154 -0
  27. package/cjs/services/company-selection-session.service.spec.js +90 -0
  28. package/cjs/services/company.service.js +34 -3
  29. package/cjs/services/company.service.spec.js +113 -0
  30. package/cjs/services/user-permission.service.js +35 -4
  31. package/cjs/services/user-permission.service.spec.js +202 -0
  32. package/cjs/services/user.service.spec.js +704 -0
  33. package/cjs/strategies/jwt.strategy.js +1 -1
  34. package/cjs/strategies/jwt.strategy.spec.js +166 -0
  35. package/cjs/validators/password.validator.spec.js +161 -0
  36. package/entities/company-branch.entity.d.ts +1 -1
  37. package/entities/pending-registration.entity.d.ts +2 -7
  38. package/fesm/controllers/authentication.controller.spec.js +215 -0
  39. package/fesm/controllers/branch.controller.spec.js +130 -0
  40. package/fesm/controllers/company-selection.controller.spec.js +80 -0
  41. package/fesm/controllers/company.controller.spec.js +127 -0
  42. package/fesm/controllers/email-verification.controller.spec.js +138 -0
  43. package/fesm/controllers/user-permission.controller.spec.js +134 -0
  44. package/fesm/controllers/user.controller.spec.js +200 -0
  45. package/fesm/docs/auth-swagger.config.spec.js +126 -0
  46. package/fesm/entities/company-branch.entity.js +2 -3
  47. package/fesm/entities/index.spec.js +69 -0
  48. package/fesm/helpers/auth-response.mapper.js +25 -0
  49. package/fesm/helpers/auth-response.mapper.spec.js +135 -0
  50. package/fesm/helpers/index.js +2 -0
  51. package/fesm/helpers/token.helper.js +40 -0
  52. package/fesm/helpers/token.helper.spec.js +82 -0
  53. package/fesm/index.js +1 -0
  54. package/fesm/interceptors/clear-token.interceptor.spec.js +103 -0
  55. package/fesm/interceptors/set-token.interceptor.spec.js +161 -0
  56. package/fesm/interfaces/authentication.interface.js +1 -1
  57. package/fesm/modules/auth.module.spec.js +239 -0
  58. package/fesm/services/auth-config.service.spec.js +93 -0
  59. package/fesm/services/auth-datasource.provider.spec.js +157 -0
  60. package/fesm/services/auth-email.service.spec.js +359 -0
  61. package/fesm/services/authentication.service.js +37 -109
  62. package/fesm/services/authentication.service.spec.js +859 -0
  63. package/fesm/services/branch.service.js +32 -5
  64. package/fesm/services/branch.service.spec.js +150 -0
  65. package/fesm/services/company-selection-session.service.spec.js +86 -0
  66. package/fesm/services/company.service.js +36 -5
  67. package/fesm/services/company.service.spec.js +109 -0
  68. package/fesm/services/user-permission.service.js +36 -5
  69. package/fesm/services/user-permission.service.spec.js +198 -0
  70. package/fesm/services/user.service.spec.js +659 -0
  71. package/fesm/strategies/jwt.strategy.js +1 -1
  72. package/fesm/strategies/jwt.strategy.spec.js +162 -0
  73. package/fesm/validators/password.validator.spec.js +157 -0
  74. package/helpers/auth-response.mapper.d.ts +22 -0
  75. package/helpers/index.d.ts +2 -0
  76. package/helpers/token.helper.d.ts +8 -0
  77. package/index.d.ts +1 -0
  78. package/interfaces/authentication.interface.d.ts +7 -0
  79. package/package.json +8 -3
  80. package/services/authentication.service.d.ts +1 -8
  81. package/services/branch.service.d.ts +7 -5
  82. package/services/company.service.d.ts +7 -4
  83. package/services/user-permission.service.d.ts +4 -1
@@ -25,14 +25,38 @@ function _ts_param(paramIndex, decorator) {
25
25
  decorator(target, key, paramIndex);
26
26
  };
27
27
  }
28
- import { HybridCache, ApiService } from '@flusys/nestjs-shared/classes';
28
+ import { ApiService, HybridCache } from '@flusys/nestjs-shared/classes';
29
+ import { IPermissionSyncAdapter, PERMISSION_SYNC_ADAPTER } from '@flusys/nestjs-shared/interfaces';
29
30
  import { UtilsService } from '@flusys/nestjs-shared/modules';
30
31
  import { applyCompanyFilter } from '@flusys/nestjs-shared/utils';
31
- import { Inject, Injectable, Scope } from '@nestjs/common';
32
+ import { Inject, Injectable, Optional, Scope } from '@nestjs/common';
33
+ import { In } from 'typeorm';
32
34
  import { CompanyBranch } from '../entities/company-branch.entity';
35
+ import { UserCompanyPermission } from '../entities/user-company-permission.entity';
33
36
  import { AuthConfigService } from './auth-config.service';
34
37
  import { AuthDataSourceProvider } from './auth-datasource.provider';
35
38
  export class BranchService extends ApiService {
39
+ async beforeDeleteOperation(dto, user, queryRunner) {
40
+ const branchIds = Array.isArray(dto.id) ? dto.id : [
41
+ dto.id
42
+ ];
43
+ await queryRunner.manager.getRepository(UserCompanyPermission).delete({
44
+ targetId: In(branchIds)
45
+ });
46
+ if (!this.permissionSyncAdapter?.onBranchDeleted) {
47
+ return;
48
+ }
49
+ const rows = await queryRunner.manager.getRepository(CompanyBranch).find({
50
+ where: {
51
+ id: In(branchIds)
52
+ },
53
+ select: [
54
+ 'id',
55
+ 'companyId'
56
+ ]
57
+ });
58
+ await Promise.all(rows.map((row)=>this.permissionSyncAdapter.onBranchDeleted(row.id, row.companyId)));
59
+ }
36
60
  // Query Customization
37
61
  async getSelectQuery(query, _user, select) {
38
62
  if (!select?.length) {
@@ -74,8 +98,8 @@ export class BranchService extends ApiService {
74
98
  }
75
99
  return result;
76
100
  }
77
- constructor(cacheManager, utilsService, dataSourceProvider, authConfig){
78
- super('company_branch', cacheManager, utilsService, BranchService.name, true, 'auth', CompanyBranch, dataSourceProvider), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "authConfig", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.authConfig = authConfig;
101
+ constructor(cacheManager, utilsService, dataSourceProvider, authConfig, permissionSyncAdapter){
102
+ super('company_branch', cacheManager, utilsService, BranchService.name, true, 'auth', CompanyBranch, dataSourceProvider), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "authConfig", void 0), _define_property(this, "permissionSyncAdapter", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.authConfig = authConfig, this.permissionSyncAdapter = permissionSyncAdapter;
79
103
  }
80
104
  }
81
105
  BranchService = _ts_decorate([
@@ -86,11 +110,14 @@ BranchService = _ts_decorate([
86
110
  _ts_param(1, Inject(UtilsService)),
87
111
  _ts_param(2, Inject(AuthDataSourceProvider)),
88
112
  _ts_param(3, Inject(AuthConfigService)),
113
+ _ts_param(4, Optional()),
114
+ _ts_param(4, Inject(PERMISSION_SYNC_ADAPTER)),
89
115
  _ts_metadata("design:type", Function),
90
116
  _ts_metadata("design:paramtypes", [
91
117
  typeof HybridCache === "undefined" ? Object : HybridCache,
92
118
  typeof UtilsService === "undefined" ? Object : UtilsService,
93
119
  typeof AuthDataSourceProvider === "undefined" ? Object : AuthDataSourceProvider,
94
- typeof AuthConfigService === "undefined" ? Object : AuthConfigService
120
+ typeof AuthConfigService === "undefined" ? Object : AuthConfigService,
121
+ typeof IPermissionSyncAdapter === "undefined" ? Object : IPermissionSyncAdapter
95
122
  ])
96
123
  ], BranchService);
@@ -0,0 +1,150 @@
1
+ import { createMockDataSourceProvider, createMockRepository } from '@test-utils/mocks/repository.mock';
2
+ import { BranchService } from './branch.service';
3
+ import { CompanyBranch } from '../entities/company-branch.entity';
4
+ import { UserCompanyPermission } from '../entities/user-company-permission.entity';
5
+ describe('BranchService', ()=>{
6
+ let service;
7
+ let mockRepo;
8
+ let mockAuthConfig;
9
+ let mockPermissionSyncAdapter;
10
+ let mockQueryRunner;
11
+ beforeEach(()=>{
12
+ mockRepo = createMockRepository();
13
+ Object.assign(mockRepo.createQueryBuilder(), {
14
+ leftJoinAndSelect: jest.fn().mockReturnThis(),
15
+ addOrderBy: jest.fn().mockReturnThis()
16
+ });
17
+ const mockDataSourceProvider = createMockDataSourceProvider(mockRepo);
18
+ mockAuthConfig = {
19
+ isCompanyFeatureEnabled: jest.fn().mockReturnValue(false)
20
+ };
21
+ mockPermissionSyncAdapter = {
22
+ onBranchDeleted: jest.fn()
23
+ };
24
+ service = new BranchService({}, {}, mockDataSourceProvider, mockAuthConfig, mockPermissionSyncAdapter);
25
+ const permissionRepo = createMockRepository();
26
+ const branchRepo = createMockRepository();
27
+ branchRepo.find.mockResolvedValue([]);
28
+ mockQueryRunner = {
29
+ manager: {
30
+ getRepository: jest.fn().mockImplementation((entity)=>{
31
+ if (entity === UserCompanyPermission) return permissionRepo;
32
+ if (entity === CompanyBranch) return branchRepo;
33
+ throw new Error('Unexpected entity');
34
+ })
35
+ }
36
+ };
37
+ });
38
+ describe('beforeDeleteOperation', ()=>{
39
+ it('deletes user permissions targeting the deleted branch ids', async ()=>{
40
+ await service.beforeDeleteOperation({
41
+ id: [
42
+ 'branch-1',
43
+ 'branch-2'
44
+ ]
45
+ }, null, mockQueryRunner);
46
+ const permissionRepo = mockQueryRunner.manager.getRepository(UserCompanyPermission);
47
+ expect(permissionRepo.delete).toHaveBeenCalledWith({
48
+ targetId: expect.anything()
49
+ });
50
+ });
51
+ it('handles a single (non-array) branch id', async ()=>{
52
+ await service.beforeDeleteOperation({
53
+ id: 'branch-1'
54
+ }, null, mockQueryRunner);
55
+ const permissionRepo = mockQueryRunner.manager.getRepository(UserCompanyPermission);
56
+ expect(permissionRepo.delete).toHaveBeenCalled();
57
+ });
58
+ it('does nothing further when no permission sync adapter is configured', async ()=>{
59
+ service = new BranchService({}, {}, createMockDataSourceProvider(mockRepo), mockAuthConfig, undefined);
60
+ await expect(service.beforeDeleteOperation({
61
+ id: [
62
+ 'branch-1'
63
+ ]
64
+ }, null, mockQueryRunner)).resolves.toBeUndefined();
65
+ });
66
+ it('notifies the permission sync adapter for each deleted branch', async ()=>{
67
+ const branchRepo = mockQueryRunner.manager.getRepository(CompanyBranch);
68
+ branchRepo.find.mockResolvedValue([
69
+ {
70
+ id: 'branch-1',
71
+ companyId: 'company-1'
72
+ }
73
+ ]);
74
+ await service.beforeDeleteOperation({
75
+ id: [
76
+ 'branch-1'
77
+ ]
78
+ }, null, mockQueryRunner);
79
+ expect(mockPermissionSyncAdapter.onBranchDeleted).toHaveBeenCalledWith('branch-1', 'company-1');
80
+ });
81
+ });
82
+ describe('getSelectQuery', ()=>{
83
+ it('applies the default selected fields when none are provided', async ()=>{
84
+ const qb = mockRepo.createQueryBuilder();
85
+ const { isRaw } = await service.getSelectQuery(qb, null);
86
+ expect(qb.select).toHaveBeenCalledWith(expect.arrayContaining([
87
+ 'company_branch.id',
88
+ 'company_branch.name',
89
+ 'company_branch.slug'
90
+ ]));
91
+ expect(isRaw).toBe(false);
92
+ });
93
+ it('honors an explicit select list', async ()=>{
94
+ const qb = mockRepo.createQueryBuilder();
95
+ await service.getSelectQuery(qb, null, [
96
+ 'id',
97
+ 'name'
98
+ ]);
99
+ expect(qb.select).toHaveBeenCalledWith([
100
+ 'company_branch.id',
101
+ 'company_branch.name'
102
+ ]);
103
+ });
104
+ });
105
+ describe('getGlobalSearchQuery', ()=>{
106
+ it('adds a LIKE search clause across name/slug/email', async ()=>{
107
+ const qb = mockRepo.createQueryBuilder();
108
+ await service.getGlobalSearchQuery(qb, 'acme');
109
+ expect(qb.andWhere).toHaveBeenCalledWith(expect.stringContaining('company_branch.name LIKE'), {
110
+ search: '%acme%'
111
+ });
112
+ });
113
+ });
114
+ describe('getExtraManipulateQuery', ()=>{
115
+ it('applies a company filter when company feature is enabled and no explicit companyId filter is present', async ()=>{
116
+ mockAuthConfig.isCompanyFeatureEnabled.mockReturnValue(true);
117
+ const qb = mockRepo.createQueryBuilder();
118
+ await service.getExtraManipulateQuery(qb, {
119
+ filter: {}
120
+ }, {
121
+ id: 'user-1',
122
+ companyId: 'company-1'
123
+ });
124
+ expect(qb.andWhere).toHaveBeenCalledWith('company_branch.company_id = :companyId', {
125
+ companyId: 'company-1'
126
+ });
127
+ });
128
+ it('does not apply a company filter when companyId is explicitly provided in the filter', async ()=>{
129
+ mockAuthConfig.isCompanyFeatureEnabled.mockReturnValue(true);
130
+ const qb = mockRepo.createQueryBuilder();
131
+ await service.getExtraManipulateQuery(qb, {
132
+ filter: {
133
+ companyId: 'explicit-company'
134
+ }
135
+ }, {
136
+ id: 'user-1',
137
+ companyId: 'company-1'
138
+ });
139
+ expect(qb.andWhere).not.toHaveBeenCalledWith('company_branch.company_id = :companyId', expect.anything());
140
+ });
141
+ it('does not apply a company filter when the company feature is disabled', async ()=>{
142
+ mockAuthConfig.isCompanyFeatureEnabled.mockReturnValue(false);
143
+ const qb = mockRepo.createQueryBuilder();
144
+ await service.getExtraManipulateQuery(qb, {
145
+ filter: {}
146
+ }, null);
147
+ expect(qb.andWhere).not.toHaveBeenCalledWith('company_branch.company_id = :companyId', expect.anything());
148
+ });
149
+ });
150
+ });
@@ -0,0 +1,86 @@
1
+ import { CompanySelectionSessionService } from './company-selection-session.service';
2
+ describe('CompanySelectionSessionService', ()=>{
3
+ let service;
4
+ let mockCacheManager;
5
+ beforeEach(()=>{
6
+ mockCacheManager = {
7
+ set: jest.fn(),
8
+ get: jest.fn(),
9
+ del: jest.fn()
10
+ };
11
+ service = new CompanySelectionSessionService(mockCacheManager);
12
+ });
13
+ describe('createSession', ()=>{
14
+ it('stores a session in the cache and returns a session id', async ()=>{
15
+ const sessionId = await service.createSession('user-1');
16
+ expect(typeof sessionId).toBe('string');
17
+ expect(sessionId.length).toBeGreaterThan(0);
18
+ expect(mockCacheManager.set).toHaveBeenCalledWith(`auth:session:${sessionId}`, expect.objectContaining({
19
+ sessionId,
20
+ userId: 'user-1'
21
+ }), 5 * 60 * 1000);
22
+ });
23
+ it('generates unique session ids on successive calls', async ()=>{
24
+ const first = await service.createSession('user-1');
25
+ const second = await service.createSession('user-1');
26
+ expect(first).not.toBe(second);
27
+ });
28
+ });
29
+ describe('getSession', ()=>{
30
+ it('returns null when no session is cached', async ()=>{
31
+ mockCacheManager.get.mockResolvedValue(undefined);
32
+ const result = await service.getSession('missing-session');
33
+ expect(result).toBeNull();
34
+ });
35
+ it('returns the session when present and not expired', async ()=>{
36
+ const session = {
37
+ sessionId: 'sess-1',
38
+ userId: 'user-1',
39
+ createdAt: new Date(),
40
+ expiresAt: new Date(Date.now() + 60_000)
41
+ };
42
+ mockCacheManager.get.mockResolvedValue(session);
43
+ const result = await service.getSession('sess-1');
44
+ expect(result).toEqual(session);
45
+ expect(mockCacheManager.get).toHaveBeenCalledWith('auth:session:sess-1');
46
+ });
47
+ it('deletes and returns null when the cached session has expired', async ()=>{
48
+ const session = {
49
+ sessionId: 'sess-1',
50
+ userId: 'user-1',
51
+ createdAt: new Date(Date.now() - 10 * 60_000),
52
+ expiresAt: new Date(Date.now() - 60_000)
53
+ };
54
+ mockCacheManager.get.mockResolvedValue(session);
55
+ const result = await service.getSession('sess-1');
56
+ expect(result).toBeNull();
57
+ expect(mockCacheManager.del).toHaveBeenCalledWith('auth:session:sess-1');
58
+ });
59
+ });
60
+ describe('consumeSession', ()=>{
61
+ it('returns and deletes the session when present', async ()=>{
62
+ const session = {
63
+ sessionId: 'sess-1',
64
+ userId: 'user-1',
65
+ createdAt: new Date(),
66
+ expiresAt: new Date(Date.now() + 60_000)
67
+ };
68
+ mockCacheManager.get.mockResolvedValue(session);
69
+ const result = await service.consumeSession('sess-1');
70
+ expect(result).toEqual(session);
71
+ expect(mockCacheManager.del).toHaveBeenCalledWith('auth:session:sess-1');
72
+ });
73
+ it('returns null and does not delete when session is missing', async ()=>{
74
+ mockCacheManager.get.mockResolvedValue(undefined);
75
+ const result = await service.consumeSession('missing');
76
+ expect(result).toBeNull();
77
+ expect(mockCacheManager.del).not.toHaveBeenCalled();
78
+ });
79
+ });
80
+ describe('deleteSession', ()=>{
81
+ it('deletes the session by key', async ()=>{
82
+ await service.deleteSession('sess-1');
83
+ expect(mockCacheManager.del).toHaveBeenCalledWith('auth:session:sess-1');
84
+ });
85
+ });
86
+ });
@@ -25,12 +25,40 @@ function _ts_param(paramIndex, decorator) {
25
25
  decorator(target, key, paramIndex);
26
26
  };
27
27
  }
28
- import { HybridCache, ApiService } from '@flusys/nestjs-shared/classes';
28
+ import { ApiService, HybridCache } from '@flusys/nestjs-shared/classes';
29
+ import { IPermissionSyncAdapter, PERMISSION_SYNC_ADAPTER } from '@flusys/nestjs-shared/interfaces';
29
30
  import { UtilsService } from '@flusys/nestjs-shared/modules';
30
- import { Inject, Injectable, Scope } from '@nestjs/common';
31
+ import { Inject, Injectable, Optional, Scope } from '@nestjs/common';
32
+ import { In } from 'typeorm';
33
+ import { CompanyBranch } from '../entities/company-branch.entity';
31
34
  import { Company } from '../entities/company.entity';
35
+ import { UserCompanyPermission } from '../entities/user-company-permission.entity';
32
36
  import { AuthDataSourceProvider } from './auth-datasource.provider';
33
37
  export class CompanyService extends ApiService {
38
+ async beforeDeleteOperation(dto, user, queryRunner) {
39
+ const companyIds = Array.isArray(dto.id) ? dto.id : [
40
+ dto.id
41
+ ];
42
+ const branchRows = await queryRunner.manager.getRepository(CompanyBranch).find({
43
+ where: {
44
+ companyId: In(companyIds)
45
+ },
46
+ select: [
47
+ 'id'
48
+ ]
49
+ });
50
+ const targetIds = [
51
+ ...companyIds,
52
+ ...branchRows.map((row)=>row.id)
53
+ ];
54
+ await queryRunner.manager.getRepository(UserCompanyPermission).delete({
55
+ targetId: In(targetIds)
56
+ });
57
+ if (!this.permissionSyncAdapter?.onCompanyDeleted) {
58
+ return;
59
+ }
60
+ await Promise.all(companyIds.map((companyId)=>this.permissionSyncAdapter.onCompanyDeleted(companyId)));
61
+ }
34
62
  // Query Customization
35
63
  async getSelectQuery(query, _user, select) {
36
64
  if (!select?.length) {
@@ -59,8 +87,8 @@ export class CompanyService extends ApiService {
59
87
  isRaw: false
60
88
  };
61
89
  }
62
- constructor(cacheManager, utilsService, dataSourceProvider){
63
- super('company', cacheManager, utilsService, CompanyService.name, true, 'auth', Company, dataSourceProvider), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService;
90
+ constructor(cacheManager, utilsService, dataSourceProvider, permissionSyncAdapter){
91
+ super('company', cacheManager, utilsService, CompanyService.name, true, 'auth', Company, dataSourceProvider), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "permissionSyncAdapter", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.permissionSyncAdapter = permissionSyncAdapter;
64
92
  }
65
93
  }
66
94
  CompanyService = _ts_decorate([
@@ -70,10 +98,13 @@ CompanyService = _ts_decorate([
70
98
  _ts_param(0, Inject('CACHE_INSTANCE')),
71
99
  _ts_param(1, Inject(UtilsService)),
72
100
  _ts_param(2, Inject(AuthDataSourceProvider)),
101
+ _ts_param(3, Optional()),
102
+ _ts_param(3, Inject(PERMISSION_SYNC_ADAPTER)),
73
103
  _ts_metadata("design:type", Function),
74
104
  _ts_metadata("design:paramtypes", [
75
105
  typeof HybridCache === "undefined" ? Object : HybridCache,
76
106
  typeof UtilsService === "undefined" ? Object : UtilsService,
77
- typeof AuthDataSourceProvider === "undefined" ? Object : AuthDataSourceProvider
107
+ typeof AuthDataSourceProvider === "undefined" ? Object : AuthDataSourceProvider,
108
+ typeof IPermissionSyncAdapter === "undefined" ? Object : IPermissionSyncAdapter
78
109
  ])
79
110
  ], CompanyService);
@@ -0,0 +1,109 @@
1
+ import { createMockDataSourceProvider, createMockRepository } from '@test-utils/mocks/repository.mock';
2
+ import { CompanyService } from './company.service';
3
+ import { CompanyBranch } from '../entities/company-branch.entity';
4
+ import { UserCompanyPermission } from '../entities/user-company-permission.entity';
5
+ describe('CompanyService', ()=>{
6
+ let service;
7
+ let mockRepo;
8
+ let mockPermissionSyncAdapter;
9
+ let mockQueryRunner;
10
+ let branchRepo;
11
+ let permissionRepo;
12
+ beforeEach(()=>{
13
+ mockRepo = createMockRepository();
14
+ const mockDataSourceProvider = createMockDataSourceProvider(mockRepo);
15
+ mockPermissionSyncAdapter = {
16
+ onCompanyDeleted: jest.fn()
17
+ };
18
+ service = new CompanyService({}, {}, mockDataSourceProvider, mockPermissionSyncAdapter);
19
+ branchRepo = createMockRepository();
20
+ branchRepo.find.mockResolvedValue([]);
21
+ permissionRepo = createMockRepository();
22
+ mockQueryRunner = {
23
+ manager: {
24
+ getRepository: jest.fn().mockImplementation((entity)=>{
25
+ if (entity === CompanyBranch) return branchRepo;
26
+ if (entity === UserCompanyPermission) return permissionRepo;
27
+ throw new Error('Unexpected entity');
28
+ })
29
+ }
30
+ };
31
+ });
32
+ describe('beforeDeleteOperation', ()=>{
33
+ it('deletes permissions for the companies and their branches', async ()=>{
34
+ branchRepo.find.mockResolvedValue([
35
+ {
36
+ id: 'branch-1'
37
+ }
38
+ ]);
39
+ await service.beforeDeleteOperation({
40
+ id: [
41
+ 'company-1'
42
+ ]
43
+ }, null, mockQueryRunner);
44
+ const deleteArg = permissionRepo.delete.mock.calls[0][0];
45
+ expect(deleteArg.targetId._value).toEqual(expect.arrayContaining([
46
+ 'company-1',
47
+ 'branch-1'
48
+ ]));
49
+ });
50
+ it('handles a single (non-array) company id', async ()=>{
51
+ await service.beforeDeleteOperation({
52
+ id: 'company-1'
53
+ }, null, mockQueryRunner);
54
+ expect(branchRepo.find).toHaveBeenCalledWith(expect.objectContaining({
55
+ where: {
56
+ companyId: expect.anything()
57
+ }
58
+ }));
59
+ });
60
+ it('does nothing further when no permission sync adapter is configured', async ()=>{
61
+ service = new CompanyService({}, {}, createMockDataSourceProvider(mockRepo), undefined);
62
+ await expect(service.beforeDeleteOperation({
63
+ id: [
64
+ 'company-1'
65
+ ]
66
+ }, null, mockQueryRunner)).resolves.toBeUndefined();
67
+ });
68
+ it('notifies the permission sync adapter for each deleted company', async ()=>{
69
+ await service.beforeDeleteOperation({
70
+ id: [
71
+ 'company-1',
72
+ 'company-2'
73
+ ]
74
+ }, null, mockQueryRunner);
75
+ expect(mockPermissionSyncAdapter.onCompanyDeleted).toHaveBeenCalledWith('company-1');
76
+ expect(mockPermissionSyncAdapter.onCompanyDeleted).toHaveBeenCalledWith('company-2');
77
+ });
78
+ });
79
+ describe('getSelectQuery', ()=>{
80
+ it('applies the default selected fields when none are provided', async ()=>{
81
+ const qb = mockRepo.createQueryBuilder();
82
+ const { isRaw } = await service.getSelectQuery(qb, null);
83
+ expect(qb.select).toHaveBeenCalledWith(expect.arrayContaining([
84
+ 'company.id',
85
+ 'company.name',
86
+ 'company.slug'
87
+ ]));
88
+ expect(isRaw).toBe(false);
89
+ });
90
+ it('honors an explicit select list', async ()=>{
91
+ const qb = mockRepo.createQueryBuilder();
92
+ await service.getSelectQuery(qb, null, [
93
+ 'id'
94
+ ]);
95
+ expect(qb.select).toHaveBeenCalledWith([
96
+ 'company.id'
97
+ ]);
98
+ });
99
+ });
100
+ describe('getGlobalSearchQuery', ()=>{
101
+ it('adds a LIKE search clause across name/slug/email', async ()=>{
102
+ const qb = mockRepo.createQueryBuilder();
103
+ await service.getGlobalSearchQuery(qb, 'acme');
104
+ expect(qb.andWhere).toHaveBeenCalledWith(expect.stringContaining('company.name LIKE'), {
105
+ search: '%acme%'
106
+ });
107
+ });
108
+ });
109
+ });
@@ -26,7 +26,8 @@ function _ts_param(paramIndex, decorator) {
26
26
  };
27
27
  }
28
28
  import { LogAction } from '@flusys/nestjs-shared';
29
- import { ConflictException, Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
29
+ import { IPermissionSyncAdapter, PERMISSION_SYNC_ADAPTER } from '@flusys/nestjs-shared/interfaces';
30
+ import { ConflictException, Inject, Injectable, NotFoundException, Optional, Scope } from '@nestjs/common';
30
31
  import { USER_PERMISSION_MESSAGES } from '../config';
31
32
  import { PermissionType, UserCompanyPermission } from '../entities/user-company-permission.entity';
32
33
  import { AuthDataSourceProvider } from './auth-datasource.provider';
@@ -92,7 +93,7 @@ export class UserPermissionService {
92
93
  throw error;
93
94
  }
94
95
  }
95
- /** Revoke permission from user */ async revokePermission(userId, targetId, type) {
96
+ async revokePermission(userId, targetId, type) {
96
97
  const repository = await this.getRepository();
97
98
  const typeName = type === PermissionType.COMPANY ? 'company' : 'branch';
98
99
  const permission = await repository.findOne({
@@ -100,7 +101,10 @@ export class UserPermissionService {
100
101
  userId,
101
102
  targetId,
102
103
  permissionType: type
103
- }
104
+ },
105
+ relations: [
106
+ type === PermissionType.BRANCH ? 'branch' : 'company'
107
+ ]
104
108
  });
105
109
  if (!permission) {
106
110
  throw new NotFoundException({
@@ -111,7 +115,29 @@ export class UserPermissionService {
111
115
  }
112
116
  });
113
117
  }
118
+ const companyId = type === PermissionType.COMPANY ? targetId : permission.branch?.companyId;
114
119
  await repository.remove(permission);
120
+ if (type === PermissionType.COMPANY) {
121
+ const revokedBranchIds = await this.revokeBranchPermissionsForCompany(repository, userId, targetId);
122
+ await this.permissionSyncAdapter?.onUserCompanyAccessRevoked?.(userId, targetId);
123
+ await Promise.all(revokedBranchIds.map((branchId)=>this.permissionSyncAdapter?.onUserBranchAccessRevoked?.(userId, branchId, targetId)));
124
+ } else {
125
+ if (companyId) await this.permissionSyncAdapter?.onUserBranchAccessRevoked?.(userId, targetId, companyId);
126
+ }
127
+ }
128
+ async revokeBranchPermissionsForCompany(repository, userId, companyId) {
129
+ const branchPermissions = await repository.createQueryBuilder('permission').innerJoin('permission.branch', 'branch').where('permission.userId = :userId', {
130
+ userId
131
+ }).andWhere('permission.permissionType = :type', {
132
+ type: PermissionType.BRANCH
133
+ }).andWhere('branch.companyId = :companyId', {
134
+ companyId
135
+ }).getMany();
136
+ if (branchPermissions.length === 0) {
137
+ return [];
138
+ }
139
+ await repository.remove(branchPermissions);
140
+ return branchPermissions.map((permission)=>permission.targetId);
115
141
  }
116
142
  /** Find user branch permissions with optional company filter */ async findUserBranches(userId, companyId) {
117
143
  const repository = await this.getRepository();
@@ -127,12 +153,14 @@ export class UserPermissionService {
127
153
  }
128
154
  return queryBuilder.getMany();
129
155
  }
130
- constructor(dataSourceProvider){
156
+ constructor(dataSourceProvider, permissionSyncAdapter){
131
157
  _define_property(this, "dataSourceProvider", void 0);
158
+ _define_property(this, "permissionSyncAdapter", void 0);
132
159
  _define_property(this, "_repository", void 0);
133
160
  // Convenience Methods
134
161
  _define_property(this, "findUserCompanies", void 0);
135
162
  this.dataSourceProvider = dataSourceProvider;
163
+ this.permissionSyncAdapter = permissionSyncAdapter;
136
164
  this._repository = null;
137
165
  this.findUserCompanies = (userId)=>this.findUserPermissions(userId, PermissionType.COMPANY);
138
166
  }
@@ -168,8 +196,11 @@ UserPermissionService = _ts_decorate([
168
196
  scope: Scope.REQUEST
169
197
  }),
170
198
  _ts_param(0, Inject(AuthDataSourceProvider)),
199
+ _ts_param(1, Optional()),
200
+ _ts_param(1, Inject(PERMISSION_SYNC_ADAPTER)),
171
201
  _ts_metadata("design:type", Function),
172
202
  _ts_metadata("design:paramtypes", [
173
- typeof AuthDataSourceProvider === "undefined" ? Object : AuthDataSourceProvider
203
+ typeof AuthDataSourceProvider === "undefined" ? Object : AuthDataSourceProvider,
204
+ typeof IPermissionSyncAdapter === "undefined" ? Object : IPermissionSyncAdapter
174
205
  ])
175
206
  ], UserPermissionService);