@coko/server
Version:
Reusable server for use by Coko's projects
728 lines (621 loc) • 18.4 kB
JavaScript
const config = require('config')
const crypto = require('crypto')
const moment = require('moment')
const {
AuthorizationError,
ValidationError,
ConflictError,
} = require('../../errors')
const logger = require('../../logger')
const authentication = require('../../authentication')
const User = require('./user.model')
const Identity = require('../identity/identity.model')
const useTransaction = require('../useTransaction')
const { displayNameConstructor } = require('../_helpers/utilities')
const createJWT = authentication.token.create
const {
identityVerification,
passwordUpdate,
requestResetPassword,
requestResetPasswordEmailNotFound,
} = require('../_helpers/emailTemplates')
const notify = require('../../services/notify')
const {
notificationTypes: { EMAIL },
} = require('../../services/constants')
const {
labels: { USER_CONTROLLER },
} = require('./constants')
const activateUser = async (id, options = {}) => {
try {
const { trx } = options
return useTransaction(
async tr => {
logger.info(
`${USER_CONTROLLER} activateUser: activating user with id ${id}`,
)
return User.activateUsers([id], { trx: tr })
},
{ trx, passedTrxOnly: true },
)
} catch (e) {
logger.error(`${USER_CONTROLLER} activateUser: ${e.message}`)
throw new Error(e)
}
}
const activateUsers = async (ids, options = {}) => {
try {
const { trx } = options
return useTransaction(
async tr => {
logger.info(
`${USER_CONTROLLER} activateUsers: activating users with ids ${ids}`,
)
return User.activateUsers(ids, { trx: tr })
},
{ trx, passedTrxOnly: true },
)
} catch (e) {
logger.error(`${USER_CONTROLLER} activateUsers: ${e.message}`)
throw new Error(e)
}
}
const getUser = async (id, options = {}) => {
try {
const { trx, ...restOptions } = options
return useTransaction(
async tr => {
logger.info(`${USER_CONTROLLER} getUser: fetching user with id ${id}`)
return User.findById(id, { trx: tr, ...restOptions })
},
{ trx, passedTrxOnly: true },
)
} catch (e) {
logger.error(`${USER_CONTROLLER} getUser: ${e.message}`)
throw new Error(e)
}
}
const getDisplayName = async user => {
try {
const { givenNames, surname, username } = user
return displayNameConstructor(givenNames, surname, username)
} catch (e) {
logger.error(`${USER_CONTROLLER} getDisplayName: ${e.message}`)
throw new Error(e)
}
}
const getUsers = async (queryParams = {}, options = {}) => {
try {
const { trx, ...restOptions } = options
return useTransaction(
async tr => {
logger.info(
`${USER_CONTROLLER} getUsers: fetching all users based on provided options ${restOptions}`,
)
return User.find(queryParams, { trx: tr, ...restOptions })
},
{ trx, passedTrxOnly: true },
)
} catch (e) {
logger.error(`${USER_CONTROLLER} getUsers: ${e.message}`)
throw new Error(e)
}
}
const deleteUser = async (id, options = {}) => {
return User.deleteById(id, { trx: options.trx })
}
const deleteUsers = async (ids, options = {}) => {
return User.deleteByIds(ids)
}
const deactivateUser = async (id, options = {}) => {
try {
const { trx } = options
return useTransaction(
async tr => {
logger.info(
`${USER_CONTROLLER} deactivateUser: deactivating user with id ${id}`,
)
return User.deactivateUsers([id], { trx: tr })
},
{ trx, passedTrxOnly: true },
)
} catch (e) {
logger.error(`${USER_CONTROLLER} deactivateUser: ${e.message}`)
throw new Error(e)
}
}
const deactivateUsers = async (ids, options = {}) => {
try {
const { trx } = options
return useTransaction(
async tr => {
logger.info(
`${USER_CONTROLLER} deactivateUsers: deactivating users with id ${ids}`,
)
return User.deactivateUsers(ids, { trx: tr })
},
{ trx, passedTrxOnly: true },
)
} catch (e) {
logger.error(`${USER_CONTROLLER} deactivateUsers: ${e.message}`)
throw new Error(e)
}
}
const updateUser = async (id, data, options = {}) => {
try {
const { email, identityId, ...restData } = data
const { trx, ...restOptions } = options
logger.info(`${USER_CONTROLLER} updateUser: updating user with id ${id}`)
return useTransaction(
async tr => {
if (!email) {
return User.patchAndFetchById(
id,
{ ...restData },
{
trx: tr,
...restOptions,
},
)
}
logger.info(
`${USER_CONTROLLER} updateUser: updating user identity with provided email`,
)
if (!identityId) {
throw new Error(
`${USER_CONTROLLER} updateUser: cannot update email without identity id`,
)
}
await Identity.patchAndFetchById(identityId, { email }, { trx: tr })
if (Object.keys(restData).length !== 0) {
logger.info(
`${USER_CONTROLLER} updateUser: updating user with provided info`,
)
return User.patchAndFetchById(id, restData, {
trx: tr,
...restOptions,
})
}
return User.findById(id, {
trx: tr,
...restOptions,
})
},
{ trx },
)
} catch (e) {
logger.error(`${USER_CONTROLLER} updateUser: ${e.message}`)
throw new Error(e)
}
}
const login = async input => {
try {
let isValid = false
let user
const { username, email, password } = input
if (!username) {
logger.info(
`${USER_CONTROLLER} login: searching for user with email ${email}`,
)
const identity = await Identity.findOne({ email })
if (!identity) throw new AuthorizationError('Wrong username or password.')
user = await User.findById(identity.userId)
} else {
logger.info(
`${USER_CONTROLLER} login: searching for user with username ${username}`,
)
user = await User.findOne({ username })
if (!user) throw new AuthorizationError('Wrong username or password.')
}
logger.info(
`${USER_CONTROLLER} login: checking password validity for user with id ${user.id}`,
)
isValid = await user.isPasswordValid(password)
if (!isValid) {
throw new AuthorizationError('Wrong username or password.')
}
logger.info(`${USER_CONTROLLER} login: password is valid`)
return {
user,
token: createJWT(user),
}
} catch (e) {
logger.error(`${USER_CONTROLLER} login: ${e.message}`)
throw new Error(e)
}
}
const signUp = async (data, options = {}) => {
try {
const { email, ...restData } = data
const { trx } = options
return useTransaction(
async tr => {
if (restData.username) {
const usernameExists = await User.findOne(
{ username: restData.username },
{ trx: tr },
)
if (usernameExists) {
logger.error(`${USER_CONTROLLER} signUp: username already exists`)
throw new ConflictError('Username already exists')
}
}
const existingIdentity = await Identity.findOne({ email }, { trx: tr })
if (existingIdentity) {
const user = await User.findById(existingIdentity.userId, { trx: tr })
if (user.agreedTc) {
logger.error(
`${USER_CONTROLLER} signUp: a user with this email already exists`,
)
throw new ConflictError('A user with this email already exists')
}
// If not agreed to tc, user's been invited but is now signing up
logger.info(
`${USER_CONTROLLER} signUp: connecting user with identity`,
)
const updatedUser = await User.patchAndFetchById(
existingIdentity.userId,
{
...restData,
},
{ trx: tr },
)
const verificationToken = crypto.randomBytes(64).toString('hex')
const verificationTokenTimestamp = new Date()
existingIdentity.patch(
{ verificationToken, verificationTokenTimestamp },
{ trx: tr },
)
const emailData = identityVerification({
verificationToken,
email: existingIdentity.email,
})
notify(EMAIL, emailData)
return updatedUser.id
}
logger.info(`${USER_CONTROLLER} signUp: creating user`)
const newUser = await User.insert(
{
...restData,
},
{ trx: tr },
)
const verificationToken = crypto.randomBytes(64).toString('hex')
const verificationTokenTimestamp = new Date()
logger.info(
`${USER_CONTROLLER} signUp: creating user local identity with provided email`,
)
await Identity.insert(
{
userId: newUser.id,
email,
isSocial: false,
verificationTokenTimestamp,
verificationToken,
isVerified: false,
isDefault: true,
},
{ trx: tr },
)
const emailData = identityVerification({
verificationToken,
email,
})
notify(EMAIL, emailData)
return newUser.id
},
{ trx },
)
} catch (e) {
logger.error(`${USER_CONTROLLER} signUp: ${e.message}`)
throw new Error(e)
}
}
const verifyEmail = async (token, options = {}) => {
try {
const { trx } = options
logger.info(`${USER_CONTROLLER} verifyEmail: verifying user email`)
return useTransaction(
async tr => {
const identity = await Identity.findOne(
{
verificationToken: token,
},
{ trx: tr },
)
const emailVerificationTokenExpiry = config.has(
'emailVerificationTokenExpiry',
)
? {
amount: config.get('emailVerificationTokenExpiry.amount'),
unit: config.get('emailVerificationTokenExpiry.unit'),
}
: {
amount: 24,
unit: 'hours',
}
if (!identity)
throw new Error(`${USER_CONTROLLER} verifyEmail: invalid token`)
if (identity.isVerified)
throw new Error(
`${USER_CONTROLLER} verifyEmail: identity has already been confirmed`,
)
if (!identity.verificationTokenTimestamp) {
throw new Error(
`${USER_CONTROLLER} verifyEmail: confirmation token does not have a corresponding timestamp`,
)
}
if (
moment()
.subtract(
emailVerificationTokenExpiry.amount,
emailVerificationTokenExpiry.unit,
)
.isAfter(identity.verificationTokenTimestamp)
) {
throw new Error(`${USER_CONTROLLER} verifyEmail: Token expired`)
}
await identity.patch(
{
isVerified: true,
},
{ trx: tr },
)
await activateUser(identity.userId, { trx: tr })
return true
},
{ trx },
)
} catch (e) {
logger.error(`${USER_CONTROLLER} verifyEmail: ${e.message}`)
throw new Error(e)
}
}
const resendVerificationEmailCommon = async (identity, options = {}) => {
const verificationToken = crypto.randomBytes(64).toString('hex')
const verificationTokenTimestamp = new Date()
await identity.patch(
{
verificationToken,
verificationTokenTimestamp,
},
{ trx: options.trx },
)
const emailData = identityVerification({
verificationToken,
email: identity.email,
})
notify(EMAIL, emailData)
}
const resendVerificationEmail = async (token, options = {}) => {
try {
logger.info(
`${USER_CONTROLLER} resendVerificationEmail: resending verification email to user`,
)
const identity = await Identity.findOne(
{
verificationToken: token,
},
{ trx: options.trx },
)
if (!identity)
throw new Error(
`${USER_CONTROLLER} resendVerificationEmail: Token does not correspond to an identity`,
)
await resendVerificationEmailCommon(identity, options)
return true
} catch (e) {
logger.error(`${USER_CONTROLLER} resendVerificationEmail: ${e.message}`)
throw e
}
}
const resendVerificationEmailAfterLogin = async (userId, options = {}) => {
try {
logger.info(
`${USER_CONTROLLER} resendVerificationEmailAfterLogin: resending verification email to user`,
)
const identity = await Identity.findOne(
{
userId,
isDefault: true,
},
{ trx: options.trx },
)
await resendVerificationEmailCommon(identity, options)
return true
} catch (e) {
logger.error(`${USER_CONTROLLER} resendVerificationEmail: ${e.message}`)
throw e
}
}
const updatePassword = async (id, currentPassword, newPassword) => {
try {
logger.info(`${USER_CONTROLLER} updatePassword: updating user password`)
await User.updatePassword(id, currentPassword, newPassword, undefined)
const identity = await Identity.findOne({ isDefault: true, userId: id })
const emailData = passwordUpdate({
email: identity.email,
})
notify(EMAIL, emailData)
return true
} catch (e) {
logger.error(`${USER_CONTROLLER} updatePassword: ${e.message}`)
throw new Error(e)
}
}
const sendPasswordResetEmail = async (email, options = {}) => {
try {
const { trx } = options
return useTransaction(
async tr => {
const identity = await Identity.findOne(
{
isDefault: true,
email: email.toLowerCase(),
},
{ trx: tr },
)
if (!identity) {
const emailData = requestResetPasswordEmailNotFound({
email: email.toLowerCase(),
})
notify(EMAIL, emailData)
return true
}
const user = await User.findById(identity.userId, { trx: tr })
const resetToken = crypto.randomBytes(32).toString('hex')
await user.patch(
{
passwordResetTimestamp: new Date(),
passwordResetToken: resetToken,
},
{ trx: tr },
)
logger.info(
`${USER_CONTROLLER} sendPasswordResetEmail: sending password reset email`,
)
const emailData = requestResetPassword({
email: email.toLowerCase(),
token: resetToken,
})
notify(EMAIL, emailData)
return true
},
{ trx, passedTrxOnly: true },
)
} catch (e) {
logger.error(`${USER_CONTROLLER} sendPasswordResetEmail: ${e.message}`)
throw new Error(e)
}
}
const resetPassword = async (token, password, options = {}) => {
try {
const { trx } = options
logger.info(`${USER_CONTROLLER} resetPassword: resetting user password`)
return useTransaction(
async tr => {
const user = await User.findOne(
{ passwordResetToken: token },
{ trx: tr, related: 'defaultIdentity' },
)
if (!user) {
throw new Error(
`${USER_CONTROLLER} resetPassword: no user found with token ${token}`,
)
}
const passwordResetTokenExpiry = config.has('passwordResetTokenExpiry')
? {
amount: config.get('passwordResetTokenExpiry.amount'),
unit: config.get('passwordResetTokenExpiry.unit'),
}
: {
amount: 24,
unit: 'hours',
}
if (
moment()
.subtract(
passwordResetTokenExpiry.amount,
passwordResetTokenExpiry.unit,
)
.isAfter(user.passwordResetTimestamp)
) {
throw new ValidationError(
`${USER_CONTROLLER} resetPassword: your token has expired`,
)
}
await user.updatePassword(
undefined,
password,
user.passwordResetToken,
{ trx: tr },
)
await user.patch(
{
passwordResetTimestamp: null,
passwordResetToken: null,
},
{ trx: tr },
)
const emailData = passwordUpdate({
email: user.defaultIdentity.email,
})
notify(EMAIL, emailData)
return true
},
{ trx, passedTrxOnly: true },
)
} catch (e) {
logger.error(`${USER_CONTROLLER} resetPassword: ${e.message}`)
throw new Error(e)
}
}
const setDefaultIdentity = async (userId, identityId, options = {}) => {
try {
const { trx } = options
return useTransaction(
async tr => {
const user = await User.findById(
userId,
{
related: 'identities',
},
{ trx: tr },
)
const { identities } = user
const previouslyDefault = identities.find(i => i.isDefault)
if (previouslyDefault && previouslyDefault.id === identityId) {
return user
}
if (previouslyDefault) {
await Identity.patchAndFetchById(
previouslyDefault.id,
{ isDefault: false },
{ trx: tr },
)
}
await Identity.patchAndFetchById(
identityId,
{ isDefault: true },
{ trx: tr },
)
return user
},
{ trx, passedTrxOnly: true },
)
} catch (e) {
logger.error(`${USER_CONTROLLER} setDefaultIdentity: ${e.message}`)
throw new Error(e)
}
}
const getUserTeams = user => {
try {
const { id } = user
return User.getTeams(id)
} catch (e) {
logger.error(`${USER_CONTROLLER} getUserTeams: ${e.message}`)
throw new Error(e)
}
}
module.exports = {
activateUser,
activateUsers,
deactivateUser,
deactivateUsers,
deleteUser,
deleteUsers,
getDisplayName,
getUser,
getUsers,
getUserTeams,
login,
updateUser,
updatePassword,
resetPassword,
resendVerificationEmail,
resendVerificationEmailAfterLogin,
setDefaultIdentity,
sendPasswordResetEmail,
signUp,
verifyEmail,
}