@tomei/sso 0.66.1-test.1 → 0.66.1-test.2

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 (25) hide show
  1. package/__tests__/unit/components/group-reporting-user/group-reporting-user.spec.ts +8 -12
  2. package/__tests__/unit/components/login-user/login.spec.ts +75 -44
  3. package/coverage/cobertura-coverage.xml +6647 -47
  4. package/coverage/test-report.xml +153 -25
  5. package/dist/__tests__/unit/components/group-reporting-user/group-reporting-user.spec.js +2 -6
  6. package/dist/__tests__/unit/components/group-reporting-user/group-reporting-user.spec.js.map +1 -1
  7. package/dist/__tests__/unit/components/login-user/login.spec.js +72 -33
  8. package/dist/__tests__/unit/components/login-user/login.spec.js.map +1 -1
  9. package/dist/src/components/login-user/login-user.js +62 -61
  10. package/dist/src/components/login-user/login-user.js.map +1 -1
  11. package/dist/src/components/login-user/user.d.ts +5 -0
  12. package/dist/src/components/login-user/user.js +39 -0
  13. package/dist/src/components/login-user/user.js.map +1 -1
  14. package/dist/src/components/user-password-history/user-password-history.d.ts +1 -0
  15. package/dist/src/components/user-password-history/user-password-history.js +41 -2
  16. package/dist/src/components/user-password-history/user-password-history.js.map +1 -1
  17. package/dist/src/index.d.ts +0 -1
  18. package/dist/src/index.js +0 -1
  19. package/dist/src/index.js.map +1 -1
  20. package/dist/tsconfig.tsbuildinfo +1 -1
  21. package/package.json +1 -2
  22. package/src/components/login-user/login-user.ts +87 -88
  23. package/src/components/login-user/user.ts +68 -0
  24. package/src/components/user-password-history/user-password-history.ts +57 -6
  25. package/src/index.ts +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomei/sso",
3
- "version": "0.66.1-test.1",
3
+ "version": "0.66.1-test.2",
4
4
  "description": "Tomei SSO Package",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -65,7 +65,6 @@
65
65
  "@tomei/general": "^0.21.10",
66
66
  "@tomei/mailer": "^0.6.2",
67
67
  "argon2": "^0.44.0",
68
- "axios": "^1.13.2",
69
68
  "redis": "^5.8.2",
70
69
  "reflect-metadata": "^0.2.2",
71
70
  "sequelize": "^6.37.7",
@@ -12,8 +12,7 @@ import { createHash, randomBytes, randomUUID } from 'crypto';
12
12
  import { UserGroupRepository } from '../user-group/user-group.repository';
13
13
  import GroupSystemAccessModel from '../../models/group-system-access.entity';
14
14
  import SystemModel from '../../models/system.entity';
15
- import { ApplicationConfig, ComponentConfig } from '@tomei/config';
16
- import axios from 'axios';
15
+ import { ApplicationConfig } from '@tomei/config';
17
16
 
18
17
  export class LoginUser extends User implements ILoginUser {
19
18
  session = {
@@ -27,24 +26,20 @@ export class LoginUser extends User implements ILoginUser {
27
26
  ): Promise<LoginUser> {
28
27
  User._RedisService = await RedisService.init();
29
28
  if (userId) {
30
- const loginApiEndpoint = ComponentConfig.getComponentConfigValue(
31
- '@tomei/sso',
32
- 'loginApiEndpoint',
33
- );
34
-
35
- let payload = { UserId: userId };
36
-
37
- const { data } = await axios.post(
38
- `${loginApiEndpoint.baseUrl}${loginApiEndpoint.getProfile}`,
39
- payload,
40
- {
41
- headers: {
42
- 'Content-Type': 'application/json',
43
- 'x-api-key': loginApiEndpoint.apiKey,
44
- },
29
+ if (dbTransaction) {
30
+ User._Repository = new UserRepository();
31
+ }
32
+ const user = await User._Repository.findOne({
33
+ where: {
34
+ UserId: userId,
45
35
  },
46
- );
47
- const user = data.loginUser;
36
+ include: [
37
+ {
38
+ model: Staff,
39
+ },
40
+ ],
41
+ transaction: dbTransaction,
42
+ });
48
43
 
49
44
  if (!user) {
50
45
  throw new Error('Invalid credentials.');
@@ -59,21 +54,21 @@ export class LoginUser extends User implements ILoginUser {
59
54
  IDType: user?.IdType || null,
60
55
  ContactNo: user?.ContactNo || null,
61
56
  Email: user.Email,
62
- Password: null,
57
+ Password: user.Password,
63
58
  Status: user.Status,
64
- DefaultPasswordChangedYN: null,
65
- FirstLoginAt: null,
66
- LastLoginAt: null,
59
+ DefaultPasswordChangedYN: user.DefaultPasswordChangedYN,
60
+ FirstLoginAt: user.FirstLoginAt,
61
+ LastLoginAt: user.LastLoginAt,
67
62
  MFAEnabled: user.MFAEnabled,
68
- MFAConfig: null,
63
+ MFAConfig: user.MFAConfig,
69
64
  MFABypassYN: user.MFABypassYN,
70
- RecoveryEmail: null,
71
- FailedLoginAttemptCount: null,
72
- LastFailedLoginAt: null,
73
- LastPasswordChangedAt: null,
74
- NeedToChangePasswordYN: null,
75
- PasscodeHash: null,
76
- PasscodeUpdatedAt: null,
65
+ RecoveryEmail: user.RecoveryEmail,
66
+ FailedLoginAttemptCount: user.FailedLoginAttemptCount,
67
+ LastFailedLoginAt: user.LastFailedLoginAt,
68
+ LastPasswordChangedAt: user.LastPasswordChangedAt,
69
+ NeedToChangePasswordYN: user.NeedToChangePasswordYN,
70
+ PasscodeHash: user.PasscodeHash,
71
+ PasscodeUpdatedAt: user.PasscodeUpdatedAt,
77
72
  CreatedById: user.CreatedById,
78
73
  CreatedAt: user.CreatedAt,
79
74
  UpdatedById: user.UpdatedById,
@@ -104,28 +99,21 @@ export class LoginUser extends User implements ILoginUser {
104
99
  throw new Error('Session name is not set');
105
100
  }
106
101
 
107
- const loginApiEndpoint = ComponentConfig.getComponentConfigValue(
108
- '@tomei/sso',
109
- 'loginApiEndpoint',
102
+ const userSession = await this._SessionService.retrieveUserSession(
103
+ this.ObjectId,
104
+ sessionName,
110
105
  );
111
- let payload = {
112
- SystemCode: systemCode,
113
- SessionName: sessionName,
114
- PrivilegeName: privilegeName,
115
- UserId: this.ObjectId,
116
- };
117
- const { data } = await axios.post(
118
- `${loginApiEndpoint.baseUrl}${loginApiEndpoint.checkPrivileges}`,
119
- payload,
120
- {
121
- headers: {
122
- 'Content-Type': 'application/json',
123
- 'x-api-key': loginApiEndpoint.apiKey,
124
- },
125
- },
106
+
107
+ const systemLogin = userSession.systemLogins.find(
108
+ (system) => system.code === systemCode,
126
109
  );
127
- const hasPrivilege = data.hasPrivilege;
128
110
 
111
+ if (!systemLogin) {
112
+ return false;
113
+ }
114
+
115
+ const privileges = systemLogin.privileges;
116
+ const hasPrivilege = privileges.includes(privilegeName);
129
117
  return hasPrivilege;
130
118
  } catch (error) {
131
119
  throw error;
@@ -143,27 +131,28 @@ export class LoginUser extends User implements ILoginUser {
143
131
  if (!sessionName) {
144
132
  throw new Error('Session name is not set');
145
133
  }
146
- const loginApiEndpoint = ComponentConfig.getComponentConfigValue(
147
- '@tomei/sso',
148
- 'loginApiEndpoint',
134
+ const userSession = await this._SessionService.retrieveUserSession(
135
+ userId,
136
+ sessionName,
149
137
  );
150
- let payload = {
151
- SystemCode: systemCode,
152
- SessionName: sessionName,
153
- SessionId: sessionId,
154
- UserId: userId,
155
- };
156
- const { data } = await axios.post(
157
- `${loginApiEndpoint.baseUrl}${loginApiEndpoint.checkSession}`,
158
- payload,
159
- {
160
- headers: {
161
- 'Content-Type': 'application/json',
162
- 'x-api-key': loginApiEndpoint.apiKey,
163
- },
164
- },
138
+
139
+ if (userSession.systemLogins.length === 0) {
140
+ throw new Error('Session expired.');
141
+ }
142
+
143
+ const systemLogin = userSession.systemLogins.find(
144
+ (sl) => sl.code === systemCode,
165
145
  );
166
- const systemLogin = data.systemLogin;
146
+
147
+ if (!systemLogin) {
148
+ throw new Error('Session expired.');
149
+ }
150
+
151
+ if (systemLogin.sessionId !== sessionId) {
152
+ throw new Error('Session expired.');
153
+ }
154
+
155
+ await this._SessionService.refreshDuration(userId, sessionName);
167
156
 
168
157
  return systemLogin;
169
158
  } catch (error) {
@@ -339,26 +328,36 @@ export class LoginUser extends User implements ILoginUser {
339
328
  throw new Error('Session name is not set in the configuration');
340
329
  }
341
330
 
342
- const loginApiEndpoint = ComponentConfig.getComponentConfigValue(
343
- '@tomei/sso',
344
- 'loginApiEndpoint',
331
+ const userSession = await this._SessionService.retrieveUserSession(
332
+ this.ObjectId,
333
+ sessionName,
345
334
  );
346
- let payload = {
347
- SystemCode: systemCode,
348
- SessionName: sessionName,
349
- SessionId: sessionId,
350
- UserId: this.ObjectId,
351
- };
352
-
353
- await axios.post(
354
- `${loginApiEndpoint.baseUrl}${loginApiEndpoint.setSession}`,
355
- payload,
356
- {
357
- headers: {
358
- 'Content-Type': 'application/json',
359
- 'x-api-key': loginApiEndpoint.apiKey,
360
- },
361
- },
335
+ const systemLogin = userSession.systemLogins.find(
336
+ (system) => system.code === systemCode,
337
+ );
338
+
339
+ if (systemLogin) {
340
+ const privileges = await this.getPrivileges(systemCode, dbTransaction);
341
+ systemLogin.sessionId = sessionId;
342
+ systemLogin.privileges = privileges;
343
+ userSession.systemLogins.map((system) =>
344
+ system.code === systemCode ? systemLogin : system,
345
+ );
346
+ } else {
347
+ // if not, add new system login into the userSession
348
+ const newLogin = {
349
+ id: systemCode,
350
+ code: systemCode,
351
+ sessionId: sessionId,
352
+ privileges: await this.getPrivileges(systemCode, dbTransaction),
353
+ };
354
+ userSession.systemLogins.push(newLogin);
355
+ }
356
+ // then update userSession inside the redis storage with 1 day duration of time-to-live
357
+ this._SessionService.setUserSession(
358
+ this.ObjectId,
359
+ userSession,
360
+ sessionName,
362
361
  );
363
362
  }
364
363
  }
@@ -2988,6 +2988,7 @@ export class User extends UserBase {
2988
2988
 
2989
2989
  this.Password = passwordHash;
2990
2990
  this.LastPasswordChangedAt = new Date();
2991
+ this.NeedToChangePasswordYN = YN.No;
2991
2992
 
2992
2993
  const entityValueAfter: IUserAttr = {
2993
2994
  UserId: this.UserId,
@@ -3022,6 +3023,7 @@ export class User extends UserBase {
3022
3023
  {
3023
3024
  Password: this.Password,
3024
3025
  LastPasswordChangedAt: this.LastPasswordChangedAt,
3026
+ NeedToChangePasswordYN: this.NeedToChangePasswordYN,
3025
3027
  },
3026
3028
  {
3027
3029
  where: {
@@ -3053,6 +3055,72 @@ export class User extends UserBase {
3053
3055
  }
3054
3056
  }
3055
3057
 
3058
+ // PASSWORD_EXPIRY_MONTHS=0 disables the policy entirely (AC#1) - checked before NeedToChangePasswordYN/history.
3059
+ async getPasswordExpiryStatus(dbTransaction?: any): Promise<{
3060
+ expired: boolean;
3061
+ expiringSoon: boolean;
3062
+ expiresInDays: number | null;
3063
+ }> {
3064
+ try {
3065
+ const expiryMonthsRaw = Number(process.env.PASSWORD_EXPIRY_MONTHS);
3066
+ const expiryMonths = Number.isFinite(expiryMonthsRaw)
3067
+ ? Math.max(0, Math.floor(expiryMonthsRaw))
3068
+ : 0;
3069
+
3070
+ if (expiryMonths === 0) {
3071
+ return { expired: false, expiringSoon: false, expiresInDays: null };
3072
+ }
3073
+
3074
+ // Forces first-login change for temp-password/admin-created accounts (AC#4), regardless of password age.
3075
+ if (this.NeedToChangePasswordYN === YN.Yes) {
3076
+ return { expired: true, expiringSoon: false, expiresInDays: null };
3077
+ }
3078
+
3079
+ const warningDaysRaw = Number(process.env.PASSWORD_EXPIRY_WARNING_DAYS);
3080
+ const warningDays = Number.isFinite(warningDaysRaw)
3081
+ ? Math.max(0, Math.floor(warningDaysRaw))
3082
+ : 14;
3083
+
3084
+ const latestHistory = await UserPasswordHistory.getLatestByUserId(
3085
+ dbTransaction,
3086
+ this.UserId,
3087
+ );
3088
+
3089
+ if (!latestHistory) {
3090
+ // Legacy/pre-existing accounts without a history row.
3091
+ return { expired: true, expiringSoon: false, expiresInDays: null };
3092
+ }
3093
+
3094
+ const createdAt = latestHistory.CreatedAt;
3095
+ const expiryDate = new Date(
3096
+ Date.UTC(
3097
+ createdAt.getUTCFullYear(),
3098
+ createdAt.getUTCMonth() + expiryMonths,
3099
+ createdAt.getUTCDate(),
3100
+ createdAt.getUTCHours(),
3101
+ createdAt.getUTCMinutes(),
3102
+ createdAt.getUTCSeconds(),
3103
+ ),
3104
+ );
3105
+
3106
+ const now = new Date();
3107
+ if (now.getTime() >= expiryDate.getTime()) {
3108
+ return { expired: true, expiringSoon: false, expiresInDays: null };
3109
+ }
3110
+
3111
+ const msPerDay = 24 * 60 * 60 * 1000;
3112
+ const expiringSoon =
3113
+ now.getTime() >= expiryDate.getTime() - warningDays * msPerDay;
3114
+ const expiresInDays = Math.ceil(
3115
+ (expiryDate.getTime() - now.getTime()) / msPerDay,
3116
+ );
3117
+
3118
+ return { expired: false, expiringSoon, expiresInDays };
3119
+ } catch (error) {
3120
+ throw error;
3121
+ }
3122
+ }
3123
+
3056
3124
  async verifyPasscode(passcode: string) {
3057
3125
  try {
3058
3126
  // Return error if this._Userid is empty "User is not created yet."
@@ -76,16 +76,30 @@ export class UserPasswordHistory
76
76
  ): Promise<void> {
77
77
  // This method used to check if password entered is valid by checking previous password history
78
78
  try {
79
- // Part 1-2: Retrieve password history policy by using component config, call ComponentConfig. by passing:
79
+ // Part 1-2: Retrieve password history policy: env var takes precedence over component config, call ComponentConfig. by passing:
80
80
  // - ComponentName: "@tomei/sso"
81
81
  // - ConfigKey: "passwordHistory"
82
82
  // If no password history set, use default value 3
83
83
 
84
- const passwordHistoryPolicy =
84
+ const envPasswordHistoryPolicy = Number(
85
+ process.env.PASSWORD_HISTORY_LIMIT,
86
+ );
87
+ const componentPasswordHistoryPolicy = Number(
85
88
  ComponentConfig.getComponentConfigValue(
86
89
  '@tomei/sso',
87
90
  'passwordHistory',
88
- ) || 3;
91
+ ),
92
+ );
93
+ const passwordHistoryPolicy =
94
+ (Number.isFinite(envPasswordHistoryPolicy) &&
95
+ envPasswordHistoryPolicy > 0
96
+ ? Math.floor(envPasswordHistoryPolicy)
97
+ : 0) ||
98
+ (Number.isFinite(componentPasswordHistoryPolicy) &&
99
+ componentPasswordHistoryPolicy > 0
100
+ ? Math.floor(componentPasswordHistoryPolicy)
101
+ : 0) ||
102
+ 3;
89
103
 
90
104
  // Part 3-4: Retrieve records from the table by using class._repo findAll() by passing:
91
105
  // where: { UserId: params.UserId }
@@ -137,16 +151,30 @@ export class UserPasswordHistory
137
151
  ): Promise<void> {
138
152
  // This method used to check if password entered is valid by checking previous password history
139
153
  try {
140
- // Part 1-2: Retrieve password history policy by using component config, call ComponentConfig. by passing:
154
+ // Part 1-2: Retrieve password history policy: env var takes precedence over component config, call ComponentConfig. by passing:
141
155
  // - ComponentName: "@tomei/sso"
142
156
  // - ConfigKey: "passwordHistory"
143
157
  // If no password history set, use default value 3
144
158
 
145
- const passwordHistoryPolicy =
159
+ const envPasswordHistoryPolicy = Number(
160
+ process.env.PASSWORD_HISTORY_LIMIT,
161
+ );
162
+ const componentPasswordHistoryPolicy = Number(
146
163
  ComponentConfig.getComponentConfigValue(
147
164
  '@tomei/sso',
148
165
  'passwordHistory',
149
- ) || 3;
166
+ ),
167
+ );
168
+ const passwordHistoryPolicy =
169
+ (Number.isFinite(envPasswordHistoryPolicy) &&
170
+ envPasswordHistoryPolicy > 0
171
+ ? Math.floor(envPasswordHistoryPolicy)
172
+ : 0) ||
173
+ (Number.isFinite(componentPasswordHistoryPolicy) &&
174
+ componentPasswordHistoryPolicy > 0
175
+ ? Math.floor(componentPasswordHistoryPolicy)
176
+ : 0) ||
177
+ 3;
150
178
 
151
179
  // Part 3: Insert new password history by calling class _repo create() method.
152
180
  const userPasswordHistory = new UserPasswordHistory();
@@ -184,4 +212,27 @@ export class UserPasswordHistory
184
212
  throw error;
185
213
  }
186
214
  }
215
+
216
+ // Latest history row's CreatedAt is the source of truth for password age (used by User.getPasswordExpiryStatus()).
217
+ public static async getLatestByUserId(
218
+ dbTransaction: any,
219
+ UserId: number,
220
+ ): Promise<UserPasswordHistory | null> {
221
+ try {
222
+ const passwordHistory = await UserPasswordHistory._Repo.findAll({
223
+ where: { UserId: UserId },
224
+ order: [['CreatedAt', 'DESC']],
225
+ limit: 1,
226
+ transaction: dbTransaction,
227
+ });
228
+
229
+ if (!passwordHistory || passwordHistory.length < 1) {
230
+ return null;
231
+ }
232
+
233
+ return new UserPasswordHistory(passwordHistory[0].get({ plain: true }));
234
+ } catch (error) {
235
+ throw error;
236
+ }
237
+ }
187
238
  }
package/src/index.ts CHANGED
@@ -6,4 +6,3 @@ export * from './session';
6
6
  export * from './enum';
7
7
  export * as ssoDb from './database';
8
8
  export * from './types';
9
- export * from './models/user.entity';