authenzify
Version:
server to manage authentication authorization of users and more
1,375 lines (1,156 loc) • 37.8 kB
JavaScript
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)
}
}