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