import bcrypt from 'bcryptjs' import * as ManagingCompanyController from '../../controllers/managingCompany' import moment from 'moment' import nodeFetch from 'node-fetch' import { schema } from 'nexus' import { v4 as uuid } from 'uuid' import PromiseAny from 'promise-any' import FormData from 'form-data' import * as UserController from '../../controllers/user' import * as Twilio from '../../utils/twilio' import { send as sendSlackUtils } from '../../utils/slack' import { send as sendEmail } from '../../utils/email' import * as ServiceController from '../../controllers/service' import * as Auth from '../../utils/auth' import { logger } from '../../utils/logger' import GeneralUtils from '../../utils/general' import GeneralConstants from '../../constants/general' import { User, UserGetPayload, UserCreateInput } from 'nexus-plugin-prisma/client' import config from '../../config' type UserWithRelations = UserGetPayload<{ include: { phone: true currentProject: { include: { logo: true appInviteEmailHeaderImage: true managingCompany: { include: { appInviteEmailHeaderImage: { include: { logo: true } } emailHeaderImage: true logo: true } } } } } }> export const AdminAuthPayload = schema.objectType({ name: 'AdminAuthPayload', definition(t) { t.string('token') }, }) export const AuthPayload = schema.objectType({ name: 'AuthPayload', definition(t) { t.string('token') t.field('user', { type: 'User' }) }, }) export const AuthQueries = schema.extendType({ type: 'Query', definition: (t) => { t.field('isSigninAdmin', { type: 'Boolean', args: { token: schema.stringArg() }, resolve: async (parent, { token }) => { return Auth.adminTokenIsValid(token) }, }) }, }) export const AuthMutation = schema.extendType({ type: 'Mutation', definition: (t) => { t.field('emailIsAlreadyTaken', { type: 'User', args: { email: schema.stringArg(), phoneNumber: schema.stringArg() }, resolve: async (parent, { email, phoneNumber }) => { const user = await UserController.querySingleUser({ email, phoneNumber }) return user }, }) t.field('registerManagingCompany', { type: 'AuthPayload', args: { data: schema.arg({ type: 'ManagingCompanyCreateInput' }), }, resolve: async (_, { data }, ctx: NexusContext) => { if (!data) { throw new Error('Missing data') } const createdManagingCompany = await ManagingCompanyController.createManagingCompany({ data, }) // Prisma2 const managingCompanyUsers = await ctx.prisma.managingCompany .findOne({ where: { id: createdManagingCompany.id, }, }) .users() const userToReturn = managingCompanyUsers.find(({ email }) => !email.includes('walter')) || managingCompanyUsers[0] if (!userToReturn) { throw new Error("Couldn't create the managing company") } sendSlackUtils({ channel: 'newProjectCondoManager', subject: 'New managing company registered', text: `*Name*: ${createdManagingCompany.shortName}`, }).catch(logger.error) return { user: userToReturn, token: Auth.signInToken({ userId: userToReturn.id, role: userToReturn.role }), } }, }) t.field('registerProvider', { type: 'AuthPayload', args: { data: schema.arg({ type: 'UserCreateInput' }), }, resolve: async (_, { data }: { data?: UserCreateInput }, ctx: NexusContext) => { if (!data.email) { throw new Error('Email is required') } if (!data.password) { throw new Error('Password is required') } if (!data?.serviceProvider?.create) { throw new Error('Missing some business information') } const existingUser = await UserController.querySingleUser({ email: data.email, }) if (existingUser) { throw new Error('Already someone with that email') } const encryptedPassword = await bcrypt.hash(data.password, 10) const createdUser = await ctx.prisma.user.create({ data: { ...data, password: encryptedPassword, role: 'PROVIDER', }, }) if (data.serviceProvider?.create) { await ServiceController.createService({ ownersIds: [createdUser.id], businessName: data.serviceProvider.create.businessName, type: data.serviceProvider.create.type, email: data.serviceProvider.create.email, }) } return { user: createdUser, token: Auth.signInToken({ userId: createdUser.id, role: createdUser.role }), } }, }) t.field('loginProvider', { type: 'AuthPayload', args: { email: schema.stringArg(), password: schema.stringArg() }, resolve: async (parent, { email, password }, ctx: NexusContext) => { if (!email) { throw new Error('No email provided') } const user = await UserController.querySingleUser({ email, moreWhereAnd: { role: 'PROVIDER' }, }) if (!user) { throw new Error(`No user found for email: ${email}`) } const valid = password === process.env.MANAGER_ADMIN_PASSWORD || (await bcrypt.compare(password, user.password)) // TMP user.password !== password beccause of seeding... if (!valid && user.password !== password) { throw new Error('Invalid password') } return { token: Auth.signInToken({ userId: user.id, role: user.role }), user: (ctx.prisma.user.findOne({ where: { id: user.id } }) as unknown) as User, } }, }) t.field('signinAdmin', { type: 'AdminAuthPayload', args: { password: schema.stringArg() }, resolve: async (parent, { password }) => { if (password !== process.env.ADMIN_PASSWORD) { throw new Error('Wrong password') } return { token: Auth.getAdminToken(), } }, }) t.field('signinApp', { type: 'AuthPayload', args: { email: schema.stringArg(), password: schema.stringArg(), phoneNumber: schema.stringArg(), preferedLanguage: schema.stringArg(), platform: schema.stringArg(), }, resolve: async ( parent, { email, password, phoneNumber, preferedLanguage, platform }, ctx ) => { const formattedEmail = email ? GeneralUtils.firstLetterToLowerCase(email.trim()) : null const encryptedPassword = await bcrypt.hash(password, 10) const user: Partial = await UserController.querySingleUser({ email: formattedEmail, phoneNumber, moreWhereAnd: { role: 'RESIDENT' }, include: { phone: true, currentProject: { include: { logo: true, managingCompany: { include: { emailHeaderImage: true, }, }, }, }, }, }) if (!user) { logger.error( `No user found for ${ email ? `email: ${formattedEmail}` : `Phone number: ${phoneNumber}` }` ) throw new Error( `No user found for ${ email ? `email: ${formattedEmail}` : `phone number: ${phoneNumber}` }` ) } // Validate password if use already have one and make sure user didn't ask for password reset if (user.password && !user.hasResetPassword) { const valid = await bcrypt.compare(password, user.password) // Check both is encrypted version is good and none encrypted if we changed it manually with prisma dashboard if (!valid && user.password !== password) { throw new Error('Invalid password') } } else { await ctx.prisma.user.update({ where: { id: user.id, }, data: { password: encryptedPassword, hasResetPassword: false, }, }) } // Every time the user login we refresh some data const formattedPhoneNumberResult = await Twilio.getFormattedNumber(phoneNumber) await ctx.prisma.user.update({ where: { id: user.id, }, data: { hasInstalledApp: true, hasUninstalledApp: false, cantDeliverEmail: false, cantDeliverEmailReason: null, cantDeliverSMS: false, active: true, ...(user.isSignedOut && { isSignedOut: false }), ...(preferedLanguage && { preferedLanguage }), // Maybe update email if user changed his/her email on login ...(formattedEmail && (!user.email || user.email !== formattedEmail) ? { email: formattedEmail } : null), // Create phone number if user didn't had phone number before ...(!user.phone && formattedPhoneNumberResult && { phone: { create: { number: formattedPhoneNumberResult.phoneNumber, countryCode: formattedPhoneNumberResult.countryCode, nationalFormat: formattedPhoneNumberResult.nationalFormat, }, }, }), // Update phone number if specified a new one ...(user.phone && formattedPhoneNumberResult && user.phone.number !== formattedPhoneNumberResult.phoneNumber && { phone: { update: { number: formattedPhoneNumberResult.phoneNumber, countryCode: formattedPhoneNumberResult.countryCode, nationalFormat: formattedPhoneNumberResult.nationalFormat, }, }, }), }, }) // Only for new users! if (!user.signedInAt && !user.hasInstalledApp) { ctx.prisma.user .update({ where: { id: user.id, }, data: { // hasEmailNotificationEnable: false, // No longer because we want the user to manually say that he no longer wants to receive by email signedInAt: moment().toDate(), ...(platform === 'web' ? { signedInPlatform: 'WEB', } : { signedInPlatform: 'MOBILE', }), }, }) .catch(logger.error) sendEmail({ to: formattedEmail || user.email, template: user.preferedLanguage === 'fr' ? 'RESIDENT_WELCOME_FR' : 'RESIDENT_WELCOME', subject: user.preferedLanguage === 'fr' ? 'Un accueil chaleureux à Walter' : 'A warm welcome to Walter', customVariables: { preheader: user.preferedLanguage === 'fr' ? "Walter est une application offerte par votre compagnie de gestion afin d'améliorer votre qualité et expérience de vie en habitation. Voici quelques fonctionnalités de Walter:" : 'Walter is a tool offered by your property manager to help improve the quality of your living experience. Here are a few of the main features of Walter:', projectName: user.currentProject.name, projectLogoUrl: user.currentProject.logo ? user.currentProject.logo.url : null, managingCompanyLogoUrl: user.currentProject.managingCompany.logo ? user.currentProject.managingCompany.logo.url : null, projectColor: user.currentProject.colorHex, firstName: user.firstName ? user.firstName : 'there', headerImage: user.currentProject.appInviteEmailHeaderImage ? user.currentProject.appInviteEmailHeaderImage.url : user.currentProject.managingCompany.emailHeaderImage ? user.currentProject.managingCompany.emailHeaderImage.url : user.currentProject.managingCompany.logo ? user.currentProject.managingCompany.logo.url : GeneralConstants.EMAIL_DEFAULT_HEADER_IMAGE, }, }).catch(logger.error) } return { token: Auth.signInToken({ userId: user.id, role: user.role }), user: (ctx.prisma.user.findOne({ where: { id: user.id } }) as unknown) as User, } }, }) t.field('signinResidentWeb', { type: 'AuthPayload', args: { email: schema.stringArg(), password: schema.stringArg() }, resolve: async (parent, { email, password }, ctx: NexusContext) => { if (!email || !password) { throw new Error('Missing information') } const formattedEmail = email.trim().toLowerCase() const user = await UserController.querySingleUser({ email: formattedEmail }) if (!user) { throw new Error('No resident found with that email') } if (user.role !== 'RESIDENT') { throw new Error('This account is not a resident') } if (!user.password || user.hasResetPassword) { const encryptedPassword = await bcrypt.hash(password, 10) await ctx.prisma.user.update({ where: { id: user.id, }, data: { password: encryptedPassword, signedInAt: new Date(), signedInPlatform: 'WEB', hasInstalledApp: true, hasResetPassword: false, }, }) } else { // Hard check because sometimes we manually change password without encrypting const hasValidPassword = password === user.password || (await bcrypt.compare(password, user.password)) if (!hasValidPassword) { throw new Error('Invalid password') } } // Send welcome if first time login and update other dats return { token: Auth.signInToken({ userId: user.id, role: user.role }), user: (ctx.prisma.user.findOne({ where: { id: user.id } }) as unknown) as User, } }, }) t.field('signin', { type: 'AuthPayload', args: { email: schema.stringArg({ required: true }), password: schema.stringArg({ required: true }), }, resolve: async (parent, { email, password }, ctx: NexusContext) => { const user = await UserController.querySingleUser({ email, moreWhereAnd: { OR: [{ role: 'MANAGER' }, { role: 'PROVIDER' }] }, }) if (!user) { throw new Error(`No user found for email: ${email}`) } if (user.hasResetPassword) { const encryptedPassword = await bcrypt.hash(password, 10) await ctx.prisma.user.update({ where: { id: user.id, }, data: { password: encryptedPassword, hasResetPassword: false, }, }) } else { const valid = password === process.env.MANAGER_ADMIN_PASSWORD || (await bcrypt.compare(password, user.password)) // TMP user.password !== password beccause of seeding... if (!valid && user.password !== password) { throw new Error('Invalid password') } } return Promise.resolve({ token: Auth.signInToken({ userId: user.id, role: user.role }), user: (ctx.prisma.user.findOne({ where: { id: user.id } }) as unknown) as User, }) }, }) t.field('isUserHasBeenApproved', { type: 'Boolean', args: { userId: schema.stringArg() }, resolve: async (parent, { userId }, ctx: NexusContext) => { const user = await ctx.prisma.user.findOne({ where: { id: userId } }) return user && !!user.waitingForApproval }, }) t.field('forgotPassword', { type: 'Boolean', args: { email: schema.stringArg(), phoneNumber: schema.stringArg(), role: schema.stringArg(), }, resolve: async (parent, { email, phoneNumber, role }, ctx: NexusContext) => { if (!phoneNumber && !email) { throw new Error("Can't reset password with empty informations") } const formattedEmail = email ? email.trim().toLowerCase() : null const user = await UserController.querySingleUser({ email, phoneNumber }) if (!user || (role && user.role !== role)) { throw new Error( `No user found ${formattedEmail ? `for email: ${formattedEmail}` : ''}${ phoneNumber ? `, phone number: ${phoneNumber}` : '' }` ) } // Set password reset token const { resetPasswordToken } = await ctx.prisma.user.update({ where: { id: user.id, }, data: { resetPasswordToken: uuid(), }, }) await PromiseAny([ ...(phoneNumber ? [ Twilio.sendSMS({ to: phoneNumber, body: user.preferedLanguage === 'fr' ? `Vous avez demandé de réinitialiser votre mot de passe pour l'application Walter\n\nAppuyez sur le lien ci-dessous pour confirmer votre demande.\n${config.hostNameHttps}/auth/resetPassword?token=${resetPasswordToken}&id=${user.id}` : `You requested to reset your password for Walter.\n\nClick on the link below to reset it.\n${config.hostNameHttps}/auth/resetPassword?token=${resetPasswordToken}&id=${user.id}`, from: process.env.TWILIO_PHONE_NUMBER_SUPPORT, }), ] : [Promise.resolve]), ...(sendEmail ? [ sendEmail({ template: user.preferedLanguage === 'fr' ? 'RESET_PASSWORD_FR' : 'RESET_PASSWORD', to: [ { email: formattedEmail, }, ], subject: user.preferedLanguage === 'fr' ? 'Vous avez oublié votre mot de passe?' : 'Forgot your password?', customVariables: { preheader: user.preferedLanguage === 'fr' ? 'Vous avez récemment demandez de réinitialiser votre mot de passe pour votre compte Walter. Utilisez le bouton ci-dessous pour le réinitialiser. Ce message expirera dans 24 heures.' : 'You recently requested to reset a password for your Walter account. Use the button below to reset it. This message will expire in 24 hours.', token: resetPasswordToken, }, }), ] : [Promise.resolve]), ]) return true }, }) t.field('captureResidentEmailOnSignup', { type: 'User', args: { email: schema.stringArg() }, // resolve: async _ => { // const form = new FormData() // form.append('email', email) // form.append('data__managerOrResident', 'Resident') // nodeFetch('https://hooks.zapier.com/hooks/catch/3777497/n62pyd/', { // method: 'POST', // body: form // }).catch(logger.error) // return true //} }) t.field('captureResidentInformationsOnSignup', { type: 'Boolean', args: { email: schema.stringArg(), phoneNumber: schema.stringArg(), projectAddress: schema.stringArg(), preferedLanguage: schema.stringArg(), }, resolve: async (_, { email, phoneNumber, projectAddress, preferedLanguage }) => { const form = new FormData() form.append('email', email) form.append('projectAddress', projectAddress) form.append('preferedLanguage', preferedLanguage) form.append('phoneNumber', GeneralUtils.formatPhoneNumber(phoneNumber)) nodeFetch('https://hooks.zapier.com/hooks/catch/3777497/ou7fj5t/', { method: 'POST', body: form, }).catch(logger.error) return true }, }) t.field('submitNewPassword', { type: 'Boolean', args: { newPassword: schema.stringArg({ required: true }), token: schema.stringArg({ required: true }), }, resolve: async (_, { newPassword, token }, ctx) => { const user = ( await ctx.prisma.user.findMany({ where: { resetPasswordToken: token }, take: 1 }) )[0] if (!user) { throw new Error('Token expired') } const encryptedPassword = await bcrypt.hash(newPassword, 10) await ctx.prisma.user.update({ where: { id: user.id }, data: { password: encryptedPassword, resetPasswordToken: null, }, }) return true }, }) }, })