From 0f96377e30b7efd7c7a984a0727b1b2f294e95b1 Mon Sep 17 00:00:00 2001 From: NGPixel Date: Tue, 16 Aug 2016 23:56:08 -0400 Subject: [PATCH] Base server logic --- controllers/admin.js | 13 ++++ controllers/auth.js | 73 ++++++++++++++++++ controllers/pages.js | 13 ++++ middlewares/flash.js | 17 +++++ models/auth.js | 65 ++++++++++++++++ package.json | 3 +- server.js | 173 ++++++++++++++++++++++++++++++++++++++++++- views/auth/login.pug | 1 + views/error.pug | 21 ++++++ views/layout.pug | 28 +++++++ 10 files changed, 404 insertions(+), 3 deletions(-) create mode 100644 controllers/admin.js create mode 100644 controllers/auth.js create mode 100644 controllers/pages.js create mode 100644 middlewares/flash.js create mode 100644 models/auth.js create mode 100644 views/auth/login.pug create mode 100644 views/error.pug create mode 100644 views/layout.pug diff --git a/controllers/admin.js b/controllers/admin.js new file mode 100644 index 00000000..8f1c3307 --- /dev/null +++ b/controllers/admin.js @@ -0,0 +1,13 @@ +"use strict"; + +var express = require('express'); +var router = express.Router(); + +/** + * Admin + */ +router.get('/', (req, res) => { + res.send('OK'); +}); + +module.exports = router; \ No newline at end of file diff --git a/controllers/auth.js b/controllers/auth.js new file mode 100644 index 00000000..02e111cf --- /dev/null +++ b/controllers/auth.js @@ -0,0 +1,73 @@ +var express = require('express'); +var router = express.Router(); +var passport = require('passport'); +var ExpressBrute = require('express-brute'); +var ExpressBruteRedisStore = require('express-brute-redis'); +var moment = require('moment'); + +/** + * Setup Express-Brute + */ +var EBstore = new ExpressBruteRedisStore({ + prefix: 'bf:', + client: red +}); +var bruteforce = new ExpressBrute(EBstore, { + freeRetries: 5, + minWait: 60 * 1000, + maxWait: 5 * 60 * 1000, + refreshTimeoutOnRequest: false, + failCallback(req, res, next, nextValidRequestDate) { + req.flash('alert', { + class: 'error', + title: 'Too many attempts!', + message: "You've made too many failed attempts in a short period of time, please try again " + moment(nextValidRequestDate).fromNow() + '.', + iconClass: 'fa-times' + }); + res.redirect('/login'); + } +}); + +/** + * Login form + */ +router.get('/login', function(req, res, next) { + res.render('auth/login', { + usr: res.locals.usr + }); +}); + +router.post('/login', bruteforce.prevent, function(req, res, next) { + passport.authenticate('local', function(err, user, info) { + + if (err) { return next(err); } + + if (!user) { + req.flash('alert', { + class: 'error', + title: 'Invalid login', + message: "The email or password is invalid.", + iconClass: 'fa-times' + }); + return res.redirect('/login'); + } + + req.logIn(user, function(err) { + if (err) { return next(err); } + req.brute.reset(function () { + return res.redirect('/'); + }); + }); + + })(req, res, next); +}); + +/** + * Logout + */ +router.get('/logout', function(req, res) { + req.logout(); + res.redirect('/'); +}); + +module.exports = router; \ No newline at end of file diff --git a/controllers/pages.js b/controllers/pages.js new file mode 100644 index 00000000..cd13fe96 --- /dev/null +++ b/controllers/pages.js @@ -0,0 +1,13 @@ +"use strict"; + +var express = require('express'); +var router = express.Router(); + +/** + * Home + */ +router.get('/', (req, res) => { + res.send('OK'); +}); + +module.exports = router; \ No newline at end of file diff --git a/middlewares/flash.js b/middlewares/flash.js new file mode 100644 index 00000000..6e94db12 --- /dev/null +++ b/middlewares/flash.js @@ -0,0 +1,17 @@ +"use strict"; + +/** + * Flash middleware + * + * @param {Express Request} req Express Request object + * @param {Express Response} res Express Response object + * @param {Function} next Next callback function + * @return {any} void + */ +module.exports = (req, res, next) => { + + res.locals.appflash = req.flash('alert'); + + next(); + +}; \ No newline at end of file diff --git a/models/auth.js b/models/auth.js new file mode 100644 index 00000000..bae38974 --- /dev/null +++ b/models/auth.js @@ -0,0 +1,65 @@ +var LocalStrategy = require('passport-local').Strategy; + +module.exports = function(passport, appconfig) { + + // Serialization user methods + + passport.serializeUser(function(user, done) { + done(null, user._id); + }); + + passport.deserializeUser(function(id, done) { + db.User.findById(id).then((user) => { + done(null, user); + }).catch((err) => { + done(err, null); + }); + }); + + // Setup local user authentication strategy + + passport.use( + 'local', + new LocalStrategy({ + usernameField : 'email', + passwordField : 'password', + passReqToCallback : true + }, + function(req, uEmail, uPassword, done) { + db.User.findOne({ 'email' : uEmail }).then((user) => { + if (user) { + user.validatePassword(uPassword).then((isValid) => { + return (isValid) ? done(null, user) : done(null, false); + }); + } else { + return done(null, false); + } + }).catch((err) => { + done(err); + }); + }) + ); + + // Check for admin access + + db.connectPromise.then(() => { + + db.User.count().then((count) => { + if(count < 1) { + winston.info('No administrator account found. Creating a new one...'); + db.User.new({ + email: appconfig.admin, + firstName: "Admin", + lastName: "Admin", + password: "admin123" + }).then(() => { + winston.info('Administrator account created successfully!'); + }).catch((ex) => { + winston.error('An error occured while creating administrator account: ' + ex); + }); + } + }); + + }); + +}; \ No newline at end of file diff --git a/package.json b/package.json index 1eb381b7..3b27f7d0 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ }, "dependencies": { "auto-load": "^2.1.0", + "bcryptjs-then": "^1.0.1", "bluebird": "^3.4.1", "body-parser": "^1.15.2", "compression": "^1.6.2", @@ -54,7 +55,7 @@ "moment-timezone": "^0.5.5", "mongoose": "^4.5.9", "mongoose-delete": "^0.3.4", - "node-bcrypt": "0.0.1", + "nodegit": "^0.14.1", "passport": "^0.3.2", "passport-local": "^1.0.0", "pug": "^2.0.0-beta5", diff --git a/server.js b/server.js index 1442ed9e..490097d5 100644 --- a/server.js +++ b/server.js @@ -14,5 +14,174 @@ winston.info('Requarks Wiki is initializing...'); global.ROOTPATH = __dirname; var appconfig = require('./models/config')('./config.yml'); -global.db = require('./models/db')(appconfig); -global.red = require('./models/redis')(appconfig); \ No newline at end of file +global.db = require('./models/mongodb')(appconfig); +global.red = require('./models/redis')(appconfig); + +var _ = require('lodash'); +var express = require('express'); +var path = require('path'); +var favicon = require('serve-favicon'); +var session = require('express-session'); +var redisStore = require('connect-redis')(session); +var cookieParser = require('cookie-parser'); +var bodyParser = require('body-parser'); +var flash = require('connect-flash'); +var compression = require('compression'); +var passport = require('passport'); +var autoload = require('auto-load'); +var expressValidator = require('express-validator'); +var http = require('http'); + +global.lang = require('i18next'); +var i18next_backend = require('i18next-node-fs-backend'); +var i18next_mw = require('i18next-express-middleware'); + +var mw = autoload(path.join(ROOTPATH, '/middlewares')); +var ctrl = autoload(path.join(ROOTPATH, '/controllers')); + +// ---------------------------------------- +// Define Express App +// ---------------------------------------- + +global.app = express(); +global.ROOTPATH = __dirname; +var _isDebug = (app.get('env') === 'development'); + +// ---------------------------------------- +// Security +// ---------------------------------------- + +app.use(mw.security); + +// ---------------------------------------- +// Passport Authentication +// ---------------------------------------- + +var strategy = require('./models/auth')(passport, appconfig); + +app.use(cookieParser()); +app.use(session({ + name: 'requarkswiki.sid', + store: new redisStore({ client: red }), + secret: appconfig.sessionSecret, + resave: false, + saveUninitialized: false +})); +app.use(flash()); +app.use(passport.initialize()); +app.use(passport.session()); + +// ---------------------------------------- +// Localization Engine +// ---------------------------------------- + +lang + .use(i18next_backend) + .use(i18next_mw.LanguageDetector) + .init({ + load: 'languageOnly', + ns: ['common'], + defaultNS: 'common', + saveMissing: false, + supportedLngs: ['en', 'fr'], + preload: ['en', 'fr'], + fallbackLng : 'en', + backend: { + loadPath: './locales/{{lng}}/{{ns}}.json' + } + }); + +// ---------------------------------------- +// View Engine Setup +// ---------------------------------------- + +app.use(compression()); + +app.use(i18next_mw.handle(lang)); +app.set('views', path.join(ROOTPATH, 'views')); +app.set('view engine', 'pug'); + +//app.use(favicon(path.join(ROOTPATH, 'assets', 'favicon.ico'))); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: false })); +app.use(expressValidator()); + +// ---------------------------------------- +// Public Assets +// ---------------------------------------- + +app.use(express.static(path.join(ROOTPATH, 'assets'))); + +// ---------------------------------------- +// View accessible data +// ---------------------------------------- + +app.locals._ = require('lodash'); +app.locals.moment = require('moment'); +app.locals.appconfig = appconfig; +//app.locals.appdata = require('./data.json'); +app.use(mw.flash); + +// ---------------------------------------- +// Controllers +// ---------------------------------------- + +app.use('/', ctrl.auth); + +app.use('/', ctrl.pages); +app.use('/admin', mw.auth, ctrl.admin); + +// ---------------------------------------- +// Error handling +// ---------------------------------------- + +// catch 404 and forward to error handler +app.use(function(req, res, next) { + var err = new Error('Not Found'); + err.status = 404; + next(err); +}); + +// error handlers +app.use(function(err, req, res, next) { + res.status(err.status || 500); + res.render('error', { + message: err.message, + error: _isDebug ? err : {} + }); +}); + +// ---------------------------------------- +// Start HTTP server +// ---------------------------------------- + +winston.info('Requarks Wiki has initialized successfully.'); + +winston.info('Starting HTTP server on port ' + appconfig.port + '...'); + +app.set('port', appconfig.port); +var server = http.createServer(app); +server.listen(appconfig.port); +server.on('error', (error) => { + if (error.syscall !== 'listen') { + throw error; + } + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error('Listening on port ' + appconfig.port + ' requires elevated privileges!'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error('Port ' + appconfig.port + ' is already in use!'); + process.exit(1); + break; + default: + throw error; + } +}); + +server.on('listening', () => { + winston.info('HTTP server started successfully! [RUNNING]'); +}); \ No newline at end of file diff --git a/views/auth/login.pug b/views/auth/login.pug new file mode 100644 index 00000000..fbcc4aab --- /dev/null +++ b/views/auth/login.pug @@ -0,0 +1 @@ +DUDE \ No newline at end of file diff --git a/views/error.pug b/views/error.pug new file mode 100644 index 00000000..a1b5e492 --- /dev/null +++ b/views/error.pug @@ -0,0 +1,21 @@ +doctype html +html + head + meta(http-equiv='X-UA-Compatible', content='IE=edge') + meta(charset='UTF-8') + title= appconfig.title + + // Favicon + each favsize in [32, 96, 16] + link(rel='icon', type='image/png', sizes=favsize + 'x' + favsize href='/images/favicon-' + favsize + 'x' + favsize + '.png') + + // CSS + link(href='https://fonts.googleapis.com/css?family=Open+Sans:400,300,600,700|Inconsolata', rel='stylesheet', type='text/css') + link(type='text/css', rel='stylesheet', href='/css/app.css') + + body(class='server-error') + #root + img(src='/images/logo-text_218x80.png') + h1 Oops, something went wrong + h4= message + pre #{error.stack} \ No newline at end of file diff --git a/views/layout.pug b/views/layout.pug new file mode 100644 index 00000000..d3d3e3c6 --- /dev/null +++ b/views/layout.pug @@ -0,0 +1,28 @@ +doctype html +html + head + meta(http-equiv='X-UA-Compatible', content='IE=edge') + meta(charset='UTF-8') + meta(name='theme-color', content='#009688') + meta(name='msapplication-TileColor', content='#009688') + title= appconfig.title + + // Favicon + each favsize in [32, 96, 16] + link(rel='icon', type='image/png', sizes=favsize + 'x' + favsize href='/images/favicon-' + favsize + 'x' + favsize + '.png') + + // CSS + link(type='text/css', rel='stylesheet', href='/css/libs.css') + link(type='text/css', rel='stylesheet', href='/css/app.css') + + block head + + body + #root + include ./common/header + include ./common/alerts + main + block content + include ./common/footer + + block outside \ No newline at end of file