├── .eslintrc.js ├── .gitignore ├── LICENSE.md ├── README.md ├── app ├── admin │ └── index.js ├── monitoring │ └── index.js └── users │ └── index.js ├── config ├── env │ ├── development.js │ ├── production.js │ └── test.js ├── express.js ├── index.js ├── middlewares │ └── authorization.js ├── passport.js └── routes.js ├── db └── index.js ├── init.sql ├── package-lock.json ├── package.json ├── public ├── images │ └── logo.png └── main.css ├── server.js ├── tests └── sample.test.js └── views ├── admin-panel.handlebars └── login.handlebars /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "commonjs": true, 4 | "node": true, 5 | "es6": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "rules": { 9 | "indent": [ 10 | "error", 11 | "tab" 12 | ], 13 | "linebreak-style": [ 14 | "error", 15 | "unix" 16 | ], 17 | "quotes": [ 18 | "error", 19 | "single" 20 | ], 21 | "semi": [ 22 | "error", 23 | "never" 24 | ], 25 | "no-process-env": "off" 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | 4 | .env 5 | node_modules/ 6 | 7 | .monitor 8 | Makefile 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 DayOne.pl 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## DOS is a boilerplate stack for building Node.js projects with Express & Posgres 2 | 3 | DOS helps you start building Node.js projects faster. DOS is a boilerplate project with minimal set of configured dependencies to let you start with Node.js using Express, Postgres & Passport. 4 | 5 | ### Features: 6 | 7 | * Express with JSON body parser, cookie parser, session 8 | * global error handling for 404 & 500 9 | * env variables 10 | * Postgres with connection pool 11 | * Passport with LocalStrategy & Postgres 12 | * authentication endpoints 13 | * session storage in Postgres 14 | * health-check endpoint with DB check 15 | * admin section of app with Handlebars 16 | * loggers 17 | * test runner using Jest 18 | 19 | ### How to start: 20 | 21 | 1. Clone repository 22 | 1. Run ```npm install``` 23 | 1. Create new database in Postgres 24 | 1. Run ```init.sql``` script in Posgres 25 | 1. Run ```npm test``` to check if everything works 26 | 1. Enjoy & build great apps 😀 27 | 28 | ### Users in DB: 29 | 30 | Passport with LocalStrategy & Postgres is configured for login endpoints. Since registration is often app & domain specific, there is no configuration for registration endpoint. The easiest way to see how admin site work, you need to insert manually. You need to generate password for admin using `bcrypt`. In project directory run `node` and then: 31 | 32 | ``` 33 | const bcrypt = require('bcrypt') 34 | const pass = 'you password goes here' 35 | const saltRounds = 10 //the higher the better - the longer it takes to generate & compare 36 | bcrypt.hashSync(pass, saltRounds) 37 | ``` 38 | 39 | You'll get generated password and now you need to insert user to DB. 40 | 41 | ``` 42 | INSERT INTO users(username, password, type) VALUES ('username', 'password', 'admin'); 43 | ``` 44 | 45 | ### Commands: 46 | 47 | * Start server: 48 | ``` 49 | npm start 50 | ``` 51 | 52 | * Debug server: 53 | ``` 54 | npm run debug 55 | ``` 56 | 57 | * Test 58 | ``` 59 | npm test 60 | ``` 61 | 62 | ### Used packages: 63 | 64 | - bcrypt -> https://github.com/kelektiv/node.bcrypt.js/ - One of the most fundamental security concern is storing passwords in application. In DOS we're using bcypt to generate salt and hash passwords. bcrypt is a password hashing function. bcrypt uses salt to protect against rainbow table attacks. What is crucial, bcrypt is adaptive function -> over time the iteration time can be increased in order to make it slower to remain resistant to increasing computation power. NPM package is using native implementation of bcrypt. 65 | 66 | - body-parser -> https://github.com/expressjs/body-parser - Body parsing middleware - parses incoming request body and expose it under `req.body` property. In DOS we're using JSON and URL-encoded parser as top-level middleware. 67 | 68 | - connect-pg-simple -> https://github.com/voxpelli/node-connect-pg-simple - express-session comes in bundled with in-memory session. However, in-memory session is not suitable for production apps. One of the most popular session storage is Redis. We're using PostgreSQL in DOS and we're going to store session data inside our Postgres db. We're using connection pool (pg.Pool) for the underlying db module. 69 | 70 | - cookie-parser -> https://github.com/expressjs/cookie-parser - Exposes cookies under `req.cookie` property 71 | 72 | - express -> https://github.com/expressjs/express - DOS choice for Node.js minimalistic web framework 73 | 74 | - express-handlebars -> https://github.com/ericf/express-handlebars - Handlebars is templating language for dynamic HTML. express-handlebars is view engine for Express using Handlebars templates. In DOS we're using Handlebars and server-side rendering for administration part of application. 75 | 76 | - express-session -> https://github.com/expressjs/session - Session middleware for Express with build-in in-memory session storage. In DOS we're using connect-pg-simple to store session data in Postgres 77 | 78 | - express-validator -> https://github.com/ctavan/express-validator - `node-validator` middleware for Express. Beside frontend validation of forms, it's important to implement validation on backend side. That's the only reliable validation for incoming data and the one that must be present in any web application. 79 | 80 | - method-override -> https://github.com/expressjs/method-override - Middleware for Express enabling HTTP verbs like PUT or DELETE in case where client doesn't support it. 81 | 82 | - morgan -> https://github.com/expressjs/morgan - Request logger middleware for Express. 83 | 84 | - passport -> https://github.com/jaredhanson/passport - Authentication middleware for Express. The main idea of Passport is extensible set of plugins known as strategies. There are variety of different strategies, which could authenticate users by username & password, OAuth like Facebook, Twitter or Google and many others. Passport maintains persistent login session, which requires both serialization and deserialization of `authenticated user`. 85 | 86 | - passport-local -> https://github.com/jaredhanson/passport-local - Authentication strategy for Passport using username & password. In DOS, we're combining local strategy with Postgres to authenticate users with username and hashed password stored in db 87 | 88 | - pg -> https://github.com/brianc/node-postgres - Non-blocking PostgreSQL client for Node.js 89 | 90 | - winston -> https://github.com/winstonjs/winston - Logging library for Node.js with support for multiple transports. Winston provides many features beside normal `console` statements like default & custom loggers, multiple transports, streaming logs, different log levels. 91 | 92 | ### Authors 93 | 94 | Marek Piechut [@marekpiechut](http://twitter.com/@marekpiechut) 95 | Bartek Witczak [@bartekwitczak](http://twitter.com/@bartekwitczak) 96 | 97 | ### License 98 | 99 | DOS starter kit is licensed under the MIT License so you can use it in free, opensource and commercial projects. Whichever you like. See [LICENSE.md] for details 100 | -------------------------------------------------------------------------------- /app/admin/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | renderLogin: (req, res) => { 3 | res.render('login') 4 | }, 5 | 6 | login: (req, res) => { 7 | if(req.user.type === 'admin') { 8 | res.redirect('/admin/panel') 9 | } else { 10 | res.redirect('/admin/login') 11 | } 12 | }, 13 | 14 | renderPanel: (req, res) => { 15 | res.render('admin-panel') 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/monitoring/index.js: -------------------------------------------------------------------------------- 1 | const winston = require('winston') 2 | 3 | module.exports = { 4 | health: (db) => (req, res, next) => { 5 | db.query('SELECT 1', (err) => { 6 | if(err) { 7 | winston.error('Error running health check query on DB', err) 8 | return next(err) 9 | } 10 | 11 | res.sendStatus(200) 12 | }) 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /app/users/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | login: (req, res) => { 3 | const { user } = req 4 | 5 | res.json(user) 6 | }, 7 | 8 | logout: (req, res, next) => { 9 | req.session.destroy((err) => { 10 | if(err) return next(err) 11 | 12 | req.logout() 13 | 14 | res.sendStatus(200) 15 | }) 16 | }, 17 | 18 | ping: function(req, res) { 19 | res.sendStatus(200) 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /config/env/development.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | db: { 3 | user: '', 4 | password: '', 5 | database: '', 6 | host: 'localhost', 7 | port: 5432, 8 | max: 50, 9 | idleTimeoutMillis: 30000 10 | }, 11 | session_secret: '' 12 | } 13 | -------------------------------------------------------------------------------- /config/env/production.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | db: { 3 | user: process.env.DB_USER, 4 | password: process.env.DB_PASSWORD, 5 | database: process.env.DB_NAME, 6 | host: process.env.DB_HOST, 7 | port: process.env.DB_PORT, 8 | max: process.env.DB_MAX_CONNECTIONS, 9 | idleTimeoutMillis: process.env.DB_IDLE_TIMEOUT 10 | }, 11 | session_secret: '' 12 | } 13 | -------------------------------------------------------------------------------- /config/env/test.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | db: {}, 3 | session_secret: 'test' 4 | } 5 | -------------------------------------------------------------------------------- /config/express.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const bodyParser = require('body-parser') 3 | const express = require('express') 4 | const expressHandlebars = require('express-handlebars') 5 | const expressValidator = require('express-validator') 6 | const session = require('express-session') 7 | const pgSession = require('connect-pg-simple')(session) 8 | const cookieParser = require('cookie-parser') 9 | const methodOverride = require('method-override') 10 | const morgan = require('morgan') 11 | const winston = require('winston') 12 | const config = require('./') 13 | 14 | const env = process.env.NODE_ENV || 'development' 15 | 16 | module.exports = (app, passport, pool) => { 17 | let log = 'dev' 18 | if (env !== 'development') { 19 | log = { 20 | stream: { 21 | write: message => winston.info(message) 22 | } 23 | } 24 | } 25 | 26 | if (env !== 'test') app.use(morgan(log)) 27 | 28 | app.engine('handlebars', expressHandlebars()) 29 | app.set('views', path.join(config.root, 'views')) 30 | app.set('view engine', 'handlebars') 31 | 32 | app.use(bodyParser.json()) 33 | app.use(bodyParser.urlencoded({ extended: true })) 34 | app.use(expressValidator()) 35 | 36 | app.use(methodOverride(function (req) { 37 | if (req.body && typeof req.body === 'object' && '_method' in req.body) { 38 | var method = req.body._method 39 | delete req.body._method 40 | return method 41 | } 42 | })) 43 | 44 | app.use(cookieParser()) 45 | app.use(session({ 46 | store: new pgSession({ 47 | pool 48 | }), 49 | secret: config.session_secret, 50 | resave: false, 51 | cookie: { maxAge: 14 * 24 * 60 * 60 * 1000 } 52 | })) 53 | 54 | app.use(passport.initialize()) 55 | app.use(passport.session()) 56 | 57 | app.use('/', express.static(path.join(config.root, 'public'))) 58 | } 59 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const development = require('./env/development') 4 | const test = require('./env/test') 5 | const production = require('./env/production') 6 | 7 | const defaults = { 8 | root: path.join(__dirname, '..'), 9 | } 10 | 11 | module.exports = { 12 | development: Object.assign({}, defaults, development), 13 | test: Object.assign({}, defaults, test), 14 | production: Object.assign({}, defaults, production) 15 | }[process.env.NODE_ENV || 'development'] 16 | -------------------------------------------------------------------------------- /config/middlewares/authorization.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | requiresLogin: (req, res, next) => { 3 | if (req.user) return next() 4 | 5 | res.sendStatus(401) 6 | }, 7 | 8 | requiresAdmin: (req, res, next) => { 9 | if (req.user && req.user.type === 'admin') return next() 10 | 11 | res.sendStatus(401) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /config/passport.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt') 2 | const winston = require('winston') 3 | const LocalStrategy = require('passport-local').Strategy 4 | 5 | module.exports = (passport, db) => { 6 | passport.use(new LocalStrategy((username, password, cb) => { 7 | db.query('SELECT id, username, password, type FROM users WHERE username=$1', [username], (err, result) => { 8 | if(err) { 9 | winston.error('Error when selecting user on login', err) 10 | return cb(err) 11 | } 12 | 13 | if(result.rows.length > 0) { 14 | const first = result.rows[0] 15 | bcrypt.compare(password, first.password, function(err, res) { 16 | if(res) { 17 | cb(null, { id: first.id, username: first.username, type: first.type }) 18 | } else { 19 | cb(null, false) 20 | } 21 | }) 22 | } else { 23 | cb(null, false) 24 | } 25 | }) 26 | })) 27 | 28 | passport.serializeUser((user, done) => { 29 | done(null, user.id) 30 | }) 31 | 32 | passport.deserializeUser((id, cb) => { 33 | db.query('SELECT id, username, type FROM users WHERE id = $1', [parseInt(id, 10)], (err, results) => { 34 | if(err) { 35 | winston.error('Error when selecting user on session deserialize', err) 36 | return cb(err) 37 | } 38 | 39 | cb(null, results.rows[0]) 40 | }) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /config/routes.js: -------------------------------------------------------------------------------- 1 | const winston = require('winston') 2 | const { requiresLogin, requiresAdmin } = require('./middlewares/authorization') 3 | const admin = require('../app/admin') 4 | const users = require('../app/users') 5 | const monitoring = require('../app/monitoring') 6 | 7 | module.exports = (app, passport, db) => { 8 | app.post('/api/login', passport.authenticate('local'), users.login) 9 | app.get('/api/logout', users.logout) 10 | app.get('/api/ping', requiresLogin, users.ping) 11 | 12 | app.get('/admin/login', admin.renderLogin) 13 | app.post('/admin/login', passport.authenticate('local', { failureRedirect: '/admin/login' }), admin.login) 14 | app.get('/admin/panel', requiresAdmin, admin.renderPanel) 15 | 16 | app.get('/health', monitoring.health(db)) 17 | 18 | app.use(function (err, req, res, next) { 19 | if (err.message && (~err.message.indexOf('not found'))) { 20 | return next() 21 | } 22 | 23 | winston.error(err.stack) 24 | 25 | return res.status(500).json({error: 'Error on backend occurred.'}) 26 | }) 27 | 28 | app.use(function (req, res) { 29 | const payload = { 30 | url: req.originalUrl, 31 | error: 'Not found' 32 | } 33 | if (req.accepts('json')) return res.status(404).json(payload) 34 | 35 | res.status(404).render('404', payload) 36 | }) 37 | } 38 | 39 | -------------------------------------------------------------------------------- /db/index.js: -------------------------------------------------------------------------------- 1 | const pg = require('pg') 2 | const config = require('../config') 3 | const winston = require('winston') 4 | 5 | const dbConfig = { 6 | user: config.db.user, 7 | password: config.db.password, 8 | database: config.db.database, 9 | host: config.db.host, 10 | port: config.db.port, 11 | max: config.db.max, 12 | idleTimeoutMillis: config.db.idleTimeoutMillis, 13 | } 14 | 15 | const pool = new pg.Pool(dbConfig) 16 | pool.on('error', function (err) { 17 | winston.error('idle client error', err.message, err.stack) 18 | }) 19 | 20 | module.exports = { 21 | pool, 22 | query: (text, params, callback) => { 23 | return pool.query(text, params, callback) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /init.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "session" ( 2 | "sid" varchar NOT NULL COLLATE "default", 3 | "sess" json NOT NULL, 4 | "expire" timestamp(6) NOT NULL 5 | ) 6 | WITH (OIDS=FALSE); 7 | 8 | ALTER TABLE "session" ADD CONSTRAINT "session_pkey" PRIMARY KEY ("sid") NOT DEFERRABLE INITIALLY IMMEDIATE; 9 | 10 | CREATE TABLE "users" ( 11 | "id" bigserial PRIMARY KEY, 12 | "username" varchar(255) UNIQUE, 13 | "password" varchar(100), 14 | "type" varchar(50) 15 | ); 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-boilerplate", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "./node_modules/.bin/cross-env NODE_ENV=development ./node_modules/.bin/nodemon server.js", 8 | "debug": "./node_modules/.bin/cross-env NODE_ENV=development ./node_modules/.bin/nodemon --debug server.js", 9 | "test": "./node_modules/.bin/cross-env NODE_ENV=test jest" 10 | }, 11 | "engines": { 12 | "node": ">=6.x" 13 | }, 14 | "author": "dayone.pl", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "cross-env": "5.0.0", 18 | "eslint": "3.19.0", 19 | "jest": "20.0.3", 20 | "nodemon": "1.11.0" 21 | }, 22 | "dependencies": { 23 | "bcrypt": "1.0.2", 24 | "body-parser": "1.17.1", 25 | "connect-pg-simple": "4.2.0", 26 | "cookie-parser": "1.4.3", 27 | "express": "4.15.2", 28 | "express-handlebars": "3.0.0", 29 | "express-session": "1.15.2", 30 | "express-validator": "3.2.0", 31 | "method-override": "2.3.8", 32 | "morgan": "1.8.1", 33 | "passport": "0.3.2", 34 | "passport-local": "1.0.0", 35 | "pg": "7.1.2", 36 | "winston": "2.3.1" 37 | }, 38 | "jest": { 39 | "testPathIgnorePatterns": [ 40 | "/config/", 41 | "/node_modules/" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dayone-labs/dos-server/5a435052ef8f62dacf82a8e0b2a3dec60ea09427/public/images/logo.png -------------------------------------------------------------------------------- /public/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #f7f9f8; 3 | } 4 | 5 | .container { 6 | width: 360px; 7 | padding: 8% 0 0; 8 | margin: auto; 9 | } 10 | 11 | .login-form { 12 | position: relative; 13 | background: #2c3343; 14 | padding: 45px; 15 | text-align: center; 16 | box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24); 17 | } 18 | 19 | .login-form input { 20 | background: #f2f2f2; 21 | width: 100%; 22 | border: 0; 23 | margin: 0 0 15px; 24 | padding: 15px; 25 | 26 | font-size: 14px; 27 | } 28 | 29 | .login-form button { 30 | color: white; 31 | text-transform: uppercase; 32 | background-color: #42b8ac; 33 | width: 100%; 34 | border: 0; 35 | padding: 15px; 36 | font-size: 14px; 37 | transition: all 0.3 ease; 38 | } 39 | 40 | .login-form button:hover, .login-form button:active, .login-form button:focus { 41 | background-color: #42b8ac; 42 | } 43 | 44 | .logo { 45 | margin-bottom: 30px; 46 | } 47 | 48 | .relative { 49 | position: relative; 50 | } 51 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const passport = require('passport') 3 | const winston = require('winston') 4 | const db = require('./db') 5 | 6 | const port = process.env.PORT || 9000 7 | const app = express() 8 | 9 | require('./config/passport')(passport, db) 10 | require('./config/express')(app, passport, db.pool) 11 | require('./config/routes')(app, passport, db) 12 | 13 | const server = app.listen(port, () => { 14 | if(app.get('env') === 'test') return 15 | 16 | winston.log('Express app started on port ' + port) 17 | }) 18 | 19 | server.on('close', () => { 20 | winston.log('Closed express server') 21 | 22 | db.pool.end(() => { 23 | winston.log('Shut down connection pool') 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /tests/sample.test.js: -------------------------------------------------------------------------------- 1 | test('Configured properly', () => { 2 | expect(1 + 1).toBe(2) 3 | }) 4 | -------------------------------------------------------------------------------- /views/admin-panel.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | DayOne - Admin panel 4 | 5 | 6 | 7 |
8 |

Welcome to Admin Panel

9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /views/login.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | DayOne - Admin panel 4 | 5 | 6 | 7 |
8 | 26 |
27 | 28 | 29 | --------------------------------------------------------------------------------