UNPKG

@tomei/sso

Version:
1,607 lines (1,442 loc) 111 kB
import { ClassError, LoginUserBase, UserBase } from '@tomei/general'; import { ISessionService } from '../../session/interfaces/session-service.interface'; import { IUserAttr, IUserInfo } from './interfaces/user-info.interface'; import { UserRepository } from './user.repository'; import { SystemRepository } from '../system/system.repository'; import { LoginHistoryRepository } from '../login-history/login-history.repository'; import { PasswordHashService } from '../password-hash/password-hash.service'; import { UserGroupRepository } from '../user-group/user-group.repository'; import Staff from '../../models/staff.entity'; import SystemPrivilege from '../../models/system-privilege.entity'; import LoginHistory from '../../models/login-history.entity'; import { YN } from '../../enum/yn.enum'; import { UserStatus } from '../../enum'; import { ApplicationConfig, ComponentConfig } from '@tomei/config'; import { ICheckUserInfoDuplicatedQuery } from './interfaces/check-user-info-duplicated.interface'; import { Op, where } from 'sequelize'; import { ActionEnum, Activity } from '@tomei/activity-history'; import GroupModel from '../../models/group.entity'; import { GroupSystemAccessRepository } from '../group-system-access/group-system-access.repository'; import { GroupRepository } from '../group/group.repository'; import SystemModel from '../../models/system.entity'; import { ISystemAccess } from './interfaces/system-access.interface'; import { UserSystemAccessRepository } from '../user-system-access/user-system-access.repository'; import GroupSystemAccessModel from '../../models/group-system-access.entity'; import { UserPrivilegeRepository } from '../user-privilege/user-privilege.repository'; import { UserObjectPrivilegeRepository } from '../user-object-privilege/user-object-privilege.repository'; import GroupPrivilegeModel from '../../models/group-privilege.entity'; import { GroupObjectPrivilegeRepository } from '../group-object-privilege/group-object-privilege.repository'; import * as speakeasy from 'speakeasy'; import { LoginStatusEnum } from '../../enum/login-status.enum'; import { RedisService } from '../../redis-client/redis.service'; import { LoginUser } from './login-user'; import { SessionService } from '../../session/session.service'; import { createHash, randomBytes, randomUUID } from 'crypto'; import { AuthContext } from 'types'; import UserModel from '../../models/user.entity'; import { UserReportingHierarchyRepository } from '../user-reporting-hierarchy/user-reporting-hierarchy.repository'; import { IUserReportingHierarchyAttr } from 'interfaces/user-reporting-hierarchy.interface'; import { UserPasswordHistory } from '../user-password-history/user-password-history'; export class User extends UserBase { ObjectId: string; Email: string; private _UserName: string; private _Password: string; private _Status: UserStatus; private _DefaultPasswordChangedYN: YN; private _FirstLoginAt: Date; private _LastLoginAt: Date; private _MFAEnabled: number; private _MFAConfig: string; private _MFABypassYN: string; private _RecoveryEmail: string; private _FailedLoginAttemptCount: number; private _LastFailedLoginAt: Date; private _LastPasswordChangedAt: Date; private _NeedToChangePasswordYN: YN; private _PasscodeHash: string; private _PasscodeUpdatedAt: Date; private _CreatedById: number; private _CreatedAt: Date; private _UpdatedById: number; private _UpdatedAt: Date; ObjectName = 'User'; TableName = 'sso_Users'; ObjectType = 'User'; staffs: any; private _OriginIP: string; protected _SessionService: ISessionService; protected static _RedisService: RedisService; protected static _Repository = new UserRepository(); private static _LoginHistoryRepository = new LoginHistoryRepository(); protected static _UserGroupRepo = new UserGroupRepository(); private static _UserPrivilegeRepo = new UserPrivilegeRepository(); private static _UserObjectPrivilegeRepo = new UserObjectPrivilegeRepository(); private static _GroupObjectPrivilegeRepo = new GroupObjectPrivilegeRepository(); protected static _SystemRepository = new SystemRepository(); protected static _UserSystemAccessRepo = new UserSystemAccessRepository(); private static _GroupSystemAccessRepo = new GroupSystemAccessRepository(); private static _GroupRepo = new GroupRepository(); protected static _UserReportingHierarchyRepo = new UserReportingHierarchyRepository(); private _dbTransaction: any; get SessionService(): ISessionService { return this._SessionService; } get UserId(): number { return parseInt(this.ObjectId); } private set UserId(value: number) { this.ObjectId = value.toString(); } get Password(): string { return this._Password; } private set Password(value: string) { this._Password = value; } get Status(): UserStatus { return this._Status; } private set Status(value: UserStatus) { this._Status = value; } get UserName(): string { return this._UserName; } set UserName(value: string) { this._UserName = value; } get DefaultPasswordChangedYN(): YN { return this._DefaultPasswordChangedYN; } private set DefaultPasswordChangedYN(value: YN) { this._DefaultPasswordChangedYN = value; } get FirstLoginAt(): Date { return this._FirstLoginAt; } private set FirstLoginAt(value: Date) { this._FirstLoginAt = value; } get LastLoginAt(): Date { return this._LastLoginAt; } private set LastLoginAt(value: Date) { this._LastLoginAt = value; } get MFAEnabled(): number { return this._MFAEnabled; } private set MFAEnabled(value: number) { this._MFAEnabled = value; } get MFAConfig(): string { return this._MFAConfig; } private set MFAConfig(value: string) { this._MFAConfig = value; } get MFABypassYN(): string { return this._MFABypassYN; } private set MFABypassYN(value: string) { this._MFABypassYN = value; } get RecoveryEmail(): string { return this._RecoveryEmail; } private set RecoveryEmail(value: string) { this._RecoveryEmail = value; } get FailedLoginAttemptCount(): number { return this._FailedLoginAttemptCount; } private set FailedLoginAttemptCount(value: number) { this._FailedLoginAttemptCount = value; } get LastFailedLoginAt(): Date { return this._LastFailedLoginAt; } private set LastFailedLoginAt(value: Date) { this._LastFailedLoginAt = value; } get LastPasswordChangedAt(): Date { return this._LastPasswordChangedAt; } private set LastPasswordChangedAt(value: Date) { this._LastPasswordChangedAt = value; } get NeedToChangePasswordYN(): YN { return this._NeedToChangePasswordYN; } private set NeedToChangePasswordYN(value: YN) { this._NeedToChangePasswordYN = value; } get CreatedById(): number { return this._CreatedById; } private set CreatedById(value: number) { this._CreatedById = value; } get CreatedAt(): Date { return this._CreatedAt; } private set CreatedAt(value: Date) { this._CreatedAt = value; } get UpdatedById(): number { return this._UpdatedById; } private set UpdatedById(value: number) { this._UpdatedById = value; } get UpdatedAt(): Date { return this._UpdatedAt; } private set UpdatedAt(value: Date) { this._UpdatedAt = value; } get PasscodeHash(): string { return this._PasscodeHash; } private set PasscodeHash(value: string) { this._PasscodeHash = value; } get PasscodeUpdatedAt(): Date { return this._PasscodeUpdatedAt; } private set PasscodeUpdatedAt(value: Date) { this._PasscodeUpdatedAt = value; } async getDetails(): Promise<{ FullName: string; UserName: string; IDNo: string; IDType: string; Email: string; ContactNo: string; }> { return { FullName: this.FullName, UserName: this.UserName, IDNo: this.IDNo, IDType: this.IDType, Email: this.Email, ContactNo: this.ContactNo, }; } constructor( sessionService: ISessionService, dbTransaction?: any, userInfo?: IUserAttr, ) { super(); this._SessionService = sessionService; if (dbTransaction) { this._dbTransaction = dbTransaction; } // set all the class properties if (userInfo) { this.UserId = userInfo.UserId; this.UserName = userInfo.UserName; this.FullName = userInfo.FullName; this.IDNo = userInfo.IDNo; this.IDType = userInfo.IDType; this.Email = userInfo.Email; this.ContactNo = userInfo.ContactNo; this.Password = userInfo.Password; this.staffs = userInfo.staffs; this.Status = userInfo.Status; this.DefaultPasswordChangedYN = userInfo.DefaultPasswordChangedYN; this.FirstLoginAt = userInfo.FirstLoginAt; this.LastLoginAt = userInfo.LastLoginAt; this.MFAEnabled = userInfo.MFAEnabled; this.MFAConfig = userInfo.MFAConfig; this.MFABypassYN = userInfo.MFABypassYN; this.RecoveryEmail = userInfo.RecoveryEmail; this.FailedLoginAttemptCount = userInfo.FailedLoginAttemptCount; this.LastFailedLoginAt = userInfo.LastFailedLoginAt; this.LastPasswordChangedAt = userInfo.LastPasswordChangedAt; this.NeedToChangePasswordYN = userInfo.NeedToChangePasswordYN; this.PasscodeHash = userInfo.PasscodeHash; this.PasscodeUpdatedAt = userInfo.PasscodeUpdatedAt; this.CreatedById = userInfo.CreatedById; this.CreatedAt = userInfo.CreatedAt; this.UpdatedById = userInfo.UpdatedById; this.UpdatedAt = userInfo.UpdatedAt; } } static async init( sessionService: ISessionService, userId?: number, dbTransaction = null, ): Promise<User> { User._RedisService = await RedisService.init(); if (userId) { if (dbTransaction) { User._Repository = new UserRepository(); } const user = await User._Repository.findOne({ where: { UserId: userId, }, include: [ { model: Staff, }, ], transaction: dbTransaction, }); if (!user) { throw new Error('Invalid credentials.'); } if (user) { const userAttr: IUserAttr = { UserId: user.UserId, UserName: user.UserName, FullName: user?.FullName || null, IDNo: user?.IdNo || null, IDType: user?.IdType || null, ContactNo: user?.ContactNo || null, Email: user.Email, Password: user.Password, Status: user.Status, DefaultPasswordChangedYN: user.DefaultPasswordChangedYN, FirstLoginAt: user.FirstLoginAt, LastLoginAt: user.LastLoginAt, MFAEnabled: user.MFAEnabled, MFAConfig: user.MFAConfig, MFABypassYN: user.MFABypassYN, RecoveryEmail: user.RecoveryEmail, FailedLoginAttemptCount: user.FailedLoginAttemptCount, LastFailedLoginAt: user.LastFailedLoginAt, LastPasswordChangedAt: user.LastPasswordChangedAt, NeedToChangePasswordYN: user.NeedToChangePasswordYN, PasscodeHash: user.PasscodeHash, PasscodeUpdatedAt: user.PasscodeUpdatedAt, CreatedById: user.CreatedById, CreatedAt: user.CreatedAt, UpdatedById: user.UpdatedById, UpdatedAt: user.UpdatedAt, staffs: user?.Staff, }; return new User(sessionService, dbTransaction, userAttr); } else { throw new Error('User not found'); } } return new User(sessionService, dbTransaction); } static async initUsingEmail( sessionService: ISessionService, email: string, dbTransaction = null, ): Promise<User> { User._RedisService = await RedisService.init(); if (email) { if (dbTransaction) { User._Repository = new UserRepository(); } const user = await User._Repository.findOne({ where: { Email: email, }, include: [ { model: Staff, }, ], transaction: dbTransaction, }); if (!user) { throw new Error('Invalid email.'); } if (user) { const userAttr: IUserAttr = { UserId: user.UserId, UserName: user.UserName, FullName: user?.FullName || null, IDNo: user?.IdNo || null, IDType: user?.IdType || null, ContactNo: user?.ContactNo || null, Email: user.Email, Password: user.Password, Status: user.Status, DefaultPasswordChangedYN: user.DefaultPasswordChangedYN, FirstLoginAt: user.FirstLoginAt, LastLoginAt: user.LastLoginAt, MFAEnabled: user.MFAEnabled, MFAConfig: user.MFAConfig, MFABypassYN: user.MFABypassYN, RecoveryEmail: user.RecoveryEmail, FailedLoginAttemptCount: user.FailedLoginAttemptCount, LastFailedLoginAt: user.LastFailedLoginAt, LastPasswordChangedAt: user.LastPasswordChangedAt, NeedToChangePasswordYN: user.NeedToChangePasswordYN, PasscodeHash: user.PasscodeHash, PasscodeUpdatedAt: user.PasscodeUpdatedAt, CreatedById: user.CreatedById, CreatedAt: user.CreatedAt, UpdatedById: user.UpdatedById, UpdatedAt: user.UpdatedAt, staffs: user?.Staff, }; return new User(sessionService, dbTransaction, userAttr); } else { throw new Error('User not found'); } } } async setEmail(email: string, dbTransaction): Promise<void> { try { //Check if email is not the same as the current email if it is, skip all the steps if (this.Email === email) { return; } //Check if email is duplicated, if yes, throw error const user = await User._Repository.findOne({ where: { Email: email, }, transaction: dbTransaction, }); if (user) { throw new ClassError( 'LoginUser', 'LoginUserErrMsg0X', 'Email already exists', ); } //Update the email this.Email = email; } catch (error) { throw error; } } async login( systemCode: string, email: string, password: string, ipAddress: string, dbTransaction, ): Promise<LoginUser> { try { //validate email if (!this.ObjectId) { // 1.1: Retrieve user data by calling _Repo.findOne with below parameter const user = await User._Repository.findOne({ transaction: dbTransaction, where: { Email: email, Status: { [Op.or]: [UserStatus.ACTIVE, UserStatus.LOCKED], }, }, include: [ { model: Staff, }, ], }); // 1.2: If Exist populate all of the object attributes with the user data retrieved from previous step. if not throw Class Error if (user) { const userAttr: IUserAttr = { UserId: user.UserId, UserName: user.UserName, FullName: user?.FullName || null, IDNo: user?.IdNo || null, IDType: user?.IdType || null, ContactNo: user?.ContactNo || null, Email: user.Email, Password: user.Password, Status: user.Status, DefaultPasswordChangedYN: user.DefaultPasswordChangedYN, FirstLoginAt: user.FirstLoginAt, LastLoginAt: user.LastLoginAt, MFAEnabled: user.MFAEnabled, MFAConfig: user.MFAConfig, MFABypassYN: user.MFABypassYN, RecoveryEmail: user.RecoveryEmail, FailedLoginAttemptCount: user.FailedLoginAttemptCount, LastFailedLoginAt: user.LastFailedLoginAt, LastPasswordChangedAt: user.LastPasswordChangedAt, NeedToChangePasswordYN: user.NeedToChangePasswordYN, PasscodeHash: user.PasscodeHash, PasscodeUpdatedAt: user.PasscodeUpdatedAt, CreatedById: user.CreatedById, CreatedAt: user.CreatedAt, UpdatedById: user.UpdatedById, UpdatedAt: user.UpdatedAt, staffs: user?.Staff || null, }; this.UserId = userAttr.UserId; this.FullName = userAttr.FullName; this.IDNo = userAttr.IDNo; this.Email = userAttr.Email; this.ContactNo = userAttr.ContactNo; this.Password = userAttr.Password; this.Status = userAttr.Status; this.DefaultPasswordChangedYN = userAttr.DefaultPasswordChangedYN; this.FirstLoginAt = userAttr.FirstLoginAt; this.LastLoginAt = userAttr.LastLoginAt; this.MFAEnabled = userAttr.MFAEnabled; this.MFAConfig = userAttr.MFAConfig; this.RecoveryEmail = userAttr.RecoveryEmail; this.FailedLoginAttemptCount = userAttr.FailedLoginAttemptCount; this.LastFailedLoginAt = userAttr.LastFailedLoginAt; this.LastPasswordChangedAt = userAttr.LastPasswordChangedAt; this.NeedToChangePasswordYN = userAttr.NeedToChangePasswordYN; this.CreatedById = userAttr.CreatedById; this.CreatedAt = userAttr.CreatedAt; this.UpdatedById = userAttr.UpdatedById; this.UpdatedAt = userAttr.UpdatedAt; this.staffs = userAttr.staffs; } else { console.error('User not found for email:', email); throw new ClassError('User', 'UserErrMsg0X', 'Invalid Credentials'); } } if (this.ObjectId && this.Email !== email) { console.error('Email mismatch:', this.Email, email); throw new Error('Invalid credentials.'); } //Call LoginUser.check2FA const check2FA = await User.check2FA(this, dbTransaction); //validate system code // 1.3: From here on until step 1.8. If any of the validation is failed, skip the other step and call incrementFailedLoginAttemptCount try { // 1.4: Validate the system user trying to access by calling __SystemRepository.findOne with below parameter. // If system does not exist, skip all below step and return to step 3 const system = await User._SystemRepository.findOne({ where: { SystemCode: systemCode, Status: 'Active', }, }); if (!system) { throw new Error('Access denied: invalid or unauthorized system.'); } // 1.5: Instantiate new PasswordHashService object and call PasswordHashService.verify method to check whether the param.Password is correct. // If not, skip all below step and return to step 3. const passwordHashService = new PasswordHashService(); const isPasswordValid = await passwordHashService.verify( password, this.Password, ); if (!isPasswordValid) { console.error('Invalid password for user:', this.UserId); throw new Error('Invalid credentials.'); } // 1.6: Validate the user access to the system by calling . if it return false, skip all below step and return to step 3 await this.checkSystemAccess( this.UserId, system.SystemCode, dbTransaction, ); // 1.7: f this.Status = "Locked" , call shouldReleaseLock if (this.Status === UserStatus.LOCKED) { const isReleaseLock = User.shouldReleaseLock(this.LastFailedLoginAt); // 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 if (isReleaseLock) { await User.releaseLock(this.UserId, dbTransaction); this.Status = UserStatus.ACTIVE; } else { throw new Error( 'Your account has been locked. Please contact the administrator for assistance.', ); } } } catch (error) { await this.incrementFailedLoginAttemptCount(dbTransaction); throw error; } // 2.1: Call alertNewLogin to check whether the ip used is new ip and alert the user if it's new. const system = await User._SystemRepository.findOne({ where: { SystemCode: systemCode, }, }); await this.alertNewLogin(this.ObjectId, system.SystemCode, ipAddress); // 2.2 : Set below properties : // FailedLoginAttemptCount: 0. // If FirstLoginAt is empty, this.FirstLoginAt = <current timestamp> // LastLoginAt = <current timestamp> this.FailedLoginAttemptCount = 0; this.LastLoginAt = new Date(); if (!this.FirstLoginAt) { this.FirstLoginAt = new Date(); } // 2.3: Call _Repo.update and update user data in the db to the current object properties value. Dont forget to use dbTransaction. await User._Repository.update( { FullName: this.FullName, UserName: this.UserName, IDNo: this.IDNo, Email: this.Email, ContactNo: this.ContactNo, Password: this.Password, Status: this.Status, DefaultPasswordChangedYN: this.DefaultPasswordChangedYN, FirstLoginAt: this.FirstLoginAt, LastLoginAt: this.LastLoginAt, MFAEnabled: this.MFAEnabled, MFAConfig: this.MFAConfig, RecoveryEmail: this.RecoveryEmail, FailedLoginAttemptCount: this.FailedLoginAttemptCount, LastFailedLoginAt: this.LastFailedLoginAt, LastPasswordChangedAt: this.LastPasswordChangedAt, NeedToChangePasswordYN: this.NeedToChangePasswordYN, }, { where: { UserId: this.UserId, }, transaction: dbTransaction, }, ); // fetch user session if exists const sessionName = ApplicationConfig.getComponentConfigValue('sessionName'); if (!sessionName) { console.error('Session name is not set in the configuration'); throw new Error('Session name is not set in the configuration'); } const userSession = await this._SessionService.retrieveUserSession( this.ObjectId, sessionName, ); const systemLogin = userSession.systemLogins.find( (system) => system.code === systemCode, ); // generate new session id const sessionId: string = randomUUID(); if (systemLogin) { const privileges = await this.getPrivileges( system.SystemCode, dbTransaction, ); systemLogin.sessionId = sessionId; systemLogin.privileges = privileges; userSession.systemLogins.map((system) => system.code === systemCode ? systemLogin : system, ); } else { // if not, add new system login into the userSession const newLogin = { id: system.SystemCode, code: system.SystemCode, sessionId: sessionId, privileges: await this.getPrivileges( system.SystemCode, dbTransaction, ), }; userSession.systemLogins.push(newLogin); } // then update userSession inside the redis storage with 1 day duration of time-to-live this._SessionService.setUserSession( this.ObjectId, userSession, sessionName, ); // record new login history await User._LoginHistoryRepository.create( { UserId: this.UserId, SystemCode: system.SystemCode, OriginIp: ipAddress, CreatedAt: new Date(), LoginStatus: LoginStatusEnum.SUCCESS, }, { transaction: dbTransaction, }, ); // Retrieve is2FAEnabledYN from sso-config with ComponentConfig. const is2FAEnabledYN = ComponentConfig.getComponentConfigValue( '@tomei/sso', 'is2FAEnabledYN', ); const loginUser = await LoginUser.init( this.SessionService, this.UserId, dbTransaction, ); if (is2FAEnabledYN === 'Y') { loginUser.session.Id = `${this.UserId}:`; } else { loginUser.session.Id = `${this.UserId}:${sessionId}`; } return loginUser; } catch (error) { if (this.ObjectId) { await User._LoginHistoryRepository.create( { UserId: this.UserId, SystemCode: systemCode, OriginIp: ipAddress, LoginStatus: LoginStatusEnum.FAILURE, CreatedAt: new Date(), }, { transaction: dbTransaction, }, ); } console.error('Login failed:', error); throw error; } } protected async checkSystemAccess( userId: number, systemCode: string, dbTransaction?: any, ): Promise<void> { try { let isUserHaveAccess = false; const systemAccess = await User._UserSystemAccessRepo.findOne({ where: { UserId: userId, SystemCode: systemCode, Status: 'Active', }, dbTransaction, }); if (systemAccess) { isUserHaveAccess = true; } else { const userGroups = await User._UserGroupRepo.findAll({ where: { UserId: userId, InheritGroupSystemAccessYN: 'Y', Status: 'Active', }, include: [ { model: GroupModel, }, ], dbTransaction, }); outer: for (const usergroup of userGroups) { const group = usergroup.Group; const groupSystemAccess = await User.getInheritedSystemAccess( dbTransaction, group, ); for (const system of groupSystemAccess) { if (system.SystemCode === systemCode) { isUserHaveAccess = true; break outer; } } } } if (!isUserHaveAccess) { throw new Error("User don't have access to the system."); } } catch (error) { console.error('Error checking system access:', error); throw error; } } async checkPrivileges( systemCode: string, privilegeName: string, ): Promise<boolean> { try { if (!this.ObjectId) { throw new Error('ObjectId(UserId) is not set'); } const sessionName = ApplicationConfig.getComponentConfigValue('sessionName'); if (!sessionName) { throw new Error('Session name is not set in the configuration'); } const userSession = await this._SessionService.retrieveUserSession( this.ObjectId, sessionName, ); const systemLogin = userSession.systemLogins.find( (system) => system.code === systemCode, ); if (!systemLogin) { return false; } const privileges = systemLogin.privileges; const hasPrivilege = privileges.includes(privilegeName); return hasPrivilege; } catch (error) { throw error; } } private async alertNewLogin( userId: string, systemCode: string, ipAddress: string, ) { try { const userLogins = await User._LoginHistoryRepository.findAll({ where: { UserId: userId, SystemCode: systemCode, }, }); const gotPreviousLogins = userLogins?.length !== 0; let ipFound: LoginHistory | undefined = undefined; if (gotPreviousLogins) { ipFound = userLogins.find((item) => item.OriginIp === ipAddress); } // if (gotPreviousLogins && !ipFound) { // const EMAIL_SENDER = // process.env.EMAIL_SENDER || 'itd-system@tomei.com.my'; // const transporter = new SMTPMailer(); // await transporter.sendMail({ // from: EMAIL_SENDER, // to: this.Email, // subject: 'New Login Alert', // html: `<p>Dear ${this.FullName},</p> // <p>There was a new login to your account from ${ipAddress} on ${new Date().toLocaleString()}.</p> // <p>If this was you, you can safely ignore this email.</p> // <p>If you suspect that someone else is trying to access your account, please contact us immediately at itd-support@tomei.com.my.</p> // <p>Thank you!,</p> // <p> // Best Regards, // IT Department // </p>`, // }); // } } catch (error) { throw error; } } public async getPrivileges( systemCode: string, dbTransaction?: any, ): Promise<string[]> { try { const system = await User._SystemRepository.findOne({ where: { SystemCode: systemCode, }, transaction: dbTransaction, }); if (!system) { throw new Error('Invalid system code.'); } /** * Ways user can get privileges: * 1. Privileges directly assigned to the user using UserPrivilege * 2. User have object that have privileges * 3. User have group that can inherit privileges * 3. User have group that have parent group that can inherit privileges to said group */ //Retrive privileges directly assigned to the user const userPrivileges = await this.getUserPersonalPrivileges( systemCode, dbTransaction, ); //Retrieve privileges from object that user have const objectPrivileges = await this.getObjectPrivileges( systemCode, dbTransaction, ); //Retrieve privileges from group that able to inherit privileges to user //Retrieve all user groups own by user that can inherit privileges for the system const userGroupOwnByUser = await User._UserGroupRepo.findAll({ where: { UserId: this.UserId, InheritGroupSystemAccessYN: 'Y', InheritGroupPrivilegeYN: 'Y', Status: 'Active', }, include: [ { model: GroupModel, where: { Status: 'Active', }, include: [ { model: GroupSystemAccessModel, where: { SystemCode: systemCode, }, }, ], }, ], transaction: dbTransaction, }); //Get all privileges from groups data let groupsPrivileges: string[] = []; for (const userGroup of userGroupOwnByUser) { const gp: string[] = await this.getInheritedPrivileges( userGroup.GroupCode, systemCode, dbTransaction, ); groupsPrivileges = [...groupsPrivileges, ...gp]; } //Map all privileges to a single array const privileges: string[] = [ ...userPrivileges, ...objectPrivileges, ...groupsPrivileges, ]; return privileges; } catch (error) { throw error; } } private async getInheritedPrivileges( groupCode: string, systemCode: string, dbTransaction?: string, ): Promise<string[]> { try { // Retrieve Group from the database based on groupCode const group = await User._GroupRepo.findOne({ where: { GroupCode: groupCode, Status: 'Active', }, include: [ { model: GroupPrivilegeModel, where: { Status: 'Active', }, include: [ { model: SystemPrivilege, where: { SystemCode: systemCode, Status: 'Active', }, }, ], }, ], transaction: dbTransaction, }); // retrieve group ObjectPrivileges const objectPrivileges = await User._GroupObjectPrivilegeRepo.findAll({ where: { GroupCode: groupCode, }, include: { model: SystemPrivilege, where: { SystemCode: systemCode, Status: 'Active', }, }, transaction: dbTransaction, }); const gp = group?.GroupPrivileges || []; const op = objectPrivileges || []; let privileges: string[] = []; // Add privileges from the group to the privileges array const groupPrivileges: string[] = []; for (const groupPrivilege of gp) { groupPrivileges.push(groupPrivilege.Privilege.PrivilegeCode); } const ops: string[] = []; for (const objectPrivilege of op) { ops.push(objectPrivilege.Privilege.PrivilegeCode); } privileges = [...privileges, ...groupPrivileges, ...ops]; // Recursive call if not root and allow inherit privileges from parent group if (group?.ParentGroupCode && group?.InheritParentPrivilegeYN === 'Y') { const parentGroupPrivileges = await this.getInheritedPrivileges( group.ParentGroupCode, systemCode, dbTransaction, ); privileges = [...privileges, ...parentGroupPrivileges]; } return privileges; } catch (error) { throw error; } } private async getUserPersonalPrivileges( systemCode: string, dbTransaction?: any, ): Promise<string[]> { try { const userPrivileges = (await User._UserPrivilegeRepo.findAll({ where: { UserId: this.UserId, Status: 'Active', }, include: { model: SystemPrivilege, where: { SystemCode: systemCode, Status: 'Active', }, }, transaction: dbTransaction, })) || []; const privileges: string[] = userPrivileges.map( (u) => u.Privilege.PrivilegeCode, ); return privileges; } catch (error) { throw error; } } private async getObjectPrivileges( systemCode: string, dbTransaction?: any, ): Promise<string[]> { try { const userObjectPrivileges = (await User._UserObjectPrivilegeRepo.findAll({ where: { UserId: this.UserId, }, include: { model: SystemPrivilege, where: { SystemCode: systemCode, Status: 'Active', }, }, transaction: dbTransaction, })) || []; const privilegesCodes: string[] = userObjectPrivileges.map( (u) => u.Privilege.PrivilegeCode, ); return privilegesCodes; } catch (error) { throw error; } } private static async checkUserInfoDuplicated( dbTransaction: any, query: ICheckUserInfoDuplicatedQuery, ) { //This method if check if duplicate user info found. try { //Part 1: Prepare Query Params //Params is all optional but at least one is required. const { Email, UserName, IdType, IdNo, ContactNo } = query; //Prepare the Params to be used as OR operation in SQL query. const where = { [Op.or]: {}, }; if (Email) { where[Op.or]['Email'] = Email; } if (UserName) { where[Op.or]['UserName'] = UserName; } //If Params.IdNo is not null, then Params.IdType is required and vice versa. if (IdType && IdNo) { where[Op.and] = [{ IdType: IdType }, { IdNo: IdNo }]; } if (ContactNo) { where[Op.or]['ContactNo'] = ContactNo; } //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 const user = await User._Repository.findAll({ where, transaction: dbTransaction, }); if (user && user.length > 0) { throw new ClassError( 'LoginUser', 'LoginUserErrMsg0X', 'User info already exists', ); } } catch (error) { throw error; } } private static generateDefaultPassword(): string { //This method will generate default password for user. try { //Part 1: Retrieve Password Policy //Retrieve all password policy from component config, call ComponentConfig.getComponentConfigValue method const passwordPolicy = ComponentConfig.getComponentConfigValue( '@tomei/sso', 'passwordPolicy', ); //Make sure all passwordPolicy keys got values, if not throw new ClassError if ( !passwordPolicy || !passwordPolicy.maxLen || !passwordPolicy.minLen || !passwordPolicy.nonAcceptableChar || !passwordPolicy.numOfCapitalLetters || !passwordPolicy.numOfNumbers || !passwordPolicy.numOfSpecialChars ) { throw new ClassError( 'LoginUser', 'LoginUserErrMsg0X', 'Missing password policy. Please set in config file.', ); } if ( passwordPolicy.numOfCapitalLetters + passwordPolicy.numOfNumbers + passwordPolicy.numOfSpecialChars > passwordPolicy.maxLen ) { throw new ClassError( 'LoginUser', 'LoginUserErrMsg0X', 'Password policy is invalid. Please set in config file.', ); } //Part 2: Generate Random Password and returns //Generate random password based on passwordPolicy const { maxLen, minLen, nonAcceptableChar, numOfCapitalLetters, numOfNumbers, numOfSpecialChars, } = passwordPolicy; const passwordLength = Math.floor(Math.random() * (maxLen - minLen + 1)) + minLen; const words = 'abcdefghijklmnopqrstuvwxyz'; const capitalLetters = words.toUpperCase(); const numbers = '0123456789'; const specialChars = '!@#$%^&*()_+-={}[]|:;"<>,.?/~`'; const nonAcceptableChars: string[] = nonAcceptableChar.split(','); const filteredWords: string[] = words .split('') .filter((word) => !nonAcceptableChars.includes(word)); const filteredCapitalLetters: string[] = capitalLetters .split('') .filter((word) => !nonAcceptableChars.includes(word)); const filteredNumbers: string[] = numbers .split('') .filter((word) => !nonAcceptableChars.includes(word)); const filteredSpecialChars: string[] = specialChars .split('') .filter((word) => !nonAcceptableChars.includes(word)); const generatedCapitalLetters: string[] = []; const generatedNumbers: string[] = []; const generatedSpecialChars: string[] = []; const generatedWords: string[] = []; for (let i = 0; i < numOfCapitalLetters; i++) { const randomIndex = Math.floor( Math.random() * filteredCapitalLetters.length, ); generatedCapitalLetters.push(filteredCapitalLetters[randomIndex]); } for (let i = 0; i < numOfNumbers; i++) { const randomIndex = Math.floor(Math.random() * filteredNumbers.length); generatedNumbers.push(filteredNumbers[randomIndex]); } for (let i = 0; i < numOfSpecialChars; i++) { const randomIndex = Math.floor( Math.random() * filteredSpecialChars.length, ); generatedSpecialChars.push(filteredSpecialChars[randomIndex]); } for ( let i = 0; i < passwordLength - (numOfCapitalLetters + numOfNumbers + numOfSpecialChars); i++ ) { const randomIndex = Math.floor(Math.random() * filteredWords.length); generatedWords.push(filteredWords[randomIndex]); } //Combine all generated words, capitalLetters, numbers and specialChars and shuffle it let generatedPassword = ''; const allGeneratedChars = generatedCapitalLetters.concat( generatedNumbers, generatedSpecialChars, generatedWords, ); allGeneratedChars.sort(() => Math.random() - 0.5); generatedPassword = allGeneratedChars.join(''); return generatedPassword; } catch (error) { throw error; } } private static async setPassword( dbTransaction: any, user: User, password: string, ): Promise<User> { //This method will set password for the user. try { //Part 1: Verify Password //Retrieve all password policy from component config const passwordPolicy = ComponentConfig.getComponentConfigValue( '@tomei/sso', 'passwordPolicy', ); //Make sure all passwordPolicy keys got values, if not throw a ClassError if ( !passwordPolicy || !passwordPolicy.maxLen || !passwordPolicy.minLen || !passwordPolicy.nonAcceptableChar || !passwordPolicy.numOfCapitalLetters || !passwordPolicy.numOfNumbers || !passwordPolicy.numOfSpecialChars ) { throw new ClassError( 'LoginUser', 'LoginUserErrMsg0X', 'Missing password policy. Please set in config file.', ); } //Compare Params.password with the password policy. If not matched, throw new ClassError try { //Check if password length is more than passwordPolicy.minLen if (password.length < passwordPolicy.minLen) { throw Error('Password is too short'); } //Check if password length is less than passwordPolicy.maxLen if (password.length > passwordPolicy.maxLen) { throw Error('Password is too long'); } //Check if password contains nonAcceptableChar const nonAcceptableChars: string[] = passwordPolicy.nonAcceptableChar.split(','); const nonAcceptableCharsFound = nonAcceptableChars.some((char) => password.includes(char), ); if (nonAcceptableCharsFound) { throw Error('Password contains unacceptable characters'); } //Check if password contains the correct amount of capital letter required from numOfCapitalLetters const capitalLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; const numOfCapitalLetters = passwordPolicy.numOfCapitalLetters; const capitalLettersFound = capitalLetters .split('') .filter((char) => password.includes(char)).length; if (capitalLettersFound < numOfCapitalLetters) { throw Error('Password does not contain enough capital letters'); } //Check if password contains the correct amount of numbers required from numOfNumbers const numbers = '0123456789'; const numOfNumbers = passwordPolicy.numOfNumbers; const numbersFound = numbers .split('') .filter((char) => password.includes(char)).length; if (numbersFound < numOfNumbers) { throw Error('Password does not contain enough numbers'); } //Check if password contains the correct amount of special characters required from numOfSpecialChars const specialChars = '!@#$%^&*()_+-={}[]|:;"<>,.?/~`'; const numOfSpecialChars = passwordPolicy.numOfSpecialChars; const specialCharsFound = specialChars .split('') .filter((char) => password.includes(char)).length; if (specialCharsFound < numOfSpecialChars) { throw Error('Password does not contain enough special characters'); } } catch (error) { throw new ClassError( 'LoginUser', 'LoginUserErrMsg0X', "Your password doesn't meet security requirements. Try using a mix of uppercase and lowercase letters, numbers, and symbols.", ); } //Part 2: Hash Password //Hash the Params.password using PasswordHashService.hashPassword method const passwordHashService = new PasswordHashService(); const hashedPassword = await passwordHashService.hashPassword(password); //Part 3: Return Updated User Instance user._Password = hashedPassword; return user; } catch (error) { throw error; } } public async generateAuthorizationToken(): Promise<{ plaintextToken: string; hashedToken: string; }> { // Generate a random token const plaintextToken = randomBytes(32).toString('hex'); // 64-character hex string // Hash the token using SHA-256 const hashedToken = createHash('sha256') .update(plaintextToken) .digest('hex'); // Save the hashed token to Redis this._SessionService.setAuthorizationCode( hashedToken, this.ObjectId, 60 * 60 * 24, ); // 24 hours // Return the plaintext token for the user to use return { plaintextToken, hashedToken }; } public async validateAuthorizationToken( autorizationToken: string, ): Promise<string> { try { const hashedSubmittedToken = createHash('sha256') .update(autorizationToken) .digest('hex'); // Check if the token exists in Redis const userId = await this._SessionService.retrieveAuthorizationCode( hashedSubmittedToken, ); if (!userId) { return null; } await this._SessionService.deleteAuthorizationCode(hashedSubmittedToken); return userId; } catch (error) { throw error; } } public static async resetPassword( sessionService: ISessionService, autorizationToken: string, password: string, dbTransaction: any, ): Promise<void> { try { const hashedSubmittedToken = createHash('sha256') .update(autorizationToken) .digest('hex'); // Check if the token exists in Redis const userId = await sessionService.retrieveAuthorizationCode(hashedSubmittedToken); if (!userId) { // Token is not valid, throw an error throw new ClassError( 'LoginUser', 'LoginUserErrMsg0X', 'Invalid token', 'setupFirstPassword', 401, ); } await sessionService.deleteAuthorizationCode(hashedSubmittedToken); console.log(`Token verified for user: ${userId}`); // Part 2: Validate Password History // Call by passing: // dbTransaction // UserId: loginUser.ObjectId // Password: password const passwordHashService = new PasswordHashService(); await UserPasswordHistory.validate( dbTransaction, parseInt(userId), password, passwordHashService, ); //Instantiate user const user = await User.init( sessionService, parseInt(userId), dbTransaction, ); //Set new password await User.setPassword(dbTransaction, user, password); //Update user record let userData = await User._Repository.update( { Password: user._Password, DefaultPasswordChangedYN: YN.Yes, NeedToChangePasswordYN: YN.No, }, { where: { UserId: user.UserId, }, transaction: dbTransaction, }, ); await UserPasswordHistory.create( dbTransaction, parseInt(userId), user._Password, ); } catch (error) { throw error; } } public static async create( loginUser: User, dbTransaction: any, user: User, ): Promise<User> { try { //This method will insert new user record //Part 1: Privilege Checking const systemCode = ApplicationConfig.getComponentConfigValue('system-code'); const isPrivileged = await loginUser.checkPrivileges( systemCode, 'User - Create', ); //If user does not have privilege to create user, throw a ClassError if (!isPrivileged) { throw new ClassError( 'LoginUser', 'LoginUserErrMsg0X', 'You do not have the privilege to create user', ); } //Part 2: Validation //Make sure Params.user.Email got values. If not, throw new ClassError if (!user.Email && !user.UserName) { throw new ClassError( 'LoginUser', 'LoginUserErrMsg0X', 'Email and Username is required', ); } //Check if user info exists, call LoginUser.CheckUserInfoDuplicated method await User.checkUserInfoDuplicated(dbTransaction, { Email: user.Email, UserName: user.UserName, IdType: user.IDType, IdNo: user.IDNo, ContactNo: user.ContactNo, }); //Part 3: Generate Default Password const defaultPassword = User.generateDefaultPassword(); user = await User.setPassword(dbTransaction, user, defaultPassword); //Part 4: Insert User Record //Set userToBeCreated to the instantiation of new LoginUser (using private constructor) const userInfo: IUserAttr = { UserName: user.UserName, FullName: user.FullName, IDNo: user.IDNo, IDType: user.IDType, Email: user.Email, ContactNo: user.ContactNo, Password: user.Password, Status: UserStatus.ACTIVE, FirstLoginAt: null, LastLoginAt: null, MFAEnabled: null, MFAConfig: null, MFABypassYN: YN.No, RecoveryEmail: null, FailedLoginAttemptCount: 0, LastFailedLoginAt: null, LastPasswordChangedAt: null, DefaultPasswordChangedYN: YN.No, NeedToChangePasswordYN: YN.Yes, PasscodeHash: null, PasscodeUpdatedAt: null, CreatedById: loginUser.UserId, CreatedAt: new Date(), UpdatedById: loginUser.UserId,