'use strict';
/**
* Plain user functionality in service methods.
* @module service/users
* @license MIT
* @author Kai KRETSCHMANN <kai@kretschmann.consulting>
*/
require('datejs');
const jsonpatch = require('json-patch');
const LoginModel = require('../models/login.js');
const DomainModel = require('../models/domain.js');
const LogauthModel = require('../models/logauth.js');
const bcrypt = require('bcrypt');
const saltRounds = 10;
const log4js = require('log4js');
const logger = log4js.getLogger();
logger.error(`env LogLevel is ${process.env.LOGLEVEL}!`);
logger.level = process.env.LOGLEVEL || /* istanbul ignore next */ 'WARN'; // LCOV_EXCL_LINE
logger.error(`active LogLevel ${logger.level}`);
/**
* Helper function getUserObject.
* @method getUserObject
* @private
* @param {string} username The textual user name
* @returns {Promise} user object or error
*/
function getUserObject (username) {
return new Promise(function (resolve, reject) {
LoginModel.find({
name: username
})
.then(itemFound => {
if (itemFound.length === 1) {
logger.info('Found user');
resolve(itemFound[0]);
} else {
reject(Error('Not found'));
}
})
.catch(err => /* istanbul ignore next */ {
logger.error(`getUser failed: ${err}`);
reject(err);
});
});
}
/**
* List all registered blacklisted domain.
* @method listDomains
* @public
* @returns {Promise} array of domain entries or error
**/
exports.listDomains = function () {
return new Promise(function (resolve, reject) {
logger.debug('In list domains service');
DomainModel.find({})
.then(item => {
resolve(item);
})
.catch(err => /* istanbul ignore next */ {
logger.error(`Not OK: ${err}`);
reject(err);
});
});
};
/**
* Add blacklisted domain.
* @method addDomain
* @public
* @param {string} domainname - Domain name
* @returns {Promise} array with domain added or error
**/
exports.addDomain = function (domainname) {
return new Promise(function (resolve, reject) {
logger.debug('In add domain service');
const tsnow = new Date();
const d = new DomainModel({
name: domainname,
tscreated: tsnow
});
d.save()
.then(item => {
resolve(item);
})
.catch(err => /* istanbul ignore next */ {
logger.error(`Not OK: ${err}`);
reject(err);
});
});
};
/**
* Delete one registered blacklisted domain.
* @method deleteDomain
* @public
* @param {string} domainname - Domain name
* @returns {Promise} OK or error
**/
exports.deleteDomain = function (domainname) {
return new Promise(function (resolve, reject) {
logger.debug('In delete domain service');
DomainModel.deleteOne({
name: domainname
})
.then(item => {
if (item.deletedCount !== 1) {
logger.error('not found, not deleted');
const e = new Error('not found');
e.code = 404;
e.msg = 'not found';
reject(e);
} else {
resolve('OK');
}
})
.catch(err => /* istanbul ignore next */ {
logger.error(`Not OK: ${err}`);
reject(err);
});
});
};
/**
* Check for registered blacklisted domain.
* @method checkDomain
* @public
* @param {string} domainname - Domain name
* @returns {Promise} domain object or null or error
**/
exports.checkDomain = function (domainname) {
return new Promise(function (resolve, reject) {
logger.debug('In check domain service');
DomainModel.find({
name: domainname
})
.then(item => {
if (item.length === 1) {
resolve(item[0]);
} else {
resolve(null);
}
})
.catch(err => /* istanbul ignore next */ {
logger.error(`Not OK: ${err}`);
reject(err);
});
});
};
/**
* List all registered users.
* @method listUsers
* @public
* @returns {Promise} array of user entries or error
**/
exports.listUsers = function () {
return new Promise(function (resolve, reject) {
logger.debug('In list users service');
LoginModel.find({}, {
role: 1,
status: 1,
name: 1,
email: 1,
tscreated: 1
})
.then(item => {
resolve(item);
})
.catch(err => /* istanbul ignore next */ {
logger.error(`Not OK: ${err}`);
reject(err);
});
});
};
/**
* Update user entry with new status
* @method putUserStatus
* @public
* @param {string} id - user id in database
* @param {string} newStatus - user status to change to
* @returns {Promise} updated user object or error
**/
exports.putUserStatus = function (id, newStatus) {
return new Promise(function (resolve, reject) {
logger.debug(`In put user status service ${id} ${newStatus}`);
LoginModel.findOneAndUpdate({
_id: id
}, {
status: newStatus
})
.then(item => {
logger.info(item._id);
resolve(item);
})
.catch(err => /* istanbul ignore next */ {
logger.error(`Not OK: ${err}`);
reject(err);
});
});
};
/**
* Run query over users.
* @method checkGetUserStatus
* @private
* @param {object} resolve
* @param {object} reject
* @param {object} query
*/
function checkGetUserStatus (resolve, reject, query) {
LoginModel.find(query, {
role: 1,
status: 1,
name: 1,
email: 1,
tscreated: 1
})
.then(item => {
if (item.length > 0) {
resolve(item[0]);
} else {
const e = new Error('not found');
e.code = 404;
e.msg = 'not found';
reject(e);
}
})
.catch(err => /* istanbul ignore next */ {
logger.error(`Not OK: ${err}`);
reject(err);
});
}
/**
* Show userdata of given id.
* @method listUser
* @public
* @param {string} id - user id in database
* @returns {Promise} object with user data or error
**/
exports.listUser = function (idUser) {
return new Promise(function (resolve, reject) {
logger.debug('In list user service');
checkGetUserStatus(resolve, reject, {
_id: idUser
});
});
};
/**
* Show userdata of given name.
* @method getUser
* @public
* @param {string} name - user name in database
* @returns {Promise} object with user data or error
**/
exports.getUser = function (name) {
return new Promise(function (resolve, reject) {
logger.debug('In get user service');
checkGetUserStatus(resolve, reject, { name });
});
};
/**
* Change user data by json patch request.
* @method patchUser
* @public
* @param {string} idUser - id of user in database
* @param {string} jpatch - json patch data
* @returns {Promise} object of updated user data or error
**/
exports.patchUser = function (idUser, jpatch) {
return new Promise(function (resolve, reject) {
logger.debug('In patch user service');
LoginModel.find({
_id: idUser
}, {
role: 1,
status: 1,
name: 1,
email: 1,
tscreated: 1
})
.then(item => {
if (item.length > 0) {
const userDoc = item[0];
const patchedUser = jsonpatch.apply(userDoc, jpatch);
patchedUser.save();
resolve(patchedUser);
} else {
const e = new Error('not found');
e.code = 404;
e.msg = 'not found';
reject(e);
}
})
.catch(err => /* istanbul ignore next */ {
logger.error(`Not OK: ${err}`);
reject(err);
});
});
};
/**
* Delete one user from database.
* @method deleteUser
* @public
* @param {string} idUser - the id of the user object
* @returns {Promise} object of user entry for the very last time
**/
exports.deleteUser = function (idUser) {
return new Promise(function (resolve, reject) {
logger.debug('In delete user service');
let e;
let userDoc;
LoginModel.find({
_id: idUser
}, {
role: 1,
status: 1,
name: 1,
email: 1,
tscreated: 1
})
.then(item => {
switch (item.length) {
case 0:
e = new Error('not found');
e.code = 404;
e.msg = 'not found';
reject(e);
break;
case 1:
logger.info('Item found');
userDoc = item[0];
userDoc.status = 'deleted';
userDoc.save()
.then(itemSave => {
logger.info('Updated item saved');
resolve(userDoc);
})
.catch(err => /* istanbul ignore next */ {
logger.error(`Not OK: ${err}`);
reject(err);
});
break;
default:
e = new Error(`More than one user found ${item.length}`);
logger.error(`Not OK: ${e}`);
reject(e);
break;
} // switch
})
.catch(err => /* istanbul ignore next */ {
logger.error(`Not OK: ${err}`);
reject(err);
});
});
};
/**
* Creates an user entry in database.
* @method createUser
* @public
* @param {string} user - json user object
* @returns {Promise} object of created user
**/
exports.createUser = function (user) {
return new Promise(function (resolve, reject) {
logger.debug('In create user service');
const domain = user.email.split('@')[1];
logger.info(`Check domain ${domain}`);
DomainModel.find({
name: domain
})
.then(item => {
if (item.length === 1) {
logger.error(`Domain black listed: ${domain}`);
const e = new Error('not that domain');
e.code = 400;
e.msg = 'not that domain';
reject(e);
} else {
const tsnow = new Date();
bcrypt.hash(user.password, saltRounds, function (err, hash) {
if (err) {
reject(err);
}
const u = new LoginModel({
name: user.username,
passwd: hash,
email: user.email,
role: 'user',
status: 'register',
tscreated: tsnow
});
u.save()
.then(_item2 => {
resolve(u);
})
.catch(errSave => /* istanbul ignore next */ {
logger.error(`Not OK: ${err}`);
reject(errSave);
});
});
}
})
.catch(err => /* istanbul ignore next */ {
logger.error(`Not OK: ${err}`);
reject(err);
});
});
};
/**
* Check login data for user.
* @method checkUser
* @public
* @param {string} name - user name
* @param {string} passwd - clear text password from web form
* @returns {Promise} object of athenticated user or error
**/
exports.checkUser = function (name, passwd) {
return new Promise(function (resolve, reject) {
logger.debug('In check users service');
LoginModel.find({
name,
status: 'active'
})
.then(item => {
if (item.length > 0) {
const pwhash = item[0].passwd;
bcrypt.compare(passwd, pwhash, function (err, result) {
if (err) {
logger.error(`Some error during compare: ${err}`);
reject(err);
}
if (result) {
logger.info('matched');
resolve(item[0]);
} else {
logger.error('Pwd mismatch');
reject(Error('pwd mismatch')); // do not show that detail to the end user, just log it
}
});
} else {
logger.error('No entry found or perhaps not yet activated');
reject(Error('not found')); // do not show that detail to the end user, just log it
}
})
.catch(err => /* istanbul ignore next */ {
logger.error(`Not OK: ${err}`);
reject(err);
});
});
};
/**
* Check if user is activated.
* @method isActiveUser
* @public
* @param {string} uname - user login name
* @returns {Promise} true or error
**/
exports.isActiveUser = function (uname) {
return new Promise(function (resolve, reject) {
logger.debug('In isActiveUser service');
LoginModel.find({
name: uname,
status: 'active'
})
.then(item => {
if (item.length > 0) {
logger.debug('Was found OK');
resolve(true);
} else {
logger.error('No match found');
reject(Error('not found'));
}
})
.catch(err => /* istanbul ignore next */ {
logger.error(`Not OK: ${err}`);
reject(err);
});
});
};
/**
* Helper function roleCheck.
* @method roleCheck
* @private
* @param {string} uname - user login name
* @param {object} item The item found in db
* @param {string} aRoles - Array of strings
* @param {object} resolve callback OK
* @param {object} reject callback bad
* @returns {Promise} true or error
*/
function roleCheck (uname, item, aRoles, resolve, reject) {
logger.info(`Check user ${uname} for roles`);
if (item.length > 0) {
const uRole = item[0].role;
logger.debug(`Check user ${uname} has role ${uRole}`);
if (aRoles.indexOf(uRole) === -1) {
logger.error(`User ${uname} does not have one of the wanted roles ${uRole}`);
reject(Error('not in role'));
}
logger.debug(`OK: User ${uname} has role ${uRole}`);
resolve(true);
} else {
logger.error(`No match for ${uname} found`);
reject(Error('not found'));
}
}
/**
* Check if user has one of the wanted roles.
* @method hasRole
* @public
* @param {string} uname - user login name
* @param {string} aRoles - Array of strings
* @returns {Promise} true or error
**/
exports.hasRole = function (uname, aRoles) {
return new Promise(function (resolve, reject) {
logger.debug('In hasRole service');
LoginModel.find({
name: uname
})
.then(item => {
if (item.length > 0) {
logger.debug('Was found OK');
const uRole = item[0].role;
logger.info(`User ${uname} has role ${uRole}`);
if (aRoles.indexOf(uRole) === -1) {
logger.error('User does not have one of the wanted roles');
reject(Error('not in role'));
} else {
logger.debug(`User ${uname} has wanted role ${uRole}`);
resolve(true);
}
} else {
logger.error('No match found');
reject(Error('not found'));
}
})
.catch(err => /* istanbul ignore next */ {
logger.error(`Not OK: ${err}`);
reject(err);
});
});
};
/**
* Check if active user has one of the wanted roles.
* @method isActiveHasRole
* @public
* @param {string} uname - user login name
* @param {string} aRoles - Array of strings
* @returns {Promise} boolean or error
**/
exports.isActiveHasRole = function (uname, aRoles) {
return new Promise(function (resolve, reject) {
logger.debug('In isActiveHasRole service');
LoginModel.find({
name: uname,
status: 'active'
})
.then(item => {
roleCheck(uname, item, aRoles, resolve, reject);
})
.catch(err => /* istanbul ignore next */ {
logger.error(`Not OK: ${err}`);
reject(err);
});
});
};
/**
* Store valid auth via JWT.
* @method logAuthSuccess
* @public
* @param {string} uname - user login name
* @param {string} myremoteip - Remote IP in IPv4 or IPv6 format, who knows
* @param {number} myvalidity - Timestamp in epoch seconds until when JWT is valid
* @returns {Promise} true or error
**/
exports.logAuthSuccess = function (uname, myremoteip, myvalidity) {
return new Promise(function (resolve, reject) {
logger.debug('In logAuthSuccess service');
getUserObject(uname)
.then(userObject => {
const tsnow = new Date();
const query = {
username: userObject,
remoteip: myremoteip
};
const newdata = {
tssuccess: tsnow,
tsexpire: null,
countexpire: 0,
validity: myvalidity
};
const updatequery = LogauthModel.findOneAndUpdate(query, newdata, { new: true, upsert: true });
updatequery.exec()
.then(_doc => {
logger.info('stored logAuthSuccess');
resolve(true);
})
.catch(err => /* istanbul ignore next */ {
logger.error(err);
reject(err);
});
});
});
};
/**
* Store invalid auth via JWT.
* @method logAuthFailure
* @public
* @param {string} uname - user login name
* @param {string} remoteip - Array of strings
* @returns {Promise} true
**/
exports.logAuthFailure = function (uname, remoteip) {
return new Promise(function (resolve, _reject) {
logger.debug('In logAuthFailure service');
resolve(true);
});
}
/**
* Store expired auth via JWT.
* @method logAuthExpired
* @public
* @param {string} uname - user login name
* @param {string} myremoteip - Array of strings
* @param {number} myvalidity - Timestamp in epoch seconds until when JWT is valid
* @returns {Promise} true or error
**/
exports.logAuthExpired = function (uname, myremoteip, myvalidity) {
return new Promise(function (resolve, reject) {
logger.debug('In logAuthExpired service');
getUserObject(uname)
.then(userObject => {
const tsnow = new Date();
const query = {
username: userObject,
remoteip: myremoteip
};
const newdata = {
tssuccess: null,
tsexpire: tsnow,
validity: myvalidity,
$inc: { countexpire: 1 }
};
const updatequery = LogauthModel.findOneAndUpdate(query, newdata, { new: true, upsert: true });
updatequery.exec()
.then(_doc => {
logger.info('stored logAuthExpired');
resolve(true);
})
.catch(err => /* istanbul ignore next */ {
logger.error(err);
reject(err);
});
});
});
};
/**
* List logauth.
* @method getLogauth
* @public
* @returns {Promise} array of logauth entries or error
**/
exports.getLogauth = function () {
return new Promise(function (resolve, reject) {
logger.debug('In getLogauth service');
LogauthModel.find({}, { _id: 0, __v: 0 })
.populate({ path: 'username', select: '-_id -__v -passwd' })
.then(item => {
resolve(item);
})
.catch(err => /* istanbul ignore next */ {
logger.error(`Not OK: ${err}`);
reject(err);
});
});
};
/**
* export unit testing for test env only
*/
if (process.env.NODE_ENV === 'test') {
exports.roleCheck = roleCheck;
}