├── .babelrc ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── data.json ├── package.json ├── public ├── app.bundle.js └── styles.bundle.css ├── server.js ├── src ├── app.js ├── exclamation_add_form.vue ├── exclamation_list.vue ├── exclamation_search_list.vue └── exclamations_viewer.vue └── views ├── dashboard.pug └── index.pug /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015" 4 | ], 5 | "plugins": [ 6 | "transform-runtime" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "plugins": [ 4 | "html" 5 | ], 6 | "rules": { 7 | "semi": ["error", "always", { "omitLastInOneLineBlock": true }], 8 | "space-before-function-paren": ["error", "never"], 9 | "func-names": ["error", "never"], 10 | "no-new": 0, 11 | "new-cap": 0 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Alex Sears 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue2-auth 2 | Authentication using VueJS 2.0, Express, and Passport 3 | -------------------------------------------------------------------------------- /data.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [ 3 | { 4 | "username": "rachel@friends.com", 5 | "password": "green", 6 | "scopes": ["read", "add", "delete"] 7 | }, 8 | { 9 | "username": "ross@friends.com", 10 | "password": "geller", 11 | "scopes": ["read"] 12 | } 13 | ], 14 | "exclamations": [ 15 | { 16 | "id": "10ed2d7b-4a6c-4dad-ac25-d0a56c697753", 17 | "text": "I'm the holiday armadillo!", 18 | "user": "ross@friends.com" 19 | }, 20 | { 21 | "id": "c03b65c8-477b-4814-aed0-b090d51e4ca0", 22 | "text": "It's like...all my life, everyone has always told me: \"You're a shoe!\"", 23 | "user": "rachel@friends.com" 24 | }, 25 | { 26 | "id": "911327fa-c6fc-467f-8138-debedaa6d3ce", 27 | "text": "I...am over...YOU.", 28 | "user": "rachel@friends.com" 29 | }, 30 | { 31 | "id": "ede699aa-9459-4feb-b95e-db1271ab41b7", 32 | "text": "Imagine the worst things you think about yourself. Now, how would you feel if the one person that you trusted the most in the world not only thinks them too, but actually uses them as reasons not to be with you.", 33 | "user": "rachel@friends.com" 34 | }, 35 | { 36 | "id": "c58741cf-22fd-4036-88de-fe51fd006cfc", 37 | "text": "You threw my sandwich away?", 38 | "user": "ross@friends.com" 39 | }, 40 | { 41 | "id": "dc8016e0-5d91-45c4-b4fa-48cecee11842", 42 | "text": "I grew up with Monica. If you didn't eat fast, you didn't eat!", 43 | "user": "ross@friends.com" 44 | }, 45 | { 46 | "id": "87ba7f3a-2ce7-4aa0-9827-28261735f518", 47 | "text": "I'm gonna go get one of those job things.", 48 | "user": "rachel@friends.com" 49 | }, 50 | { 51 | "id": "9aad4cbc-7fff-45b3-8373-a64d3fdb239b", 52 | "text": "Ross, I am a human doodle!", 53 | "user": "rachel@friends.com" 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue2-auth", 3 | "version": "0.1.0", 4 | "description": "Authentication using VueJS 2.0, Express, and Passport", 5 | "main": "index.js", 6 | "scripts": { 7 | "prestart": "npm run build:js", 8 | "start": "node server.js", 9 | "serve": "nodemon server.js", 10 | "build:js": "browserify src/app.js -t vueify -p [ vueify/plugins/extract-css -o public/styles.bundle.css ] -t babelify -o public/app.bundle.js", 11 | "watch:js": "watchify src/app.js -t vueify -t babelify -p browserify-hmr -p [ vueify/plugins/extract-css -o public/styles.bundle.css ] -o public/app.bundle.js", 12 | "dev": "concurrently \"npm run serve\" \"npm run watch:js\"" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/searsaw/vue2-auth.git" 17 | }, 18 | "keywords": [], 19 | "author": "Alex Sears (http://alexsears.com/)", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/searsaw/vue2-auth/issues" 23 | }, 24 | "homepage": "https://github.com/searsaw/vue2-auth#readme", 25 | "engines": { 26 | "node": "6.1.0" 27 | }, 28 | "dependencies": { 29 | "axios": "^0.12.0", 30 | "body-parser": "^1.15.1", 31 | "connect-mongo": "^1.2.1", 32 | "express": "^4.13.4", 33 | "express-session": "^1.13.0", 34 | "flash": "^1.1.0", 35 | "node-uuid": "^1.4.7", 36 | "passport": "^0.3.2", 37 | "passport-local": "^1.0.0", 38 | "pug": "^2.0.0-beta3", 39 | "vue": "^2.0.0-alpha.8", 40 | "babel-core": "^6.10.4", 41 | "babel-plugin-transform-runtime": "^6.9.0", 42 | "babel-preset-es2015": "^6.9.0", 43 | "babel-runtime": "^6.9.2", 44 | "babelify": "^7.3.0", 45 | "browserify": "^13.0.1", 46 | "vueify": "^9.1.0" 47 | }, 48 | "devDependencies": { 49 | "browserify-hmr": "^0.3.1", 50 | "concurrently": "^2.1.0", 51 | "eslint": "^2.11.1", 52 | "eslint-config-airbnb-base": "^3.0.1", 53 | "eslint-plugin-html": "^1.5.1", 54 | "eslint-plugin-import": "^1.8.1", 55 | "nodemon": "^1.9.2", 56 | "vue-hot-reload-api": "^2.0.3", 57 | "watchify": "^3.7.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /public/styles.bundle.css: -------------------------------------------------------------------------------- 1 | .exclamations-viewer, 2 | .add-form-container { 3 | margin-top: 20px; 4 | } 5 | .exclamation-list[data-v-2] { 6 | background-color: #FAFAFA; 7 | border: 2px solid #222; 8 | border-radius: 7px; 9 | } 10 | 11 | .exclamation-list h1[data-v-2] { 12 | font-size: 1.5em; 13 | text-align: center; 14 | } 15 | 16 | .exclamation[data-v-2]:nth-child(2) { 17 | border-top: 1px solid #222; 18 | } 19 | 20 | .exclamation[data-v-2] { 21 | padding: 5px; 22 | border-bottom: 1px solid #222; 23 | } 24 | 25 | .user[data-v-2] { 26 | font-weight: bold; 27 | margin-top: 10px; 28 | margin-bottom: 5px; 29 | } -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // Import needed modules 2 | const express = require('express'); 3 | const bodyParser = require('body-parser'); 4 | const session = require('express-session'); 5 | const MongoStore = require('connect-mongo')(session); 6 | const flash = require('flash'); 7 | const passport = require('passport'); 8 | const LocalStrategy = require('passport-local'); 9 | const uuid = require('node-uuid'); 10 | const appData = require('./data.json'); 11 | 12 | // Create app data (mimics a DB) 13 | const userData = appData.users; 14 | const exclamationData = appData.exclamations; 15 | 16 | function getUser(username) { 17 | const user = userData.find(u => u.username === username); 18 | return Object.assign({}, user); 19 | } 20 | 21 | // Create default port 22 | const PORT = process.env.PORT || 3000; 23 | 24 | // Create a new server 25 | const server = express(); 26 | 27 | // Configure server 28 | server.use(bodyParser.json()); 29 | server.use(bodyParser.urlencoded({ extended: false })); 30 | server.use(session({ 31 | secret: process.env.SESSION_SECRET || 'awesomecookiesecret', 32 | resave: false, 33 | saveUninitialized: false, 34 | store: new MongoStore({ 35 | url: process.env.MONGO_URL || 'mongodb://localhost/vue2-auth', 36 | }), 37 | })); 38 | server.use(flash()); 39 | server.use(express.static('public')); 40 | server.use(passport.initialize()); 41 | server.use(passport.session()); 42 | server.set('views', './views'); 43 | server.set('view engine', 'pug'); 44 | 45 | // Configure Passport 46 | passport.use(new LocalStrategy( 47 | (username, password, done) => { 48 | const user = getUser(username); 49 | 50 | if (!user || user.password !== password) { 51 | return done(null, false, { message: 'Username and password combination is wrong' }); 52 | } 53 | 54 | delete user.password; 55 | 56 | return done(null, user); 57 | } 58 | )); 59 | 60 | // Serialize user in session 61 | passport.serializeUser((user, done) => { 62 | done(null, user.username); 63 | }); 64 | 65 | passport.deserializeUser((username, done) => { 66 | const user = getUser(username); 67 | 68 | delete user.password; 69 | 70 | done(null, user); 71 | }); 72 | 73 | // Create custom middleware functions 74 | function hasScope(scope) { 75 | return (req, res, next) => { 76 | const { scopes } = req.user; 77 | 78 | if (!scopes.includes(scope)) { 79 | req.flash('error', 'The username and password are not valid.'); 80 | return res.redirect('/'); 81 | } 82 | 83 | return next(); 84 | }; 85 | } 86 | 87 | function canDelete(req, res, next) { 88 | const { scopes, username } = req.user; 89 | const { id } = req.params; 90 | const exclamation = exclamationData.find(exc => exc.id === id); 91 | 92 | if (!exclamation) { 93 | return res.sendStatus(404); 94 | } 95 | 96 | if (exclamation.user !== username && !scopes.includes('delete')) { 97 | return res.status(403).json({ message: "You can't delete that exclamation." }); 98 | } 99 | 100 | return next(); 101 | } 102 | 103 | function isAuthenticated(req, res, next) { 104 | if (!req.user) { 105 | req.flash('error', 'You must be logged in.'); 106 | return res.redirect('/'); 107 | } 108 | 109 | return next(); 110 | } 111 | 112 | // Create home route 113 | server.get('/', (req, res) => { 114 | if (req.user) { 115 | return res.redirect('/dashboard'); 116 | } 117 | 118 | return res.render('index'); 119 | }); 120 | 121 | server.get('/dashboard', 122 | isAuthenticated, 123 | (req, res) => { 124 | res.render('dashboard'); 125 | } 126 | ); 127 | 128 | // Create auth routes 129 | const authRoutes = express.Router(); 130 | 131 | authRoutes.post('/login', 132 | passport.authenticate('local', { 133 | failureRedirect: '/', 134 | successRedirect: '/dashboard', 135 | failureFlash: true, 136 | }) 137 | ); 138 | 139 | server.use('/auth', authRoutes); 140 | 141 | // Create API routes 142 | const apiRoutes = express.Router(); 143 | 144 | apiRoutes.use(isAuthenticated); 145 | 146 | apiRoutes.get('/me', (req, res) => { 147 | res.json({ user: req.user }); 148 | }); 149 | 150 | // Get all of a user's exclamations 151 | apiRoutes.get('/exclamations', 152 | hasScope('read'), 153 | (req, res) => { 154 | const exclamations = exclamationData; 155 | 156 | res.json({ exclamations }); 157 | } 158 | ); 159 | 160 | // Add an exclamation 161 | apiRoutes.post('/exclamations', 162 | hasScope('add'), 163 | (req, res) => { 164 | const { username } = req.user; 165 | const { text } = req.body; 166 | const exclamation = { 167 | id: uuid.v4(), 168 | text, 169 | user: username, 170 | }; 171 | 172 | exclamationData.unshift(exclamation); 173 | 174 | res.status(201).json({ exclamation }); 175 | } 176 | ); 177 | 178 | // Delete an exclamation 179 | apiRoutes.delete('/exclamations/:id', 180 | canDelete, 181 | (req, res) => { 182 | const { id } = req.params; 183 | const exclamationIndex = exclamationData.findIndex(exc => exc.id === id); 184 | 185 | exclamationData.splice(exclamationIndex, 1); 186 | 187 | res.sendStatus(204); 188 | } 189 | ); 190 | 191 | server.use('/api', apiRoutes); 192 | 193 | // Start the server 194 | server.listen(PORT, () => { 195 | console.log(`The API is listening on port ${PORT}`); 196 | }); 197 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import ExclamationsViewer from './exclamations_viewer.vue'; 3 | 4 | new Vue({ 5 | el: '#app-container', 6 | render(createElement) { 7 | return createElement(ExclamationsViewer); 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/exclamation_add_form.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 27 | -------------------------------------------------------------------------------- /src/exclamation_list.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 39 | 40 | 68 | -------------------------------------------------------------------------------- /src/exclamation_search_list.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 58 | -------------------------------------------------------------------------------- /src/exclamations_viewer.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 28 | 29 | 80 | -------------------------------------------------------------------------------- /views/dashboard.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang='en') 3 | head 4 | title Dashboard 5 | link(rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css' integrity='sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7' crossorigin='anonymous') 6 | link(rel='stylesheet' href='/styles.bundle.css') 7 | body 8 | #app-container 9 | script(src='app.bundle.js') 10 | -------------------------------------------------------------------------------- /views/index.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang='en') 3 | head 4 | title Exclamations! 5 | link(rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css' integrity='sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7' crossorigin='anonymous') 6 | 7 | link(rel='stylesheet' href='/styles.bundle.css') 8 | style. 9 | h1 { 10 | margin-bottom: 20px; 11 | } 12 | body 13 | .container-fluid 14 | .row 15 | .col-md-4.col-md-offset-4 16 | while message = flash.shift() 17 | .alert.alert-danger 18 | p= message.message 19 | h1.text-center Exclamations! 20 | form(action='/auth/login' method='POST') 21 | .form-group 22 | label(for='username') Email Address 23 | input.form-control(name='username') 24 | .form-group 25 | label(for='password') Password 26 | input.form-control(name='password' type='password') 27 | button.btn.btn-primary(type='submit') Login 28 | --------------------------------------------------------------------------------