@tomei/sso 0.34.2 → 0.34.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2343 @@
1
+ import { ClassError, LoginUserBase, UserBase } from '@tomei/general';
2
+ import { ISessionService } from '../../session/interfaces/session-service.interface';
3
+ import { IUserAttr, IUserInfo } from './interfaces/user-info.interface';
4
+ import { UserRepository } from './user.repository';
5
+ import { SystemRepository } from '../system/system.repository';
6
+ import { LoginHistoryRepository } from '../login-history/login-history.repository';
7
+ import { PasswordHashService } from '../password-hash/password-hash.service';
8
+ import { UserGroupRepository } from '../user-group/user-group.repository';
9
+ import { SMTPMailer } from '@tomei/mailer';
10
+ import { ISystemLogin } from '../../../src/interfaces/system-login.interface';
11
+ import Staff from '../../models/staff.entity';
12
+ import SystemPrivilege from '../../models/system-privilege.entity';
13
+ import LoginHistory from '../../models/login-history.entity';
14
+ import { YN } from '../../enum/yn.enum';
15
+ import { UserStatus } from '../../enum';
16
+ import { ApplicationConfig, ComponentConfig } from '@tomei/config';
17
+ import { ICheckUserInfoDuplicatedQuery } from './interfaces/check-user-info-duplicated.interface';
18
+ import { Op, where } from 'sequelize';
19
+ import { ActionEnum, Activity } from '@tomei/activity-history';
20
+ import UserModel from '../../models/user.entity';
21
+ import GroupModel from '../../models/group.entity';
22
+ import { GroupSystemAccessRepository } from '../group-system-access/group-system-access.repository';
23
+ import { GroupRepository } from '../group/group.repository';
24
+ import SystemModel from '../../models/system.entity';
25
+ import { ISystemAccess } from './interfaces/system-access.interface';
26
+ import { UserSystemAccessRepository } from '../user-system-access/user-system-access.repository';
27
+ import GroupSystemAccessModel from '../../models/group-system-access.entity';
28
+ import { UserPrivilegeRepository } from '../user-privilege/user-privilege.repository';
29
+ import { UserObjectPrivilegeRepository } from '../user-object-privilege/user-object-privilege.repository';
30
+ import GroupPrivilegeModel from '../../models/group-privilege.entity';
31
+ import { GroupObjectPrivilegeRepository } from '../group-object-privilege/group-object-privilege.repository';
32
+ import * as speakeasy from 'speakeasy';
33
+ import { LoginStatusEnum } from '../../enum/login-status.enum';
34
+ import { RedisService } from '../../redis-client/redis.service';
35
+ import { LoginUser } from './login-user';
36
+
37
+ export class User extends UserBase {
38
+ ObjectId: string;
39
+ Email: string;
40
+ private _UserName: string;
41
+ private _Password: string;
42
+ private _Status: UserStatus;
43
+ private _DefaultPasswordChangedYN: YN;
44
+ private _FirstLoginAt: Date;
45
+ private _LastLoginAt: Date;
46
+ private _MFAEnabled: number;
47
+ private _MFAConfig: string;
48
+ private _RecoveryEmail: string;
49
+ private _FailedLoginAttemptCount: number;
50
+ private _LastFailedLoginAt: Date;
51
+ private _LastPasswordChangedAt: Date;
52
+ private _NeedToChangePasswordYN: YN;
53
+ private _CreatedById: number;
54
+ private _CreatedAt: Date;
55
+ private _UpdatedById: number;
56
+ private _UpdatedAt: Date;
57
+ ObjectName = 'User';
58
+ TableName = 'sso_Users';
59
+ ObjectType = 'User';
60
+ staffs: any;
61
+
62
+ private _OriginIP: string;
63
+ protected _SessionService: ISessionService;
64
+ protected static _RedisService: RedisService;
65
+ protected static _Repository = new UserRepository();
66
+
67
+ private static _LoginHistoryRepository = new LoginHistoryRepository();
68
+ protected static _UserGroupRepo = new UserGroupRepository();
69
+ private static _UserPrivilegeRepo = new UserPrivilegeRepository();
70
+ private static _UserObjectPrivilegeRepo = new UserObjectPrivilegeRepository();
71
+ private static _GroupObjectPrivilegeRepo =
72
+ new GroupObjectPrivilegeRepository();
73
+
74
+ private static _SystemRepository = new SystemRepository();
75
+ protected static _UserSystemAccessRepo = new UserSystemAccessRepository();
76
+ private static _GroupSystemAccessRepo = new GroupSystemAccessRepository();
77
+ private static _GroupRepo = new GroupRepository();
78
+
79
+ private _dbTransaction: any;
80
+
81
+ get SessionService(): ISessionService {
82
+ return this._SessionService;
83
+ }
84
+
85
+ get UserId(): number {
86
+ return parseInt(this.ObjectId);
87
+ }
88
+
89
+ private set UserId(value: number) {
90
+ this.ObjectId = value.toString();
91
+ }
92
+
93
+ get Password(): string {
94
+ return this._Password;
95
+ }
96
+
97
+ private set Password(value: string) {
98
+ this._Password = value;
99
+ }
100
+
101
+ get Status(): UserStatus {
102
+ return this._Status;
103
+ }
104
+
105
+ private set Status(value: UserStatus) {
106
+ this._Status = value;
107
+ }
108
+
109
+ get UserName(): string {
110
+ return this._UserName;
111
+ }
112
+
113
+ set UserName(value: string) {
114
+ this._UserName = value;
115
+ }
116
+
117
+ get DefaultPasswordChangedYN(): YN {
118
+ return this._DefaultPasswordChangedYN;
119
+ }
120
+
121
+ private set DefaultPasswordChangedYN(value: YN) {
122
+ this._DefaultPasswordChangedYN = value;
123
+ }
124
+
125
+ get FirstLoginAt(): Date {
126
+ return this._FirstLoginAt;
127
+ }
128
+
129
+ private set FirstLoginAt(value: Date) {
130
+ this._FirstLoginAt = value;
131
+ }
132
+
133
+ get LastLoginAt(): Date {
134
+ return this._LastLoginAt;
135
+ }
136
+
137
+ private set LastLoginAt(value: Date) {
138
+ this._LastLoginAt = value;
139
+ }
140
+
141
+ get MFAEnabled(): number {
142
+ return this._MFAEnabled;
143
+ }
144
+
145
+ private set MFAEnabled(value: number) {
146
+ this._MFAEnabled = value;
147
+ }
148
+
149
+ get MFAConfig(): string {
150
+ return this._MFAConfig;
151
+ }
152
+
153
+ private set MFAConfig(value: string) {
154
+ this._MFAConfig = value;
155
+ }
156
+
157
+ get RecoveryEmail(): string {
158
+ return this._RecoveryEmail;
159
+ }
160
+
161
+ private set RecoveryEmail(value: string) {
162
+ this._RecoveryEmail = value;
163
+ }
164
+
165
+ get FailedLoginAttemptCount(): number {
166
+ return this._FailedLoginAttemptCount;
167
+ }
168
+
169
+ private set FailedLoginAttemptCount(value: number) {
170
+ this._FailedLoginAttemptCount = value;
171
+ }
172
+
173
+ get LastFailedLoginAt(): Date {
174
+ return this._LastFailedLoginAt;
175
+ }
176
+
177
+ private set LastFailedLoginAt(value: Date) {
178
+ this._LastFailedLoginAt = value;
179
+ }
180
+
181
+ get LastPasswordChangedAt(): Date {
182
+ return this._LastPasswordChangedAt;
183
+ }
184
+
185
+ private set LastPasswordChangedAt(value: Date) {
186
+ this._LastPasswordChangedAt = value;
187
+ }
188
+
189
+ get NeedToChangePasswordYN(): YN {
190
+ return this._NeedToChangePasswordYN;
191
+ }
192
+
193
+ private set NeedToChangePasswordYN(value: YN) {
194
+ this._NeedToChangePasswordYN = value;
195
+ }
196
+
197
+ get CreatedById(): number {
198
+ return this._CreatedById;
199
+ }
200
+
201
+ private set CreatedById(value: number) {
202
+ this._CreatedById = value;
203
+ }
204
+
205
+ get CreatedAt(): Date {
206
+ return this._CreatedAt;
207
+ }
208
+
209
+ private set CreatedAt(value: Date) {
210
+ this._CreatedAt = value;
211
+ }
212
+
213
+ get UpdatedById(): number {
214
+ return this._UpdatedById;
215
+ }
216
+
217
+ private set UpdatedById(value: number) {
218
+ this._UpdatedById = value;
219
+ }
220
+
221
+ get UpdatedAt(): Date {
222
+ return this._UpdatedAt;
223
+ }
224
+
225
+ private set UpdatedAt(value: Date) {
226
+ this._UpdatedAt = value;
227
+ }
228
+
229
+ async getDetails(): Promise<{
230
+ FullName: string;
231
+ UserName: string;
232
+ IDNo: string;
233
+ IDType: string;
234
+ Email: string;
235
+ ContactNo: string;
236
+ }> {
237
+ return {
238
+ FullName: this.FullName,
239
+ UserName: this.UserName,
240
+ IDNo: this.IDNo,
241
+ IDType: this.IDType,
242
+ Email: this.Email,
243
+ ContactNo: this.ContactNo,
244
+ };
245
+ }
246
+
247
+ constructor(
248
+ sessionService: ISessionService,
249
+ dbTransaction?: any,
250
+ userInfo?: IUserAttr,
251
+ ) {
252
+ super();
253
+ this._SessionService = sessionService;
254
+
255
+ if (dbTransaction) {
256
+ this._dbTransaction = dbTransaction;
257
+ }
258
+ // set all the class properties
259
+ if (userInfo) {
260
+ this.UserId = userInfo.UserId;
261
+ this.UserName = userInfo.FullName;
262
+ this.FullName = userInfo.FullName;
263
+ this.IDNo = userInfo.IDNo;
264
+ this.Email = userInfo.Email;
265
+ this.ContactNo = userInfo.ContactNo;
266
+ this.Password = userInfo.Password;
267
+ this.staffs = userInfo.staffs;
268
+ this.Status = userInfo.Status;
269
+ this.DefaultPasswordChangedYN = userInfo.DefaultPasswordChangedYN;
270
+ this.FirstLoginAt = userInfo.FirstLoginAt;
271
+ this.LastLoginAt = userInfo.LastLoginAt;
272
+ this.MFAEnabled = userInfo.MFAEnabled;
273
+ this.MFAConfig = userInfo.MFAConfig;
274
+ this.RecoveryEmail = userInfo.RecoveryEmail;
275
+ this.FailedLoginAttemptCount = userInfo.FailedLoginAttemptCount;
276
+ this.LastFailedLoginAt = userInfo.LastFailedLoginAt;
277
+ this.LastPasswordChangedAt = userInfo.LastPasswordChangedAt;
278
+ this.NeedToChangePasswordYN = userInfo.NeedToChangePasswordYN;
279
+ this.CreatedById = userInfo.CreatedById;
280
+ this.CreatedAt = userInfo.CreatedAt;
281
+ this.UpdatedById = userInfo.UpdatedById;
282
+ this.UpdatedAt = userInfo.UpdatedAt;
283
+ }
284
+ }
285
+
286
+ static async init(
287
+ sessionService: ISessionService,
288
+ userId?: number,
289
+ dbTransaction = null,
290
+ ): Promise<User> {
291
+ User._RedisService = await RedisService.init();
292
+ if (userId) {
293
+ if (dbTransaction) {
294
+ User._Repository = new UserRepository();
295
+ }
296
+ const user = await User._Repository.findOne({
297
+ where: {
298
+ UserId: userId,
299
+ },
300
+ include: [
301
+ {
302
+ model: Staff,
303
+ },
304
+ ],
305
+ transaction: dbTransaction,
306
+ });
307
+
308
+ if (!user) {
309
+ throw new Error('Invalid credentials.');
310
+ }
311
+
312
+ if (user) {
313
+ const userAttr: IUserAttr = {
314
+ UserId: user.UserId,
315
+ UserName: user.UserName,
316
+ FullName: user?.Staff?.FullName,
317
+ IDNo: user?.Staff?.IdNo,
318
+ ContactNo: user?.Staff?.Mobile,
319
+ Email: user.Email,
320
+ Password: user.Password,
321
+ Status: user.Status,
322
+ DefaultPasswordChangedYN: user.DefaultPasswordChangedYN,
323
+ FirstLoginAt: user.FirstLoginAt,
324
+ LastLoginAt: user.LastLoginAt,
325
+ MFAEnabled: user.MFAEnabled,
326
+ MFAConfig: user.MFAConfig,
327
+ RecoveryEmail: user.RecoveryEmail,
328
+ FailedLoginAttemptCount: user.FailedLoginAttemptCount,
329
+ LastFailedLoginAt: user.LastFailedLoginAt,
330
+ LastPasswordChangedAt: user.LastPasswordChangedAt,
331
+ NeedToChangePasswordYN: user.NeedToChangePasswordYN,
332
+ CreatedById: user.CreatedById,
333
+ CreatedAt: user.CreatedAt,
334
+ UpdatedById: user.UpdatedById,
335
+ UpdatedAt: user.UpdatedAt,
336
+ staffs: user?.Staff,
337
+ };
338
+
339
+ return new User(sessionService, dbTransaction, userAttr);
340
+ } else {
341
+ throw new Error('User not found');
342
+ }
343
+ }
344
+ return new User(sessionService, dbTransaction);
345
+ }
346
+
347
+ async setEmail(email: string, dbTransaction): Promise<void> {
348
+ try {
349
+ //Check if email is not the same as the current email if it is, skip all the steps
350
+ if (this.Email === email) {
351
+ return;
352
+ }
353
+
354
+ //Check if email is duplicated, if yes, throw error
355
+ const user = await User._Repository.findOne({
356
+ where: {
357
+ Email: email,
358
+ },
359
+ transaction: dbTransaction,
360
+ });
361
+
362
+ if (user) {
363
+ throw new ClassError(
364
+ 'LoginUser',
365
+ 'LoginUserErrMsg0X',
366
+ 'Email already exists',
367
+ );
368
+ }
369
+
370
+ //Update the email
371
+ this.Email = email;
372
+ } catch (error) {
373
+ throw error;
374
+ }
375
+ }
376
+
377
+ async login(
378
+ systemCode: string,
379
+ email: string,
380
+ password: string,
381
+ ipAddress: string,
382
+ dbTransaction,
383
+ ): Promise<LoginUser> {
384
+ try {
385
+ //validate email
386
+ if (!this.ObjectId) {
387
+ // 1.1: Retrieve user data by calling _Repo.findOne with below parameter
388
+ const user = await User._Repository.findOne({
389
+ transaction: dbTransaction,
390
+ where: {
391
+ Email: email,
392
+ Status: {
393
+ [Op.or]: [UserStatus.ACTIVE, UserStatus.LOCKED],
394
+ },
395
+ },
396
+ include: [
397
+ {
398
+ model: Staff,
399
+ },
400
+ ],
401
+ });
402
+
403
+ // 1.2: If Exist populate all of the object attributes with the user data retrieved from previous step. if not throw Class Error
404
+ if (user) {
405
+ const userAttr: IUserAttr = {
406
+ UserId: user.UserId,
407
+ UserName: user.UserName,
408
+ FullName: user?.Staff?.FullName || null,
409
+ IDNo: user?.Staff?.IdNo || null,
410
+ ContactNo: user?.Staff?.Mobile || null,
411
+ Email: user.Email,
412
+ Password: user.Password,
413
+ Status: user.Status,
414
+ DefaultPasswordChangedYN: user.DefaultPasswordChangedYN,
415
+ FirstLoginAt: user.FirstLoginAt,
416
+ LastLoginAt: user.LastLoginAt,
417
+ MFAEnabled: user.MFAEnabled,
418
+ MFAConfig: user.MFAConfig,
419
+ RecoveryEmail: user.RecoveryEmail,
420
+ FailedLoginAttemptCount: user.FailedLoginAttemptCount,
421
+ LastFailedLoginAt: user.LastFailedLoginAt,
422
+ LastPasswordChangedAt: user.LastPasswordChangedAt,
423
+ NeedToChangePasswordYN: user.NeedToChangePasswordYN,
424
+ CreatedById: user.CreatedById,
425
+ CreatedAt: user.CreatedAt,
426
+ UpdatedById: user.UpdatedById,
427
+ UpdatedAt: user.UpdatedAt,
428
+ staffs: user?.Staff || null,
429
+ };
430
+
431
+ this.UserId = userAttr.UserId;
432
+ this.FullName = userAttr.FullName;
433
+ this.IDNo = userAttr.IDNo;
434
+ this.Email = userAttr.Email;
435
+ this.ContactNo = userAttr.ContactNo;
436
+ this.Password = userAttr.Password;
437
+ this.Status = userAttr.Status;
438
+ this.DefaultPasswordChangedYN = userAttr.DefaultPasswordChangedYN;
439
+ this.FirstLoginAt = userAttr.FirstLoginAt;
440
+ this.LastLoginAt = userAttr.LastLoginAt;
441
+ this.MFAEnabled = userAttr.MFAEnabled;
442
+ this.MFAConfig = userAttr.MFAConfig;
443
+ this.RecoveryEmail = userAttr.RecoveryEmail;
444
+ this.FailedLoginAttemptCount = userAttr.FailedLoginAttemptCount;
445
+ this.LastFailedLoginAt = userAttr.LastFailedLoginAt;
446
+ this.LastPasswordChangedAt = userAttr.LastPasswordChangedAt;
447
+ this.NeedToChangePasswordYN = userAttr.NeedToChangePasswordYN;
448
+ this.CreatedById = userAttr.CreatedById;
449
+ this.CreatedAt = userAttr.CreatedAt;
450
+ this.UpdatedById = userAttr.UpdatedById;
451
+ this.UpdatedAt = userAttr.UpdatedAt;
452
+ this.staffs = userAttr.staffs;
453
+ } else {
454
+ throw new ClassError('User', 'UserErrMsg0X', 'Invalid Credentials');
455
+ }
456
+ }
457
+
458
+ if (this.ObjectId && this.Email !== email) {
459
+ throw new Error('Invalid credentials.');
460
+ }
461
+
462
+ //Call LoginUser.check2FA
463
+ const check2FA = await User.check2FA(this, dbTransaction);
464
+
465
+ //validate system code
466
+
467
+ // 1.3: From here on until step 1.8. If any of the validation is failed, skip the other step and call incrementFailedLoginAttemptCount
468
+
469
+ try {
470
+ // 1.4: Validate the system user trying to access by calling __SystemRepository.findOne with below parameter.
471
+ // If system does not exist, skip all below step and return to step 3
472
+ const system = await User._SystemRepository.findOne({
473
+ where: {
474
+ SystemCode: systemCode,
475
+ Status: 'Active',
476
+ },
477
+ });
478
+ if (!system) {
479
+ throw new Error('Invalid credentials.');
480
+ }
481
+
482
+ // 1.5: Instantiate new PasswordHashService object and call PasswordHashService.verify method to check whether the param.Password is correct.
483
+ // If not, skip all below step and return to step 3.
484
+ const passwordHashService = new PasswordHashService();
485
+ const isPasswordValid = await passwordHashService.verify(
486
+ password,
487
+ this.Password,
488
+ );
489
+ if (!isPasswordValid) {
490
+ throw new Error('Invalid credentials.');
491
+ }
492
+
493
+ // 1.6: Validate the user access to the system by calling . if it return false, skip all below step and return to step 3
494
+ await this.checkSystemAccess(
495
+ this.UserId,
496
+ system.SystemCode,
497
+ dbTransaction,
498
+ );
499
+
500
+ // 1.7: f this.Status = "Locked" , call shouldReleaseLock
501
+ if (this.Status === UserStatus.LOCKED) {
502
+ const isReleaseLock = User.shouldReleaseLock(this.LastFailedLoginAt);
503
+ // 1.8: if the previous step returns true, call releaseLock then update this.Status = "Active". if false, skip all below step and return to step 3
504
+ if (isReleaseLock) {
505
+ await User.releaseLock(this.UserId, dbTransaction);
506
+ this.Status = UserStatus.ACTIVE;
507
+ } else {
508
+ throw new Error('Invalid credentials.');
509
+ }
510
+ }
511
+ } catch (error) {
512
+ await this.incrementFailedLoginAttemptCount(dbTransaction);
513
+ }
514
+
515
+ // 2.1: Call alertNewLogin to check whether the ip used is new ip and alert the user if it's new.
516
+ const system = await User._SystemRepository.findOne({
517
+ where: {
518
+ SystemCode: systemCode,
519
+ },
520
+ });
521
+ await this.alertNewLogin(this.ObjectId, system.SystemCode, ipAddress);
522
+
523
+ // 2.2 : Set below properties :
524
+ // FailedLoginAttemptCount: 0.
525
+ // If FirstLoginAt is empty, this.FirstLoginAt = <current timestamp>
526
+ // LastLoginAt = <current timestamp>
527
+ this.FailedLoginAttemptCount = 0;
528
+ this.LastLoginAt = new Date();
529
+ if (!this.FirstLoginAt) {
530
+ this.FirstLoginAt = new Date();
531
+ }
532
+
533
+ // 2.3: Call _Repo.update and update user data in the db to the current object properties value. Dont forget to use dbTransaction.
534
+ await User._Repository.update(
535
+ {
536
+ FullName: this.FullName,
537
+ UserName: this.UserName,
538
+ IDNo: this.IDNo,
539
+ Email: this.Email,
540
+ ContactNo: this.ContactNo,
541
+ Password: this.Password,
542
+ Status: this.Status,
543
+ DefaultPasswordChangedYN: this.DefaultPasswordChangedYN,
544
+ FirstLoginAt: this.FirstLoginAt,
545
+ LastLoginAt: this.LastLoginAt,
546
+ MFAEnabled: this.MFAEnabled,
547
+ MFAConfig: this.MFAConfig,
548
+ RecoveryEmail: this.RecoveryEmail,
549
+ FailedLoginAttemptCount: this.FailedLoginAttemptCount,
550
+ LastFailedLoginAt: this.LastFailedLoginAt,
551
+ LastPasswordChangedAt: this.LastPasswordChangedAt,
552
+ NeedToChangePasswordYN: this.NeedToChangePasswordYN,
553
+ },
554
+ {
555
+ where: {
556
+ UserId: this.UserId,
557
+ },
558
+ transaction: dbTransaction,
559
+ },
560
+ );
561
+
562
+ // fetch user session if exists
563
+ const userSession = await this._SessionService.retrieveUserSession(
564
+ this.ObjectId,
565
+ );
566
+ let systemLogin = userSession.systemLogins.find(
567
+ (system) => system.code === systemCode,
568
+ );
569
+
570
+ // generate new session id
571
+ const { randomUUID } = require('crypto');
572
+ const sessionId = randomUUID();
573
+
574
+ if (systemLogin) {
575
+ systemLogin = systemLogin.sessionId = sessionId;
576
+ userSession.systemLogins.map((system) =>
577
+ system.code === systemCode ? systemLogin : system,
578
+ );
579
+ } else {
580
+ // if not, add new system login into the userSession
581
+ const newLogin = {
582
+ id: system.SystemCode,
583
+ code: system.SystemCode,
584
+ sessionId: sessionId,
585
+ privileges: await this.getPrivileges(
586
+ system.SystemCode,
587
+ dbTransaction,
588
+ ),
589
+ };
590
+ userSession.systemLogins.push(newLogin);
591
+ }
592
+ // then update userSession inside the redis storage with 1 day duration of time-to-live
593
+ this._SessionService.setUserSession(this.ObjectId, userSession);
594
+
595
+ // record new login history
596
+ await User._LoginHistoryRepository.create(
597
+ {
598
+ UserId: this.UserId,
599
+ SystemCode: system.SystemCode,
600
+ OriginIp: ipAddress,
601
+ CreatedAt: new Date(),
602
+ LoginStatus: LoginStatusEnum.SUCCESS,
603
+ },
604
+ {
605
+ transaction: dbTransaction,
606
+ },
607
+ );
608
+
609
+ // Retrieve is2FAEnabledYN from sso-config with ComponentConfig.
610
+ const is2FAEnabledYN = ComponentConfig.getComponentConfigValue(
611
+ '@tomei/sso',
612
+ 'is2FAEnabledYN',
613
+ );
614
+ const loginUser = await LoginUser.init(
615
+ this.SessionService,
616
+ this.UserId,
617
+ dbTransaction,
618
+ );
619
+
620
+ if (is2FAEnabledYN === 'Y') {
621
+ loginUser.session.Id = `${this.UserId}:`;
622
+ } else {
623
+ loginUser.session.Id = `${this.UserId}:${sessionId}`;
624
+ }
625
+
626
+ return loginUser;
627
+ } catch (error) {
628
+ if (this.ObjectId) {
629
+ await User._LoginHistoryRepository.create(
630
+ {
631
+ UserId: this.UserId,
632
+ SystemCode: systemCode,
633
+ OriginIp: ipAddress,
634
+ LoginStatus: LoginStatusEnum.FAILURE,
635
+ CreatedAt: new Date(),
636
+ },
637
+ {
638
+ transaction: dbTransaction,
639
+ },
640
+ );
641
+ }
642
+ throw error;
643
+ }
644
+ }
645
+
646
+ protected async checkSystemAccess(
647
+ userId: number,
648
+ systemCode: string,
649
+ dbTransaction?: any,
650
+ ): Promise<void> {
651
+ try {
652
+ let isUserHaveAccess = false;
653
+
654
+ const systemAccess = await User._UserSystemAccessRepo.findOne({
655
+ where: {
656
+ UserId: userId,
657
+ SystemCode: systemCode,
658
+ Status: 'Active',
659
+ },
660
+ dbTransaction,
661
+ });
662
+
663
+ if (systemAccess) {
664
+ isUserHaveAccess = true;
665
+ } else {
666
+ const userGroups = await User._UserGroupRepo.findAll({
667
+ where: {
668
+ UserId: userId,
669
+ InheritGroupAccessYN: 'Y',
670
+ Status: 'Active',
671
+ },
672
+ include: [
673
+ {
674
+ model: GroupModel,
675
+ },
676
+ ],
677
+ dbTransaction,
678
+ });
679
+
680
+ for (const usergroup of userGroups) {
681
+ const group = usergroup.Group;
682
+ const groupSystemAccess = await User.getInheritedSystemAccess(
683
+ dbTransaction,
684
+ group,
685
+ );
686
+
687
+ for (const system of groupSystemAccess) {
688
+ if (system.SystemCode === systemCode) {
689
+ isUserHaveAccess = true;
690
+ break;
691
+ }
692
+ }
693
+ }
694
+ }
695
+
696
+ if (!isUserHaveAccess) {
697
+ throw new Error("User don't have access to the system.");
698
+ }
699
+ } catch (error) {
700
+ throw error;
701
+ }
702
+ }
703
+
704
+ async checkPrivileges(
705
+ systemCode: string,
706
+ privilegeName: string,
707
+ ): Promise<boolean> {
708
+ try {
709
+ if (!this.ObjectId) {
710
+ throw new Error('ObjectId(UserId) is not set');
711
+ }
712
+
713
+ const userSession = await this._SessionService.retrieveUserSession(
714
+ this.ObjectId,
715
+ );
716
+
717
+ const systemLogin = userSession.systemLogins.find(
718
+ (system) => system.code === systemCode,
719
+ );
720
+
721
+ if (!systemLogin) {
722
+ return false;
723
+ }
724
+
725
+ const privileges = systemLogin.privileges;
726
+ const hasPrivilege = privileges.includes(privilegeName);
727
+ return hasPrivilege;
728
+ } catch (error) {
729
+ throw error;
730
+ }
731
+ }
732
+
733
+ private async alertNewLogin(
734
+ userId: string,
735
+ systemCode: string,
736
+ ipAddress: string,
737
+ ) {
738
+ try {
739
+ const userLogins = await User._LoginHistoryRepository.findAll({
740
+ where: {
741
+ UserId: userId,
742
+ SystemCode: systemCode,
743
+ },
744
+ });
745
+
746
+ const gotPreviousLogins = userLogins?.length !== 0;
747
+ let ipFound: LoginHistory | undefined = undefined;
748
+ if (gotPreviousLogins) {
749
+ ipFound = userLogins.find((item) => item.OriginIp === ipAddress);
750
+ }
751
+
752
+ // if (gotPreviousLogins && !ipFound) {
753
+ // const EMAIL_SENDER =
754
+ // process.env.EMAIL_SENDER || 'itd-system@tomei.com.my';
755
+ // const transporter = new SMTPMailer();
756
+
757
+ // await transporter.sendMail({
758
+ // from: EMAIL_SENDER,
759
+ // to: this.Email,
760
+ // subject: 'New Login Alert',
761
+ // html: `<p>Dear ${this.FullName},</p>
762
+ // <p>There was a new login to your account from ${ipAddress} on ${new Date().toLocaleString()}.</p>
763
+ // <p>If this was you, you can safely ignore this email.</p>
764
+ // <p>If you suspect that someone else is trying to access your account, please contact us immediately at itd-support@tomei.com.my.</p>
765
+ // <p>Thank you!,</p>
766
+ // <p>
767
+ // Best Regards,
768
+ // IT Department
769
+ // </p>`,
770
+ // });
771
+ // }
772
+ } catch (error) {
773
+ throw error;
774
+ }
775
+ }
776
+
777
+ private async getPrivileges(
778
+ systemCode: string,
779
+ dbTransaction?: any,
780
+ ): Promise<string[]> {
781
+ try {
782
+ const system = await User._SystemRepository.findOne({
783
+ where: {
784
+ SystemCode: systemCode,
785
+ },
786
+ transaction: dbTransaction,
787
+ });
788
+
789
+ if (!system) {
790
+ throw new Error('Invalid system code.');
791
+ }
792
+
793
+ /**
794
+ * Ways user can get privileges:
795
+ * 1. Privileges directly assigned to the user using UserPrivilege
796
+ * 2. User have object that have privileges
797
+ * 3. User have group that can inherit privileges
798
+ * 3. User have group that have parent group that can inherit privileges to said group
799
+ */
800
+
801
+ //Retrive privileges directly assigned to the user
802
+ const userPrivileges = await this.getUserPersonalPrivileges(
803
+ systemCode,
804
+ dbTransaction,
805
+ );
806
+
807
+ //Retrieve privileges from object that user have
808
+ const objectPrivileges = await this.getObjectPrivileges(
809
+ systemCode,
810
+ dbTransaction,
811
+ );
812
+
813
+ //Retrieve privileges from group that able to inherit privileges to user
814
+ //Retrieve all user groups own by user that can inherit privileges for the system
815
+ const userGroupOwnByUser = await User._UserGroupRepo.findAll({
816
+ where: {
817
+ UserId: this.UserId,
818
+ InheritGroupSystemAccessYN: 'Y',
819
+ InheritGroupPrivilegeYN: 'Y',
820
+ Status: 'Active',
821
+ },
822
+ include: [
823
+ {
824
+ model: GroupModel,
825
+ where: {
826
+ Status: 'Active',
827
+ },
828
+ include: [
829
+ {
830
+ model: GroupSystemAccessModel,
831
+ where: {
832
+ SystemCode: systemCode,
833
+ },
834
+ },
835
+ ],
836
+ },
837
+ ],
838
+ transaction: dbTransaction,
839
+ });
840
+
841
+ //Get all privileges from groups data
842
+ let groupsPrivileges: string[] = [];
843
+ for (const userGroup of userGroupOwnByUser) {
844
+ const gp: string[] = await this.getInheritedPrivileges(
845
+ userGroup.GroupCode,
846
+ systemCode,
847
+ dbTransaction,
848
+ );
849
+ groupsPrivileges = [...groupsPrivileges, ...gp];
850
+ }
851
+
852
+ //Map all privileges to a single array
853
+ const privileges: string[] = [
854
+ ...userPrivileges,
855
+ ...objectPrivileges,
856
+ ...groupsPrivileges,
857
+ ];
858
+ return privileges;
859
+ } catch (error) {
860
+ throw error;
861
+ }
862
+ }
863
+
864
+ private async getInheritedPrivileges(
865
+ groupCode: string,
866
+ systemCode: string,
867
+ dbTransaction?: string,
868
+ ): Promise<string[]> {
869
+ try {
870
+ // Retrieve Group from the database based on groupCode
871
+ const group = await User._GroupRepo.findOne({
872
+ where: {
873
+ GroupCode: groupCode,
874
+ Status: 'Active',
875
+ },
876
+ include: [
877
+ {
878
+ model: GroupPrivilegeModel,
879
+ where: {
880
+ Status: 'Active',
881
+ },
882
+ include: [
883
+ {
884
+ model: SystemPrivilege,
885
+ where: {
886
+ SystemCode: systemCode,
887
+ Status: 'Active',
888
+ },
889
+ },
890
+ ],
891
+ },
892
+ ],
893
+ transaction: dbTransaction,
894
+ });
895
+
896
+ // retrieve group ObjectPrivileges
897
+ const objectPrivileges = await User._GroupObjectPrivilegeRepo.findAll({
898
+ where: {
899
+ GroupCode: groupCode,
900
+ },
901
+ include: {
902
+ model: SystemPrivilege,
903
+ where: {
904
+ SystemCode: systemCode,
905
+ Status: 'Active',
906
+ },
907
+ },
908
+ transaction: dbTransaction,
909
+ });
910
+
911
+ let privileges: string[] = [];
912
+ // Add privileges from the group to the privileges array
913
+ const groupPrivileges: string[] = [];
914
+ for (const groupPrivilege of group.GroupPrivileges) {
915
+ groupPrivileges.push(groupPrivilege.Privilege.PrivilegeCode);
916
+ }
917
+
918
+ const ops: string[] = [];
919
+ for (const objectPrivilege of objectPrivileges) {
920
+ ops.push(objectPrivilege.Privilege.PrivilegeCode);
921
+ }
922
+
923
+ privileges = [...privileges, ...groupPrivileges, ...ops];
924
+
925
+ // Recursive call if not root and allow inherit privileges from parent group
926
+ if (group.ParentGroupCode && group.InheritParentPrivilegeYN === 'Y') {
927
+ const parentGroupPrivileges = await this.getInheritedPrivileges(
928
+ group.ParentGroupCode,
929
+ systemCode,
930
+ dbTransaction,
931
+ );
932
+ privileges = [...privileges, ...parentGroupPrivileges];
933
+ }
934
+
935
+ return privileges;
936
+ } catch (error) {
937
+ throw error;
938
+ }
939
+ }
940
+
941
+ private async getUserPersonalPrivileges(
942
+ systemCode: string,
943
+ dbTransaction?: any,
944
+ ): Promise<string[]> {
945
+ try {
946
+ const userPrivileges = await User._UserPrivilegeRepo.findAll({
947
+ where: {
948
+ UserId: this.UserId,
949
+ Status: 'Active',
950
+ },
951
+ include: {
952
+ model: SystemPrivilege,
953
+ where: {
954
+ SystemCode: systemCode,
955
+ Status: 'Active',
956
+ },
957
+ },
958
+ transaction: dbTransaction,
959
+ });
960
+
961
+ const privileges: string[] = userPrivileges.map(
962
+ (u) => u.Privilege.PrivilegeCode,
963
+ );
964
+ return privileges;
965
+ } catch (error) {
966
+ throw error;
967
+ }
968
+ }
969
+
970
+ private async getObjectPrivileges(
971
+ systemCode: string,
972
+ dbTransaction?: any,
973
+ ): Promise<string[]> {
974
+ try {
975
+ const userObjectPrivileges = await User._UserObjectPrivilegeRepo.findAll({
976
+ where: {
977
+ UserId: this.UserId,
978
+ },
979
+ include: {
980
+ model: SystemPrivilege,
981
+ where: {
982
+ SystemCode: systemCode,
983
+ Status: 'Active',
984
+ },
985
+ },
986
+ transaction: dbTransaction,
987
+ });
988
+
989
+ const privilegesCodes: string[] = userObjectPrivileges.map(
990
+ (u) => u.Privilege.PrivilegeCode,
991
+ );
992
+ return privilegesCodes;
993
+ } catch (error) {
994
+ throw error;
995
+ }
996
+ }
997
+
998
+ private static async checkUserInfoDuplicated(
999
+ dbTransaction: any,
1000
+ query: ICheckUserInfoDuplicatedQuery,
1001
+ ) {
1002
+ //This method if check if duplicate user info found.
1003
+ try {
1004
+ //Part 1: Prepare Query Params
1005
+ //Params is all optional but at least one is required.
1006
+ const { Email, UserName, IdType, IdNo, ContactNo } = query;
1007
+ //Prepare the Params to be used as OR operation in SQL query.
1008
+ const where = {
1009
+ [Op.or]: {},
1010
+ };
1011
+ if (Email) {
1012
+ where[Op.or]['Email'] = Email;
1013
+ }
1014
+
1015
+ if (UserName) {
1016
+ where[Op.or]['UserName'] = UserName;
1017
+ }
1018
+ //If Params.IdNo is not null, then Params.IdType is required and vice versa.
1019
+ if (IdType && IdNo) {
1020
+ where[Op.or]['IdType'] = IdType;
1021
+ where[Op.or]['IdNo'] = IdNo;
1022
+ }
1023
+ if (ContactNo) {
1024
+ where[Op.or]['ContactNo'] = ContactNo;
1025
+ }
1026
+ //Call LoginUser._Repo findOne method by passing the OR operation object based on query params in Part 1. Code example can be referred at bottom part. Make sure to pass the dbTransaction
1027
+ const user = await User._Repository.findAll({
1028
+ where,
1029
+ transaction: dbTransaction,
1030
+ });
1031
+
1032
+ if (user && user.length > 0) {
1033
+ throw new ClassError(
1034
+ 'LoginUser',
1035
+ 'LoginUserErrMsg0X',
1036
+ 'User info already exists',
1037
+ );
1038
+ }
1039
+ } catch (error) {
1040
+ throw error;
1041
+ }
1042
+ }
1043
+
1044
+ private static generateDefaultPassword(): string {
1045
+ //This method will generate default password for user.
1046
+ try {
1047
+ //Part 1: Retrieve Password Policy
1048
+ //Retrieve all password policy from component config, call ComponentConfig.getComponentConfigValue method
1049
+ const passwordPolicy = ComponentConfig.getComponentConfigValue(
1050
+ '@tomei/sso',
1051
+ 'passwordPolicy',
1052
+ );
1053
+
1054
+ //Make sure all passwordPolicy keys got values, if not throw new ClassError
1055
+ if (
1056
+ !passwordPolicy ||
1057
+ !passwordPolicy.maxLen ||
1058
+ !passwordPolicy.minLen ||
1059
+ !passwordPolicy.nonAcceptableChar ||
1060
+ !passwordPolicy.numOfCapitalLetters ||
1061
+ !passwordPolicy.numOfNumbers ||
1062
+ !passwordPolicy.numOfSpecialChars
1063
+ ) {
1064
+ throw new ClassError(
1065
+ 'LoginUser',
1066
+ 'LoginUserErrMsg0X',
1067
+ 'Missing password policy. Please set in config file.',
1068
+ );
1069
+ }
1070
+
1071
+ if (
1072
+ passwordPolicy.numOfCapitalLetters +
1073
+ passwordPolicy.numOfNumbers +
1074
+ passwordPolicy.numOfSpecialChars >
1075
+ passwordPolicy.maxLen
1076
+ ) {
1077
+ throw new ClassError(
1078
+ 'LoginUser',
1079
+ 'LoginUserErrMsg0X',
1080
+ 'Password policy is invalid. Please set in config file.',
1081
+ );
1082
+ }
1083
+
1084
+ //Part 2: Generate Random Password and returns
1085
+ //Generate random password based on passwordPolicy
1086
+ const {
1087
+ maxLen,
1088
+ minLen,
1089
+ nonAcceptableChar,
1090
+ numOfCapitalLetters,
1091
+ numOfNumbers,
1092
+ numOfSpecialChars,
1093
+ } = passwordPolicy;
1094
+ const passwordLength =
1095
+ Math.floor(Math.random() * (maxLen - minLen + 1)) + minLen;
1096
+ const words = 'abcdefghijklmnopqrstuvwxyz';
1097
+ const capitalLetters = words.toUpperCase();
1098
+ const numbers = '0123456789';
1099
+ const specialChars = '!@#$%^&*()_+-={}[]|:;"<>,.?/~`';
1100
+ const nonAcceptableChars: string[] = nonAcceptableChar.split(',');
1101
+
1102
+ const filteredWords: string[] = words
1103
+ .split('')
1104
+ .filter((word) => !nonAcceptableChars.includes(word));
1105
+ const filteredCapitalLetters: string[] = capitalLetters
1106
+ .split('')
1107
+ .filter((word) => !nonAcceptableChars.includes(word));
1108
+ const filteredNumbers: string[] = numbers
1109
+ .split('')
1110
+ .filter((word) => !nonAcceptableChars.includes(word));
1111
+ const filteredSpecialChars: string[] = specialChars
1112
+ .split('')
1113
+ .filter((word) => !nonAcceptableChars.includes(word));
1114
+
1115
+ const generatedCapitalLetters: string[] = [];
1116
+ const generatedNumbers: string[] = [];
1117
+ const generatedSpecialChars: string[] = [];
1118
+ const generatedWords: string[] = [];
1119
+
1120
+ for (let i = 0; i < numOfCapitalLetters; i++) {
1121
+ const randomIndex = Math.floor(
1122
+ Math.random() * filteredCapitalLetters.length,
1123
+ );
1124
+ generatedCapitalLetters.push(filteredCapitalLetters[randomIndex]);
1125
+ }
1126
+
1127
+ for (let i = 0; i < numOfNumbers; i++) {
1128
+ const randomIndex = Math.floor(Math.random() * filteredNumbers.length);
1129
+ generatedNumbers.push(filteredNumbers[randomIndex]);
1130
+ }
1131
+
1132
+ for (let i = 0; i < numOfSpecialChars; i++) {
1133
+ const randomIndex = Math.floor(
1134
+ Math.random() * filteredSpecialChars.length,
1135
+ );
1136
+ generatedSpecialChars.push(filteredSpecialChars[randomIndex]);
1137
+ }
1138
+
1139
+ for (
1140
+ let i = 0;
1141
+ i <
1142
+ passwordLength -
1143
+ (numOfCapitalLetters + numOfNumbers + numOfSpecialChars);
1144
+ i++
1145
+ ) {
1146
+ const randomIndex = Math.floor(Math.random() * filteredWords.length);
1147
+ generatedWords.push(filteredWords[randomIndex]);
1148
+ }
1149
+
1150
+ //Combine all generated words, capitalLetters, numbers and specialChars and shuffle it
1151
+ let generatedPassword = '';
1152
+ const allGeneratedChars = generatedCapitalLetters.concat(
1153
+ generatedNumbers,
1154
+ generatedSpecialChars,
1155
+ generatedWords,
1156
+ );
1157
+ allGeneratedChars.sort(() => Math.random() - 0.5);
1158
+ generatedPassword = allGeneratedChars.join('');
1159
+ return generatedPassword;
1160
+ } catch (error) {
1161
+ throw error;
1162
+ }
1163
+ }
1164
+
1165
+ private static async setPassword(
1166
+ dbTransaction: any,
1167
+ user: User,
1168
+ password: string,
1169
+ ): Promise<User> {
1170
+ //This method will set password for the user.
1171
+ try {
1172
+ //Part 1: Verify Password
1173
+ //Retrieve all password policy from component config
1174
+ const passwordPolicy = ComponentConfig.getComponentConfigValue(
1175
+ '@tomei/sso',
1176
+ 'passwordPolicy',
1177
+ );
1178
+
1179
+ //Make sure all passwordPolicy keys got values, if not throw a ClassError
1180
+ if (
1181
+ !passwordPolicy ||
1182
+ !passwordPolicy.maxLen ||
1183
+ !passwordPolicy.minLen ||
1184
+ !passwordPolicy.nonAcceptableChar ||
1185
+ !passwordPolicy.numOfCapitalLetters ||
1186
+ !passwordPolicy.numOfNumbers ||
1187
+ !passwordPolicy.numOfSpecialChars
1188
+ ) {
1189
+ throw new ClassError(
1190
+ 'LoginUser',
1191
+ 'LoginUserErrMsg0X',
1192
+ 'Missing password policy. Please set in config file.',
1193
+ );
1194
+ }
1195
+
1196
+ //Compare Params.password with the password policy. If not matched, throw new ClassError
1197
+ try {
1198
+ //Check if password length is more than passwordPolicy.minLen
1199
+ if (password.length < passwordPolicy.minLen) {
1200
+ throw Error('Password is too short');
1201
+ }
1202
+
1203
+ //Check if password length is less than passwordPolicy.maxLen
1204
+ if (password.length > passwordPolicy.maxLen) {
1205
+ throw Error('Password is too long');
1206
+ }
1207
+
1208
+ //Check if password contains nonAcceptableChar
1209
+ const nonAcceptableChars: string[] =
1210
+ passwordPolicy.nonAcceptableChar.split(',');
1211
+ const nonAcceptableCharsFound = nonAcceptableChars.some((char) =>
1212
+ password.includes(char),
1213
+ );
1214
+ if (nonAcceptableCharsFound) {
1215
+ throw Error('Password contains unacceptable characters');
1216
+ }
1217
+
1218
+ //Check if password contains the correct amount of capital letter required from numOfCapitalLetters
1219
+ const capitalLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
1220
+ const numOfCapitalLetters = passwordPolicy.numOfCapitalLetters;
1221
+ const capitalLettersFound = capitalLetters
1222
+ .split('')
1223
+ .filter((char) => password.includes(char)).length;
1224
+ if (capitalLettersFound < numOfCapitalLetters) {
1225
+ throw Error('Password does not contain enough capital letters');
1226
+ }
1227
+
1228
+ //Check if password contains the correct amount of numbers required from numOfNumbers
1229
+ const numbers = '0123456789';
1230
+ const numOfNumbers = passwordPolicy.numOfNumbers;
1231
+ const numbersFound = numbers
1232
+ .split('')
1233
+ .filter((char) => password.includes(char)).length;
1234
+ if (numbersFound < numOfNumbers) {
1235
+ throw Error('Password does not contain enough numbers');
1236
+ }
1237
+
1238
+ //Check if password contains the correct amount of special characters required from numOfSpecialChars
1239
+ const specialChars = '!@#$%^&*()_+-={}[]|:;"<>,.?/~`';
1240
+ const numOfSpecialChars = passwordPolicy.numOfSpecialChars;
1241
+ const specialCharsFound = specialChars
1242
+ .split('')
1243
+ .filter((char) => password.includes(char)).length;
1244
+ if (specialCharsFound < numOfSpecialChars) {
1245
+ throw Error('Password does not contain enough special characters');
1246
+ }
1247
+ } catch (error) {
1248
+ throw new ClassError(
1249
+ 'LoginUser',
1250
+ 'LoginUserErrMsg0X',
1251
+ "Your password doesn't meet security requirements. Try using a mix of uppercase and lowercase letters, numbers, and symbols.",
1252
+ );
1253
+ }
1254
+
1255
+ //Part 2: Hash Password
1256
+ //Hash the Params.password using PasswordHashService.hashPassword method
1257
+ const passwordHashService = new PasswordHashService();
1258
+ const hashedPassword = await passwordHashService.hashPassword(password);
1259
+
1260
+ //Part 3: Return Updated User Instance
1261
+ user._Password = hashedPassword;
1262
+ return user;
1263
+ } catch (error) {
1264
+ throw error;
1265
+ }
1266
+ }
1267
+
1268
+ public static async create(
1269
+ loginUser: User,
1270
+ dbTransaction: any,
1271
+ user: User,
1272
+ ): Promise<User> {
1273
+ try {
1274
+ //This method will insert new user record
1275
+ //Part 1: Privilege Checking
1276
+ const systemCode =
1277
+ ApplicationConfig.getComponentConfigValue('system-code');
1278
+ const isPrivileged = await loginUser.checkPrivileges(
1279
+ systemCode,
1280
+ 'User - Create',
1281
+ );
1282
+
1283
+ //If user does not have privilege to create user, throw a ClassError
1284
+ if (!isPrivileged) {
1285
+ throw new ClassError(
1286
+ 'LoginUser',
1287
+ 'LoginUserErrMsg0X',
1288
+ 'You do not have the privilege to create user',
1289
+ );
1290
+ }
1291
+
1292
+ //Part 2: Validation
1293
+ //Make sure Params.user.Email got values. If not, throw new ClassError
1294
+ if (!user.Email && !user.UserName) {
1295
+ throw new ClassError(
1296
+ 'LoginUser',
1297
+ 'LoginUserErrMsg0X',
1298
+ 'Email and Username is required',
1299
+ );
1300
+ }
1301
+ //Check if user info exists, call LoginUser.CheckUserInfoDuplicated method
1302
+ await User.checkUserInfoDuplicated(dbTransaction, {
1303
+ Email: user.Email,
1304
+ UserName: user.UserName,
1305
+ IdType: user.IDType,
1306
+ IdNo: user.IDNo,
1307
+ ContactNo: user.ContactNo,
1308
+ });
1309
+
1310
+ //Part 3: Generate Default Password
1311
+ const defaultPassword = User.generateDefaultPassword();
1312
+ user = await User.setPassword(dbTransaction, user, defaultPassword);
1313
+ //Part 4: Insert User Record
1314
+ //Set userToBeCreated to the instantiation of new LoginUser (using private constructor)
1315
+ const userInfo: IUserAttr = {
1316
+ UserName: user.UserName,
1317
+ FullName: user.FullName,
1318
+ IDNo: user.IDNo,
1319
+ Email: user.Email,
1320
+ ContactNo: user.ContactNo,
1321
+ Password: user.Password,
1322
+ Status: UserStatus.ACTIVE,
1323
+ FirstLoginAt: null,
1324
+ LastLoginAt: null,
1325
+ MFAEnabled: null,
1326
+ MFAConfig: null,
1327
+ RecoveryEmail: null,
1328
+ FailedLoginAttemptCount: 0,
1329
+ LastFailedLoginAt: null,
1330
+ LastPasswordChangedAt: null,
1331
+ DefaultPasswordChangedYN: YN.No,
1332
+ NeedToChangePasswordYN: YN.Yes,
1333
+ CreatedById: loginUser.UserId,
1334
+ CreatedAt: new Date(),
1335
+ UpdatedById: loginUser.UserId,
1336
+ UpdatedAt: new Date(),
1337
+ UserId: null,
1338
+ };
1339
+ //Call LoginUser._Repo create method to insert new user record
1340
+ const newUser = await User._Repository.create(
1341
+ {
1342
+ Email: userInfo.Email,
1343
+ UserName: userInfo.UserName,
1344
+ Password: userInfo.Password,
1345
+ Status: userInfo.Status,
1346
+ DefaultPasswordChangedYN: userInfo.DefaultPasswordChangedYN,
1347
+ FirstLoginAt: userInfo.FirstLoginAt,
1348
+ LastLoginAt: userInfo.LastLoginAt,
1349
+ MFAEnabled: userInfo.MFAEnabled,
1350
+ MFAConfig: userInfo.MFAConfig,
1351
+ RecoveryEmail: userInfo.RecoveryEmail,
1352
+ FailedLoginAttemptCount: userInfo.FailedLoginAttemptCount,
1353
+ LastFailedLoginAt: userInfo.LastFailedLoginAt,
1354
+ LastPasswordChangedAt: userInfo.LastPasswordChangedAt,
1355
+ NeedToChangePasswordYN: userInfo.NeedToChangePasswordYN,
1356
+ CreatedById: userInfo.CreatedById,
1357
+ CreatedAt: userInfo.CreatedAt,
1358
+ UpdatedById: userInfo.UpdatedById,
1359
+ UpdatedAt: userInfo.UpdatedAt,
1360
+ },
1361
+ {
1362
+ transaction: dbTransaction,
1363
+ },
1364
+ );
1365
+
1366
+ userInfo.UserId = newUser.UserId;
1367
+ const userToBeCreated = new User(
1368
+ loginUser.SessionService,
1369
+ dbTransaction,
1370
+ userInfo,
1371
+ );
1372
+
1373
+ //Part 5: Record Create User Activity
1374
+ const activity = new Activity();
1375
+ activity.ActivityId = activity.createId();
1376
+ activity.Action = ActionEnum.ADD;
1377
+ activity.Description = 'Create User';
1378
+ activity.EntityType = 'LoginUser';
1379
+ activity.EntityId = newUser.UserId.toString();
1380
+ activity.EntityValueBefore = JSON.stringify({});
1381
+ activity.EntityValueAfter = JSON.stringify(newUser.get({ plain: true }));
1382
+
1383
+ await activity.create(loginUser.ObjectId, dbTransaction);
1384
+ return userToBeCreated;
1385
+ } catch (error) {
1386
+ throw error;
1387
+ }
1388
+ }
1389
+
1390
+ private async incrementFailedLoginAttemptCount(dbTransaction: any) {
1391
+ // This method is used to process all the necessary step after login failed by invalid credential
1392
+
1393
+ // 1. Retrieve maxFailedLoginAttempts and autoReleaseYN from component config, call ComponentConfig.getComponentConfigValue
1394
+ const maxFailedLoginAttempts = ComponentConfig.getComponentConfigValue(
1395
+ '@tomei/sso',
1396
+ 'maxFailedLoginAttempts',
1397
+ );
1398
+
1399
+ const autoReleaseYN = ComponentConfig.getComponentConfigValue(
1400
+ '@tomei/sso',
1401
+ 'autoReleaseYN',
1402
+ );
1403
+
1404
+ // 2. Make sure all maxFailedLoginAttempts keys got values
1405
+ if (!maxFailedLoginAttempts || !autoReleaseYN) {
1406
+ throw new ClassError(
1407
+ 'LoginUser',
1408
+ 'LoginUserErrMsg0X',
1409
+ 'Missing maxFailedLoginAttempts and or autoReleaseYN. Please set in config file.',
1410
+ );
1411
+ }
1412
+
1413
+ // 3. Set below object property FailedLoginAttemptCount & LastFailedLoginAt
1414
+ const FailedLoginAttemptCount = this.FailedLoginAttemptCount + 1;
1415
+ const LastFailedLoginAt = new Date();
1416
+
1417
+ // 4. If this.FailedLoginAttemptCount > config.maxFailedLogginAttempts, then set this.Status = 'Locked'
1418
+ if (FailedLoginAttemptCount > maxFailedLoginAttempts) {
1419
+ this.Status = UserStatus.LOCKED;
1420
+ }
1421
+
1422
+ // 5. Update the user data by calling _Repo.update with these parameter
1423
+ await User._Repository.update(
1424
+ {
1425
+ FailedLoginAttemptCount: FailedLoginAttemptCount,
1426
+ LastFailedLoginAt: LastFailedLoginAt,
1427
+ Status: this.Status,
1428
+ },
1429
+ {
1430
+ where: {
1431
+ UserId: this.UserId,
1432
+ },
1433
+ transaction: dbTransaction,
1434
+ },
1435
+ );
1436
+
1437
+ // 6. Depending on the Status and config.AutoReleaseYN, throw below error:
1438
+
1439
+ // 6.1 If Status = "Locked" and config.AutoReleaseYN = "Y", throws below error:
1440
+ if (this.Status === UserStatus.LOCKED && autoReleaseYN === 'Y') {
1441
+ throw new ClassError(
1442
+ 'LoginUser',
1443
+ 'LoginUserErrMsg0X',
1444
+ 'Your account has been temporarily locked due to too many failed login attempts, please try again later.',
1445
+ );
1446
+ }
1447
+
1448
+ // 6.2 If Status = "Locked" and config.AutoReleaseYN = "N", throws below error:
1449
+ if (this.Status === UserStatus.LOCKED && autoReleaseYN === 'N') {
1450
+ throw new ClassError(
1451
+ 'LoginUser',
1452
+ 'LoginUserErrMsg0X',
1453
+ 'Your account has been locked due to too many failed login attempts, please contact IT Support for instructions on how to unlock your account',
1454
+ );
1455
+ }
1456
+
1457
+ // 6.2 If Status = "Locked" and config.AutoReleaseYN = "N", throws below error:
1458
+ if (this.Status == UserStatus.LOCKED) {
1459
+ throw new ClassError(
1460
+ 'LoginUser',
1461
+ 'LoginUserErrMsg0X',
1462
+ 'Invalid credentials.',
1463
+ );
1464
+ }
1465
+ }
1466
+
1467
+ public static shouldReleaseLock(LastFailedLoginAt) {
1468
+ // This method is used to check whether the account is eligible to be unlocked.
1469
+
1470
+ // 1. Retrieve maxFailedLoginAttempts and autoReleaseYN from component config, call ComponentConfig.getComponentConfigValue
1471
+ const minuteToAutoRelease = ComponentConfig.getComponentConfigValue(
1472
+ '@tomei/sso',
1473
+ 'minuteToAutoRelease',
1474
+ );
1475
+
1476
+ const autoReleaseYN = ComponentConfig.getComponentConfigValue(
1477
+ '@tomei/sso',
1478
+ 'autoReleaseYN',
1479
+ );
1480
+
1481
+ // 2. Make sure all maxFailedLoginAttempts keys got values
1482
+ if (!minuteToAutoRelease || !autoReleaseYN) {
1483
+ throw new ClassError(
1484
+ 'LoginUser',
1485
+ 'LoginUserErrMsg0X',
1486
+ 'Missing minuteToAutoRelease and or autoReleaseYN. Please set in config file.',
1487
+ );
1488
+ }
1489
+
1490
+ // 6. Depending on the config.AutoReleaseYN, do the following :
1491
+
1492
+ // 6.1 If config.AutoReleaseYN = "Y":
1493
+ if (autoReleaseYN === 'Y') {
1494
+ const lastFailedDate = new Date(LastFailedLoginAt);
1495
+ const currentDate = new Date();
1496
+ const timeDifferenceInMillis =
1497
+ currentDate.getTime() - lastFailedDate.getTime();
1498
+ const timeDifferenceInMinutes: number =
1499
+ timeDifferenceInMillis / (1000 * 60);
1500
+
1501
+ if (timeDifferenceInMinutes > +minuteToAutoRelease) {
1502
+ return true;
1503
+ } else {
1504
+ return false;
1505
+ }
1506
+ // 6.2 If config.AutoReleaseYN = "N":
1507
+ } else if (autoReleaseYN === 'N') {
1508
+ return false;
1509
+ }
1510
+ }
1511
+
1512
+ private static releaseLock(UserId: number, dbTransaction: any) {
1513
+ // This method is used to process all the necessary step after login failed by invalid credential
1514
+
1515
+ // 1. Update User Status and FailedLoginAttemptCount
1516
+ this._Repository.update(
1517
+ {
1518
+ FailedLoginAttemptCount: 0,
1519
+ Status: UserStatus.ACTIVE,
1520
+ },
1521
+ {
1522
+ where: {
1523
+ UserId: UserId,
1524
+ },
1525
+ transaction: dbTransaction,
1526
+ },
1527
+ );
1528
+ }
1529
+
1530
+ public static async getGroups(loginUser: User, dbTransaction: any) {
1531
+ // This method will retrieve all user groups.
1532
+
1533
+ // Part 1: Privilege Checking
1534
+ const systemCode = ApplicationConfig.getComponentConfigValue('system-code');
1535
+ const isPrivileged = await loginUser.checkPrivileges(
1536
+ systemCode,
1537
+ 'UserGroup - List Own',
1538
+ );
1539
+ if (!isPrivileged) {
1540
+ throw new Error('You do not have permission to list UserGroup.');
1541
+ }
1542
+
1543
+ // Part 2: Retrieve User Groups & Returns
1544
+ const userGroups = await User._UserGroupRepo.findAll({
1545
+ where: {
1546
+ UserId: loginUser.ObjectId,
1547
+ Status: 'Active',
1548
+ },
1549
+ include: [{ model: UserModel, as: 'User' }, { model: GroupModel }],
1550
+ transaction: dbTransaction,
1551
+ });
1552
+
1553
+ return userGroups;
1554
+ }
1555
+
1556
+ protected static async getInheritedSystemAccess(
1557
+ dbTransaction: any,
1558
+ group: GroupModel,
1559
+ ) {
1560
+ // This method is a recursive method that will returns group system access with its parent group system access if applicable.
1561
+ // Part 1: Retrieve Group System Access
1562
+ const dataSystemAccesses = await User._GroupSystemAccessRepo.findAll({
1563
+ where: {
1564
+ GroupCode: group.GroupCode,
1565
+ Status: 'Active',
1566
+ },
1567
+ include: [{ model: SystemModel }],
1568
+ transaction: dbTransaction,
1569
+ });
1570
+ let systemAccesses = dataSystemAccesses;
1571
+
1572
+ // Part 2: Retrieve Parent Group System Access If Applicable
1573
+ // 2.1 Check if Params.group.InheritParentSystemAccessYN is "Y" and Params.group.ParentGroupCode is not empty
1574
+ if (group.InheritParentPrivilegeYN === 'Y' && group.ParentGroupCode) {
1575
+ const GroupCode = group.ParentGroupCode;
1576
+ const parentGroup = await User._GroupRepo.findByPk(
1577
+ GroupCode,
1578
+ dbTransaction,
1579
+ );
1580
+ const dataParentSystemAccesses = await User.getInheritedSystemAccess(
1581
+ dbTransaction,
1582
+ parentGroup,
1583
+ );
1584
+
1585
+ const parentSystemAccesses = dataParentSystemAccesses;
1586
+
1587
+ systemAccesses = systemAccesses.concat(parentSystemAccesses);
1588
+ }
1589
+ // Part 3: Return Array
1590
+ return systemAccesses;
1591
+ }
1592
+
1593
+ private static async combineSystemAccess(
1594
+ loginUser: User,
1595
+ dbTransaction: any,
1596
+ groups: any,
1597
+ ) {
1598
+ // Part 1: Retrieve User System Access
1599
+ const userAccess = await User._UserSystemAccessRepo.findAll({
1600
+ where: {
1601
+ UserId: loginUser.ObjectId,
1602
+ Status: 'Active',
1603
+ },
1604
+ include: [{ model: SystemModel }],
1605
+ transaction: dbTransaction,
1606
+ });
1607
+
1608
+ // Part 2: Create Group System Access Promises
1609
+ const groupAccessPromises = groups.map(async (e) => {
1610
+ if (e.InheritParentSystemAccessYN) {
1611
+ return await this.getInheritedSystemAccess(dbTransaction, e);
1612
+ } else {
1613
+ return [];
1614
+ }
1615
+ });
1616
+
1617
+ // Part 3: Resolve Promises and Flatten the Results
1618
+ const groupAccess = (await Promise.all(groupAccessPromises)).flat(); // Combine all group
1619
+
1620
+ // Part 4: Combine, Remove Duplicates & Returns
1621
+ const allAccess = userAccess.concat(groupAccess);
1622
+ const uniqueAccess = new Set(
1623
+ allAccess.filter((value, index, self) => {
1624
+ // Check for duplicates based on object properties (replace with your actual comparison logic)
1625
+ return self.some((prev) => prev.SystemCode === value.SystemCode);
1626
+ }),
1627
+ );
1628
+ return Array.from(uniqueAccess) as ISystemAccess[];
1629
+ }
1630
+
1631
+ public static async getSystems(loginUser: User, dbTransaction: any) {
1632
+ // This method will retrieve all system records which user has accessed to.
1633
+
1634
+ // Part 1: Privilege Checking
1635
+ const systemCode = ApplicationConfig.getComponentConfigValue('system-code');
1636
+ const isPrivileged = await loginUser.checkPrivileges(
1637
+ systemCode,
1638
+ 'System – List Own',
1639
+ );
1640
+ if (!isPrivileged) {
1641
+ throw new Error('You do not have permission to list UserGroup.');
1642
+ }
1643
+
1644
+ // Part 2: Retrieve System Access
1645
+ const groups = await User.getGroups(loginUser, dbTransaction);
1646
+ const systemAccess = await User.combineSystemAccess(
1647
+ loginUser,
1648
+ dbTransaction,
1649
+ groups,
1650
+ );
1651
+ const output = [];
1652
+ if (systemAccess) {
1653
+ for (let i = 0; i < systemAccess.length; i++) {
1654
+ const system = await User._SystemRepository.findOne({
1655
+ where: {
1656
+ SystemCode: systemAccess[i].SystemCode,
1657
+ Status: 'Active',
1658
+ },
1659
+ });
1660
+ output.push({
1661
+ UserSystemAccessId: systemAccess[i].UserSystemAccessId,
1662
+ UserId: systemAccess[i].UserId,
1663
+ SystemCode: systemAccess[i].SystemCode,
1664
+ Status: systemAccess[i].Status,
1665
+ CreatedById: systemAccess[i].CreatedById,
1666
+ UpdatedById: systemAccess[i].UpdatedById,
1667
+ CreatedAt: systemAccess[i].CreatedAt,
1668
+ UpdatedAt: systemAccess[i].UpdatedAt,
1669
+ System: system,
1670
+ });
1671
+ }
1672
+ }
1673
+
1674
+ // Part 3: Map Result to System Object
1675
+ return output;
1676
+ }
1677
+
1678
+ //This method will check if user enable 2FA or not.
1679
+ private static async check2FA(loginUser: User, dbTransaction: any) {
1680
+ try {
1681
+ //Use LoginUser._Repo findOne() method
1682
+ const user = await User._Repository.findOne({
1683
+ where: {
1684
+ UserId: loginUser.UserId,
1685
+ },
1686
+ transaction: dbTransaction,
1687
+ });
1688
+
1689
+ //From the return record, if MFAEnabled value is 1 then return true else return false.
1690
+ if (user.MFAEnabled === 1) {
1691
+ return true;
1692
+ }
1693
+ return false;
1694
+ } catch (error) {
1695
+ throw error;
1696
+ }
1697
+ }
1698
+
1699
+ //This method will set the 2FA setting for the login user
1700
+ public static async setup2FA(userId: number, dbTransaction: any) {
1701
+ // 1. Retrieve system code from app config using ApplicationConfig.
1702
+ const systemCode = ApplicationConfig.getComponentConfigValue('system-code');
1703
+
1704
+ // 2. Retrieve user data by calling LoginUser._Repository.findOne with UserId
1705
+ const user = await User._Repository.findOne({
1706
+ where: {
1707
+ UserId: userId,
1708
+ },
1709
+ });
1710
+
1711
+ // 3. If user data not found then return throw Class Error
1712
+ if (!user) {
1713
+ throw new ClassError(
1714
+ 'LoginUser',
1715
+ 'LoginUserErrMsg0X',
1716
+ 'Invalid Credentials',
1717
+ );
1718
+ }
1719
+
1720
+ // 4. Generate the 2FA secret code by calling speakeasy.generateSecret with parameter
1721
+ const secretCode = speakeasy.generateSecret({ name: systemCode });
1722
+
1723
+ // parse MFA Config
1724
+ let userMFAConfig = null;
1725
+ if (user?.MFAConfig !== null && typeof user?.MFAConfig === 'string') {
1726
+ try {
1727
+ userMFAConfig = JSON.parse(user?.MFAConfig);
1728
+ } catch (error) {
1729
+ console.error('Invalid JSON string on MFAConfig:', error);
1730
+ }
1731
+ }
1732
+
1733
+ // 5. Set MFAConfig
1734
+ const MFAConfig = {
1735
+ totp: {
1736
+ enabled: true,
1737
+ secret: secretCode.base32,
1738
+ issuer: systemCode,
1739
+ },
1740
+ sms: {
1741
+ enabled: userMFAConfig?.sms?.enable || false,
1742
+ phoneNumber: userMFAConfig?.sms?.phoneNumber || '',
1743
+ },
1744
+ email: {
1745
+ enabled: userMFAConfig?.email?.enable || false,
1746
+ emailAddress: userMFAConfig?.email?.emailAddress || '',
1747
+ },
1748
+ };
1749
+
1750
+ // 6. Set login user properties
1751
+ user.MFAEnabled = 0;
1752
+ user.MFAConfig = JSON.stringify(MFAConfig);
1753
+
1754
+ // 7. Update the user data by calling LoginUser._Repository.Update
1755
+ await User._Repository.update(
1756
+ {
1757
+ MFAEnabled: user.MFAEnabled,
1758
+ MFAConfig: user.MFAConfig,
1759
+ },
1760
+ {
1761
+ where: {
1762
+ UserId: userId,
1763
+ },
1764
+ transaction: dbTransaction,
1765
+ },
1766
+ );
1767
+
1768
+ // 8. return <result from step 2>.otpauth_url
1769
+ return secretCode.otpauth_url;
1770
+ }
1771
+
1772
+ //This method will verify whether 2FA have been set correctly
1773
+ public async verify2FASetup(
1774
+ userId: number,
1775
+ mfaToken: string,
1776
+ dbTransaction: any,
1777
+ ) {
1778
+ // 1. Retrieve user data by calling LoginUser._Repository.findOne with UserId
1779
+ const user = await User._Repository.findOne({
1780
+ where: {
1781
+ UserId: userId,
1782
+ },
1783
+ });
1784
+
1785
+ // 2. If user data not found then return throw Class Error
1786
+ if (!user) {
1787
+ throw new ClassError(
1788
+ 'LoginUser',
1789
+ 'LoginUserErrMsg0X',
1790
+ 'Invalid Credentials',
1791
+ );
1792
+ }
1793
+
1794
+ // parse MFA Config
1795
+ let userMFAConfig = null;
1796
+ if (user?.MFAConfig !== null && typeof user?.MFAConfig === 'string') {
1797
+ try {
1798
+ userMFAConfig = JSON.parse(user?.MFAConfig);
1799
+ } catch (error) {
1800
+ console.error('Invalid JSON string on MFAConfig:', error);
1801
+ }
1802
+ }
1803
+
1804
+ // 3. Verify the mfaToken by calling speakeasy.totp.verify
1805
+ const isVerified = await speakeasy.totp.verify({
1806
+ secret: userMFAConfig.totp.secret,
1807
+ encoding: 'base32',
1808
+ token: mfaToken,
1809
+ });
1810
+
1811
+ // 4. if not verified, then return false. if verified, Call LoginUser._Repo.update and update user data in database
1812
+ if (!isVerified) {
1813
+ return false;
1814
+ }
1815
+
1816
+ await User._Repository.update(
1817
+ {
1818
+ MFAEnabled: 1,
1819
+ },
1820
+ {
1821
+ where: {
1822
+ UserId: userId,
1823
+ },
1824
+ transaction: dbTransaction,
1825
+ },
1826
+ );
1827
+
1828
+ // 5. Retrieve Session
1829
+ const userSession = await this._SessionService.retrieveUserSession(
1830
+ `${userId}`,
1831
+ );
1832
+ const systemCode = ApplicationConfig.getComponentConfigValue('system-code');
1833
+
1834
+ const systemLogin = userSession.systemLogins.find(
1835
+ (e) => e.code === systemCode,
1836
+ );
1837
+ return `${userId}:${systemLogin.sessionId}`;
1838
+ }
1839
+
1840
+ // This method will verify 2FA codes
1841
+ public async verify2FACode(
1842
+ userId: number,
1843
+ mfaToken: string,
1844
+ dbTransaction: any,
1845
+ ) {
1846
+ // 1. Retrieve user data by calling LoginUser._Repository.findOne with UserId
1847
+ const user = await User._Repository.findOne({
1848
+ where: {
1849
+ UserId: userId,
1850
+ },
1851
+ transaction: dbTransaction,
1852
+ });
1853
+
1854
+ // 2. If user data not found then return throw Class Error
1855
+ if (!user) {
1856
+ throw new ClassError(
1857
+ 'LoginUser',
1858
+ 'LoginUserErrMsg0X',
1859
+ 'Invalid Credentials',
1860
+ );
1861
+ }
1862
+
1863
+ // parse MFA Config
1864
+ let userMFAConfig = null;
1865
+ if (user?.MFAConfig !== null && typeof user?.MFAConfig === 'string') {
1866
+ try {
1867
+ userMFAConfig = JSON.parse(user?.MFAConfig);
1868
+ } catch (error) {
1869
+ console.error('Invalid JSON string on MFAConfig:', error);
1870
+ }
1871
+ }
1872
+
1873
+ // 3. Verify the mfaToken by calling speakeasy.totp.verify
1874
+ const isVerified = await speakeasy.totp.verify({
1875
+ secret: userMFAConfig.totp.secret,
1876
+ encoding: 'base32',
1877
+ token: mfaToken,
1878
+ });
1879
+
1880
+ // 4. if not verified, then return false. if verified, Call LoginUser._Repo.update and update user data in database
1881
+ if (!isVerified) {
1882
+ return false;
1883
+ }
1884
+
1885
+ // 5. Retrieve Session
1886
+ const userSession = await this._SessionService.retrieveUserSession(
1887
+ `${userId}`,
1888
+ );
1889
+ const systemCode = ApplicationConfig.getComponentConfigValue('system-code');
1890
+
1891
+ const systemLogin = userSession.systemLogins.find(
1892
+ (e) => e.code === systemCode,
1893
+ );
1894
+ return `${userId}:${systemLogin.sessionId}`;
1895
+ }
1896
+
1897
+ public async addUserGroup(
1898
+ GroupCode: string,
1899
+ loginUser: User,
1900
+ dbTransaction: any,
1901
+ ) {
1902
+ // 1. Retrieve group data by calling LoginUser._GroupRepo.findOne with GroupCode
1903
+ const group = await User._GroupRepo.findOne({
1904
+ where: {
1905
+ GroupCode,
1906
+ },
1907
+ transaction: dbTransaction,
1908
+ });
1909
+
1910
+ // 2. If group data not found then return throw Class Error
1911
+ if (!group) {
1912
+ throw new ClassError(
1913
+ 'LoginUser',
1914
+ 'LoginUserErrMsg0X',
1915
+ 'Invalid Group Code',
1916
+ );
1917
+ }
1918
+
1919
+ //3. Create new UserGroup record
1920
+ const entityValueAfter = {
1921
+ UserId: this.UserId,
1922
+ GroupCode: group.GroupCode,
1923
+ CreatedAt: new Date(),
1924
+ CreatedById: loginUser.UserId,
1925
+ UpdatedAt: new Date(),
1926
+ UpdatedById: loginUser.UserId,
1927
+ };
1928
+ await User._UserGroupRepo.create(entityValueAfter, {
1929
+ transaction: dbTransaction,
1930
+ });
1931
+
1932
+ //4. Record Create UserGroup Activity
1933
+ const activity = new Activity();
1934
+ activity.ActivityId = activity.createId();
1935
+ activity.Action = ActionEnum.ADD;
1936
+ activity.Description = 'Add User Group';
1937
+ activity.EntityType = 'UserGroup';
1938
+ activity.EntityId = group.GroupCode;
1939
+ activity.EntityValueBefore = JSON.stringify({});
1940
+ activity.EntityValueAfter = JSON.stringify(entityValueAfter);
1941
+
1942
+ await activity.create(loginUser.ObjectId, dbTransaction);
1943
+ }
1944
+
1945
+ public async update(
1946
+ data: {
1947
+ UserName: string;
1948
+ Email: string;
1949
+ Status: UserStatus;
1950
+ RecoveryEmail: string;
1951
+ BuildingCode?: string;
1952
+ CompanyCode?: string;
1953
+ DepartmentCode?: string;
1954
+ },
1955
+ loginUser: User,
1956
+ dbTransaction: any,
1957
+ ) {
1958
+ //Part 1: Privilege Checking
1959
+ const systemCode = ApplicationConfig.getComponentConfigValue('system-code');
1960
+ const isPrivileged = await loginUser.checkPrivileges(
1961
+ systemCode,
1962
+ 'User - Update',
1963
+ );
1964
+
1965
+ //If user does not have privilege to update user, throw a ClassError
1966
+ if (!isPrivileged) {
1967
+ throw new ClassError(
1968
+ 'LoginUser',
1969
+ 'LoginUserErrMsg0X',
1970
+ 'You do not have the privilege to update user',
1971
+ );
1972
+ }
1973
+
1974
+ //Part 2: Validation
1975
+ //Make sure UserId got values. If not, throw new ClassError
1976
+ if (!this.UserId) {
1977
+ throw new ClassError(
1978
+ 'LoginUser',
1979
+ 'LoginUserErrMsg0X',
1980
+ 'UserId is required',
1981
+ );
1982
+ }
1983
+
1984
+ //Make sure email is unique, call LoginUser.CheckUserInfoDuplicated method
1985
+ if (data.Email !== this.Email || data.UserName !== this.UserName) {
1986
+ await User.checkUserInfoDuplicated(dbTransaction, {
1987
+ Email: data.Email,
1988
+ UserName: data.UserName,
1989
+ });
1990
+ }
1991
+
1992
+ //Part 3: Update Building, Company, Department
1993
+ //If Params.BuildingCode is not null,
1994
+ if (data.BuildingCode) {
1995
+ //Check if BuildingCode is valid, call GroupModel.findOne method
1996
+ const building = await GroupModel.findOne({
1997
+ where: {
1998
+ Type: 'Building',
1999
+ GroupCode: data.BuildingCode,
2000
+ },
2001
+ transaction: dbTransaction,
2002
+ });
2003
+
2004
+ //If BuildingCode is invalid, throw new ClassError
2005
+ if (!building) {
2006
+ throw new ClassError(
2007
+ 'LoginUser',
2008
+ 'LoginUserErrMsg0X',
2009
+ 'Invalid Building Code',
2010
+ );
2011
+ }
2012
+
2013
+ //If BuildingCode is valid, call UserGroup.findOne method to find the user building record
2014
+ const userBuilding = await User._UserGroupRepo.findOne({
2015
+ where: {
2016
+ UserId: this.UserId,
2017
+ },
2018
+ include: [
2019
+ {
2020
+ model: GroupModel,
2021
+ where: {
2022
+ Type: 'Building',
2023
+ },
2024
+ },
2025
+ ],
2026
+ transaction: dbTransaction,
2027
+ });
2028
+
2029
+ //If user building record found, call UserGroup.update method to update the record if not found, call UserGroup.create method to create new record
2030
+ if (userBuilding) {
2031
+ await User._UserGroupRepo.update(
2032
+ {
2033
+ GroupCode: data.BuildingCode,
2034
+ UpdatedAt: new Date(),
2035
+ UpdatedById: loginUser.UserId,
2036
+ },
2037
+ {
2038
+ where: {
2039
+ UserId: this.UserId,
2040
+ GroupCode: userBuilding.GroupCode,
2041
+ },
2042
+ transaction: dbTransaction,
2043
+ },
2044
+ );
2045
+ } else {
2046
+ await User._UserGroupRepo.create(
2047
+ {
2048
+ UserId: this.UserId,
2049
+ GroupCode: data.BuildingCode,
2050
+ CreatedAt: new Date(),
2051
+ CreatedById: loginUser.UserId,
2052
+ UpdatedAt: new Date(),
2053
+ UpdatedById: loginUser.UserId,
2054
+ },
2055
+ {
2056
+ transaction: dbTransaction,
2057
+ },
2058
+ );
2059
+ }
2060
+ }
2061
+
2062
+ //If Params.CompanyCode is not null,
2063
+ if (data.CompanyCode) {
2064
+ //Check if CompanyCode is valid, call GroupModel.findOne method
2065
+ const company = await GroupModel.findOne({
2066
+ where: {
2067
+ Type: 'Company',
2068
+ GroupCode: data.CompanyCode,
2069
+ },
2070
+ transaction: dbTransaction,
2071
+ });
2072
+
2073
+ //If CompanyCode is invalid, throw a ClassError
2074
+ if (!company) {
2075
+ throw new ClassError(
2076
+ 'LoginUser',
2077
+ 'LoginUserErrMsg0X',
2078
+ 'Invalid Company Code',
2079
+ );
2080
+ }
2081
+
2082
+ //If CompanyCode is valid, call UserGroup.findOne method to find the user company record
2083
+ const userCompany = await User._UserGroupRepo.findOne({
2084
+ where: {
2085
+ UserId: this.UserId,
2086
+ },
2087
+ include: [
2088
+ {
2089
+ model: GroupModel,
2090
+ where: {
2091
+ Type: 'Company',
2092
+ },
2093
+ },
2094
+ ],
2095
+ transaction: dbTransaction,
2096
+ });
2097
+
2098
+ //If user company record found, call UserGroup.update method to update the record if not found, call UserGroup.create method to create new record
2099
+ if (userCompany) {
2100
+ await User._UserGroupRepo.update(
2101
+ {
2102
+ GroupCode: data.CompanyCode,
2103
+ UpdatedAt: new Date(),
2104
+ UpdatedById: loginUser.UserId,
2105
+ },
2106
+ {
2107
+ where: {
2108
+ UserId: this.UserId,
2109
+ GroupCode: userCompany.GroupCode,
2110
+ },
2111
+ transaction: dbTransaction,
2112
+ },
2113
+ );
2114
+ } else {
2115
+ await User._UserGroupRepo.create(
2116
+ {
2117
+ UserId: this.UserId,
2118
+ GroupCode: data.CompanyCode,
2119
+ CreatedAt: new Date(),
2120
+ CreatedById: loginUser.UserId,
2121
+ UpdatedAt: new Date(),
2122
+ UpdatedById: loginUser.UserId,
2123
+ },
2124
+ {
2125
+ transaction: dbTransaction,
2126
+ },
2127
+ );
2128
+ }
2129
+ }
2130
+
2131
+ //If Params.DepartmentCode is not null,
2132
+ if (data.DepartmentCode) {
2133
+ //Check if DepartmentCode is valid, call GroupModel.findOne method
2134
+ const department = await GroupModel.findOne({
2135
+ where: {
2136
+ Type: 'Department',
2137
+ GroupCode: data.DepartmentCode,
2138
+ },
2139
+ transaction: dbTransaction,
2140
+ });
2141
+
2142
+ //If DepartmentCode is invalid, throw a ClassError
2143
+ if (!department) {
2144
+ throw new ClassError(
2145
+ 'LoginUser',
2146
+ 'LoginUserErrMsg0X',
2147
+ 'Invalid Department Code',
2148
+ );
2149
+ }
2150
+
2151
+ //If DepartmentCode is valid, call UserGroup.findOne method to find the user department record
2152
+ const userDepartment = await User._UserGroupRepo.findOne({
2153
+ where: {
2154
+ UserId: this.UserId,
2155
+ },
2156
+ include: [
2157
+ {
2158
+ model: GroupModel,
2159
+ where: {
2160
+ Type: 'Department',
2161
+ },
2162
+ },
2163
+ ],
2164
+ transaction: dbTransaction,
2165
+ });
2166
+
2167
+ //If user department record found, call UserGroup.update method to update the record if not found, call UserGroup.create method to create new record
2168
+ if (userDepartment) {
2169
+ await User._UserGroupRepo.update(
2170
+ {
2171
+ GroupCode: data.DepartmentCode,
2172
+ UpdatedAt: new Date(),
2173
+ UpdatedById: loginUser.UserId,
2174
+ },
2175
+ {
2176
+ where: {
2177
+ UserId: this.UserId,
2178
+ GroupCode: userDepartment.GroupCode,
2179
+ },
2180
+ transaction: dbTransaction,
2181
+ },
2182
+ );
2183
+ } else {
2184
+ await User._UserGroupRepo.create(
2185
+ {
2186
+ UserId: this.UserId,
2187
+ GroupCode: data.DepartmentCode,
2188
+ CreatedAt: new Date(),
2189
+ CreatedById: loginUser.UserId,
2190
+ UpdatedAt: new Date(),
2191
+ UpdatedById: loginUser.UserId,
2192
+ },
2193
+ {
2194
+ transaction: dbTransaction,
2195
+ },
2196
+ );
2197
+ }
2198
+ }
2199
+
2200
+ //Part 4: Update User Record
2201
+ //Set EntityValueBefore
2202
+ const entityValueBefore = {
2203
+ UserId: this.UserId,
2204
+ UserName: this.UserName,
2205
+ Email: this.Email,
2206
+ Password: this.Password,
2207
+ Status: this.Status,
2208
+ DefaultPasswordChangedYN: this.DefaultPasswordChangedYN,
2209
+ FirstLoginAt: this.FirstLoginAt,
2210
+ LastLoginAt: this.LastLoginAt,
2211
+ MFAEnabled: this.MFAEnabled,
2212
+ MFAConfig: this.MFAConfig,
2213
+ RecoveryEmail: this.RecoveryEmail,
2214
+ FailedLoginAttemptCount: this.FailedLoginAttemptCount,
2215
+ LastFailedLoginAt: this.LastFailedLoginAt,
2216
+ LastPasswordChangedAt: this.LastPasswordChangedAt,
2217
+ NeedToChangePasswordYN: this.NeedToChangePasswordYN,
2218
+ CreatedById: this.CreatedById,
2219
+ CreatedAt: this.CreatedAt,
2220
+ UpdatedById: this.UpdatedById,
2221
+ UpdatedAt: this.UpdatedAt,
2222
+ };
2223
+
2224
+ //Update user record
2225
+ this.UserName = data.UserName;
2226
+ this.Email = data.Email;
2227
+ this.Status = data.Status;
2228
+ this.RecoveryEmail = data.RecoveryEmail;
2229
+ this.UpdatedAt = new Date();
2230
+ this.UpdatedById = loginUser.UserId;
2231
+ //Call LoginUser._Repo update method to update user record
2232
+ await User._Repository.update(
2233
+ {
2234
+ UserName: this.UserName,
2235
+ Email: this.Email,
2236
+ Status: this.Status,
2237
+ RecoveryEmail: this.RecoveryEmail,
2238
+ UpdatedById: this.UpdatedById,
2239
+ UpdatedAt: this.UpdatedAt,
2240
+ },
2241
+ {
2242
+ where: {
2243
+ UserId: this.UserId,
2244
+ },
2245
+ transaction: dbTransaction,
2246
+ },
2247
+ );
2248
+
2249
+ //Part 5: Record Update User Activity
2250
+ //Set EntityValueAfter
2251
+ const entityValueAfter = {
2252
+ UserId: this.UserId,
2253
+ UserName: this.UserName,
2254
+ Email: this.Email,
2255
+ Password: this.Password,
2256
+ Status: this.Status,
2257
+ DefaultPasswordChangedYN: this.DefaultPasswordChangedYN,
2258
+ FirstLoginAt: this.FirstLoginAt,
2259
+ LastLoginAt: this.LastLoginAt,
2260
+ MFAEnabled: this.MFAEnabled,
2261
+ MFAConfig: this.MFAConfig,
2262
+ RecoveryEmail: this.RecoveryEmail,
2263
+ FailedLoginAttemptCount: this.FailedLoginAttemptCount,
2264
+ LastFailedLoginAt: this.LastFailedLoginAt,
2265
+ LastPasswordChangedAt: this.LastPasswordChangedAt,
2266
+ NeedToChangePasswordYN: this.NeedToChangePasswordYN,
2267
+ CreatedById: this.CreatedById,
2268
+ CreatedAt: this.CreatedAt,
2269
+ UpdatedById: this.UpdatedById,
2270
+ UpdatedAt: this.UpdatedAt,
2271
+ };
2272
+
2273
+ //Call Activity.create method to create new activity record
2274
+ const activity = new Activity();
2275
+ activity.ActivityId = activity.createId();
2276
+ activity.Action = ActionEnum.UPDATE;
2277
+ activity.Description = 'Update User';
2278
+ activity.EntityType = 'LoginUser';
2279
+ activity.EntityId = this.UserId.toString();
2280
+ activity.EntityValueBefore = JSON.stringify(entityValueBefore);
2281
+ activity.EntityValueAfter = JSON.stringify(entityValueAfter);
2282
+
2283
+ await activity.create(loginUser.ObjectId, dbTransaction);
2284
+
2285
+ //Return Updated User Instance
2286
+ return this;
2287
+ }
2288
+
2289
+ public static async findById(
2290
+ loginUser: LoginUser,
2291
+ dbTransaction: any,
2292
+ UserId: string,
2293
+ ) {
2294
+ const systemCode = ApplicationConfig.getComponentConfigValue('system-code');
2295
+ const isPrivileged = await loginUser.checkPrivileges(
2296
+ systemCode,
2297
+ 'USER_VIEW',
2298
+ );
2299
+
2300
+ //If user does not have privilege to update user, throw a ClassError
2301
+ if (!isPrivileged) {
2302
+ throw new ClassError(
2303
+ 'LoginUser',
2304
+ 'LoginUserErrMsg0X',
2305
+ 'You do not have the privilege to find user',
2306
+ );
2307
+ }
2308
+
2309
+ const user = await User._Repository.findOne({
2310
+ where: {
2311
+ UserId: UserId,
2312
+ Status: 'Active',
2313
+ },
2314
+ transaction: dbTransaction,
2315
+ });
2316
+ const userAttr: IUserAttr = {
2317
+ UserId: user.UserId,
2318
+ UserName: user.UserName,
2319
+ FullName: user?.Staff?.FullName || null,
2320
+ IDNo: user?.Staff?.IdNo || null,
2321
+ ContactNo: user?.Staff?.Mobile || null,
2322
+ Email: user.Email,
2323
+ Password: user.Password,
2324
+ Status: user.Status,
2325
+ DefaultPasswordChangedYN: user.DefaultPasswordChangedYN,
2326
+ FirstLoginAt: user.FirstLoginAt,
2327
+ LastLoginAt: user.LastLoginAt,
2328
+ MFAEnabled: user.MFAEnabled,
2329
+ MFAConfig: user.MFAConfig,
2330
+ RecoveryEmail: user.RecoveryEmail,
2331
+ FailedLoginAttemptCount: user.FailedLoginAttemptCount,
2332
+ LastFailedLoginAt: user.LastFailedLoginAt,
2333
+ LastPasswordChangedAt: user.LastPasswordChangedAt,
2334
+ NeedToChangePasswordYN: user.NeedToChangePasswordYN,
2335
+ CreatedById: user.CreatedById,
2336
+ CreatedAt: user.CreatedAt,
2337
+ UpdatedById: user.UpdatedById,
2338
+ UpdatedAt: user.UpdatedAt,
2339
+ staffs: user?.Staff || null,
2340
+ };
2341
+ return new User(null, dbTransaction, userAttr);
2342
+ }
2343
+ }