@flusys/nestjs-auth 6.4.0 → 7.0.0-beta.1

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 (40) hide show
  1. package/cjs/config/event-actions.js +45 -0
  2. package/cjs/config/index.js +1 -0
  3. package/cjs/config/message-keys.js +12 -0
  4. package/cjs/modules/auth.module.js +2 -1
  5. package/cjs/services/auth-email.service.js +21 -0
  6. package/cjs/services/auth-email.service.spec.js +52 -0
  7. package/cjs/services/authentication.service.js +62 -4
  8. package/cjs/services/authentication.service.spec.js +58 -0
  9. package/cjs/services/branch.service.js +20 -0
  10. package/cjs/services/branch.service.spec.js +15 -0
  11. package/cjs/services/company.service.js +20 -0
  12. package/cjs/services/company.service.spec.js +14 -0
  13. package/cjs/services/user-permission.service.js +20 -0
  14. package/cjs/services/user-permission.service.spec.js +52 -0
  15. package/cjs/services/user.service.js +36 -18
  16. package/cjs/services/user.service.spec.js +15 -1
  17. package/config/event-actions.d.ts +21 -0
  18. package/config/index.d.ts +1 -0
  19. package/config/message-keys.d.ts +6 -0
  20. package/fesm/config/event-actions.js +24 -0
  21. package/fesm/config/index.js +1 -0
  22. package/fesm/config/message-keys.js +6 -0
  23. package/fesm/modules/auth.module.js +3 -2
  24. package/fesm/services/auth-email.service.js +22 -1
  25. package/fesm/services/auth-email.service.spec.js +52 -0
  26. package/fesm/services/authentication.service.js +64 -6
  27. package/fesm/services/authentication.service.spec.js +58 -0
  28. package/fesm/services/branch.service.js +22 -2
  29. package/fesm/services/branch.service.spec.js +15 -0
  30. package/fesm/services/company.service.js +22 -2
  31. package/fesm/services/company.service.spec.js +14 -0
  32. package/fesm/services/user-permission.service.js +21 -1
  33. package/fesm/services/user-permission.service.spec.js +52 -0
  34. package/fesm/services/user.service.js +36 -18
  35. package/fesm/services/user.service.spec.js +15 -1
  36. package/interfaces/auth-module-options.interface.d.ts +2 -0
  37. package/package.json +3 -3
  38. package/services/auth-email.service.d.ts +1 -0
  39. package/services/authentication.service.d.ts +3 -0
  40. 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';
@@ -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
- return this.generateLoginResponseWithCompany(user, company, branch);
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
- return (0, _helpers.generateTokens)(this.jwtService, this.authConfig, {
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
- return {
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
- return {
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
  });