Reference Source

js/security/authentication_plugin_lowdb.js

// NPM IMPORTS
import assert   from 'assert'
import lowdb    from 'lowdb'
import FileSync  from 'lowdb/adapters/FileSync'
import path     from 'path'

// COMMON IMPORTS
import T from 'devapt-core-common/dist/js/utils/types'

// SERVER IMPORTS
import runtime from '../base/runtime'
import AuthenticationPlugin from './authentication_plugin'


let context = 'server/security/authentication_lowdb_plugin'



/**
 * Authentication class with a LowDb user/login database.
 * @author Luc BORIES
 * @license Apache-2.0
 */
export default class AuthenticationLowDbPlugin extends AuthenticationPlugin
{
	/**
	 * Create an Authentication plugin class based on query parameters.
	 * 
	 * @param {AuhtenticationManager} arg_manager - authentication plugins manager.
	 * @param {string} arg_name - plugin name.
	 * @param {string|undefined} arg_log_context - optional.
	 * 
	 * @returns {nothing}
	 */
	constructor(arg_manager, arg_name, arg_log_context)
	{
		super(runtime, arg_manager, arg_name, 'AuthenticationLowDbPlugin', arg_log_context ? arg_log_context : context)
		
		this.is_authentication_lowdb_plugin = true
		
		// this.is_trace_enabled = true
	}
	
	
	
	/**
	 * Enable authentication plugin with contextual informations.
	 * 
	 * @param {object|undefined} arg_settings - optional contextual settings.
	 * 
	 * @returns {object} - a promise object of a boolean result (success:true, failure:false)
	 */
	enable(arg_settings)
	{
		this.enter_group('enable')
		
		const self = this
		
		// console.log(arg_settings, 'arg_settings')
		
		// GET SETTINGS PLAIN OBJECT
		arg_settings = T.isFunction(arg_settings.toJS) ? arg_settings.toJS() : arg_settings
		
		// CALL BASE CLASS METHOD
		let resolved_promise = super.enable(arg_settings)
		
		// SET FIELD NAMES FOR USER NAME
		if (arg_settings && T.isString(arg_settings.username_fieldname) )
		{
			this.username_fieldname = arg_settings.username_fieldname
		}
		
		// SET FIELD NAMES FOR PASSWORD
		if (arg_settings && T.isString(arg_settings.password_fieldname) )
		{
			this.password_fieldname = arg_settings.password_fieldname
		}
		
		// SET FIELD NAMES FOR ID
		if (arg_settings && T.isString(arg_settings.id_fieldname) )
		{
			this.id_fieldname = arg_settings.id_fieldname
		}
		
		// SET REDIRECTION ROUTE ON SUCCESS
		if (arg_settings && T.isString(arg_settings.success_redirect) )
		{
			this.success_redirect = arg_settings.success_redirect
		}
		
		// SET REDIRECTION ROUTE ON FAILURE
		if (arg_settings && T.isString(arg_settings.failure_redirect) )
		{
			this.failure_redirect = arg_settings.failure_redirect
		}
		
		// SET USE OF SESSION FLAG
		if (arg_settings && T.isBoolean(arg_settings.use_session) )
		{
			this.use_session = arg_settings.use_session
		}
		
		// SET FILE NAME
		this.file_name = null
		if (arg_settings && T.isString(arg_settings.file_name) )
		{
			this.file_name = arg_settings.file_name
		}
		
		// LOAD FILE DB
		this.file_db = null
		if (this.file_name)
		{
			resolved_promise = resolved_promise.then(
				function()
				{
					const base_dir = runtime.get_setting('base_dir', null)
					assert( T.isString(base_dir), context + ':enable:bad base dir string')
					
					const json_full_path = path.join(base_dir, '../resources', self.file_name)
					// console.log(json_full_path, 'json_full_path')
					
					self.file_adapter = new FileSync(json_full_path)
					self.file_db = lowdb(self.file_adapter)
					
					// console.log(self.file_db.object, 'self.file_db.object')
					// console.log( require(json_full_path), 'self.file_db JSON file')
					
					self.leave_group('enable (async:resolved)')
				}
			)
			resolved_promise.catch(
				(reason) => {
					self.leave_group('enable (async:error)')
					self.error('failed to load db file:' + reason)
					console.error('failed to load db file:' + reason)
				}
			)
		}
		
		this.leave_group('enable (async:waiting)')
		return resolved_promise
	}
	
	
	
	/**
	 * Disable authentication plugin with contextual informations.
	 * 
	 * @param {object|undefined} arg_settings - optional contextual settings.
	 * 
	 * @returns {object} - a promise object of a boolean result (success:true, failure:false)
	 */
	disable(arg_settings)
	{
		const resolved_promise = super.disable(arg_settings)
		return resolved_promise
	}
	
	
	
	/**
	 * Get a authentication middleware to use on servers (see Connect/Express middleware signature).
	 * 
	 * @param {boolean} arg_should_401 - should send an 401 error on authentication failure.
	 * @param {Function} arg_next_auth_mw - next authentication middleware.
	 * 
	 * @returns {Function} - function(request,response,next){...}
	 */
	create_middleware(arg_should_401, arg_next_auth_mw)
	{
		const self = this
		self.debug('create_middleware')
		// console.log('auth_lowdb_plugin.create_middleware')
		
		const auth_mgr = runtime.security().get_authentication_manager()
		
		return (req, res, next) => {
			let credentials = undefined
			try{
				credentials = auth_mgr.get_credentials(req)
			} catch(e)
			{
				console.error(context + ':create_middleware:middleware:exception', e)
				self.error(context + ':create_middleware:middleware:exception', e)
				next('Authentication failure(exception)')
				return
			}
			
			// DEBUG
			// console.log('auth_lowdb_plugin.create_middleware:credential=', credentials)
			
			if ( auth_mgr.check_request_authentication(req) )
			{
				// console.log(context + ':auth_lowdb_plugin.create_middleware.request is already authenticated')
				self.debug('auth_lowdb_plugin.create_middleware.request is already authenticated')
				next()
				return
			}
			else
			{
				// console.log(context + ':auth_lowdb_plugin.create_middleware:not authenticated with credentials'/*, credentials*/)
				self.debug('auth_lowdb_plugin.create_middleware:not authenticated with credentials'/*, credentials*/)
			}
			
			
			this.authenticate(credentials)
			.then(
				(result) => {
					// console.log(context + ':auth_lowdb_plugin.create_middleware.authenticate then')
					self.debug('auth_lowdb_plugin.create_middleware.authenticate then')
					
					// SUCCESS
					if (result)
					{
						// console.log(context + ':auth_lowdb_plugin.create_middleware.authenticate then:next')
						self.debug('auth_lowdb_plugin.create_middleware.authenticate then:next')
						
						req.is_authenticated = true
						next()
						return
					}
					
					// FAILURE WITH ERROR
					if (arg_should_401)
					{
						// SEND OUTPUT
						res.status(401)
						res.contentType = 'json'
						res.send({ message: 'Authentication failure' })
						
						// console.log(context + ':auth_lowdb_plugin.create_middleware.authenticate then:auth failure')
						self.debug('auth_lowdb_plugin.create_middleware.authenticate then:auth failure')
						
						next('Authentication failure')
						return
					}
					
					// FAILURE WITHOUT ERROR
					// console.log(context + ':auth_lowdb_plugin.create_middleware.authenticate failure without error:next')
					self.debug('auth_lowdb_plugin.create_middleware.authenticate failure without error:next')
					
					if (arg_next_auth_mw)
					{
						arg_next_auth_mw(req, res, next)
						return
					}
					next()
					return
				}
			)
			.catch(
				(reason) => {
					// console.log(context + ':auth_lowdb_plugin.create_middleware.authenticate exception', reason)
					self.debug('auth_lowdb_plugin.create_middleware.authenticate exception', reason)
					
					// SEND OUTPUT
					res.status(401)
					res.contentType = 'json'
					res.send({ message: reason })
					
					next(reason)
				}
			)
		}
	}
	
	
	
	/**
	 * Authenticate a user with a file giving request credentials.
	 * 
	 * @param {Credentials|undefined} arg_credentials - request credentials object.
	 * 
	 * @returns {object} - a promise of boolean.
	 */
	authenticate(arg_credentials)
	{
		this.debug('authenticate')
		// console.log(arg_credentials, context + ':authenticate:arg_credentials')
		
		if (! this.file_db)
		{
			this.debug('authenticate:failure:bad db')
			return Promise.resolve(false)
		}
		// assert( T.isFunction(this.file_db), context + ':authenticate:bad db object')
		assert( T.isObject(arg_credentials) && arg_credentials.is_credentials, context + ':authenticate:bad credentials object')
		arg_credentials = arg_credentials.get_credentials()

		// HAS AUTHENTICATION INFORMATIONS
		if ( !(T.isString(arg_credentials.user_name) && T.isString(arg_credentials.user_pass_digest)) )
		{
			this.debug('authenticate:failure:no credentials', arg_credentials)
			return Promise.resolve(false)
		}
		// assert( T.isString(arg_credentials.user_name), context + ':authenticate:bad credentials.user_name string')
		// assert( T.isString(arg_credentials.user_pass_digest), context + ':authenticate:bad credentials.user_pass_digest string')
		
		// CREATE QUERY
		const username_field = this.username_fieldname ? this.username_fieldname : 'username'
		const password_field = this.id_password_fieldnamefieldname ? this.password_fieldname : 'password'
		let query = {}
		query[username_field] = arg_credentials.user_name
		query[password_field] = arg_credentials.user_pass_digest
		
		// EXECUTE QUERY
		try{
			// console.log(context + '.authenticate:db', this.file_db)
			// console.log(context + '.authenticate:db', this.file_db.object)
			// console.log(context + '.authenticate:query', query)
			
			const users = this.file_db.get('users').find(query).value()
			if (users)
			{
				// const first_user = (T.isArray(users) && users.length > 0) ? users[0] : (T.isObject(users) ? users : null)
				const first_user = users
				if ( T.isFunction(arg_credentials.done_cb) )
				{
					arg_credentials.done_cb(first_user)
				}
				// console.log('authenticate user found')
				
				this.debug('authenticate:success')
				return Promise.resolve(true)
			}
		}
		catch(e)
		{
			console.error('authenticate user error', e)
		}
		
		// console.log('authenticate user NOT found')
		
		this.debug('authenticate:failure for', arg_credentials)
		return Promise.resolve(false)
	}
	
	
	
	/**
	 * Get user id from a user record.
	 * 
	 * @param {object} arg_user_record - user record object.
	 * 
	 * @returns {string} - user id.
	 */
	get_user_id(arg_user_record)
	{
		const id_field = this.id_fieldname ? this.id_fieldname : 'id'
		return ( T.isObject(arg_user_record) && (id_field in arg_user_record) ) ? arg_user_record[id_field] : null
	}
	
	
	
	/**
	 * Get user record by its id.
	 * 
	 * @param {string} arg_user_id - user id.
	 * 
	 * @returns {string} - user id.
	 */
	get_user_by_id(arg_user_id)
	{
		assert( T.isFunction(this.file_db), context + ':get_user_by_id:bad db object')
		
		// CREATE QUERY
		const id_field = this.id_fieldname ? this.id_fieldname : 'id'
		let query = {}
		query[id_field] = arg_user_id
		
		// EXECUTE QUERY
		try{
			let users = this.file_db('users').find(query)
			if (users)
			{
				return (T.isArray(users) && users.length > 0) ? users[0] : (T.isObject(users) ? users : null)
			}
		}
		catch(e)
		{
		}
		
		return null
	}
}