├── .bowerrc ├── .codeclimate.yml ├── .csslintrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .jshint ├── .travis.yml ├── LICENSE ├── Procfile ├── README.md ├── app ├── controllers │ ├── auth.js │ ├── index.js │ └── register.js ├── models │ └── user.js ├── public │ ├── images │ │ └── logo.png │ ├── javascript │ │ └── application.js │ └── stylesheets │ │ ├── cover.css │ │ └── style.css ├── routes │ ├── api │ │ └── user.js │ ├── auth.js │ ├── index.js │ └── register.js ├── services │ ├── auth.js │ ├── index.js │ └── register.js ├── utils │ ├── crypt.js │ └── validation.js └── views │ ├── auth │ └── login.jade │ ├── error.jade │ ├── includes │ ├── facebook-meta.jade │ ├── footer.jade │ ├── landing.jade │ ├── messages.jade │ ├── meta.jade │ ├── twitter-cards.jade │ └── user-info.jade │ ├── index.jade │ ├── layouts │ └── default.jade │ └── register │ └── index.jade ├── bin └── www ├── bower.json ├── config ├── config.json ├── mongoose.js ├── passport.js └── routes.js ├── favicon.ico ├── locales ├── en.json ├── pt-br.json └── pt.json ├── main.js ├── package.json └── tests.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "app/public/vendor" 3 | } -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | csslint: 4 | enabled: true 5 | duplication: 6 | enabled: false 7 | config: 8 | languages: 9 | - javascript 10 | eslint: 11 | enabled: true 12 | fixme: 13 | enabled: true 14 | ratings: 15 | paths: 16 | - "**.css" 17 | - "**.js" -------------------------------------------------------------------------------- /.csslintrc: -------------------------------------------------------------------------------- 1 | --exclude-exts=.min.css 2 | --ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = false 8 | indent_style = space 9 | indent_size = 4 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.json] 15 | indent_size = 2 16 | 17 | [app/public/**.{jade,js,css}] 18 | indent_size = 2 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*{.,-}min.js 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | ecmaFeatures: 2 | modules: true 3 | jsx: true 4 | 5 | env: 6 | amd: true 7 | browser: true 8 | es6: true 9 | jquery: true 10 | node: true 11 | 12 | # http://eslint.org/docs/rules/ 13 | rules: 14 | # Possible Errors 15 | comma-dangle: [2, never] 16 | no-cond-assign: 2 17 | no-console: 0 18 | no-constant-condition: 2 19 | no-control-regex: 2 20 | no-debugger: 2 21 | no-dupe-args: 2 22 | no-dupe-keys: 2 23 | no-duplicate-case: 2 24 | no-empty: 2 25 | no-empty-character-class: 2 26 | no-ex-assign: 2 27 | no-extra-boolean-cast: 2 28 | no-extra-parens: 0 29 | no-extra-semi: 2 30 | no-func-assign: 2 31 | no-inner-declarations: [2, functions] 32 | no-invalid-regexp: 2 33 | no-irregular-whitespace: 2 34 | no-negated-in-lhs: 2 35 | no-obj-calls: 2 36 | no-regex-spaces: 2 37 | no-sparse-arrays: 2 38 | no-unexpected-multiline: 2 39 | no-unreachable: 2 40 | use-isnan: 2 41 | valid-jsdoc: 0 42 | valid-typeof: 2 43 | 44 | # Best Practices 45 | accessor-pairs: 2 46 | block-scoped-var: 0 47 | complexity: 0 48 | consistent-return: 0 49 | curly: 0 50 | default-case: 0 51 | dot-location: 0 52 | dot-notation: 0 53 | eqeqeq: 2 54 | guard-for-in: 2 55 | no-alert: 2 56 | no-caller: 2 57 | no-case-declarations: 2 58 | no-div-regex: 2 59 | no-else-return: 0 60 | no-empty-label: 0 61 | no-empty-pattern: 2 62 | no-eq-null: 2 63 | no-eval: 2 64 | no-extend-native: 2 65 | no-extra-bind: 2 66 | no-fallthrough: 2 67 | no-floating-decimal: 0 68 | no-implicit-coercion: 0 69 | no-implied-eval: 2 70 | no-invalid-this: 0 71 | no-iterator: 2 72 | no-labels: 0 73 | no-lone-blocks: 2 74 | no-loop-func: 2 75 | no-magic-number: 0 76 | no-multi-spaces: 0 77 | no-multi-str: 0 78 | no-native-reassign: 2 79 | no-new-func: 2 80 | no-new-wrappers: 2 81 | no-new: 2 82 | no-octal-escape: 2 83 | no-octal: 2 84 | no-proto: 2 85 | no-redeclare: 2 86 | no-return-assign: 2 87 | no-script-url: 2 88 | no-self-compare: 2 89 | no-sequences: 0 90 | no-throw-literal: 0 91 | no-unused-expressions: 2 92 | no-useless-call: 2 93 | no-useless-concat: 2 94 | no-void: 2 95 | no-warning-comments: 0 96 | no-with: 2 97 | radix: 2 98 | vars-on-top: 0 99 | wrap-iife: 2 100 | yoda: 0 101 | 102 | # Strict 103 | strict: [2, "global"] 104 | 105 | # Variables 106 | init-declarations: 0 107 | no-catch-shadow: 2 108 | no-delete-var: 2 109 | no-label-var: 2 110 | no-shadow-restricted-names: 2 111 | no-shadow: 0 112 | no-undef-init: 2 113 | no-undef: 0 114 | no-undefined: 0 115 | no-unused-vars: 0 116 | no-use-before-define: 0 117 | 118 | # Node.js and CommonJS 119 | callback-return: 2 120 | global-require: 0 121 | handle-callback-err: 2 122 | no-mixed-requires: 0 123 | no-new-require: 0 124 | no-path-concat: 2 125 | no-process-exit: 0 126 | no-restricted-modules: 0 127 | no-sync: 0 128 | 129 | # Stylistic Issues 130 | array-bracket-spacing: 0 131 | block-spacing: 0 132 | brace-style: 0 133 | camelcase: 0 134 | comma-spacing: 0 135 | comma-style: 0 136 | computed-property-spacing: 0 137 | consistent-this: 0 138 | eol-last: 0 139 | func-names: 0 140 | func-style: 0 141 | id-length: 0 142 | id-match: 0 143 | indent: 0 144 | jsx-quotes: 0 145 | key-spacing: 0 146 | linebreak-style: 0 147 | lines-around-comment: 0 148 | max-depth: 0 149 | max-len: 0 150 | max-nested-callbacks: 0 151 | max-params: 0 152 | max-statements: [2, 30] 153 | new-cap: 0 154 | new-parens: 0 155 | newline-after-var: 0 156 | no-array-constructor: 0 157 | no-bitwise: 0 158 | no-continue: 0 159 | no-inline-comments: 0 160 | no-lonely-if: 0 161 | no-mixed-spaces-and-tabs: 0 162 | no-multiple-empty-lines: 0 163 | no-negated-condition: 0 164 | no-nested-ternary: 0 165 | no-new-object: 0 166 | no-plusplus: 0 167 | no-restricted-syntax: 0 168 | no-spaced-func: 0 169 | no-ternary: 0 170 | no-trailing-spaces: 0 171 | no-underscore-dangle: 0 172 | no-unneeded-ternary: 0 173 | object-curly-spacing: 0 174 | one-var: 0 175 | operator-assignment: 0 176 | operator-linebreak: 0 177 | padded-blocks: 0 178 | quote-props: 0 179 | quotes: 0 180 | require-jsdoc: 0 181 | semi-spacing: 0 182 | semi: 0 183 | sort-vars: 0 184 | space-after-keywords: 0 185 | space-before-blocks: 0 186 | space-before-function-paren: 0 187 | space-before-keywords: 0 188 | space-in-parens: 0 189 | space-infix-ops: 0 190 | space-return-throw-case: 0 191 | space-unary-ops: 0 192 | spaced-comment: 0 193 | wrap-regex: 0 194 | 195 | # ECMAScript 6 196 | arrow-body-style: 0 197 | arrow-parens: 0 198 | arrow-spacing: 0 199 | constructor-super: 0 200 | generator-star-spacing: 0 201 | no-arrow-condition: 0 202 | no-class-assign: 0 203 | no-const-assign: 0 204 | no-dupe-class-members: 0 205 | no-this-before-super: 0 206 | no-var: 0 207 | object-shorthand: 0 208 | prefer-arrow-callback: 0 209 | prefer-const: 0 210 | prefer-reflect: 0 211 | prefer-spread: 0 212 | prefer-template: 0 213 | require-yield: 0 214 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 30 | node_modules 31 | bower_components 32 | 33 | # Debug log from npm 34 | npm-debug.log 35 | 36 | app/public/vendor -------------------------------------------------------------------------------- /.jshint: -------------------------------------------------------------------------------- 1 | { 2 | "asi": false, 3 | "bitwise": false, 4 | "browser": true, 5 | "curly": true, 6 | "eqeqeq": true, 7 | "esnext": true, 8 | "evil": false, 9 | "forin": false, 10 | "globals": { 11 | "after": true, 12 | "afterEach": true, 13 | "assert": true, 14 | "before": true, 15 | "beforeEach": true, 16 | "describe": true, 17 | "it": true, 18 | "mocha": true, 19 | "test": true, 20 | "Kairos": true, 21 | "fabric": true 22 | }, 23 | "immed": true, 24 | "indent": 2, 25 | "lastsemic": false, 26 | "maxdepth": false, 27 | "multistr": false, 28 | "newcap": true, 29 | "noarg": true, 30 | "node": true, 31 | "onevar": false, 32 | "quotmark": "single", 33 | "regexp": true, 34 | "smarttabs": true, 35 | "strict": true, 36 | "trailing": true, 37 | "undef": true, 38 | "unused": true 39 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 4.2.3 4 | - 4.2.4 5 | - 5.3.0 6 | install: 7 | - npm install 8 | script: 9 | - npm test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Rodrigo Gomes da Silva 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above 8 | copyright notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 15 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 16 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 17 | THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 18 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 19 | OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 21 | TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 22 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm install bower -g && bower install && node ./bin/www -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node.js Web Jade Scaffold 2 | 3 | [![Build Status](https://travis-ci.org/rodrigogs/nodejs-web-jade-scaffold.svg?branch=master)](https://travis-ci.org/rodrigogs/nodejs-web-jade-scaffold) 4 | [![Code Climate](https://codeclimate.com/github/rodrigogs/nodejs-web-jade-scaffold/badges/gpa.svg)](https://codeclimate.com/github/rodrigogs/nodejs-web-jade-scaffold) 5 | [![Dependency Status](https://david-dm.org/rodrigogs/nodejs-web-jade-scaffold.svg)](https://david-dm.org/rodrigogs/nodejs-web-jade-scaffold) 6 | [![devDependency Status](https://david-dm.org/rodrigogs/nodejs-web-jade-scaffold/dev-status.svg)](https://david-dm.org/rodrigogs/nodejs-web-jade-scaffold#info=devDependencies) 7 | 8 | ## Structure 9 | ``` 10 | project 11 | ├── app 12 | │ ├── controllers 13 | │ │ ├── api 14 | │ │ │ └── example.js 15 | │ │ └── example.js 16 | │ │ 17 | │ ├── models 18 | │ │ └── example.js 19 | │ │ 20 | │ ├── public 21 | │ │ ├── javascript 22 | │ │ │ └── example.js 23 | │ │ └── stylesheets 24 | │ │ └── example.css 25 | │ │ 26 | │ ├── routes 27 | │ │ ├── api 28 | │ │ │ └── example.js 29 | │ │ └── example.js 30 | │ │ 31 | │ ├── services 32 | │ │ └── example.js 33 | │ │ 34 | │ ├── utils 35 | │ │ └── example.js 36 | │ │ 37 | │ └── views 38 | │ ├── error.jade <-- Error template 39 | │ ├── includes 40 | │ │ ├── footer.jade <-- Footer template 41 | │ │ ├── messages.jade <-- Flash messages template 42 | │ │ ├── meta.jade <-- General meta content 43 | │ │ ├── facebook-meta.jade <-- Facebook meta data 44 | │ │ └── twitter-cards.jade <-- Twitter meta data for whitelisting 45 | │ ├── layouts 46 | │ │ └── default.jade <-- Default layout 47 | │ └── index.jade <-- Index template 48 | │ 49 | ├── bin 50 | │ └── www <-- HTTP server runner 51 | │ 52 | ├── config 53 | │ ├── config.json <-- General configurations file 54 | │ ├── mongoose.js <-- Mongoose connection configuration 55 | │ ├── passport.js <-- Passport routes and strategies 56 | │ └── routes.js <-- General routes 57 | │ 58 | ├── locales <-- Locale files should be referenced in config.json file 59 | │ ├── en.json 60 | │ ├── pt-br.json 61 | │ └── pt.json 62 | │ 63 | ├── main.js <-- Main app file 64 | ├── tests.js < -- Tests file 65 | ├── bower.json 66 | ├── favicon.ico <-- Website favicon 67 | └── package.json 68 | ``` 69 | 70 | ## Setup 71 | 72 | ### Installation 73 | 74 | > npm install 75 | 76 | > npm install bower -g 77 | 78 | > bower install 79 | 80 | > [Install MongoDB](https://www.mongodb.org/downloads) 81 | 82 | ### Configuration 83 | 84 | #### Environment variables 85 | * IP 86 | - export IP="192.168.0.1" 87 | * PORT 88 | - export PORT="8080" 89 | * SESSION_SECRET 90 | - export SESSION_SECRET="mysecret" 91 | * DATABASE_URL 92 | - export DATABASE_URL="mongodb://localhost:27017/example" 93 | * FACEBOOK_APP_ID: See https://developers.facebook.com/docs/apps/register#app-id 94 | - export FACEBOOK_APP_ID="myfacebookid" 95 | * FACEBOOK_APP_SECRET: See https://developers.facebook.com/docs/apps/register#app-secret 96 | - export FACEBOOK_APP_SECRET="myfacebooksecret" 97 | * TWITTER_CONSUMER_KEY 98 | - export TWITTER_CONSUMER_KEY="mytwitterconsumerkey" 99 | * TWITTER_CONSUMER_SECRET 100 | - export TWITTER_CONSUMER_SECRET="mytwitterconsumersecret" 101 | * GOOGLE_CLIENT_ID 102 | - export GOOGLE_CLIENT_ID="mygoogleclientid" 103 | * GOOGLE_CLIENT_SECRET 104 | - exports GOOGLE_CLIENT_SECRET="mygoogleclientsecret" 105 | * GITHUB_CLIENT_ID 106 | - export GITHUB_CLIENT_ID="mygithubclientid" 107 | * GITHUB_CLIENT_SECRET 108 | - exports GITHUB_CLIENT_SECRET="mygithubclientsecret" 109 | * LINKEDIN_KEY 110 | - export LINKEDIN_KEY="mylinkedinkey" 111 | * LINKEDIN_KEY 112 | - export LINKEDIN_SECRET="mylinkedinsecret" 113 | * INSTAGRAM_CLIENT_ID 114 | - export INSTAGRAM_CLIENT_ID="myinstamgramclientid" 115 | * INSTAGRAM_CLIENT_SECRET 116 | - export INSTAGRAM_CLIENT_SECRET="myinstamgramclientsecret" 117 | 118 | #### Project configuration 119 | * Project/config/config.json 120 | - DATABASE.RECONNECT: Enable or disable auto reconnection. 121 | - DATABASE.RECONNECTION_INTERVAL: Interval for auto reconnection tries. 122 | - LOCALES: Reference your locale files in the locale folder. Only the locales defined here will be use by the app. 123 | - HTTP_LOG_CONFIG: See https://github.com/expressjs/morgan#predefined-formats 124 | - AUTH.ENABLED: Enable or disable authentication. 125 | - AUTH.LOCAL.ENABLED: Enable or disable local authentication. 126 | - AUTH.FACEBOOK.ENABLED: Enable or disable Facebook authentication. 127 | - AUTH.FACEBOOK.PROFILE_FIELDS: Facebook profile fields wanted. 128 | - AUTH.FACEBOOK.OPTIONS: Facebook API options. 129 | - AUTH.TWITTER.ENABLED: Enable or disable Twitter authentication. 130 | - AUTH.GOOGLE.ENABLED: Enable or disable Google authentication. 131 | - AUTH.GOOGLE.OPTIONS: Google API options. 132 | - AUTH.GITHUB.ENABLED: Enable or disable GitHub authentication. 133 | - AUTH.LINKEDIN.ENABLED: Enable or disable Linkedin authentication. 134 | - AUTH.LINKEDIN.OPTIONS: Linkedin api options. 135 | - AUTH.INSTAGRAM.ENABLED: Enable or disable Instagram authentication. 136 | 137 | ## Launching 138 | 139 | First start MongoDB if you don't have a running instance 140 | 141 | > mongod 142 | 143 | #### Development 144 | 145 | > npm start 146 | 147 | #### Production 148 | 149 | > node bin/www 150 | 151 | #### Test 152 | 153 | > npm test 154 | 155 | [Running Example](http://nodejs-web-jade-scaffold.herokuapp.com/) 156 | 157 | ## TODO 158 | 159 | * User/Role management 160 | * More tests 161 | 162 | ## License 163 | 164 | [Licence](https://github.com/rodrigogs/nodejs-web-jade-scaffold/blob/master/LICENSE) © Rodrigo Gomes da Silva 165 | -------------------------------------------------------------------------------- /app/controllers/auth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const CONFIG = require('../../config/config.json'); 4 | const passport = require('passport'); 5 | 6 | const OPTIONS = { 7 | badRequestMessage: 'passport.badrequest', // 18n reference 8 | successRedirect: '/', 9 | failureRedirect: '/auth/login', 10 | failureFlash: true 11 | }; 12 | 13 | module.exports = { 14 | 15 | /** 16 | * Index action 17 | */ 18 | index: (req, res, next) => { 19 | res.render('auth/login'); 20 | }, 21 | 22 | /** 23 | * Login action 24 | */ 25 | login: passport.authenticate('local', OPTIONS), 26 | 27 | /** 28 | * Facebook action 29 | */ 30 | facebook: passport.authenticate('facebook', CONFIG.AUTH.FACEBOOK.OPTIONS), 31 | 32 | /** 33 | * FacebookCallback action 34 | */ 35 | facebookCallback: passport.authenticate('facebook', OPTIONS), 36 | 37 | /** 38 | * Twitter action 39 | */ 40 | twitter: passport.authenticate('twitter'), 41 | 42 | /** 43 | * TwitterCallback action 44 | */ 45 | twitterCallback: passport.authenticate('twitter', OPTIONS), 46 | 47 | /** 48 | * Google action 49 | */ 50 | google: passport.authenticate('google', CONFIG.AUTH.GOOGLE.OPTIONS), 51 | 52 | /** 53 | * GoogleCallback action 54 | */ 55 | googleCallback: passport.authenticate('google', OPTIONS), 56 | 57 | /** 58 | * Github action 59 | */ 60 | github: passport.authenticate('github'), 61 | 62 | /** 63 | * GithubCallback action 64 | */ 65 | githubCallback: passport.authenticate('github', OPTIONS), 66 | 67 | /** 68 | * Linkedin action 69 | */ 70 | linkedin: passport.authenticate('linkedin', { state: CONFIG.AUTH.LINKEDIN.OPTIONS.state }), 71 | 72 | /** 73 | * LinkedinCallback action 74 | */ 75 | linkedinCallback: passport.authenticate('linkedin', OPTIONS), 76 | 77 | /** 78 | * Instagram action 79 | */ 80 | instagram: passport.authenticate('instagram'), 81 | 82 | /** 83 | * InstagramCallback action 84 | */ 85 | instagramCallback: passport.authenticate('instagram', OPTIONS), 86 | 87 | /** 88 | * Logout action 89 | */ 90 | logout: (req, res, next) => { 91 | req.logout(); 92 | res.redirect('/'); 93 | } 94 | }; -------------------------------------------------------------------------------- /app/controllers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const IndexService = require('../services/index'); 4 | 5 | module.exports = { 6 | 7 | /** 8 | * Index action 9 | */ 10 | index: (req, res, next) => { 11 | let message = IndexService.getMessage(); 12 | 13 | res.render('index', {message: message}); 14 | } 15 | }; -------------------------------------------------------------------------------- /app/controllers/register.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RegisterService = require('../services/register'); 4 | 5 | module.exports = { 6 | 7 | /** 8 | * Index action 9 | */ 10 | index: (req, res, next) => { 11 | res.render('register/index'); 12 | }, 13 | 14 | /** 15 | * Register action 16 | */ 17 | register: (req, res, next) => { 18 | RegisterService.register(req.body, (err, errors, user) => { 19 | if (err) { 20 | return next(err); 21 | } 22 | 23 | if (errors) { 24 | for (let error in errors) { 25 | if ((error = errors[error]).messageCode) { 26 | req.flash('warning', req.__(error.messageCode, req.__(error.property), error.value)); 27 | } 28 | } 29 | return res.render('register/index', {user: req.body}); 30 | } 31 | 32 | if (!user) { 33 | req.flash('warning', req.__('user.error-creating')); 34 | return res.render('register/index', {user: req.body}); 35 | } 36 | 37 | req.flash('success', req.__('user.created')); 38 | res.redirect('/'); 39 | }); 40 | } 41 | }; -------------------------------------------------------------------------------- /app/models/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mongoose = require('mongoose'); 4 | const uniqueValidator = require('mongoose-unique-validator'); 5 | const Schema = mongoose.Schema; 6 | const CryptUtils = require('../utils/crypt'); 7 | const validator = require('validator'); 8 | 9 | const UserSchema = new Schema({ 10 | name: { 11 | type: String, 12 | required: true 13 | }, 14 | last_name: { 15 | type: String 16 | }, 17 | email: { 18 | type: String, 19 | index: { unique: true }, 20 | required: true, 21 | validate: [email => { // Since Validator version 5.0.0 validator.isEmail itself does not work. I had to wrap it with a function, so it works. 22 | return validator.isEmail(email) 23 | }, 'user.invalidemail'] 24 | }, 25 | user_name: { 26 | type: String, 27 | index: { unique: true }, 28 | required: true 29 | }, 30 | password: { 31 | type: String 32 | }, 33 | facebook_id: { 34 | type: String, 35 | index: { 36 | unique: true, 37 | sparse: true 38 | } 39 | }, 40 | twitter_id: { 41 | type: String, 42 | index: { 43 | unique: true, 44 | sparse: true 45 | } 46 | }, 47 | google_id: { 48 | type: String, 49 | index: { 50 | unique: true, 51 | sparse: true 52 | } 53 | }, 54 | github_id: { 55 | type: String, 56 | index: { 57 | unique: true, 58 | sparse: true 59 | } 60 | }, 61 | linkedin_id: { 62 | type: String, 63 | index: { 64 | unique: true, 65 | sparse: true 66 | } 67 | }, 68 | instagram_id: { 69 | type: String, 70 | index: { 71 | unique: true, 72 | sparse: true 73 | } 74 | }, 75 | creation_date: { 76 | type: Date, 77 | default: new Date() 78 | } 79 | }); 80 | 81 | UserSchema.post('validate', doc => { 82 | if (doc.isModified('password')) { 83 | doc.password = CryptUtils.encrypt(doc.password); 84 | } 85 | 86 | if (!doc._id) { 87 | doc.creation_date = Date.now(); 88 | } 89 | }); 90 | 91 | UserSchema.plugin(uniqueValidator, { message: 'mongoose.unique-error' }); 92 | 93 | module.exports = mongoose.model('User', UserSchema); -------------------------------------------------------------------------------- /app/public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrigogs/nodejs-web-jade-scaffold/b2d87b790a02baaa82f7b8e7317ce854a97a72bd/app/public/images/logo.png -------------------------------------------------------------------------------- /app/public/javascript/application.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrigogs/nodejs-web-jade-scaffold/b2d87b790a02baaa82f7b8e7317ce854a97a72bd/app/public/javascript/application.js -------------------------------------------------------------------------------- /app/public/stylesheets/cover.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Globals 3 | */ 4 | 5 | /* Links */ 6 | a, 7 | a:focus, 8 | a:hover { 9 | color: #fff; 10 | } 11 | 12 | /* Custom default button */ 13 | .btn-default, 14 | .btn-default:hover, 15 | .btn-default:focus { 16 | color: #333; 17 | text-shadow: none; /* Prevent inheritence from `body` */ 18 | background-color: #fff; 19 | border: 1px solid #fff; 20 | } 21 | 22 | 23 | /* 24 | * Base structure 25 | */ 26 | 27 | html, 28 | body { 29 | height: 100%; 30 | background-color: #333; 31 | } 32 | body { 33 | color: #fff; 34 | text-align: center; 35 | text-shadow: 0 1px 3px rgba(0,0,0,.5); 36 | } 37 | 38 | /* Extra markup and styles for table-esque vertical and horizontal centering */ 39 | .site-wrapper { 40 | display: table; 41 | width: 100%; 42 | height: 100%; /* For at least Firefox */ 43 | min-height: 100%; 44 | -webkit-box-shadow: inset 0 0 100px rgba(0,0,0,.5); 45 | box-shadow: inset 0 0 100px rgba(0,0,0,.5); 46 | } 47 | .site-wrapper-inner { 48 | display: table-cell; 49 | vertical-align: top; 50 | } 51 | .cover-container { 52 | margin-right: auto; 53 | margin-left: auto; 54 | } 55 | 56 | /* Padding for spacing */ 57 | .inner { 58 | padding: 30px; 59 | } 60 | 61 | 62 | /* 63 | * Header 64 | */ 65 | .masthead-brand { 66 | margin-top: 10px; 67 | margin-bottom: 10px; 68 | } 69 | 70 | .masthead-nav > li { 71 | display: inline-block; 72 | } 73 | .masthead-nav > li + li { 74 | margin-left: 20px; 75 | } 76 | .masthead-nav > li > a { 77 | padding-right: 0; 78 | padding-left: 0; 79 | font-size: 16px; 80 | font-weight: bold; 81 | color: #fff; /* IE8 proofing */ 82 | color: rgba(255,255,255,.75); 83 | border-bottom: 2px solid transparent; 84 | } 85 | .masthead-nav > li > a:hover, 86 | .masthead-nav > li > a:focus { 87 | background-color: transparent; 88 | border-bottom-color: #a9a9a9; 89 | border-bottom-color: rgba(255,255,255,.25); 90 | } 91 | .masthead-nav > .active > a, 92 | .masthead-nav > .active > a:hover, 93 | .masthead-nav > .active > a:focus { 94 | color: #fff; 95 | border-bottom-color: #fff; 96 | } 97 | 98 | @media (min-width: 768px) { 99 | .masthead-brand { 100 | float: left; 101 | } 102 | .masthead-nav { 103 | float: right; 104 | } 105 | } 106 | 107 | 108 | /* 109 | * Cover 110 | */ 111 | 112 | .cover { 113 | padding: 65px 20px; 114 | } 115 | .cover .btn-lg { 116 | padding: 10px 20px; 117 | font-weight: bold; 118 | } 119 | 120 | 121 | /* 122 | * Footer 123 | */ 124 | 125 | .mastfoot { 126 | color: #999; /* IE8 proofing */ 127 | color: rgba(255,255,255,.5); 128 | } 129 | 130 | 131 | /* 132 | * Affix and center 133 | */ 134 | 135 | @media (min-width: 768px) { 136 | /* Pull out the header and footer */ 137 | .masthead { 138 | position: fixed; 139 | top: 0; 140 | } 141 | .mastfoot { 142 | position: fixed; 143 | bottom: 0; 144 | } 145 | /* Start the vertical centering */ 146 | .site-wrapper-inner { 147 | vertical-align: middle; 148 | } 149 | /* Handle the widths */ 150 | .masthead, 151 | .mastfoot, 152 | .cover-container { 153 | width: 100%; /* Must be percentage or pixels for horizontal alignment */ 154 | } 155 | } 156 | 157 | @media (min-width: 992px) { 158 | .masthead, 159 | .mastfoot, 160 | .cover-container { 161 | width: 700px; 162 | } 163 | } -------------------------------------------------------------------------------- /app/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | .cover-heading { 2 | margin-top: 50px; 3 | } 4 | 5 | #tech-slider li.sy-slide > a { 6 | height: 100%; 7 | width: auto; 8 | } 9 | 10 | #tech-slider li.sy-slide a > img { 11 | max-width: 100%; 12 | max-height: 250px; 13 | height: auto; 14 | width: auto; 15 | margin: auto; 16 | } -------------------------------------------------------------------------------- /app/routes/api/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const router = express.Router(); 5 | 6 | const UserController = require('../../controllers/api/user'); 7 | 8 | router.route('/') 9 | .get(UserController.list) 10 | .post(UserController.create) 11 | 12 | router.route('/:id') 13 | .get(UserController.findById) 14 | .put(UserController.update) 15 | .delete(UserController.delete) 16 | 17 | module.exports = router; -------------------------------------------------------------------------------- /app/routes/auth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const router = express.Router(); 5 | 6 | const AuthController = require('../controllers/auth'); 7 | 8 | router.route('/auth/login') 9 | .get(AuthController.index) 10 | .post(AuthController.login) 11 | .get(AuthController.logout); 12 | 13 | router 14 | .get('/auth/facebook', AuthController.facebook) 15 | .get('/auth/facebook/callback', AuthController.facebookCallback); 16 | 17 | router 18 | .get('/auth/twitter', AuthController.twitter) 19 | .get('/auth/twitter/callback', AuthController.twitterCallback); 20 | 21 | router 22 | .get('/auth/google', AuthController.google) 23 | .get('/auth/google/callback', AuthController.googleCallback); 24 | 25 | router 26 | .get('/auth/github', AuthController.github) 27 | .get('/auth/github/callback', AuthController.githubCallback); 28 | 29 | router 30 | .get('/auth/linkedin', AuthController.linkedin) 31 | .get('/auth/linkedin/callback', AuthController.linkedinCallback); 32 | 33 | router 34 | .get('/auth/instagram', AuthController.instagram) 35 | .get('/auth/instagram/callback', AuthController.instagramCallback); 36 | 37 | router.route('/auth/logout') 38 | .get(AuthController.logout); 39 | 40 | module.exports = router; -------------------------------------------------------------------------------- /app/routes/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const router = express.Router(); 5 | 6 | const IndexController = require('../controllers/index'); 7 | 8 | router.route('/') 9 | .get(IndexController.index); 10 | 11 | module.exports = router; -------------------------------------------------------------------------------- /app/routes/register.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const router = express.Router(); 5 | 6 | const RegisterController = require('../controllers/register'); 7 | 8 | router.route('/register') 9 | .get(RegisterController.index) 10 | .post(RegisterController.register); 11 | 12 | module.exports = router; -------------------------------------------------------------------------------- /app/services/auth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const validator = require('validator'); 4 | const UserSchema = require('../models/user'); 5 | 6 | /** 7 | * 8 | */ 9 | const _getQuery = info => { 10 | const query = {$or: []}; 11 | if (info.user_name) { 12 | query.$or.push({ 13 | user_name: info.user_name, 14 | password: info.password 15 | }); 16 | } 17 | if (info.email) query.$or.push({email: info.email}); 18 | if (info.facebook_id) query.$or.push({facebook_id: info.facebook_id}); 19 | if (info.twitter_id) query.$or.push({twitter_id: info.twitter_id}); 20 | if (info.google_id) query.$or.push({google_id: info.google_id}); 21 | if (info.github_id) query.$or.push({github_id: info.github_id}); 22 | if (info.linkedin_id) query.$or.push({linkedin_id: info.linkedin_id}); 23 | if (info.instagram_id) query.$or.push({instagram_id: info.instagram_id}); 24 | 25 | return query; 26 | }; 27 | 28 | /** 29 | * 30 | */ 31 | const _resolveLocal = (user, callback) => { 32 | if (!user) { 33 | return callback(null, false, {type: 'danger', message: 'auth.failed'}); 34 | } 35 | 36 | return callback(null, user); 37 | }; 38 | 39 | /** 40 | * 41 | */ 42 | const _updateUser = (user, info, callback) => { 43 | user.facebook_id = info.facebook_id || user.facebook_id; 44 | user.twitter_id = info.twitter_id || user.twitter_id; 45 | user.google_id = info.google_id || user.google_id; 46 | user.github_id = info.github_id || user.github_id; 47 | user.linkedin_id = info.linkedin_id || user.linkedin_id; 48 | user.instagram_id = info.instagram_id || user.instagram_id; 49 | 50 | user.save(callback); 51 | }; 52 | 53 | /** 54 | * 55 | */ 56 | const _createUser = (info, callback) => { 57 | if (!info.user_name && info.email && validator.isEmail(info.email)) { 58 | info.user_name = info.email.split('@')[0]; 59 | } 60 | 61 | let user = new UserSchema(info); 62 | 63 | user.save(callback); 64 | }; 65 | 66 | module.exports = { 67 | 68 | /** 69 | * ResolveUser 70 | */ 71 | resolveUser: (userInfo, callback) => { 72 | const isLocal = userInfo.isLocal; 73 | delete userInfo.isLocal; 74 | 75 | UserSchema.findOne(_getQuery(userInfo), (err, user) => { 76 | if (err) { 77 | return callback(err); 78 | } 79 | 80 | if (isLocal) { 81 | return _resolveLocal(user, callback); 82 | } 83 | 84 | if (user) { 85 | return _updateUser(user, userInfo, callback); 86 | } 87 | 88 | return _createUser(userInfo, callback); 89 | }); 90 | } 91 | }; -------------------------------------------------------------------------------- /app/services/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | 5 | /** 6 | * 7 | */ 8 | getMessage: () => { 9 | return 'main.cover-description'; 10 | } 11 | }; -------------------------------------------------------------------------------- /app/services/register.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const UserSchema = require('../models/user'); 4 | const ValidationUtils = require('../utils/validation'); 5 | 6 | module.exports = { 7 | 8 | /** 9 | * 10 | */ 11 | register: (user, callback) => { 12 | UserSchema.findOne({user_name: user.user_name}, (err, usr) => { 13 | if (err) { 14 | return callback(err); 15 | } 16 | 17 | if (usr) { 18 | return callback(null, [{ messageCode: 'user.exists' }]); 19 | } 20 | 21 | user = new UserSchema(user); 22 | user.save((err, user) => { 23 | if (err) { 24 | if (ValidationUtils.hasValidationErrors(err)) { 25 | return callback(null, ValidationUtils.extractErrors(err, 'user')); 26 | } 27 | return callback(err); 28 | } 29 | 30 | callback(null, null, user); 31 | }); 32 | }); 33 | } 34 | }; -------------------------------------------------------------------------------- /app/utils/crypt.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const crypto = require('crypto'); 4 | 5 | module.exports = { 6 | 7 | /** 8 | * 9 | */ 10 | encrypt: value => { 11 | let cipher = crypto.createCipher('aes-256-ctr', 'my-key'); 12 | let crypted = cipher.update(value, 'utf8', 'hex'); 13 | crypted += cipher.final('hex'); 14 | 15 | return crypted; 16 | } 17 | }; -------------------------------------------------------------------------------- /app/utils/validation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const filterObject = require('filter-object'); 4 | 5 | module.exports = { 6 | 7 | /** 8 | * 9 | */ 10 | hasValidationErrors: error => { 11 | if (!error.errors) { 12 | return false; 13 | } 14 | 15 | return !!filterObject(error.errors, err => { 16 | return err.name === 'ValidatorError'; 17 | }); 18 | }, 19 | 20 | /** 21 | * 22 | */ 23 | extractErrors: (error, i18nAlias) => { 24 | const errors = []; 25 | 26 | if (!error.errors) { 27 | return []; 28 | } 29 | 30 | const validationErrors = filerObject(error.errors, err => { 31 | return err.name === 'ValidatorError'; 32 | }); 33 | 34 | for (let err in validationErrors) { 35 | if ((err = validationErrors[err]).message) { 36 | errors.push({ 37 | messageCode: err.message, 38 | property: i18nAlias ? `${i18nAlias}.${err.path}` : err.path, 39 | value: err.value 40 | }); 41 | } 42 | } 43 | 44 | return errors; 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /app/views/auth/login.jade: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block stylesheets 4 | 5 | block javascripts 6 | 7 | block content 8 | .masthead.clearfix 9 | .inner 10 | h3.masthead-brand #{__('login')} 11 | nav 12 | ul.nav.masthead-nav 13 | li.active 14 | a(href='/') #{__('landing-page')} 15 | 16 | .inner.cover 17 | include ../includes/messages 18 | 19 | form.form.col-md-12.center-block(method='post') 20 | if AUTH_CONFIG.LOCAL.ENABLED 21 | .form-group 22 | input.form-control.input-lg(type='text' placeholder='#{__("user.user_name")}' name='username') 23 | .form-group 24 | input.form-control.input-lg(type='password' placeholder='#{__("user.password")}' name='password') 25 | .form-group 26 | button.btn.btn-success.btn-lg.btn-block #{__('sign-in')} 27 | span.pull-right 28 | a(href='/register') #{__('register')} 29 | 30 | br 31 | hr 32 | 33 | .row 34 | if AUTH_CONFIG.FACEBOOK.ENABLED 35 | .col-xs-12.col-md-6 36 | .form-group 37 | a.btn.btn-block.btn-social.btn-facebook(href='/auth/facebook') 38 | span.fa.fa-facebook 39 | | #{__('facebook.sign-in')} 40 | 41 | if AUTH_CONFIG.TWITTER.ENABLED 42 | .col-xs-12.col-md-6 43 | .form-group 44 | a.btn.btn-block.btn-social.btn-twitter(href='/auth/twitter') 45 | span.fa.fa-twitter 46 | | #{__('twitter.sign-in')} 47 | 48 | if AUTH_CONFIG.GOOGLE.ENABLED 49 | .col-xs-12.col-md-6 50 | .form-group 51 | a.btn.btn-block.btn-social.btn-google(href='/auth/google') 52 | span.fa.fa-google 53 | | #{__('google.sign-in')} 54 | 55 | if AUTH_CONFIG.GITHUB.ENABLED 56 | .col-xs-12.col-md-6 57 | .form-group 58 | a.btn.btn-block.btn-social.btn-github(href='/auth/github') 59 | span.fa.fa-github 60 | | #{__('github.sign-in')} 61 | 62 | if AUTH_CONFIG.LINKEDIN.ENABLED 63 | .col-xs-12.col-md-6 64 | .form-group 65 | a.btn.btn-block.btn-social.btn-linkedin(href='/auth/linkedin') 66 | span.fa.fa-linkedin 67 | | #{__('linkedin.sign-in')} 68 | 69 | if AUTH_CONFIG.INSTAGRAM.ENABLED 70 | .col-xs-12.col-md-6 71 | .form-group 72 | a.btn.btn-block.btn-social.btn-instagram(href='/auth/instagram') 73 | span.fa.fa-instagram 74 | | #{__('instagram.sign-in')} -------------------------------------------------------------------------------- /app/views/error.jade: -------------------------------------------------------------------------------- 1 | extends layouts/default 2 | 3 | block content 4 | .masthead.clearfix 5 | .inner 6 | h3.masthead-brand #{__('error')} 7 | nav 8 | ul.nav.masthead-nav 9 | li.active 10 | a(href='/') #{__('landing-page')} 11 | 12 | .inner.cover 13 | include includes/messages 14 | 15 | p.lead #{error.message} 16 | p #{error.stack} -------------------------------------------------------------------------------- /app/views/includes/facebook-meta.jade: -------------------------------------------------------------------------------- 1 | meta(property='og:url' content='//nodejs-web-jade-scaffold.herokuapp.com/') 2 | meta(property='og:type' content='article') 3 | meta(property='og:title' content='#{__("title")}') 4 | meta(property='og:description' content='#{__("main.cover-description")}') 5 | meta(property='og:image' content='http://nodejs-web-jade-scaffold.herokuapp.com/static/images/logo.png') -------------------------------------------------------------------------------- /app/views/includes/footer.jade: -------------------------------------------------------------------------------- 1 | p GitHub  2 | a(href='https://github.com/rodrigogs/nodejs-web-jade-scaffold') Repository 3 | | , by  4 | a(href='https://github.com/rodrigogs') rodrigogs 5 | | . -------------------------------------------------------------------------------- /app/views/includes/landing.jade: -------------------------------------------------------------------------------- 1 | h1.cover-heading #{__('main.cover-title')} 2 | p.lead #{__(message)} 3 | 4 | .inner.cover 5 | ul#out-of-the-box-demo 6 | li 7 | a(href='https://nodejs.org') 8 | img(src='https://nodejs.org/static/images/logos/nodejs-new-white-pantone.png') 9 | li 10 | a(href='http://jade-lang.com/') 11 | img(src='http://jade-lang.com/style/jade-logo-header.svg') 12 | li 13 | a(href='http://expressjs.com') 14 | img(src='https://i.cloudup.com/zfY6lL7eFa-3000x3000.png') 15 | li 16 | a(href='http://passportjs.org/') 17 | img(src='http://passportjs.org/images/logo.svg') 18 | li 19 | a(href='https://www.mongodb.org/') 20 | img(src='https://upload.wikimedia.org/wikipedia/en/thumb/4/45/MongoDB-Logo.svg/527px-MongoDB-Logo.svg.png') 21 | li 22 | a(href='http://getbootstrap.com/') 23 | img(src='https://upload.wikimedia.org/wikipedia/commons/e/ea/Boostrap_logo.svg') -------------------------------------------------------------------------------- /app/views/includes/messages.jade: -------------------------------------------------------------------------------- 1 | while _msg = flash.shift() 2 | - _msg.type = _msg.type === 'error' ? 'danger' : _msg.type; 3 | div(class='alert alert-#{_msg.type}' role='alert') 4 | p= __(_msg.message) -------------------------------------------------------------------------------- /app/views/includes/meta.jade: -------------------------------------------------------------------------------- 1 | meta(http-equiv='Content-Type' content='text/html; charset=utf-8') 2 | meta(http-equiv='cache-control' content='max-age=86400000') 3 | meta(http-equiv='X-UA-Compatible' content='IE=edge') 4 | meta(name='viewport' content='width=device-width, initial-scale=1') 5 | meta(name='robots' content='index, follow') 6 | meta(name='description' content='#{__("main.cover-description")}') 7 | meta(name='keywords' content='JavaScript,Node.js,Jade,Express,Passport,Scaffold,MongoDB,Bootstrap,kickstart,opensource') 8 | meta(name='author' content='Rodrigo Gomes da Silva') -------------------------------------------------------------------------------- /app/views/includes/twitter-cards.jade: -------------------------------------------------------------------------------- 1 | meta(name='twitter:card' content='summary') 2 | meta(name='twitter:site' content='@rodrigoagogo') 3 | meta(name='twitter:creator' content='@rodrigoagogo') 4 | meta(name='twitter:title' content='#{__("title")}') 5 | meta(name='twitter:description' content='#{__("main.cover-description")}') 6 | meta(name='twitter:image' content='http://nodejs-web-jade-scaffold.herokuapp.com/static/images/logo.png') -------------------------------------------------------------------------------- /app/views/includes/user-info.jade: -------------------------------------------------------------------------------- 1 | h1.cover-heading #{user.info.name} 2 | if user.info.image 3 | .lead 4 | img(src='#{user.info.image}') 5 | if user.info.description 6 | p.lead #{user.info.description} -------------------------------------------------------------------------------- /app/views/index.jade: -------------------------------------------------------------------------------- 1 | extends layouts/default 2 | 3 | block stylesheets 4 | 5 | block javascripts 6 | script(type='text/javascript'). 7 | $('#out-of-the-box-demo').slippry({ 8 | transition: 'fade', 9 | slippryWrapper: '
', 10 | captions: false, 11 | pager: false 12 | }); 13 | 14 | block content 15 | .masthead.clearfix 16 | .inner 17 | h3.masthead-brand #{__('landing-page')} 18 | if AUTH_CONFIG.ENABLED 19 | nav 20 | ul.nav.masthead-nav 21 | li.active 22 | if user 23 | a(href='/auth/logout') #{__('logout')} 24 | else 25 | a(href='/auth/login') #{__('login')} 26 | 27 | include includes/messages 28 | 29 | if user 30 | include includes/user-info 31 | else 32 | include includes/landing -------------------------------------------------------------------------------- /app/views/layouts/default.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang='en') 3 | head 4 | include ../includes/meta 5 | include ../includes/twitter-cards 6 | include ../includes/facebook-meta 7 | 8 | block meta 9 | 10 | title #{__('title')} 11 | body 12 | .site-wrapper 13 | .site-wrapper-inner 14 | .cover-container 15 | block content 16 | .mastfoot 17 | .inner 18 | include ../includes/footer 19 | 20 | link(rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css' integrity='sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7' crossorigin='anonymous') 21 | link(rel='stylesheet' href='/static/vendor/bootstrap-social/bootstrap-social.css') 22 | link(rel='stylesheet' href='/static/vendor/font-awesome/css/font-awesome.min.css') 23 | link(rel='stylesheet' href='/static/vendor/slippry/dist/slippry.css') 24 | link(rel='stylesheet' href='/static/stylesheets/cover.css') 25 | link(rel='stylesheet' href='/static/stylesheets/style.css') 26 | 27 | block stylesheets 28 | 29 | script(src='/static/javascript/application.js') 30 | script(src='https://code.jquery.com/jquery-2.1.4.min.js') 31 | script(src='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js' integrity='sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS' crossorigin='anonymous') 32 | script(src='/static/vendor/slippry/dist/slippry.min.js') 33 | 34 | block javascripts -------------------------------------------------------------------------------- /app/views/register/index.jade: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block stylesheets 4 | 5 | block javascripts 6 | 7 | block content 8 | .masthead.clearfix 9 | .inner 10 | h3.masthead-brand #{__('register')} 11 | nav 12 | ul.nav.masthead-nav 13 | li.active 14 | a(href='/auth/login') #{__('login')} 15 | 16 | .inner.cover 17 | include ../includes/messages 18 | 19 | - user = user || {}; 20 | form.form.col-md-12.center-block(method='post') 21 | .form-group 22 | input.form-control.input-lg(type='text' placeholder='#{__("user.name")}' name='name' required value='#{user.name || ""}') 23 | .form-group 24 | input.form-control.input-lg(type='text' placeholder='#{__("user.last_name")}' name='last_name' required value='#{user.last_name || ""}') 25 | .form-group 26 | input.form-control.input-lg(type='email' placeholder='#{__("user.email")}' name='email' required value='#{user.email || ""}') 27 | .form-group 28 | input.form-control.input-lg(type='text' placeholder='#{__("user.user_name")}' name='user_name' required value='#{user.user_name || ""}') 29 | .form-group 30 | input.form-control.input-lg(type='password' placeholder='#{__("user.password")}' name='password' required value='#{user.password || ""}') 31 | .form-group 32 | button.btn.btn-success.btn-lg.btn-block #{__('register')} -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | /** 5 | * Module dependencies. 6 | */ 7 | const app = require('../main'); 8 | const http = require('http'); 9 | 10 | /** 11 | * Normalize a port into a number, string, or false. 12 | */ 13 | const normalizePort = val => { 14 | const port = parseInt(val, 10); 15 | 16 | if (isNaN(port)) { 17 | // named pipe 18 | return val; 19 | } 20 | 21 | if (port >= 0) { 22 | // port number 23 | return port; 24 | } 25 | 26 | return false; 27 | }; 28 | 29 | /** 30 | * Event listener for HTTP server "error" event. 31 | */ 32 | const onError = error => { 33 | if (error.syscall !== 'listen') { 34 | throw error; 35 | } 36 | 37 | const bind = typeof port === 'string' 38 | ? `Pipe ${port}` 39 | : `Port ${port}`; 40 | 41 | // handle specific listen errors with friendly messages 42 | switch (error.code) { 43 | case 'EACCES': 44 | console.error(`${bind} requires elevated privileges`); 45 | process.exit(1); 46 | break; 47 | case 'EADDRINUSE': 48 | console.error(`${bind} is already in use`); 49 | process.exit(1); 50 | break; 51 | default: 52 | throw error; 53 | } 54 | }; 55 | 56 | /** 57 | * Event listener for HTTP server "listening" event. 58 | */ 59 | const onListening = () => { 60 | const addr = server.address(); 61 | console.log(`Listening at ${addr.address}:${addr.port}`); 62 | }; 63 | 64 | /** 65 | * Get port from environment and store in Express. 66 | */ 67 | const port = normalizePort(process.env.PORT || '3000'); 68 | app.set('port', port); 69 | app.set('address', process.env.IP || 'localhost'); 70 | 71 | /** 72 | * Create HTTP server. 73 | */ 74 | let server = http.createServer(app); 75 | 76 | /** 77 | * Listen on provided port, on all network interfaces. 78 | */ 79 | if (process.env.IP) { 80 | server.listen(app.get('port'), app.get('address')); 81 | } else { 82 | server.listen(app.get('port')); 83 | } 84 | 85 | server.on('error', onError); 86 | server.on('listening', onListening); -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-web-jade-scaffold", 3 | "description": "", 4 | "main": "main.js", 5 | "authors": [ 6 | "Rodrigo Gomes da Silva " 7 | ], 8 | "license": "BSD-2-Clause", 9 | "keywords": [ 10 | "web", 11 | "jade", 12 | "scaffold", 13 | "express", 14 | "kickstart", 15 | "simple" 16 | ], 17 | "moduleType": [], 18 | "homepage": "", 19 | "ignore": [ 20 | "**/.*", 21 | "node_modules", 22 | "bower_components", 23 | "test", 24 | "tests" 25 | ], 26 | "dependencies": { 27 | "bootstrap-social": "~5.0.0", 28 | "slippry": "^1.3.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "DATABASE": { 3 | "RECONNECT": true, 4 | "RECONNECTION_INTERVAL": 5000 5 | }, 6 | "HTTP_LOG_CONFIG": "dev", 7 | "LOCALES": [ 8 | "en", 9 | "pt", 10 | "pt-br" 11 | ], 12 | "AUTH": { 13 | "ENABLED": true, 14 | "LOCAL": { 15 | "ENABLED": true 16 | }, 17 | "FACEBOOK": { 18 | "ENABLED": true, 19 | "PROFILE_FIELDS": [ 20 | "id", 21 | "first_name", 22 | "last_name", 23 | "email", 24 | "picture", 25 | "about", 26 | "bio" 27 | ], 28 | "OPTIONS": { 29 | "display": "touch", 30 | "scope": [ 31 | "public_profile", 32 | "user_about_me", 33 | "email" 34 | ], 35 | "authorizationURL": "https://www.facebook.com/v2.5/dialog/oauth", 36 | "profileURL": "https://graph.facebook.com/v2.5/me" 37 | } 38 | }, 39 | "TWITTER": { 40 | "ENABLED": true 41 | }, 42 | "GOOGLE": { 43 | "ENABLED": true, 44 | "OPTIONS": { 45 | "scope": [ 46 | "https://www.googleapis.com/auth/userinfo.profile", 47 | "https://www.googleapis.com/auth/userinfo.email" 48 | ] 49 | } 50 | }, 51 | "GITHUB": { 52 | "ENABLED": true 53 | }, 54 | "LINKEDIN": { 55 | "ENABLED": true, 56 | "OPTIONS": { 57 | "state": "SOME STATE", 58 | "scope": [ 59 | "r_emailaddress", 60 | "r_basicprofile" 61 | ] 62 | } 63 | }, 64 | "INSTAGRAM": { 65 | "ENABLED": true 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /config/mongoose.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const CONFIG = require('./config.json'); 4 | const mongoose = require('mongoose'); 5 | 6 | const CONN_STR = process.env.DATABASE_URL; 7 | 8 | mongoose.connect(CONN_STR); 9 | 10 | mongoose.connection.on('connected', () => { 11 | console.log(`Mongoose default connection open to ${CONN_STR}`); 12 | }); 13 | 14 | mongoose.connection.on('error', err => { 15 | console.log(`Mongoose default connection error: ${err}`); 16 | if (CONFIG.DATABASE.RECONNECT) { 17 | return; 18 | } 19 | process.exit(1); 20 | }); 21 | 22 | mongoose.connection.on('disconnected', () => { 23 | console.log('Mongoose default connection disconnected'); 24 | if (CONFIG.DATABASE.RECONNECT) { 25 | reconnect(); 26 | } 27 | }); 28 | 29 | mongoose.connection.once('open', () => { 30 | console.log('Mongoose default connection is open'); 31 | }); 32 | 33 | process.on('SIGINT', () => { 34 | mongoose.connection.close(() => { 35 | console.log('Mongoose default connection disconnected through app termination'); 36 | process.exit(0); 37 | }); 38 | }); 39 | 40 | const reconnect = () => { 41 | const timeout = CONFIG.DATABASE.RECONNECTION_INTERVAL || 5000; 42 | console.log(`Retrying to connect to database in ${timeout / 1000} seconds`); 43 | 44 | setTimeout(() => { 45 | mongoose.connect(CONN_STR); 46 | }, timeout); 47 | }; -------------------------------------------------------------------------------- /config/passport.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const CONFIG = require('./config.json'); 4 | const passport = require('passport'); 5 | const LocalStrategy = require('passport-local').Strategy; 6 | const FacebookStrategy = require('passport-facebook').Strategy; 7 | const TwitterStrategy = require('passport-twitter').Strategy; 8 | const GoogleStrategy = require('passport-google-oauth').OAuth2Strategy; 9 | const GitHubStrategy = require('passport-github').Strategy; 10 | const LinkedInStrategy = require('passport-linkedin-oauth2').Strategy; 11 | const InstagramStrategy = require('passport-instagram').Strategy; 12 | const AuthService = require('../app/services/auth'); 13 | const UserSchema = require('../app/models/user'); 14 | const CryptUtils = require('../app/utils/crypt'); 15 | 16 | const _getThirdPartyInfo = profile => { 17 | if (profile.provider === 'facebook') { 18 | return { 19 | name: `${profile._json.first_name} ${profile._json.last_name}`, 20 | description: profile._json.about || profile._json.bio, 21 | image: profile._json.picture.data.url 22 | }; 23 | } 24 | if (profile.provider === 'twitter') { 25 | return { 26 | name: profile.displayName, 27 | description: profile._json.description, 28 | image: profile._json.profile_image_url 29 | }; 30 | } 31 | if (profile.provider === 'google') { 32 | const currentOrg = profile._json 33 | .organizations.find((org) => { return org.primary === true }) || {}; 34 | 35 | const currentPlace = profile 36 | ._json.placesLived.find((pla) => { return pla.primary === true }) || {}; 37 | 38 | return { 39 | name: profile.displayName, 40 | description: (currentOrg ? `${currentOrg.title} - ${currentOrg.name}` : currentPlace.value) || '', 41 | image: profile.photos[0].value 42 | }; 43 | } 44 | if (profile.provider === 'github') { 45 | return { 46 | name: profile.displayName, 47 | description: ((profile._json.company || profile._json.location) || progile._json.blog) || profile._json.email, 48 | image: profile.photos[0].value 49 | }; 50 | } 51 | if (profile.provider === 'linkedin') { 52 | return { 53 | name: profile.displayName, 54 | description: profile._json.headline, 55 | image: profile.photos[0].value 56 | } 57 | } 58 | if (profile.provider === 'instagram') { 59 | return { 60 | name: profile.displayName, 61 | description: profile._json.data.bio, 62 | image: profile._json.data.profile_picture 63 | } 64 | } 65 | }; 66 | 67 | /*--------------- Serialization ---------------*/ 68 | 69 | passport.serializeUser((user, done) => { 70 | done(null, {id: user._id, info: user.info}); 71 | }); 72 | 73 | passport.deserializeUser((user, done) => { 74 | const info = user.info || {}; 75 | UserSchema.findById(user.id, (err, user) => { 76 | if (err) { 77 | return done(err); 78 | } 79 | user = user.toObject(); 80 | user.info = info; 81 | done(err, user); 82 | }); 83 | }); 84 | 85 | /*----------------- Strategies -----------------*/ 86 | 87 | if (CONFIG.AUTH.LOCAL.ENABLED) { 88 | passport.use(new LocalStrategy((username, password, done) => { 89 | AuthService.resolveUser({ 90 | user_name: username, 91 | password: CryptUtils.encrypt(password), 92 | isLocal: true 93 | }, (err, user, info) => { 94 | if (err) { 95 | return done(err); 96 | } 97 | if (user) { 98 | user.info = { name: `${user.name} ${user.last_name}`}; 99 | } 100 | done(null, user, info); 101 | }); 102 | })); 103 | } 104 | 105 | if (CONFIG.AUTH.FACEBOOK.ENABLED) { 106 | passport.use(new FacebookStrategy({ 107 | clientID: process.env.FACEBOOK_APP_ID, 108 | clientSecret: process.env.FACEBOOK_APP_SECRET, 109 | callbackURL: '/auth/facebook/callback', 110 | profileFields: CONFIG.AUTH.FACEBOOK.PROFILE_FIELDS 111 | }, (accessToken, refreshToken, profile, done) => { 112 | AuthService.resolveUser({ 113 | facebook_id: profile.id, 114 | email: profile._json.email, 115 | name: profile._json.first_name, 116 | last_name: profile._json.last_name 117 | }, (err, user, info) => { 118 | if (err) { 119 | return done(err); 120 | } 121 | if (user) { 122 | user.info = _getThirdPartyInfo(profile); 123 | } 124 | done(null, user, info); 125 | }); 126 | })); 127 | } 128 | 129 | if (CONFIG.AUTH.TWITTER.ENABLED) { 130 | passport.use(new TwitterStrategy({ 131 | consumerKey: process.env.TWITTER_CONSUMER_KEY, 132 | consumerSecret: process.env.TWITTER_CONSUMER_SECRET, 133 | callbackURL: "/auth/twitter/callback", 134 | includeEmail: true 135 | }, (accessToken, refreshToken, profile, done) => { 136 | AuthService.resolveUser({ 137 | twitter_id: profile.id, 138 | name: profile.displayName.split(' ')[0], 139 | last_name: profile.displayName.split(' ').slice(1).join(' '), 140 | email: `${profile.id}@twitter.com` // Until there is no way to retrieve email from twitter API 141 | }, (err, user, info) => { 142 | if (err) { 143 | return done(err); 144 | } 145 | if (user) { 146 | user.info = _getThirdPartyInfo(profile); 147 | } 148 | done(null, user, info); 149 | }); 150 | })); 151 | } 152 | 153 | if (CONFIG.AUTH.GOOGLE.ENABLED) { 154 | passport.use(new GoogleStrategy({ 155 | clientID: process.env.GOOGLE_CLIENT_ID, 156 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 157 | callbackURL: '/auth/google/callback' 158 | }, (accessToken, refreshToken, profile, done) => { 159 | // Dont forget to enable Google+ API in Developer Console in order to get user's information 160 | AuthService.resolveUser({ 161 | google_id: profile.id, 162 | name: profile.name.givenName, 163 | last_name: profile.name.familyName, 164 | email: profile.emails[0].value 165 | }, (err, user, info) => { 166 | if (err) { 167 | return done(err); 168 | } 169 | if (user) { 170 | user.info = _getThirdPartyInfo(profile); 171 | } 172 | done(null, user, info); 173 | }); 174 | })); 175 | } 176 | 177 | if (CONFIG.AUTH.GITHUB.ENABLED) { 178 | passport.use(new GitHubStrategy({ 179 | clientID: process.env.GITHUB_CLIENT_ID, 180 | clientSecret: process.env.GITHUB_CLIENT_SECRET, 181 | callbackURL: '/auth/github/callback' 182 | }, (accessToken, refreshToken, profile, done) => { 183 | AuthService.resolveUser({ 184 | github_id: profile.id, 185 | name: profile.displayName.split(' ')[0], 186 | last_name: profile.displayName.split(' ').slice(1).join(' '), 187 | email: profile.emails[0].value 188 | }, (err, user, info) => { 189 | if (err) { 190 | return done(err); 191 | } 192 | if (user) { 193 | user.info = _getThirdPartyInfo(profile); 194 | } 195 | done(null, user, info); 196 | }); 197 | })); 198 | } 199 | 200 | if (CONFIG.AUTH.LINKEDIN.ENABLED) { 201 | passport.use(new LinkedInStrategy({ 202 | clientID: process.env.LINKEDIN_KEY, 203 | clientSecret: process.env.LINKEDIN_SECRET, 204 | callbackURL: '/auth/linkedin/callback', 205 | scope: CONFIG.AUTH.LINKEDIN.OPTIONS.scope 206 | }, (accessToken, refreshToken, profile, done) => { 207 | AuthService.resolveUser({ 208 | linkedin_id: profile.id, 209 | name: profile.name.givenName, 210 | last_name: profile.name.familyName, 211 | email: profile.emails[0].value 212 | }, (err, user, info) => { 213 | if (err) { 214 | return done(err); 215 | } 216 | if (user) { 217 | user.info = _getThirdPartyInfo(profile); 218 | } 219 | done(null, user, info); 220 | }); 221 | })); 222 | } 223 | 224 | if (CONFIG.AUTH.INSTAGRAM.ENABLED) { 225 | passport.use(new InstagramStrategy({ 226 | clientID: process.env.INSTAGRAM_CLIENT_ID, 227 | clientSecret: process.env.INSTAGRAM_CLIENT_SECRET, 228 | callbackURL: '/auth/instagram/callback' 229 | }, (accessToken, refreshToken, profile, done) => { 230 | AuthService.resolveUser({ 231 | instagram_id: profile.id, 232 | name: profile.displayName.split(' ')[0], 233 | last_name: profile.displayName.split(' ').slice(1).join(' '), 234 | email: `${profile.username}@instagram.com` 235 | }, (err, user, info) => { 236 | if (err) { 237 | return done(err); 238 | } 239 | if (user) { 240 | user.info = _getThirdPartyInfo(profile); 241 | } 242 | done(null, user, info); 243 | }); 244 | })); 245 | } -------------------------------------------------------------------------------- /config/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const CONFIG = require('./config.json'); 4 | const router = require('express').Router(); 5 | const logger = require('winston'); 6 | 7 | module.exports = () => { 8 | 9 | router.use((req, res, next) => { 10 | // Remove express http headers 11 | res.removeHeader('X-Powered-By'); 12 | res.locals.user = req.user; 13 | next(); 14 | }); 15 | 16 | /*------------------- Routes -------------------*/ 17 | 18 | const auth = require('../app/routes/auth'); 19 | const register = require('../app/routes/register'); 20 | const index = require('../app/routes/index'); 21 | 22 | if (CONFIG.AUTH.ENABLED) { 23 | router.use(auth); 24 | router.use(register); 25 | } 26 | 27 | router.use(index); 28 | 29 | /*----------------- Routes API -----------------*/ 30 | 31 | // const user = require('../app/routes/api/user'); 32 | 33 | // router.use('/api/user', user); 34 | 35 | /*--------------- Error Handler ----------------*/ 36 | 37 | router.use((req, res, next) => { 38 | let err = new Error('Not Found'); 39 | err.statusCode = 404; 40 | next(err); 41 | }); 42 | 43 | router.use((err, req, res, next) => { 44 | logger.error(err); 45 | 46 | const isApiRequest = req.originalUrl.substring(0, 4) === '/api'; 47 | 48 | /** 49 | * Remove Error's `stack` property. We don't want 50 | * users to see this at the production env 51 | */ 52 | if (req.app.get('env') !== 'development') { 53 | delete err.stack; 54 | } 55 | 56 | res.status(err.statusCode || 500); 57 | 58 | if (isApiRequest) { 59 | res.send({error: err}); 60 | } else { 61 | res.render('error', {error: err}); 62 | } 63 | }); 64 | 65 | return router; 66 | }; 67 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrigogs/nodejs-web-jade-scaffold/b2d87b790a02baaa82f7b8e7317ce854a97a72bd/favicon.ico -------------------------------------------------------------------------------- /locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Node.js Web Jade Scaffold", 3 | "main.cover-title": "Node.js Web Jade Scaffold", 4 | "main.cover-description": "Web application featuring Node.js, Express, Jade, Passport, MongoDB and Bootstrap", 5 | "landing-page": "Landing Page", 6 | "login": "Login", 7 | "logout": "Logout", 8 | "sign-in": "Sign In", 9 | "facebook.sign-in": "Sign In with Facebook", 10 | "twitter.sign-in": "Sign In with Twitter", 11 | "google.sign-in": "Sign In with Google", 12 | "github.sign-in": "Sign In with GitHub", 13 | "linkedin.sign-in": "Sign In with Linkedin", 14 | "instagram.sign-in": "Sign In with Instagram", 15 | "register": "Register", 16 | "error": "Error", 17 | "auth.failed": "Invalid username or password", 18 | "user.name": "Name", 19 | "user.user_name": "Username", 20 | "user.last_name": "Last name", 21 | "user.email": "Email", 22 | "user.password": "Password", 23 | "user.created": "User created", 24 | "user.exists": "User exists", 25 | "user.invalidemail": "Invalid email", 26 | "user.error-creating": "Error creating user", 27 | "passport.badrequest": "Missing credentials", 28 | "mongoose.unique-error": "Field %s is unique, value %s is already taken" 29 | } -------------------------------------------------------------------------------- /locales/pt-br.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Node.js Web Jade Scaffold", 3 | "main.cover-title": "Node.js Web Jade Scaffold", 4 | "main.cover-description": "Aplicação web implementando Node.js, Express, Jade, Passport, MongoDB e Bootstrap", 5 | "landing-page": "Página Inicial", 6 | "login": "Fazer Login", 7 | "logout": "Sair", 8 | "sign-in": "Entrar", 9 | "facebook.sign-in": "Entrar com o Facebook", 10 | "twitter.sign-in": "Entrar com o Twitter", 11 | "google.sign-in": "Entrar com o Google", 12 | "github.sign-in": "Entrar com o GitHub", 13 | "linkedin.sign-in": "Entrar com o Linkedin", 14 | "instagram.sign-in": "Entrar com o Instagram", 15 | "register": "Registrar", 16 | "error": "Erro", 17 | "auth.failed": "Nome de usuário ou senha inválido", 18 | "user.name": "Nome", 19 | "user.user_name": "Nome de usuário", 20 | "user.last_name": "Sobrenome", 21 | "user.email": "Email", 22 | "user.password": "Senha", 23 | "user.created": "Usuário criado", 24 | "user.exists": "Usuário já cadastrado", 25 | "user.invalidemail": "Email inválido", 26 | "user.error-creating": "Erro criando usuário", 27 | "passport.badrequest": "Credenciais não infomadas", 28 | "mongoose.unique-error": "O campo %s é único, o valor %s já existe" 29 | } -------------------------------------------------------------------------------- /locales/pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Node.js Web Jade Scaffold", 3 | "main.cover-title": "Node.js Web Jade Scaffold", 4 | "main.cover-description": "Aplicação web implementando Node.js, Express, Jade, Passport, MongoDB e Bootstrap", 5 | "landing-page": "Página Inicial", 6 | "login": "Fazer Login", 7 | "logout": "Sair", 8 | "sign-in": "Entrar", 9 | "facebook.sign-in": "Entrar com o Facebook", 10 | "twitter.sign-in": "Entrar com o Twitter", 11 | "google.sign-in": "Entrar com o Google", 12 | "github.sign-in": "Entrar com o GitHub", 13 | "linkedin.sign-in": "Entrar com o Linkedin", 14 | "instagram.sign-in": "Entrar com o Instagram", 15 | "register": "Registrar", 16 | "error": "Erro", 17 | "auth.failed": "Nome de usuário ou senha inválido", 18 | "user.name": "Nome", 19 | "user.user_name": "Nome de usuário", 20 | "user.last_name": "Sobrenome", 21 | "user.email": "Email", 22 | "user.password": "Senha", 23 | "user.created": "Usuário criado", 24 | "user.exists": "Usuário já cadastrado", 25 | "user.invalidemail": "Email inválido", 26 | "user.error-creating": "Erro criando usuário", 27 | "passport.badrequest": "Credenciais não infomadas", 28 | "mongoose.unique-error": "O campo %s é único, o valor %s já existe" 29 | } -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const express = require('express'); 5 | const helmet = require('helmet'); 6 | const logger = require('morgan'); 7 | const bodyParser = require('body-parser'); 8 | const favicon = require('serve-favicon'); 9 | const flash = require('flash'); 10 | const session = require('express-session'); 11 | const methodOverride = require('method-override'); 12 | const passport = require('passport'); 13 | const i18n = require('i18n'); 14 | const cookieParser = require('cookie-parser'); 15 | const robots = require('express-robots'); 16 | const compression = require('compression') 17 | 18 | const CONFIG = require('./config/config.json'); 19 | const routes = require('./config/routes')(); 20 | 21 | const app = express(); 22 | 23 | app.locals.AUTH_CONFIG = CONFIG.AUTH; 24 | 25 | app.use(helmet()); 26 | app.use(logger(CONFIG.HTTP_LOG_CONFIG)); 27 | app.use(cookieParser()); 28 | app.use(bodyParser.json()); 29 | app.use(bodyParser.urlencoded({extended: false})); 30 | app.use(session({secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: true})); 31 | app.use(flash()); 32 | app.use(methodOverride('_method')); 33 | app.use(passport.initialize()); 34 | app.use(passport.session()); 35 | 36 | // i18n config: https://github.com/mashpie/i18n-node#list-of-configuration-options 37 | i18n.configure({ 38 | directory: path.join(__dirname, 'locales'), 39 | locales: CONFIG.LOCALES, 40 | updateFiles: false 41 | }); 42 | 43 | app.use(i18n.init); 44 | 45 | // Robots config: https://www.npmjs.com/package/express-robots 46 | app.use(robots({UserAgent: '*', Disallow: ''})); 47 | 48 | // Compression config: https://www.npmjs.com/package/compression 49 | app.use(compression()); 50 | 51 | // Static resources 52 | app.use(favicon(path.join(__dirname, 'favicon.ico'))); 53 | app.use('/static', express.static(path.join(__dirname, 'app/public'))); 54 | 55 | // View engine setup 56 | app.set('views', path.join(__dirname, 'app/views')); 57 | app.set('view engine', 'jade'); 58 | 59 | // Config 60 | app.use(routes); 61 | require('./config/mongoose'); 62 | require('./config/passport'); 63 | 64 | module.exports = app; 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-web-jade-scaffold", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "main.js", 6 | "scripts": { 7 | "start": "node node_modules/nodemon/bin/nodemon.js ./bin/www", 8 | "test": "ava tests.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/rodrigogs/nodejs-web-jade-scaffold" 13 | }, 14 | "keywords": [ 15 | "web", 16 | "jade", 17 | "scaffold", 18 | "express", 19 | "kickstart", 20 | "simple" 21 | ], 22 | "author": { 23 | "name": "Rodrigo Gomes da Silva", 24 | "email": "rodrigo.smscom@gmail.com", 25 | "web": "https://github.com/rodrigogs" 26 | }, 27 | "license": "BSD-2-Clause", 28 | "engines": { 29 | "node": ">=4.2.3" 30 | }, 31 | "dependencies": { 32 | "body-parser": "^1.15.0", 33 | "compression": "^1.6.1", 34 | "cookie-parser": "^1.4.1", 35 | "express": "^4.13.4", 36 | "express-robots": "^0.1.5", 37 | "express-session": "^1.13.0", 38 | "filter-object": "^2.1.0", 39 | "flash": "^1.1.0", 40 | "helmet": "^2.1.1", 41 | "i18n": "^0.8.0", 42 | "jade": "^1.11.0", 43 | "method-override": "^2.3.5", 44 | "mongoose": "^4.4.7", 45 | "mongoose-unique-validator": "^1.0.2", 46 | "morgan": "^1.7.0", 47 | "nodemon": "^1.9.1", 48 | "passport": "^0.3.2", 49 | "passport-facebook": "^2.1.0", 50 | "passport-github": "^1.1.0", 51 | "passport-google-oauth": "^1.0.0", 52 | "passport-instagram": "^1.0.0", 53 | "passport-linkedin-oauth2": "^1.4.0", 54 | "passport-local": "^1.0.0", 55 | "passport-twitter": "^1.0.4", 56 | "serve-favicon": "^2.3.0", 57 | "validator": "^5.1.0", 58 | "winston": "^2.2.0" 59 | }, 60 | "devDependencies": { 61 | "ava": "^0.16.0", 62 | "request": "^2.69.0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import test from 'ava'; 4 | import http from 'http'; 5 | import request from 'request'; 6 | import app from './main'; 7 | 8 | const baseUrl = 'http://127.0.0.1:3000'; 9 | let server; 10 | 11 | test.before(() => { 12 | server = http.createServer(app).listen(3000, '127.0.0.1'); 13 | }); 14 | 15 | test.after(() => { 16 | server.close(); 17 | }); 18 | 19 | const getMethod = (t, url, params) => { 20 | t.plan(3); 21 | 22 | request.get({uri: url, qs: params}, (err, response, body) => { 23 | t.notOk(err, `Request error for url ${url}`); 24 | t.true(response && (response.statusCode >= 200 && response.statusCode < 399), `Url ${url} failed with status ${response ? response.statusCode : ''}`); 25 | t.ok(body, `Body is empty`); 26 | t.end(); 27 | }); 28 | }; 29 | 30 | const postMethod = (t, url, params) => { 31 | t.plan(3); 32 | 33 | request.post(url, {form: params}, (err, response, body) => { 34 | t.notOk(err, `Request error for url ${url}`); 35 | t.true(response && (response.statusCode >= 200 && response.statusCode < 399), `Url ${url} failed with status ${response ? response.statusCode : ''}`); 36 | t.ok(body, `Body is empty`); 37 | t.end(); 38 | }); 39 | }; 40 | 41 | 42 | test.cb('test / url', t => { 43 | const url = `${baseUrl}/`; 44 | getMethod(t, url); 45 | }); 46 | 47 | test.cb('test /login url', t => { 48 | const url = `${baseUrl}/auth/login`; 49 | getMethod(t, url); 50 | }); 51 | 52 | test.cb('test /logout url', t => { 53 | const url = `${baseUrl}/auth/logout`; 54 | getMethod(t, url); 55 | }); 56 | 57 | test.cb('test /logout url', t => { 58 | const url = `${baseUrl}/register`; 59 | getMethod(t, url); 60 | }); 61 | 62 | test.cb('test /logout url', t => { 63 | const url = `${baseUrl}/register`; 64 | let now = new Date(); 65 | postMethod(t, url, {name: 'test', last_name: 'last name', user_name: 'test' + now.toString(), password: '123', email: now.getTime() + '@test.com'}); 66 | }); 67 | --------------------------------------------------------------------------------