user-managements-node-server
Version:
starter for express node server with user managements, authentication authorization
638 lines (591 loc) • 19.4 kB
JavaScript
import initConn from '../entities/conn'
import httpStatus from 'http-status'
import sequelize from 'sequelize'
import bcrypt from 'bcrypt-nodejs'
import {
THIRDPARTY,
ACTION_VERIFICATIONS,
VERBAL_CODE,
POLICY_CODE_MAPPER,
EVENTS,
VERIFY_USER_BY } from '../consts'
import { isValidActionId, isValidUsernameAndPassword, isValidPasswords } from '../utils/validator'
import { last, first } from 'lodash'
import { getToken } from '../utils/tokenizer'
import HttpError from '../utils/HttpError'
import { extract } from '../utils/SequelizeHelper'
import { getEncryptedPasswordAndSalt, verifyPassword } from '../utils/credentials'
const init = ({ config, logger, _3rdPartyProviders, dal, emit }) =>{
const EmailService = {} //from '../services/email-service'
const emailVerificationTo = {
[VERIFY_USER_BY.ADMIN_EMAIL_LINK]: VERBAL_CODE.USER_CREATED_EMAIL_VERIFICATION_SENT_TO_ADMIN,
[VERIFY_USER_BY.EMAIL_LINK]: VERBAL_CODE.USER_CREATED_EMAIL_VERIFICATION_SENT_TO_USER,
[VERIFY_USER_BY.AUTO]: VERBAL_CODE.USER_CREATE_AND_AUTO_VERIFY
}
const isAutoValid =
config.verifyUserBy === VERIFY_USER_BY.AUTO
const is3rdPartyAutoValid =
config.verify3rdPartyUserBy === VERIFY_USER_BY.AUTO
const { Users, ActionVerifications, Tokens } = dal
const conn = initConn({ config: config.database })
const setActionVerification = ({ user, actionType }, { returning }) => (
ActionVerifications
.findOne({ where: { username: user.username, actionType, deleted: false } })
.then((exist) => (
exist
? exist
: ActionVerifications.create({ username: user.username, actionType }, { returning })
))
.then(extract)
.then(({ actionId }) => ({ ...user, actionId }))
)
const getActivationActionVerification = user => (
setActionVerification({
user,
actionType: ACTION_VERIFICATIONS.ACTIVATE_USER
},
{ returning: true })
)
const getForgotPasswordActionVerification = user => (
setActionVerification({
user,
actionType: ACTION_VERIFICATIONS.FORGORT_PASSWORD
},
{ returning: true })
)
const signUp = ({ username, password }) => (
isValidUsernameAndPassword({ username, password })
.catch(validation => {
logger.error(validation)
const {
isValidPassword,
isValidUsername
} = validation
const code = POLICY_CODE_MAPPER[`${isValidUsername}-${isValidPassword}`]
throw new HttpError(httpStatus.BAD_REQUEST, code)
})
.then(() => getEncryptedPasswordAndSalt(password))
.then( ({ password, salt }) => ({
password,
salt,
username,
isValid: isAutoValid
}))
.then((user) => (
Users.create(user)
.then(extract)
.catch(
sequelize.UniqueConstraintError,
error => {
logger.error(error)
throw new HttpError(httpStatus.CONFLICT, VERBAL_CODE.USERNAME_ALREADY_EXIST)
})
))
.then(({ password, ...user }) => user)
.then(user => {
if(isAutoValid){
return user
}
return(
getActivationActionVerification(user)
)
})
.then((info) => (
emit(EVENTS.USER_CREATED, info)
))
.then(() => {
logger
.trace(`User verify by: ${emailVerificationTo[config.verifyUserBy]}`)
return {
httpStatusCode: httpStatus.CREATED,
message: httpStatus[httpStatus.CREATED]
}
})
.catch((error) => {
logger.error(error)
throw error
})
)
const verify = ({ actionId }) => {
logger
.trace(`Verify user by action (${actionId})`)
return (
isValidActionId({ actionId })
.then(() => conn.transaction())
.then(transaction => {
return ActionVerifications
.findOne({ where: { actionId, actionType: ACTION_VERIFICATIONS.ACTIVATE_USER } })
.then((response) => {
if (!response) {
throw new HttpError(httpStatus.FORBIDDEN, VERBAL_CODE.ACTION_VERIFICATION_DOES_NOT_EXIST)
}
return response
})
.then(extract)
.then((actionVerification) => {
if (actionVerification.deleted) {
throw new HttpError(httpStatus.FORBIDDEN, VERBAL_CODE.ACTION_VERIFICATION_OBSOLETE)
}
return actionVerification
})
.then(({ actionId }) => (
ActionVerifications
.update({ deleted: true }, { where: { actionId }, returning: true })
.then((actionVerificationUpdated) => {
return last(actionVerificationUpdated)
})
.then(effected => {
if(!effected){
throw new HttpError(httpStatus.INTERNAL_SERVER_ERROR, VERBAL_CODE.DELETE_ACTION_VERIFICATION_FAILED)
}
return ActionVerifications.findOne({ where: { actionId }})
})
.then(extract)
.then(({ username }) => ({
username,
transaction
}))
)
)
})
.then(({ username, transaction }) => (
Users
.findOne({ where: { username, isValid: false } })
.then(user => {
if (!user) {
throw new HttpError(httpStatus.FORBIDDEN, VERBAL_CODE.USER_DOES_NOT_EXIST)
}
return user
})
.then(extract)
.then((user) => (
{
user,
transaction
}
))
))
.then(({ user, transaction }) => (
Users
.update({ isValid: true }, { where: { id: user.id }, transaction, returning: true })
.then((userMetadata) => ({
effected: !!userMetadata[0],
transaction,
username: user.username
}))
))
.then(({ transaction, username }) => {
transaction
.commit()
const logMessage =
config.verifyUserBy
? VERBAL_CODE.USER_VERIFIED_BY_ACTIVATION_LINK
: VERBAL_CODE.USER_VERIFIED_BY_ACTIVATION_LINK_ADMIN
logger.trace(logMessage)
logger.trace(`User (${username}) activated.`)
emit(EVENTS.USER_APPROVED, { username, admin: config.adminEmail })
return {
httpStatusCode: httpStatus.OK
}
})
.catch(error => {
const { code } = error
logger.error(code || error)
throw error
})
)
}
const isThirdpartySupprted = (thirdParty) => (
new Promise((resolve, reject) => {
if(!_3rdPartyProviders[thirdParty]){
logger.error(`${thirdParty} unsupported, if you wish for support extends providers api`)
reject(new HttpError(httpStatus[httpStatus.BAD_REQUEST], httpStatus.BAD_REQUEST))
return
}
resolve(true)
})
)
const signInViaThirdparty = ({ thirdParty, username, password }) => {
const logPrefix = `User (${username}) - signInViaThirdparty (${thirdParty}) ->`
logger
.trace(`${logPrefix} requested.`)
return isThirdpartySupprted(thirdParty)
.then(() => verifyThirdPartyUser({ username, password, thirdParty }))
.then(user => {
if(!user.isValid){
logger
.error(`${VERBAL_CODE.INVALID_USERNAME_OR_TOKEN} - ${username}`)
throw new HttpError(
httpStatus.UNAUTHORIZED,
VERBAL_CODE.INVALID_USERNAME_OR_TOKEN
)
}
return user
})
.then((user) => (
signUpThirdPartyUser({
...user,
thirdParty,
password
})
))
}
const signInUser = ({ username, password }) => {
const logPrefix = `User (${username}) - signInUser ->`
logger
.trace(`${logPrefix} requested.`)
return (
Users
.findOne({ where: { username } })
.then((response) => {
if (!response) {
throw VERBAL_CODE.USER_DOES_NOT_EXIST
}
return response
})
.then(extract)
.then((user) => {
if(!user.isValid){
throw VERBAL_CODE.USER_IS_NOT_VALID
}
return user
})
.then((user) => (
verifyPassword({ password, encryptedPassword: user.password, salt: user.salt })
.then(() => user)
.catch((error) => {
logger.error(error)
throw VERBAL_CODE.INVALID_PASSWORD
})
))
.catch(error => {
logger.error(error)
throw new HttpError(httpStatus.UNAUTHORIZED)
})
)
}
const signIn = ({ username, password, thirdParty, clientInfo, userAgentIdentity }) => {
return isValidUsernameAndPassword({ username, password })
.then(() => (
config.loginWithThirdParty && thirdParty ?
signInViaThirdparty({ thirdParty, username, password, userAgentIdentity }) :
signInUser({ username, password, userAgentIdentity })
))
.then(user => {
if(!user.isValid){
logger.error(`User ${user.username} is not valid.`)
throw new HttpError(httpStatus.UNAUTHORIZED)
}
return user
})
.then(user => generateUserToken({ user, userAgentIdentity, ip: clientInfo.ip }))
}
const loginVia3rdParty = ({ username, token, thirdParty }) => {
return (
_3rdPartyProviders[thirdParty].verify({ token })
)
}
const thirdPartyProp = {
[THIRDPARTY.FACEBOOK]: 'facebookToken',
[THIRDPARTY.GOOGLE]: 'googleToken'
}
const createUser = (userData) => (
Users
.create({
...userData,
password: bcrypt.hashSync(userData.password)
})
.then(extract)
)
const updateUser = (params, condition, transaction) => (
Users
.update(
params,
{
where: condition,
returning: true
},
{ transaction }
)
.then((updated) => first(last(updated)))
.then(extract)
)
const signUpThirdPartyUser = ({ thirdParty, password, email: username, profilePhoto, name: fullName }) => (
Users
.findOne({ where: { username }, returning: true })
.then(user => (
user ?
updateUser({
[thirdPartyProp[thirdParty]]: password,
profilePhoto
}, { username }) :
createUser({ username, password, fullName, [thirdPartyProp[thirdParty]]: password, profilePhoto, isValid: is3rdPartyAutoValid })
.then((user) => {
const { username, fullName } = user
// sendNotificationToAdminWhenUserCreated({ username, admin: config.adminEmail, fullName })
return user
})
)
)
)
const verifyThirdPartyUser = ({ username, password: token, thirdParty }) => {
return (
!!_3rdPartyProviders[thirdParty]
&& loginVia3rdParty({ username, token, thirdParty })
)
}
const getUserInfo = ({ id }) => (
Users.findOne({ where: { id } })
.then(extract)
.then(({ username, createdAt, updatedAt, profilePhoto }) => (
{ username, createdAt, updatedAt, profilePhoto }
))
.catch(error => {
logger
.error(error)
throw error
})
)
const generateUserToken = ({ user, userAgentIdentity, ip }) => {
logger
.trace(`User (${user.username}) -> generateUserToken requested.`)
return new Promise((resolve, reject) => (
getToken(userAgentIdentity, config.tokenHash)
.then(({ token }) => (
Tokens
.findOne({ where: { userAgentIdentity, userId: user.id } })
.then(extract)
.then(tokenInfo => {
if(tokenInfo){
return (
Tokens
.update(
{ token },
{ where: { userAgentIdentity, userId: user.id } },
{returning: true }
)
)
}
return (
Tokens
.create(
{ token ,userAgentIdentity, userId: user.id, ip },
{returning: true }
)
.then(extract)
)
})
.then((response) => {
response ?
resolve({ token }) :
reject({ message: VERBAL_CODE.INVALID_USERNAME_OR_PASSWORD })
})
))
))
}
const forgotPassword = ({ username }) => {
logger.trace(`User (${username}) - forgotPassword`)
return (
Users
.findOne({ where: { username }, returning: true })
.then(user => {
if (!user) {
logger
.error(`User doesn't exist (${username}).`)
throw new HttpError(httpStatus.NOT_FOUND)
}
return user
})
.then(extract)
.then((user) => {
if (!user.isValid) {
logger
.error(`User is not valid or not activated yet (${username}).`)
throw new HttpError (httpStatus.FORBIDDEN)
}
return user
})
.then(getForgotPasswordActionVerification)
// .then(sendResetPasswordEmail)
.then(() => {
logger
.trace(`Reset link sent to user (${username}).`)
return {
httpStatusCode: httpStatus.OK,
message: VERBAL_CODE.RESTORE_PASSWORD_LINK_SENT_TO_USER_IS_EMAIL
}
})
)
}
const isPasswordsEqual = ({ password, confirmPassword }) => (
new Promise((resolve, reject) => {
password === confirmPassword
? resolve()
: reject(
new HttpError(
httpStatus.BAD_REQUEST,
VERBAL_CODE.CONFIRM_PASSWORD_NOT_EQUAL_TO_PASSWORD
)
)
})
)
const updateActionVerificationRequest = transaction => ({ actionId }) => (
ActionVerifications
.update(
{ deleted: true },
{
where:
{ actionId, actionType: ACTION_VERIFICATIONS.FORGORT_PASSWORD },
returning: true,
transaction
}
)
.then((actionVerification) => {
const affected =
[].concat(actionVerification)
.filter( x => x)
logger.trace(`ActionVerification updated: ${affected.join(',')}`)
return affected
})
.then(() => ActionVerifications.findOne(
{
where:
{ actionId, actionType: ACTION_VERIFICATIONS.FORGORT_PASSWORD }
}
))
.then(extract)
.then((actionVerification) => {
return {
actionVerification,
transaction
}
})
)
const changeUserPassword = ({ id, password, transaction }) => (
getEncryptedPasswordAndSalt(password)
.then(({ password, salt }) => (
Users
.update(
{ password, salt},
{
where: { id },
transaction,
returning: true
}
)
))
)
const changePassword = ({ actionId, password, confirmPassword }) => (
isValidPasswords({ password, confirmPassword })
.then(isPasswordsEqual)
.catch(error => {
logger.error(`Invalid password/confirmPassword ${error.code}`)
throw error
})
.then(() =>
isValidActionId({ actionId })
.then(() => conn.transaction())
.then(transaction => (
ActionVerifications
.findOne({
where: {
actionId,
actionType: ACTION_VERIFICATIONS.FORGORT_PASSWORD
}
})
.then(actionVerification => {
if(!actionVerification){
logger.error(VERBAL_CODE.ACTION_VERIFICATION_DOES_NOT_EXIST)
throw new HttpError(
httpStatus.FORBIDDEN,
httpStatus[httpStatus.FORBIDDEN]
)
}
return actionVerification
})
.then(extract)
.then(actionVerification => {
if(actionVerification.deleted){
logger.error(VERBAL_CODE.ACTION_VERIFICATION_OBSOLETE)
throw new HttpError(
httpStatus.FORBIDDEN,
httpStatus[httpStatus.FORBIDDEN]
)
}
return actionVerification
})
.then(updateActionVerificationRequest(transaction))
.then(({ actionVerification, transaction }) => (
Users
.findOne({ where: { username: actionVerification.username } })
.then(user => {
if(!user){
logger.error(VERBAL_CODE.USER_DOES_NOT_EXIST)
throw new HttpError(
httpStatus.FORBIDDEN,
httpStatus[httpStatus.FORBIDDEN]
)
}
return user
})
.then(extract)
.then(user => {
if(!user.isValid){
logger.error(VERBAL_CODE.USER_IS_NOT_VALID)
throw new HttpError(
httpStatus.FORBIDDEN,
httpStatus[httpStatus.FORBIDDEN]
)
}
return user
})
.then(user => ({ user, transaction }))
))
.then(({ user, transaction }) => (
changeUserPassword({ id: user.id, password, transaction})
.then(() => transaction.commit() )
))
.then(() => ({
httpStatusCode: httpStatus.OK,
message: VERBAL_CODE.PASSWORD_SUCCESSFULLY_CHANGED
}))
.catch((error) => {
transaction.rollback()
logger.error(`Error: ${error.httpStatusCode} - ${error.code}`)
logger.error(error)
throw error
})
))
)
)
const signOut = ({ username, id, userAgentIdentity }) => {
return Tokens
.update({ token: null }, { where: { userAgentIdentity, userId: id }, returning: true })
.then((response) => {
if (!response) {
throw `Update user (${username}) in db failed`
}
return response
})
.then(() => ({
httpStatusCode: httpStatus.OK
}))
.catch(error => {
logger.error(error)
throw new HttpError(
httpStatus.UNAUTHORIZED,
httpStatus[httpStatus.UNAUTHORIZED]
)
})
}
return {
signUp,
verify,
signIn,
forgotPassword,
changePassword,
getUserInfo,
signOut
}
}
export default init