@tomei/sso 0.13.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.commitlintrc.json +22 -22
  2. package/.eslintrc +16 -16
  3. package/.eslintrc.js +35 -35
  4. package/.husky/commit-msg +15 -15
  5. package/.husky/pre-commit +7 -7
  6. package/.prettierrc +4 -4
  7. package/Jenkinsfile +57 -57
  8. package/README.md +23 -23
  9. package/__tests__/unit/components/login-user/login-user.spec.ts +742 -742
  10. package/__tests__/unit/components/password-hash/password-hash.service.spec.ts +31 -31
  11. package/__tests__/unit/redis-client/redis.service.spec.ts +23 -23
  12. package/__tests__/unit/session/session.service.spec.ts +47 -47
  13. package/__tests__/unit/system-privilege/system-privilage.spec.ts +91 -91
  14. package/create-sso-user.sql +39 -39
  15. package/dist/__tests__/unit/components/login-history/login-history.repository.spec.d.ts +0 -0
  16. package/dist/__tests__/unit/components/login-history/login-history.repository.spec.js +1 -0
  17. package/dist/__tests__/unit/components/login-history/login-history.repository.spec.js.map +1 -0
  18. package/dist/__tests__/unit/components/login-user/user.repository.spec.d.ts +0 -0
  19. package/dist/__tests__/unit/components/login-user/user.repository.spec.js +1 -0
  20. package/dist/__tests__/unit/components/login-user/user.repository.spec.js.map +1 -0
  21. package/dist/__tests__/unit/components/system/system.repository.spec.d.ts +0 -0
  22. package/dist/__tests__/unit/components/system/system.repository.spec.js +1 -0
  23. package/dist/__tests__/unit/components/system/system.repository.spec.js.map +1 -0
  24. package/dist/__tests__/unit/components/system-access/system-access.repository.spec.d.ts +0 -0
  25. package/dist/__tests__/unit/components/system-access/system-access.repository.spec.js +1 -0
  26. package/dist/__tests__/unit/components/system-access/system-access.repository.spec.js.map +1 -0
  27. package/dist/src/components/login-user/login-user.d.ts +1 -0
  28. package/dist/src/components/login-user/login-user.js +9 -8
  29. package/dist/src/components/login-user/login-user.js.map +1 -1
  30. package/dist/src/prisma-client/__mocks__/prisma.d.ts +3 -0
  31. package/dist/src/prisma-client/__mocks__/prisma.js +14 -0
  32. package/dist/src/prisma-client/__mocks__/prisma.js.map +1 -0
  33. package/dist/src/prisma-client/client.d.ts +3 -0
  34. package/dist/src/prisma-client/client.js +6 -0
  35. package/dist/src/prisma-client/client.js.map +1 -0
  36. package/dist/src/prisma-client/index.d.ts +1 -0
  37. package/dist/src/prisma-client/index.js +18 -0
  38. package/dist/src/prisma-client/index.js.map +1 -0
  39. package/dist/tsconfig.tsbuildinfo +1 -1
  40. package/jest.config.js +13 -13
  41. package/migrations/01-alter-system-privilege-table.js +13 -13
  42. package/migrations/02-alter-user-group-table.js +78 -78
  43. package/migrations/03-alter-user-system-privilege-table.js +38 -38
  44. package/migrations/04-create-user-user-group-table.js +55 -55
  45. package/migrations/05-create-login-history-table.js +49 -49
  46. package/package.json +79 -79
  47. package/sampledotenv +7 -7
  48. package/src/components/login-user/login-user.ts +557 -556
  49. package/tsconfig.build.json +5 -5
  50. package/tsconfig.json +23 -23
@@ -1,556 +1,557 @@
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
- staffs: any;
32
-
33
- private _OriginIP: string;
34
- private _SessionService: ISessionService;
35
- private _PasswordHashService = new PasswordHashService();
36
- private static _Repository = new UserRepository();
37
- private static _SystemRepository = new SystemRepository();
38
- private static _SystemAccessRepository = new SystemAccessRepository();
39
- private static _LoginHistoryRepository = new LoginHistoryRepository();
40
- private static _UserUserGroupRepository = new UserUserGroupRepository();
41
- private static _UserGroupRepository = new UserGroupRepository();
42
- private _dbTransaction: any;
43
-
44
- async getDetails(): Promise<{
45
- FullName: string;
46
- IDNo: string;
47
- IDType: string;
48
- Email: string;
49
- ContactNo: string;
50
- }> {
51
- return {
52
- FullName: this.FullName,
53
- IDNo: this.IDNo,
54
- IDType: this.IDType,
55
- Email: this.Email,
56
- ContactNo: this.ContactNo,
57
- };
58
- }
59
-
60
- private constructor(
61
- sessionService: ISessionService,
62
- dbTransaction?: any,
63
- userInfo?: IUserInfo,
64
- ) {
65
- super();
66
- this._SessionService = sessionService;
67
-
68
- if (dbTransaction) {
69
- this._dbTransaction = dbTransaction;
70
- }
71
- // set all the class properties
72
- if (userInfo) {
73
- this.ObjectId = userInfo.ObjectId;
74
- this.FullName = userInfo.FullName;
75
- this.IDNo = userInfo.IDNo;
76
- this.Email = userInfo.Email;
77
- this.ContactNo = userInfo.ContactNo;
78
- this.Password = userInfo.Password;
79
- this.staffs = userInfo.staffs;
80
- }
81
- }
82
-
83
- static async init(
84
- sessionService: ISessionService,
85
- userId?: string,
86
- dbTransaction = null,
87
- ): Promise<LoginUser> {
88
- if (userId) {
89
- if (dbTransaction) {
90
- LoginUser._Repository = new UserRepository();
91
- }
92
- const user = await LoginUser._Repository.findOne({
93
- where: {
94
- id: Number(userId),
95
- },
96
- include: [
97
- {
98
- model: Staff,
99
- },
100
- ],
101
- });
102
-
103
- if (!user) {
104
- throw new Error('Invalid credentials.');
105
- }
106
-
107
- if (user) {
108
- const userInfo: IUserInfo = {
109
- ObjectId: user.id.toString(),
110
- FullName: user.Staff.FullName,
111
- IDNo: user.Staff.IdNo,
112
- ContactNo: user.Staff.Mobile,
113
- Email: user.Email,
114
- Password: user.Password,
115
- staffs: user.Staff,
116
- };
117
-
118
- return new LoginUser(sessionService, dbTransaction, userInfo);
119
- } else {
120
- throw new Error('User not found');
121
- }
122
- }
123
- return new LoginUser(sessionService, dbTransaction);
124
- }
125
-
126
- async login(
127
- systemCode: string,
128
- email: string,
129
- password: string,
130
- ipAddress: string,
131
- ): Promise<string> {
132
- try {
133
- //validate email
134
- if (!this.ObjectId) {
135
- const user = await LoginUser._Repository.findOne({
136
- where: {
137
- Email: email,
138
- },
139
- include: [
140
- {
141
- model: Staff,
142
- },
143
- ],
144
- });
145
-
146
- const userInfo: IUserInfo = {
147
- ObjectId: user.id.toString(),
148
- FullName: user.Staff.FullName,
149
- IDNo: user.Staff.IdNo,
150
- ContactNo: user.Staff.Mobile,
151
- Email: user.Staff.Email,
152
- Password: user.Password,
153
- staffs: user.Staff,
154
- };
155
-
156
- this.ObjectId = userInfo.ObjectId;
157
- this.FullName = userInfo.FullName;
158
- this.IDNo = userInfo.IDNo;
159
- this.Email = userInfo.Email;
160
- this.ContactNo = userInfo.ContactNo;
161
- this.Password = userInfo.Password;
162
- this.staffs = userInfo.staffs;
163
- }
164
-
165
- if (this.ObjectId && this.Email !== email) {
166
- throw new Error('Invalid credentials.');
167
- }
168
-
169
- //validate password
170
- const isPasswordValid = await this._PasswordHashService.verify(
171
- password,
172
- this.Password,
173
- );
174
-
175
- if (!isPasswordValid) {
176
- throw new Error('Invalid credentials.');
177
- }
178
-
179
- //validate system code
180
- const system = await LoginUser._SystemRepository.findOne({
181
- where: {
182
- code: systemCode,
183
- },
184
- });
185
-
186
- if (!system) {
187
- throw new Error('Invalid system code.');
188
- }
189
-
190
- //validate system access
191
- await this.checkSystemAccess(this.ObjectId, system.id);
192
- // alert user if new login
193
- // await this.alertNewLogin(this.ObjectId, system.id.toString(), ipAddress);
194
-
195
- // fetch user session if exists
196
- const userSession = await this._SessionService.retrieveUserSession(
197
- this.ObjectId,
198
- );
199
- let systemLogin = userSession.systemLogins.find(
200
- (system) => system.code === systemCode,
201
- );
202
-
203
- // generate new session id
204
- const { randomUUID } = require('crypto');
205
- const sessionId = randomUUID();
206
-
207
- if (systemLogin) {
208
- systemLogin = systemLogin.sessionId = sessionId;
209
- userSession.systemLogins.map((system) =>
210
- system.code === systemCode ? systemLogin : system,
211
- );
212
- } else {
213
- // if not, add new system login into the userSession
214
- const newLogin = {
215
- id: system.id.toString(),
216
- code: system.Code,
217
- sessionId: sessionId,
218
- privileges: await this.getPrivileges(system.Code),
219
- };
220
- userSession.systemLogins.push(newLogin);
221
- }
222
- // then update userSession inside the redis storage with 1 day duration of time-to-live
223
- this._SessionService.setUserSession(this.ObjectId, userSession);
224
-
225
- // record new login history
226
- await LoginUser._LoginHistoryRepository.create({
227
- UserId: this.ObjectId,
228
- SystemId: system.id,
229
- OriginIp: ipAddress,
230
- CreatedAt: new Date(),
231
- });
232
-
233
- return `${this.ObjectId}:${sessionId}`;
234
- } catch (error) {
235
- throw error;
236
- }
237
- }
238
-
239
- private async checkSystemAccess(
240
- userId: string,
241
- systemId: number,
242
- ): Promise<void> {
243
- try {
244
- const systemAccess = await LoginUser._SystemAccessRepository.findOne({
245
- where: {
246
- UserId: userId,
247
- SystemId: systemId,
248
- },
249
- });
250
-
251
- if (!systemAccess) {
252
- throw new Error("User don't have access to the system.");
253
- }
254
- } catch (error) {
255
- throw error;
256
- }
257
- }
258
-
259
- private async alertNewLogin(
260
- userId: string,
261
- systemId: string,
262
- ipAddress: string,
263
- ) {
264
- try {
265
- const userLogins = await LoginUser._LoginHistoryRepository.findAll({
266
- where: {
267
- UserId: userId,
268
- SystemId: systemId,
269
- },
270
- });
271
-
272
- const gotPreviousLogins = userLogins?.length !== 0;
273
- let ipFound: LoginHistory | undefined = undefined;
274
- if (gotPreviousLogins) {
275
- ipFound = userLogins.find((item) => item.OriginIp === ipAddress);
276
- }
277
-
278
- if (gotPreviousLogins && !ipFound) {
279
- const EMAIL_SENDER =
280
- process.env.EMAIL_SENDER || 'itd-system@tomei.com.my';
281
- const transporter = new SMTPMailer();
282
-
283
- await transporter.sendMail({
284
- from: EMAIL_SENDER,
285
- to: this.Email,
286
- subject: 'New Login Alert',
287
- html: `<p>Dear ${this.FullName},</p>
288
- <p>There was a new login to your account from ${ipAddress} on ${new Date().toLocaleString()}.</p>
289
- <p>If this was you, you can safely ignore this email.</p>
290
- <p>If you suspect that someone else is trying to access your account, please contact us immediately at itd-support@tomei.com.my.</p>
291
- <p>Thank you!,</p>
292
- <p>
293
- Best Regards,
294
- IT Department
295
- </p>`,
296
- });
297
- }
298
- } catch (error) {
299
- throw error;
300
- }
301
- }
302
-
303
- private async getPrivileges(systemCode: string): Promise<string[]> {
304
- try {
305
- const system = await LoginUser._SystemRepository.findOne({
306
- where: {
307
- Code: systemCode,
308
- },
309
- });
310
-
311
- if (!system) {
312
- throw new Error('Invalid system code.');
313
- }
314
-
315
- // retrive user userGroups with system privileges
316
- const userUserGroups = await this.getUserUserGroupFromDB(system.id);
317
-
318
- // get all userGroup data from user userGroups
319
- const userGroupData = userUserGroups.map((u) => u.UserGroup);
320
- // get all privileges from userGroup data
321
- let privileges: string[] = [];
322
- for (const userGroup of userGroupData) {
323
- const groupSystemPrivileges = userGroup.GroupSystemPrivileges.map(
324
- (g) => g.SystemPrivilege.Code,
325
- );
326
- const groupRolePrivileges = userGroup.GroupRolePrivileges.map(
327
- (g) => g.SystemPrivilege.Code,
328
- );
329
-
330
- // if userGroup is not root, get all parent tree privileges
331
- if (
332
- userGroup.GroupLevel !== 0 &&
333
- userGroup.AllowInheritFromParentYN === 'Y'
334
- ) {
335
- // get all parent tree privileges
336
- const parentTreePrivileges = await this.getPrivilegesFromUserGroup(
337
- userGroup.ParentGroupCode,
338
- );
339
-
340
- privileges = [...privileges, ...parentTreePrivileges];
341
- }
342
-
343
- privileges = [
344
- ...privileges,
345
- ...groupSystemPrivileges,
346
- ...groupRolePrivileges,
347
- ];
348
- }
349
-
350
- // retrive all user personal privileges
351
- const userPrivileges = await this.getUserPersonalPrivileges(system.id);
352
-
353
- privileges = [...privileges, ...userPrivileges];
354
- privileges = [...new Set(privileges)];
355
- return privileges;
356
- } catch (error) {
357
- throw error;
358
- }
359
- }
360
-
361
- private async getPrivilegesFromUserGroup(
362
- groupCode: string,
363
- ): Promise<string[]> {
364
- try {
365
- // Retrieve userGroup from the database based on groupCode
366
- const userGroup = await this.getUserGroupFromDB(groupCode);
367
- let privileges: string[] = [];
368
-
369
- // Add privileges from the userGroup to the privileges array
370
- privileges = [
371
- ...privileges,
372
- ...userGroup.GroupSystemPrivileges.map((g) => g.SystemPrivilege.Code),
373
- ...userGroup.GroupRolePrivileges.map((g) => g.SystemPrivilege.Code),
374
- ];
375
-
376
- // Recursive call if conditions are not met and ParentGroupCode exists
377
- const isContinue =
378
- userGroup.GroupLevel !== 0 &&
379
- userGroup.AllowInheritFromParentYN === 'Y';
380
- if (isContinue) {
381
- const recursivePrivileges = await this.getPrivilegesFromUserGroup(
382
- userGroup.ParentGroupCode,
383
- );
384
- privileges = privileges.concat(recursivePrivileges);
385
- }
386
-
387
- // return privileges array
388
- return privileges;
389
- } catch (error) {
390
- throw error;
391
- }
392
- }
393
-
394
- private async getUserGroupFromDB(groupCode: string): Promise<any> {
395
- try {
396
- const userGroup = await LoginUser._UserGroupRepository.findOne({
397
- where: {
398
- GroupCode: groupCode,
399
- },
400
- include: [
401
- {
402
- model: GroupSystemPrivilege,
403
- include: {
404
- model: SystemPrivilege,
405
- },
406
- },
407
- {
408
- model: GroupRolePrivilege,
409
- include: {
410
- model: SystemPrivilege,
411
- },
412
- },
413
- ],
414
- });
415
- return userGroup;
416
- } catch (error) {
417
- throw error;
418
- }
419
- }
420
-
421
- private async getUserUserGroupFromDB(systemCode: number) {
422
- try {
423
- return await LoginUser._UserUserGroupRepository.findAll({
424
- where: {
425
- UserId: this.ObjectId,
426
- SystemId: systemCode,
427
- },
428
- include: {
429
- model: UserGroup,
430
- include: [
431
- {
432
- model: GroupSystemPrivilege,
433
- include: {
434
- model: SystemPrivilege,
435
- },
436
- },
437
- {
438
- model: GroupRolePrivilege,
439
- include: {
440
- model: SystemPrivilege,
441
- },
442
- },
443
- ],
444
- },
445
- });
446
- } catch (error) {
447
- throw error;
448
- }
449
- }
450
-
451
- private async getUserPersonalPrivileges(systemId: number): Promise<string[]> {
452
- try {
453
- const userRole = await LoginUser._Repository.findOne({
454
- where: {
455
- id: this.ObjectId,
456
- },
457
- include: {
458
- model: SystemPrivilege,
459
- },
460
- });
461
-
462
- //retrive all user systemPrevileges data from user roles
463
- let userSystemPrivileges = userRole.SystemPrivileges;
464
-
465
- userSystemPrivileges = userSystemPrivileges.filter(
466
- (u) => u.SystemId === systemId,
467
- );
468
-
469
- const userPrivileges: string[] = userSystemPrivileges.map((u) => u.Code);
470
- return userPrivileges;
471
- } catch (error) {
472
- throw error;
473
- }
474
- }
475
-
476
- async checkPrivileges(
477
- systemCode: string,
478
- privilegeName: string,
479
- ): Promise<boolean> {
480
- try {
481
- if (!this.ObjectId) {
482
- throw new Error('ObjectId(UserId) is not set');
483
- }
484
-
485
- const userSession = await this._SessionService.retrieveUserSession(
486
- this.ObjectId,
487
- );
488
-
489
- const systemLogin = userSession.systemLogins.find(
490
- (system) => system.code === systemCode,
491
- );
492
-
493
- if (!systemLogin) {
494
- return false;
495
- }
496
-
497
- const privileges = systemLogin.privileges;
498
- const hasPrivilege = privileges.includes(privilegeName);
499
- return hasPrivilege;
500
- } catch (error) {
501
- throw error;
502
- }
503
- }
504
-
505
- async checkSession(
506
- systemCode: string,
507
- sessionId: string,
508
- userId: string,
509
- ): Promise<ISystemLogin> {
510
- try {
511
- const userSession = await this._SessionService.retrieveUserSession(
512
- userId,
513
- );
514
-
515
- if (userSession.systemLogins.length === 0) {
516
- throw new Error('Session expired.');
517
- }
518
-
519
- const systemLogin = userSession.systemLogins.find(
520
- (sl) => sl.code === systemCode,
521
- );
522
-
523
- if (!systemLogin) {
524
- throw new Error('Session expired.');
525
- }
526
-
527
- if (systemLogin.sessionId !== sessionId) {
528
- throw new Error('Session expired.');
529
- }
530
-
531
- await this._SessionService.refreshDuration(userId);
532
-
533
- return systemLogin;
534
- } catch (error) {
535
- throw error;
536
- }
537
- }
538
-
539
- async logout(systemCode: string) {
540
- try {
541
- if (!this.ObjectId) {
542
- throw new Error('ObjectId(UserId) is not set');
543
- }
544
- const userSession = await this._SessionService.retrieveUserSession(
545
- this.ObjectId,
546
- );
547
- const index = userSession.systemLogins.findIndex(
548
- (system) => system.code === systemCode,
549
- );
550
- userSession.systemLogins.splice(index, 1);
551
- this._SessionService.setUserSession(this.ObjectId, userSession);
552
- } catch (error) {
553
- throw error;
554
- }
555
- }
556
- }
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
+ }