Strider-CD/strider

View on GitHub
apps/strider/lib/app.js

Summary

Maintainability
F
2 wks
Test Coverage
// The Strider (web) app.

const debug = require('debug')('strider');
const chalk = require('chalk');
const express = require('express');
const EventEmitter = require('events').EventEmitter;
require('everypaas');

const cors = require('cors');
const path = require('path');
const swig = require('swig');

// middleware
const morganDebug = require('morgan-debug');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const compression = require('compression');
const expressSession = require('express-session');
const serveFavicon = require('serve-favicon');
const errorHandler = require('errorhandler');
const methodOverride = require('method-override');
const connectFlash = require('connect-flash');
const connectMongo = require('connect-mongo');
const csrf = require('csurf');

const setupDb = require('./utils/setup-db');
const Backchannel = require('./backchannel');
const common = require('./common');
require('./logging');
const middleware = require('./middleware');
const routes = require('./routes');
const providerRouter = require('./routes/provider');
const websockets = require('./websockets');
const models = require('./models');
const auth = require('./auth');
const pluginTemplates = require('./plugin-templates');
const pjson = require('../package.json');

const routesAdmin = require('./routes/admin');
const routesJobs = require('./routes/jobs');
const api = require('./routes/api');
const apiV2 = require('./routes/v2');
const collaboratorsRouter = require('./routes/collaborators');
const apiBranches = require('./routes/api/branches');
const apiJobs = require('./routes/api/jobs');
const apiRepo = require('./routes/api/repo');
const apiConfig = require('./routes/api/config');

const mongoStore = connectMongo(expressSession);
const MONTH_IN_MILLISECONDS = 2629743000;
const env = process.env.NODE_ENV || 'development';
const isDevelopment = env === 'development';
const isProduction = env === 'production';
const isTest = env === 'test';
const csrfProtection = csrf({ cookie: true });
let sessionStore;

exports.init = function (config) {
  const mongoose = setupDb(config, (err) => {
    if (err) {
      process.exit(1);
    }
  });

  sessionStore = new mongoStore({
    mongooseConnection: mongoose.connection,
  });

  swig.init({
    root: config.viewpath,
    // allows errors to be thrown and caught by express instead of suppressed by Swig
    allowErrors: true,
    cache: false,
    filters: require('./utils/swig-filters'),
    tags: require('./utils/swig-tags').tags,
    extensions: { plugin: pluginTemplates },
  });

  const app = express();

  if (isDevelopment) {
    app.use(morganDebug('strider:http', 'dev'));
  }

  if (isTest) {
    // awesome view testingness
    require('./views-test')(app);
  }

  // During development we build to an ignored file, only during release we do normal build
  const viewDirs = [path.join(__dirname, 'views')];

  if (config.developing) {
    viewDirs.push(path.join(__dirname, '..', '.dev-dist'));
  }
  viewDirs.push(path.join(__dirname, '..', 'dist'));

  debug('view dirs', viewDirs);

  app.set('views', viewDirs);
  app.engine('html', pluginTemplates.engine);

  if (config.cors) {
    app.use(cors(config.cors));
  }

  app.use(middleware.bodySetter);
  // parse application/x-www-form-urlencoded
  app.use(
    bodyParser.urlencoded({ extended: false, limit: config.body_parser_limit })
  );
  // parse application/json
  app.use(bodyParser.json({ limit: config.body_parser_limit }));
  app.use(cookieParser());
  app.use(compression());
  app.use(methodOverride());
  app.use(
    serveFavicon(path.join(__dirname, '..', 'public', 'favicon.ico'), {
      maxAge: 2592000000,
    })
  );

  app.use(
    expressSession({
      secret: config.session_secret,
      store: sessionStore,
      cookie: { maxAge: MONTH_IN_MILLISECONDS },
      resave: false,
      saveUninitialized: true,
    })
  );
  app.use(connectFlash());

  app.use(function (req, res, next) {
    res.locals.models = models;
    next();
  });

  auth.setup(app); // app.use(passport) is included

  app.use(
    '/vendor',
    express.static(
      path.join(__dirname, '..', '..', '..', 'clients', 'classic-ui', 'vendor'),
      {
        maxAge: MONTH_IN_MILLISECONDS,
      }
    )
  );

  if (config.developing) {
    app.use(
      express.static(path.join(__dirname, '..', '.dev-dist', 'ember'), {
        maxAge: MONTH_IN_MILLISECONDS,
        index: false,
      })
    );
  } else {
    app.use(
      express.static(path.join(__dirname, '..', 'dist', 'ember'), {
        maxAge: MONTH_IN_MILLISECONDS,
        index: false,
      })
    );
  }

  app.use(
    express.static(path.join(__dirname, '..', 'dist'), {
      maxAge: MONTH_IN_MILLISECONDS,
    })
  );

  app.use(
    express.static(path.join(__dirname, '..', 'public'), {
      maxAge: MONTH_IN_MILLISECONDS,
    })
  );

  if (!config.smtp) {
    debug('No SMTP creds - forgot password flow will not work');
  }

  // Routes

  apiV2.default(app);
  app.get('/', routes.index);

  app.get('/about', function (req, res) {
    res.render('about');
  });

  app.get('/status', routes.status);
  app.post(
    '/login',
    function (req, res, next) {
      if (!req.user) {
        auth.authenticate(function (err, user) {
          if (err) {
            return next(err);
          }
          if (!user) {
            return res
              .status(401)
              .json({ errors: ['Password incorrect or user does not exist'] });
          }
          req.login(user, function (err) {
            if (err) {
              return next(err);
            }
            return res.redirect('/');
          });
        })(req, res, next);
        return;
      }

      res.redirect('/');
    },
    auth.authenticate
  );
  app.get('/logout', auth.logout);
  app.post('/forgot', auth.forgot);
  app.get('/reset/:token', auth.reset);
  app.post('/reset/:token', auth.resetPost);

  // Compiled plugin config assets
  app.get(
    '/scripts/plugin-config-compiled.js',
    apiConfig.server('config', 'js')
  );
  app.get(
    '/styles/plugin-config-compiled.css',
    apiConfig.server('config', 'css')
  );
  app.get(
    '/scripts/account-plugins-compiled.js',
    apiConfig.server('account', 'js')
  );
  app.get(
    '/styles/account-plugins-compiled.css',
    apiConfig.server('account', 'css')
  );
  app.get(
    '/scripts/plugin-status-compiled.js',
    apiConfig.server('status', 'js')
  );
  app.get(
    '/styles/plugin-status-compiled.css',
    apiConfig.server('status', 'css')
  );

  app.get('/admin/projects', auth.requireAdminOr401, routesAdmin.projects);
  app.get('/admin/users', auth.requireAdminOr401, csrfProtection, routesAdmin.users);
  app.get('/admin/jobs', auth.requireAdminOr401, function (req, res) {
    res.render('admin/jobs.html', {
      version: pjson.version,
    });
  });
  app.post(
    '/admin/make_admin',
    auth.requireAdminOr401,
    csrfProtection,
    middleware.csrfErrorHandler,
    routesAdmin.makeAdmin);
  app.post(
    '/admin/remove_user',
    auth.requireAdminOr401,
    csrfProtection,
    middleware.csrfErrorHandler,
    routesAdmin.removeUser
  );
  app.get('/admin/invites', auth.requireAdminOr401, csrfProtection, routesAdmin.invites);
  app.get(
    '/admin/:org/:repo/job/:job_id',
    auth.requireAdminOr401,
    routesAdmin.job
  );
  app.get('/admin/plugins', auth.requireAdminOr401, routesAdmin.plugins.get);
  app.put('/admin/plugins', auth.requireAdminOr401, routesAdmin.plugins.put);

  app.use('/account', auth.requireUser, require('./routes/account'));
  app.use('/projects', auth.requireUser, require('./routes/projects'));

  // Requires at least read-only access to the repository in the path
  // app.get('/:org/:repo/', middleware.project, routesJobs.html);
  app.put('/:org/:repo/', auth.requireUser, apiRepo.createProject);
  // app.get('/:org/:repo/job/:job_id?', middleware.project, routesJobs.multijob);
  app.get('/:org/:repo/jobs/', middleware.project, routesJobs.jobs);
  app.post(
    '/:org/:repo/start',
    auth.requireUser,
    middleware.project,
    auth.requireProjectAdmin,
    apiJobs.jobsStart
  );
  app.delete(
    '/:org/:repo/cache',
    auth.requireUser,
    middleware.project,
    auth.requireProjectAdmin,
    apiRepo.clearCache
  );

  app.delete(
    '/:org/:repo/',
    middleware.project,
    auth.requireProjectAdmin,
    apiRepo.deleteProject
  );

  // provider
  app.use(providerRouter);

  // collaborators
  app.use(collaboratorsRouter);

  // branches
  app.use(
    '/:org/:repo/branches/',
    auth.requireUserOr401,
    middleware.project,
    auth.requireProjectAdmin,
    apiBranches
  );

  // keygen
  app.post(
    '/:org/:repo/keygen/',
    auth.requireUser,
    middleware.project,
    auth.requireProjectAdmin,
    apiConfig.keygen
  );

  /* Requires admin access to the repository in the path */
  if ('development' === app.get('env')) {
    console.log('dev config');
    app.get(
      ['/:org/:repo/config', '/:org/:repo/config/'],
      auth.requireUser,
      middleware.project,
      auth.requireProjectAdmin,
      routes.reloadConfig,
      routes.config
    );
  } else {
    console.log('prod config');
    app.get(
      ['/:org/:repo/config', '/:org/:repo/config/'],
      auth.requireUser,
      middleware.project,
      auth.requireProjectAdmin,
      routes.config
    );
  }

  app.put(
    '/:org/:repo/config',
    auth.requireUser,
    middleware.project,
    auth.requireProjectAdmin,
    routes.setConfig
  );

  app.all(
    '/:org/:repo/config/branch/runner',
    auth.requireUser,
    middleware.project,
    auth.requireProjectAdmin
  );
  app.get('/:org/:repo/config/branch/runner', routes.getRunnerConfig);
  app.put('/:org/:repo/config/branch/runner', routes.setRunnerConfig);
  app.put('/:org/:repo/config/branch/runner/id', routes.setRunnerId);
  app.all(
    '/:org/:repo/config/branch/:plugin',
    auth.requireUser,
    middleware.project,
    auth.requireProjectAdmin,
    middleware.projectPlugin
  );
  app.get('/:org/:repo/config/branch/:plugin', routes.getPluginConfig);
  app.put('/:org/:repo/config/branch/:plugin', routes.setPluginConfig);
  app.put(
    '/:org/:repo/config/branch/',
    auth.requireUser,
    middleware.project,
    auth.requireProjectAdmin,
    routes.configureBranch
  );

  // app.get('/api/job/:id', apiJobs.raw);
  app.use('/api', api);
  app.get('/api/jobs', auth.requireUserOr401, apiJobs.jobs);
  // app.get('/api/jobs/:org/:repo', middleware.project, apiJobs.repoJobs);

  app.use(function (req, res, next) {
    let userCreatedTimestamp = 0;
    if (req.user !== undefined) {
      userCreatedTimestamp = parseInt(req.user.id.substr(0, 8), 16);
    }
    res.locals.user_created_timestamp = userCreatedTimestamp;
    next();
  });

  common.app = app;
  common.session_store = sessionStore;
  //
  // ### Strider Webapp Event Emitter
  //
  // Strider has a Node.Js Event Emitter which emits many events.  This can be
  // used by extensions to add extremely flexible custom handling for just about
  // anything.
  //
  common.emitter = new EventEmitter();

  return app;
};

exports.run = function (app) {
  const config = require('./config');

  app.get('/*', routes.emberIndex);

  if (isDevelopment) {
    app.use(errorHandler({ dumpExceptions: true, showStack: true }));
  }

  if (isProduction) {
    app.use(errorHandler({ dumpExceptions: true, showStack: false }));
  }

  // Custom 404 handler.
  // Run after extensions, which might load static middlewares.
  app.use(middleware.custom404);

  // Initialize socket.io
  const server = app.listen(config.port, config.host);
  const sockets = websockets.init(server, sessionStore);
  new Backchannel(common.emitter, sockets);

  console.log(
    chalk.green('Express server listening on port %s in %s mode'),
    config.port,
    app.settings.env
  );
};