OpenVeo Core server

API Docs for: 7.0.0
Show:

File: app/server/servers/ApplicationServer.js

'use strict';

/**
 * @module core-servers
 */

var path = require('path');
var util = require('util');
var async = require('async');
var express = require('express');
var consolidate = require('consolidate');
var session = require('express-session');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var passport = require('passport');
var favicon = require('serve-favicon');
var openVeoApi = require('@openveo/api');
var Server = process.require('app/server/servers/Server.js');
var routeLoader = process.require('app/server/loaders/routeLoader.js');
var permissionLoader = process.require('app/server/loaders/permissionLoader.js');
var entityLoader = process.require('app/server/loaders/entityLoader.js');
var namespaceLoader = process.require('app/server/loaders/namespaceLoader.js');
var DefaultController = process.require('app/server/controllers/DefaultController.js');
var ErrorController = process.require('app/server/controllers/ErrorController.js');
var storage = process.require('app/server/storage.js');
var authenticator = process.require('app/server/authenticator.js');
var SocketServer = openVeoApi.socket.SocketServer;
var SocketNamespace = openVeoApi.socket.SocketNamespace;
var strategyFactory = openVeoApi.passport.strategyFactory;

var defaultController = new DefaultController();
var errorController = new ErrorController();

// Application's environment mode.
var env = (process.env.NODE_ENV == 'production') ? 'prod' : 'dev';

// Headers for static files
var staticHeaders = {
  'x-timestamp': Date.now(),
  'Access-Control-Allow-Origin': '*'
};

// Common options for all static servers delivering static files.
var staticServerOptions = {
  extensions: ['htm', 'html'],
  setHeaders: function(response) {
    response.set(staticHeaders);
  }
};

/**
 * Defines an HTTP server for the openveo application, which serves front and back end pages.
 *
 * @class ApplicationServer
 * @extends Server
 * @constructor
 * @param {Object} configuration Service configuration
 * @param {String} configuration.sessionSecret Hash to encrypt sessions
 * @param {Number} configuration.httpPort HTTP server port
 * @param {Number} configuration.socketPort Socket server port
 */
function ApplicationServer(configuration) {
  ApplicationServer.super_.call(this, configuration);

  Object.defineProperties(this, {

    /**
     * List of path holding template engine views.
     *
     * @property viewsFolders
     * @type Array
     */
    viewsFolders: {value: [], writable: true},

    /**
     * Image styles for image processing.
     *
     * @property imagesStyle
     * @type Object
     * @final
     */
    imagesStyle: {value: {}},

    /**
     * Back end menu description object.
     *
     * @property menu
     * @type Array
     */
    menu: {value: [], writable: true},

    /**
     * Migrations scripts to execute.
     *
     * @property migrations
     * @type Object
     * @final
     */
    migrations: {value: {}},

    /**
     * Socket server.
     *
     * @property socketServer
     * @type SocketServer
     * @final
     */
    socketServer: {value: new SocketServer()},

    /**
     * Database session storage.
     *
     * @property sessionStore
     * @type Object
     */
    sessionStore: {value: null, writable: true},

    /**
     * Express session middleware.
     *
     * @property sessionMiddleware
     * @type Object
     */
    sessionMiddleware: {value: null, writable: true}

  });

  // Apply favicon
  this.httpServer.use(favicon(process.root + '/assets/favicon.ico'));

  // Set mustache as the template engine
  this.httpServer.engine('html', consolidate.mustache);
  this.httpServer.set('view engine', 'html');

  // Log each request
  this.httpServer.use(openVeoApi.middlewares.logRequestMiddleware);

  // Save server configuration
  storage.setServerConfiguration(configuration);

}

module.exports = ApplicationServer;
util.inherits(ApplicationServer, Server);

/**
 * Initializes passport strategies to manage user authentication.
 *
 * @method initializePassport
 * @private
 */
function initializePassport() {
  var self = this;
  this.httpServer.use(passport.initialize());
  this.httpServer.use(passport.session());

  // Instantiate passport strategies
  if (this.configuration.auth) {
    Object.keys(this.configuration.auth).forEach(function(strategy) {
      self.configuration.auth[strategy].usernameField = 'login';
      self.configuration.auth[strategy].passwordField = 'password';
      self.configuration.auth[strategy].logoutUri = 'be';

      passport.use(strategyFactory.get(strategy, self.configuration.auth[strategy], function(user, callback) {
        authenticator.verifyUserAuthentication(user, strategy, function(error, userWithPermissions) {
          if (error) {
            process.logger.error(error.message, {error: error, method: 'verify'});
            callback(null, false);
          } else
            callback(null, userWithPermissions);
        });
      }));
    });
  }

  // Add local strategy at the end because user verification does not work like for other strategies
  passport.use(strategyFactory.get(openVeoApi.passport.STRATEGIES.LOCAL, {
    usernameField: 'login',
    passwordField: 'password'
  }, function(login, password, callback) {
    authenticator.verifyUserByCredentials(login, password, function(error, user) {
      if (error) {
        process.logger.error(error.message, {error: error, method: 'verify'});
        callback(null, false);
      } else
        callback(null, user);
    });
  }));

  // In order to support login sessions, Passport serialize and
  // deserialize user instances to and from the session
  passport.serializeUser(authenticator.serializeUser);

  // When subsequent requests are received, the serialized datas are used to find
  // the user, which will be restored to req.user
  passport.deserializeUser(function(id, callback) {
    authenticator.deserializeUser(id, function(error, user) {
      if (error) {
        process.logger.error(error.message, {error: error, method: 'deserializeUser'});
        callback(null, false);
      } else
        callback(null, user);
    });
  });
}

/**
 * Prepares the express application.
 *
 * @method onDatabaseAvailable
 * @async
 * @param {Database} db The application database
 * @param {Function} callback Function to call when its done with:
 *  - **Error** An error if something went wrong
 */
ApplicationServer.prototype.onDatabaseAvailable = function(db, callback) {
  this.sessionStore = db.getStore('core_sessions');

  // Update Session store with opened database connection
  // Allowed server to restart without loosing any session
  this.sessionMiddleware = session({
    secret: this.configuration.sessionSecret,
    saveUninitialized: true,
    resave: true,
    store: this.sessionStore
  });
  this.httpServer.use(this.sessionMiddleware);

  // The cookieParser and session middlewares are required
  // by passport
  this.httpServer.use(cookieParser());
  this.httpServer.use(bodyParser.urlencoded({
    extended: true
  }));
  this.httpServer.use(bodyParser.json());

  initializePassport.call(this);
  callback();
};

/**
 * Loads plugin.
 *
 * Mounts plugin's assets directories, public router, private router, menu
 * views folders and permissions.
 *
 * @method onPluginLoaded
 * @async
 * @param {Object} plugin The openveo plugin
 * @param {Function} callback Function to call when its done with:
 *  - **Error** An error if something went wrong
 */
ApplicationServer.prototype.onPluginLoaded = function(plugin, callback) {
  var self = this;
  process.logger.info('Start loading plugin ' + plugin.name);


  // If plugin has an assets directory, it will be loaded as a static server
  if (plugin.assets && plugin.mountPath) {
    process.logger.info('Mount ' + plugin.assets + ' on ' + plugin.mountPath);
    this.httpServer.use(plugin.mountPath, express.static(plugin.assets, staticServerOptions));

    if (env === 'dev') {
      var frontJSPath = path.normalize(plugin.assets + '/../app/client/front/js');
      var adminJSPath = path.normalize(plugin.assets + '/../app/client/admin/js');
      this.httpServer.use(plugin.mountPath, express.static(frontJSPath, staticServerOptions));
      this.httpServer.use(plugin.mountPath, express.static(adminJSPath, staticServerOptions));
    }

  }

  // Build plugin public routes
  if (plugin.router && plugin.routes && plugin.mountPath)
    routeLoader.applyRoutes(routeLoader.decodeRoutes(plugin.path, plugin.routes), plugin.router);

  // Build plugin private routes
  if (plugin.privateRouter && plugin.privateRoutes && plugin.mountPath)
    routeLoader.applyRoutes(routeLoader.decodeRoutes(plugin.path, plugin.privateRoutes), plugin.privateRouter);

  // Build routes for entities
  if (plugin.privateRouter && plugin.entities) {
    var entitiesRoutes = entityLoader.buildEntitiesRoutes(plugin.entities);
    routeLoader.applyRoutes(routeLoader.decodeRoutes(plugin.path, entitiesRoutes), plugin.privateRouter);
  }

  // Mount plugin public router to the plugin mount path
  if (plugin.router && plugin.mountPath) {
    process.logger.info('Mount ' + plugin.name + ' public router on ' + plugin.mountPath);
    this.httpServer.use(plugin.mountPath, plugin.router);
  }

  // Mount plugin private router to the plugin private mount path
  if (plugin.privateRouter && plugin.mountPath) {
    process.logger.info('Mount ' + plugin.name + ' private router on ' + plugin.mountPath);
    this.httpServer.use('/be' + plugin.mountPath, plugin.privateRouter);
  }

  // Found back end menu configuration for the plugin
  if (plugin.menu)
    this.menu = this.menu.concat(plugin.menu);

  // Found a list of folders containing views for the plugin
  if (plugin.viewsFolders)
    this.viewsFolders = this.viewsFolders.concat(plugin.viewsFolders);

  // Update migration script to apply
  if (plugin.migrations)
    this.migrations[plugin.name] = plugin.migrations;

  // Found images folders to process
  if (plugin.imageProcessingFolders) {

    // Set thumbnail processor on images folders
    plugin.imageProcessingFolders.forEach(function(folder) {
      var cacheDirectory = folder.cacheDirectory || path.join(folder.imagesDirectory, '.cache');

      process.logger.info('Mount ' + folder.imagesDirectory + ' images processor on ' + plugin.mountPath);
      self.httpServer.use(
        plugin.mountPath,
        openVeoApi.middlewares.imageProcessorMiddleware(
          folder.imagesDirectory,
          cacheDirectory,
          plugin.imageProcessingStyles,
          staticHeaders
        )
      );

      process.logger.info('Mount ' + folder.imagesDirectory + ' on ' + plugin.mountPath);
      self.httpServer.use(plugin.mountPath, express.static(folder.imagesDirectory, staticServerOptions));
    });

  }

  // Found namespaces for the plugin
  if (plugin.namespaces) {

    /*
     * Mounts namespaces on the socket server.
     *
     * @param {Object} namespaces Namespaces with the namespaces names as the key and the list
     * of namespaces messages as the value
     * @param {Boolean} isPrivate true to make each namespace private requiring a back end
     * authentication
     */
    var mountNamespaces = function(namespaces, isPrivate) {
      for (var namespaceName in namespaces) {
        var messagesDescriptors = namespaces[namespaceName];
        var mountPath = (plugin.mountPath === '/') ? namespaceName : plugin.mountPath + '/' + namespaceName;
        var socketNamespace = null;

        // Namespaces names must be unique
        if (!self.socketServer.getNamespace(mountPath)) {

          // Namespace not registered yet
          // Mount it
          socketNamespace = new SocketNamespace();
          process.logger.info('Mount ' + plugin.name + ' "' + namespaceName + '" namespace on ' + mountPath);

          // Attaches messages' handlers to namespace
          namespaceLoader.addHandlers(socketNamespace, messagesDescriptors, plugin.path);

          if (isPrivate) {

            // Use HTTP session middleware to populate socket request with HTTP session information
            socketNamespace.use(function(socket, next) {
              self.sessionMiddleware(socket.request, socket.request.res, next);
            });

            // Add middleware to check that the user is authenticated to the back end
            // Add a middleware to all routes on the namespace to control the back end authentication
            // depending on the requested route
            socketNamespace.use(function(socket, next) {
              if (socket.request.session && socket.request.session.passport && socket.request.session.passport.user)
                next();
              else
                next({error: 'Not authenticated'});
            });
          }

          // Add namespace to socket server
          self.socketServer.addNamespace(mountPath, socketNamespace);
        }
      }
    };

    // Mount public namespaces on the socket server
    if (plugin.namespaces.public)
      mountNamespaces(plugin.namespaces.public);

    // Mount private namespaces on the socket server
    if (plugin.namespaces.private)
      mountNamespaces(plugin.namespaces.private, true);

  }

  process.logger.info(plugin.name + ' plugin loaded');
  callback();
};

/**
 * Finalizes the ApplicationServer initialization.
 *
 * Mounts the assets directories of core and plugins, sets views
 * folders, sets permissions and set default route and error handling.
 * Default route must load the main view due to AngularJS single
 * application.
 *
 * @method onPluginsLoaded
 * @method async
 * @param {Function} callback Function to call when its done with:
 *  - **Error** An error if something went wrong
 */
ApplicationServer.prototype.onPluginsLoaded = function(callback) {
  var plugins = process.api.getPlugins();
  var entities = storage.getEntities();

  // Set views folders for template engine
  this.httpServer.set('views', this.viewsFolders);

  storage.setMenu(this.menu);

  // Handle not found and errors
  this.httpServer.all('/be*', defaultController.defaultAction);
  this.httpServer.all('*', errorController.notFoundPageAction);

  // Handle errors
  this.httpServer.use(errorController.errorAction);

  // Build permissions
  permissionLoader.buildPermissions(entities, plugins, function(error, permissions) {
    if (error)
      return callback(error);

    // Store application's permissions
    storage.setPermissions(permissions);

    callback();
  });
};

/**
 * Starts the HTTP and socket servers.
 *
 * @method startServer
 * @async
 * @param {Function} callback Function to call when it's done with :
 *  - **Error** An error if something went wrong, null otherwise
 */
ApplicationServer.prototype.startServer = function(callback) {
  var self = this;

  async.series([

    // Start HTTP server
    function(callback) {
      self.httpServer.listen(self.configuration.httpPort, function(error) {
        process.logger.info('HTTP Server listening on port ' + self.configuration.httpPort);
        callback(error);
      });
    },

    // Start socket server
    function(callback) {
      self.socketServer.listen(self.configuration.socketPort, function() {
        process.logger.info('Socket server listening on port ' + self.configuration.socketPort);
        callback();
      });
    }
  ], function(error, results) {

    // If process is a child process, send an event to parent process informing that the server has started
    if (process.connected)
      process.send({status: 'started'});

    callback(error);
  });

};