├── .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 | [![Quality](https://codeclimate.com/github/seeden/maglev.png)](https://codeclimate.com/github/seeden/maglev/badges) 5 | [![Dependencies](https://david-dm.org/seeden/maglev.png)](https://david-dm.org/seeden/maglev) 6 | [![Gitter chat](https://badges.gitter.im/seeden/maglev.png)](https://gitter.im/seeden/maglev) 7 | [![Gittip](https://img.shields.io/gittip/seeden.svg?style=flat)](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 | `); 50 | } 51 | 52 | /** 53 | * Channel for facebook API 54 | */ 55 | export function channel(req, res) { 56 | const oneYear = 31536000; 57 | res.set({ 58 | Pragma: 'public', 59 | 'Cache-Control': `max-age=${oneYear}`, 60 | Expires: new Date(Date.now() + oneYear * 1000).toUTCString(), 61 | }); 62 | 63 | res.send(''); 64 | } 65 | 66 | export function ensureBySignedRequest(req, res, next) { 67 | if (!req.body.signedRequest || !req.body.profile) { 68 | return next(new WebError(400)); 69 | } 70 | 71 | const User = req.models.User; 72 | const options = req.server.options; 73 | const profile = req.body.profile; 74 | 75 | const session = req.body.session || false; 76 | const signedRequest = FB.parseSignedRequest(req.body.signedRequest, options.facebook.appSecret); 77 | 78 | if (!signedRequest) { 79 | return next(new WebError(400, 'Parsing signed request')); 80 | } 81 | 82 | if (!signedRequest.user_id) { 83 | return next(new WebError(400, 'User ID is missing')); 84 | } 85 | 86 | // if user is authentificated and ids is same 87 | if (req.user && req.user.facebook.id === signedRequest.user_id) { 88 | return next(); 89 | } 90 | 91 | // search user in database 92 | User.findByFacebookID(signedRequest.user_id, ok(next, (user) => { 93 | if (user) { 94 | return req.logIn(user, { session }, next); 95 | } 96 | 97 | if (!options.registration.simple) { 98 | return next(new WebError(400, 'User needs to be registered')); 99 | } 100 | 101 | if (profile.id !== signedRequest.user_id) { 102 | return next(new WebError(400, 'Profile.id is different from signedRequest.user_id')); 103 | } 104 | 105 | User.createByFacebook(profile, ok(next, (createdUser) => { 106 | req.logIn(createdUser, { session }, next); 107 | })); 108 | })); 109 | } 110 | 111 | export function redirectPeopleToCanvas(req, res, next) { 112 | const facebookBot = 'facebookexternalhit'; 113 | const options = req.server.options; 114 | 115 | if (!req.headers['user-agent'] || req.headers['user-agent'].indexOf(facebookBot) === -1) { 116 | return res.redirect(302, `https://apps.facebook.com/${options.facebook.namespace}/`); 117 | } 118 | 119 | next(); 120 | } 121 | -------------------------------------------------------------------------------- /src/controllers/file.js: -------------------------------------------------------------------------------- 1 | import multiparty from 'multiparty'; 2 | import fs from 'fs'; 3 | import { map } from 'async'; 4 | import WebError from 'web-error'; 5 | import Download from 'download'; 6 | import tmp from 'temporary'; 7 | import ok from 'okay'; 8 | 9 | function deleteFiles(files, callback) { 10 | map(files, (file, cb) => { 11 | fs.unlink(file.path, (err) => { 12 | if (err && err.message.indexOf('ENOENT') === -1) { 13 | return cb(err); 14 | } 15 | 16 | cb(null, file); 17 | }); 18 | }, ok(callback, (removedFiles) => { 19 | callback(null, removedFiles); 20 | })); 21 | } 22 | 23 | export function upload(req, res, next) { 24 | const serverOptions = req.server.options; 25 | const files = req.objects.files = []; 26 | 27 | const options = { 28 | maxFieldsSize: serverOptions.upload.maxFieldsSize, 29 | maxFields: serverOptions.upload.maxFields, 30 | }; 31 | 32 | const form = new multiparty.Form(options); 33 | 34 | form.on('error', (err) => { 35 | next(err); 36 | }); 37 | 38 | form.on('field', (field, value) => { 39 | req.body[field] = value; 40 | }); 41 | 42 | form.on('file', (name, file) => { 43 | files.push(file); 44 | }); 45 | 46 | form.on('close', () => { 47 | next(); 48 | }); 49 | 50 | form.parse(req); 51 | } 52 | 53 | export function clear(req, res, next) { 54 | if (!req.objects.files || !req.objects.files.length) { 55 | return next(); 56 | } 57 | 58 | deleteFiles(req.objects.files, ok(next, (removedFiles) => { 59 | req.objects.files = []; 60 | req.objects.removedFiles = removedFiles; 61 | next(); 62 | })); 63 | } 64 | 65 | export function clearAfterError(err, req, res, next) { 66 | clear(req, res, ok(next, () => { 67 | next(err); 68 | })); 69 | } 70 | 71 | export function get(req, res, next) { 72 | const file = req.objects.file; 73 | if (!file) { 74 | return next(new WebError(404)); 75 | } 76 | 77 | res.jsonp({ 78 | file: file.toPrivateJSON(), 79 | }); 80 | } 81 | 82 | export function download(req, res, next) { 83 | const files = req.objects.files = []; 84 | 85 | if (!req.body.url) { 86 | return next(new WebError(401)); 87 | } 88 | 89 | const downloadInstance = new Download().get(req.body.url); 90 | downloadInstance.run(ok(next, (downloadedFiles) => { 91 | if (!downloadedFiles.length) { 92 | return next(new WebError(401)); 93 | } 94 | 95 | const tmpFile = new tmp.File(); 96 | 97 | const file = { 98 | fieldName: 'file', 99 | originalFilename: downloadedFiles[0].path, 100 | path: tmpFile.path, 101 | size: downloadedFiles[0].contents.length, 102 | }; 103 | 104 | tmpFile.writeFile(downloadedFiles[0].contents, ok(next, () => { 105 | files.push(file); 106 | next(); 107 | })); 108 | })); 109 | } 110 | -------------------------------------------------------------------------------- /src/controllers/index.js: -------------------------------------------------------------------------------- 1 | import * as auth from './auth'; 2 | import * as facebook from './facebook'; 3 | import * as file from './file'; 4 | import * as logic from './logic'; 5 | import * as page from './page'; 6 | import * as password from './password'; 7 | import * as permission from './permission'; 8 | import * as rbac from './rbac'; 9 | import * as role from './role'; 10 | import * as token from './token'; 11 | import * as user from './user'; 12 | import * as userPermission from './userpermission'; 13 | import * as redirect from './redirect'; 14 | import * as domain from './domain'; 15 | 16 | 17 | export { 18 | auth, 19 | domain, 20 | facebook, 21 | file, 22 | logic, 23 | page, 24 | password, 25 | permission, 26 | redirect, 27 | rbac, 28 | role, 29 | token, 30 | user, 31 | userPermission, 32 | }; 33 | -------------------------------------------------------------------------------- /src/controllers/logic.js: -------------------------------------------------------------------------------- 1 | export function or(fn1, fn2) { 2 | return (req, res, next) => { 3 | fn1(req, res, (err) => { 4 | if (!err) { 5 | return next(); 6 | } 7 | 8 | fn2(req, res, next); 9 | }); 10 | }; 11 | } 12 | 13 | export function and(fn1, fn2) { 14 | return (req, res, next) => { 15 | fn1(req, res, (err) => { 16 | if (err) { 17 | return next(err); 18 | } 19 | 20 | fn2(req, res, next); 21 | }); 22 | }; 23 | } 24 | 25 | export function cond(condition, fnOK, fnElse) { 26 | return (req, res, next) => { 27 | condition(req, res, (err) => { 28 | if (!err) { 29 | return fnOK(req, res, next); 30 | } 31 | 32 | fnElse(req, res, next); 33 | }); 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/controllers/page.js: -------------------------------------------------------------------------------- 1 | import WebError from 'web-error'; 2 | import prettyjson from 'prettyjson'; 3 | import debug from 'debug'; 4 | 5 | const log = debug('maglev:pageController'); 6 | 7 | /** 8 | * Handler of errors caused by controllers 9 | * @param {Error} err 10 | * @param {Request} req 11 | * @param {Response} res 12 | * @param {Function} next 13 | */ 14 | export function error(err, req, res, next) { 15 | const server = req.server; 16 | const options = server.options; 17 | 18 | log(err); 19 | 20 | err.req = req; 21 | err.url = req.originalUrl; 22 | 23 | server.emit('err', err); 24 | 25 | const errorObj = { 26 | status: err.status || 500, 27 | message: err.message || 'Internal server error', 28 | stack: err.stack, 29 | url: req.originalUrl, 30 | errors: err.errors || [], 31 | }; 32 | 33 | if (errorObj.status >= 500 && options.log && options.morgan.options.stream) { 34 | const data = prettyjson.render(err); 35 | options.morgan.options.stream.write(data + '\n'); 36 | } 37 | 38 | res.status(errorObj.status).format({ 39 | 'text/plain': function sendTextPlain() { 40 | res.send(errorObj.message + '\n' + errorObj.stack); 41 | }, 42 | 43 | 'text/html': function sendTextHtml() { 44 | const view = (errorObj.status === 404) ? 'error404' : 'error'; 45 | res.render(view, errorObj); 46 | }, 47 | 48 | 'application/json': function sendJSON() { 49 | res.jsonp(errorObj); 50 | }, 51 | }); 52 | } 53 | 54 | /** 55 | * Handler of not founded pages 56 | * @param {Request} req 57 | * @param {Response} res 58 | */ 59 | export function notFound(req, res, next) { 60 | return next(new WebError(404)); 61 | } 62 | -------------------------------------------------------------------------------- /src/controllers/password.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import async from 'async'; 3 | import WebError from 'web-error'; 4 | import ok from 'okay'; 5 | 6 | export function tokenToUser(req, res, next, id) { 7 | const User = req.models.User; 8 | const options = req.server.options; 9 | 10 | if (!id) { 11 | return next(new WebError(400, 'Token is undefined')); 12 | } 13 | 14 | jwt.verify(id, options.mail.token.secret, ok(next, (data) => { 15 | if (!data.user) { 16 | return next(new WebError(400, 'Unknown user')); 17 | } 18 | 19 | User.findById(id, ok(next, (user) => { 20 | if (!user) { 21 | return next(new WebError(404)); 22 | } 23 | 24 | req.objects.user = user; 25 | next(); 26 | })); 27 | })); 28 | } 29 | 30 | 31 | /** 32 | * Change user password 33 | */ 34 | export function change(req, res, next) { 35 | const user = req.objects.user; 36 | 37 | if (!user) { 38 | return next(new WebError(404)); 39 | } 40 | 41 | if (!req.body.password) { 42 | return next(new WebError(400, 'Parameter password is missing')); 43 | } 44 | 45 | if (!user.hasPassword()) { 46 | user.setPassword(req.body.password, ok(next, () => { 47 | res.status(204).end(); 48 | })); 49 | } else { 50 | if (!req.body.password_old) { 51 | return next(new WebError(400, 'Parameter password_old is missing')); 52 | } 53 | 54 | user.comparePassword(req.body.password_old, ok(next, (isMatch) => { 55 | if (!isMatch) { 56 | return next(new WebError(400, 'Password is not match with actual password')); 57 | } 58 | 59 | user.setPassword(req.body.password, ok(next, () => { 60 | res.status(204).end(); 61 | })); 62 | })); 63 | } 64 | } 65 | 66 | export function generateForgotToken(user, tokenSecret, expiresInMinutes = 60 * 24) { 67 | if (!tokenSecret) { 68 | throw new Error('Token secret is undefined'); 69 | } 70 | 71 | const data = { 72 | user: user._id, 73 | }; 74 | 75 | return jwt.sign(data, tokenSecret, { expiresInMinutes }); 76 | } 77 | 78 | export function forgot(req, res, next) { 79 | const User = req.models.User; 80 | const server = req.server; 81 | const options = server.options; 82 | const mail = server.mail; 83 | 84 | if (!req.body.username) { 85 | return next(new WebError(400, 'Parameter username is missing')); 86 | } 87 | 88 | User.findByUsername(req.body.username, false, ok(next, (user) => { 89 | if (!user) { 90 | return next(new WebError(404)); 91 | } 92 | 93 | if (!user.hasEmail()) { 94 | return next(new WebError(401, 'User has no email')); 95 | } 96 | 97 | // generate token 98 | const token = generateForgotToken(user, options.mail.token.secret, options.mail.token.expiration); 99 | 100 | // render mails 101 | const data = { 102 | user, 103 | from: options.mail.default.from, 104 | to: user.email, 105 | subject: 'Password Assistance', 106 | token, 107 | }; 108 | 109 | async.series({ 110 | html: (callback) => { 111 | res.render('mail/forgot', data, callback); 112 | }, 113 | text: (callback) => { 114 | res.render('mail/forgot_plain', data, callback); 115 | }, 116 | }, ok(next, (result) => { 117 | const mailOptions = { 118 | from: options.mail.default.from, 119 | to: user.email, 120 | subject: 'Password Assistance', 121 | html: result.html, 122 | text: result.text, 123 | }; 124 | 125 | mail.sendMail(mailOptions, ok(next, () => { 126 | return res.status(204).end(); 127 | })); 128 | })); 129 | })); 130 | } 131 | -------------------------------------------------------------------------------- /src/controllers/permission.js: -------------------------------------------------------------------------------- 1 | import WebError from 'web-error'; 2 | import ok from 'okay'; 3 | 4 | export function loadPermission(req, res, next, name) { 5 | const rbac = req.server.rbac; 6 | 7 | if (!name) { 8 | return next(new WebError(400)); 9 | } 10 | 11 | rbac.getPermissionByName(name, ok(next, (permission) => { 12 | if (!permission) { 13 | return next(new WebError(404)); 14 | } 15 | 16 | req.objects.permission = permission; 17 | next(); 18 | })); 19 | } 20 | 21 | /** 22 | * Create new permission 23 | */ 24 | export function create(req, res, next) { 25 | const rbac = req.server.rbac; 26 | 27 | if (!req.body.action || !req.body.resource) { 28 | return next(new WebError(400, 'Permission action or resource is undefined')); 29 | } 30 | 31 | rbac.createPermission(req.body.action, req.body.resource, ok(next, (permission) => { 32 | if (!permission) { 33 | return next(new WebError(400)); 34 | } 35 | 36 | return res.jsonp({ 37 | permission: { 38 | action: permission.action, 39 | resource: permission.resource, 40 | name: permission.name, 41 | }, 42 | }); 43 | })); 44 | } 45 | 46 | /** 47 | * Remove existing permission 48 | */ 49 | export function remove(req, res, next) { 50 | const User = req.models.User; 51 | 52 | if (!req.objects.permission) { 53 | return next(new WebError(404)); 54 | } 55 | 56 | const permission = req.objects.permission; 57 | 58 | // unassign permission from all users 59 | User.removePermissionFromCollection(permission.name, ok(next, () => { 60 | permission.remove(ok(next, (isDeleted) => { 61 | if (!isDeleted) { 62 | return next(new WebError(400)); 63 | } 64 | 65 | return res.status(204).end(); 66 | })); 67 | })); 68 | } 69 | 70 | /** 71 | * Return true if poermission exists 72 | */ 73 | export function exists(req, res, next) { 74 | if (!req.objects.permission) { 75 | return next(new WebError(404)); 76 | } 77 | 78 | return res.status(204).end(); 79 | } 80 | 81 | /** 82 | * Get permission details 83 | */ 84 | export function get(req, res, next) { 85 | if (!req.objects.permission) { 86 | return next(new WebError(404)); 87 | } 88 | 89 | const perm = req.objects.permission; 90 | 91 | return res.jsonp({ 92 | permission: { 93 | action: perm.action, 94 | resource: perm.resource, 95 | name: perm.name, 96 | }, 97 | }); 98 | } 99 | -------------------------------------------------------------------------------- /src/controllers/rbac.js: -------------------------------------------------------------------------------- 1 | import WebError from 'web-error'; 2 | import ok from 'okay'; 3 | 4 | /** 5 | * Return middleware function for permission check 6 | * @param {String} action Name of action 7 | * @param {String} resource Name of resource 8 | * @param {String} redirect Url where is user redirected when he has no permissions 9 | * @param {Number} status Status code of redirect action 10 | * @return {Function} Middleware function 11 | */ 12 | export function can(action, resource, redirect, redirectStatus = 302) { 13 | return (req, res, next) => { 14 | const server = req.server; 15 | const options = server.options; 16 | const rbac = server.rbac; 17 | const user = req.user; 18 | 19 | const callback = ok(next, (canDoIt) => { 20 | if (!canDoIt) { 21 | if (redirect) { 22 | return res.redirect(redirectStatus, redirect); 23 | } 24 | 25 | return next(new WebError(401, `You have no access: ${action}_${resource}`)); 26 | } 27 | 28 | next(); 29 | }); 30 | 31 | if (!user) { 32 | rbac.can(options.rbac.role.guest, action, resource, callback); 33 | } else { 34 | user.can(rbac, action, resource, callback); 35 | } 36 | }; 37 | } 38 | 39 | /** 40 | * Return middleware function for permission check 41 | * @param {String} name Name of role 42 | * @param {String} redirect Url where is user redirected when he has no permissions 43 | * @param {Number} status Status code of redirect action 44 | * @return {Function} Middleware function 45 | */ 46 | export function hasRole(name, redirect, redirectStatus = 302) { 47 | return (req, res, next) => { 48 | const server = req.server; 49 | const rbac = server.rbac; 50 | 51 | if (!req.user) { 52 | return next(new WebError(401)); 53 | } 54 | 55 | req.user.hasRole(rbac, name, ok(next, (has) => { 56 | if (!has) { 57 | if (redirect) { 58 | return res.redirect(redirectStatus, redirect); 59 | } 60 | return next(new WebError(401)); 61 | } 62 | 63 | next(); 64 | })); 65 | }; 66 | } 67 | 68 | /** 69 | * Allow only guest user show content 70 | * @param {String} redirect Url where is user redirected when he has no permissions 71 | * @param {Number} status Status code of redirect action 72 | * @return {Function} Middleware function 73 | */ 74 | export function isGuest(redirect, redirectStatus = 302) { 75 | return (req, res, next) => { 76 | if (!req.user) { 77 | return next(); 78 | } 79 | 80 | if (redirect) { 81 | return res.redirect(redirectStatus, redirect); 82 | } 83 | 84 | next(new WebError(401, 'You are not a guest')); 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /src/controllers/redirect.js: -------------------------------------------------------------------------------- 1 | import RedirectCode from '../constants/RedirectCode'; 2 | 3 | export { RedirectCode }; 4 | 5 | /* 6 | /^\/(en|sk|it)(\/.*)/ 7 | to '$2' 8 | */ 9 | export function regExpPath(pathRegExp, to, code = RedirectCode.TEMPORARY) { 10 | if (!(pathRegExp instanceof RegExp)) { 11 | throw new Error('Path is not regexp'); 12 | } 13 | 14 | return (req, res, next) => { 15 | if (!pathRegExp.test(req.path)) { 16 | return next(); 17 | } 18 | 19 | const newPath = req.path.replace(pathRegExp, to); 20 | 21 | res.redirect(newPath, code); 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/controllers/role.js: -------------------------------------------------------------------------------- 1 | import WebError from 'web-error'; 2 | import ok from 'okay'; 3 | 4 | export function loadRole(req, res, next, name) { 5 | const rbac = req.server.rbac; 6 | 7 | if (!name) { 8 | return next(new WebError(400)); 9 | } 10 | 11 | rbac.getRole(name, ok(next, (role) => { 12 | if (!role) { 13 | return next(new WebError(404)); 14 | } 15 | 16 | req.objects.role = role; 17 | next(); 18 | })); 19 | } 20 | 21 | /** 22 | * Create new role 23 | */ 24 | export function create(req, res, next) { 25 | const rbac = req.server.rbac; 26 | 27 | if (!req.body.name) { 28 | return next(new WebError(400, 'Role name is undefined')); 29 | } 30 | 31 | rbac.createRole(req.body.name, ok(next, (role) => { 32 | if (!role) { 33 | return next(new WebError(400)); 34 | } 35 | 36 | return res.jsonp({ 37 | role: { 38 | name: role.name, 39 | }, 40 | }); 41 | })); 42 | } 43 | 44 | /** 45 | * Remove existing role 46 | */ 47 | export function remove(req, res, next) { 48 | const User = req.models.User; 49 | 50 | if (!req.objects.role) { 51 | return next(new WebError(404)); 52 | } 53 | 54 | const role = req.objects.role; 55 | 56 | // unassign role from all users 57 | User.removeRoleFromCollection(role.name, ok(next, () => { 58 | // remove role from rbac 59 | role.remove(ok(next, (isDeleted) => { 60 | if (!isDeleted) { 61 | return next(new WebError(400)); 62 | } 63 | 64 | return res.status(204).end(); 65 | })); 66 | })); 67 | } 68 | 69 | /** 70 | * Return true if role exists 71 | */ 72 | export function exists(req, res, next) { 73 | if (!req.objects.role) { 74 | return next(new WebError(404)); 75 | } 76 | 77 | return res.status(204).end(); 78 | } 79 | 80 | /** 81 | * Get role details 82 | */ 83 | export function get(req, res, next) { 84 | if (!req.objects.role) { 85 | return next(new WebError(404)); 86 | } 87 | 88 | const role = req.objects.role; 89 | 90 | return res.jsonp({ 91 | role: { 92 | name: role.name, 93 | }, 94 | }); 95 | } 96 | 97 | /** 98 | * Grant role or permission to the role 99 | */ 100 | export function grant(req, res, next) { 101 | const rbac = req.server.rbac; 102 | 103 | if (!req.objects.role || !req.body.name) { 104 | return next(new WebError(400)); 105 | } 106 | 107 | const role = req.objects.role; 108 | 109 | rbac.grantByName(role.name, req.body.name, ok(next, (isGranted) => { 110 | if (!isGranted) { 111 | return next(new WebError(400)); 112 | } 113 | 114 | return res.status(204).end(); 115 | })); 116 | } 117 | 118 | /** 119 | * Revoke role or permission to the role 120 | */ 121 | export function revoke(req, res, next) { 122 | const rbac = req.server.rbac; 123 | 124 | if (!req.objects.role || !req.body.name) { 125 | return next(new WebError(400)); 126 | } 127 | 128 | const role = req.objects.role; 129 | 130 | rbac.revokeByName(role.name, req.body.name, ok(next, (isRevoked) => { 131 | if (!isRevoked) { 132 | return next(new WebError(400)); 133 | } 134 | 135 | return res.status(204).end(); 136 | })); 137 | } 138 | -------------------------------------------------------------------------------- /src/controllers/token.js: -------------------------------------------------------------------------------- 1 | import WebError from 'web-error'; 2 | import ok from 'okay'; 3 | 4 | export function generateForCurrent(req, res, next) { 5 | const user = req.user; 6 | const options = req.server.options; 7 | 8 | if (!user) { 9 | return next(new WebError(401, 'User is undefined')); 10 | } 11 | 12 | res.jsonp({ 13 | token: user.generateBearerToken(options.token.secret, options.token.expiration), 14 | user: user.toPrivateJSON(), 15 | }); 16 | } 17 | 18 | export function generate(req, res, next) { 19 | const User = req.models.User; 20 | const options = req.server.options; 21 | 22 | if (!req.body.username || !req.body.password) { 23 | return next(new WebError(400, 'One of parameter missing')); 24 | } 25 | 26 | User.findByUsernamePassword(req.body.username, req.body.password, false, ok(next, (user) => { 27 | if (!user) { 28 | return next(new WebError(404, 'Invalid username or password')); 29 | } 30 | 31 | res.jsonp({ 32 | token: user.generateBearerToken(options.token.secret, options.token.expiration), 33 | user: user.toPrivateJSON(), 34 | }); 35 | })); 36 | } 37 | 38 | export function invalidate(req, res, next) { 39 | if (!req.body.access_token) { 40 | return next(new WebError(400, 'Token is missing')); 41 | } 42 | 43 | // TODO remove from keystore db and invalidate token 44 | return res.status(501).jsonp({}); 45 | } 46 | 47 | export function ensure(req, res, next) { 48 | req.server.secure.authenticate('bearer', { 49 | session: false, 50 | })(req, res, next); 51 | } 52 | 53 | export function ensureWithSession(req, res, next) { 54 | if (req.isAuthenticated() === true) { 55 | return next(); // already authenticated via session cookie 56 | } 57 | 58 | req.server.secure.authenticate('bearer', { 59 | session: false, 60 | })(req, res, next); 61 | } 62 | 63 | export function tryEnsure(req, res, next) { 64 | req.server.secure.authenticate(['bearer', 'anonymous'], { 65 | session: false, 66 | })(req, res, next); 67 | } 68 | -------------------------------------------------------------------------------- /src/controllers/user.js: -------------------------------------------------------------------------------- 1 | import WebError from 'web-error'; 2 | import tv4 from 'tv4'; 3 | import ok from 'okay'; 4 | 5 | export function isOwner(req, res, next) { 6 | if (!req.user || !req.objects.user) { 7 | return next(new WebError(401)); 8 | } 9 | 10 | if (!req.user.isMe(req.objects.user)) { 11 | return next(new WebError(401)); 12 | } 13 | 14 | next(); 15 | } 16 | 17 | export function loadByID(req, res, next, id) { 18 | const User = req.models.User; 19 | 20 | if (!id) { 21 | return next(new WebError(400)); 22 | } 23 | 24 | User.findById(id, ok(next, (user) => { 25 | if (!user) { 26 | return next(new WebError(404)); 27 | } 28 | 29 | req.objects.user = user; 30 | next(); 31 | })); 32 | } 33 | 34 | export function loadByPermalink(req, res, next, permalink) { 35 | const User = req.models.User; 36 | 37 | if (!permalink) { 38 | return next(new WebError(400)); 39 | } 40 | 41 | User.findOne({ 42 | permalink, 43 | }, ok(next, (user) => { 44 | if (!user) { 45 | return next(new WebError(404)); 46 | } 47 | 48 | req.objects.user = user; 49 | next(); 50 | })); 51 | } 52 | 53 | /** 54 | * Create user by simple registraion 55 | */ 56 | export function create(req, res, next) { 57 | const User = req.models.User; 58 | const options = req.server.options; 59 | 60 | exports.createSchema = exports.createSchema || User.getRestJSONSchema(); 61 | const result = tv4.validateMultiple(req.body, exports.createSchema); 62 | if (!result.valid) { 63 | return next(new WebError(400, 'Validation errors', result.errors)); 64 | } 65 | 66 | User.create(req.body, ok(next, (user) => { 67 | if (!user) { 68 | return next(new Error('User is undefined')); 69 | } 70 | 71 | res.jsonp({ 72 | token: user.generateBearerToken(options.token.secret, options.token.expiration), 73 | user: user.toPrivateJSON(), 74 | }); 75 | })); 76 | } 77 | 78 | export function remove(req, res, next) { 79 | const user = req.objects.user; 80 | if (!user) { 81 | return next(new WebError(404)); 82 | } 83 | 84 | user.remove(ok(next, () => { 85 | res.status(204).end(); 86 | })); 87 | } 88 | 89 | export function current(req, res, next) { 90 | const user = req.user; 91 | if (!user) { 92 | return next(new WebError(404)); 93 | } 94 | 95 | res.jsonp({ 96 | user: user.toPrivateJSON(), 97 | }); 98 | } 99 | -------------------------------------------------------------------------------- /src/controllers/userpermission.js: -------------------------------------------------------------------------------- 1 | import WebError from 'web-error'; 2 | import ok from 'okay'; 3 | 4 | export function getScope(req, res, next) { 5 | const rbac = req.server.rbac; 6 | const user = req.user; 7 | if (!user) { 8 | return next(new WebError(401)); 9 | } 10 | 11 | user.getScope(rbac, ok(next, (scope) => { 12 | res.jsonp({ scope }); 13 | })); 14 | } 15 | 16 | export function can(req, res, next) { 17 | const rbac = req.server.rbac; 18 | const user = req.user; 19 | if (!user) { 20 | return next(new WebError(401)); 21 | } 22 | 23 | const action = req.body.action; 24 | const resource = req.body.resource; 25 | if (!action || !resource) { 26 | return next(new WebError(400)); 27 | } 28 | 29 | user.can(rbac, action, resource, ok(next, (userCan) => { 30 | res.jsonp({ 31 | can: userCan, 32 | }); 33 | })); 34 | } 35 | 36 | export function addPermission(req, res, next) { 37 | const rbac = req.server.rbac; 38 | const user = req.user; 39 | if (!user) { 40 | return next(new WebError(401)); 41 | } 42 | 43 | const action = req.body.action; 44 | const resource = req.body.resource; 45 | if (!action || !resource) { 46 | return next(new WebError(400)); 47 | } 48 | 49 | user.addPermission(rbac, action, resource, ok(next, () => { 50 | res.status(204).end(); 51 | })); 52 | } 53 | 54 | export function removePermission(req, res, next) { 55 | const rbac = req.server.rbac; 56 | const user = req.user; 57 | if (!user) { 58 | return next(new WebError(401)); 59 | } 60 | 61 | const permissionName = req.body.permissionName; 62 | if (!permissionName) { 63 | return next(new WebError(400)); 64 | } 65 | 66 | user.removePermission(rbac, permissionName, ok(next, () => { 67 | res.status(204).end(); 68 | })); 69 | } 70 | 71 | export function hasRole(req, res, next) { 72 | const rbac = req.server.rbac; 73 | const user = req.user; 74 | if (!user) { 75 | return next(new WebError(401)); 76 | } 77 | 78 | const role = req.body.role; 79 | if (!role) { 80 | return next(new WebError(400)); 81 | } 82 | 83 | user.hasRole(rbac, role, ok(next, (has) => { 84 | res.jsonp({ has }); 85 | })); 86 | } 87 | 88 | export function setRole(req, res, next) { 89 | const rbac = req.server.rbac; 90 | const user = req.user; 91 | if (!user) { 92 | return next(new WebError(401)); 93 | } 94 | 95 | const role = req.body.role; 96 | if (!role) { 97 | return next(new WebError(400)); 98 | } 99 | 100 | user.setRole(rbac, role, ok(next, () => { 101 | res.status(204).end(); 102 | })); 103 | } 104 | 105 | export function removeRole(req, res, next) { 106 | const user = req.user; 107 | if (!user) { 108 | return next(new WebError(401)); 109 | } 110 | 111 | user.removeRole(ok(next, () => { 112 | res.status(204).end(); 113 | })); 114 | } 115 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Server from './server'; 2 | import MiddlewareType from './constants/MiddlewareType'; 3 | import * as controllers from './controllers'; 4 | 5 | export { MiddlewareType, controllers }; 6 | export default Server; 7 | -------------------------------------------------------------------------------- /src/models.js: -------------------------------------------------------------------------------- 1 | import { each } from 'async'; 2 | 3 | export default class Models { 4 | constructor(server, options = {}) { 5 | this._options = options; 6 | this._server = server; 7 | 8 | this._models = new Map(); 9 | this._modelFactories = new Map(); 10 | } 11 | 12 | get options() { 13 | return this._options; 14 | } 15 | 16 | get server() { 17 | return this._server; 18 | } 19 | 20 | _createModelFromFactory(name) { 21 | if (!this._modelFactories.has(name)) { 22 | throw new Error(`Modul is not registered: ${name}`); 23 | } 24 | 25 | const modelFactory = this._modelFactories.get(name); 26 | const config = { 27 | model: null, 28 | callbacks: [], 29 | }; 30 | 31 | if (typeof modelFactory !== 'function') { 32 | throw new Error(`Model factory is not a function for model: ${name}`); 33 | } 34 | 35 | config.model = modelFactory(this.server, function modelFactoryCallback(error) { 36 | config.loaded = true; 37 | config.error = error; 38 | 39 | config.callbacks.forEach(function eachCallback(callback) { 40 | callback(error, config.model); 41 | }); 42 | 43 | config.callbacks = []; 44 | }); 45 | 46 | this._models.set(name, config); 47 | } 48 | 49 | model(name, callback) { 50 | if (!this._models.has(name)) { 51 | this._createModelFromFactory(name); 52 | } 53 | 54 | const config = this._models.get(name); 55 | 56 | if (!callback) { 57 | return config.model; 58 | } 59 | 60 | // TODO replace it with async.memorize 61 | if (config.loaded) { 62 | callback(config.error, config.model); 63 | } else { 64 | config.callbacks.push(callback); 65 | } 66 | 67 | return config.model; 68 | } 69 | 70 | register(modelModul) { 71 | const name = modelModul.name; 72 | if (!name) { 73 | throw new Error('Model has no name'); 74 | } 75 | 76 | this._modelFactories.set(name, modelModul.default 77 | ? modelModul.default 78 | : modelModul); 79 | 80 | Object.defineProperty(this, name, { 81 | get: function getProperty() { 82 | return this.model(name); 83 | }, 84 | }); 85 | } 86 | 87 | preload(callback) { 88 | const keys = []; 89 | this._modelFactories.forEach(function eachModelFactory(factory, modelName) { 90 | keys.push(modelName); 91 | }); 92 | 93 | each(keys, (modelName, cb) => this.model(modelName, cb), callback); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/models/provider.js: -------------------------------------------------------------------------------- 1 | export const name = 'Provider'; 2 | 3 | /** 4 | * Generate provider uid name from provider name and user ID 5 | * @param {String} providerName Provider name 6 | * @param {String} uid User ID 7 | * @return {String} Provider UID 8 | */ 9 | export function genNameUID(providerName, uid) { 10 | return providerName + '_' + uid; 11 | } 12 | 13 | export function createSchema(Schema) { 14 | // add properties to schema 15 | const schema = new Schema({ 16 | user: { type: Schema.Types.ObjectId, ref: 'User', required: true }, 17 | name: { type: String, required: true }, 18 | uid: { type: String, required: true }, 19 | nameUID: { type: String, required: true }, 20 | data: { type: String }, 21 | }); 22 | 23 | // add indexes 24 | schema.index({ user: 1, name: 1 }); 25 | schema.index({ nameUID: 1 }, { unique: true }); 26 | 27 | // add preprocess validation 28 | schema.pre('save', function saveCallback(next) { 29 | // only hash the password if it has been modified (or is new) 30 | if (this.isModified('name') || this.isModified('uid') || !this.nameUID) { 31 | this.nameUID = genNameUID(this.name, this.uid); 32 | } 33 | 34 | next(); 35 | }); 36 | 37 | return schema; 38 | } 39 | -------------------------------------------------------------------------------- /src/models/user.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import bcrypt from 'bcryptjs'; 3 | import { genNameUID } from './provider'; 4 | import permalink from 'mongoose-permalink'; 5 | import mongooseHRBAC from 'mongoose-hrbac'; 6 | import jsonSchemaPlugin from 'mongoose-json-schema'; 7 | import ok from 'okay'; 8 | import { series } from 'async'; 9 | 10 | export const name = 'User'; 11 | 12 | // max of 5 attempts, resulting in a 2 hour lock 13 | const SALT_WORK_FACTOR = 10; 14 | // const MAX_LOGIN_ATTEMPTS = 5; 15 | // const LOCK_TIME = 2 * 60 * 60 * 1000; 16 | 17 | function toPrivateJSON() { 18 | const data = this.toJSON({ virtuals: true }); 19 | data.id = data._id; 20 | 21 | delete data._id; 22 | delete data.__v; 23 | 24 | return data; 25 | } 26 | 27 | function getDisplayName() { 28 | return this.name || this.username; 29 | } 30 | 31 | function updateUserByFacebookProfile(user, profile, callback) { 32 | let changed = false; 33 | 34 | if (!user.firstName && profile.first_name) { 35 | user.firstName = profile.first_name; 36 | changed = true; 37 | } 38 | 39 | if (!user.lastName && profile.last_name) { 40 | user.lastName = profile.last_name; 41 | changed = true; 42 | } 43 | 44 | if (!user.name && profile.name) { 45 | user.name = profile.name; 46 | changed = true; 47 | } 48 | 49 | if (!user.locale && profile.locale) { 50 | user.locale = profile.locale; 51 | changed = true; 52 | } 53 | 54 | series([ 55 | // try to setup email 56 | (cb) => { 57 | const User = user.models('User'); 58 | 59 | if (!user.email && profile.email) { 60 | User.findOne({ 61 | email: profile.email, 62 | }, (err, foundedUser) => { 63 | if (err) { 64 | return cb(err); 65 | } 66 | 67 | if (!foundedUser) { 68 | user.email = profile.email; 69 | changed = true; 70 | } 71 | 72 | cb(null); 73 | }); 74 | } else { 75 | cb(null); 76 | } 77 | }, 78 | // save user 79 | (cb) => { 80 | if (!changed) { 81 | return cb(null); 82 | } 83 | 84 | user.save(cb); 85 | }, 86 | ], (err) => { 87 | callback(err, user); 88 | }); 89 | } 90 | 91 | /** 92 | * Create user by user profile from facebook 93 | * @param {Object} profile Profile from facebook 94 | * @param {Function} callback Callback with created user 95 | */ 96 | function createByFacebook(profile, callback) { 97 | if (!profile.id) { 98 | return callback(new Error('Profile id is undefined')); 99 | } 100 | 101 | this.findByFacebookID(profile.id, ok(callback, (user) => { 102 | if (user) { 103 | return updateUserByFacebookProfile(user, profile, callback); 104 | } 105 | 106 | this.create({ 107 | username: profile.username || null, 108 | firstName: profile.first_name, 109 | lastName: profile.last_name, 110 | name: profile.name, 111 | email: profile.email, 112 | locale: profile.locale, 113 | }, ok(callback, (newUser) => { 114 | newUser.addProvider('facebook', profile.id, profile, ok(callback, () => { 115 | callback(null, newUser); 116 | })); 117 | })); 118 | })); 119 | } 120 | 121 | /** 122 | * Create user by user profile from twitter 123 | * @param {Object} profile Profile from twitter 124 | * @param {Function} callback Callback with created user 125 | */ 126 | function createByTwitter(profile, callback) { 127 | if (!profile.id) { 128 | return callback(new Error('Profile id is undefined')); 129 | } 130 | 131 | this.findByTwitterID(profile.id, ok(callback, (user) => { 132 | if (user) { 133 | return callback(null, user); 134 | } 135 | 136 | this.create({ 137 | username: profile.username || null, 138 | name: profile.displayName, 139 | }, ok(callback, (newUser) => { 140 | newUser.addProvider('twitter', profile.id, profile, ok(callback, () => { 141 | callback(null, newUser); 142 | })); 143 | })); 144 | })); 145 | } 146 | 147 | /** 148 | * Generate access token for actual user 149 | * @param {String} Secret for generating of token 150 | * @param {[Number]} expiresInMinutes 151 | * @param {[Array]} scope List of scopes 152 | * @return {Object} Access token of user 153 | */ 154 | function generateBearerToken(tokenSecret, expiresInMinutes = 60 * 24 * 14, scope = []) { 155 | if (!tokenSecret) { 156 | throw new Error('Token secret is undefined'); 157 | } 158 | 159 | const data = { 160 | user: this._id.toString(), 161 | }; 162 | 163 | if (scope.length) { 164 | data.scope = scope; 165 | } 166 | 167 | const token = jwt.sign(data, tokenSecret, { 168 | expiresInMinutes, 169 | }); 170 | 171 | return { 172 | type: 'Bearer', 173 | value: token, 174 | }; 175 | } 176 | 177 | function isMe(user) { 178 | return user && this._id.toString() === user._id.toString(); 179 | } 180 | 181 | function findByUsername(username, strict, callback) { 182 | if (typeof strict === 'function') { 183 | callback = strict; 184 | strict = true; 185 | } 186 | 187 | if (strict) { 188 | return this.findOne({ username }, callback); 189 | } 190 | 191 | return this.findOne({ $or: [ 192 | { username }, 193 | { email: username }, 194 | ]}, callback); 195 | } 196 | 197 | /** 198 | * Find user by his facebook ID 199 | * @param {String} id Facebook id of user assigned in database 200 | * @param {Function} callback 201 | */ 202 | function findByFacebookID(uid, callback) { 203 | return this.findByProviderUID('facebook', uid, callback); 204 | } 205 | 206 | function findByTwitterID(uid, callback) { 207 | return this.findByProviderUID('twitter', uid, callback); 208 | } 209 | 210 | function findByProviderUID(providerName, uid, callback) { 211 | const Provider = this.model('Provider'); 212 | 213 | return Provider.findOne({ 214 | nameUID: genNameUID(providerName, uid), 215 | }).populate('user').exec(ok(callback, (provider) => { 216 | if (!provider) { 217 | return callback(null, provider); 218 | } 219 | 220 | return callback(null, provider.user); 221 | })); 222 | } 223 | 224 | /** 225 | * Find user by his username/email and his password 226 | * @param {String} username Username or email of user 227 | * @param {String} password Password of user 228 | * @param {Function} callback 229 | */ 230 | function findByUsernamePassword(username, password, strict, callback) { 231 | if (typeof strict === 'function') { 232 | callback = strict; 233 | strict = true; 234 | } 235 | 236 | return this.findByUsername(username, strict, ok(callback, (user) => { 237 | if (!user) { 238 | return callback(null, null); 239 | } 240 | 241 | user.comparePassword(password, ok(callback, (isMatch) => { 242 | if (!isMatch) { 243 | return callback(null, null); 244 | } 245 | 246 | callback(null, user); 247 | })); 248 | })); 249 | } 250 | 251 | function addProvider(providerName, providerUID, data, callback) { 252 | const Provider = this.model('Provider'); 253 | 254 | this.hasProvider(providerName, providerUID, ok(callback, (has) => { 255 | if (has) { 256 | return callback(new Error('This provider is already associated to this user')); 257 | } 258 | 259 | Provider.create({ 260 | user: this._id, 261 | name: providerName, 262 | uid: providerUID, 263 | nameUID: genNameUID(providerName, providerUID), 264 | data: JSON.stringify(data), 265 | }, callback); 266 | })); 267 | } 268 | 269 | function removeProvider(providerName, providerUID, callback) { 270 | const Provider = this.model('Provider'); 271 | 272 | if (!providerName || !providerUID) { 273 | return callback(new Error('Provider name or uid is undefined')); 274 | } 275 | 276 | Provider.remove({ 277 | user: this._id, 278 | nameUID: genNameUID(providerName, providerUID), 279 | }, callback); 280 | } 281 | 282 | function getProvider(providerName, providerUID, callback) { 283 | if (typeof providerUID === 'function') { 284 | callback = providerUID; 285 | providerUID = false; 286 | } 287 | 288 | const Provider = this.model('Provider'); 289 | const query = { 290 | user: this._id, 291 | }; 292 | 293 | if (!providerUID) { 294 | query.name = providerName; 295 | } else { 296 | query.nameUID = genNameUID(providerName, providerUID); 297 | } 298 | 299 | return Provider.findOne(query, callback); 300 | } 301 | 302 | function hasProvider(providerName, providerUID, callback) { 303 | if (typeof providerUID === 'function') { 304 | callback = providerUID; 305 | providerUID = false; 306 | } 307 | 308 | this.getProvider(providerName, providerUID, ok(callback, (provider) => { 309 | callback(null, !!provider); 310 | })); 311 | } 312 | 313 | /** 314 | * Compare user entered password with stored user's password 315 | * @param {String} candidatePassword 316 | * @param {Function} callback 317 | */ 318 | function comparePassword(candidatePassword, callback) { 319 | bcrypt.compare(candidatePassword, this.password, (err, isMatch) => { 320 | if (err) { 321 | return callback(err); 322 | } 323 | 324 | callback(null, isMatch); 325 | }); 326 | } 327 | 328 | function hasPassword() { 329 | return !!this.password; 330 | } 331 | 332 | function setPassword(password, callback) { 333 | this.password = password; 334 | return this.save(callback); 335 | } 336 | 337 | function hasEmail() { 338 | return !!this.email ? true : false; 339 | } 340 | 341 | function setEmail(email, callback) { 342 | this.email = email; 343 | return this.save(callback); 344 | } 345 | 346 | function hasUsername() { 347 | return !!this.username; 348 | } 349 | 350 | function setUsername(username, callback) { 351 | this.username = username; 352 | return this.save(callback); 353 | } 354 | 355 | /* 356 | function incLoginAttempts(callback) { 357 | // if we have a previous lock that has expired, restart at 1 358 | if (this.lockUntil && this.lockUntil < Date.now()) { 359 | return this.update({ 360 | $set: { loginAttempts: 1 }, 361 | $unset: { lockUntil: 1 } 362 | }, callback); 363 | } 364 | // otherwise we're incrementing 365 | var updates = { 366 | $inc: { 367 | loginAttempts: 1 368 | } 369 | }; 370 | 371 | // lock the account if we've reached max attempts and it's not locked already 372 | if (this.loginAttempts + 1 >= MAX_LOGIN_ATTEMPTS && !this.isLocked) { 373 | updates.$set = { 374 | lockUntil: Date.now() + LOCK_TIME 375 | }; 376 | } 377 | 378 | return this.update(updates, callback); 379 | }*/ 380 | 381 | /** 382 | * Create schema for model 383 | * @param {mongoose.Schema} Schema 384 | * @return {mongoose.Schema} User Instance of user schema 385 | */ 386 | export function createSchema(Schema) { 387 | // add properties to schema 388 | const schema = new Schema({ 389 | firstName: { type: String }, 390 | lastName: { type: String }, 391 | name: { type: String }, 392 | 393 | email: { type: String, unique: true, sparse: true }, 394 | username: { type: String, unique: true, sparse: true }, 395 | 396 | password: { type: String }, 397 | 398 | locale: { type: String }, 399 | 400 | loginAttempts: { type: Number, required: true, 'default': 0 }, 401 | lockUntil: { type: Number }, 402 | }); 403 | 404 | schema.virtual('isLocked').get(function isLocked() { 405 | // check for a future lockUntil timestamp 406 | return !!(this.lockUntil && this.lockUntil > Date.now()); 407 | }); 408 | 409 | schema.pre('validate', function validate(next) { 410 | const user = this; 411 | 412 | // update name 413 | if ((user.isModified('firstName') || user.isModified('lastName')) && !user.isModified('name')) { 414 | if (user.firstName && user.lastName) { 415 | user.name = user.firstName + ' ' + user.lastName; 416 | } else { 417 | user.name = user.firstName || user.lastName; 418 | } 419 | } 420 | 421 | next(); 422 | }); 423 | 424 | // add preprocess validation 425 | schema.pre('save', function save(next) { 426 | const user = this; 427 | 428 | // only hash the password if it has been modified (or is new) 429 | if (!user.isModified('password')) { 430 | return next(); 431 | } 432 | 433 | // hash the password using our new salt 434 | bcrypt.hash(user.password, SALT_WORK_FACTOR, (err, hash) => { 435 | if (err) { 436 | return next(err); 437 | } 438 | 439 | // override the cleartext password with the hashed one 440 | user.password = hash; 441 | next(); 442 | }); 443 | }); 444 | 445 | // add RBAC permissions 446 | schema.plugin(mongooseHRBAC, { 447 | defaultRole: 'user', 448 | }); 449 | 450 | // add permalink 451 | schema.plugin(permalink, { 452 | sources: ['name', 'firstName', 'lastName', 'username'], 453 | pathOptions: { 454 | restExclude: true, 455 | }, 456 | }); 457 | 458 | schema.plugin(jsonSchemaPlugin, {}); 459 | 460 | schema.methods.generateBearerToken = generateBearerToken; 461 | 462 | // auth 463 | schema.methods.isMe = isMe; 464 | 465 | // password 466 | schema.methods.hasPassword = hasPassword; 467 | schema.methods.setPassword = setPassword; 468 | schema.methods.comparePassword = comparePassword; 469 | // schema.methods.incLoginAttempts = incLoginAttempts; 470 | 471 | // email 472 | schema.methods.hasEmail = hasEmail; 473 | schema.methods.setEmail = setEmail; 474 | 475 | // username 476 | schema.methods.hasUsername = hasUsername; 477 | schema.methods.setUsername = setUsername; 478 | 479 | // create 480 | schema.statics.createByFacebook = createByFacebook; 481 | schema.statics.createByTwitter = createByTwitter; 482 | 483 | // search 484 | schema.statics.findByUsername = findByUsername; 485 | schema.statics.findByUsernamePassword = findByUsernamePassword; 486 | 487 | schema.statics.findByProviderUID = findByProviderUID; 488 | schema.statics.findByFacebookID = findByFacebookID; 489 | schema.statics.findByTwitterID = findByTwitterID; 490 | 491 | 492 | // providers 493 | schema.methods.addProvider = addProvider; 494 | schema.methods.removeProvider = removeProvider; 495 | schema.methods.getProvider = getProvider; 496 | schema.methods.hasProvider = hasProvider; 497 | 498 | schema.methods.toPrivateJSON = toPrivateJSON; 499 | schema.methods.getDisplayName = getDisplayName; 500 | 501 | return schema; 502 | } 503 | 504 | 505 | /* 506 | UserSchema.statics.getAuthenticated = function(username, password, cb) { 507 | this.findOne({ username: username }, function(err, user) { 508 | if (err) { 509 | return cb(err); 510 | } 511 | 512 | // make sure the user exists 513 | if (!user) { 514 | return cb(null, null, reasons.NOT_FOUND); 515 | } 516 | 517 | // check if the account is currently locked 518 | if (user.isLocked) { 519 | // just increment login attempts if account is already locked 520 | return user.incLoginAttempts(function(err) { 521 | if (err) return cb(err); 522 | return cb(null, null, reasons.MAX_ATTEMPTS); 523 | }); 524 | } 525 | 526 | // test for a matching password 527 | user.comparePassword(password, function(err, isMatch) { 528 | if (err) return cb(err); 529 | 530 | // check if the password was a match 531 | if (isMatch) { 532 | // if there's no lock or failed attempts, just return the user 533 | if (!user.loginAttempts && !user.lockUntil) return cb(null, user); 534 | // reset attempts and lock info 535 | var updates = { 536 | $set: { loginAttempts: 0 }, 537 | $unset: { lockUntil: 1 } 538 | }; 539 | return user.update(updates, function(err) { 540 | if (err) return cb(err); 541 | return cb(null, user); 542 | }); 543 | } 544 | 545 | // password is incorrect, so increment login attempts before responding 546 | user.incLoginAttempts(function(err) { 547 | if (err) return cb(err); 548 | return cb(null, null, reasons.PASSWORD_INCORRECT); 549 | }); 550 | }); 551 | }); 552 | }; 553 | */ 554 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | const projectRoot = path.normalize(`${__dirname}/../../..`); 4 | 5 | export default { 6 | root: `${projectRoot}/app`, 7 | 8 | rbac: { 9 | options: {}, 10 | role: { 11 | guest: 'guest', 12 | }, 13 | }, 14 | 15 | log: true, 16 | 17 | morgan: { 18 | format: process.env.NODE_ENV === 'development' ? 'dev' : 'tiny', 19 | options: { 20 | // stream: process.stdout 21 | }, 22 | }, 23 | 24 | server: { 25 | build: 1, 26 | host: process.env.HOST || '127.0.0.1', 27 | port: process.env.PORT 28 | ? parseInt(process.env.PORT, 10) 29 | : 4000, 30 | }, 31 | 32 | request: { 33 | timeout: 1000 * 60 * 5, 34 | }, 35 | 36 | compression: {}, 37 | 38 | powered: { 39 | value: 'Maglev', 40 | }, 41 | 42 | responseTime: {}, 43 | 44 | methodOverride: { 45 | // https://github.com/expressjs/method-override 46 | enabled: true, 47 | getter: 'X-HTTP-Method-Override', 48 | options: {}, 49 | }, 50 | 51 | bodyParser: [{ 52 | parse: 'urlencoded', 53 | options: { 54 | extended: true, 55 | }, 56 | }, { 57 | parse: 'json', 58 | options: {}, 59 | }, { 60 | parse: 'json', 61 | options: { 62 | type: 'application/vnd.api+json', 63 | }, 64 | }], 65 | 66 | cookieParser: { 67 | secret: null, 68 | options: {}, 69 | }, 70 | 71 | token: { 72 | secret: null, 73 | expiration: 60 * 24 * 14, 74 | }, 75 | 76 | session: { 77 | secret: null, 78 | cookie: { 79 | maxAge: 14 * 24 * 60 * 60 * 1000, // 2 weeks 80 | }, 81 | resave: true, 82 | saveUninitialized: true, 83 | }, 84 | 85 | sessionRecovery: { 86 | tries: 3, 87 | }, 88 | 89 | view: { 90 | engine: 'swig', 91 | }, 92 | 93 | router: { 94 | api: { 95 | path: '/api', 96 | }, 97 | }, 98 | 99 | locale: { 100 | 'default': 'en', 101 | available: ['en'], 102 | inUrl: false, 103 | }, 104 | 105 | country: { 106 | 'default': null, 107 | available: [], 108 | inUrl: false, 109 | }, 110 | 111 | registration: { 112 | simple: true, 113 | }, 114 | 115 | facebook: { 116 | appID: null, 117 | appSecret: null, 118 | namespace: null, 119 | }, 120 | 121 | upload: { 122 | maxFieldsSize: 1024 * 1024 * 20, 123 | maxFields: 1000, 124 | path: null, 125 | }, 126 | 127 | cors: {}, 128 | 129 | page: { 130 | error: null, 131 | notFound: null, 132 | }, 133 | 134 | strategies: [], 135 | 136 | css: { 137 | path: '/public/css', 138 | root: `${projectRoot}/public/css`, 139 | options: { 140 | render: { 141 | ieCompat: false, 142 | }, 143 | }, 144 | }, 145 | 146 | 'static': { 147 | path: '/public', 148 | root: `${projectRoot}/public`, 149 | options: { 150 | index: ['index.html'], 151 | maxAge: '31 days', 152 | }, 153 | }, 154 | 155 | favicon: { 156 | root: `${projectRoot}/public/favicon.ico`, 157 | options: {}, 158 | }, 159 | 160 | robots: { 161 | root: `${projectRoot}/public/robots.txt`, 162 | }, 163 | 164 | memoryLeaks: { 165 | watch: false, 166 | showHeap: false, 167 | path: null, 168 | }, 169 | 170 | socket: { 171 | idleTimeout: 10 * 1000, 172 | }, 173 | 174 | shutdown: { 175 | timeout: 30 * 1000, 176 | }, 177 | 178 | sourceMap: { 179 | root: 'public/dist', 180 | }, 181 | }; 182 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import methods from 'methods'; 2 | import express from 'express'; 3 | import vhost from 'vhost'; 4 | 5 | export default class Router { 6 | constructor(options = {}, parent = null) { 7 | this._options = options; 8 | this._expressRouter = express.Router(options); 9 | this._parent = parent; 10 | } 11 | 12 | get parent() { 13 | return this._parent; 14 | } 15 | 16 | get expressRouter() { 17 | return this._expressRouter; 18 | } 19 | 20 | end() { 21 | return this.parent; 22 | } 23 | 24 | vhost(host) { 25 | if (this._parent) { 26 | throw new Error('Vhost must be first fn'); 27 | } 28 | 29 | const router = new Router(this._options, this); 30 | this.expressRouter.use(vhost(host, router.expressRouter)); 31 | return router; 32 | } 33 | 34 | route(prefix) { 35 | const router = new Router(this._options, this); 36 | this.expressRouter.use(prefix, router.expressRouter); 37 | return router; 38 | } 39 | 40 | api(prefix) { 41 | return this.route(prefix || this._options.api.path); 42 | } 43 | 44 | param(...args) { 45 | this.expressRouter.param.apply(this.expressRouter, args); 46 | return this; 47 | } 48 | } 49 | 50 | methods.forEach(function eachMethod(method) { 51 | Router.prototype[method] = function methodHandler(...args) { 52 | const expressRouter = this.expressRouter; 53 | expressRouter[method].apply(expressRouter, args); 54 | return this; 55 | }; 56 | }); 57 | -------------------------------------------------------------------------------- /src/secure.js: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | import * as strategy from './strategy'; 3 | 4 | export default class Secure { 5 | constructor(server) { 6 | this._server = server; 7 | 8 | this._prepare(); 9 | } 10 | 11 | get server() { 12 | return this._server; 13 | } 14 | 15 | get passport() { 16 | return passport; 17 | } 18 | 19 | _prepare() { 20 | const server = this.server; 21 | const pp = this.passport; 22 | 23 | pp.serializeUser(function serializeUserCallback(user, done) { 24 | done(null, user.id); 25 | }); 26 | 27 | pp.deserializeUser(function deserializeUserCallback(id, done) { 28 | const User = server.models.User; 29 | 30 | User.findById(id, done); 31 | }); 32 | 33 | const options = server.options; 34 | const models = server.models; 35 | 36 | pp.use(strategy.anonymous(options, models)); 37 | pp.use(strategy.local(options, models)); 38 | pp.use(strategy.bearer(options, models)); 39 | 40 | options.strategies.forEach(function eachStrategy(strategy2) { 41 | pp.use(strategy2(options, models)); 42 | }); 43 | } 44 | 45 | authenticate(...args) { 46 | const pp = this.passport; 47 | return pp.authenticate.apply(pp, args); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import RBAC from 'rbac'; 3 | import extend from 'node.extend'; 4 | import defaultOptions from './options'; 5 | import Router from './router'; 6 | import App from './app'; 7 | import Secure from './secure'; 8 | import Models from './models'; 9 | import debug from 'debug'; 10 | import domain from 'domain'; 11 | import { EventEmitter } from 'events'; 12 | import ok from 'okay'; 13 | import { each } from 'async'; 14 | 15 | const log = debug('maglev:server'); 16 | const portOffset = parseInt(process.env.NODE_APP_INSTANCE || 0, 10); 17 | 18 | function walk(path, fileCallback, callback) { 19 | fs.readdir(path, ok(callback, (files) => { 20 | each(files, (file, cb) => { 21 | const newPath = path + '/' + file; 22 | fs.stat(newPath, ok(cb, (stat) => { 23 | if (stat.isFile()) { 24 | if (/(.*)\.(js$|coffee$)/.test(file)) { 25 | const model = require(newPath); 26 | return fileCallback(model, newPath, file, cb); 27 | } 28 | } else if (stat.isDirectory()) { 29 | return walk(newPath, fileCallback, cb); 30 | } 31 | 32 | cb(); 33 | })); 34 | }, callback); 35 | })); 36 | } 37 | 38 | export default class Server extends EventEmitter { 39 | constructor(options, callback) { 40 | super(); 41 | 42 | if (!callback) { 43 | throw new Error('Please use callback for server'); 44 | } 45 | 46 | options = extend(true, {}, defaultOptions, options); 47 | 48 | if (!options.db) { 49 | throw new Error('Db is not defined'); 50 | } 51 | 52 | this._options = options; 53 | this._db = options.db; 54 | 55 | this.catchErrors(() => { 56 | this.init(options, callback); 57 | }); 58 | } 59 | 60 | handleError(err) { 61 | log(err); 62 | 63 | this.emit('err', err); 64 | 65 | this.closeGracefully(); 66 | } 67 | 68 | catchErrors(callback) { 69 | const dom = domain.create(); 70 | 71 | dom.id = 'ServerDomain'; 72 | dom.on('error', (err) => this.handleError(err)); 73 | 74 | try { 75 | dom.run(callback); 76 | } catch (err) { 77 | process.nextTick(() => this.handleError(err)); 78 | } 79 | } 80 | 81 | init(options, callback) { 82 | // catch system termination 83 | const signals = ['SIGINT', 'SIGTERM', 'SIGQUIT']; 84 | signals.forEach((signal) => { 85 | process.on(signal, () => this.closeGracefully()); 86 | }); 87 | 88 | // catch PM2 termination 89 | process.on('message', (msg) => { 90 | if (msg === 'shutdown') { 91 | this.closeGracefully(); 92 | } 93 | }); 94 | 95 | this._rbac = new RBAC(options.rbac.options, ok(callback, () => { 96 | this._router = new Router(options.router); // router is used in app 97 | this._models = new Models(this, options.models); // models is used in secure 98 | this._secure = new Secure(this); 99 | 100 | this._app = new App(this, options); 101 | 102 | this._loadRoutes(ok(callback, () => { 103 | this._loadModels(ok(callback, () => { 104 | callback(null, this); 105 | })); 106 | })); 107 | })); 108 | } 109 | 110 | notifyPM2Online() { 111 | if (!process.send) { 112 | return; 113 | } 114 | 115 | // after callback 116 | process.nextTick(function notify() { 117 | process.send('online'); 118 | }); 119 | } 120 | 121 | get options() { 122 | return this._options; 123 | } 124 | 125 | get rbac() { 126 | return this._rbac; 127 | } 128 | 129 | get db() { 130 | return this._db; 131 | } 132 | 133 | get secure() { 134 | return this._secure; 135 | } 136 | 137 | get app() { 138 | return this._app; 139 | } 140 | 141 | get router() { 142 | return this._router; 143 | } 144 | 145 | get models() { 146 | return this._models; 147 | } 148 | 149 | listen(callback) { 150 | if (!callback) { 151 | throw new Error('Callback is undefined'); 152 | } 153 | 154 | if (this._listening) { 155 | callback(new Error('Server is already listening')); 156 | return this; 157 | } 158 | 159 | this._listening = true; 160 | 161 | const options = this.options; 162 | this.app.listen(options.server.port + portOffset, options.server.host, ok(callback, () => { 163 | log(`Server is listening on port: ${this.app.httpServer.address().port}`); 164 | 165 | this.notifyPM2Online(); 166 | 167 | callback(null, this); 168 | })); 169 | 170 | return this; 171 | } 172 | 173 | close(callback) { 174 | if (!callback) { 175 | throw new Error('Callback is undefined'); 176 | } 177 | 178 | if (!this._listening) { 179 | callback(new Error('Server is not listening')); 180 | return this; 181 | } 182 | 183 | this._listening = false; 184 | 185 | this.app.close(callback); 186 | 187 | return this; 188 | } 189 | 190 | closeGracefully() { 191 | log('Received kill signal (SIGTERM), shutting down gracefully.'); 192 | if (!this._listening) { 193 | log('Ended without any problem'); 194 | process.exit(0); 195 | return; 196 | } 197 | 198 | let termTimeoutID = null; 199 | 200 | this.close(function closeCallback(err) { 201 | if (termTimeoutID) { 202 | clearTimeout(termTimeoutID); 203 | termTimeoutID = null; 204 | } 205 | 206 | if (err) { 207 | log(err.message); 208 | process.exit(1); 209 | return; 210 | } 211 | 212 | log('Ended without any problem'); 213 | process.exit(0); 214 | }); 215 | 216 | const options = this.options; 217 | termTimeoutID = setTimeout(function timeoutCallback() { 218 | termTimeoutID = null; 219 | log('Could not close connections in time, forcefully shutting down'); 220 | process.exit(1); 221 | }, options.shutdown.timeout); 222 | } 223 | 224 | _loadModels(callback) { 225 | const models = this._models; 226 | const path = this.options.root + '/models'; 227 | 228 | walk(path, (model, modelPath, file, cb) => { 229 | try { 230 | log(`Loading model: ${modelPath}`); 231 | models.register(model); 232 | cb(); 233 | } catch (err) { 234 | log(`Problem with model: ${modelPath}`); 235 | cb(err); 236 | } 237 | }, ok(callback, () => { 238 | // preload all models 239 | models.preload(callback); 240 | })); 241 | } 242 | 243 | _loadRoutes(callback) { 244 | const router = this.router; 245 | const path = this.options.root + '/routes'; 246 | 247 | walk(path, (route, routePath, file, cb) => { 248 | try { 249 | log(`Loading route: ${routePath}`); 250 | const routeFn = route.default ? route.default : route; 251 | routeFn(router); 252 | cb(); 253 | } catch (err) { 254 | log(`Problem with route: ${routePath}`); 255 | cb(err); 256 | } 257 | }, callback); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/strategy.js: -------------------------------------------------------------------------------- 1 | import { Strategy as LocalStrategy } from 'passport-local'; 2 | import { Strategy as BearerStrategy } from 'passport-http-bearer'; 3 | import { Strategy as AnonymousStrategy } from 'passport-anonymous'; 4 | 5 | import { Strategy as FacebookStrategy } from 'passport-facebook'; 6 | import { Strategy as TwitterStrategy } from 'passport-twitter'; 7 | import { Strategy as FacebookCanvasStrategy } from 'passport-facebook-canvas'; 8 | 9 | import jwt from 'jsonwebtoken'; 10 | import WebError from 'web-error'; 11 | 12 | export function anonymous() { 13 | return new AnonymousStrategy(); 14 | } 15 | 16 | export function local(options, models) { 17 | // Use local strategy TODO find by object 18 | return new LocalStrategy({ 19 | usernameField: 'username', 20 | passwordField: 'password', 21 | }, (email, password, done) => { 22 | const User = models.User; 23 | 24 | User.findOne({ 25 | email, 26 | }, (err, user) => { 27 | if (err) { 28 | return done(err); 29 | } 30 | 31 | if (!user) { 32 | return done(null, false, { 33 | message: 'Unknown user', 34 | }); 35 | } 36 | 37 | if (!user.authenticate(password)) { 38 | return done(null, false, { 39 | message: 'Invalid password', 40 | }); 41 | } 42 | 43 | return done(null, user); 44 | }); 45 | }); 46 | } 47 | 48 | export function bearer(options, models) { 49 | return new BearerStrategy((token, done) => { 50 | const User = models.User; 51 | 52 | if (!token) { 53 | return done(new WebError(401, 'Invalid token')); 54 | } 55 | 56 | jwt.verify(token, options.token.secret, (err, data) => { 57 | if (err) { 58 | return done(new WebError(401, err.message)); 59 | } 60 | 61 | if (!data.user) { 62 | return done(new WebError(404, 'Unknown user')); 63 | } 64 | 65 | User.findById(data.user, (err2, user) => { 66 | if (err2) { 67 | return done(err2); 68 | } 69 | 70 | if (!user) { 71 | return done(new WebError(404, 'Unknown user')); 72 | } 73 | 74 | return done(null, user); 75 | }); 76 | }); 77 | }); 78 | } 79 | 80 | export function facebook(options, models) { 81 | return new FacebookStrategy({ 82 | clientID: options.facebook.appID, 83 | clientSecret: options.facebook.appSecret, 84 | }, (accessToken, refreshToken, profile, done) => { 85 | const User = models.User; 86 | 87 | if (!profile.id) { 88 | return done(new Error('Profile ID is null')); 89 | } 90 | 91 | if (!options.facebook.appID || !options.facebook.appSecret) { 92 | return done(new Error('Missing Facebook appID or appSecret')); 93 | } 94 | 95 | User.findByFacebookID(profile.id, (err, user) => { 96 | if (err || user) { 97 | return done(err, user); 98 | } 99 | 100 | if (!options.registration.simple) { 101 | return done(null, false, { 102 | message: 'Unknown user', 103 | }); 104 | } 105 | 106 | User.createByFacebook(profile._json, done); 107 | }); 108 | }); 109 | } 110 | 111 | export function twitter(options, models) { 112 | return new TwitterStrategy({ 113 | consumerKey: options.twitter.consumerKey, 114 | consumerSecret: options.twitter.consumerSecret, 115 | }, (token, tokenSecret, profile, done) => { 116 | const User = models.User; 117 | 118 | if (!profile.id) { 119 | return done(new Error('Profile ID is null')); 120 | } 121 | 122 | if (!options.twitter.consumerKey || !options.twitter.consumerSecret) { 123 | return done(new Error('Missing Twitter consumerKey or consumerSecret')); 124 | } 125 | 126 | User.findByTwitterID(profile.id, (err, user) => { 127 | if (err || user) { 128 | return done(err, user); 129 | } 130 | 131 | if (!options.registration.simple) { 132 | return done(null, false, { 133 | message: 'Unknown user', 134 | }); 135 | } 136 | 137 | User.createByTwitter(profile, done); 138 | }); 139 | }); 140 | } 141 | 142 | export function facebookCanvas(options, models) { 143 | return new FacebookCanvasStrategy({ 144 | clientID: options.facebook.appID, 145 | clientSecret: options.facebook.appSecret, 146 | }, (accessToken, refreshToken, profile, done) => { 147 | const User = models.User; 148 | 149 | if (!profile.id) { 150 | return done(new Error('Profile ID is null')); 151 | } 152 | 153 | if (!options.facebook.appID || !options.facebook.appSecret) { 154 | return done(new Error('Missing Facebook appID or appSecret')); 155 | } 156 | 157 | User.findByFacebookID(profile.id, (err, user) => { 158 | if (err || user) { 159 | return done(err, user); 160 | } 161 | 162 | if (!options.registration.simple) { 163 | return done(null, false, { 164 | message: 'Unknown user', 165 | }); 166 | } 167 | 168 | User.createByFacebook(profile._json, done); 169 | }); 170 | }); 171 | } 172 | -------------------------------------------------------------------------------- /tests/models/article.js: -------------------------------------------------------------------------------- 1 | import { Schema } from 'mongoose'; 2 | export const name = 'Article'; 3 | 4 | export default function createModel(server, callback) { 5 | const schema = new Schema({ 6 | title: { type: String } 7 | }); 8 | 9 | callback(null); 10 | return server.db.model(name, schema); 11 | } 12 | -------------------------------------------------------------------------------- /tests/models/provider.js: -------------------------------------------------------------------------------- 1 | import { Schema } from 'mongoose'; 2 | import * as provider from '../../src/models/provider'; 3 | export const name = 'Provider'; 4 | 5 | export default function createModel(server, callback) { 6 | const schema = provider.createSchema(Schema); 7 | 8 | callback(null); 9 | return server.db.model(name, schema); 10 | } 11 | -------------------------------------------------------------------------------- /tests/models/user.js: -------------------------------------------------------------------------------- 1 | import { Schema } from 'mongoose'; 2 | import * as user from '../../src/models/user'; 3 | export const name = 'User'; 4 | 5 | export default function createModel(server, callback) { 6 | const schema = user.createSchema(Schema); 7 | 8 | callback(null); 9 | return server.db.model(name, schema); 10 | } 11 | -------------------------------------------------------------------------------- /tests/routes/test.js: -------------------------------------------------------------------------------- 1 | export default function (route) { 2 | route 3 | .api() 4 | .route('/test') 5 | .get('/', (req, res) => { 6 | res.status(204).jsonp({}); 7 | }) 8 | .get('/error', () => { 9 | throw new Error('I am error'); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /tests/routes/vhost.js: -------------------------------------------------------------------------------- 1 | export default function (route) { 2 | route 3 | .vhost('local.zabavnetesty.sk') 4 | .api() 5 | .route('/test1') 6 | .get('/', (req, res) => { 7 | res.jsonp({ 8 | host: 'local.zabavnetesty.sk', 9 | }); 10 | }); 11 | 12 | route 13 | .vhost('local.meetbus.com') 14 | .api() 15 | .route('/test2') 16 | .get('/', (req, res) => { 17 | res.jsonp({ 18 | host: 'local.meetbus.com', 19 | }); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /tests/run.js: -------------------------------------------------------------------------------- 1 | import should from 'should'; 2 | import Server from '../src/server'; 3 | import mongoose from 'mongoose'; 4 | import request from 'supertest'; 5 | 6 | describe('Run server', function() { 7 | let server = null; 8 | 9 | it('should be able to run simple server', function(done) { 10 | server = new Server({ 11 | root: __dirname, 12 | db: mongoose.connect('mongodb://localhost/maglev'), 13 | session: { 14 | secret: '123456789' 15 | }, 16 | server: { 17 | port: 4433 18 | }, 19 | //favicon: false, 20 | //robots: false 21 | }, function(err, server) { 22 | if (err) { 23 | throw err; 24 | } 25 | 26 | server.listen(done); 27 | }); 28 | }); 29 | 30 | it('should not be able to listen again', function(done) { 31 | server.listen(function(err) { 32 | err.message.should.equal('Server is already listening'); 33 | done(); 34 | }); 35 | }); 36 | 37 | it('should be able to get value from route', function(done) { 38 | const uri = '/api/test'; 39 | 40 | request('http://localhost:4433') 41 | .get(uri) 42 | .set('Accept', 'application/json') 43 | .expect('Content-Type', /json/) 44 | .expect(204) 45 | .end(function(err, res) { 46 | done(); 47 | }); 48 | }); 49 | 50 | it('should be able to get module', function(done) { 51 | const Article = server.models.Article; 52 | Article.create({ 53 | title: 'Book name' 54 | }, function(err, article) { 55 | if (err) { 56 | throw err; 57 | } 58 | 59 | article.title.should.equal('Book name'); 60 | 61 | done(); 62 | }); 63 | }); 64 | 65 | it('should be able to handle error', function(done) { 66 | const uri = '/api/test/error'; 67 | 68 | request('http://localhost:4433') 69 | .get(uri) 70 | .set('Accept', 'application/json') 71 | .expect(500) 72 | .end(function(err, res) { 73 | done(); 74 | }); 75 | }); 76 | 77 | it('should be able to get correct value from vhost1', (done) => { 78 | const uri = '/api/test1'; 79 | 80 | request('http://local.zabavnetesty.sk:4433') 81 | .get(uri) 82 | .set('Accept', 'application/json') 83 | .expect('Content-Type', /json/) 84 | .end((err, res) => { 85 | const { host } = res.body; 86 | host.should.equal('local.zabavnetesty.sk'); 87 | done(); 88 | }); 89 | }); 90 | 91 | it('should be able to get correct value from vhost2', (done) => { 92 | const uri = '/api/test2'; 93 | 94 | request('http://local.meetbus.com:4433') 95 | .get(uri) 96 | .set('Accept', 'application/json') 97 | .expect('Content-Type', /json/) 98 | .end((err, res) => { 99 | const { host } = res.body; 100 | host.should.equal('local.meetbus.com'); 101 | done(); 102 | }); 103 | }); 104 | 105 | it('should not be able to get correct value from vhost2', (done) => { 106 | const uri = '/api/test1'; 107 | 108 | request('http://local.meetbus.com:4433') 109 | .get(uri) 110 | .set('Accept', 'application/json') 111 | .expect('Content-Type', /json/) 112 | .expect(404) 113 | .end(() => { 114 | done(); 115 | }); 116 | }); 117 | 118 | it('should be able to close server', function(done) { 119 | server.close(function(err) { 120 | if (err) { 121 | throw err; 122 | } 123 | 124 | done(); 125 | }); 126 | }); 127 | 128 | it('should not be able to close server again', function(done) { 129 | server.close(function(err) { 130 | err.message.should.equal('Server is not listening'); 131 | done(); 132 | }); 133 | }); 134 | 135 | let userSaved = null; 136 | 137 | it('should be able to create user', function(done) { 138 | const User = server.models.User; 139 | User.create({ 140 | firstName: 'Zlatko', 141 | lastName: 'Fedor' 142 | }, function(err, user) { 143 | if (err) { 144 | throw err; 145 | } 146 | 147 | user.firstName.should.equal('Zlatko'); 148 | user.lastName.should.equal('Fedor'); 149 | user.name.should.equal('Zlatko Fedor'); 150 | 151 | userSaved = user; 152 | 153 | done(); 154 | }); 155 | }); 156 | 157 | it('should be able to add facebook provider', function(done) { 158 | userSaved.addProvider('facebook', 12345, {}, function(err, provider) { 159 | if (err) { 160 | console.log(err); 161 | throw err; 162 | } 163 | 164 | should.exist(provider); 165 | 166 | provider.user.toString().should.equal(userSaved._id.toString()); 167 | provider.nameUID.should.equal('facebook_12345'); 168 | 169 | done(); 170 | }); 171 | }); 172 | 173 | it('should be able to find user by facebook id', function(done) { 174 | const User = server.models.User; 175 | User.findByFacebookID(12345, function(err, user) { 176 | if (err) { 177 | throw err; 178 | } 179 | 180 | should.exist(user); 181 | 182 | user.firstName.should.equal('Zlatko'); 183 | user.lastName.should.equal('Fedor'); 184 | user.name.should.equal('Zlatko Fedor'); 185 | 186 | done(); 187 | }); 188 | }); 189 | 190 | it('should be able to get provider without uid', function(done) { 191 | userSaved.getProvider('facebook', function(err, provider) { 192 | if (err) { 193 | throw err; 194 | } 195 | 196 | provider.user.toString().should.equal(userSaved._id.toString()); 197 | provider.nameUID.should.equal('facebook_12345'); 198 | 199 | done(); 200 | }); 201 | }); 202 | 203 | it('should be able to get provider', function(done) { 204 | userSaved.getProvider('facebook', 12345, function(err, provider) { 205 | if (err) { 206 | throw err; 207 | } 208 | 209 | provider.user.toString().should.equal(userSaved._id.toString()); 210 | provider.nameUID.should.equal('facebook_12345'); 211 | 212 | done(); 213 | }); 214 | }); 215 | 216 | it('should be able to use function hasProvider', function(done) { 217 | userSaved.hasProvider('facebook', 12345, function(err, has) { 218 | if (err) { 219 | throw err; 220 | } 221 | 222 | has.should.equal(true); 223 | 224 | done(); 225 | }); 226 | }); 227 | 228 | it('should be able to use function hasProvider', function(done) { 229 | userSaved.hasProvider('facebook', function(err, has) { 230 | if (err) { 231 | throw err; 232 | } 233 | 234 | has.should.equal(true); 235 | 236 | done(); 237 | }); 238 | }); 239 | 240 | it('should be able to use function hasProvider', function(done) { 241 | userSaved.hasProvider('facebook', 1234566666, function(err, has) { 242 | if (err) { 243 | throw err; 244 | } 245 | 246 | has.should.equal(false); 247 | 248 | done(); 249 | }); 250 | }); 251 | 252 | it('should be able to use function hasProvider', function(done) { 253 | userSaved.hasProvider('twitter', function(err, has) { 254 | if (err) { 255 | throw err; 256 | } 257 | 258 | has.should.equal(false); 259 | 260 | done(); 261 | }); 262 | }); 263 | 264 | it('should be able to use removeProvider', function(done) { 265 | userSaved.removeProvider('facebook', 12345, function(err) { 266 | if (err) { 267 | throw err; 268 | } 269 | 270 | done(); 271 | }); 272 | }); 273 | 274 | it('should be able to use function hasProvider', function(done) { 275 | userSaved.hasProvider('facebook', 12345, function(err, has) { 276 | if (err) { 277 | throw err; 278 | } 279 | 280 | has.should.equal(false); 281 | 282 | done(); 283 | }); 284 | }); 285 | 286 | it('should be able to create by facebook profile', function(done) { 287 | const User = server.models.User; 288 | User.createByFacebook({ 289 | id: 44444, 290 | name: 'Zlatko Fedor', 291 | first_name: 'Zlatko', 292 | last_name: 'Fedor', 293 | email: 'fb@fb.com' 294 | }, function(err, user) { 295 | if (err) { 296 | throw err; 297 | } 298 | 299 | user.firstName.should.equal('Zlatko'); 300 | user.lastName.should.equal('Fedor'); 301 | user.name.should.equal('Zlatko Fedor'); 302 | user.email.should.equal('fb@fb.com'); 303 | 304 | userSaved = user; 305 | 306 | done(); 307 | }); 308 | }); 309 | 310 | it('should be able to use function hasProvider', function(done) { 311 | userSaved.hasProvider('facebook', 44444, function(err, has) { 312 | if (err) { 313 | throw err; 314 | } 315 | 316 | has.should.equal(true); 317 | done(); 318 | }); 319 | }); 320 | 321 | it('should be able to clean all', function(done) { 322 | const { User, Provider } = server.models; 323 | User.remove({}, function(err) { 324 | if (err) { 325 | throw err; 326 | } 327 | 328 | Provider.remove({}, function(err) { 329 | if (err) { 330 | throw err; 331 | } 332 | 333 | done(); 334 | }); 335 | }); 336 | }); 337 | }); 338 | --------------------------------------------------------------------------------