@flusys/nestjs-auth 6.4.1 → 7.0.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/config/event-actions.js +45 -0
- package/cjs/config/index.js +1 -0
- package/cjs/config/message-keys.js +12 -0
- package/cjs/modules/auth.module.js +2 -1
- package/cjs/services/auth-email.service.js +21 -0
- package/cjs/services/auth-email.service.spec.js +52 -0
- package/cjs/services/authentication.service.js +62 -4
- package/cjs/services/authentication.service.spec.js +58 -0
- package/cjs/services/branch.service.js +20 -0
- package/cjs/services/branch.service.spec.js +15 -0
- package/cjs/services/company.service.js +20 -0
- package/cjs/services/company.service.spec.js +14 -0
- package/cjs/services/user-permission.service.js +20 -0
- package/cjs/services/user-permission.service.spec.js +52 -0
- package/config/event-actions.d.ts +21 -0
- package/config/index.d.ts +1 -0
- package/config/message-keys.d.ts +6 -0
- package/fesm/config/event-actions.js +24 -0
- package/fesm/config/index.js +1 -0
- package/fesm/config/message-keys.js +6 -0
- package/fesm/modules/auth.module.js +3 -2
- package/fesm/services/auth-email.service.js +22 -1
- package/fesm/services/auth-email.service.spec.js +52 -0
- package/fesm/services/authentication.service.js +64 -6
- package/fesm/services/authentication.service.spec.js +58 -0
- package/fesm/services/branch.service.js +22 -2
- package/fesm/services/branch.service.spec.js +15 -0
- package/fesm/services/company.service.js +22 -2
- package/fesm/services/company.service.spec.js +14 -0
- package/fesm/services/user-permission.service.js +21 -1
- package/fesm/services/user-permission.service.spec.js +52 -0
- package/interfaces/auth-module-options.interface.d.ts +2 -0
- package/package.json +3 -3
- package/services/auth-email.service.d.ts +1 -0
- package/services/authentication.service.d.ts +3 -0
- package/services/user-permission.service.d.ts +1 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain event actions published by the auth module, alongside the CRUD
|
|
3
|
+
* lifecycle actions every ApiService emits.
|
|
4
|
+
*/ "use strict";
|
|
5
|
+
Object.defineProperty(exports, "__esModule", {
|
|
6
|
+
value: true
|
|
7
|
+
});
|
|
8
|
+
function _export(target, all) {
|
|
9
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
get: Object.getOwnPropertyDescriptor(all, name).get
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
_export(exports, {
|
|
15
|
+
get AUTH_EVENT_ACTIONS () {
|
|
16
|
+
return AUTH_EVENT_ACTIONS;
|
|
17
|
+
},
|
|
18
|
+
get AUTH_EVENT_ENTITIES () {
|
|
19
|
+
return AUTH_EVENT_ENTITIES;
|
|
20
|
+
},
|
|
21
|
+
get AUTH_EVENT_MODULE () {
|
|
22
|
+
return AUTH_EVENT_MODULE;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
const AUTH_EVENT_ACTIONS = {
|
|
26
|
+
LOGGED_IN: 'logged-in',
|
|
27
|
+
LOGGED_OUT: 'logged-out',
|
|
28
|
+
TOKEN_REFRESHED: 'token-refreshed',
|
|
29
|
+
COMPANY_SWITCHED: 'company-switched',
|
|
30
|
+
REGISTERED: 'registered',
|
|
31
|
+
PASSWORD_CHANGED: 'password-changed',
|
|
32
|
+
PASSWORD_RESET_REQUESTED: 'password-reset-requested',
|
|
33
|
+
PASSWORD_RESET: 'password-reset',
|
|
34
|
+
EMAIL_VERIFIED: 'email-verified',
|
|
35
|
+
PERMISSION_GRANTED: 'permission-granted',
|
|
36
|
+
PERMISSION_REVOKED: 'permission-revoked'
|
|
37
|
+
};
|
|
38
|
+
const AUTH_EVENT_ENTITIES = {
|
|
39
|
+
USER: 'appuser',
|
|
40
|
+
COMPANY: 'company',
|
|
41
|
+
COMPANY_BRANCH: 'company_branch',
|
|
42
|
+
SESSION: 'session',
|
|
43
|
+
USER_PERMISSION: 'user_company_permission'
|
|
44
|
+
};
|
|
45
|
+
const AUTH_EVENT_MODULE = 'auth';
|
package/cjs/config/index.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
3
3
|
value: true
|
|
4
4
|
});
|
|
5
5
|
_export_star(require("./auth.constants"), exports);
|
|
6
|
+
_export_star(require("./event-actions"), exports);
|
|
6
7
|
_export_star(require("./message-keys"), exports);
|
|
7
8
|
function _export_star(from, to) {
|
|
8
9
|
Object.keys(from).forEach(function(k) {
|
|
@@ -13,6 +13,12 @@ _export(exports, {
|
|
|
13
13
|
get AUTH_MESSAGES () {
|
|
14
14
|
return AUTH_MESSAGES;
|
|
15
15
|
},
|
|
16
|
+
get BRANCH_MESSAGES () {
|
|
17
|
+
return BRANCH_MESSAGES;
|
|
18
|
+
},
|
|
19
|
+
get COMPANY_MESSAGES () {
|
|
20
|
+
return COMPANY_MESSAGES;
|
|
21
|
+
},
|
|
16
22
|
get EMAIL_VERIFICATION_MESSAGES () {
|
|
17
23
|
return EMAIL_VERIFICATION_MESSAGES;
|
|
18
24
|
},
|
|
@@ -87,6 +93,12 @@ const USER_MESSAGES = {
|
|
|
87
93
|
STATUS_UPDATE_SUCCESS: 'user.status.update.success',
|
|
88
94
|
LOOKUP_SUCCESS: 'user.lookup.success'
|
|
89
95
|
};
|
|
96
|
+
const COMPANY_MESSAGES = {
|
|
97
|
+
READONLY_DELETE_FORBIDDEN: 'company.readonly.delete.forbidden'
|
|
98
|
+
};
|
|
99
|
+
const BRANCH_MESSAGES = {
|
|
100
|
+
READONLY_DELETE_FORBIDDEN: 'branch.readonly.delete.forbidden'
|
|
101
|
+
};
|
|
90
102
|
const USER_PERMISSION_MESSAGES = {
|
|
91
103
|
COMPANY_LIST_SUCCESS: 'user.permission.company.list.success',
|
|
92
104
|
BRANCH_LIST_SUCCESS: 'user.permission.branch.list.success',
|
|
@@ -103,7 +103,8 @@ let AuthModule = class AuthModule {
|
|
|
103
103
|
_services.AuthenticationService,
|
|
104
104
|
_settokeninterceptor.SetToken,
|
|
105
105
|
_cleartokeninterceptor.ClearToken,
|
|
106
|
-
_services.UserService
|
|
106
|
+
_services.UserService,
|
|
107
|
+
(0, _modules.createModuleEventsProvider)('auth', _authconstants.AUTH_MODULE_OPTIONS)
|
|
107
108
|
];
|
|
108
109
|
if (flags.enableEmailVerification) {
|
|
109
110
|
providers.push(_services.AuthEmailService);
|
|
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "AuthEmailService", {
|
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
11
|
const _nestjsshared = require("@flusys/nestjs-shared");
|
|
12
|
+
const _classes = require("@flusys/nestjs-shared/classes");
|
|
12
13
|
const _constants = require("@flusys/nestjs-shared/constants");
|
|
13
14
|
const _common = require("@nestjs/common");
|
|
14
15
|
const _bcrypt = /*#__PURE__*/ _interop_require_wildcard(require("bcrypt"));
|
|
@@ -242,6 +243,9 @@ let AuthEmailService = class AuthEmailService {
|
|
|
242
243
|
}
|
|
243
244
|
});
|
|
244
245
|
}
|
|
246
|
+
await this.publishUserEvent(_config.AUTH_EVENT_ACTIONS.PASSWORD_RESET_REQUESTED, user.id, {
|
|
247
|
+
email: user.email
|
|
248
|
+
});
|
|
245
249
|
return {
|
|
246
250
|
success: true,
|
|
247
251
|
message: 'If the email exists, a password reset link has been sent.',
|
|
@@ -283,6 +287,9 @@ let AuthEmailService = class AuthEmailService {
|
|
|
283
287
|
// Mark token as used
|
|
284
288
|
userToken.usedAt = new Date();
|
|
285
289
|
await tokenRepository.save(userToken);
|
|
290
|
+
await this.publishUserEvent(_config.AUTH_EVENT_ACTIONS.PASSWORD_RESET, user.id, {
|
|
291
|
+
email: user.email
|
|
292
|
+
});
|
|
286
293
|
return {
|
|
287
294
|
success: true,
|
|
288
295
|
message: 'Password has been reset successfully.',
|
|
@@ -366,12 +373,26 @@ let AuthEmailService = class AuthEmailService {
|
|
|
366
373
|
// Mark token as used
|
|
367
374
|
userToken.usedAt = new Date();
|
|
368
375
|
await tokenRepository.save(userToken);
|
|
376
|
+
await this.publishUserEvent(_config.AUTH_EVENT_ACTIONS.EMAIL_VERIFIED, user.id, {
|
|
377
|
+
email: user.email
|
|
378
|
+
});
|
|
369
379
|
return {
|
|
370
380
|
type: 'verified',
|
|
371
381
|
message: 'Email has been verified successfully.',
|
|
372
382
|
messageKey: _config.EMAIL_VERIFICATION_MESSAGES.VERIFICATION_SUCCESS
|
|
373
383
|
};
|
|
374
384
|
}
|
|
385
|
+
async publishUserEvent(action, userId, metadata) {
|
|
386
|
+
await (0, _classes.publishDomainEvent)({
|
|
387
|
+
module: _config.AUTH_EVENT_MODULE,
|
|
388
|
+
entity: _config.AUTH_EVENT_ENTITIES.USER,
|
|
389
|
+
action,
|
|
390
|
+
ids: [
|
|
391
|
+
userId
|
|
392
|
+
],
|
|
393
|
+
metadata
|
|
394
|
+
});
|
|
395
|
+
}
|
|
375
396
|
async resendVerificationEmail(email, verifyUrlBase) {
|
|
376
397
|
this.assertEmailEnabled();
|
|
377
398
|
const pendingRepository = await this.getRepository(_pendingregistrationentity.PendingRegistration);
|
|
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
const _common = require("@nestjs/common");
|
|
6
6
|
const _bcrypt = /*#__PURE__*/ _interop_require_wildcard(require("bcrypt"));
|
|
7
7
|
const _repositorymock = require("@test-utils/mocks/repository.mock");
|
|
8
|
+
const _classes = require("@flusys/nestjs-shared/classes");
|
|
8
9
|
const _authemailservice = require("./auth-email.service");
|
|
9
10
|
const _appuserentity = require("../entities/app-user.entity");
|
|
10
11
|
const _pendingregistrationentity = require("../entities/pending-registration.entity");
|
|
@@ -401,4 +402,55 @@ describe('AuthEmailService', ()=>{
|
|
|
401
402
|
})).rejects.toThrow(_common.InternalServerErrorException);
|
|
402
403
|
});
|
|
403
404
|
});
|
|
405
|
+
describe('domain events', ()=>{
|
|
406
|
+
let publish;
|
|
407
|
+
beforeEach(()=>{
|
|
408
|
+
publish = jest.fn().mockResolvedValue(undefined);
|
|
409
|
+
_classes.EventBusRegistry.reset();
|
|
410
|
+
_classes.EventBusRegistry.setBus({
|
|
411
|
+
publish,
|
|
412
|
+
subscribe: jest.fn(),
|
|
413
|
+
close: jest.fn()
|
|
414
|
+
});
|
|
415
|
+
_classes.EventBusRegistry.configureModule('auth', {
|
|
416
|
+
enabled: true
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
afterEach(()=>{
|
|
420
|
+
_classes.EventBusRegistry.reset();
|
|
421
|
+
});
|
|
422
|
+
it('publishes auth.appuser.password-reset-requested once the reset email is sent', async ()=>{
|
|
423
|
+
userRepo.findOne.mockResolvedValue(buildUser());
|
|
424
|
+
tokenRepo.save.mockResolvedValue(buildUserToken());
|
|
425
|
+
await service.forgotPassword('user@example.com', 'https://app.test/reset');
|
|
426
|
+
expect(publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
427
|
+
name: 'auth.appuser.password-reset-requested'
|
|
428
|
+
}));
|
|
429
|
+
});
|
|
430
|
+
it('publishes nothing for an unknown email so enumeration stays impossible', async ()=>{
|
|
431
|
+
userRepo.findOne.mockResolvedValue(null);
|
|
432
|
+
await service.forgotPassword('missing@example.com', 'https://app.test/reset');
|
|
433
|
+
expect(publish).not.toHaveBeenCalled();
|
|
434
|
+
});
|
|
435
|
+
it('publishes auth.appuser.password-reset when the token is consumed', async ()=>{
|
|
436
|
+
const token = buildUserToken();
|
|
437
|
+
tokenRepo.findOne.mockResolvedValue(token);
|
|
438
|
+
userRepo.findOne.mockResolvedValue(buildUser());
|
|
439
|
+
userRepo.save.mockResolvedValue(buildUser());
|
|
440
|
+
tokenRepo.save.mockResolvedValue(token);
|
|
441
|
+
await service.resetPassword('raw-token', 'NewPassword1!');
|
|
442
|
+
expect(publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
443
|
+
name: 'auth.appuser.password-reset'
|
|
444
|
+
}));
|
|
445
|
+
});
|
|
446
|
+
it('does not publish when the auth module has events disabled', async ()=>{
|
|
447
|
+
_classes.EventBusRegistry.configureModule('auth', {
|
|
448
|
+
enabled: false
|
|
449
|
+
});
|
|
450
|
+
userRepo.findOne.mockResolvedValue(buildUser());
|
|
451
|
+
tokenRepo.save.mockResolvedValue(buildUserToken());
|
|
452
|
+
await service.forgotPassword('user@example.com', 'https://app.test/reset');
|
|
453
|
+
expect(publish).not.toHaveBeenCalled();
|
|
454
|
+
});
|
|
455
|
+
});
|
|
404
456
|
});
|
|
@@ -579,7 +579,12 @@ let AuthenticationService = class AuthenticationService {
|
|
|
579
579
|
messageKey: _config.AUTH_MESSAGES.BRANCH_NOT_FOUND
|
|
580
580
|
});
|
|
581
581
|
}
|
|
582
|
-
|
|
582
|
+
const response = this.generateLoginResponseWithCompany(user, company, branch);
|
|
583
|
+
this.publishSessionEvent(_config.AUTH_EVENT_ACTIONS.COMPANY_SWITCHED, user, {
|
|
584
|
+
companyId: company.id,
|
|
585
|
+
branchId: branch.id
|
|
586
|
+
});
|
|
587
|
+
return response;
|
|
583
588
|
}
|
|
584
589
|
async refreshToken(refreshToken) {
|
|
585
590
|
try {
|
|
@@ -673,7 +678,7 @@ let AuthenticationService = class AuthenticationService {
|
|
|
673
678
|
}
|
|
674
679
|
}
|
|
675
680
|
}
|
|
676
|
-
|
|
681
|
+
const tokens = (0, _helpers.generateTokens)(this.jwtService, this.authConfig, {
|
|
677
682
|
id: user.id,
|
|
678
683
|
email: user.email,
|
|
679
684
|
type: 'access',
|
|
@@ -685,6 +690,11 @@ let AuthenticationService = class AuthenticationService {
|
|
|
685
690
|
branchId: payload.branchId
|
|
686
691
|
}
|
|
687
692
|
});
|
|
693
|
+
this.publishSessionEvent(_config.AUTH_EVENT_ACTIONS.TOKEN_REFRESHED, user, {
|
|
694
|
+
companyId: payload.companyId ?? null,
|
|
695
|
+
branchId: payload.branchId ?? null
|
|
696
|
+
});
|
|
697
|
+
return tokens;
|
|
688
698
|
} catch {
|
|
689
699
|
throw new _common.UnauthorizedException({
|
|
690
700
|
message: 'Invalid or expired refresh token',
|
|
@@ -711,6 +721,7 @@ let AuthenticationService = class AuthenticationService {
|
|
|
711
721
|
}
|
|
712
722
|
user.password = await _bcrypt.hash(newPassword, _config.BCRYPT_SALT_ROUNDS);
|
|
713
723
|
await userRepository.save(user);
|
|
724
|
+
this.publishUserEvent(_config.AUTH_EVENT_ACTIONS.PASSWORD_CHANGED, user);
|
|
714
725
|
}
|
|
715
726
|
async logout(userId) {
|
|
716
727
|
if (!this.cacheManager) {
|
|
@@ -724,6 +735,17 @@ let AuthenticationService = class AuthenticationService {
|
|
|
724
735
|
const refreshTokenExpiry = this.authConfig.getRefreshTokenExpiration();
|
|
725
736
|
const ttlMs = (0, _helpers.parseExpiryToMs)(refreshTokenExpiry);
|
|
726
737
|
await this.cacheManager.set(cacheKey, revokedAt, ttlMs);
|
|
738
|
+
await (0, _classes.publishDomainEvent)({
|
|
739
|
+
module: _config.AUTH_EVENT_MODULE,
|
|
740
|
+
entity: _config.AUTH_EVENT_ENTITIES.SESSION,
|
|
741
|
+
action: _config.AUTH_EVENT_ACTIONS.LOGGED_OUT,
|
|
742
|
+
ids: [
|
|
743
|
+
userId
|
|
744
|
+
],
|
|
745
|
+
user: {
|
|
746
|
+
id: userId
|
|
747
|
+
}
|
|
748
|
+
});
|
|
727
749
|
}
|
|
728
750
|
async isTokenRevoked(userId, tokenIssuedAt) {
|
|
729
751
|
if (!this.cacheManager || !tokenIssuedAt) {
|
|
@@ -897,6 +919,7 @@ let AuthenticationService = class AuthenticationService {
|
|
|
897
919
|
type: 'access'
|
|
898
920
|
});
|
|
899
921
|
this.sendLoginNotification(user.id);
|
|
922
|
+
this.publishSessionEvent(_config.AUTH_EVENT_ACTIONS.LOGGED_IN, user);
|
|
900
923
|
return {
|
|
901
924
|
accessToken,
|
|
902
925
|
refreshToken,
|
|
@@ -912,6 +935,10 @@ let AuthenticationService = class AuthenticationService {
|
|
|
912
935
|
branchId: branch?.id
|
|
913
936
|
});
|
|
914
937
|
this.sendLoginNotification(user.id, company.id);
|
|
938
|
+
this.publishSessionEvent(_config.AUTH_EVENT_ACTIONS.LOGGED_IN, user, {
|
|
939
|
+
companyId: company.id,
|
|
940
|
+
branchId: branch?.id ?? null
|
|
941
|
+
});
|
|
915
942
|
return {
|
|
916
943
|
accessToken,
|
|
917
944
|
refreshToken,
|
|
@@ -958,16 +985,47 @@ let AuthenticationService = class AuthenticationService {
|
|
|
958
985
|
};
|
|
959
986
|
}
|
|
960
987
|
generateRegistrationResponse(user) {
|
|
961
|
-
|
|
988
|
+
const response = {
|
|
962
989
|
...this.generateLoginResponse(user),
|
|
963
990
|
requiresEmailVerification: false
|
|
964
991
|
};
|
|
992
|
+
this.publishUserEvent(_config.AUTH_EVENT_ACTIONS.REGISTERED, user);
|
|
993
|
+
return response;
|
|
965
994
|
}
|
|
966
995
|
generateRegistrationResponseWithCompany(user, company, branch) {
|
|
967
|
-
|
|
996
|
+
const response = {
|
|
968
997
|
...this.generateLoginResponseWithCompany(user, company, branch),
|
|
969
998
|
requiresEmailVerification: false
|
|
970
999
|
};
|
|
1000
|
+
this.publishUserEvent(_config.AUTH_EVENT_ACTIONS.REGISTERED, user, {
|
|
1001
|
+
companyId: company.id,
|
|
1002
|
+
branchId: branch?.id ?? null
|
|
1003
|
+
});
|
|
1004
|
+
return response;
|
|
1005
|
+
}
|
|
1006
|
+
publishSessionEvent(action, user, metadata = {}) {
|
|
1007
|
+
this.publishAuthEvent(_config.AUTH_EVENT_ENTITIES.SESSION, action, user, metadata);
|
|
1008
|
+
}
|
|
1009
|
+
publishUserEvent(action, user, metadata = {}) {
|
|
1010
|
+
this.publishAuthEvent(_config.AUTH_EVENT_ENTITIES.USER, action, user, metadata);
|
|
1011
|
+
}
|
|
1012
|
+
/** Never awaited - a login must not wait on, or fail because of, the event bus */ publishAuthEvent(entity, action, user, metadata) {
|
|
1013
|
+
void (0, _classes.publishDomainEvent)({
|
|
1014
|
+
module: _config.AUTH_EVENT_MODULE,
|
|
1015
|
+
entity,
|
|
1016
|
+
action,
|
|
1017
|
+
ids: [
|
|
1018
|
+
user.id
|
|
1019
|
+
],
|
|
1020
|
+
metadata: {
|
|
1021
|
+
email: user.email,
|
|
1022
|
+
...metadata
|
|
1023
|
+
},
|
|
1024
|
+
user: {
|
|
1025
|
+
id: user.id,
|
|
1026
|
+
email: user.email
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
971
1029
|
}
|
|
972
1030
|
// NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
|
|
973
1031
|
constructor(jwtService, authConfig, cacheManager, dataSourceProvider, sessionService, authEmailService, userEnricher, notificationAdapter){
|
|
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
const _common = require("@nestjs/common");
|
|
6
6
|
const _bcrypt = /*#__PURE__*/ _interop_require_wildcard(require("bcrypt"));
|
|
7
7
|
const _repositorymock = require("@test-utils/mocks/repository.mock");
|
|
8
|
+
const _classes = require("@flusys/nestjs-shared/classes");
|
|
8
9
|
const _authenticationservice = require("./authentication.service");
|
|
9
10
|
const _appuserentity = require("../entities/app-user.entity");
|
|
10
11
|
const _companyentity = require("../entities/company.entity");
|
|
@@ -901,4 +902,61 @@ describe('AuthenticationService', ()=>{
|
|
|
901
902
|
}));
|
|
902
903
|
});
|
|
903
904
|
});
|
|
905
|
+
describe('domain events', ()=>{
|
|
906
|
+
let publish;
|
|
907
|
+
beforeEach(()=>{
|
|
908
|
+
publish = jest.fn().mockResolvedValue(undefined);
|
|
909
|
+
_classes.EventBusRegistry.reset();
|
|
910
|
+
_classes.EventBusRegistry.setBus({
|
|
911
|
+
publish,
|
|
912
|
+
subscribe: jest.fn(),
|
|
913
|
+
close: jest.fn()
|
|
914
|
+
});
|
|
915
|
+
_classes.EventBusRegistry.configureModule('auth', {
|
|
916
|
+
enabled: true
|
|
917
|
+
});
|
|
918
|
+
});
|
|
919
|
+
afterEach(()=>{
|
|
920
|
+
_classes.EventBusRegistry.reset();
|
|
921
|
+
});
|
|
922
|
+
it('publishes auth.session.logged-in when a session is issued', async ()=>{
|
|
923
|
+
userRepo.findOne.mockResolvedValue(buildUser());
|
|
924
|
+
_bcrypt.compare.mockResolvedValue(true);
|
|
925
|
+
const service = buildService();
|
|
926
|
+
await service.login('user@example.com', 'password');
|
|
927
|
+
expect(publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
928
|
+
name: 'auth.session.logged-in',
|
|
929
|
+
action: 'logged-in',
|
|
930
|
+
payload: expect.objectContaining({
|
|
931
|
+
ids: [
|
|
932
|
+
'user-1'
|
|
933
|
+
]
|
|
934
|
+
})
|
|
935
|
+
}));
|
|
936
|
+
});
|
|
937
|
+
it('publishes auth.session.logged-out on logout', async ()=>{
|
|
938
|
+
const service = buildService();
|
|
939
|
+
await service.logout('user-1');
|
|
940
|
+
expect(publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
941
|
+
name: 'auth.session.logged-out'
|
|
942
|
+
}));
|
|
943
|
+
});
|
|
944
|
+
it('publishes auth.appuser.password-changed after a successful password change', async ()=>{
|
|
945
|
+
userRepo.findOne.mockResolvedValue(buildUser());
|
|
946
|
+
_bcrypt.compare.mockResolvedValue(true);
|
|
947
|
+
const service = buildService();
|
|
948
|
+
await service.changePassword('user-1', 'old', 'new');
|
|
949
|
+
expect(publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
950
|
+
name: 'auth.appuser.password-changed'
|
|
951
|
+
}));
|
|
952
|
+
});
|
|
953
|
+
it('does not publish when the auth module has events disabled', async ()=>{
|
|
954
|
+
_classes.EventBusRegistry.configureModule('auth', {
|
|
955
|
+
enabled: false
|
|
956
|
+
});
|
|
957
|
+
const service = buildService();
|
|
958
|
+
await service.logout('user-1');
|
|
959
|
+
expect(publish).not.toHaveBeenCalled();
|
|
960
|
+
});
|
|
961
|
+
});
|
|
904
962
|
});
|
|
@@ -14,6 +14,7 @@ const _modules = require("@flusys/nestjs-shared/modules");
|
|
|
14
14
|
const _utils = require("@flusys/nestjs-shared/utils");
|
|
15
15
|
const _common = require("@nestjs/common");
|
|
16
16
|
const _typeorm = require("typeorm");
|
|
17
|
+
const _config = require("../config");
|
|
17
18
|
const _companybranchentity = require("../entities/company-branch.entity");
|
|
18
19
|
const _usercompanypermissionentity = require("../entities/user-company-permission.entity");
|
|
19
20
|
const _authconfigservice = require("./auth-config.service");
|
|
@@ -50,6 +51,24 @@ let BranchService = class BranchService extends _classes.ApiService {
|
|
|
50
51
|
const branchIds = Array.isArray(dto.id) ? dto.id : [
|
|
51
52
|
dto.id
|
|
52
53
|
];
|
|
54
|
+
await this.ensureDataSourceRepository();
|
|
55
|
+
const readonlyBranches = await this.repository.find({
|
|
56
|
+
where: {
|
|
57
|
+
id: (0, _typeorm.In)(branchIds),
|
|
58
|
+
readOnly: true,
|
|
59
|
+
deletedAt: (0, _typeorm.IsNull)()
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
if (readonlyBranches.length > 0) {
|
|
63
|
+
const names = readonlyBranches.map((b)=>b.name).join(', ');
|
|
64
|
+
throw new _common.ForbiddenException({
|
|
65
|
+
message: `Cannot delete readonly branch(es): ${names}`,
|
|
66
|
+
messageKey: _config.BRANCH_MESSAGES.READONLY_DELETE_FORBIDDEN,
|
|
67
|
+
messageVariables: {
|
|
68
|
+
names
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
53
72
|
await queryRunner.manager.getRepository(_usercompanypermissionentity.UserCompanyPermission).delete({
|
|
54
73
|
targetId: (0, _typeorm.In)(branchIds)
|
|
55
74
|
});
|
|
@@ -77,6 +96,7 @@ let BranchService = class BranchService extends _classes.ApiService {
|
|
|
77
96
|
'email',
|
|
78
97
|
'phone',
|
|
79
98
|
'isActive',
|
|
99
|
+
'readOnly',
|
|
80
100
|
'createdAt'
|
|
81
101
|
];
|
|
82
102
|
}
|
|
@@ -14,6 +14,7 @@ describe('BranchService', ()=>{
|
|
|
14
14
|
let mockQueryRunner;
|
|
15
15
|
beforeEach(()=>{
|
|
16
16
|
mockRepo = (0, _repositorymock.createMockRepository)();
|
|
17
|
+
mockRepo.find.mockResolvedValue([]);
|
|
17
18
|
Object.assign(mockRepo.createQueryBuilder(), {
|
|
18
19
|
leftJoinAndSelect: jest.fn().mockReturnThis(),
|
|
19
20
|
addOrderBy: jest.fn().mockReturnThis()
|
|
@@ -82,6 +83,20 @@ describe('BranchService', ()=>{
|
|
|
82
83
|
}, null, mockQueryRunner);
|
|
83
84
|
expect(mockPermissionSyncAdapter.onBranchDeleted).toHaveBeenCalledWith('branch-1', 'company-1');
|
|
84
85
|
});
|
|
86
|
+
it('rejects deleting a readonly branch', async ()=>{
|
|
87
|
+
mockRepo.find.mockResolvedValue([
|
|
88
|
+
{
|
|
89
|
+
id: 'branch-1',
|
|
90
|
+
name: 'Default Branch',
|
|
91
|
+
readOnly: true
|
|
92
|
+
}
|
|
93
|
+
]);
|
|
94
|
+
const permissionRepo = mockQueryRunner.manager.getRepository(_usercompanypermissionentity.UserCompanyPermission);
|
|
95
|
+
await expect(service.beforeDeleteOperation({
|
|
96
|
+
id: 'branch-1'
|
|
97
|
+
}, null, mockQueryRunner)).rejects.toThrow('Cannot delete readonly branch');
|
|
98
|
+
expect(permissionRepo.delete).not.toHaveBeenCalled();
|
|
99
|
+
});
|
|
85
100
|
});
|
|
86
101
|
describe('getSelectQuery', ()=>{
|
|
87
102
|
it('applies the default selected fields when none are provided', async ()=>{
|
|
@@ -13,6 +13,7 @@ const _interfaces = require("@flusys/nestjs-shared/interfaces");
|
|
|
13
13
|
const _modules = require("@flusys/nestjs-shared/modules");
|
|
14
14
|
const _common = require("@nestjs/common");
|
|
15
15
|
const _typeorm = require("typeorm");
|
|
16
|
+
const _config = require("../config");
|
|
16
17
|
const _companybranchentity = require("../entities/company-branch.entity");
|
|
17
18
|
const _companyentity = require("../entities/company.entity");
|
|
18
19
|
const _usercompanypermissionentity = require("../entities/user-company-permission.entity");
|
|
@@ -49,6 +50,24 @@ let CompanyService = class CompanyService extends _classes.ApiService {
|
|
|
49
50
|
const companyIds = Array.isArray(dto.id) ? dto.id : [
|
|
50
51
|
dto.id
|
|
51
52
|
];
|
|
53
|
+
await this.ensureDataSourceRepository();
|
|
54
|
+
const readonlyCompanies = await this.repository.find({
|
|
55
|
+
where: {
|
|
56
|
+
id: (0, _typeorm.In)(companyIds),
|
|
57
|
+
readOnly: true,
|
|
58
|
+
deletedAt: (0, _typeorm.IsNull)()
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
if (readonlyCompanies.length > 0) {
|
|
62
|
+
const names = readonlyCompanies.map((c)=>c.name).join(', ');
|
|
63
|
+
throw new _common.ForbiddenException({
|
|
64
|
+
message: `Cannot delete readonly company(ies): ${names}`,
|
|
65
|
+
messageKey: _config.COMPANY_MESSAGES.READONLY_DELETE_FORBIDDEN,
|
|
66
|
+
messageVariables: {
|
|
67
|
+
names
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
52
71
|
const branchRows = await queryRunner.manager.getRepository(_companybranchentity.CompanyBranch).find({
|
|
53
72
|
where: {
|
|
54
73
|
companyId: (0, _typeorm.In)(companyIds)
|
|
@@ -79,6 +98,7 @@ let CompanyService = class CompanyService extends _classes.ApiService {
|
|
|
79
98
|
'email',
|
|
80
99
|
'phone',
|
|
81
100
|
'isActive',
|
|
101
|
+
'readOnly',
|
|
82
102
|
'createdAt'
|
|
83
103
|
];
|
|
84
104
|
}
|
|
@@ -15,6 +15,7 @@ describe('CompanyService', ()=>{
|
|
|
15
15
|
let permissionRepo;
|
|
16
16
|
beforeEach(()=>{
|
|
17
17
|
mockRepo = (0, _repositorymock.createMockRepository)();
|
|
18
|
+
mockRepo.find.mockResolvedValue([]);
|
|
18
19
|
const mockDataSourceProvider = (0, _repositorymock.createMockDataSourceProvider)(mockRepo);
|
|
19
20
|
mockPermissionSyncAdapter = {
|
|
20
21
|
onCompanyDeleted: jest.fn()
|
|
@@ -79,6 +80,19 @@ describe('CompanyService', ()=>{
|
|
|
79
80
|
expect(mockPermissionSyncAdapter.onCompanyDeleted).toHaveBeenCalledWith('company-1');
|
|
80
81
|
expect(mockPermissionSyncAdapter.onCompanyDeleted).toHaveBeenCalledWith('company-2');
|
|
81
82
|
});
|
|
83
|
+
it('rejects deleting a readonly company', async ()=>{
|
|
84
|
+
mockRepo.find.mockResolvedValue([
|
|
85
|
+
{
|
|
86
|
+
id: 'company-1',
|
|
87
|
+
name: 'Default Company',
|
|
88
|
+
readOnly: true
|
|
89
|
+
}
|
|
90
|
+
]);
|
|
91
|
+
await expect(service.beforeDeleteOperation({
|
|
92
|
+
id: 'company-1'
|
|
93
|
+
}, null, mockQueryRunner)).rejects.toThrow('Cannot delete readonly company');
|
|
94
|
+
expect(branchRepo.find).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
82
96
|
});
|
|
83
97
|
describe('getSelectQuery', ()=>{
|
|
84
98
|
it('applies the default selected fields when none are provided', async ()=>{
|
|
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "UserPermissionService", {
|
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
11
|
const _nestjsshared = require("@flusys/nestjs-shared");
|
|
12
|
+
const _classes = require("@flusys/nestjs-shared/classes");
|
|
12
13
|
const _interfaces = require("@flusys/nestjs-shared/interfaces");
|
|
13
14
|
const _common = require("@nestjs/common");
|
|
14
15
|
const _config = require("../config");
|
|
@@ -102,6 +103,7 @@ let UserPermissionService = class UserPermissionService {
|
|
|
102
103
|
}
|
|
103
104
|
throw error;
|
|
104
105
|
}
|
|
106
|
+
await this.publishPermissionEvent(_config.AUTH_EVENT_ACTIONS.PERMISSION_GRANTED, userId, targetId, type);
|
|
105
107
|
}
|
|
106
108
|
async revokePermission(userId, targetId, type) {
|
|
107
109
|
const repository = await this.getRepository();
|
|
@@ -134,6 +136,24 @@ let UserPermissionService = class UserPermissionService {
|
|
|
134
136
|
} else {
|
|
135
137
|
if (companyId) await this.permissionSyncAdapter?.onUserBranchAccessRevoked?.(userId, targetId, companyId);
|
|
136
138
|
}
|
|
139
|
+
await this.publishPermissionEvent(_config.AUTH_EVENT_ACTIONS.PERMISSION_REVOKED, userId, targetId, type, companyId);
|
|
140
|
+
}
|
|
141
|
+
async publishPermissionEvent(action, userId, targetId, type, companyId) {
|
|
142
|
+
await (0, _classes.publishDomainEvent)({
|
|
143
|
+
module: _config.AUTH_EVENT_MODULE,
|
|
144
|
+
entity: _config.AUTH_EVENT_ENTITIES.USER_PERMISSION,
|
|
145
|
+
action,
|
|
146
|
+
ids: [
|
|
147
|
+
targetId
|
|
148
|
+
],
|
|
149
|
+
metadata: {
|
|
150
|
+
targetUserId: userId,
|
|
151
|
+
permissionType: type,
|
|
152
|
+
...companyId ? {
|
|
153
|
+
companyId
|
|
154
|
+
} : {}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
137
157
|
}
|
|
138
158
|
async revokeBranchPermissionsForCompany(repository, userId, companyId) {
|
|
139
159
|
const branchPermissions = await repository.createQueryBuilder('permission').innerJoin('permission.branch', 'branch').where('permission.userId = :userId', {
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
3
3
|
value: true
|
|
4
4
|
});
|
|
5
5
|
const _common = require("@nestjs/common");
|
|
6
|
+
const _classes = require("@flusys/nestjs-shared/classes");
|
|
6
7
|
const _repositorymock = require("@test-utils/mocks/repository.mock");
|
|
7
8
|
const _userpermissionservice = require("./user-permission.service");
|
|
8
9
|
const _usercompanypermissionentity = require("../entities/user-company-permission.entity");
|
|
@@ -199,4 +200,55 @@ describe('UserPermissionService', ()=>{
|
|
|
199
200
|
});
|
|
200
201
|
});
|
|
201
202
|
});
|
|
203
|
+
describe('domain events', ()=>{
|
|
204
|
+
let publish;
|
|
205
|
+
beforeEach(()=>{
|
|
206
|
+
publish = jest.fn().mockResolvedValue(undefined);
|
|
207
|
+
_classes.EventBusRegistry.reset();
|
|
208
|
+
_classes.EventBusRegistry.setBus({
|
|
209
|
+
publish,
|
|
210
|
+
subscribe: jest.fn(),
|
|
211
|
+
close: jest.fn()
|
|
212
|
+
});
|
|
213
|
+
_classes.EventBusRegistry.configureModule('auth', {
|
|
214
|
+
enabled: true
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
afterEach(()=>{
|
|
218
|
+
_classes.EventBusRegistry.reset();
|
|
219
|
+
});
|
|
220
|
+
it('publishes permission-granted after a new assignment', async ()=>{
|
|
221
|
+
mockRepo.findOne.mockResolvedValue(null);
|
|
222
|
+
mockRepo.create.mockReturnValue(buildPermission());
|
|
223
|
+
mockRepo.save.mockResolvedValue(buildPermission());
|
|
224
|
+
await service.assignPermission('user-1', 'company-1', _usercompanypermissionentity.PermissionType.COMPANY);
|
|
225
|
+
expect(publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
226
|
+
name: 'auth.user_company_permission.permission-granted',
|
|
227
|
+
payload: expect.objectContaining({
|
|
228
|
+
ids: [
|
|
229
|
+
'company-1'
|
|
230
|
+
],
|
|
231
|
+
metadata: expect.objectContaining({
|
|
232
|
+
targetUserId: 'user-1',
|
|
233
|
+
permissionType: _usercompanypermissionentity.PermissionType.COMPANY
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
}));
|
|
237
|
+
});
|
|
238
|
+
it('does not publish when the assignment is rejected as a duplicate', async ()=>{
|
|
239
|
+
mockRepo.findOne.mockResolvedValue(buildPermission());
|
|
240
|
+
await expect(service.assignPermission('user-1', 'company-1', _usercompanypermissionentity.PermissionType.COMPANY)).rejects.toThrow(_common.ConflictException);
|
|
241
|
+
expect(publish).not.toHaveBeenCalled();
|
|
242
|
+
});
|
|
243
|
+
it('does not publish when the auth module has events disabled', async ()=>{
|
|
244
|
+
_classes.EventBusRegistry.configureModule('auth', {
|
|
245
|
+
enabled: false
|
|
246
|
+
});
|
|
247
|
+
mockRepo.findOne.mockResolvedValue(null);
|
|
248
|
+
mockRepo.create.mockReturnValue(buildPermission());
|
|
249
|
+
mockRepo.save.mockResolvedValue(buildPermission());
|
|
250
|
+
await service.assignPermission('user-1', 'company-1', _usercompanypermissionentity.PermissionType.COMPANY);
|
|
251
|
+
expect(publish).not.toHaveBeenCalled();
|
|
252
|
+
});
|
|
253
|
+
});
|
|
202
254
|
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const AUTH_EVENT_ACTIONS: {
|
|
2
|
+
readonly LOGGED_IN: "logged-in";
|
|
3
|
+
readonly LOGGED_OUT: "logged-out";
|
|
4
|
+
readonly TOKEN_REFRESHED: "token-refreshed";
|
|
5
|
+
readonly COMPANY_SWITCHED: "company-switched";
|
|
6
|
+
readonly REGISTERED: "registered";
|
|
7
|
+
readonly PASSWORD_CHANGED: "password-changed";
|
|
8
|
+
readonly PASSWORD_RESET_REQUESTED: "password-reset-requested";
|
|
9
|
+
readonly PASSWORD_RESET: "password-reset";
|
|
10
|
+
readonly EMAIL_VERIFIED: "email-verified";
|
|
11
|
+
readonly PERMISSION_GRANTED: "permission-granted";
|
|
12
|
+
readonly PERMISSION_REVOKED: "permission-revoked";
|
|
13
|
+
};
|
|
14
|
+
export declare const AUTH_EVENT_ENTITIES: {
|
|
15
|
+
readonly USER: "appuser";
|
|
16
|
+
readonly COMPANY: "company";
|
|
17
|
+
readonly COMPANY_BRANCH: "company_branch";
|
|
18
|
+
readonly SESSION: "session";
|
|
19
|
+
readonly USER_PERMISSION: "user_company_permission";
|
|
20
|
+
};
|
|
21
|
+
export declare const AUTH_EVENT_MODULE = "auth";
|