openhim-core
Version:
The OpenHIM core application that provides logging and routing of http requests
390 lines (329 loc) • 13 kB
JavaScript
import logger from 'winston'
import moment from 'moment'
import atna from 'atna-audit'
import os from 'os'
import { UserModelAPI } from '../model/users'
import * as authorisation from './authorisation'
import * as contact from '../contact'
import { config } from '../config'
import * as utils from '../utils'
import * as auditing from '../auditing'
import crypto from 'crypto'
import { promisify } from 'util'
config.newUserExpiry = config.get('newUserExpiry')
config.userPasswordResetExpiry = config.get('userPasswordResetExpiry')
config.alerts = config.get('alerts')
const himSourceID = config.get('auditing').auditEvents.auditSourceID
/*
* Get authentication details
*/
export async function authenticate (ctx, email) {
email = unescape(email)
try {
const user = await UserModelAPI.findOne({ email: utils.caseInsensitiveRegex(email) })
if (!user) {
utils.logAndSetResponse(ctx, 404, `Could not find user by email ${email}`, 'info')
// Audit unknown user requested
let audit = atna.construct.userLoginAudit(atna.constants.OUTCOME_SERIOUS_FAILURE, himSourceID, os.hostname(), email)
audit = atna.construct.wrapInSyslog(audit)
return auditing.sendAuditEvent(audit, () => logger.debug('Processed internal audit'))
} else {
ctx.body = {
salt: user.passwordSalt,
ts: new Date()
}
}
} catch (e) {
return utils.logAndSetResponse(ctx, 500, `Error during authentication ${e}`, 'error')
}
}
/**
* Reset password Functions
*/
const passwordResetPlainMessageTemplate = (firstname, setPasswordLink) => `\
<---------- Existing User - Reset Password ---------->
Hi ${firstname},
A request has been made to reset your password on the OpenHIM instance running on ${config.alerts.himInstance}
Follow the below link to reset your password and log into OpenHIM Console
${setPasswordLink}
<---------- Existing User - Reset Password ---------->\
`
const passwordResetHtmlMessageTemplate = (firstname, setPasswordLink) => `\
<h1>Reset OpenHIM Password</h1>
<p>Hi ${firstname},<br/><br/>A request has been made to reset your password on the OpenHIM instance running on ${config.alerts.himInstance}</p>
<p>Follow the below link to set your password and log into OpenHIM Console</p>
<p>${setPasswordLink}</p>\
`
function generateRandomToken () {
return crypto.randomBytes(16).toString('hex')
}
/*
* update user token/expiry and send new password email
*/
export async function userPasswordResetRequest (ctx, email) {
email = unescape(email)
if (email === 'root@openhim.org') {
ctx.body = 'Cannot request password reset for \'root@openhim.org\''
ctx.status = 403
return
}
// Generate the new user token here
// set expiry date = true
const token = generateRandomToken()
const { duration, durationType } = config.userPasswordResetExpiry
const expiry = moment().add(duration, durationType).utc().format()
const updateUserTokenExpiry = {
token,
tokenType: 'existingUser',
expiry
}
try {
const user = await UserModelAPI.findOneAndUpdate({ email: utils.caseInsensitiveRegex(email) }, updateUserTokenExpiry)
if (!user) {
ctx.body = `Tried to request password reset for invalid email address: ${email}`
ctx.status = 404
logger.info(`Tried to request password reset for invalid email address: ${email}`)
return
}
const { consoleURL } = config.alerts
const setPasswordLink = `${consoleURL}/#!/set-password/${token}`
// Send email to user to reset password
const plainMessage = passwordResetPlainMessageTemplate(user.firstname, setPasswordLink)
const htmlMessage = passwordResetHtmlMessageTemplate(user.firstname, setPasswordLink)
const sendEmail = promisify(contact.contactUser)
const sendEmailError = await sendEmail('email', email, 'OpenHIM Console Password Reset', plainMessage, htmlMessage)
if (sendEmailError) {
utils.logAndSetResponse(ctx, 500, `Could not send email to user via the API ${sendEmailError}`, 'error')
return
}
logger.info('The email has been sent to the user')
ctx.body = 'Successfully set user token/expiry for password reset.'
ctx.status = 201
return logger.info(`User updated token/expiry for password reset ${email}`)
} catch (error) {
utils.logAndSetResponse(ctx, 500, `Could not update user with email ${email} via the API ${error}`, 'error')
}
}
/**
*New User Set Password Functions
*/
// get the new user details
export async function getUserByToken (ctx, token) {
token = unescape(token)
try {
const projectionRestriction = {
email: 1,
firstname: 1,
surname: 1,
msisdn: 1,
token: 1,
tokenType: 1,
locked: 1,
expiry: 1,
_id: 0
}
const result = await UserModelAPI.findOne({ token }, projectionRestriction)
if (!result) {
ctx.body = `User with token ${token} could not be found.`
ctx.status = 404
} else if (moment(result.expiry).isBefore(moment())) {
// user- set password - expired
ctx.body = `Token ${token} has expired`
ctx.status = 410
} else {
ctx.body = result
}
} catch (e) {
utils.logAndSetResponse(ctx, 500, `Could not find user with token ${token} via the API ${e}`, 'error')
}
}
// update the password/details for the new user
export async function updateUserByToken (ctx, token) {
let userDataExpiry
token = unescape(token)
const userData = ctx.request.body
try {
// first try get new user details to check expiry date
userDataExpiry = await UserModelAPI.findOne({ token })
if (!userDataExpiry) {
ctx.body = `User with token ${token} could not be found.`
ctx.status = 404
return
} else if (moment(userDataExpiry.expiry).isBefore(moment())) {
// new user- set password - expired
ctx.body = `User with token ${token} has expired to set their password.`
ctx.status = 410
return
}
} catch (error) {
utils.logAndSetResponse(ctx, 500, `Could not find user with token ${token} via the API ${error}`, 'error')
return
}
// check to make sure 'msisdn' isnt 'undefined' when saving
let msisdn = null
if (userData.msisdn) {
msisdn = userData.msisdn
}
// construct user object to prevent other properties from being updated
const userUpdateObj = {
token: null,
tokenType: null,
expiry: null,
passwordAlgorithm: userData.passwordAlgorithm,
passwordSalt: userData.passwordSalt,
passwordHash: userData.passwordHash
}
if (userDataExpiry.tokenType === 'newUser') {
userUpdateObj.firstname = userData.firstname
userUpdateObj.surname = userData.surname
userUpdateObj.locked = false
userUpdateObj.msisdn = msisdn
}
try {
await UserModelAPI.findOneAndUpdate({ token }, userUpdateObj)
ctx.body = 'Successfully set new user password.'
return logger.info(`User updated by token ${token}`)
} catch (error) {
return utils.logAndSetResponse(ctx, 500, `Could not update user with token ${token} via the API ${error}`, 'error')
}
}
/**
New User Set Password Functions
*/
const plainMessageTemplate = (firstname, setPasswordLink) => `\
<---------- New User - Set Password ---------->
Hi ${firstname},
A profile has been created for you on the OpenHIM instance running on ${config.alerts.himInstance}
Follow the below link to set your password and log into OpenHIM Console
${setPasswordLink}
<---------- New User - Set Password ---------->\
`
const htmlMessageTemplate = (firstname, setPasswordLink) => `\
<h1>New OpenHIM Profile</h1>
<p>Hi ${firstname},<br/><br/>A profile has been created for you on the OpenHIM instance running on ${config.alerts.himInstance}</p>
<p>Follow the below link to set your password and log into OpenHIM Console</p>
<p>${setPasswordLink}</p>\
`
/*
* Adds a user
*/
export async function addUser (ctx) {
// Test if the user is authorised
if (!authorisation.inGroup('admin', ctx.authenticated)) {
utils.logAndSetResponse(ctx, 403, `User ${ctx.authenticated.email} is not an admin, API access to addUser denied.`, 'info')
return
}
const userData = ctx.request.body
// Generate the new user token here
// set locked = true
// set expiry date = true
const token = generateRandomToken()
userData.token = token
userData.tokenType = 'newUser'
userData.locked = true
userData.email = userData.email.toLowerCase()
const { duration, durationType } = config.newUserExpiry
userData.expiry = moment().add(duration, durationType).utc().format()
const consoleURL = config.alerts.consoleURL
const setPasswordLink = `${consoleURL}/#!/set-password/${token}`
try {
const user = new UserModelAPI(userData)
await user.save()
// Send email to new user to set password
const plainMessage = plainMessageTemplate(userData.firstname, setPasswordLink)
const htmlMessage = htmlMessageTemplate(userData.firstname, setPasswordLink)
contact.contactUser('email', userData.email, 'OpenHIM Console Profile', plainMessage, htmlMessage, (err) => {
if (err) {
return logger.error(`The email could not be sent to the user via the API ${err}`)
} else {
return logger.info('The email has been sent to the new user')
}
})
ctx.body = 'User successfully created'
ctx.status = 201
logger.info(`User ${ctx.authenticated.email} created user ${userData.email}`)
} catch (e) {
utils.logAndSetResponse(ctx, 500, `Could not add user via the API ${e}`, 'error')
}
}
/*
* Retrieves the details of a specific user
*/
export async function getUser (ctx, email) {
email = unescape(email)
// Test if the user is authorised, allow a user to fetch their own details
if (!authorisation.inGroup('admin', ctx.authenticated) && (ctx.authenticated.email !== email)) {
utils.logAndSetResponse(ctx, 403, `User ${ctx.authenticated.email} is not an admin, API access to getUser denied.`, 'info')
return
}
try {
const result = await UserModelAPI.findOne({ email: utils.caseInsensitiveRegex(email) })
if (!result) {
ctx.body = `User with email ${email} could not be found.`
ctx.status = 404
} else {
ctx.body = result
}
} catch (e) {
utils.logAndSetResponse(ctx, 500, `Could not get user via the API ${e}`, 'error')
}
}
export async function updateUser (ctx, email) {
email = unescape(email)
// Test if the user is authorised, allow a user to update their own details
if (!authorisation.inGroup('admin', ctx.authenticated) && (ctx.authenticated.email !== email)) {
utils.logAndSetResponse(ctx, 403, `User ${ctx.authenticated.email} is not an admin, API access to updateUser denied.`, 'info')
return
}
const userData = ctx.request.body
// reset token/locked/expiry when user is updated and password supplied
if (userData.passwordAlgorithm && userData.passwordHash && userData.passwordSalt) {
userData.token = null
userData.tokenType = null
userData.locked = false
userData.expiry = null
}
// Don't allow a non-admin user to change their groups
if ((ctx.authenticated.email === email) && !authorisation.inGroup('admin', ctx.authenticated)) { delete userData.groups }
// Ignore _id if it exists (update is by email)
if (userData._id) { delete userData._id }
try {
await UserModelAPI.findOneAndUpdate({ email: utils.caseInsensitiveRegex(email) }, userData)
ctx.body = 'Successfully updated user.'
logger.info(`User ${ctx.authenticated.email} updated user ${userData.email}`)
} catch (e) {
utils.logAndSetResponse(ctx, 500, `Could not update user ${email} via the API ${e}`, 'error')
}
}
export async function removeUser (ctx, email) {
// Test if the user is authorised
if (!authorisation.inGroup('admin', ctx.authenticated)) {
utils.logAndSetResponse(ctx, 403, `User ${ctx.authenticated.email} is not an admin, API access to removeUser denied.`, 'info')
return
}
email = unescape(email)
// Test if the user is root@openhim.org
if (email === 'root@openhim.org') {
utils.logAndSetResponse(ctx, 403, 'User root@openhim.org is OpenHIM root, User cannot be deleted through the API', 'info')
return
}
try {
await UserModelAPI.findOneAndRemove({ email: utils.caseInsensitiveRegex(email) })
ctx.body = `Successfully removed user with email ${email}`
logger.info(`User ${ctx.authenticated.email} removed user ${email}`)
} catch (e) {
utils.logAndSetResponse(ctx, 500, `Could not remove user ${email} via the API ${e}`, 'error')
}
}
export async function getUsers (ctx) {
// Test if the user is authorised
if (!authorisation.inGroup('admin', ctx.authenticated)) {
utils.logAndSetResponse(ctx, 403, `User ${ctx.authenticated.email} is not an admin, API access to getUsers denied.`, 'info')
return
}
try {
ctx.body = await UserModelAPI.find()
} catch (e) {
utils.logAndSetResponse(ctx, 500, `Could not fetch all users via the API ${e}`, 'error')
}
}