├── .gitignore ├── LICENSE ├── README.md ├── config └── db.dev.conf.js ├── controllers ├── index.js └── login.js ├── models └── auth.js ├── package.json ├── public └── css │ └── layout.css ├── routes.js ├── server.js ├── users.sql ├── util ├── auth.js ├── messages.js └── rendering.js └── views ├── data ├── basic-loop.html └── basic.html ├── index ├── index.html └── user-home.html ├── layouts └── default.html └── login ├── index.html └── register.html /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | lib-cov 3 | *.seed 4 | *.log 5 | *.csv 6 | *.dat 7 | *.out 8 | *.pid 9 | *.gz 10 | .DS_Store 11 | *.swp 12 | 13 | pids 14 | logs 15 | results 16 | 17 | npm-debug.log 18 | node_modules 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Orlando Castillo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | nodejs-mysql-boilerplate 2 | ======================== 3 | 4 | Very basic nodejs/express setup with mysql authentication. 5 | 6 | I found mongodb based boilerplates but I needed something that would work with mysql. I quickly put this together and I'm just getting familiar with Node, so let me know if you have any tips or suggestions for improving this. I hope you find this useful. 7 | 8 | This example only does local authentication, not authorization, with Passport.js. Nor does it have any front-end setup - I'll leave that up to you. I choose Bookshelf.js ORM because it nicely extends backbone.js, which is what I am most familiar with. Knex.js would also work just fine or even just plain mysql. 9 | 10 | ## Quick Start 11 | 12 | * Run 'users.sql' 13 | * Rename util/bookshelf.example.js to util/bookshelf.js and adjust to your database settings 14 | * Go to http://yourdomain:3000/register to create a user 15 | * Start building your app! 16 | -------------------------------------------------------------------------------- /config/db.dev.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | host : 'localhost', 3 | user : 'your_username', 4 | password : 'your_password', 5 | database : 'your_database', 6 | charset : 'utf8' 7 | } 8 | -------------------------------------------------------------------------------- /controllers/index.js: -------------------------------------------------------------------------------- 1 | var rendering = require('../util/rendering'); 2 | 3 | 4 | exports.home = function(req, res) { 5 | res.render('index/index'); 6 | } 7 | 8 | 9 | exports.userHome = function(req, res) { 10 | res.render('index/user-home'); 11 | } 12 | -------------------------------------------------------------------------------- /controllers/login.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'), 2 | passport = require('passport'), 3 | data = require('../models/auth')(); 4 | 5 | 6 | exports.registerPage = function(req, res) { 7 | res.render('login/register', {username: req.flash('username')}); 8 | } 9 | 10 | 11 | exports.registerPost = function(req, res) { 12 | var vpw = req.body.vpw; 13 | var pwu = req.body.pw; 14 | var un = req.body.un; 15 | 16 | req.flash('username', un); 17 | 18 | if(vpw !== pwu) { 19 | req.flash('error', 'Your passwords did not match.'); 20 | res.redirect('/register'); 21 | return; 22 | } 23 | 24 | req.checkBody('un', 'Please enter a valid email.').notEmpty().isEmail(); 25 | var errors = req.validationErrors(); 26 | if (errors) { 27 | var msg = errors[0].msg; 28 | req.flash('error', msg); 29 | res.redirect('/register'); 30 | return; 31 | } 32 | 33 | var new_salt = Math.round((new Date().valueOf() * Math.random())) + ''; 34 | var pw = crypto.createHmac('sha1', new_salt).update(pwu).digest('hex'); 35 | var created = new Date().toISOString().slice(0, 19).replace('T', ' '); 36 | 37 | new data.ApiUser({email: un, password: pw, salt: new_salt, created: created}).save().then(function(model) { 38 | passport.authenticate('local')(req, res, function () { 39 | res.redirect('/home'); 40 | }) 41 | }, function(err) { 42 | req.flash('error', 'Unable to create account.'); 43 | res.redirect('/register'); 44 | }); 45 | } 46 | 47 | 48 | exports.loginPage = function(req, res) { 49 | res.render('login/index', {username: req.flash('username')}); 50 | } 51 | 52 | 53 | exports.checkLogin = function(req, res, next) { 54 | passport.authenticate('local', function(err, user, info) { 55 | if (err || !user) { 56 | req.flash('username', req.body.un); 57 | req.flash('error', info.message); 58 | return res.redirect('/login'); 59 | } 60 | req.logIn(user, function(err) { 61 | if (err) { 62 | req.flash('error', info.message); 63 | return res.redirect('/login'); 64 | } 65 | req.flash('success', 'Welcome!'); 66 | return res.redirect('/home'); 67 | }); 68 | })(req, res, next); 69 | } 70 | 71 | 72 | exports.logout = function(req, res) { 73 | req.logout(); 74 | req.flash('info', 'You are now logged out.'); 75 | res.redirect('/login'); 76 | } 77 | -------------------------------------------------------------------------------- /models/auth.js: -------------------------------------------------------------------------------- 1 | var Bookshelf = require('bookshelf').mysqlAuth; 2 | 3 | module.exports = function() { 4 | var bookshelf = {}; 5 | 6 | bookshelf.ApiUser = Bookshelf.Model.extend({ 7 | tableName: 'users' 8 | }); 9 | 10 | return bookshelf; 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Node.js-Express-MySQL-Boilerplate", 3 | "description": "Node.js/Express/MySQL Boilerplate", 4 | "version": "0.0.3", 5 | "private": true, 6 | "engines": { 7 | "node": ">=0.10.x", 8 | "npm": ">=1.2.x" 9 | }, 10 | "scripts": { 11 | "start": "NODE_ENV=development ./node_modules/.bin/nodemon server.js" 12 | }, 13 | "dependencies": { 14 | "express": "^4.12.4", 15 | "body-parser": "^1.12.4", 16 | "cookie-parser": "^1.3.5", 17 | "cookie-session": "^1.1.0", 18 | "swig": "^1.4.2", 19 | "express-validator": "^2.10.0", 20 | "connect-flash": "^0.1.1", 21 | "mysql": "^2.6.2", 22 | "knex": "^0.8.6", 23 | "bookshelf": "^0.8.1", 24 | "underscore": "^1.8.3", 25 | "crypto": "^0.0.3", 26 | "passport": "^0.2.2", 27 | "passport-local": "^1.0.0", 28 | "async": "^1.0.0", 29 | "view-helpers": "^0.1.5", 30 | "serve-static": "^1.9.3" 31 | }, 32 | "devDependencies": { 33 | "nodemon": "latest" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /public/css/layout.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: arial, helvetica; 3 | font-size: 12px; 4 | } 5 | nav ul { 6 | list-style: none; 7 | margin: 0; 8 | padding: 0; 9 | } 10 | nav ul > li { 11 | display: inline-block; 12 | margin: 0; 13 | padding: .25em; 14 | } 15 | 16 | #messages { 17 | list-style: none; 18 | margin: 0; 19 | padding: 0; 20 | } 21 | #messages li { 22 | border: 1px solid #c8c8c8; 23 | color: #333; 24 | margin: .5em 0; 25 | padding: .25em; 26 | } 27 | #messages li.error { 28 | border-color: red; 29 | color: red; 30 | } 31 | #messages li.success { 32 | border-color: green; 33 | color: green; 34 | } 35 | 36 | form ul { 37 | list-style: none; 38 | margin: 0; 39 | padding: 0; 40 | } 41 | label { 42 | display: block; 43 | float: left; 44 | width: 80px; 45 | } 46 | -------------------------------------------------------------------------------- /routes.js: -------------------------------------------------------------------------------- 1 | 2 | var rendering = require('./util/rendering'), 3 | indexController = require('./controllers/index'), 4 | loginController = require('./controllers/login'); 5 | 6 | module.exports = function (app, passport) { 7 | 8 | // Home 9 | app.get('/', indexController.home); 10 | app.get('/home', ensureAuthenticated, indexController.userHome); 11 | 12 | 13 | // Auth 14 | app.get('/register', loginController.registerPage); 15 | app.post('/register', loginController.registerPost); 16 | app.get('/login', loginController.loginPage); 17 | app.post('/login', loginController.checkLogin); 18 | app.get('/logout', loginController.logout); 19 | 20 | // 'rendering' can be used to format api calls (if you have an api) 21 | // into either html or json depending on the 'Accept' request header 22 | app.get('/apitest', function(req, res) { 23 | rendering.render(req, res, { 24 | 'data': { 25 | 'test': { 26 | 'testsub': { 27 | 'str': 'testsub hello world' 28 | }, 29 | 'testsub2': 42 30 | }, 31 | 'test2': 'hello world' 32 | } 33 | }); 34 | }) 35 | 36 | 37 | // Auth Middleware 38 | function ensureAuthenticated(req, res, next) { 39 | if (req.isAuthenticated()) { return next(); } 40 | res.redirect('/login'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 2 | // TODO: Find a better way to load different configs in different env 3 | var dbConfig; 4 | try { 5 | // Look for dev conf for local development 6 | dbConfig = require('./config/db.dev.conf.js'); 7 | } catch(e) { 8 | try { 9 | // production conf? 10 | dbConfig = require('./config/db.conf.js'); 11 | } catch(e) { 12 | console.log('Startup failed. No db config file found.'); 13 | return false; 14 | } 15 | } 16 | 17 | 18 | var knex = require('knex')({ 19 | client: 'mysql', 20 | connection: dbConfig 21 | }), 22 | express = require('express'), 23 | bodyParser = require('body-parser'), 24 | cookieParser = require('cookie-parser'), 25 | cookieSession = require('cookie-session'), 26 | serveStatic = require('serve-static'), 27 | expressValidator = require('express-validator'), 28 | flash = require('connect-flash'), 29 | swig = require('swig'), 30 | passport = require('passport'), 31 | crypto = require('crypto'), 32 | Bookshelf = require('bookshelf'), 33 | messages = require('./util/messages'); 34 | 35 | var app = express(); 36 | 37 | Bookshelf.mysqlAuth = Bookshelf(knex); 38 | 39 | app.use(cookieParser('halsisiHHh445JjO0')); 40 | app.use(cookieSession({ 41 | keys: ['key1', 'key2'] 42 | })); 43 | 44 | app.use(bodyParser.urlencoded({ extended: true })); 45 | app.use(bodyParser.json()); 46 | 47 | app.use(expressValidator()); 48 | app.use(passport.initialize()); 49 | app.use(passport.session()); 50 | app.use(flash()); 51 | app.use(serveStatic('./public')); 52 | //app.use(express.favicon(__dirname + '/public/images/shortcut-icon.png')); 53 | app.use(messages()); 54 | 55 | app.engine('html', swig.renderFile); 56 | app.set('view engine', 'html'); 57 | app.set('views', __dirname + '/views'); 58 | 59 | require('./util/auth')(passport); 60 | require('./routes')(app, passport); 61 | 62 | app.listen(process.env.PORT || 3000); 63 | 64 | console.log('Listening on port 3000'); 65 | -------------------------------------------------------------------------------- /users.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `users` ( 2 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 3 | `first_name` varchar(30) NOT NULL DEFAULT '', 4 | `last_name` varchar(30) NOT NULL DEFAULT '', 5 | `email` varchar(30) NOT NULL DEFAULT '', 6 | `password` varchar(60) NOT NULL DEFAULT '', 7 | `active` tinyint(1) unsigned NOT NULL DEFAULT '1', 8 | `created` datetime NOT NULL, 9 | `modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 10 | `salt` varchar(50) NOT NULL DEFAULT '', 11 | PRIMARY KEY (`id`), 12 | UNIQUE KEY `email` (`email`) 13 | ) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8; -------------------------------------------------------------------------------- /util/auth.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'), 2 | LocalStrategy = require('passport-local').Strategy, 3 | data = require('../models/auth')(); 4 | 5 | module.exports = function(passport) { 6 | 7 | passport.serializeUser(function(user, done) { 8 | done(null, user.id); 9 | }); 10 | 11 | 12 | passport.deserializeUser(function(user_id, done) { 13 | new data.ApiUser({id: user_id}).fetch().then(function(user) { 14 | return done(null, user); 15 | }, function(error) { 16 | return done(error); 17 | }); 18 | }); 19 | 20 | 21 | passport.use(new LocalStrategy({ 22 | usernameField: 'un', 23 | passwordField: 'pw' 24 | },function(email, password, done) { 25 | new data.ApiUser({email: email}).fetch({require: true}).then(function(user) { 26 | var sa = user.get('salt'); 27 | var pw = user.get('password'); 28 | var upw = crypto.createHmac('sha1', sa).update(password).digest('hex'); 29 | if(upw == pw) { 30 | return done(null, user); 31 | } 32 | return done(null, false, { 'message': 'Invalid password'}); 33 | }, function(error) { 34 | return done(null, false, { 'message': 'Unknown user'}); 35 | }); 36 | })); 37 | }; 38 | -------------------------------------------------------------------------------- /util/messages.js: -------------------------------------------------------------------------------- 1 | var flash = require('connect-flash'); 2 | 3 | module.exports = function() { 4 | return function(req, res, next) { 5 | var error_messages = req.flash('error'); 6 | var info_messages = req.flash('info'); 7 | var success_messages = req.flash('success'); 8 | res.locals.messages = []; 9 | for(var i in error_messages) { 10 | res.locals.messages.push({type: 'error', message: error_messages[i]}); 11 | } 12 | for(var i in info_messages) { 13 | res.locals.messages.push({type: 'info', message: info_messages[i]}); 14 | } 15 | for(var i in success_messages) { 16 | res.locals.messages.push({type: 'success', message: success_messages[i]}); 17 | } 18 | res.locals.isAuthenticated = req.isAuthenticated(); 19 | next(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /util/rendering.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | 4 | // Useful with RESTful APIs. Allows you to either send json response or html 5 | // var rendering = require('./util/rendering'); 6 | // rendering.render(req, res, data); 7 | render: function(req, res, data) { 8 | res.header("Access-Control-Allow-Origin", "*"); 9 | res.header("Access-Control-Allow-Headers", "X-Requested-With"); 10 | 11 | if(/application\/json/.test(req.get('accept'))) { 12 | res.json(data); 13 | } else { 14 | res.render('data/basic', { 15 | data: data 16 | }); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /views/data/basic-loop.html: -------------------------------------------------------------------------------- 1 |
Public home page.
7 | {% endblock %} -------------------------------------------------------------------------------- /views/index/user-home.html: -------------------------------------------------------------------------------- 1 | {% extends '../layouts/default.html' %} 2 | 3 | {% block content %} 4 |Hurray! You're logged in!
7 | {% endblock %} -------------------------------------------------------------------------------- /views/layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |