├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── gulpfile.js ├── package.json ├── src ├── app.js ├── constants │ ├── MiddlewareType.js │ ├── RedirectCode.js │ └── failedlogin.js ├── controllers │ ├── auth.js │ ├── domain.js │ ├── facebook.js │ ├── file.js │ ├── index.js │ ├── logic.js │ ├── page.js │ ├── password.js │ ├── permission.js │ ├── rbac.js │ ├── redirect.js │ ├── role.js │ ├── token.js │ ├── user.js │ └── userpermission.js ├── index.js ├── models.js ├── models │ ├── provider.js │ └── user.js ├── options.js ├── router.js ├── secure.js ├── server.js └── strategy.js └── tests ├── models ├── article.js ├── provider.js └── user.js ├── routes ├── test.js └── vhost.js └── run.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "stage-0", 4 | "es2015" 5 | ], 6 | "plugins": [ 7 | "transform-class-properties" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "rules": { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | node_modules 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - iojs 4 | - "4" 5 | - "5" 6 | script: npm test 7 | notifications: 8 | email: 9 | recipients: 10 | - zlatkofedor@cherrysro.com 11 | on_success: change 12 | on_failure: always 13 | services: 14 | - mongodb 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Zlatko Fedor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Maglev (Preconfigured simple NodeJS framework) 2 | 3 | 4 | [](https://codeclimate.com/github/seeden/maglev/badges) 5 | [](https://david-dm.org/seeden/maglev) 6 | [](https://gitter.im/seeden/maglev) 7 | [](https://gratipay.com/seeden/) 8 | 9 | 10 | Maglev is a simple pre configured server based on [Express](http://expressjs.com/) web framework, [Passport](http://passportjs.org/) authentication middleware and [Mongoose](http://mongoosejs.com/) database layer. 11 | Maglev supports MVC patterns and RESTful routes. 12 | 13 | 14 | ## Install 15 | 16 | ```sh 17 | npm install maglev 18 | ``` 19 | 20 | ## Features 21 | 22 | * Predefined models and controllers (User, Token, Role, Permission, Logic...) 23 | * Extended routing for REST api based on Express 24 | * Token and session authentication 25 | * Role based access system 26 | * [Swig](http://paularmstrong.github.io/swig/) template system with custom helpers 27 | 28 | ## Require 29 | 30 | Maglev is using two peerDependencies [Mongoose](http://mongoosejs.com/) and [express-session](https://github.com/expressjs/session). Please add it into your package.json if you want to use mongoose. 31 | 32 | ## Usage 33 | 34 | ```js 35 | var mongoose = require('mongoose'); 36 | var Server = require('maglev'); 37 | 38 | var server = new Server({ 39 | root: __dirname, 40 | db: mongoose.connect('mongodb://localhost/maglev'), 41 | session: { 42 | secret: '123456789' 43 | }, 44 | favicon: false 45 | }); 46 | 47 | server.start(); 48 | ``` 49 | 50 | ## Directory Structure 51 | 52 | * *controllers* Contains the controllers that handle requests sent to an application. 53 | * *models* Contains the models for accessing and storing data in a database. 54 | * *views* Contains the views and layouts that are rendered by an application. 55 | * *public* Static files and compiled assets served by the application. 56 | 57 | ## Models 58 | Define new model 59 | 60 | ```js 61 | var mongoose = require('mongoose'); 62 | var Schema = mongoose.Schema; 63 | 64 | function createSchema() { 65 | var schema = new Schema({ 66 | city: { type: String, required: true }, 67 | street: { type: String, required: true }, 68 | state: { type: String, required: true } 69 | }); 70 | 71 | return schema; 72 | } 73 | 74 | module.exports = function (server) { 75 | return server.db.model('Address', createSchema()); 76 | }; 77 | ``` 78 | 79 | ## Routes 80 | 81 | ```js 82 | var token = require('maglev/dist/controllers/token'); 83 | var message = require('../controllers/message'); 84 | 85 | module.exports = function(route) { 86 | route 87 | .api() 88 | .get('/messages', token.ensure, message.get) 89 | .put('/messages/mark/read/:id', token.ensure, message.markAsRead) 90 | .put('/messages/mark/unread/:id', token.ensure, message.markAsUnread); 91 | }; 92 | ``` 93 | 94 | ## There are other configuration parameters 95 | 96 | ```js 97 | { 98 | root: null, 99 | 100 | rbac: { 101 | storage: null, 102 | role: { 103 | guest: 'guest' 104 | } 105 | }, 106 | 107 | log: true, 108 | 109 | morgan: { 110 | format: process.env.NODE_ENV === 'development' ? 'dev' : 'combined', 111 | options: { 112 | immediate: false 113 | //stream: process.stdout 114 | } 115 | }, 116 | 117 | server: { 118 | build: 1, 119 | host: process.env.HOST || '127.0.0.1', 120 | port: process.env.PORT || 4000 121 | }, 122 | 123 | request: { 124 | timeout: 1000*60*5 125 | }, 126 | 127 | compression: {}, 128 | 129 | powered: { 130 | value: 'Maglev' 131 | }, 132 | 133 | responseTime: {}, 134 | 135 | methodOverride: { 136 | //https://github.com/expressjs/method-override 137 | enabled: true, 138 | getter: 'X-HTTP-Method-Override', 139 | options: {} 140 | }, 141 | 142 | bodyParser: [{ 143 | parse: 'urlencoded', 144 | options: { 145 | extended: true 146 | } 147 | }, { 148 | parse: 'json', 149 | options: {} 150 | }, { 151 | parse: 'json', 152 | options: { 153 | type: 'application/vnd.api+json' 154 | } 155 | }], 156 | 157 | cookieParser: { 158 | secret: null, 159 | options: {} 160 | }, 161 | 162 | token: { 163 | secret: null, 164 | expiration: 60*24*14 165 | }, 166 | 167 | session: { 168 | secret: null, 169 | cookie: { 170 | maxAge: 14 *24 * 60 * 60 * 1000 //2 weeks 171 | }, 172 | resave: true, 173 | saveUninitialized: true 174 | }, 175 | 176 | view: { 177 | engine: 'swig' 178 | }, 179 | 180 | router: { 181 | api: { 182 | path: '/api' 183 | } 184 | }, 185 | 186 | locale: { 187 | 'default': 'en', 188 | available: ['en'], 189 | inUrl: false 190 | }, 191 | 192 | country: { 193 | 'default': null, 194 | available: [], 195 | inUrl: false 196 | }, 197 | 198 | registration: { 199 | simple: true 200 | }, 201 | 202 | facebook: { 203 | clientID: null, 204 | clientSecret: null, 205 | namespace: null 206 | }, 207 | 208 | upload: { 209 | maxFieldsSize: 2000000, 210 | maxFields: 1000, 211 | path: null 212 | }, 213 | 214 | cors: {}, 215 | 216 | page: { 217 | error: null, 218 | notFound: null 219 | }, 220 | 221 | strategies: [], 222 | 223 | css: { 224 | root: 'public/css', 225 | options: {} 226 | }, 227 | 228 | 'static': { 229 | root: 'public', 230 | options: { 231 | index: false 232 | } 233 | }, 234 | 235 | favicon: { 236 | root: 'public/favicon.ico', 237 | options: {} 238 | } 239 | }; 240 | ``` 241 | 242 | ## Credits 243 | 244 | [Zlatko Fedor](http://github.com/seeden) 245 | 246 | ## License 247 | 248 | The MIT License (MIT) 249 | 250 | Copyright (c) 2015 Zlatko Fedor zlatkofedor@cherrysro.com 251 | 252 | Permission is hereby granted, free of charge, to any person obtaining a copy 253 | of this software and associated documentation files (the "Software"), to deal 254 | in the Software without restriction, including without limitation the rights 255 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 256 | copies of the Software, and to permit persons to whom the Software is 257 | furnished to do so, subject to the following conditions: 258 | 259 | The above copyright notice and this permission notice shall be included in 260 | all copies or substantial portions of the Software. 261 | 262 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 263 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 264 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 265 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 266 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 267 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 268 | THE SOFTWARE. -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var mocha = require('gulp-mocha'); 3 | var babel = require('gulp-babel'); 4 | 5 | gulp.task('test', function () { 6 | return gulp.src('./tests/**/*.js') 7 | .pipe(babel()) 8 | .pipe(mocha({ 9 | timeout: 10000 10 | })); 11 | }); 12 | 13 | gulp.task('build', function (callback) { 14 | return gulp.src('./src/**/*.js') 15 | .pipe(babel()) 16 | .pipe(gulp.dest("./dist")); 17 | }); 18 | 19 | gulp.doneCallback = function (err) { 20 | process.exit(err ? 1 : 0); 21 | }; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "maglev", 3 | "version": "5.0.13", 4 | "description": "Preconfigured NodeJS framework", 5 | "author": { 6 | "name": "Zlatko Fedor", 7 | "email": "zfedor@gmail.com", 8 | "url": "http://www.cherrysro.com/" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/seeden/maglev.git" 13 | }, 14 | "keywords": [ 15 | "framework", 16 | "passport", 17 | "express", 18 | "MVC", 19 | "mongoose", 20 | "swig", 21 | "rbac", 22 | "mean" 23 | ], 24 | "private": false, 25 | "license": "MIT", 26 | "main": "dist/index.js", 27 | "engines": { 28 | "node": ">= 0.12.0" 29 | }, 30 | "scripts": { 31 | "prepublish": "npm run build", 32 | "build": "node ./node_modules/gulp/bin/gulp.js build", 33 | "test": "babel-node ./node_modules/gulp/bin/gulp.js test", 34 | "eslint": "node ./node_modules/eslint/bin/eslint.js ./src || exit 0" 35 | }, 36 | "dependencies": { 37 | "async": "^2.0.0-rc.3", 38 | "bcryptjs": "^2.3.0", 39 | "body-parser": "^1.15.0", 40 | "compression": "^1.6.1", 41 | "connect-flash": "^0.1.1", 42 | "connect-timeout": "^1.7.0", 43 | "consolidate": "^0.14.0", 44 | "cookie-parser": "^1.4.1", 45 | "cors": "^2.7.1", 46 | "csurf": "^1.8.3", 47 | "debug": "^2.2.0", 48 | "download": "^4.4.3", 49 | "express": "^4.13.4", 50 | "express-domain-middleware": "^0.1.0", 51 | "fb": "^1.0.2", 52 | "gm": "^1.22.0", 53 | "jsonwebtoken": "^5.7.0", 54 | "keymirror": "^0.1.1", 55 | "less-middleware": "^2.1.0", 56 | "lodash": "^4.11.1", 57 | "method-override": "^2.3.5", 58 | "methods": "^1.1.2", 59 | "mkdirp": "^0.5.1", 60 | "mongoose-hrbac": "^4.0.1", 61 | "mongoose-json-schema": "^0.0.7", 62 | "mongoose-permalink": "^2.0.0", 63 | "morgan": "^1.7.0", 64 | "multiparty": "^4.1.2", 65 | "mv": "^2.1.1", 66 | "node-uuid": "^1.4.7", 67 | "node.extend": "^1.1.5", 68 | "okay": "^1.0.0", 69 | "passport": "^0.3.2", 70 | "passport-anonymous": "^1.0.1", 71 | "passport-facebook": "^2.1.0", 72 | "passport-facebook-canvas": "^0.0.3", 73 | "passport-http-bearer": "^1.0.1", 74 | "passport-local": "^1.0.0", 75 | "passport-twitter": "^1.0.4", 76 | "prettyjson": "^1.1.3", 77 | "puid": "^1.0.5", 78 | "rbac": "^4.0.1", 79 | "robots.txt": "^1.1.0", 80 | "response-time": "^2.3.1", 81 | "serve-favicon": "^2.3.0", 82 | "serve-static": "^1.10.2", 83 | "swig": "^1.4.2", 84 | "temporary": "^0.0.8", 85 | "tv4": "^1.2.7", 86 | "vhost": "^3.0.2", 87 | "web-error": "^3.1.2" 88 | }, 89 | "devDependencies": { 90 | "babel-cli": "^6.7.7", 91 | "babel-core": "^6.7.7", 92 | "babel-eslint": "^6.0.3", 93 | "babel-loader": "^6.2.4", 94 | "babel-plugin-transform-class-properties": "^6.6.0", 95 | "babel-preset-es2015": "^6.6.0", 96 | "babel-preset-stage-0": "^6.5.0", 97 | "babel-preset-stage-1": "^6.5.0", 98 | "eslint": "2.8.0", 99 | "eslint-config-airbnb": "^7.0.0", 100 | "eslint-loader": "^1.3.0", 101 | "eslint-plugin-react": "^4.3.0", 102 | "eslint-plugin-jsx-a11y": "^0.6.2", 103 | "connect-mongo": "^1.1.0", 104 | "gulp": "^3.9.1", 105 | "gulp-babel": "^6.1.2", 106 | "gulp-mocha": "^2.2.0", 107 | "gulp-util": "^3.0.7", 108 | "mongoose": "^4.4.12", 109 | "should": "^8.3.1", 110 | "supertest": "^1.2.0", 111 | "express-session": "^1.13.0", 112 | "webpack": "^1.13.0" 113 | }, 114 | "peerDependencies": { 115 | "express-session": "1.x" 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import debug from 'debug'; 3 | import http from 'http'; 4 | import isArray from 'lodash/isArray'; 5 | 6 | import expressDomainMiddleware from 'express-domain-middleware'; 7 | import compression from 'compression'; 8 | import serveFavicon from 'serve-favicon'; 9 | import serveStatic from 'serve-static'; 10 | import cookieParser from 'cookie-parser'; 11 | import session from 'express-session'; 12 | import bodyParser from 'body-parser'; 13 | import methodOverride from 'method-override'; 14 | import responseTime from 'response-time'; 15 | import timeout from 'connect-timeout'; 16 | import morgan from 'morgan'; 17 | import cors from 'cors'; 18 | import lessMiddleware from 'less-middleware'; 19 | 20 | import request from 'express/lib/request'; 21 | import consolidate from 'consolidate'; 22 | import flash from 'connect-flash'; 23 | import robots from 'robots.txt'; 24 | import MiddlewareType from './constants/MiddlewareType'; 25 | 26 | import * as fileController from './controllers/file'; 27 | import * as pageController from './controllers/page'; 28 | 29 | const log = debug('maglev:app'); 30 | 31 | function connectionToUnique(conn) { 32 | return `${conn.remoteAddress}:${conn.remotePort}`; 33 | } 34 | 35 | 36 | export default class App { 37 | constructor(server, options = {}) { 38 | if (!options.root) { 39 | throw new Error('Root is undefined'); 40 | } 41 | 42 | log(`App root: ${options.root}`); 43 | 44 | this._server = server; 45 | this._options = options; 46 | this._expressApp = express(); 47 | this._httpServer = null; 48 | this._activeConnections = {}; 49 | 50 | // prepare basic 51 | this._prepareErrorHandler(); 52 | this._prepareCompression(); 53 | this._prepareLog(); 54 | this._prepareEngine(); 55 | this._prepareHtml(); 56 | 57 | this._prepareMiddleware(MiddlewareType.BEFORE_STATIC); 58 | 59 | // prepare static 60 | this._prepareStatic(); 61 | 62 | // prepare middlewares 63 | this._prepareVars(); 64 | this._prepareSession(); 65 | this._prepareSecure(); 66 | this._prepareMiddleware(MiddlewareType.BEFORE_ROUTER); 67 | this._prepareRouter(); 68 | this._prepareMiddleware(MiddlewareType.AFTER_ROUTER); 69 | } 70 | 71 | get options() { 72 | return this._options; 73 | } 74 | 75 | get activeConnections() { 76 | return this._activeConnections; 77 | } 78 | 79 | get server() { 80 | return this._server; 81 | } 82 | 83 | get httpServer() { 84 | return this._httpServer; 85 | } 86 | 87 | get expressApp() { 88 | return this._expressApp; 89 | } 90 | 91 | listen(port, host, callback) { 92 | if (this._httpServer) { 93 | return callback(new Error('You need to close http server first')); 94 | } 95 | 96 | this._httpServer = http 97 | .createServer(this.expressApp) 98 | .listen(port, host, callback); 99 | 100 | this.handleConnectionEvents(); 101 | 102 | return this; 103 | } 104 | 105 | handleConnectionEvents() { 106 | // TODO UNHANDLE 107 | const { activeConnections, httpServer } = this; 108 | 109 | httpServer.on('connection', function onConnectionCallback(connection) { 110 | const key = connectionToUnique(connection); 111 | activeConnections[key] = { 112 | connection, 113 | requests: 0, 114 | }; 115 | 116 | connection.once('close', function onCloseCallback() { 117 | if (activeConnections[key]) { 118 | delete activeConnections[key]; 119 | } 120 | }); 121 | }); 122 | 123 | httpServer.on('request', function onRequestCallback(request, response) { 124 | const key = connectionToUnique(request.connection); 125 | 126 | const settings = activeConnections[key]; 127 | if (!settings) { 128 | return; 129 | } 130 | 131 | settings.requests++; 132 | 133 | response.once('finish', function onFinishCallback() { 134 | const settings = activeConnections[key]; 135 | if (!settings) { 136 | return; 137 | } 138 | 139 | settings.requests--; 140 | }); 141 | }); 142 | } 143 | 144 | _destroyUnusedConnections() { 145 | const { activeConnections } = this; 146 | 147 | // remove unused connections 148 | Object.keys(activeConnections).forEach(function destroyConnection(key) { 149 | const settings = activeConnections[key]; 150 | if (settings.requests) { 151 | return; 152 | } 153 | 154 | settings.connection.destroy(); 155 | delete activeConnections[key]; 156 | }); 157 | } 158 | 159 | close(callback) { 160 | const { activeConnections, httpServer, options } = this; 161 | 162 | if (!httpServer) { 163 | return callback(new Error('You need to listen first')); 164 | } 165 | 166 | log('Closing http server'); 167 | httpServer.close((err) => { 168 | if (err) { 169 | return callback(err); 170 | } 171 | 172 | this._httpServer = null; 173 | 174 | // check current state of the connections 175 | if (!Object.keys(activeConnections).length) { 176 | log('There is no idle connections'); 177 | return callback(); 178 | } 179 | 180 | log(`Starting idle connection timeout ${options.socket.idleTimeout}`); 181 | setTimeout(() => { 182 | Object.keys(activeConnections).forEach((key) => { 183 | const settings = activeConnections[key]; 184 | if (!settings) { 185 | return; 186 | } 187 | 188 | log(`Destroying connection: ${key}`); 189 | settings.connection.destroy(); 190 | }); 191 | 192 | log('All connections destroyed'); 193 | callback(); 194 | }, options.socket.idleTimeout); 195 | }); 196 | 197 | // destroy connections without requests 198 | this._destroyUnusedConnections(); 199 | 200 | return this; 201 | } 202 | 203 | _prepareErrorHandler() { 204 | const app = this.expressApp; 205 | 206 | app.use(expressDomainMiddleware); 207 | } 208 | 209 | _prepareCompression() { 210 | const app = this.expressApp; 211 | const options = this.options; 212 | 213 | if (!options.compression) { 214 | return; 215 | } 216 | 217 | app.use(compression(options.compression)); 218 | } 219 | 220 | _prepareLog() { 221 | const app = this.expressApp; 222 | const options = this.options; 223 | 224 | if (!options.log) { 225 | return; 226 | } 227 | 228 | app.set('showStackError', true); 229 | 230 | if (!options.morgan) { 231 | return; 232 | } 233 | app.use(morgan(options.morgan.format, options.morgan.options)); 234 | } 235 | 236 | _prepareEngine() { 237 | const app = this.expressApp; 238 | const options = this.options; 239 | 240 | app.locals.pretty = true; 241 | app.locals.cache = 'memory'; 242 | app.enable('jsonp callback'); 243 | 244 | app.engine('html', consolidate[options.view.engine]); 245 | 246 | app.set('view engine', 'html'); 247 | app.set('views', `${options.root}/views`); 248 | } 249 | 250 | _prepareHtml() { 251 | const app = this.expressApp; 252 | const options = this.options; 253 | 254 | if (!options.powered) { 255 | app.disable('x-powered-by'); 256 | } 257 | 258 | if (options.responseTime) { 259 | app.use(responseTime(options.responseTime)); 260 | } 261 | 262 | if (options.cors) { 263 | app.use(cors(options.cors)); 264 | } 265 | 266 | if (options.request.timeout) { 267 | app.use(timeout(options.request.timeout)); 268 | } 269 | 270 | if (options.cookieParser) { 271 | app.use(cookieParser(options.cookieParser.secret, options.cookieParser.options)); 272 | } 273 | 274 | if (options.bodyParser) { 275 | for (let index = 0; index < options.bodyParser.length; index++) { 276 | const bp = options.bodyParser[index]; 277 | app.use(bodyParser[bp.parse](bp.options)); 278 | } 279 | } 280 | 281 | if (options.methodOverride) { 282 | app.use(methodOverride(options.methodOverride.getter, options.methodOverride.options)); 283 | } 284 | } 285 | 286 | _prepareVars() { 287 | const app = this.expressApp; 288 | const server = this.server; 289 | const options = this.options; 290 | 291 | // add access to req from template 292 | app.use(function setTemplateVariables(req, res, next) { 293 | res.locals._req = req; 294 | res.locals._production = process.env.NODE_ENV === 'production'; 295 | res.locals._build = options.server.build; 296 | 297 | next(); 298 | }); 299 | 300 | // add access to req from template 301 | app.use(function setBasicVariables(req, res, next) { 302 | req.objects = {}; 303 | req.server = server; 304 | req.models = server.models; 305 | 306 | next(); 307 | }); 308 | } 309 | 310 | _prepareSession() { 311 | const app = this.expressApp; 312 | const options = this.options; 313 | 314 | if (!options.session) { 315 | return; 316 | } 317 | 318 | // use session middleware 319 | const sessionMiddleware = session(options.session); 320 | app.use(sessionMiddleware); 321 | 322 | if (!options.sessionRecovery) { 323 | return; 324 | } 325 | 326 | // session recovery 327 | app.use(function sessionRecovery(req, res, next) { 328 | let tries = options.sessionRecovery.tries; 329 | 330 | function lookupSession(error) { 331 | if (error) { 332 | return next(error); 333 | } 334 | 335 | if (typeof req.session !== 'undefined') { 336 | return next(); 337 | } 338 | 339 | tries -= 1; 340 | 341 | if (tries < 0) { 342 | return next(new Error('Session is undefined')); 343 | } 344 | 345 | sessionMiddleware(req, res, lookupSession); 346 | } 347 | 348 | lookupSession(); 349 | }); 350 | } 351 | 352 | _prepareSecure() { 353 | const app = this.expressApp; 354 | const server = this.server; 355 | const options = this.options; 356 | 357 | app.use(server.secure.passport.initialize()); 358 | 359 | if (options.session) { 360 | app.use(server.secure.passport.session()); 361 | } 362 | } 363 | 364 | _prepareStatic() { 365 | const app = this.expressApp; 366 | const options = this.options; 367 | 368 | if (options.flash) { 369 | app.use(flash()); 370 | } 371 | 372 | try { 373 | if (options.favicon) { 374 | log(`FavIcon root: ${options.favicon.root}`); 375 | app.use(serveFavicon(options.favicon.root, options.favicon.options)); 376 | } 377 | 378 | if (options.robots) { 379 | log(`Robots root: ${options.robots.root}`); 380 | app.use(robots(options.robots.root)); 381 | } 382 | } catch (err) { 383 | if (err.code !== 'ENOENT') { 384 | throw err; 385 | } 386 | 387 | log(err.message); 388 | } 389 | 390 | if (options.css) { 391 | log(`CSS root: ${options.css.root}`); 392 | app.use(options.css.path, lessMiddleware(options.css.root, options.css.options)); 393 | } 394 | 395 | if (options.static) { 396 | if (!options.static.path || !options.static.root) { 397 | throw new Error('Static path or root is undefined'); 398 | } 399 | 400 | log(`Static root: ${options.static.root}`); 401 | app.use(options.static.path, serveStatic(options.static.root, options.static.options)); 402 | } 403 | } 404 | 405 | _prepareRouter() { 406 | const app = this.expressApp; 407 | const options = this.options; 408 | const server = this.server; 409 | 410 | // use server router 411 | app.use(server.router.expressRouter); 412 | 413 | // delete uploaded files 414 | app.use(fileController.clearAfterError); // error must be first 415 | app.use(fileController.clear); 416 | 417 | // at the end add 500 and 404 418 | app.use(options.page.notFound || pageController.notFound); 419 | app.use(options.page.error || pageController.error); 420 | } 421 | 422 | _prepareMiddleware(type) { 423 | const app = this.expressApp; 424 | const options = this.options; 425 | const middlewares = options.middleware; 426 | 427 | if (!middlewares || !middlewares[type]) { 428 | return; 429 | } 430 | 431 | const middleware = middlewares[type]; 432 | 433 | if (typeof middleware === 'function') { 434 | app.use(middleware); 435 | } else if (isArray(middleware)) { 436 | middleware.forEach((fn) => { 437 | app.use(fn); 438 | }); 439 | } 440 | } 441 | } 442 | 443 | function prepareRequest(req) { 444 | req.__defineGetter__('httpHost', function getHttpHost() { 445 | const trustProxy = this.app.get('trust proxy'); 446 | const host = trustProxy && this.get('X-Forwarded-Host'); 447 | 448 | return host || this.get('Host'); 449 | }); 450 | 451 | req.__defineGetter__('port', function getPort() { 452 | const host = this.httpHost; 453 | if (!host) { 454 | return null; 455 | } 456 | 457 | const parts = host.split(':'); 458 | return (parts.length === 2) ? parseInt(parts[1], 10) : 80; 459 | }); 460 | 461 | req.__defineGetter__('protocolHost', function getProtocolHost() { 462 | return `${this.protocol}://${this.httpHost}`; 463 | }); 464 | } 465 | 466 | prepareRequest(request); 467 | -------------------------------------------------------------------------------- /src/constants/MiddlewareType.js: -------------------------------------------------------------------------------- 1 | import keymirror from 'keymirror'; 2 | 3 | export default keymirror({ 4 | BEFORE_STATIC: null, 5 | BEFORE_ROUTER: null, 6 | AFTER_ROUTER: null, 7 | }); 8 | -------------------------------------------------------------------------------- /src/constants/RedirectCode.js: -------------------------------------------------------------------------------- 1 | export default { 2 | PERMANENT: 301, 3 | TEMPORARY: 302, 4 | }; 5 | -------------------------------------------------------------------------------- /src/constants/failedlogin.js: -------------------------------------------------------------------------------- 1 | import keymirror from 'keymirror'; 2 | 3 | export default keymirror({ 4 | NOT_FOUND: null, 5 | PASSWORD_INCORRECT: null, 6 | MAX_ATTEMPTS: null, 7 | }); 8 | -------------------------------------------------------------------------------- /src/controllers/auth.js: -------------------------------------------------------------------------------- 1 | export function login(req, res, next) { 2 | req.server.secure.authenticate('local', {})(req, res, next); 3 | } 4 | 5 | /** 6 | * Login user by his username and password 7 | * @param {String} failureRedirect Url for failured login attempt 8 | * @return {Function} Controller function 9 | */ 10 | export function loginOrRedirect(failureRedirect) { 11 | return (req, res, next) => { 12 | req.server.secure.authenticate('local', { 13 | failureRedirect, 14 | })(req, res, next); 15 | }; 16 | } 17 | 18 | export function ensure(req, res, next) { 19 | if (req.isAuthenticated() === true) { 20 | return next(); 21 | } 22 | 23 | return res.status(401).format({ 24 | 'text/plain': () => { 25 | res.send('User is not authorized'); 26 | }, 27 | 'text/html': () => { 28 | res.send('User is not authorized'); 29 | }, 30 | 'application/json': () => { 31 | res.jsonp({ 32 | error: 'User is not authorized', 33 | }); 34 | }, 35 | }); 36 | } 37 | 38 | export function logout(req, res, next) { 39 | req.logout(); 40 | next(); 41 | } 42 | -------------------------------------------------------------------------------- /src/controllers/domain.js: -------------------------------------------------------------------------------- 1 | import WebError from 'web-error'; 2 | import RedirectCode from '../constants/RedirectCode'; 3 | 4 | export { RedirectCode }; 5 | 6 | export function restrictTo(domain, code = RedirectCode.TEMPORARY) { 7 | return (req, res, next) => { 8 | if (req.hostname === domain) { 9 | return next(); 10 | } 11 | 12 | const newUrl = `${req.protocol}://${domain}${req.originalUrl}`; 13 | res.redirect(code, newUrl); 14 | }; 15 | } 16 | 17 | export function restrictPath(domain, path) { 18 | return (req, res, next) => { 19 | if (req.hostname !== domain) { 20 | return next(); 21 | } 22 | 23 | if (path instanceof RegExp && path.test(req.path)) { 24 | return next(); 25 | } 26 | 27 | if (path === req.path) { 28 | return next(); 29 | } 30 | 31 | next(new WebError(404)); 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/controllers/facebook.js: -------------------------------------------------------------------------------- 1 | import FB from 'fb'; 2 | import WebError from 'web-error'; 3 | import ok from 'okay'; 4 | 5 | const fbScope = ['email', 'publish_actions']; 6 | const fbSuccessRedirect = '/'; 7 | const fbFailureRedirect = '/?fb_error=signin'; 8 | const fbAuthUrl = '/auth/facebook'; 9 | const fbCallbackUrl = '/auth/facebook/callback'; 10 | const fbCanvasRedirectUrl = '/auth/facebook/autologin'; 11 | 12 | export function ensure(req, res, next) { 13 | req.server.secure.authenticate('facebook', { 14 | scope: fbScope, 15 | failureRedirect: fbFailureRedirect, 16 | callbackURL: req.protocolHost + fbCallbackUrl, 17 | })(req, res, next); 18 | } 19 | 20 | export function ensureCallback(req, res, next) { 21 | req.server.secure.authenticate('facebook', { 22 | successRedirect: fbSuccessRedirect, 23 | failureRedirect: fbFailureRedirect, 24 | callbackURL: req.protocolHost + fbCallbackUrl, 25 | })(req, res, next); 26 | } 27 | 28 | export function ensureCanvas(req, res, next) { 29 | req.server.secure.authenticate('facebook-canvas', { 30 | scope: fbScope, 31 | successRedirect: fbSuccessRedirect, 32 | failureRedirect: fbCanvasRedirectUrl, 33 | callbackURL: req.protocolHost + fbCallbackUrl, 34 | })(req, res, next); 35 | } 36 | 37 | /** 38 | * Redirect unauthorized facebook canvas application to the facebook ensure page 39 | * @param {Request} req 40 | * @param {Response} res 41 | */ 42 | export function redirectToEnsure(req, res) { 43 | res.send( ` 44 |
45 | 48 | 49 |