UNPKG

authenzify

Version:

server to manage authentication authorization of users and more

1,375 lines (1,156 loc) 37.8 kB
import pkg from 'jsonwebtoken' const { sign, verify } = pkg import { parsePhoneNumberFromString } from 'libphonenumber-js' import { encrypt, getSaltHex, doesPasswordMatch, } from '../../util/encryption.js' import { USER_ACTIONS, SIGN_IN_ERRORS, SIGN_UP_ERRORS, PERMISSIONS_ERRORS, USER_VERIFICATIONS, MISSING_CONFIGURATION, CHANGE_PASSWORD_ERRORS, FORGOT_PASSWORD_ERRORS, } from '../../errors/error-codes.js' import { unique } from '../../util/helpers.js' import HttpError from '../../errors/HttpError.js' import { emitter } from '../../services/emitter.js' import { verifyExistence } from '../../util/util.js' import { googleApisProvider } from '../providers/google.js' import { generateTenantId } from '../../util/record-id-prefixes.js' import { USERS_SERVICE_EVENTS } from '../../events/users-service.events.js' import { VERIFICATION_TYPES, PASSWORDLESS_CHANNELS, USER_SIGNED_UP_OR_IN_BY, } from '../../constant.js' const prmFnList = [ 'findPermission', 'findPermissions', 'createPermission', 'deletePermission', 'findPermissionByName', 'findPermissionsGroup', 'findPermissionsGroups', 'initializePermissions', 'deletePermissionsGroup', 'createPermissionsGroup', 'findPermissionsGroupsByNames', ] const usrFnList = [ 'create', 'findOne', 'updateUser', 'verifyUser', 'setLastLogin', 'createGoogleUser', 'updateUserPermissions', ] const vrfFnList = ['delete', 'findOne', 'findByUserId', 'createVerification'] export class UsersManagement { #services googleApis #configService constructor({ services, configService }) { verifyExistence(services.Users, usrFnList) verifyExistence(services.Permissions, prmFnList) verifyExistence(services.Verifications, vrfFnList) this.#services = services this.#configService = configService this.googleApis = this.#configService.googleSignInClientId ? googleApisProvider({ GOOGLE_CLIENT_ID: this.#configService.googleSignInClientId, }) : null if (!this.#configService.googleSignInClientId) { console.warn( `Google client id wasn't supply, services/APIs Google will not works`, ) } } doesUsernamePolicyValid({ email }) { return Promise.resolve(this.#configService.doesUsernamePolicyValid(email)) } doesPasswordPolicyValid({ password }) { return Promise.resolve( this.#configService.doesPasswordPolicyValid(password), ) } async encrypt({ password }) { const salt = getSaltHex(this.#configService.saltLength) const passwordEncrypted = await encrypt({ salt, expression: password, passwordPrivateKey: this.#configService.passwordPrivateKey, }) return { salt, password: passwordEncrypted, } } getExpirationAt(milliseconds) { return new Date(Date.now() + milliseconds) } async innerSignUp(userDetails) { try { const { email, password, phone, ...profile } = userDetails const usernamePolicyIsValid = await this.#configService.doesUsernamePolicyValid(email) if (!usernamePolicyIsValid) { throw new HttpError(SIGN_UP_ERRORS.INVALID_USERNAME_POLICY) } const passwordPolicyIsValid = await this.#configService.doesPasswordPolicyValid(password) if (!passwordPolicyIsValid) { throw new HttpError(SIGN_UP_ERRORS.INVALID_PASSWORD_POLICY) } const exists = await this.#services.Users.findOne({ email }) if (exists) { throw new HttpError(SIGN_UP_ERRORS.USER_ALREADY_EXISTS) } const encryptedPassword = await this.encrypt({ password }) const defaultPermissionsSignUp = await this.#services.Permissions.findPermissions({ isBase: true }) const user = await this.#services.Users.create({ email, phone, profile, ...encryptedPassword, signedUpVia: USER_SIGNED_UP_OR_IN_BY.EMAIL, isValid: this.#configService.activateUserAuto, permissions: defaultPermissionsSignUp.map(({ name }) => name), }) const userClean = this.mapUser(user) return userClean } catch (error) { throw error } } verifyToken(token) { const decoded = verify( token, this.#configService.publicKey, this.#configService.jwtVerifyOptions, ) return decoded } async verifyUser(user, verification) { const res = await this.#services.Users.verifyUser(user, verification) return res } mapUser(user) { const { id, email, profile, isValid, username, tenantId, isDeleted, permissions, permissionsGroups, } = user return { id, email, isValid, profile, username, tenantId, isDeleted, permissions, permissionsGroups, } } async getUser({ id }) { const user = await this.#services.Users.findOne({ id }) return user ? this.mapUser(user) : user } async signUp(userDetails) { try { const user = await this.innerSignUp(userDetails) emitter.emit(USERS_SERVICE_EVENTS.USER_SIGN_UP, user) return user } catch (error) { emitter.emit(USERS_SERVICE_EVENTS.USER_SIGN_UP_ERROR, error) throw error } } async buildSignInUserInfo({ user }) { const { id, email, phone, profile, tenantId, username, avatarUrl, permissions, permissionsGroups, } = user await this.getUserPermissions({ tenantId, permissions, permissionsGroups, }) return { id, email, phone, profile, username, tenantId, avatarUrl, permissions, } } async getUserPermissions({ tenantId, permissions, permissionsGroups }) { const permissionsGroupsFull = await this.#services.Permissions.findPermissionsGroupsByNames({ tenantId, names: permissionsGroups, }) const permissionsFromGroups = permissionsGroupsFull .map(({ permissions }) => permissions) .flat() .map(({ name }) => name) const allPermissionsUnique = [] .concat(permissionsFromGroups) .concat(permissions) .filter(unique) return allPermissionsUnique } async innerSignIn(credentials) { const user = await this.#services.Users.findOne({ email: credentials.email, }) if (!user) { throw new HttpError(SIGN_IN_ERRORS.USER_NOT_EXIST) } const { isValid, isDeleted } = user if (isDeleted) { throw new HttpError(SIGN_IN_ERRORS.USER_DELETED) } if (!isValid) { throw new HttpError(SIGN_IN_ERRORS.USER_NOT_VERIFIED) } if (!user.password) { throw new HttpError(SIGN_IN_ERRORS.USER_DOES_NOT_HAVE_A_PASSWORD) } const isMatch = await doesPasswordMatch({ salt: user.salt, password: credentials.password, encryptedPassword: user.password, passwordPrivateKey: this.#configService.passwordPrivateKey, }) if (!isMatch) { throw new HttpError(SIGN_IN_ERRORS.INVALID_USERNAME_OR_PASSWORD) } const token = this.loginUser({ user, signedInVia: USER_SIGNED_UP_OR_IN_BY.EMAIL, }) return token } async signIn(credentials) { try { const token = await this.innerSignIn(credentials) emitter.emit(USERS_SERVICE_EVENTS.USER_SIGN_IN, token) return token } catch (error) { emitter.emit(USERS_SERVICE_EVENTS.USER_SIGN_IN_ERROR, error) throw error } } async verifyActivation(verificationId) { await this.cleanExpiredVerifications() const verification = await this.#services.Verifications.findOne({ isDeleted: false, id: verificationId, type: VERIFICATION_TYPES.SIGN_UP, }) if (!verification) { throw new HttpError(SIGN_UP_ERRORS.INVALID_ACTION) } if (verification.isDeleted) { throw new HttpError(SIGN_UP_ERRORS.INVALID_ACTION) } const user = await this.getUser({ id: verification.userId }) if (!user) { throw new HttpError(SIGN_UP_ERRORS.USER_DOES_NOT_EXISTS) } if (user.isDeleted) { throw new HttpError(SIGN_UP_ERRORS.USER_DOES_NOT_EXISTS) } const res = await this.#services.Users.verifyUser(user, verification) return true } async changePassword({ password, newPassword, userInfo }) { if (!password?.trim() || !newPassword?.trim()) { throw new HttpError(CHANGE_PASSWORD_ERRORS.INVALID_PASSWORD_POLICY) } const user = await this.#services.Users.findOne({ email: userInfo.email, }) if (!user) { throw new HttpError(CHANGE_PASSWORD_ERRORS.USER_NOT_EXIST) } if (user.isDeleted) { throw new HttpError(CHANGE_PASSWORD_ERRORS.USER_NOT_EXIST) } if (!user.isValid) { throw new HttpError(CHANGE_PASSWORD_ERRORS.USER_NOT_EXIST) } const isMatch = await doesPasswordMatch({ password: password, encryptedPassword: user.password, salt: user.salt, passwordPrivateKey: this.#configService.passwordPrivateKey, }) if (!isMatch) { throw new HttpError(CHANGE_PASSWORD_ERRORS.INVALID_USERNAME_OR_PASSWORD) } const isNewPasswordMatch = await doesPasswordMatch({ password: newPassword, encryptedPassword: user.password, salt: user.salt, passwordPrivateKey: this.#configService.passwordPrivateKey, }) if (isNewPasswordMatch) { throw new HttpError( CHANGE_PASSWORD_ERRORS.CANNOT_CHANGE_TO_THE_EXISTING_PASSWORD, ) } const passwordPolicyIsValid = await this.#configService.doesPasswordPolicyValid(newPassword) if (!passwordPolicyIsValid) { throw new HttpError(CHANGE_PASSWORD_ERRORS.INVALID_PASSWORD_POLICY) } const encryptedPassword = await this.encrypt({ password: newPassword }) const updatedUser = await this.#services.Users.updateUser( { id: user.id }, encryptedPassword, ) return !!updatedUser?.modifiedCount } async forgotPassword({ email }) { const user = await this.#services.Users.findOne({ email, }) if (!user) { throw new HttpError(FORGOT_PASSWORD_ERRORS.USER_NOT_EXIST) } if (user.isDeleted) { throw new HttpError(FORGOT_PASSWORD_ERRORS.USER_DELETED) } if (!user.isValid) { throw new HttpError(FORGOT_PASSWORD_ERRORS.USER_NOT_EXIST) } const expiresAt = this.getExpirationAt( this.#configService.passwordResetExpiration, ) const verification = await this.#services.Verifications.createVerification({ expiresAt, extraInfo: {}, userId: user.id, type: VERIFICATION_TYPES.RESET_PASSWORD_REQUEST, }) const notRequestedVerification = await this.#services.Verifications.createVerification({ expiresAt, extraInfo: {}, userId: user.id, type: VERIFICATION_TYPES.DID_NOT_REQUESTED_TO_RESET_PASSWORD, }) emitter.emit(USERS_SERVICE_EVENTS.RESET_PASSWORD_REQUESTED, { user, verification, notRequestedVerification, }) return true } doesUserSignedUpWithGoogleAndFirstTimeResetPassword(user) { return ( user.signedUpVia === USER_SIGNED_UP_OR_IN_BY.GOOGLE && !user.password && !user.salt ) } async applyForgotPassword({ verificationId, newPassword }) { await this.cleanExpiredVerifications() const verification = await this.#services.Verifications.findOne({ id: verificationId, type: VERIFICATION_TYPES.RESET_PASSWORD_REQUEST, isDeleted: false, }) if (!verification || verification.isDeleted) { throw new HttpError(CHANGE_PASSWORD_ERRORS.INVALID_ACTION) } const { userId } = verification const user = await this.#services.Users.findOne({ id: userId }) if (!user) { throw new HttpError(CHANGE_PASSWORD_ERRORS.USER_NOT_EXIST) } if (user.isDeleted) { throw new HttpError(CHANGE_PASSWORD_ERRORS.USER_NOT_EXIST) } if (!user.isValid) { throw new HttpError(CHANGE_PASSWORD_ERRORS.USER_NOT_EXIST) } if (!this.doesUserSignedUpWithGoogleAndFirstTimeResetPassword(user)) { const isMatch = await doesPasswordMatch({ password: newPassword, encryptedPassword: user.password, salt: user.salt, passwordPrivateKey: this.#configService.passwordPrivateKey, }) if (isMatch) { throw new HttpError(CHANGE_PASSWORD_ERRORS.INVALID_PASSWORD_POLICY) } } const encryptedPassword = await this.encrypt({ password: newPassword }) const updatedUser = await this.#services.Users.updateUser( { id: user.id }, encryptedPassword, ) await this.#services.Verifications.delete(verificationId) return !!updatedUser?.modifiedCount } //#region Permissions async addPermission(permission) { const createResponse = await this.#services.Permissions.createPermission(permission) return createResponse } async addPermissionsGroup(permissionGroup) { const createResponse = await this.#services.Permissions.createPermissionsGroup(permissionGroup) return createResponse } async deletePermission({ id }) { const deleteResponse = await this.#services.Permissions.deletePermission({ id, }) return deleteResponse } async deletePermissionsGroup({ tenantId, id }) { const deleteResponse = await this.#services.Permissions.deletePermissionsGroup({ tenantId, id }) return deleteResponse } async getPermission({ id }) { const permission = await this.#services.Permissions.findPermission({ id, }) return permission } async getPermissionByName({ name }) { const permission = await this.#services.Permissions.findPermissionByName({ name, }) return permission } async getPermissions(filter) { const permissions = await this.#services.Permissions.findPermissions(filter) return permissions } async getPermissionsGroup({ tenantId, id }) { const permissionsGroup = await this.#services.Permissions.findPermissionsGroup({ tenantId, id, }) return permissionsGroup } async getPermissionsGroups({ tenantId, filter }) { const permissionsGroups = await this.#services.Permissions.findPermissionsGroups({ tenantId, filter, }) return permissionsGroups } async initializePermissions(permissions) { return this.#services.Permissions.initializePermissions(permissions) } async createPermissionsGroupsForNewCompany({ tenantId }) { const existing = await this.getPermissionsGroups({ tenantId, filter: {} }) const existingNames = existing.map(({ name }) => name) const createPermissionsGroupsResponse = await Promise.all( Object.entries(this.#configService.permissionsGroups) // Taking roles from config .filter(([name]) => !existingNames.includes(name)) // Filter the existing roles (this for case that application roles already sets and now adding new) .map(([name, { permissions }]) => { //Creating the roles that doesn't exists return this.addPermissionsGroup({ name, tenantId, permissions, isDeleted: false, }) }), ) return createPermissionsGroupsResponse } async combineAllUserPermissions(user) { const permissions = user.permissions || [] const groupPermissions = user.permissionsGroups?.map(({ permissions }) => permissions).flat() || [] return [].concat(permissions).concat(groupPermissions) } async throwIfNotAllowToApproveUsers({ adminUser }) { // Case of permission for approve user didn't defined by the application if (!this.#configService.approvePermissionsByPermissionsName) { throw new HttpError( PERMISSIONS_ERRORS.PERMISSION_FOR_APPROVE_USERS_NOT_DEFINED, ) } const allAdminPermissions = await this.getUserPermissions({ tenantId: adminUser.tenantId, permissions: adminUser.permissions, permissionsGroups: adminUser.permissionsGroups, }) // In case it has the right permissions const hasTheRightPermissionToApprove = allAdminPermissions.includes( this.#configService.approvePermissionsByPermissionsName, ) // Check the case if this is the first user's (doesIsCompanyInitializedUser) is asking permission, // in this case the user doesn't have yet the permission but he's the company owner user so he can approve himself if (!hasTheRightPermissionToApprove) { throw new HttpError( PERMISSIONS_ERRORS.PERMISSION_CANNOT_INITIATE_BY_OTHER_USER, ) } return true } throwIfUserIsNotValid({ userInfo, user, id }) { if (userInfo.id !== id) { throw new HttpError( PERMISSIONS_ERRORS.PERMISSION_CANNOT_INITIATE_BY_OTHER_USER, ) } if (!user) { throw new HttpError(PERMISSIONS_ERRORS.USER_DOES_NOT_EXISTS) } if (userInfo.email !== user.email) { throw new HttpError(PERMISSIONS_ERRORS.INVALID_INITIATOR_EMAIL) } } throwIfAdminUserIsNotValid({ adminUser }) { if (!adminUser || adminUser.isDeleted || !adminUser.isValid) { throw new HttpError(PERMISSIONS_ERRORS.ADMIN_USER_DOES_NOT_EXISTS) } return true } async initializeNewCompanyPermissions({ id }) { // Case is the first user const tenantId = generateTenantId() await this.createPermissionsGroupsForNewCompany({ tenantId }) await this.#services.Users.updateUserPermissions( { id }, { tenantId, doesIsCompanyInitializedUser: true, }, { permissions: this.#configService.approvePermissionsByPermissionsName, }, ) return this.#services.Users.findOne({ id }) } doesItTheAdminRequestThePermissions({ user, adminEmail }) { return user.email === adminEmail } userRegisterAsNewCompany({ adminEmail, user }) { return ( this.doesItTheAdminRequestThePermissions({ user, adminEmail }) && !user.tenantId ) } getHighestRole = () => { const highestRoles = Object.entries(this.#configService.permissionsGroups) .filter(([_, value]) => value?.highest) .map(([name]) => ({ name })) return highestRoles } async getAvailablePermissionsForUser({ user, adminUser }) { const isTheFirstUser = user.email === adminUser.email if (isTheFirstUser) { return this.getHighestRole() } const permissionsGroups = await this.getPermissionsGroups({ tenantId: adminUser.tenantId, filter: {}, }) return permissionsGroups } async cleanExpiredVerifications() { const updatedAt = new Date() await this.#services.Verifications.updateMany( { expiresAt: { $lt: new Date() }, isDeleted: { $ne: true }, }, { $set: { isDeleted: true, deletedAt: updatedAt, updatedAt }, }, ) } async createUserPermissionsVerification({ id, adminUser, user }) { const permissionsGroups = await this.getAvailablePermissionsForUser({ user, adminUser, }) await this.cleanExpiredVerifications() const verificationExists = await this.#services.Verifications.findByUserId({ id, isDeleted: false, type: VERIFICATION_TYPES.USER_PERMISSIONS_REQUEST, }) if (verificationExists) { await this.#services.Verifications.delete(verificationExists.id) } const anonymousRoles = Object.entries(this.#configService.permissionsGroups) .filter(([_, value]) => value.anonymousRole) .map(([name]) => name) const expiresAt = this.getExpirationAt( this.#configService.userPermissionsRequestLinkExpiration, ) const verification = await this.#services.Verifications.createVerification({ expiresAt, userId: id, type: VERIFICATION_TYPES.USER_PERMISSIONS_REQUEST, extraInfo: { adminEmail: adminUser.email, userEmail: user.email, permissionsGroups: permissionsGroups .filter(({ name }) => !anonymousRoles.includes(name)) .map(({ name }) => name), }, }) return { verification, permissionsGroups } } async requestPermissionForUser({ id, companyDetails, userInfo }) { const user = await this.getUser({ id }) this.throwIfUserIsNotValid({ userInfo, user, id }) const { email: adminEmail } = companyDetails const adminUser = await this.#services.Users.findOne({ email: adminEmail, }) if (this.userRegisterAsNewCompany({ adminEmail, user })) { const updatedAdminUser = await this.initializeNewCompanyPermissions({ id, }) if (this.#configService.skipFirstCompanyUserSelectsRoleByEmail) { const highestRoles = this.getHighestRole() const [role] = highestRoles await this.setRequestedPermissions({ adminUser: updatedAdminUser, user: updatedAdminUser, role: role.name, }) const updateUserWithPermissions = await this.#services.Users.findOne({ email: adminEmail, }) const token = await this.loginUser({ user: updateUserWithPermissions, signedInVia: USER_SIGNED_UP_OR_IN_BY.SYSTEM, }) return { token, isAdminUser: true } } } else { await this.throwIfAdminUserIsNotValid({ adminUser }) await this.throwIfNotAllowToApproveUsers({ adminUser }) } const { verification, permissionsGroups } = await this.createUserPermissionsVerification({ id, adminUser, user }) emitter.emit(USERS_SERVICE_EVENTS.USER_PERMISSIONS_REQUESTED, { permissionsGroups, verification, user, adminEmail: companyDetails.email, }) return { isAdminUser: false, token: null } } doesTheUserIsValid({ user }) { return user && !user.isDeleted && user.isValid } doesUserHasTheRightPermissionToApprove({ user }) { const { permissions, permissionsGroups = [] } = user const permissionsFromGroups = Object.entries( this.#configService.permissionsGroups, ) .filter(([role]) => permissionsGroups.includes(role)) .map(([_, { permissions }]) => permissions) .flat() .map(({ name }) => name) return [] .concat(permissions) .concat(permissionsFromGroups) .includes(this.#configService.approvePermissionsByPermissionsName) } setRequestedPermissions = async ({ adminUser, user, role }) => { const permissionsGroup = await this.#services.Permissions.findPermissionsGroups({ tenantId: adminUser.tenantId, filter: { name: role }, }) if (!permissionsGroup.find(({ name }) => name === role)) { throw new HttpError(PERMISSIONS_ERRORS.ROLE_NOT_ALLOWED) } const res = await this.#services.Users.updateUser( { id: user.id }, { tenantId: user.tenantId || adminUser.tenantId, // Need to supports multi tenants permissionsGroups: permissionsGroup.map(({ name }) => name), }, ) return res } async verifyUserPermissionRequest({ verificationId, role, userInfo }) { await this.cleanExpiredVerifications() const verification = await this.#services.Verifications.findOne({ id: verificationId, type: VERIFICATION_TYPES.USER_PERMISSIONS_REQUEST, isDeleted: false, }) if (!verification || verification.isDeleted) { throw new HttpError(PERMISSIONS_ERRORS.INVALID_ACTION) } const { extraInfo: { adminEmail, userEmail, permissionsGroups }, } = verification const user = await this.#services.Users.findOne({ email: userEmail }) if (!permissionsGroups.includes(role)) { throw new HttpError(PERMISSIONS_ERRORS.ROLE_NOT_ALLOWED) } if ( !this.doesTheUserIsValid({ user, }) ) { throw new HttpError(PERMISSIONS_ERRORS.USER_DOES_NOT_EXISTS) } const isUserAdminRequest = this.doesItTheAdminRequestThePermissions({ user, adminEmail, }) const adminUser = isUserAdminRequest ? user : await this.#services.Users.findOne({ email: adminEmail, }) if (userInfo.id !== adminUser.id) { throw new HttpError(PERMISSIONS_ERRORS.USER_NOT_ALLOWED) } // Check if the user that request the permissions doesn't have the right permission to approve if (!this.doesUserHasTheRightPermissionToApprove({ user })) { // If the user doesn't have the permissions so check if the user is also the admin // and if it does the admin so it's the same user and we shouldn't check again the permissions if (isUserAdminRequest) { throw new HttpError(PERMISSIONS_ERRORS.USER_NOT_ALLOWED) } else { // This is for the case the user is not the admin so we need to check if the admin has the permission for approve the user // First check existence of the admin user if ( !this.doesTheUserIsValid({ user: adminUser, }) ) { throw new HttpError(PERMISSIONS_ERRORS.ADMIN_USER_DOES_NOT_EXISTS) } // Check if the adminUser has the right permission to approve the permissions for the user if (!this.doesUserHasTheRightPermissionToApprove({ user: adminUser })) { throw new HttpError(PERMISSIONS_ERRORS.USER_NOT_ALLOWED) } } } // Check if the requested role is exists this for making sure that the user didn't hack for role that it doesn't allow to await this.setRequestedPermissions({ adminUser, user, role }) await this.#services.Verifications.delete(verificationId) emitter.emit(USERS_SERVICE_EVENTS.USER_PERMISSIONS_APPROVED, { user, adminUser, }) return true } //#endregion //#region Events onSignInUpCodeGenerated(onSignInUpCodeGeneratedFunction) { emitter.addListener( USERS_SERVICE_EVENTS.USER_VERIFICATION_CODE_GENERATED, onSignInUpCodeGeneratedFunction, ) } onAssignIdentifierCodeGenerated(onAssignCodeGeneratedFunction) { emitter.addListener( USERS_SERVICE_EVENTS.ASSIGN_IDENTIFIER_VERIFICATION_CODE_GENERATED, onAssignCodeGeneratedFunction, ) } onSignUp(onSignUpFunction) { emitter.addListener(USERS_SERVICE_EVENTS.USER_SIGN_UP, onSignUpFunction) } onSignUpError(onSignUpFunction) { emitter.addListener( USERS_SERVICE_EVENTS.USER_SIGN_UP_ERROR, onSignUpFunction, ) } onSignIn(onSignInFunction) { emitter.addListener(USERS_SERVICE_EVENTS.USER_SIGN_IN, onSignInFunction) } onPermissionsRequested(onPermissionsRequestedFunction) { emitter.addListener( USERS_SERVICE_EVENTS.USER_PERMISSIONS_REQUESTED, onPermissionsRequestedFunction, ) } onPermissionsApproved(onPermissionsApprovedFunction) { emitter.addListener( USERS_SERVICE_EVENTS.USER_PERMISSIONS_APPROVED, onPermissionsApprovedFunction, ) } onForgotPassword(onForgotPasswordFunction) { emitter.addListener( USERS_SERVICE_EVENTS.RESET_PASSWORD_REQUESTED, onForgotPasswordFunction, ) } //#endregion Events async loginUser({ user, signedInVia }) { const userInfo = await this.buildSignInUserInfo({ user }) const token = sign( userInfo, this.#configService.privateKey, this.#configService.jwtSignOptions, ) await this.#services.Users.setLastLogin( { id: user.id }, { lastLogin: new Date(), signedInVia, }, ) return token } async innerSignInGoogle(googleEmail) { const user = await this.#services.Users.findOne({ email: googleEmail, }) if (!user) { throw new HttpError(SIGN_IN_ERRORS.USER_NOT_EXIST) } const { isValid, isDeleted } = user if (isDeleted) { throw new HttpError(SIGN_IN_ERRORS.USER_DELETED) } if (!isValid) { throw new HttpError(SIGN_IN_ERRORS.USER_NOT_VERIFIED) } const token = await this.loginUser({ user, signedInVia: USER_SIGNED_UP_OR_IN_BY.GOOGLE, }) return token } async innerSignUpGoogle(googleUser) { try { const defaultPermissionsSignUp = await this.#services.Permissions.findPermissions({ isBase: true }) const user = await this.#services.Users.createGoogleUser({ ...googleUser, permissions: defaultPermissionsSignUp.map(({ name }) => name), }) const userClean = this.mapUser(user) return userClean } catch (error) { throw error } } async signInGoogle(token) { try { if (!this.googleApis) { throw new HttpError(MISSING_CONFIGURATION) } const googleUser = await this.googleApis.verifyClientToken(token) const userExists = await this.#services.Users.findOne({ email: googleUser.email, }) if (!userExists) { const { email, firstName, lastName, userVerified } = googleUser const userAfterSignedUpViaGoogleResponse = await this.innerSignUpGoogle( { email, profile: { firstName, lastName, }, isValid: userVerified, isDeleted: false, signedUpVia: USER_SIGNED_UP_OR_IN_BY.GOOGLE, extendedInfo: { googleUser }, avatarUrl: googleUser.avatarUrl, }, ) emitter.emit( USERS_SERVICE_EVENTS.USER_SIGN_UP_VIA_GOOGLE, userAfterSignedUpViaGoogleResponse, ) } else { const { firstName, lastName } = googleUser await this.#services.Users.updateUser( { id: userExists.id }, { profile: { firstName, lastName, }, extendedInfo: { googleUser }, avatarUrl: userExists.avatarUrl ? userExists.avatarUrl : googleUser.avatarUrl, }, ) } const tokenResponse = await this.innerSignInGoogle(googleUser.email) emitter.emit(USERS_SERVICE_EVENTS.USER_SIGN_IN_VIA_GOOGLE, tokenResponse) return tokenResponse } catch (error) { emitter.emit(USERS_SERVICE_EVENTS.USER_SIGN_IN_VIA_GOOGLE_ERROR, error) throw error } } async getSignInCode({ signInVia, identifier: identifierDirty, deviceIdentity, }) { const type = USER_SIGNED_UP_OR_IN_BY[signInVia.toUpperCase()] const identifier = this.normalizeIdentifier({ assignType: signInVia, identifier: identifierDirty, }) if (!type) { throw new HttpError(SIGN_IN_ERRORS.INVALID_CHANNEL_FOR_VERIFICATION_CODE) } const filter = type === USER_SIGNED_UP_OR_IN_BY.EMAIL ? { email: identifier } : { phone: identifier } const exists = await this.#services.Users.findOne(filter) if (!exists) { const defaultPermissionsSignUp = await this.#services.Permissions.findPermissions({ isBase: true }) await this.#services.Users.create({ ...filter, deviceIdentity, signedUpVia: signInVia, isValid: this.#configService.activateUserAuto, permissions: defaultPermissionsSignUp.map(({ name }) => name), }) } const code = this.#configService.otpGenerator() const user = await this.#services.Users.findOne(filter) const expiresAt = this.getExpirationAt( this.#configService.signInCodeExpiration, ) const verification = await this.#services.Verifications.createVerification({ code, expiresAt, identifier, signInVia, userId: user.id, extraInfo: { identifier, code, signInVia }, type: VERIFICATION_TYPES.PASSWORDLESS_VERIFICATION_CODE, }) emitter.emit(USERS_SERVICE_EVENTS.USER_VERIFICATION_CODE_GENERATED, { identifier, signInVia, verificationInfo: { code, identifier, signInVia, userId: user.id, verificationId: verification.id, }, }) return { verificationId: verification.id } } async signInOrUpWithCode({ code, signedInVia, identifier, verificationId }) { const verification = await this.#services.Verifications.findOne({ isDeleted: false, id: verificationId, type: VERIFICATION_TYPES.PASSWORDLESS_VERIFICATION_CODE, }) if (code !== verification.code || identifier !== verification.identifier) { throw new HttpError( SIGN_IN_ERRORS.INVALID_VERIFICATION_CODE_OR_IDENTIFIER, ) } if (!verification?.userId) { throw new HttpError( SIGN_IN_ERRORS.INVALID_VERIFICATION_CODE_OR_IDENTIFIER, ) } const user = await this.#services.Users.findOne({ id: verification.userId }) const token = await this.loginUser({ user, signedInVia }) return token } normalizeIdentifier({ assignType, identifier }) { switch (assignType.trim().toLowerCase()) { case PASSWORDLESS_CHANNELS.phone: const phone = this.normalizePhone(identifier) return phone default: return identifier } } async getAssignPhoneOrEmailCode({ user, assignInfo }) { const { assignType, identifier: identifierDirty } = assignInfo const identifier = this.normalizeIdentifier({ assignType, identifier: identifierDirty, }) if (!assignType) { throw new HttpError(USER_VERIFICATIONS.INVALID_ASSIGN_TYPE) } if (user[assignType]) { console.error( `Can't assign/change of existing type: ${assignType}: ${user[assignType]}`, ) throw new HttpError(USER_VERIFICATIONS.INVALID_ASSIGN_TYPE) } const passwordLessChannelsList = Object.entries(PASSWORDLESS_CHANNELS).map( ([, assignType]) => assignType.toLowerCase(), ) if (!passwordLessChannelsList.includes(assignType.toLowerCase())) { console.error( `Can't assign type: ${assignType}, should be one of the following: ${passwordLessChannelsList.join(' or ')}`, ) throw new HttpError(USER_VERIFICATIONS.INVALID_ASSIGN_TYPE) } const code = this.#configService.otpGenerator() const expiresAt = this.getExpirationAt( this.#configService.assignIdentifierCodeExpiration, ) const verification = await this.#services.Verifications.createVerification({ code, expiresAt, identifier, assignType, userId: user.id, extraInfo: { identifier, code, assignType }, type: VERIFICATION_TYPES.ASSIGN_PHONE_OR_EMAIL, }) emitter.emit( USERS_SERVICE_EVENTS.ASSIGN_IDENTIFIER_VERIFICATION_CODE_GENERATED, { identifier, assignType, verificationInfo: { code, identifier, userId: user.id, verificationId: verification.id, }, }, ) return { verificationId: verification.id } } normalizePhone(phone) { try { const parsed = parsePhoneNumberFromString(phone) if (!parsed.isValid()) { return null } return parsed.number } catch { return null } } async assignIdentifierViaCode({ user, code, assignType, verificationId, identifier: identifierDirty, }) { const identifier = this.normalizeIdentifier({ assignType, identifier: identifierDirty, }) await this.cleanExpiredVerifications() const verification = await this.#services.Verifications.findOne({ assignType, userId: user.id, isDeleted: false, id: verificationId, type: VERIFICATION_TYPES.ASSIGN_PHONE_OR_EMAIL, }) if (code !== verification.code || identifier !== verification.identifier) { throw new HttpError( SIGN_IN_ERRORS.INVALID_VERIFICATION_CODE_OR_IDENTIFIER, ) } if (!verification?.userId) { throw new HttpError( SIGN_IN_ERRORS.INVALID_VERIFICATION_CODE_OR_IDENTIFIER, ) } if (!assignType) { throw new HttpError(USER_VERIFICATIONS.INVALID_ASSIGN_TYPE) } if (user[assignType]) { console.error( `Can't assign/change of existing type: ${assignType}: ${user[assignType]}`, ) throw new HttpError(USER_VERIFICATIONS.INVALID_ASSIGN_TYPE) } await this.#services.Users.updateUser( { id: user.id }, { [assignType]: identifier }, ) await this.#services.Verifications.delete(verificationId) const updatedUser = await this.#services.Users.findOne({ id: user.id }) return { userId: updatedUser.id, phone: updatedUser.phone, email: updatedUser.email, } } async updateUserProfile({ profile, user }) { const { email, phone, tenantId, ...profileClean } = profile if (email || phone || tenantId) { throw new HttpError( USER_ACTIONS.CHANGING_USER_EMAIL_PHONE_TENANT_OR_PASSWORD_IS_NOT_ALLOW, ) } const keyValuePairs = Object.entries(profileClean) const errorList = [] for (const [key, value] of keyValuePairs) { if (value === null || value === undefined || `${value}`.trim() === '') { errorList.push(`${key} can't be ${value}`) } } if (errorList.length) { throw new HttpError({ ...USER_ACTIONS.ITS_INVALID_TO_OVERRIDE_PROFILE_PROPS_TO_BLANK, description: errorList.join(','), }) } await this.#services.Users.updateUser( { id: user.id }, { profile: profileClean }, ) const userUpdated = await this.#services.Users.findOne({ id: user.id }) return this.mapUser(userUpdated) } async createVerification(...props) { return await this.#services.Verifications.createVerification(...props) } }