server/components/session/component.js

'use strict';

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

/**
 * Module dependencies, required for this module
 * @ignore
 */
const TwyrBaseComponent = require('twyr-base-component').TwyrBaseComponent;
const TwyrCompError = require('twyr-component-error').TwyrComponentError;

/**
 * @class   Session
 * @extends {TwyrBaseComponent}
 * @classdesc The Twyr Web Application Server Session component - manages login/logout and related API.
 *
 *
 */
class Session extends TwyrBaseComponent {
	// #region Constructor
	constructor(parent, loader) {
		super(parent, loader);
	}
	// #endregion

	// #region Protected methods - need to be overriden by derived classes
	/**
	 * @async
	 * @function
	 * @override
	 * @instance
	 * @memberof Session
	 * @name     _addRoutes
	 *
	 * @returns  {undefined} Nothing.
	 *
	 * @summary  Adds routes to the Koa Router.
	 */
	async _addRoutes() {
		try {
			this.$router.get('/user', this._getUser.bind(this));

			this.$router.post('/login', this._login.bind(this));
			this.$router.get('/logout', this._logout.bind(this));

			this.$router.post('/reset-password', this._resetPassword.bind(this));

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

	// #region Route Handlers
	async _getUser(ctxt) {
		// console.log(`ctxt.state.user: ${JSON.stringify(ctxt.state.user, null, '\t')}`);

		ctxt.status = 200;
		ctxt.body = {
			'user_id': ctxt.state.user ? ctxt.state.user.user_id : 'ffffffff-ffff-4fff-ffff-ffffffffffff',
			'tenant_id': ctxt.state.user ? ctxt.state.user.tenantId : 'ffffffff-ffff-4fff-ffff-ffffffffffff',
			'name': ctxt.state.user ? `${ctxt.state.user.first_name} ${ctxt.state.user.last_name}` : 'Public',
			'loggedIn': !!ctxt.state.user,
			'permissions': (ctxt.state.user ? ctxt.state.user.permissions.map((permission) => { return permission.name; }) : ['public']),
			'designation': ctxt.state.user ? ctxt.state.user.tenantAttributes.designation : '',
			'defaultApplication': ctxt.state.user ? ctxt.state.user.tenantAttributes.default_route : '',
			'otherTenants': ctxt.state.user ? ctxt.state.user.tenantAttributes.allowed_tenants : [],
			'image': ctxt.state.user ? ctxt.state.user.profile_image : '',
			'imageMetadata': ctxt.state.user ? ctxt.state.user.profile_image_metadata : {}
		};

		if(ctxt.state.user && !ctxt.body.permissions.includes('registered'))
			ctxt.body.permissions.push('registered');

		return null;
	}

	async _login(ctxt) {
		if(ctxt.isAuthenticated()) throw new TwyrCompError(`Already logged in`);

		return this.$dependencies.AuthService.authenticate('twyr-local', async (err, user, info, status) => {
			if(err) throw new TwyrCompError(`Login Error: `, err);
			if(!user) throw new TwyrCompError(`User: ${ctxt.request.body.username} not found`);

			await ctxt.login(user);

			ctxt.status = 200;
			ctxt.body = { 'status': status || 200, 'info': info || { 'message': `Login for ${user.first_name} ${user.last_name} successful` } };

			const dbSrvc = this.$dependencies.DatabaseService;
			const allowedTenants = await dbSrvc.knex.raw(`SELECT * FROM tenants WHERE tenant_id IN (SELECT tenant_id FROM tenants_users WHERE user_id = ? AND access_status = 'authorized') AND enabled = true`, [user.user_id]);

			const allowedTenantIds = allowedTenants.rows.map((allowedTenant) => {
				return allowedTenant['tenant_id'];
			});

			if(allowedTenantIds.includes(ctxt.state.tenant.tenant_id)) {
				ctxt.body.nextAction = 'proceed';
				return;
			}

			await ctxt.logout();

			if(allowedTenantIds.length === 0) {
				ctxt.body.nextAction = 'redirect';
				ctxt.body.redirectDomain = 'www';

				return;
			}

			if(allowedTenantIds.length === 1) {
				ctxt.body.nextAction = 'redirect';
				ctxt.body.redirectDomain = allowedTenants.rows[0].sub_domain;

				return;
			}

			if(allowedTenantIds.length > 1) {
				ctxt.body.nextAction = 'choose';
				return;
			}
		})(ctxt);
	}

	async _logout(ctxt) {
		if(!ctxt.isAuthenticated()) throw new TwyrCompError(`No active session`);

		const cacheSrvc = this.$dependencies['CacheService'];
		const cachedKeys = await cacheSrvc.keysAsync(`twyr!webapp!user!${ctxt.state.user.user_id}!*`);

		const cacheMulti = cacheSrvc.multi();
		cachedKeys.forEach((cachedKey) => {
			cacheMulti.delAsync(cachedKey, 300);
		});

		await cacheMulti.execAsync();
		await ctxt.logout();

		ctxt.status = 200;
		ctxt.body = { 'status': 200, 'info': { 'message': `Logout successful` } };
	}

	async _resetPassword(ctxt) {
		await this.$dependencies.ApiService.execute('Session::resetPassword', ctxt);

		ctxt.status = 200;
		ctxt.body = { 'status': 200, 'message': `Password reset successful. Please check your email for the new password` };

		return;
	}
	// #endregion

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

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

exports.component = Session;