@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.
- package/cjs/controllers/authentication.controller.spec.js +219 -0
- package/cjs/controllers/branch.controller.spec.js +134 -0
- package/cjs/controllers/company-selection.controller.spec.js +84 -0
- package/cjs/controllers/company.controller.spec.js +131 -0
- package/cjs/controllers/email-verification.controller.spec.js +142 -0
- package/cjs/controllers/user-permission.controller.spec.js +138 -0
- package/cjs/controllers/user.controller.spec.js +204 -0
- package/cjs/docs/auth-swagger.config.spec.js +130 -0
- package/cjs/entities/company-branch.entity.js +2 -3
- package/cjs/entities/index.spec.js +73 -0
- package/cjs/helpers/auth-response.mapper.js +46 -0
- package/cjs/helpers/auth-response.mapper.spec.js +139 -0
- package/cjs/helpers/index.js +19 -0
- package/cjs/helpers/token.helper.js +58 -0
- package/cjs/helpers/token.helper.spec.js +86 -0
- package/cjs/index.js +1 -0
- package/cjs/interceptors/clear-token.interceptor.spec.js +107 -0
- package/cjs/interceptors/set-token.interceptor.spec.js +165 -0
- package/cjs/modules/auth.module.spec.js +243 -0
- package/cjs/services/auth-config.service.spec.js +97 -0
- package/cjs/services/auth-datasource.provider.spec.js +161 -0
- package/cjs/services/auth-email.service.spec.js +404 -0
- package/cjs/services/authentication.service.js +37 -109
- package/cjs/services/authentication.service.spec.js +904 -0
- package/cjs/services/branch.service.js +30 -3
- package/cjs/services/branch.service.spec.js +154 -0
- package/cjs/services/company-selection-session.service.spec.js +90 -0
- package/cjs/services/company.service.js +34 -3
- package/cjs/services/company.service.spec.js +113 -0
- package/cjs/services/user-permission.service.js +35 -4
- package/cjs/services/user-permission.service.spec.js +202 -0
- package/cjs/services/user.service.spec.js +704 -0
- package/cjs/strategies/jwt.strategy.js +1 -1
- package/cjs/strategies/jwt.strategy.spec.js +166 -0
- package/cjs/validators/password.validator.spec.js +161 -0
- package/entities/company-branch.entity.d.ts +1 -1
- package/entities/pending-registration.entity.d.ts +2 -7
- package/fesm/controllers/authentication.controller.spec.js +215 -0
- package/fesm/controllers/branch.controller.spec.js +130 -0
- package/fesm/controllers/company-selection.controller.spec.js +80 -0
- package/fesm/controllers/company.controller.spec.js +127 -0
- package/fesm/controllers/email-verification.controller.spec.js +138 -0
- package/fesm/controllers/user-permission.controller.spec.js +134 -0
- package/fesm/controllers/user.controller.spec.js +200 -0
- package/fesm/docs/auth-swagger.config.spec.js +126 -0
- package/fesm/entities/company-branch.entity.js +2 -3
- package/fesm/entities/index.spec.js +69 -0
- package/fesm/helpers/auth-response.mapper.js +25 -0
- package/fesm/helpers/auth-response.mapper.spec.js +135 -0
- package/fesm/helpers/index.js +2 -0
- package/fesm/helpers/token.helper.js +40 -0
- package/fesm/helpers/token.helper.spec.js +82 -0
- package/fesm/index.js +1 -0
- package/fesm/interceptors/clear-token.interceptor.spec.js +103 -0
- package/fesm/interceptors/set-token.interceptor.spec.js +161 -0
- package/fesm/interfaces/authentication.interface.js +1 -1
- package/fesm/modules/auth.module.spec.js +239 -0
- package/fesm/services/auth-config.service.spec.js +93 -0
- package/fesm/services/auth-datasource.provider.spec.js +157 -0
- package/fesm/services/auth-email.service.spec.js +359 -0
- package/fesm/services/authentication.service.js +37 -109
- package/fesm/services/authentication.service.spec.js +859 -0
- package/fesm/services/branch.service.js +32 -5
- package/fesm/services/branch.service.spec.js +150 -0
- package/fesm/services/company-selection-session.service.spec.js +86 -0
- package/fesm/services/company.service.js +36 -5
- package/fesm/services/company.service.spec.js +109 -0
- package/fesm/services/user-permission.service.js +36 -5
- package/fesm/services/user-permission.service.spec.js +198 -0
- package/fesm/services/user.service.spec.js +659 -0
- package/fesm/strategies/jwt.strategy.js +1 -1
- package/fesm/strategies/jwt.strategy.spec.js +162 -0
- package/fesm/validators/password.validator.spec.js +157 -0
- package/helpers/auth-response.mapper.d.ts +22 -0
- package/helpers/index.d.ts +2 -0
- package/helpers/token.helper.d.ts +8 -0
- package/index.d.ts +1 -0
- package/interfaces/authentication.interface.d.ts +7 -0
- package/package.json +8 -3
- package/services/authentication.service.d.ts +1 -8
- package/services/branch.service.d.ts +7 -5
- package/services/company.service.d.ts +7 -4
- 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 {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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);
|