@tomei/sso 0.62.0 → 0.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/coverage/lcov-report/components/login-user/login-user.ts.html +80 -73
  2. package/dist/src/components/login-history/index.d.ts +1 -0
  3. package/dist/src/components/login-history/index.js +1 -0
  4. package/dist/src/components/login-history/index.js.map +1 -1
  5. package/dist/src/components/login-history/login-history.repository.d.ts +2 -2
  6. package/dist/src/components/login-history/login-history.repository.js.map +1 -1
  7. package/dist/src/components/login-user/interfaces/user-info.interface.d.ts +1 -0
  8. package/dist/src/components/login-user/login-user.js +1 -0
  9. package/dist/src/components/login-user/login-user.js.map +1 -1
  10. package/dist/src/components/login-user/user.d.ts +28 -3
  11. package/dist/src/components/login-user/user.js +360 -16
  12. package/dist/src/components/login-user/user.js.map +1 -1
  13. package/dist/src/components/user-system-access/user-system-access.js +1 -1
  14. package/dist/src/components/user-system-access/user-system-access.js.map +1 -1
  15. package/dist/src/models/login-history.entity.d.ts +2 -2
  16. package/dist/src/models/login-history.entity.js +13 -13
  17. package/dist/src/models/login-history.entity.js.map +1 -1
  18. package/dist/src/models/user.entity.d.ts +1 -0
  19. package/dist/src/models/user.entity.js +8 -0
  20. package/dist/src/models/user.entity.js.map +1 -1
  21. package/dist/tsconfig.tsbuildinfo +1 -1
  22. package/migrations/20250610070720-added-MFBypassYN-to-sso-user.js +30 -0
  23. package/package.json +1 -1
  24. package/src/components/login-history/index.ts +1 -0
  25. package/src/components/login-history/login-history.repository.ts +4 -4
  26. package/src/components/login-history/login-history.ts +124 -0
  27. package/src/components/login-user/interfaces/user-info.interface.ts +1 -0
  28. package/src/components/login-user/login-user.ts +1 -0
  29. package/src/components/login-user/user.ts +438 -18
  30. package/src/components/user-system-access/user-system-access.ts +1 -1
  31. package/src/interfaces/login-history-search-attr.interface.ts +8 -0
  32. package/src/interfaces/login-history.interface.ts +11 -0
  33. package/src/models/login-history.entity.ts +2 -2
  34. package/src/models/user.entity.ts +7 -0
@@ -49,6 +49,7 @@ export class User extends UserBase {
49
49
  private _LastLoginAt: Date;
50
50
  private _MFAEnabled: number;
51
51
  private _MFAConfig: string;
52
+ private _MFABypassYN: string;
52
53
  private _RecoveryEmail: string;
53
54
  private _FailedLoginAttemptCount: number;
54
55
  private _LastFailedLoginAt: Date;
@@ -162,6 +163,14 @@ export class User extends UserBase {
162
163
  this._MFAConfig = value;
163
164
  }
164
165
 
166
+ get MFABypassYN(): string {
167
+ return this._MFABypassYN;
168
+ }
169
+
170
+ private set MFABypassYN(value: string) {
171
+ this._MFABypassYN = value;
172
+ }
173
+
165
174
  get RecoveryEmail(): string {
166
175
  return this._RecoveryEmail;
167
176
  }
@@ -296,6 +305,7 @@ export class User extends UserBase {
296
305
  this.LastLoginAt = userInfo.LastLoginAt;
297
306
  this.MFAEnabled = userInfo.MFAEnabled;
298
307
  this.MFAConfig = userInfo.MFAConfig;
308
+ this.MFABypassYN = userInfo.MFABypassYN;
299
309
  this.RecoveryEmail = userInfo.RecoveryEmail;
300
310
  this.FailedLoginAttemptCount = userInfo.FailedLoginAttemptCount;
301
311
  this.LastFailedLoginAt = userInfo.LastFailedLoginAt;
@@ -352,6 +362,7 @@ export class User extends UserBase {
352
362
  LastLoginAt: user.LastLoginAt,
353
363
  MFAEnabled: user.MFAEnabled,
354
364
  MFAConfig: user.MFAConfig,
365
+ MFABypassYN: user.MFABypassYN,
355
366
  RecoveryEmail: user.RecoveryEmail,
356
367
  FailedLoginAttemptCount: user.FailedLoginAttemptCount,
357
368
  LastFailedLoginAt: user.LastFailedLoginAt,
@@ -416,6 +427,7 @@ export class User extends UserBase {
416
427
  LastLoginAt: user.LastLoginAt,
417
428
  MFAEnabled: user.MFAEnabled,
418
429
  MFAConfig: user.MFAConfig,
430
+ MFABypassYN: user.MFABypassYN,
419
431
  RecoveryEmail: user.RecoveryEmail,
420
432
  FailedLoginAttemptCount: user.FailedLoginAttemptCount,
421
433
  LastFailedLoginAt: user.LastFailedLoginAt,
@@ -510,6 +522,7 @@ export class User extends UserBase {
510
522
  LastLoginAt: user.LastLoginAt,
511
523
  MFAEnabled: user.MFAEnabled,
512
524
  MFAConfig: user.MFAConfig,
525
+ MFABypassYN: user.MFABypassYN,
513
526
  RecoveryEmail: user.RecoveryEmail,
514
527
  FailedLoginAttemptCount: user.FailedLoginAttemptCount,
515
528
  LastFailedLoginAt: user.LastFailedLoginAt,
@@ -574,8 +587,7 @@ export class User extends UserBase {
574
587
  },
575
588
  });
576
589
  if (!system) {
577
- console.error('Invalid system code:', systemCode);
578
- throw new Error('Invalid credentials.');
590
+ throw new Error('Access denied: invalid or unauthorized system.');
579
591
  }
580
592
 
581
593
  // 1.5: Instantiate new PasswordHashService object and call PasswordHashService.verify method to check whether the param.Password is correct.
@@ -605,14 +617,14 @@ export class User extends UserBase {
605
617
  await User.releaseLock(this.UserId, dbTransaction);
606
618
  this.Status = UserStatus.ACTIVE;
607
619
  } else {
608
- console.error('User is still locked:', this.UserId);
609
- throw new Error('Invalid credentials.');
620
+ throw new Error(
621
+ 'Your account has been locked. Please contact the administrator for assistance.',
622
+ );
610
623
  }
611
624
  }
612
625
  } catch (error) {
613
626
  await this.incrementFailedLoginAttemptCount(dbTransaction);
614
- console.error('Login failed for user:', this.UserId, error);
615
- throw new Error('Invalid credentials.');
627
+ throw error;
616
628
  }
617
629
 
618
630
  // 2.1: Call alertNewLogin to check whether the ip used is new ip and alert the user if it's new.
@@ -1580,6 +1592,7 @@ export class User extends UserBase {
1580
1592
  LastLoginAt: null,
1581
1593
  MFAEnabled: null,
1582
1594
  MFAConfig: null,
1595
+ MFABypassYN: YN.No,
1583
1596
  RecoveryEmail: null,
1584
1597
  FailedLoginAttemptCount: 0,
1585
1598
  LastFailedLoginAt: null,
@@ -1727,7 +1740,7 @@ export class User extends UserBase {
1727
1740
  throw new ClassError(
1728
1741
  'LoginUser',
1729
1742
  'LoginUserErrMsg0X',
1730
- 'Invalid credentials.',
1743
+ 'Your account has been locked due to too many failed login attempts, please contact IT Support for instructions on how to unlock your account.',
1731
1744
  );
1732
1745
  }
1733
1746
  }
@@ -1998,15 +2011,31 @@ export class User extends UserBase {
1998
2011
  }
1999
2012
 
2000
2013
  // 3. Verify the mfaToken by calling speakeasy.totp.verify
2001
- const isVerified = await speakeasy.totp.verify({
2014
+ const isCurrentValid = await speakeasy.totp.verify({
2002
2015
  secret: userMFAConfig.totp.secret,
2003
2016
  encoding: 'base32',
2004
2017
  token: mfaToken,
2018
+ window: 0, // strict current time window
2005
2019
  });
2020
+ if (!isCurrentValid) {
2021
+ const isExpired = await speakeasy.totp.verify({
2022
+ secret: userMFAConfig.totp.secret,
2023
+ encoding: 'base32',
2024
+ token: mfaToken,
2025
+ window: 2, // allow slight leeway: previous or next 2 time steps
2026
+ });
2006
2027
 
2007
- // 4. if not verified, then return false. if verified, Call LoginUser._Repo.update and update user data in database
2008
- if (!isVerified) {
2009
- return false;
2028
+ if (isExpired) {
2029
+ return {
2030
+ success: false,
2031
+ reason: 'MFA token has expired. Please try again.',
2032
+ };
2033
+ } else {
2034
+ return {
2035
+ success: false,
2036
+ reason: 'Invalid MFA token. Check your authenticator app.',
2037
+ };
2038
+ }
2010
2039
  }
2011
2040
 
2012
2041
  user.MFAEnabled = 1;
@@ -2035,7 +2064,7 @@ export class User extends UserBase {
2035
2064
  const systemLogin = userSession.systemLogins.find(
2036
2065
  (e) => e.code === systemCode,
2037
2066
  );
2038
- return `${userId}:${systemLogin.sessionId}`;
2067
+ return { success: true, sessionId: `${userId}:${systemLogin.sessionId}` };
2039
2068
  }
2040
2069
 
2041
2070
  // This method will verify 2FA codes
@@ -2073,15 +2102,31 @@ export class User extends UserBase {
2073
2102
  }
2074
2103
 
2075
2104
  // 3. Verify the mfaToken by calling speakeasy.totp.verify
2076
- const isVerified = await speakeasy.totp.verify({
2105
+ const isCurrentValid = await speakeasy.totp.verify({
2077
2106
  secret: userMFAConfig.totp.secret,
2078
2107
  encoding: 'base32',
2079
2108
  token: mfaToken,
2109
+ window: 0, // strict current time window
2080
2110
  });
2111
+ if (!isCurrentValid) {
2112
+ const isExpired = await speakeasy.totp.verify({
2113
+ secret: userMFAConfig.totp.secret,
2114
+ encoding: 'base32',
2115
+ token: mfaToken,
2116
+ window: 2, // allow slight leeway: previous or next 2 time steps
2117
+ });
2081
2118
 
2082
- // 4. if not verified, then return false. if verified, Call LoginUser._Repo.update and update user data in database
2083
- if (!isVerified) {
2084
- return false;
2119
+ if (isExpired) {
2120
+ return {
2121
+ success: false,
2122
+ reason: 'MFA token has expired. Please try again.',
2123
+ };
2124
+ } else {
2125
+ return {
2126
+ success: false,
2127
+ reason: 'Invalid MFA token. Check your authenticator app.',
2128
+ };
2129
+ }
2085
2130
  }
2086
2131
 
2087
2132
  // 5. Retrieve Session
@@ -2104,7 +2149,7 @@ export class User extends UserBase {
2104
2149
  const systemLogin = userSession.systemLogins.find(
2105
2150
  (e) => e.code === systemCode,
2106
2151
  );
2107
- return `${userId}:${systemLogin.sessionId}`;
2152
+ return { success: true, sessionId: `${userId}:${systemLogin.sessionId}` };
2108
2153
  }
2109
2154
 
2110
2155
  public async bypass2FA(systemCode: string, dbTransaction: any) {
@@ -2147,7 +2192,10 @@ export class User extends UserBase {
2147
2192
  const systemLogin = userSession.systemLogins.find(
2148
2193
  (e) => e.code === systemCode,
2149
2194
  );
2150
- return `${this.UserId}:${systemLogin.sessionId}`;
2195
+ return {
2196
+ success: true,
2197
+ sessionId: `${this.UserId}:${systemLogin.sessionId}`,
2198
+ };
2151
2199
  } catch (error) {
2152
2200
  throw error;
2153
2201
  }
@@ -2610,6 +2658,7 @@ export class User extends UserBase {
2610
2658
  LastLoginAt: user.LastLoginAt,
2611
2659
  MFAEnabled: user.MFAEnabled,
2612
2660
  MFAConfig: user.MFAConfig,
2661
+ MFABypassYN: user.MFABypassYN,
2613
2662
  RecoveryEmail: user.RecoveryEmail,
2614
2663
  FailedLoginAttemptCount: user.FailedLoginAttemptCount,
2615
2664
  LastFailedLoginAt: user.LastFailedLoginAt,
@@ -2736,6 +2785,7 @@ export class User extends UserBase {
2736
2785
  LastLoginAt: user.LastLoginAt,
2737
2786
  MFAEnabled: user.MFAEnabled,
2738
2787
  MFAConfig: user.MFAConfig,
2788
+ MFABypassYN: user.MFABypassYN,
2739
2789
  RecoveryEmail: user.RecoveryEmail,
2740
2790
  FailedLoginAttemptCount: user.FailedLoginAttemptCount,
2741
2791
  LastFailedLoginAt: user.LastFailedLoginAt,
@@ -2799,6 +2849,7 @@ export class User extends UserBase {
2799
2849
  LastLoginAt: this.LastLoginAt,
2800
2850
  MFAEnabled: this.MFAEnabled,
2801
2851
  MFAConfig: this.MFAConfig,
2852
+ MFABypassYN: this.MFABypassYN,
2802
2853
  RecoveryEmail: this.RecoveryEmail,
2803
2854
  FailedLoginAttemptCount: this.FailedLoginAttemptCount,
2804
2855
  LastFailedLoginAt: this.LastFailedLoginAt,
@@ -2830,6 +2881,7 @@ export class User extends UserBase {
2830
2881
  LastLoginAt: this.LastLoginAt,
2831
2882
  MFAEnabled: this.MFAEnabled,
2832
2883
  MFAConfig: this.MFAConfig,
2884
+ MFABypassYN: this.MFABypassYN,
2833
2885
  RecoveryEmail: this.RecoveryEmail,
2834
2886
  FailedLoginAttemptCount: this.FailedLoginAttemptCount,
2835
2887
  LastFailedLoginAt: this.LastFailedLoginAt,
@@ -2920,6 +2972,7 @@ export class User extends UserBase {
2920
2972
  LastLoginAt: this.LastLoginAt,
2921
2973
  MFAEnabled: this.MFAEnabled,
2922
2974
  MFAConfig: this.MFAConfig,
2975
+ MFABypassYN: this.MFABypassYN,
2923
2976
  RecoveryEmail: this.RecoveryEmail,
2924
2977
  FailedLoginAttemptCount: this.FailedLoginAttemptCount,
2925
2978
  LastFailedLoginAt: this.LastFailedLoginAt,
@@ -2951,6 +3004,7 @@ export class User extends UserBase {
2951
3004
  LastLoginAt: this.LastLoginAt,
2952
3005
  MFAEnabled: this.MFAEnabled,
2953
3006
  MFAConfig: this.MFAConfig,
3007
+ MFABypassYN: this.MFABypassYN,
2954
3008
  RecoveryEmail: this.RecoveryEmail,
2955
3009
  FailedLoginAttemptCount: this.FailedLoginAttemptCount,
2956
3010
  LastFailedLoginAt: this.LastFailedLoginAt,
@@ -3140,4 +3194,370 @@ export class User extends UserBase {
3140
3194
  throw error;
3141
3195
  }
3142
3196
  }
3197
+
3198
+ public async enable2FABypass(loginUser: LoginUser, dbTransaction: any) {
3199
+ try {
3200
+ // 1. Check if MFABypassYN is already enabled
3201
+ if (this.MFABypassYN === YN.Yes) {
3202
+ throw new ClassError(
3203
+ 'User',
3204
+ 'UserErrMsg0X',
3205
+ 'Bypass already enabled.',
3206
+ 'enable2FABypass',
3207
+ );
3208
+ }
3209
+
3210
+ // 2. Check if user has MANAGE_MFA privilege
3211
+ const systemCode =
3212
+ ApplicationConfig.getComponentConfigValue('system-code');
3213
+ const isPrivileged = await loginUser.checkPrivileges(
3214
+ systemCode,
3215
+ 'MANAGE_MFA',
3216
+ );
3217
+ if (!isPrivileged) {
3218
+ throw new ClassError(
3219
+ 'LoginUser',
3220
+ 'LoginUserErrMsg0X',
3221
+ 'You do not have permission to enable MFA bypass.',
3222
+ );
3223
+ }
3224
+
3225
+ const entityValueBefore: IUserAttr = {
3226
+ UserId: this.UserId,
3227
+ UserName: this.UserName,
3228
+ FullName: this.FullName,
3229
+ IDNo: this.IDNo,
3230
+ IDType: this.IDType,
3231
+ ContactNo: this.ContactNo,
3232
+ Email: this.Email,
3233
+ Password: this.Password,
3234
+ Status: this.Status,
3235
+ DefaultPasswordChangedYN: this.DefaultPasswordChangedYN,
3236
+ FirstLoginAt: this.FirstLoginAt,
3237
+ LastLoginAt: this.LastLoginAt,
3238
+ MFAEnabled: this.MFAEnabled,
3239
+ MFAConfig: this.MFAConfig,
3240
+ MFABypassYN: this.MFABypassYN,
3241
+ RecoveryEmail: this.RecoveryEmail,
3242
+ FailedLoginAttemptCount: this.FailedLoginAttemptCount,
3243
+ LastFailedLoginAt: this.LastFailedLoginAt,
3244
+ LastPasswordChangedAt: this.LastPasswordChangedAt,
3245
+ NeedToChangePasswordYN: this.NeedToChangePasswordYN,
3246
+ CreatedById: this.CreatedById,
3247
+ CreatedAt: this.CreatedAt,
3248
+ UpdatedById: this.UpdatedById,
3249
+ UpdatedAt: this.UpdatedAt,
3250
+ PasscodeHash: this.PasscodeHash,
3251
+ PasscodeUpdatedAt: this.PasscodeUpdatedAt,
3252
+ };
3253
+
3254
+ // 3. Update user record
3255
+ this.MFABypassYN = YN.Yes;
3256
+ this.MFAEnabled = 0;
3257
+ this.UpdatedAt = new Date();
3258
+ this.UpdatedById = loginUser.UserId;
3259
+
3260
+ await User._Repository.update(
3261
+ {
3262
+ MFABypassYN: this.MFABypassYN,
3263
+ MFAEnabled: this.MFAEnabled,
3264
+ UpdatedAt: this.UpdatedAt,
3265
+ UpdatedById: this.UpdatedById,
3266
+ },
3267
+ {
3268
+ where: {
3269
+ UserId: this.UserId,
3270
+ },
3271
+ transaction: dbTransaction,
3272
+ },
3273
+ );
3274
+
3275
+ const entityValueAfter: IUserAttr = {
3276
+ UserId: this.UserId,
3277
+ UserName: this.UserName,
3278
+ FullName: this.FullName,
3279
+ IDNo: this.IDNo,
3280
+ IDType: this.IDType,
3281
+ ContactNo: this.ContactNo,
3282
+ Email: this.Email,
3283
+ Password: this.Password,
3284
+ Status: this.Status,
3285
+ DefaultPasswordChangedYN: this.DefaultPasswordChangedYN,
3286
+ FirstLoginAt: this.FirstLoginAt,
3287
+ LastLoginAt: this.LastLoginAt,
3288
+ MFAEnabled: this.MFAEnabled,
3289
+ MFAConfig: this.MFAConfig,
3290
+ MFABypassYN: this.MFABypassYN,
3291
+ RecoveryEmail: this.RecoveryEmail,
3292
+ FailedLoginAttemptCount: this.FailedLoginAttemptCount,
3293
+ LastFailedLoginAt: this.LastFailedLoginAt,
3294
+ LastPasswordChangedAt: this.LastPasswordChangedAt,
3295
+ NeedToChangePasswordYN: this.NeedToChangePasswordYN,
3296
+ CreatedById: this.CreatedById,
3297
+ CreatedAt: this.CreatedAt,
3298
+ UpdatedById: this.UpdatedById,
3299
+ UpdatedAt: this.UpdatedAt,
3300
+ PasscodeHash: this.PasscodeHash,
3301
+ PasscodeUpdatedAt: this.PasscodeUpdatedAt,
3302
+ };
3303
+
3304
+ // Record update activity using Activity class create method.
3305
+ const activity = new Activity();
3306
+ activity.ActivityId = activity.createId();
3307
+ activity.Action = ActionEnum.UPDATE;
3308
+ activity.Description = `Enable 2FA Bypass For User ${this.Email}`;
3309
+ activity.EntityType = this.ObjectType;
3310
+ activity.EntityId = this.UserId.toString();
3311
+ activity.EntityValueBefore = JSON.stringify(entityValueBefore);
3312
+ activity.EntityValueAfter = JSON.stringify(entityValueAfter);
3313
+
3314
+ await activity.create(loginUser.ObjectId, dbTransaction);
3315
+ } catch (error) {
3316
+ throw error;
3317
+ }
3318
+ }
3319
+
3320
+ public async disable2FABypass(loginUser: LoginUser, dbTransaction: any) {
3321
+ try {
3322
+ // 1. Check if MFABypassYN is already disabled
3323
+ if (this.MFABypassYN === YN.No) {
3324
+ throw new ClassError(
3325
+ 'User',
3326
+ 'UserErrMsg0X',
3327
+ 'Bypass already disabled.',
3328
+ 'disable2FABypass',
3329
+ );
3330
+ }
3331
+
3332
+ // 2. Check if user has MANAGE_MFA privilege
3333
+ const systemCode =
3334
+ ApplicationConfig.getComponentConfigValue('system-code');
3335
+ const isPrivileged = await loginUser.checkPrivileges(
3336
+ systemCode,
3337
+ 'MANAGE_MFA',
3338
+ );
3339
+ if (!isPrivileged) {
3340
+ throw new ClassError(
3341
+ 'LoginUser',
3342
+ 'LoginUserErrMsg0X',
3343
+ 'You do not have permission to enable MFA bypass.',
3344
+ );
3345
+ }
3346
+
3347
+ const entityValueBefore: IUserAttr = {
3348
+ UserId: this.UserId,
3349
+ UserName: this.UserName,
3350
+ FullName: this.FullName,
3351
+ IDNo: this.IDNo,
3352
+ IDType: this.IDType,
3353
+ ContactNo: this.ContactNo,
3354
+ Email: this.Email,
3355
+ Password: this.Password,
3356
+ Status: this.Status,
3357
+ DefaultPasswordChangedYN: this.DefaultPasswordChangedYN,
3358
+ FirstLoginAt: this.FirstLoginAt,
3359
+ LastLoginAt: this.LastLoginAt,
3360
+ MFAEnabled: this.MFAEnabled,
3361
+ MFAConfig: this.MFAConfig,
3362
+ MFABypassYN: this.MFABypassYN,
3363
+ RecoveryEmail: this.RecoveryEmail,
3364
+ FailedLoginAttemptCount: this.FailedLoginAttemptCount,
3365
+ LastFailedLoginAt: this.LastFailedLoginAt,
3366
+ LastPasswordChangedAt: this.LastPasswordChangedAt,
3367
+ NeedToChangePasswordYN: this.NeedToChangePasswordYN,
3368
+ CreatedById: this.CreatedById,
3369
+ CreatedAt: this.CreatedAt,
3370
+ UpdatedById: this.UpdatedById,
3371
+ UpdatedAt: this.UpdatedAt,
3372
+ PasscodeHash: this.PasscodeHash,
3373
+ PasscodeUpdatedAt: this.PasscodeUpdatedAt,
3374
+ };
3375
+
3376
+ // 3. Update user record
3377
+ this.MFABypassYN = YN.No;
3378
+ this.MFAEnabled = 0;
3379
+ this.UpdatedAt = new Date();
3380
+ this.UpdatedById = loginUser.UserId;
3381
+
3382
+ await User._Repository.update(
3383
+ {
3384
+ MFABypassYN: this.MFABypassYN,
3385
+ MFAEnabled: this.MFAEnabled,
3386
+ UpdatedAt: this.UpdatedAt,
3387
+ UpdatedById: this.UpdatedById,
3388
+ },
3389
+ {
3390
+ where: {
3391
+ UserId: this.UserId,
3392
+ },
3393
+ transaction: dbTransaction,
3394
+ },
3395
+ );
3396
+
3397
+ const entityValueAfter: IUserAttr = {
3398
+ UserId: this.UserId,
3399
+ UserName: this.UserName,
3400
+ FullName: this.FullName,
3401
+ IDNo: this.IDNo,
3402
+ IDType: this.IDType,
3403
+ ContactNo: this.ContactNo,
3404
+ Email: this.Email,
3405
+ Password: this.Password,
3406
+ Status: this.Status,
3407
+ DefaultPasswordChangedYN: this.DefaultPasswordChangedYN,
3408
+ FirstLoginAt: this.FirstLoginAt,
3409
+ LastLoginAt: this.LastLoginAt,
3410
+ MFAEnabled: this.MFAEnabled,
3411
+ MFAConfig: this.MFAConfig,
3412
+ MFABypassYN: this.MFABypassYN,
3413
+ RecoveryEmail: this.RecoveryEmail,
3414
+ FailedLoginAttemptCount: this.FailedLoginAttemptCount,
3415
+ LastFailedLoginAt: this.LastFailedLoginAt,
3416
+ LastPasswordChangedAt: this.LastPasswordChangedAt,
3417
+ NeedToChangePasswordYN: this.NeedToChangePasswordYN,
3418
+ CreatedById: this.CreatedById,
3419
+ CreatedAt: this.CreatedAt,
3420
+ UpdatedById: this.UpdatedById,
3421
+ UpdatedAt: this.UpdatedAt,
3422
+ PasscodeHash: this.PasscodeHash,
3423
+ PasscodeUpdatedAt: this.PasscodeUpdatedAt,
3424
+ };
3425
+
3426
+ // Record update activity using Activity class create method.
3427
+ const activity = new Activity();
3428
+ activity.ActivityId = activity.createId();
3429
+ activity.Action = ActionEnum.UPDATE;
3430
+ activity.Description = `Disable 2FA Bypass For User ${this.Email}`;
3431
+ activity.EntityType = this.ObjectType;
3432
+ activity.EntityId = this.UserId.toString();
3433
+ activity.EntityValueBefore = JSON.stringify(entityValueBefore);
3434
+ activity.EntityValueAfter = JSON.stringify(entityValueAfter);
3435
+
3436
+ await activity.create(loginUser.ObjectId, dbTransaction);
3437
+ } catch (error) {
3438
+ throw error;
3439
+ }
3440
+ }
3441
+
3442
+ public async reset2FA(loginUser: LoginUser, dbTransaction: any) {
3443
+ try {
3444
+ // 1. Check if MFABypassYN is already disabled
3445
+ if (this.MFAEnabled === 0) {
3446
+ throw new ClassError(
3447
+ 'User',
3448
+ 'UserErrMsg0X',
3449
+ 'User not yet setup 2FA.',
3450
+ 'reset2FA',
3451
+ );
3452
+ }
3453
+
3454
+ // 2. Check if user has MANAGE_MFA privilege
3455
+ const systemCode =
3456
+ ApplicationConfig.getComponentConfigValue('system-code');
3457
+ const isPrivileged = await loginUser.checkPrivileges(
3458
+ systemCode,
3459
+ 'MANAGE_MFA',
3460
+ );
3461
+ if (!isPrivileged) {
3462
+ throw new ClassError(
3463
+ 'LoginUser',
3464
+ 'LoginUserErrMsg0X',
3465
+ 'You do not have permission to reset 2FA.',
3466
+ );
3467
+ }
3468
+
3469
+ const entityValueBefore: IUserAttr = {
3470
+ UserId: this.UserId,
3471
+ UserName: this.UserName,
3472
+ FullName: this.FullName,
3473
+ IDNo: this.IDNo,
3474
+ IDType: this.IDType,
3475
+ ContactNo: this.ContactNo,
3476
+ Email: this.Email,
3477
+ Password: this.Password,
3478
+ Status: this.Status,
3479
+ DefaultPasswordChangedYN: this.DefaultPasswordChangedYN,
3480
+ FirstLoginAt: this.FirstLoginAt,
3481
+ LastLoginAt: this.LastLoginAt,
3482
+ MFAEnabled: this.MFAEnabled,
3483
+ MFAConfig: this.MFAConfig,
3484
+ MFABypassYN: this.MFABypassYN,
3485
+ RecoveryEmail: this.RecoveryEmail,
3486
+ FailedLoginAttemptCount: this.FailedLoginAttemptCount,
3487
+ LastFailedLoginAt: this.LastFailedLoginAt,
3488
+ LastPasswordChangedAt: this.LastPasswordChangedAt,
3489
+ NeedToChangePasswordYN: this.NeedToChangePasswordYN,
3490
+ CreatedById: this.CreatedById,
3491
+ CreatedAt: this.CreatedAt,
3492
+ UpdatedById: this.UpdatedById,
3493
+ UpdatedAt: this.UpdatedAt,
3494
+ PasscodeHash: this.PasscodeHash,
3495
+ PasscodeUpdatedAt: this.PasscodeUpdatedAt,
3496
+ };
3497
+
3498
+ // 3. Update user record
3499
+ this.MFAEnabled = 0;
3500
+ this.MFABypassYN = YN.No;
3501
+ this.UpdatedAt = new Date();
3502
+ this.UpdatedById = loginUser.UserId;
3503
+
3504
+ await User._Repository.update(
3505
+ {
3506
+ MFAEnabled: this.MFAEnabled,
3507
+ MFABypassYN: this.MFABypassYN,
3508
+ UpdatedAt: this.UpdatedAt,
3509
+ UpdatedById: this.UpdatedById,
3510
+ },
3511
+ {
3512
+ where: {
3513
+ UserId: this.UserId,
3514
+ },
3515
+ transaction: dbTransaction,
3516
+ },
3517
+ );
3518
+
3519
+ const entityValueAfter: IUserAttr = {
3520
+ UserId: this.UserId,
3521
+ UserName: this.UserName,
3522
+ FullName: this.FullName,
3523
+ IDNo: this.IDNo,
3524
+ IDType: this.IDType,
3525
+ ContactNo: this.ContactNo,
3526
+ Email: this.Email,
3527
+ Password: this.Password,
3528
+ Status: this.Status,
3529
+ DefaultPasswordChangedYN: this.DefaultPasswordChangedYN,
3530
+ FirstLoginAt: this.FirstLoginAt,
3531
+ LastLoginAt: this.LastLoginAt,
3532
+ MFAEnabled: this.MFAEnabled,
3533
+ MFAConfig: this.MFAConfig,
3534
+ MFABypassYN: this.MFABypassYN,
3535
+ RecoveryEmail: this.RecoveryEmail,
3536
+ FailedLoginAttemptCount: this.FailedLoginAttemptCount,
3537
+ LastFailedLoginAt: this.LastFailedLoginAt,
3538
+ LastPasswordChangedAt: this.LastPasswordChangedAt,
3539
+ NeedToChangePasswordYN: this.NeedToChangePasswordYN,
3540
+ CreatedById: this.CreatedById,
3541
+ CreatedAt: this.CreatedAt,
3542
+ UpdatedById: this.UpdatedById,
3543
+ UpdatedAt: this.UpdatedAt,
3544
+ PasscodeHash: this.PasscodeHash,
3545
+ PasscodeUpdatedAt: this.PasscodeUpdatedAt,
3546
+ };
3547
+
3548
+ // Record update activity using Activity class create method.
3549
+ const activity = new Activity();
3550
+ activity.ActivityId = activity.createId();
3551
+ activity.Action = ActionEnum.UPDATE;
3552
+ activity.Description = `Reset 2FA for User ${this.Email}`;
3553
+ activity.EntityType = this.ObjectType;
3554
+ activity.EntityId = this.UserId.toString();
3555
+ activity.EntityValueBefore = JSON.stringify(entityValueBefore);
3556
+ activity.EntityValueAfter = JSON.stringify(entityValueAfter);
3557
+
3558
+ await activity.create(loginUser.ObjectId, dbTransaction);
3559
+ } catch (error) {
3560
+ throw error;
3561
+ }
3562
+ }
3143
3563
  }
@@ -249,7 +249,7 @@ export class UserSystemAccess extends ObjectBase {
249
249
  Status: UserStatus.ACTIVE,
250
250
  },
251
251
  as: 'User',
252
- attributes: ['UserId', 'FullName'],
252
+ attributes: ['UserId', 'FullName', 'Email'],
253
253
  },
254
254
  ],
255
255
  };
@@ -0,0 +1,8 @@
1
+ export interface ILoginHistorySearchAttr {
2
+ UserId?: number;
3
+ SystemCode?: string;
4
+ LoginStatus?: string;
5
+ OriginIP?: string;
6
+ DateFrom?: Date;
7
+ DateTo?: Date;
8
+ }
@@ -0,0 +1,11 @@
1
+ export interface ILoginHistoryAttr {
2
+ HistoryId: number;
3
+ UserId: number;
4
+ User?: {
5
+ UserName: string;
6
+ };
7
+ OriginIp: string;
8
+ SystemCode: string;
9
+ LoginStatus: string;
10
+ CreatedAt: Date;
11
+ }
@@ -17,7 +17,7 @@ import { LoginStatusEnum } from '../enum/login-status.enum';
17
17
  createdAt: 'CreatedAt',
18
18
  updatedAt: false,
19
19
  })
20
- export default class LoginHistory extends Model {
20
+ export default class LoginHistoryModel extends Model {
21
21
  @Column({
22
22
  primaryKey: true,
23
23
  allowNull: false,
@@ -38,7 +38,7 @@ export default class LoginHistory extends Model {
38
38
  allowNull: true,
39
39
  type: DataType.STRING(10),
40
40
  })
41
- SystemCode: number;
41
+ SystemCode: string;
42
42
 
43
43
  @Column({
44
44
  allowNull: true,
@@ -88,6 +88,13 @@ export default class User extends Model {
88
88
  })
89
89
  DefaultPasswordChangedYN: YN;
90
90
 
91
+ @Column({
92
+ allowNull: true,
93
+ type: DataType.CHAR(1),
94
+ defaultValue: 'N',
95
+ })
96
+ MFABypassYN: YN;
97
+
91
98
  @Column({
92
99
  type: DataType.DATE,
93
100
  })