server/features/tenant_administration/features/user_manager/middlewares/main/middleware.js

/* eslint-disable security/detect-object-injection */

'use strict';

/**
 * Module dependencies, required for ALL Twyr' modules
 * @ignore
 */

/**
 * Module dependencies, required for this module
 * @ignore
 */
const TwyrBaseMiddleware = require('twyr-base-middleware').TwyrBaseMiddleware;
const TwyrMiddlewareError = require('twyr-middleware-error').TwyrMiddlewareError;

/**
 * @class   Main
 * @extends {TwyrBaseMiddleware}
 * @classdesc The Twyr Web Application Server Tenant Administration Feature Main middleware - manages CRUD for account data.
 *
 *
 */
class Main extends TwyrBaseMiddleware {
	// #region Constructor
	constructor(parent, loader) {
		super(parent, loader);
	}
	// #endregion

	// #region startup/teardown code
	/**
	 * @async
	 * @function
	 * @override
	 * @instance
	 * @memberof ApiService
	 * @name     _setup
	 *
	 * @returns  {null} Nothing.
	 *
	 * @summary  Sets up the broker to manage API exposed by other modules.
	 */
	async _setup() {
		try {
			await super._setup();

			const dbSrvc = this.$dependencies.DatabaseService;
			const self = this; // eslint-disable-line consistent-this

			Object.defineProperty(this, '$TenantModel', {
				'__proto__': null,
				'configurable': true,

				'value': dbSrvc.Model.extend({
					'tableName': 'tenants',
					'idAttribute': 'tenant_id',
					'hasTimestamps': true,

					'users': function() {
						return this.hasMany(self.$TenantUserModel, 'tenant_id');
					}
				})
			});

			Object.defineProperty(this, '$TenantUserModel', {
				'__proto__': null,
				'configurable': true,

				'value': dbSrvc.Model.extend({
					'tableName': 'tenants_users',
					'idAttribute': 'tenant_user_id',
					'hasTimestamps': true,

					'tenant': function() {
						return this.belongsTo(self.$TenantModel, 'tenant_id');
					},

					'user': function() {
						return this.belongsTo(self.$UserModel, 'user_id');
					}
				})
			});

			Object.defineProperty(this, '$UserModel', {
				'__proto__': null,
				'configurable': true,

				'value': dbSrvc.Model.extend({
					'tableName': 'users',
					'idAttribute': 'user_id',
					'hasTimestamps': true,

					'tenantUsers': function() {
						return this.hasMany(self.$TenantUserModel, 'user_id');
					},

					'contacts': function() {
						return this.hasMany(self.$UserContactModel, 'user_id');
					}
				})
			});

			Object.defineProperty(this, '$UserContactModel', {
				'__proto__': null,
				'configurable': true,

				'value': dbSrvc.Model.extend({
					'tableName': 'user_contacts',
					'idAttribute': 'user_contact_id',
					'hasTimestamps': true,

					'user': function() {
						return this.belongsTo(self.$UserModel, 'user_id');
					}
				})
			});

			return null;
		}
		catch(err) {
			throw new TwyrMiddlewareError(`${this.name}::_setup error`, err);
		}
	}

	/**
	 * @async
	 * @function
	 * @override
	 * @instance
	 * @memberof ApiService
	 * @name     _teardown
	 *
	 * @returns  {undefined} Nothing.
	 *
	 * @summary  Deletes the broker that manages API.
	 */
	async _teardown() {
		try {
			delete this.$TenantModel;

			await super._teardown();
			return null;
		}
		catch(err) {
			throw new TwyrMiddlewareError(`${this.name}::_teardown error`, err);
		}
	}
	// #endregion

	// #region Protected methods
	async _registerApis() {
		try {
			const ApiService = this.$dependencies.ApiService;

			await ApiService.add(`${this.name}::searchUsers`, this._searchUsers.bind(this));
			await ApiService.add(`${this.name}::resetUserPassword`, this._resetUserPassword.bind(this));
			await ApiService.add(`${this.name}::getAllTenantUsers`, this._getAllTenantUsers.bind(this));

			await ApiService.add(`${this.name}::getTenantUser`, this._getTenantUser.bind(this));
			await ApiService.add(`${this.name}::createTenantUser`, this._createTenantUser.bind(this));
			await ApiService.add(`${this.name}::updateTenantUser`, this._updateTenantUser.bind(this));

			await ApiService.add(`${this.name}::getUserFromTenantUser`, this._getUserFromTenantUser.bind(this));
			await ApiService.add(`${this.name}::getUser`, this._getUser.bind(this));
			await ApiService.add(`${this.name}::createUser`, this._createUser.bind(this));
			await ApiService.add(`${this.name}::updateUser`, this._updateUser.bind(this));

			await super._registerApis();
			return null;
		}
		catch(err) {
			throw new TwyrMiddlewareError(`${this.name}::_registerApis`, err);
		}
	}

	async _deregisterApis() {
		try {
			const ApiService = this.$dependencies.ApiService;

			await ApiService.remove(`${this.name}::updateUser`, this._updateUser.bind(this));
			await ApiService.remove(`${this.name}::createUser`, this._createUser.bind(this));
			await ApiService.remove(`${this.name}::getUser`, this._getUser.bind(this));
			await ApiService.remove(`${this.name}::getUserFromTenantUser`, this._getUser.bind(this));

			await ApiService.remove(`${this.name}::updateTenantUser`, this._updateTenantUser.bind(this));
			await ApiService.remove(`${this.name}::createTenantUser`, this._createTenantUser.bind(this));
			await ApiService.remove(`${this.name}::getTenantUser`, this._getTenantUser.bind(this));

			await ApiService.remove(`${this.name}::getAllTenantUsers`, this._getAllTenantUsers.bind(this));
			await ApiService.remove(`${this.name}::resetUserPassword`, this._resetUserPassword.bind(this));
			await ApiService.remove(`${this.name}::searchUsers`, this._searchUsers.bind(this));

			await super._deregisterApis();
			return null;
		}
		catch(err) {
			throw new TwyrMiddlewareError(`${this.name}::_deregisterApis`, err);
		}
	}
	// #endregion

	// #region API
	async _searchUsers(ctxt) {
		try {
			const dbSrvc = this.$dependencies.DatabaseService;
			const userList = await dbSrvc.knex.raw(`SELECT user_id AS id, email, first_name, last_name FROM users WHERE user_id NOT IN (SELECT user_id FROM tenants_users WHERE tenant_id = ?) AND email ILIKE ?`, [ctxt.state.tenant.tenant_id, `%${ctxt.query.email}%`]);

			return userList.rows;
		}
		catch(err) {
			throw new TwyrMiddlewareError(`${this.name}::_searchUsers`, err);
		}
	}

	async _resetUserPassword(ctxt) {
		const dbSrvc = this.$dependencies.DatabaseService;

		const userEmail = await dbSrvc.knex.raw(`SELECT email FROM users WHERE user_id = ?`, [ctxt.request.body.user]);
		if(!userEmail.rows.length) throw new TwyrMiddlewareError(`Invalid User: ${ctxt.request.body.user}`);

		const uuid = require('uuid/v4');
		const upash = require('upash');

		if(ctxt.request.body.generate === 'true') { // eslint-disable-line curly
			try {
				const RandomOrg = require('random-org');
				const random = new RandomOrg(this.$config.randomServer);

				const randomPasswordResponse = await random.generateStrings(this.$config.passwordFormat);
				ctxt.request.body.password = randomPasswordResponse.random.data.pop();
			}
			catch(err) {
				console.error(err.message);
				ctxt.request.body.password = uuid().toString().replace(/-/g, '');
			}
		}

		const hashedPassword = await upash.hash(ctxt.request.body.password);
		await dbSrvc.knex.raw(`UPDATE users SET password = ? WHERE user_id = ?`, [hashedPassword, ctxt.request.body.user]);

		const messageOptions = JSON.parse(JSON.stringify(this.$config.resetPasswordMail));
		messageOptions['to'] = userEmail.rows[0]['email'];
		messageOptions['text'] = `Your new password on Twyr is ${ctxt.request.body.password}`;

		const mailerSrvc = this.$dependencies.MailerService;
		const sendMailResult = await mailerSrvc.sendMail(messageOptions);

		if(twyrEnv === 'development' || twyrEnv === 'test') console.log(`Message Options: ${JSON.stringify(messageOptions, null, '\t')}\nSend Mail Result: ${JSON.stringify(sendMailResult, null, '\t')}`);
		return { 'status': true };
	}

	async _getAllTenantUsers(ctxt) {
		try {
			let tenantUserData = await this.$TenantUserModel
			.query(function(qb) {
				qb
				.where({ 'tenant_id': ctxt.state.tenant.tenant_id })
				.andWhere('user_id', '<>', 'ffffffff-ffff-4fff-ffff-ffffffffffff');
			})
			.fetchAll({
				'withRelated': (ctxt.query.include && ctxt.query.include.length) ? ctxt.query.include.split(',').map((related) => { return related.trim(); }) : ['tenant', 'user', 'user.contacts']
			});

			tenantUserData = this.$jsonApiMapper.map(tenantUserData, 'tenant-administration/user-manager/tenant-user', {
				'typeForModel': {
					'tenant': 'tenant-administration/tenant',
					'user': 'tenant-administration/user-manager/user',
					'contacts': 'tenant-administration/user-manager/user-contact'
				},

				'enableLinks': false
			});

			return tenantUserData;
		}
		catch(err) {
			throw new TwyrMiddlewareError(`${this.name}::_getAllTenantUsers`, err);
		}
	}

	async _getTenantUser(ctxt) {
		try {
			let tenantUserData = await this.$TenantUserModel
			.query(function(qb) {
				qb
				.where('tenant_user_id', '=', ctxt.params.tenantUserId)
				.andWhere({ 'tenant_id': ctxt.state.tenant.tenant_id });
			})
			.fetch({
				'withRelated': (ctxt.query.include && ctxt.query.include.length) ? ctxt.query.include.split(',').map((related) => { return related.trim(); }) : ['tenant', 'user', 'user.contacts']
			});

			tenantUserData = this.$jsonApiMapper.map(tenantUserData, 'tenant-administration/user-manager/tenant-user', {
				'typeForModel': {
					'tenant': 'tenant-administration/tenant',
					'user': 'tenant-administration/user-manager/user',
					'contacts': 'tenant-administration/user-manager/user-contact'
				},

				'enableLinks': false
			});

			return tenantUserData;
		}
		catch(err) {
			throw new TwyrMiddlewareError(`${this.name}::_getTenantUser`, err);
		}
	}

	async _createTenantUser(ctxt) {
		try {
			const tenantUser = ctxt.request.body;
			const jsonDeserializedData = await this.$jsonApiDeserializer.deserializeAsync(tenantUser);

			jsonDeserializedData['tenant_user_id'] = jsonDeserializedData['id'];
			delete jsonDeserializedData.id;
			delete jsonDeserializedData.created_at;
			delete jsonDeserializedData.updated_at;

			Object.keys(tenantUser.data.relationships || {}).forEach((relationshipName) => {
				if(!tenantUser.data.relationships[relationshipName].data) {
					delete jsonDeserializedData[relationshipName];
					return;
				}

				if(!tenantUser.data.relationships[relationshipName].data.id) {
					delete jsonDeserializedData[relationshipName];
					return;
				}

				jsonDeserializedData[`${relationshipName}_id`] = tenantUser.data.relationships[relationshipName].data.id;
			});

			const savedRecord = await this.$TenantUserModel
				.forge()
				.save(jsonDeserializedData, {
					'method': 'insert',
					'patch': false
				});

			return {
				'data': {
					'type': tenantUser.data.type,
					'id': savedRecord.get('tenant_user_id')
				}
			};
		}
		catch(err) {
			throw new TwyrMiddlewareError(`${this.name}::_createTenantUser`, err);
		}
	}

	async _updateTenantUser(ctxt) {
		try {
			const TenantUserRecord = new this.$TenantUserModel({
				'tenant_user_id': ctxt.params.tenantUserId
			});

			const tenantUserData = await TenantUserRecord
			.query(function(qb) {
				qb.where({ 'tenant_id': ctxt.state.tenant.tenant_id });
			})
			.fetch();

			if(!tenantUserData) throw new Error(`Invalid record id`);

			const jsonDeserializedData = await this.$jsonApiDeserializer.deserializeAsync(ctxt.request.body);

			jsonDeserializedData['tenant_user_id'] = jsonDeserializedData['id'];
			delete jsonDeserializedData['id'];

			const savedRecord = await tenantUserData
			.save(jsonDeserializedData, {
				'method': 'update',
				'patch': true
			});

			return {
				'data': {
					'type': ctxt.request.body.data.type,
					'id': savedRecord.get('tenant_user_id')
				}
			};
		}
		catch(err) {
			throw new TwyrMiddlewareError(`${this.name}::_updateTenantUser`, err);
		}
	}

	async _getUserFromTenantUser(ctxt) {
		try {
			const TenantUserRecord = new this.$TenantUserModel({
				'tenant_user_id': ctxt.params.tenantUserId
			});

			const tenantUserData = await TenantUserRecord
			.query(function(qb) {
				qb.where({ 'tenant_id': ctxt.state.tenant.tenant_id });
			})
			.fetch();

			const UserRecord = new this.$UserModel({
				'user_id': tenantUserData.get('user_id')
			});

			let userData = await UserRecord.fetch();
			userData = this.$jsonApiMapper.map(userData, 'tenant-administration/user-manager/users', {
				'enableLinks': false
			});

			delete userData.data.attributes.password;
			userData.data.attributes['middle_names'] = userData.data.attributes['middle_names'] || '';

			return userData;
		}
		catch(err) {
			throw new TwyrMiddlewareError(`${this.name}::_getUserFromTenantUser`, err);
		}
	}

	async _getUser(ctxt) {
		try {
			const UserRecord = new this.$UserModel({
				'user_id': ctxt.params.userId
			});

			let userData = await UserRecord.fetch({
				'withRelated': (ctxt.query.include && ctxt.query.include.length) ? ctxt.query.include.split(',').map((related) => { return related.trim(); }) : ['contacts']
			});

			userData = this.$jsonApiMapper.map(userData, 'tenant-administration/user-manager/users', {
				'typeForModel': {
					'contacts': 'tenant-administration/user-manager/user-contact'
				},

				'enableLinks': false
			});

			delete userData.data.attributes.password;
			userData.data.attributes['middle_names'] = userData.data.attributes['middle_names'] || '';

			return userData;
		}
		catch(err) {
			throw new TwyrMiddlewareError(`${this.name}::_getUser`, err);
		}
	}

	async _createUser(ctxt) {
		try {
			const user = ctxt.request.body;
			const jsonDeserializedData = await this.$jsonApiDeserializer.deserializeAsync(user);

			const upash = require('upash');
			const hashedPassword = await upash.hash(jsonDeserializedData['password']);

			jsonDeserializedData['user_id'] = jsonDeserializedData.id;
			jsonDeserializedData['password'] = hashedPassword;
			delete jsonDeserializedData['id'];

			const savedRecord = await this.$UserModel
				.forge()
				.save(jsonDeserializedData, {
					'method': 'insert',
					'patch': false
				});

			return {
				'data': {
					'type': user.data.type,
					'id': savedRecord.get('user_id')
				}
			};
		}
		catch(err) {
			throw new TwyrMiddlewareError(`${this.name}::_createUser`, err);
		}
	}

	async _updateUser(ctxt) {
		try {
			const user = ctxt.request.body;

			delete user.data.relationships;
			delete user.included;

			const jsonDeserializedData = await this.$jsonApiDeserializer.deserializeAsync(user);
			jsonDeserializedData['user_id'] = jsonDeserializedData.id;

			delete jsonDeserializedData.id;
			delete jsonDeserializedData.email;
			delete jsonDeserializedData.created_at;
			delete jsonDeserializedData.updated_at;

			const savedRecord = await this.$UserModel
				.forge()
				.save(jsonDeserializedData, {
					'method': 'update',
					'patch': true
				});

			const cacheSrvc = this.$dependencies['CacheService'];
			await cacheSrvc.delAsync(`twyr!webapp!user!${jsonDeserializedData['user_id']}!basics`);

			return {
				'data': {
					'type': user.data.type,
					'id': savedRecord.get('user_id')
				}
			};
		}
		catch(err) {
			throw new TwyrMiddlewareError(`${this.name}::_updateUser`, err);
		}
	}
	// #endregion

	// #region Properties
	/**
	 * @override
	 */
	get dependencies() {
		return ['MailerService'].concat(super.dependencies);
	}

	/**
	 * @override
	 */
	get basePath() {
		return __dirname;
	}
	// #endregion
}

exports.middleware = Main;