@tomei/sso 0.15.3 → 0.15.5

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.
@@ -1,557 +1,749 @@
1
- import { IAddress, LoginUserBase } from '@tomei/general';
2
- import { ISessionService } from '../../session/interfaces/session-service.interface';
3
- import { IUserInfo } from './interfaces/user-info.interface';
4
- import { UserRepository } from './user.repository';
5
- import { SystemRepository } from '../system/system.repository';
6
- import { SystemAccessRepository } from '../system-access/system-access.repository';
7
- import { LoginHistoryRepository } from '../login-history/login-history.repository';
8
- import { UserUserGroupRepository } from '../user-user-group/user-user-group.repository';
9
- import { PasswordHashService } from '../password-hash/password-hash.service';
10
- import { UserGroupRepository } from '../user-group/user-group.repository';
11
- import { SMTPMailer } from '@tomei/mailer';
12
- import { ISystemLogin } from '../../../src/interfaces/system-login.interface';
13
- import Staff from '../../models/staff.entity';
14
- import SystemPrivilege from '../../models/system-privilege.entity';
15
- import LoginHistory from '../../models/login-history.entity';
16
- import GroupSystemPrivilege from '../../models/group-system-privilege.entity';
17
- import GroupRolePrivilege from '../../models/group-role-privilege.entity';
18
- import UserGroup from '../../models/user-group.entity';
19
-
20
- export class LoginUser extends LoginUserBase {
21
- FullName: string;
22
- IDNo: string;
23
- IDType: string;
24
- Email: string;
25
- ContactNo: string;
26
- Password: string;
27
- DefaultAddress: IAddress;
28
- ObjectId: string;
29
- ObjectName = 'User';
30
- TableName = 'sso_users';
31
- ObjectType = 'User';
32
- staffs: any;
33
-
34
- private _OriginIP: string;
35
- private _SessionService: ISessionService;
36
- private _PasswordHashService = new PasswordHashService();
37
- private static _Repository = new UserRepository();
38
- private static _SystemRepository = new SystemRepository();
39
- private static _SystemAccessRepository = new SystemAccessRepository();
40
- private static _LoginHistoryRepository = new LoginHistoryRepository();
41
- private static _UserUserGroupRepository = new UserUserGroupRepository();
42
- private static _UserGroupRepository = new UserGroupRepository();
43
- private _dbTransaction: any;
44
-
45
- async getDetails(): Promise<{
46
- FullName: string;
47
- IDNo: string;
48
- IDType: string;
49
- Email: string;
50
- ContactNo: string;
51
- }> {
52
- return {
53
- FullName: this.FullName,
54
- IDNo: this.IDNo,
55
- IDType: this.IDType,
56
- Email: this.Email,
57
- ContactNo: this.ContactNo,
58
- };
59
- }
60
-
61
- private constructor(
62
- sessionService: ISessionService,
63
- dbTransaction?: any,
64
- userInfo?: IUserInfo,
65
- ) {
66
- super();
67
- this._SessionService = sessionService;
68
-
69
- if (dbTransaction) {
70
- this._dbTransaction = dbTransaction;
71
- }
72
- // set all the class properties
73
- if (userInfo) {
74
- this.ObjectId = userInfo.ObjectId;
75
- this.FullName = userInfo.FullName;
76
- this.IDNo = userInfo.IDNo;
77
- this.Email = userInfo.Email;
78
- this.ContactNo = userInfo.ContactNo;
79
- this.Password = userInfo.Password;
80
- this.staffs = userInfo.staffs;
81
- }
82
- }
83
-
84
- static async init(
85
- sessionService: ISessionService,
86
- userId?: string,
87
- dbTransaction = null,
88
- ): Promise<LoginUser> {
89
- if (userId) {
90
- if (dbTransaction) {
91
- LoginUser._Repository = new UserRepository();
92
- }
93
- const user = await LoginUser._Repository.findOne({
94
- where: {
95
- id: Number(userId),
96
- },
97
- include: [
98
- {
99
- model: Staff,
100
- },
101
- ],
102
- });
103
-
104
- if (!user) {
105
- throw new Error('Invalid credentials.');
106
- }
107
-
108
- if (user) {
109
- const userInfo: IUserInfo = {
110
- ObjectId: user.id.toString(),
111
- FullName: user.Staff.FullName,
112
- IDNo: user.Staff.IdNo,
113
- ContactNo: user.Staff.Mobile,
114
- Email: user.Email,
115
- Password: user.Password,
116
- staffs: user.Staff,
117
- };
118
-
119
- return new LoginUser(sessionService, dbTransaction, userInfo);
120
- } else {
121
- throw new Error('User not found');
122
- }
123
- }
124
- return new LoginUser(sessionService, dbTransaction);
125
- }
126
-
127
- async login(
128
- systemCode: string,
129
- email: string,
130
- password: string,
131
- ipAddress: string,
132
- ): Promise<string> {
133
- try {
134
- //validate email
135
- if (!this.ObjectId) {
136
- const user = await LoginUser._Repository.findOne({
137
- where: {
138
- Email: email,
139
- },
140
- include: [
141
- {
142
- model: Staff,
143
- },
144
- ],
145
- });
146
-
147
- const userInfo: IUserInfo = {
148
- ObjectId: user.id.toString(),
149
- FullName: user.Staff.FullName,
150
- IDNo: user.Staff.IdNo,
151
- ContactNo: user.Staff.Mobile,
152
- Email: user.Staff.Email,
153
- Password: user.Password,
154
- staffs: user.Staff,
155
- };
156
-
157
- this.ObjectId = userInfo.ObjectId;
158
- this.FullName = userInfo.FullName;
159
- this.IDNo = userInfo.IDNo;
160
- this.Email = userInfo.Email;
161
- this.ContactNo = userInfo.ContactNo;
162
- this.Password = userInfo.Password;
163
- this.staffs = userInfo.staffs;
164
- }
165
-
166
- if (this.ObjectId && this.Email !== email) {
167
- throw new Error('Invalid credentials.');
168
- }
169
-
170
- //validate password
171
- const isPasswordValid = await this._PasswordHashService.verify(
172
- password,
173
- this.Password,
174
- );
175
-
176
- if (!isPasswordValid) {
177
- throw new Error('Invalid credentials.');
178
- }
179
-
180
- //validate system code
181
- const system = await LoginUser._SystemRepository.findOne({
182
- where: {
183
- code: systemCode,
184
- },
185
- });
186
-
187
- if (!system) {
188
- throw new Error('Invalid system code.');
189
- }
190
-
191
- //validate system access
192
- await this.checkSystemAccess(this.ObjectId, system.id);
193
- // alert user if new login
194
- // await this.alertNewLogin(this.ObjectId, system.id.toString(), ipAddress);
195
-
196
- // fetch user session if exists
197
- const userSession = await this._SessionService.retrieveUserSession(
198
- this.ObjectId,
199
- );
200
- let systemLogin = userSession.systemLogins.find(
201
- (system) => system.code === systemCode,
202
- );
203
-
204
- // generate new session id
205
- const { randomUUID } = require('crypto');
206
- const sessionId = randomUUID();
207
-
208
- if (systemLogin) {
209
- systemLogin = systemLogin.sessionId = sessionId;
210
- userSession.systemLogins.map((system) =>
211
- system.code === systemCode ? systemLogin : system,
212
- );
213
- } else {
214
- // if not, add new system login into the userSession
215
- const newLogin = {
216
- id: system.id.toString(),
217
- code: system.Code,
218
- sessionId: sessionId,
219
- privileges: await this.getPrivileges(system.Code),
220
- };
221
- userSession.systemLogins.push(newLogin);
222
- }
223
- // then update userSession inside the redis storage with 1 day duration of time-to-live
224
- this._SessionService.setUserSession(this.ObjectId, userSession);
225
-
226
- // record new login history
227
- await LoginUser._LoginHistoryRepository.create({
228
- UserId: this.ObjectId,
229
- SystemId: system.id,
230
- OriginIp: ipAddress,
231
- CreatedAt: new Date(),
232
- });
233
-
234
- return `${this.ObjectId}:${sessionId}`;
235
- } catch (error) {
236
- throw error;
237
- }
238
- }
239
-
240
- private async checkSystemAccess(
241
- userId: string,
242
- systemId: number,
243
- ): Promise<void> {
244
- try {
245
- const systemAccess = await LoginUser._SystemAccessRepository.findOne({
246
- where: {
247
- UserId: userId,
248
- SystemId: systemId,
249
- },
250
- });
251
-
252
- if (!systemAccess) {
253
- throw new Error("User don't have access to the system.");
254
- }
255
- } catch (error) {
256
- throw error;
257
- }
258
- }
259
-
260
- private async alertNewLogin(
261
- userId: string,
262
- systemId: string,
263
- ipAddress: string,
264
- ) {
265
- try {
266
- const userLogins = await LoginUser._LoginHistoryRepository.findAll({
267
- where: {
268
- UserId: userId,
269
- SystemId: systemId,
270
- },
271
- });
272
-
273
- const gotPreviousLogins = userLogins?.length !== 0;
274
- let ipFound: LoginHistory | undefined = undefined;
275
- if (gotPreviousLogins) {
276
- ipFound = userLogins.find((item) => item.OriginIp === ipAddress);
277
- }
278
-
279
- if (gotPreviousLogins && !ipFound) {
280
- const EMAIL_SENDER =
281
- process.env.EMAIL_SENDER || 'itd-system@tomei.com.my';
282
- const transporter = new SMTPMailer();
283
-
284
- await transporter.sendMail({
285
- from: EMAIL_SENDER,
286
- to: this.Email,
287
- subject: 'New Login Alert',
288
- html: `<p>Dear ${this.FullName},</p>
289
- <p>There was a new login to your account from ${ipAddress} on ${new Date().toLocaleString()}.</p>
290
- <p>If this was you, you can safely ignore this email.</p>
291
- <p>If you suspect that someone else is trying to access your account, please contact us immediately at itd-support@tomei.com.my.</p>
292
- <p>Thank you!,</p>
293
- <p>
294
- Best Regards,
295
- IT Department
296
- </p>`,
297
- });
298
- }
299
- } catch (error) {
300
- throw error;
301
- }
302
- }
303
-
304
- private async getPrivileges(systemCode: string): Promise<string[]> {
305
- try {
306
- const system = await LoginUser._SystemRepository.findOne({
307
- where: {
308
- Code: systemCode,
309
- },
310
- });
311
-
312
- if (!system) {
313
- throw new Error('Invalid system code.');
314
- }
315
-
316
- // retrive user userGroups with system privileges
317
- const userUserGroups = await this.getUserUserGroupFromDB(system.id);
318
-
319
- // get all userGroup data from user userGroups
320
- const userGroupData = userUserGroups.map((u) => u.UserGroup);
321
- // get all privileges from userGroup data
322
- let privileges: string[] = [];
323
- for (const userGroup of userGroupData) {
324
- const groupSystemPrivileges = userGroup.GroupSystemPrivileges.map(
325
- (g) => g.SystemPrivilege.Code,
326
- );
327
- const groupRolePrivileges = userGroup.GroupRolePrivileges.map(
328
- (g) => g.SystemPrivilege.Code,
329
- );
330
-
331
- // if userGroup is not root, get all parent tree privileges
332
- if (
333
- userGroup.GroupLevel !== 0 &&
334
- userGroup.AllowInheritFromParentYN === 'Y'
335
- ) {
336
- // get all parent tree privileges
337
- const parentTreePrivileges = await this.getPrivilegesFromUserGroup(
338
- userGroup.ParentGroupCode,
339
- );
340
-
341
- privileges = [...privileges, ...parentTreePrivileges];
342
- }
343
-
344
- privileges = [
345
- ...privileges,
346
- ...groupSystemPrivileges,
347
- ...groupRolePrivileges,
348
- ];
349
- }
350
-
351
- // retrive all user personal privileges
352
- const userPrivileges = await this.getUserPersonalPrivileges(system.id);
353
-
354
- privileges = [...privileges, ...userPrivileges];
355
- privileges = [...new Set(privileges)];
356
- return privileges;
357
- } catch (error) {
358
- throw error;
359
- }
360
- }
361
-
362
- private async getPrivilegesFromUserGroup(
363
- groupCode: string,
364
- ): Promise<string[]> {
365
- try {
366
- // Retrieve userGroup from the database based on groupCode
367
- const userGroup = await this.getUserGroupFromDB(groupCode);
368
- let privileges: string[] = [];
369
-
370
- // Add privileges from the userGroup to the privileges array
371
- privileges = [
372
- ...privileges,
373
- ...userGroup.GroupSystemPrivileges.map((g) => g.SystemPrivilege.Code),
374
- ...userGroup.GroupRolePrivileges.map((g) => g.SystemPrivilege.Code),
375
- ];
376
-
377
- // Recursive call if conditions are not met and ParentGroupCode exists
378
- const isContinue =
379
- userGroup.GroupLevel !== 0 &&
380
- userGroup.AllowInheritFromParentYN === 'Y';
381
- if (isContinue) {
382
- const recursivePrivileges = await this.getPrivilegesFromUserGroup(
383
- userGroup.ParentGroupCode,
384
- );
385
- privileges = privileges.concat(recursivePrivileges);
386
- }
387
-
388
- // return privileges array
389
- return privileges;
390
- } catch (error) {
391
- throw error;
392
- }
393
- }
394
-
395
- private async getUserGroupFromDB(groupCode: string): Promise<any> {
396
- try {
397
- const userGroup = await LoginUser._UserGroupRepository.findOne({
398
- where: {
399
- GroupCode: groupCode,
400
- },
401
- include: [
402
- {
403
- model: GroupSystemPrivilege,
404
- include: {
405
- model: SystemPrivilege,
406
- },
407
- },
408
- {
409
- model: GroupRolePrivilege,
410
- include: {
411
- model: SystemPrivilege,
412
- },
413
- },
414
- ],
415
- });
416
- return userGroup;
417
- } catch (error) {
418
- throw error;
419
- }
420
- }
421
-
422
- private async getUserUserGroupFromDB(systemCode: number) {
423
- try {
424
- return await LoginUser._UserUserGroupRepository.findAll({
425
- where: {
426
- UserId: this.ObjectId,
427
- SystemId: systemCode,
428
- },
429
- include: {
430
- model: UserGroup,
431
- include: [
432
- {
433
- model: GroupSystemPrivilege,
434
- include: {
435
- model: SystemPrivilege,
436
- },
437
- },
438
- {
439
- model: GroupRolePrivilege,
440
- include: {
441
- model: SystemPrivilege,
442
- },
443
- },
444
- ],
445
- },
446
- });
447
- } catch (error) {
448
- throw error;
449
- }
450
- }
451
-
452
- private async getUserPersonalPrivileges(systemId: number): Promise<string[]> {
453
- try {
454
- const userRole = await LoginUser._Repository.findOne({
455
- where: {
456
- id: this.ObjectId,
457
- },
458
- include: {
459
- model: SystemPrivilege,
460
- },
461
- });
462
-
463
- //retrive all user systemPrevileges data from user roles
464
- let userSystemPrivileges = userRole.SystemPrivileges;
465
-
466
- userSystemPrivileges = userSystemPrivileges.filter(
467
- (u) => u.SystemId === systemId,
468
- );
469
-
470
- const userPrivileges: string[] = userSystemPrivileges.map((u) => u.Code);
471
- return userPrivileges;
472
- } catch (error) {
473
- throw error;
474
- }
475
- }
476
-
477
- async checkPrivileges(
478
- systemCode: string,
479
- privilegeName: string,
480
- ): Promise<boolean> {
481
- try {
482
- if (!this.ObjectId) {
483
- throw new Error('ObjectId(UserId) is not set');
484
- }
485
-
486
- const userSession = await this._SessionService.retrieveUserSession(
487
- this.ObjectId,
488
- );
489
-
490
- const systemLogin = userSession.systemLogins.find(
491
- (system) => system.code === systemCode,
492
- );
493
-
494
- if (!systemLogin) {
495
- return false;
496
- }
497
-
498
- const privileges = systemLogin.privileges;
499
- const hasPrivilege = privileges.includes(privilegeName);
500
- return hasPrivilege;
501
- } catch (error) {
502
- throw error;
503
- }
504
- }
505
-
506
- async checkSession(
507
- systemCode: string,
508
- sessionId: string,
509
- userId: string,
510
- ): Promise<ISystemLogin> {
511
- try {
512
- const userSession = await this._SessionService.retrieveUserSession(
513
- userId,
514
- );
515
-
516
- if (userSession.systemLogins.length === 0) {
517
- throw new Error('Session expired.');
518
- }
519
-
520
- const systemLogin = userSession.systemLogins.find(
521
- (sl) => sl.code === systemCode,
522
- );
523
-
524
- if (!systemLogin) {
525
- throw new Error('Session expired.');
526
- }
527
-
528
- if (systemLogin.sessionId !== sessionId) {
529
- throw new Error('Session expired.');
530
- }
531
-
532
- await this._SessionService.refreshDuration(userId);
533
-
534
- return systemLogin;
535
- } catch (error) {
536
- throw error;
537
- }
538
- }
539
-
540
- async logout(systemCode: string) {
541
- try {
542
- if (!this.ObjectId) {
543
- throw new Error('ObjectId(UserId) is not set');
544
- }
545
- const userSession = await this._SessionService.retrieveUserSession(
546
- this.ObjectId,
547
- );
548
- const index = userSession.systemLogins.findIndex(
549
- (system) => system.code === systemCode,
550
- );
551
- userSession.systemLogins.splice(index, 1);
552
- this._SessionService.setUserSession(this.ObjectId, userSession);
553
- } catch (error) {
554
- throw error;
555
- }
556
- }
557
- }
1
+ import { LoginUserBase } from '@tomei/general';
2
+ import { ISessionService } from '../../session/interfaces/session-service.interface';
3
+ import { IUserAttr } from './interfaces/user-info.interface';
4
+ import { UserRepository } from './user.repository';
5
+ import { SystemRepository } from '../system/system.repository';
6
+ import { SystemAccessRepository } from '../system-access/system-access.repository';
7
+ import { LoginHistoryRepository } from '../login-history/login-history.repository';
8
+ import { UserUserGroupRepository } from '../user-user-group/user-user-group.repository';
9
+ import { PasswordHashService } from '../password-hash/password-hash.service';
10
+ import { UserGroupRepository } from '../user-group/user-group.repository';
11
+ import { SMTPMailer } from '@tomei/mailer';
12
+ import { ISystemLogin } from '../../../src/interfaces/system-login.interface';
13
+ import Staff from '../../models/staff.entity';
14
+ import SystemPrivilege from '../../models/system-privilege.entity';
15
+ import LoginHistory from '../../models/login-history.entity';
16
+ import GroupSystemPrivilege from '../../models/group-system-privilege.entity';
17
+ import GroupRolePrivilege from '../../models/group-role-privilege.entity';
18
+ import UserGroup from '../../models/user-group.entity';
19
+ import { YN } from '../../enum/yn.enum';
20
+
21
+ export class LoginUser extends LoginUserBase {
22
+ ObjectId: string;
23
+ Email: string;
24
+ private _Password: string;
25
+ private _Status: string;
26
+ private _DefaultPasswordChangedYN: YN;
27
+ private _FirstLoginAt: Date;
28
+ private _LastLoginAt: Date;
29
+ private _MFAEnabled: number;
30
+ private _MFAConfig: string;
31
+ private _RecoveryEmail: string;
32
+ private _FailedLoginAttemptCount: number;
33
+ private _LastFailedLoginAt: Date;
34
+ private _LastPasswordChangedAt: Date;
35
+ private _NeedToChangePasswordYN: YN;
36
+ private _CreatedById: number;
37
+ private _CreatedAt: Date;
38
+ private _UpdatedById: number;
39
+ private _UpdatedAt: Date;
40
+ ObjectName = 'User';
41
+ TableName = 'sso_Users';
42
+ ObjectType = 'User';
43
+ staffs: any;
44
+
45
+ private _OriginIP: string;
46
+ private _SessionService: ISessionService;
47
+ private _PasswordHashService = new PasswordHashService();
48
+ private static _Repository = new UserRepository();
49
+ private static _SystemRepository = new SystemRepository();
50
+ private static _SystemAccessRepository = new SystemAccessRepository();
51
+ private static _LoginHistoryRepository = new LoginHistoryRepository();
52
+ private static _UserUserGroupRepository = new UserUserGroupRepository();
53
+ private static _UserGroupRepository = new UserGroupRepository();
54
+ private _dbTransaction: any;
55
+
56
+ get UserId(): number {
57
+ return parseInt(this.ObjectId);
58
+ }
59
+
60
+ private set UserId(value: number) {
61
+ this.ObjectId = value.toString();
62
+ }
63
+
64
+ get Password(): string {
65
+ return this._Password;
66
+ }
67
+
68
+ private set Password(value: string) {
69
+ this._Password = value;
70
+ }
71
+
72
+ get Status(): string {
73
+ return this._Status;
74
+ }
75
+
76
+ private set Status(value: string) {
77
+ this._Status = value;
78
+ }
79
+
80
+ get DefaultPasswordChangedYN(): YN {
81
+ return this._DefaultPasswordChangedYN;
82
+ }
83
+
84
+ private set DefaultPasswordChangedYN(value: YN) {
85
+ this._DefaultPasswordChangedYN = value;
86
+ }
87
+
88
+ get FirstLoginAt(): Date {
89
+ return this._FirstLoginAt;
90
+ }
91
+
92
+ private set FirstLoginAt(value: Date) {
93
+ this._FirstLoginAt = value;
94
+ }
95
+
96
+ get LastLoginAt(): Date {
97
+ return this._LastLoginAt;
98
+ }
99
+
100
+ private set LastLoginAt(value: Date) {
101
+ this._LastLoginAt = value;
102
+ }
103
+
104
+ get MFAEnabled(): number {
105
+ return this._MFAEnabled;
106
+ }
107
+
108
+ private set MFAEnabled(value: number) {
109
+ this._MFAEnabled = value;
110
+ }
111
+
112
+ get MFAConfig(): string {
113
+ return this._MFAConfig;
114
+ }
115
+
116
+ private set MFAConfig(value: string) {
117
+ this._MFAConfig = value;
118
+ }
119
+
120
+ get RecoveryEmail(): string {
121
+ return this._RecoveryEmail;
122
+ }
123
+
124
+ private set RecoveryEmail(value: string) {
125
+ this._RecoveryEmail = value;
126
+ }
127
+
128
+ get FailedLoginAttemptCount(): number {
129
+ return this._FailedLoginAttemptCount;
130
+ }
131
+
132
+ private set FailedLoginAttemptCount(value: number) {
133
+ this._FailedLoginAttemptCount = value;
134
+ }
135
+
136
+ get LastFailedLoginAt(): Date {
137
+ return this._LastFailedLoginAt;
138
+ }
139
+
140
+ private set LastFailedLoginAt(value: Date) {
141
+ this._LastFailedLoginAt = value;
142
+ }
143
+
144
+ get LastPasswordChangedAt(): Date {
145
+ return this._LastPasswordChangedAt;
146
+ }
147
+
148
+ private set LastPasswordChangedAt(value: Date) {
149
+ this._LastPasswordChangedAt = value;
150
+ }
151
+
152
+ get NeedToChangePasswordYN(): YN {
153
+ return this._NeedToChangePasswordYN;
154
+ }
155
+
156
+ private set NeedToChangePasswordYN(value: YN) {
157
+ this._NeedToChangePasswordYN = value;
158
+ }
159
+
160
+ get CreatedById(): number {
161
+ return this._CreatedById;
162
+ }
163
+
164
+ private set CreatedById(value: number) {
165
+ this._CreatedById = value;
166
+ }
167
+
168
+ get CreatedAt(): Date {
169
+ return this._CreatedAt;
170
+ }
171
+
172
+ private set CreatedAt(value: Date) {
173
+ this._CreatedAt = value;
174
+ }
175
+
176
+ get UpdatedById(): number {
177
+ return this._UpdatedById;
178
+ }
179
+
180
+ private set UpdatedById(value: number) {
181
+ this._UpdatedById = value;
182
+ }
183
+
184
+ get UpdatedAt(): Date {
185
+ return this._UpdatedAt;
186
+ }
187
+
188
+ private set UpdatedAt(value: Date) {
189
+ this._UpdatedAt = value;
190
+ }
191
+
192
+ async getDetails(): Promise<{
193
+ FullName: string;
194
+ IDNo: string;
195
+ IDType: string;
196
+ Email: string;
197
+ ContactNo: string;
198
+ }> {
199
+ return {
200
+ FullName: this.FullName,
201
+ IDNo: this.IDNo,
202
+ IDType: this.IDType,
203
+ Email: this.Email,
204
+ ContactNo: this.ContactNo,
205
+ };
206
+ }
207
+
208
+ private constructor(
209
+ sessionService: ISessionService,
210
+ dbTransaction?: any,
211
+ userInfo?: IUserAttr,
212
+ ) {
213
+ super();
214
+ this._SessionService = sessionService;
215
+
216
+ if (dbTransaction) {
217
+ this._dbTransaction = dbTransaction;
218
+ }
219
+ // set all the class properties
220
+ if (userInfo) {
221
+ this.UserId = userInfo.UserId;
222
+ this.FullName = userInfo.FullName;
223
+ this.IDNo = userInfo.IDNo;
224
+ this.Email = userInfo.Email;
225
+ this.ContactNo = userInfo.ContactNo;
226
+ this.Password = userInfo.Password;
227
+ this.staffs = userInfo.staffs;
228
+ }
229
+ }
230
+
231
+ static async init(
232
+ sessionService: ISessionService,
233
+ userId?: number,
234
+ dbTransaction = null,
235
+ ): Promise<LoginUser> {
236
+ if (userId) {
237
+ if (dbTransaction) {
238
+ LoginUser._Repository = new UserRepository();
239
+ }
240
+ const user = await LoginUser._Repository.findOne({
241
+ where: {
242
+ UserId: userId,
243
+ },
244
+ include: [
245
+ {
246
+ model: Staff,
247
+ },
248
+ ],
249
+ });
250
+
251
+ if (!user) {
252
+ throw new Error('Invalid credentials.');
253
+ }
254
+
255
+ if (user) {
256
+ const userAttr: IUserAttr = {
257
+ UserId: user.UserId,
258
+ FullName: user.Staff.FullName,
259
+ IDNo: user.Staff.IdNo,
260
+ ContactNo: user.Staff.Mobile,
261
+ Email: user.Email,
262
+ Password: user.Password,
263
+ Status: user.Status,
264
+ DefaultPasswordChangedYN: user.DefaultPasswordChangedYN,
265
+ FirstLoginAt: user.FirstLoginAt,
266
+ LastLoginAt: user.LastLoginAt,
267
+ MFAEnabled: user.MFAEnabled,
268
+ MFAConfig: user.MFAConfig,
269
+ RecoveryEmail: user.RecoveryEmail,
270
+ FailedLoginAttemptCount: user.FailedLoginAttemptCount,
271
+ LastFailedLoginAt: user.LastFailedLoginAt,
272
+ LastPasswordChangedAt: user.LastPasswordChangedAt,
273
+ NeedToChangePasswordYN: user.NeedToChangePasswordYN,
274
+ CreatedById: user.CreatedById,
275
+ CreatedAt: user.CreatedAt,
276
+ UpdatedById: user.UpdatedById,
277
+ UpdatedAt: user.UpdatedAt,
278
+ staffs: user.Staff,
279
+ };
280
+
281
+ return new LoginUser(sessionService, dbTransaction, userAttr);
282
+ } else {
283
+ throw new Error('User not found');
284
+ }
285
+ }
286
+ return new LoginUser(sessionService, dbTransaction);
287
+ }
288
+
289
+ async login(
290
+ systemCode: string,
291
+ email: string,
292
+ password: string,
293
+ ipAddress: string,
294
+ ): Promise<string> {
295
+ try {
296
+ //validate email
297
+ if (!this.ObjectId) {
298
+ const user = await LoginUser._Repository.findOne({
299
+ where: {
300
+ Email: email,
301
+ },
302
+ include: [
303
+ {
304
+ model: Staff,
305
+ },
306
+ ],
307
+ });
308
+
309
+ const userAttr: IUserAttr = {
310
+ UserId: user.UserId,
311
+ FullName: user.Staff.FullName,
312
+ IDNo: user.Staff.IdNo,
313
+ ContactNo: user.Staff.Mobile,
314
+ Email: user.Email,
315
+ Password: user.Password,
316
+ Status: user.Status,
317
+ DefaultPasswordChangedYN: user.DefaultPasswordChangedYN,
318
+ FirstLoginAt: user.FirstLoginAt,
319
+ LastLoginAt: user.LastLoginAt,
320
+ MFAEnabled: user.MFAEnabled,
321
+ MFAConfig: user.MFAConfig,
322
+ RecoveryEmail: user.RecoveryEmail,
323
+ FailedLoginAttemptCount: user.FailedLoginAttemptCount,
324
+ LastFailedLoginAt: user.LastFailedLoginAt,
325
+ LastPasswordChangedAt: user.LastPasswordChangedAt,
326
+ NeedToChangePasswordYN: user.NeedToChangePasswordYN,
327
+ CreatedById: user.CreatedById,
328
+ CreatedAt: user.CreatedAt,
329
+ UpdatedById: user.UpdatedById,
330
+ UpdatedAt: user.UpdatedAt,
331
+ staffs: user.Staff,
332
+ };
333
+
334
+ this.UserId = userAttr.UserId;
335
+ this.FullName = userAttr.FullName;
336
+ this.IDNo = userAttr.IDNo;
337
+ this.Email = userAttr.Email;
338
+ this.ContactNo = userAttr.ContactNo;
339
+ this.Password = userAttr.Password;
340
+ this.Status = userAttr.Status;
341
+ this.DefaultPasswordChangedYN = userAttr.DefaultPasswordChangedYN;
342
+ this.FirstLoginAt = userAttr.FirstLoginAt;
343
+ this.LastLoginAt = userAttr.LastLoginAt;
344
+ this.MFAEnabled = userAttr.MFAEnabled;
345
+ this.MFAConfig = userAttr.MFAConfig;
346
+ this.RecoveryEmail = userAttr.RecoveryEmail;
347
+ this.FailedLoginAttemptCount = userAttr.FailedLoginAttemptCount;
348
+ this.LastFailedLoginAt = userAttr.LastFailedLoginAt;
349
+ this.LastPasswordChangedAt = userAttr.LastPasswordChangedAt;
350
+ this.NeedToChangePasswordYN = userAttr.NeedToChangePasswordYN;
351
+ this.CreatedById = userAttr.CreatedById;
352
+ this.CreatedAt = userAttr.CreatedAt;
353
+ this.UpdatedById = userAttr.UpdatedById;
354
+ this.UpdatedAt = userAttr.UpdatedAt;
355
+ this.staffs = userAttr.staffs;
356
+ }
357
+
358
+ if (this.ObjectId && this.Email !== email) {
359
+ throw new Error('Invalid credentials.');
360
+ }
361
+
362
+ //validate password
363
+ const isPasswordValid = await this._PasswordHashService.verify(
364
+ password,
365
+ this.Password,
366
+ );
367
+
368
+ if (!isPasswordValid) {
369
+ throw new Error('Invalid credentials.');
370
+ }
371
+
372
+ //validate system code
373
+ const system = await LoginUser._SystemRepository.findOne({
374
+ where: {
375
+ code: systemCode,
376
+ },
377
+ });
378
+
379
+ if (!system) {
380
+ throw new Error('Invalid system code.');
381
+ }
382
+
383
+ //validate system access
384
+ await this.checkSystemAccess(this.UserId, system.id);
385
+ // alert user if new login
386
+ await this.alertNewLogin(this.ObjectId, system.id.toString(), ipAddress);
387
+
388
+ // fetch user session if exists
389
+ const userSession = await this._SessionService.retrieveUserSession(
390
+ this.ObjectId,
391
+ );
392
+ let systemLogin = userSession.systemLogins.find(
393
+ (system) => system.code === systemCode,
394
+ );
395
+
396
+ // generate new session id
397
+ const { randomUUID } = require('crypto');
398
+ const sessionId = randomUUID();
399
+
400
+ if (systemLogin) {
401
+ systemLogin = systemLogin.sessionId = sessionId;
402
+ userSession.systemLogins.map((system) =>
403
+ system.code === systemCode ? systemLogin : system,
404
+ );
405
+ } else {
406
+ // if not, add new system login into the userSession
407
+ const newLogin = {
408
+ id: system.id.toString(),
409
+ code: system.Code,
410
+ sessionId: sessionId,
411
+ privileges: await this.getPrivileges(system.Code),
412
+ };
413
+ userSession.systemLogins.push(newLogin);
414
+ }
415
+ // then update userSession inside the redis storage with 1 day duration of time-to-live
416
+ this._SessionService.setUserSession(this.ObjectId, userSession);
417
+
418
+ // record new login history
419
+ await LoginUser._LoginHistoryRepository.create({
420
+ UserId: this.UserId,
421
+ SystemId: system.id,
422
+ OriginIp: ipAddress,
423
+ CreatedAt: new Date(),
424
+ });
425
+
426
+ return `${this.UserId}:${sessionId}`;
427
+ } catch (error) {
428
+ throw error;
429
+ }
430
+ }
431
+
432
+ private async checkSystemAccess(
433
+ userId: number,
434
+ systemId: number,
435
+ ): Promise<void> {
436
+ try {
437
+ const systemAccess = await LoginUser._SystemAccessRepository.findOne({
438
+ where: {
439
+ UserId: userId,
440
+ SystemId: systemId,
441
+ },
442
+ });
443
+
444
+ if (!systemAccess) {
445
+ throw new Error("User don't have access to the system.");
446
+ }
447
+ } catch (error) {
448
+ throw error;
449
+ }
450
+ }
451
+
452
+ private async alertNewLogin(
453
+ userId: string,
454
+ systemId: string,
455
+ ipAddress: string,
456
+ ) {
457
+ try {
458
+ const userLogins = await LoginUser._LoginHistoryRepository.findAll({
459
+ where: {
460
+ UserId: userId,
461
+ SystemId: systemId,
462
+ },
463
+ });
464
+
465
+ const gotPreviousLogins = userLogins?.length !== 0;
466
+ let ipFound: LoginHistory | undefined = undefined;
467
+ if (gotPreviousLogins) {
468
+ ipFound = userLogins.find((item) => item.OriginIp === ipAddress);
469
+ }
470
+
471
+ if (gotPreviousLogins && !ipFound) {
472
+ const EMAIL_SENDER =
473
+ process.env.EMAIL_SENDER || 'itd-system@tomei.com.my';
474
+ const transporter = new SMTPMailer();
475
+
476
+ await transporter.sendMail({
477
+ from: EMAIL_SENDER,
478
+ to: this.Email,
479
+ subject: 'New Login Alert',
480
+ html: `<p>Dear ${this.FullName},</p>
481
+ <p>There was a new login to your account from ${ipAddress} on ${new Date().toLocaleString()}.</p>
482
+ <p>If this was you, you can safely ignore this email.</p>
483
+ <p>If you suspect that someone else is trying to access your account, please contact us immediately at itd-support@tomei.com.my.</p>
484
+ <p>Thank you!,</p>
485
+ <p>
486
+ Best Regards,
487
+ IT Department
488
+ </p>`,
489
+ });
490
+ }
491
+ } catch (error) {
492
+ throw error;
493
+ }
494
+ }
495
+
496
+ private async getPrivileges(systemCode: string): Promise<string[]> {
497
+ try {
498
+ const system = await LoginUser._SystemRepository.findOne({
499
+ where: {
500
+ Code: systemCode,
501
+ },
502
+ });
503
+
504
+ if (!system) {
505
+ throw new Error('Invalid system code.');
506
+ }
507
+
508
+ // retrive user userGroups with system privileges
509
+ const userUserGroups = await this.getUserUserGroupFromDB(system.id);
510
+
511
+ // get all userGroup data from user userGroups
512
+ const userGroupData = userUserGroups.map((u) => u.UserGroup);
513
+ // get all privileges from userGroup data
514
+ let privileges: string[] = [];
515
+ for (const userGroup of userGroupData) {
516
+ const groupSystemPrivileges = userGroup.GroupSystemPrivileges.map(
517
+ (g) => g.SystemPrivilege.Code,
518
+ );
519
+ const groupRolePrivileges = userGroup.GroupRolePrivileges.map(
520
+ (g) => g.SystemPrivilege.Code,
521
+ );
522
+
523
+ // if userGroup is not root, get all parent tree privileges
524
+ if (
525
+ userGroup.GroupLevel !== 0 &&
526
+ userGroup.AllowInheritFromParentYN === 'Y'
527
+ ) {
528
+ // get all parent tree privileges
529
+ const parentTreePrivileges = await this.getPrivilegesFromUserGroup(
530
+ userGroup.ParentGroupCode,
531
+ );
532
+
533
+ privileges = [...privileges, ...parentTreePrivileges];
534
+ }
535
+
536
+ privileges = [
537
+ ...privileges,
538
+ ...groupSystemPrivileges,
539
+ ...groupRolePrivileges,
540
+ ];
541
+ }
542
+
543
+ // retrive all user personal privileges
544
+ const userPrivileges = await this.getUserPersonalPrivileges(system.id);
545
+
546
+ privileges = [...privileges, ...userPrivileges];
547
+ privileges = [...new Set(privileges)];
548
+ return privileges;
549
+ } catch (error) {
550
+ throw error;
551
+ }
552
+ }
553
+
554
+ private async getPrivilegesFromUserGroup(
555
+ groupCode: string,
556
+ ): Promise<string[]> {
557
+ try {
558
+ // Retrieve userGroup from the database based on groupCode
559
+ const userGroup = await this.getUserGroupFromDB(groupCode);
560
+ let privileges: string[] = [];
561
+
562
+ // Add privileges from the userGroup to the privileges array
563
+ privileges = [
564
+ ...privileges,
565
+ ...userGroup.GroupSystemPrivileges.map((g) => g.SystemPrivilege.Code),
566
+ ...userGroup.GroupRolePrivileges.map((g) => g.SystemPrivilege.Code),
567
+ ];
568
+
569
+ // Recursive call if conditions are not met and ParentGroupCode exists
570
+ const isContinue =
571
+ userGroup.GroupLevel !== 0 &&
572
+ userGroup.AllowInheritFromParentYN === 'Y';
573
+ if (isContinue) {
574
+ const recursivePrivileges = await this.getPrivilegesFromUserGroup(
575
+ userGroup.ParentGroupCode,
576
+ );
577
+ privileges = privileges.concat(recursivePrivileges);
578
+ }
579
+
580
+ // return privileges array
581
+ return privileges;
582
+ } catch (error) {
583
+ throw error;
584
+ }
585
+ }
586
+
587
+ private async getUserGroupFromDB(groupCode: string): Promise<any> {
588
+ try {
589
+ const userGroup = await LoginUser._UserGroupRepository.findOne({
590
+ where: {
591
+ GroupCode: groupCode,
592
+ },
593
+ include: [
594
+ {
595
+ model: GroupSystemPrivilege,
596
+ include: {
597
+ model: SystemPrivilege,
598
+ },
599
+ },
600
+ {
601
+ model: GroupRolePrivilege,
602
+ include: {
603
+ model: SystemPrivilege,
604
+ },
605
+ },
606
+ ],
607
+ });
608
+ return userGroup;
609
+ } catch (error) {
610
+ throw error;
611
+ }
612
+ }
613
+
614
+ private async getUserUserGroupFromDB(systemCode: number) {
615
+ try {
616
+ return await LoginUser._UserUserGroupRepository.findAll({
617
+ where: {
618
+ UserId: this.UserId,
619
+ SystemId: systemCode,
620
+ },
621
+ include: {
622
+ model: UserGroup,
623
+ include: [
624
+ {
625
+ model: GroupSystemPrivilege,
626
+ include: {
627
+ model: SystemPrivilege,
628
+ },
629
+ },
630
+ {
631
+ model: GroupRolePrivilege,
632
+ include: {
633
+ model: SystemPrivilege,
634
+ },
635
+ },
636
+ ],
637
+ },
638
+ });
639
+ } catch (error) {
640
+ throw error;
641
+ }
642
+ }
643
+
644
+ private async getUserPersonalPrivileges(systemId: number): Promise<string[]> {
645
+ try {
646
+ const userRole = await LoginUser._Repository.findOne({
647
+ where: {
648
+ UserId: this.ObjectId,
649
+ },
650
+ include: {
651
+ model: SystemPrivilege,
652
+ },
653
+ });
654
+
655
+ //retrive all user systemPrevileges data from user roles
656
+ let userSystemPrivileges = userRole.SystemPrivileges;
657
+
658
+ userSystemPrivileges = userSystemPrivileges.filter(
659
+ (u) => u.SystemId === systemId,
660
+ );
661
+
662
+ const userPrivileges: string[] = userSystemPrivileges.map((u) => u.Code);
663
+ return userPrivileges;
664
+ } catch (error) {
665
+ throw error;
666
+ }
667
+ }
668
+
669
+ async checkPrivileges(
670
+ systemCode: string,
671
+ privilegeName: string,
672
+ ): Promise<boolean> {
673
+ try {
674
+ if (!this.ObjectId) {
675
+ throw new Error('ObjectId(UserId) is not set');
676
+ }
677
+
678
+ const userSession = await this._SessionService.retrieveUserSession(
679
+ this.ObjectId,
680
+ );
681
+
682
+ const systemLogin = userSession.systemLogins.find(
683
+ (system) => system.code === systemCode,
684
+ );
685
+
686
+ if (!systemLogin) {
687
+ return false;
688
+ }
689
+
690
+ const privileges = systemLogin.privileges;
691
+ const hasPrivilege = privileges.includes(privilegeName);
692
+ return hasPrivilege;
693
+ } catch (error) {
694
+ throw error;
695
+ }
696
+ }
697
+
698
+ async checkSession(
699
+ systemCode: string,
700
+ sessionId: string,
701
+ userId: string,
702
+ ): Promise<ISystemLogin> {
703
+ try {
704
+ const userSession = await this._SessionService.retrieveUserSession(
705
+ userId,
706
+ );
707
+
708
+ if (userSession.systemLogins.length === 0) {
709
+ throw new Error('Session expired.');
710
+ }
711
+
712
+ const systemLogin = userSession.systemLogins.find(
713
+ (sl) => sl.code === systemCode,
714
+ );
715
+
716
+ if (!systemLogin) {
717
+ throw new Error('Session expired.');
718
+ }
719
+
720
+ if (systemLogin.sessionId !== sessionId) {
721
+ throw new Error('Session expired.');
722
+ }
723
+
724
+ await this._SessionService.refreshDuration(userId);
725
+
726
+ return systemLogin;
727
+ } catch (error) {
728
+ throw error;
729
+ }
730
+ }
731
+
732
+ async logout(systemCode: string) {
733
+ try {
734
+ if (!this.ObjectId) {
735
+ throw new Error('ObjectId(UserId) is not set');
736
+ }
737
+ const userSession = await this._SessionService.retrieveUserSession(
738
+ this.ObjectId,
739
+ );
740
+ const index = userSession.systemLogins.findIndex(
741
+ (system) => system.code === systemCode,
742
+ );
743
+ userSession.systemLogins.splice(index, 1);
744
+ this._SessionService.setUserSession(this.ObjectId, userSession);
745
+ } catch (error) {
746
+ throw error;
747
+ }
748
+ }
749
+ }