├── .editorconfig ├── .eslint.server.js ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── app.js ├── package.json ├── screenshot-user-admin.png ├── server ├── api │ └── controllers │ │ ├── messageController.js │ │ └── userController.js ├── config │ └── index.js ├── main │ ├── common │ │ ├── auth-header.js │ │ └── utils.js │ └── controllers │ │ └── authController.js ├── middleware │ └── auth-check.js ├── models │ ├── index.js │ └── user.js ├── passport │ ├── local-login.js │ └── local-signup.js ├── routes │ ├── api.js │ └── auth.js └── views │ ├── error.pug │ └── layout.pug ├── src ├── favicon.ico ├── img │ └── ico │ │ ├── apple-touch-icon.png │ │ ├── chrome-touch-icon-192x192.png │ │ ├── icon-128x128.png │ │ └── mstile-150x150.png ├── index.html ├── jsx │ ├── common │ │ ├── imports.js │ │ ├── main-routes.jsx │ │ ├── navbar.jsx │ │ ├── private-routes.jsx │ │ └── utils.js │ ├── components │ │ ├── form-submit-errors.jsx │ │ ├── form-validation-errors.jsx │ │ └── password-change-form.jsx │ ├── config │ │ └── index.js │ ├── index.jsx │ ├── modules │ │ └── auth.jsx │ ├── pages │ │ ├── home.jsx │ │ ├── not-found.jsx │ │ ├── private │ │ │ ├── admin1.jsx │ │ │ ├── private1.jsx │ │ │ ├── profile-password.jsx │ │ │ ├── profile.jsx │ │ │ └── site-admin │ │ │ │ ├── user-delete.jsx │ │ │ │ ├── user-edit.jsx │ │ │ │ ├── user-password.jsx │ │ │ │ └── users.jsx │ │ ├── public1.jsx │ │ ├── signin.jsx │ │ └── signup.jsx │ └── services │ │ ├── message-service.js │ │ ├── request.js │ │ └── user-service.js ├── manifest.json ├── manifest.webapp ├── robots.txt ├── scss │ ├── _mixins.scss │ ├── bootstrap-overrides.scss │ ├── margin-padding.scss │ ├── site-branding.scss │ └── style.scss └── shared │ ├── model-validations.js │ └── roles.js ├── webpack.common.config.js ├── webpack.config.js └── webpack.prod.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.eslint.server.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'espree', 3 | rules: { 4 | strict: 0 5 | }, 6 | env: { 7 | browser: false, 8 | commonjs: true, 9 | es6: true 10 | }, 11 | rules: { 12 | 'comma-dangle': ['error', 'never'], 13 | indent: ['error', 2, {'SwitchCase': 1}], 14 | 'linebreak-style': ['error', 'unix'], 15 | quotes: ['error', 'single'], 16 | semi: ['error', 'always'], 17 | 'no-trailing-spaces': ['error'], 18 | 'spaced-comment': ['error', 'always'], 19 | 'no-multiple-empty-lines': ['error', { 'max': 2, 'maxEOF': 1 }] 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'babel-eslint', 3 | rules: { 4 | strict: 0 5 | }, 6 | env: { 7 | browser: true, 8 | commonjs: true, 9 | es6: true, 10 | jquery: true, 11 | node: true, 12 | }, 13 | extends: 'airbnb', 14 | parserOptions: { 15 | sourceType: 'module', 16 | }, 17 | plugins: ['react'], 18 | settings: { 19 | 'import/resolver': { 20 | node: { 21 | extensions: ['.js','.jsx'] 22 | } 23 | } 24 | }, 25 | rules: { 26 | 'comma-dangle': ['error', 'never'], 27 | indent: ['error', 2, {'SwitchCase': 1}], 28 | 'linebreak-style': ['error', 'unix'], 29 | quotes: ['error', 'single'], 30 | semi: ['error', 'always'], 31 | 'no-unused-vars': ['warn'], 32 | 'no-console': 0, 33 | 'prefer-template': ['off'], 34 | 'space-infix-ops': ['off'], 35 | 'no-trailing-spaces': ['error'], 36 | 37 | // curly: ['error', 'multi', 'consistent'], 38 | 'brace-style': 2, 39 | 40 | // Do not require default for switch-case 41 | 'default-case': ['off'], 42 | 43 | //'import/extensions': ['.jsx'], 44 | 'arrow-body-style': ['error', 'always'], 45 | 46 | // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/prefer-stateless-function.md 47 | 'react/prefer-stateless-function': [0, { 'ignorePureComponents': true }], 48 | 'react/jsx-filename-extension': [1, { 'extensions': ['.jsx'] }], 49 | 'react/jsx-closing-bracket-location': [1, 'after-props'], 50 | 51 | 'func-names': ['error', 'never'], 52 | 53 | 'padded-blocks': ['off'], 54 | 55 | // Advanced Rules 56 | 'no-unused-expressions': 'warn', 57 | 'no-useless-concat': 'warn', 58 | 'block-scoped-var': 'error', 59 | 'consistent-return': 'error', 60 | 61 | //'object-shorthand': ['error', 'consistent'], 62 | 63 | // Definition for rule 'jsx-a11y/href-no-hash' was not found 64 | // https://github.com/facebookincubator/create-react-app/issues/2631 65 | 'jsx-a11y/href-no-hash': 'off', 66 | //'jsx-a11y/anchor-is-valid': ['warn', { 'aspects': ['invalidHref'] }] 67 | 68 | 'jsx-a11y/label-has-for': [2, { 69 | 'components': ['Label'], 70 | 'required': { 71 | 'every': ['id'] 72 | }, 73 | 'allowChildren': false, 74 | }], 75 | 76 | 77 | // Turn off: 'Absolute imports should come before relative ignore' 78 | // Turned off by default w/ esling, airbnb turns it back on 79 | // https://github.com/airbnb/javascript/blob/master/packages/eslint-config-airbnb-base/rules/imports.js#L112 80 | // disallow non-import statements appearing before import statements 81 | // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/first.md 82 | 'import/first': ['off'], 83 | 84 | 'import/newline-after-import': ['off'], 85 | 'space-before-function-paren': ['off'], 86 | 87 | 'import/no-extraneous-dependencies': ['error', { devDependencies: true }] 88 | } 89 | }; 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .eslintcache 4 | *.log 5 | .DS_Store 6 | .idea 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Mark Timmermann 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 | # Boilerplate web app: Role Based Permissions, Passport local authentication, React, Node.js, Bootstrap, Webpack 2 | 3 | Website boilterplate project setup with local passport authentication strategy and role based permissions. 4 | 5 | Api routes are authorized with an encrypted JsonWebToken, each api route can be assigned allowable user roles for permissions. 6 | 7 | ##### Server 8 | * Node.js 9 | * Express 4 10 | * Passport local authentication strategy 11 | * JsonWebToken 12 | * Authorized token required for api routes, using middelware authorization check 13 | * MongoDB/Mongoose 14 | 15 | ##### Client JS 16 | * Webpack 3 17 | * ESLint 18 | * React 15.6 19 | * React Router 4 (react-router-dom) 20 | * Private and Public react routes 21 | * ES6 22 | 23 | #### Client Styles 24 | * Bootstrap 25 | * Font Awesome 26 | * Sass 27 | 28 | ---- 29 | 30 | #### Configuration 31 | 32 | ##### Client 33 | * Token expiration setting 34 | * src/jsx/config/index 35 | 36 | ```js 37 | /** 38 | * Set the token expire age in milliseconds. 39 | */ 40 | session: { 41 | maxAge: 2 * 60 * 60 * 1000 // 2 hours 42 | } 43 | ``` 44 | 45 | ##### Shared Client\Server 46 | * Model validations 47 | * src/shared/model-validations.js 48 | 49 | ```js 50 | password: { 51 | minLength: { 52 | value: 8, 53 | message: 'Password must be a mininum of 8 characters' 54 | } 55 | }, 56 | email: { 57 | regex: { 58 | value: /^([\w.%+-]+)@([\w-]+\.)+([\w]{2,})$/i, 59 | message: 'Email is not valid.' 60 | } 61 | } 62 | ``` 63 | * Roles 64 | * src/shared/roles.js 65 | 66 | ```js 67 | siteAdmin: 'SiteAdmin', 68 | admin: 'Admin', 69 | user: 'User' 70 | ... 71 | ``` 72 | 73 | ---- 74 | 75 | Build the client files dist folder 76 | ```sh 77 | # For dev -> 78 | npm run build:dev 79 | # or to watch and build on the fly 80 | npm run watch 81 | 82 | # For prod-> 83 | npm run build:prod 84 | ``` 85 | 86 | ---- 87 | 88 | Start the server 89 | ```sh 90 | npm run start 91 | ``` 92 | 93 | ---- 94 | ### Post Setup 95 | 96 | There are three built in roles: 97 | 'User' (default), 'Admin' and 'SiteAdmin' 98 | 99 | Only the Admin and SiteAdmin can access the User administration tool. The SiteAdmin can edit the users, the Admin role can only view the user list. 100 | 101 | To add a SiteAdmin, create a new user account from the /signup page. Then manually update the database, set that user role to 'SiteAdmin' 102 | 103 | ##### User Admin view. Server-side paging, sortable, search by text filtering. 104 | ![alt-text](https://raw.githubusercontent.com/mtimmermann/Boilerplate-Role-Based-Permissions-Nodejs/master/screenshot-user-admin.png "User Admin") 105 | 106 | 107 | ##### License 108 | [MIT](LICENSE) 109 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const port = process.env.PORT || 3000; 4 | const cookieParser = require('cookie-parser'); 5 | const bodyParser = require('body-parser'); 6 | const passport = require('passport'); 7 | const config = require('./server/config'); 8 | 9 | // Connect to the database and load models 10 | require('./server/models').connect(config.dbUri); 11 | 12 | const compression = require('compression'); // Compression middleware, compress responses from all routes 13 | const helmet = require('helmet'); // Protect against web vunerablities, http headers, https://www.npmjs.com/package/helmet 14 | 15 | const auth = require('./server/routes/auth'); 16 | const api = require('./server/routes/api'); 17 | 18 | const app = express(); 19 | const http = require('http').createServer(app); 20 | 21 | 22 | app.use(compression()); 23 | app.use(helmet()); 24 | 25 | // app.use(logger('dev')); 26 | app.use(bodyParser.json()); 27 | app.use(bodyParser.urlencoded({ extended: false })); 28 | app.use(cookieParser()); 29 | 30 | 31 | // Use the passport middleware 32 | app.use(passport.initialize()); 33 | 34 | // load passport strategies 35 | const localSignupStrategy = require('./server/passport/local-signup'); 36 | const localLoginStrategy = require('./server/passport/local-login'); 37 | passport.use('local-signup', localSignupStrategy); 38 | passport.use('local-login', localLoginStrategy); 39 | 40 | 41 | // View engine setup 42 | app.set('views', path.join(__dirname, '/server/views')); 43 | app.set('view engine', 'pug'); 44 | 45 | // Serve static assets normally 46 | app.use(express.static(path.join(__dirname, '/dist'))); 47 | 48 | // Define routes 49 | app.use('/auth', auth); 50 | app.use('/api', api); 51 | 52 | 53 | // Catch 404 and forward to error handler 54 | // app.use(function(req, res, next) { 55 | // var err = new Error('Not Found'); 56 | // err.status = 404; 57 | // next(err); 58 | // }); 59 | 60 | // Single page app method for 404s, return the static html file 61 | // Handles all routes so you do not get a not found error 62 | app.get('*', function (req, res, next) { 63 | res.sendFile(path.resolve(__dirname, 'dist', 'index.html')); 64 | }); 65 | 66 | 67 | // Error handler 68 | app.use(function(err, req, res, next) { 69 | // set locals, only providing error in development 70 | res.locals.message = err.message; 71 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 72 | 73 | // Render the error page 74 | res.status(err.status || 500); 75 | res.render('error'); 76 | }); 77 | 78 | 79 | /** 80 | * Listen on provided port, on all network interfaces. 81 | */ 82 | http.listen(port); 83 | console.log('Server started on port ' + port); 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boilerplate-role-based-permissions-nodejs", 3 | "version": "1.0.0", 4 | "description": "Boilerplate role based permissions, local passport authentication", 5 | "main": "bundle.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "lint": "eslint webpack.**.js --ext .js --ext .jsx src --cache", 9 | "lint:table": "eslint webpack.**.js --ext .js --ext .jsx src --cache --format table || true", 10 | "lint:server": "eslint --no-eslintrc -c .eslint.server.js app.js server/**/*.js", 11 | "watch": "webpack --watch --progress", 12 | "build:dev": "webpack", 13 | "build:prod": "webpack --config ./webpack.prod.config.js", 14 | "start:dev": "nodemon app.js", 15 | "start1:": "if-env NODE_ENV=production && npm run build:prod && node app.js || npm run start:dev", 16 | "start": "node app.js" 17 | }, 18 | "keywords": [ 19 | "react", 20 | "passport", 21 | "nodejs", 22 | "webpack", 23 | "boilerplate", 24 | "bootstrap" 25 | ], 26 | "author": "Mark Timmermann", 27 | "license": "MIT", 28 | "dependencies": { 29 | "bcryptjs": "^2.4.3", 30 | "body-parser": "^1.17.2", 31 | "bootstrap": "^3.3.7", 32 | "compression": "^1.7.0", 33 | "cookie-parser": "^1.4.3", 34 | "express": "^4.15.4", 35 | "font-awesome": "^4.6.3", 36 | "helmet": "^3.8.1", 37 | "if-env": "^1.0.0", 38 | "install": "^0.10.1", 39 | "jquery": "^3.1.0", 40 | "jsonwebtoken": "^7.4.3", 41 | "mongoose": "^4.10.8", 42 | "mongoose-paginate": "^5.0.3", 43 | "mongoose-timestamp": "^0.6.0", 44 | "npm": "^5.3.0", 45 | "passport": "^0.4.0", 46 | "passport-local": "^1.0.0", 47 | "prop-types": "^15.5.10", 48 | "pug": "^2.0.0-rc.3", 49 | "react": "^15.6.1", 50 | "react-addons-css-transition-group": "^15.6.0", 51 | "react-dom": "^15.6.1", 52 | "react-router-dom": "^4.1.2", 53 | "react-table": "^6.5.3" 54 | }, 55 | "devDependencies": { 56 | "babel-core": "^6.13.2", 57 | "babel-eslint": "^7.2.3", 58 | "babel-loader": "^7.1.1", 59 | "babel-preset-es2015": "^6.13.2", 60 | "babel-preset-react": "^6.24.1", 61 | "browser-sync": "^2.14.0", 62 | "browser-sync-webpack-plugin": "^1.1.2", 63 | "clean-webpack-plugin": "^0.1.10", 64 | "copy-webpack-plugin": "^4.0.1", 65 | "css-loader": "^0.28.4", 66 | "eslint": "^4.4.1", 67 | "eslint-config-airbnb": "^15.1.0", 68 | "eslint-loader": "^1.9.0", 69 | "eslint-plugin-import": "^2.7.0", 70 | "eslint-plugin-jsx-a11y": "^6.0.2", 71 | "eslint-plugin-react": "^7.2.0", 72 | "extract-text-webpack-plugin": "^3.0.0", 73 | "file-loader": "^0.11.2", 74 | "html-loader": "^0.5.1", 75 | "html-webpack-plugin": "^2.22.0", 76 | "node-sass": "^4.5.3", 77 | "postcss-loader": "^2.0.6", 78 | "sass-loader": "^6.0.6", 79 | "style-loader": "^0.18.2", 80 | "url-loader": "^0.5.7", 81 | "webpack": "^3.5.3", 82 | "webpack-merge": "^4.1.0" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /screenshot-user-admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtimmermann/Boilerplate-Role-Based-Permissions-Nodejs/4f86649a4a2a62bec83e5ec1fbc05d02b43c4686/screenshot-user-admin.png -------------------------------------------------------------------------------- /server/api/controllers/messageController.js: -------------------------------------------------------------------------------- 1 | 2 | // GET /api/messages/public1 3 | exports.getPublicMessage1 = function(req, res, next) { 4 | return res.json({ 5 | message: 'public message 1 from /api/messages/public1' 6 | }); 7 | }; 8 | 9 | // GET /api/messages/private1 10 | exports.getPrivateMessage1 = function(req, res, next) { 11 | return res.json({ 12 | message: 'Authorized: private message 1 from /api/messages/private1' 13 | }); 14 | }; 15 | 16 | // GET /api/messages/admin1 17 | exports.getAdminMessage1 = function(req, res, next) { 18 | return res.json({ 19 | message: 'Authorized - Roles [\'Admin\',\'SiteAdmin\']: admin message 1 from /api/messages/admin1' 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /server/api/controllers/userController.js: -------------------------------------------------------------------------------- 1 | const User = require('mongoose').model('User'); 2 | const Roles = require('../../../src/shared/roles'); 3 | const utils = require('../../main/common/utils'); 4 | const AuthHeader = require('../../main/common/auth-header'); 5 | const { validations } = require('../../config'); 6 | 7 | // GET /api/users 8 | // List users, paginations options 9 | exports.list = function(req, res, next) { 10 | 11 | const pageOptions = { 12 | page: req.query['page'] || 1, 13 | limit: req.query['limit'] || 1000, 14 | sort: req.query['sort'] || 'name asc' 15 | }; 16 | 17 | let filterOptions = {}; 18 | try { 19 | const filterParam = JSON.parse(req.query['filter']); 20 | if (Array.isArray(filterParam) && filterParam.length > 0) { 21 | filterParam.forEach((item) => { 22 | filterOptions[item.id] = new RegExp(item.value, 'i'); 23 | }); 24 | } 25 | } catch (err) { 26 | console.log('Could not parse \'filter\' param '+ err); 27 | } 28 | 29 | // User.find({}, '-password -__v', (err, users) => { 30 | User.paginate(filterOptions, pageOptions, (err, result) => { 31 | if (err) { 32 | console.log(err); 33 | return res.status(500).json({ 34 | success: false, 35 | errors: [JSON.stringify(err)] 36 | }); 37 | } 38 | 39 | return res.json(result); 40 | }); 41 | }; 42 | 43 | 44 | // GET /api/users/:id 45 | exports.find = function(req, res, next) { 46 | 47 | User.findById(req.params.id, (err, user) => { 48 | if (err || !user) { 49 | if (err) console.log(err); 50 | return res.status(404).json({ 51 | success: false, 52 | errors: [ err ? err.message : `user id '${req.params.id} not found'` ] 53 | }); 54 | } 55 | 56 | return res.json({ 57 | success: true, 58 | data: user 59 | }); 60 | }); 61 | }; 62 | 63 | 64 | // DELETE /api/users/:id 65 | exports.destroy = function(req, res, next) { 66 | 67 | User.findByIdAndRemove(req.params.id, (err, user) => { 68 | if (err || !user) { 69 | if (err) console.log(err); 70 | return res.status(404).json({ 71 | success: false, 72 | errors: [ err ? err.message : `user id '${req.params.id} not found'` ] 73 | }); 74 | } 75 | 76 | return res.json({ 77 | success: true, 78 | data: user 79 | }); 80 | }); 81 | }; 82 | 83 | 84 | // PUT /api/users/profile/password 85 | // User changing thier profile password - auth priveledges 86 | exports.updateProfilePassword = function(req, res, next) { 87 | if (!req.body.user || typeof req.body.user !== 'object') { 88 | return res.status(409).json({ success: false, errors: ['\'user\' param is required'] }); 89 | } 90 | 91 | AuthHeader.getId(req.headers.authorization, function(err, authId) { 92 | if (err) { 93 | console.log(err); 94 | return res.status(409).json({ success: false, errors: [err.message] }); 95 | } 96 | 97 | if (req.body.user.id !== authId) return res.status(401).end(); 98 | 99 | savePassword(req.body.user.id, req.body.user.password, (err2, data) => { 100 | if (err2) { 101 | if (err2) console.log(err2); 102 | return res.status(409).json({ success: false, errors: [err2.message] }); 103 | } 104 | 105 | return res.json({ success: true }); 106 | }); 107 | }); 108 | }; 109 | 110 | // PUT /api/users/password 111 | // SiteAdmin changing user password - SiteAdmin privelege 112 | exports.updatePassword = function(req, res, next) { 113 | if (!req.body.user || typeof req.body.user !== 'object') { 114 | return res.status(409).json({ success: false, errors: ['\'user\' param is required'] }); 115 | } 116 | 117 | savePassword(req.body.user.id, req.body.user.password, (err2, data) => { 118 | if (err2) { 119 | if (err2) console.log(err2); 120 | return res.status(409).json({ success: false, errors: [err2.message] }); 121 | } 122 | 123 | return res.json({ success: true }); 124 | }); 125 | }; 126 | 127 | 128 | // PUT /api/users 129 | exports.updateUser = function(req, res, next) { 130 | if (!req.body.user || typeof req.body.user !== 'object') { 131 | return res.status(409).json({ success: false, errors: ['\'user\' param is required'] }); 132 | } 133 | 134 | const user = req.body.user; 135 | delete user.password; 136 | 137 | updateUser(user, (err, data) => { 138 | if (err) { 139 | if (err) console.log(err); 140 | return res.status(409).json({ success: false, errors: [err.message] }); 141 | } 142 | 143 | return res.json({ success: true }); 144 | }); 145 | }; 146 | 147 | 148 | // PUT /api/users/profile 149 | exports.updateProfile = function(req, res, next) { 150 | if (!req.body.user || typeof req.body.user !== 'object') { 151 | return res.status(409).json({ success: false, errors: ['\'user\' param is required'] }); 152 | } 153 | 154 | AuthHeader.getId(req.headers.authorization, function(err, authId) { 155 | if (err) { 156 | console.log(err); 157 | return res.status(409).json({ success: false, errors: [err.message] }); 158 | } 159 | 160 | const user = req.body.user; 161 | if (user.id !== authId) return res.status(401).end(); 162 | 163 | delete user.role; 164 | delete user.password; 165 | 166 | updateUser(user, (err, data) => { 167 | if (err) { 168 | if (err) console.log(err); 169 | return res.status(409).json({ success: false, errors: [err.message] }); 170 | } 171 | 172 | return res.json({ success: true }); 173 | }); 174 | }); 175 | }; 176 | 177 | 178 | function savePassword(userId, password, callback) { 179 | password = password.trim(); 180 | if (password.length < validations.password.minLength.value) 181 | return callback(validations.password.minLength.message); 182 | 183 | utils.hash(password.trim(), (err, hash) => { 184 | if (err) { 185 | console.log(err); 186 | return callback(err); 187 | } 188 | 189 | const user = { password: hash }; 190 | User.findOneAndUpdate({ _id: userId }, user, (err2, data) => { 191 | if (err2) { 192 | console.log(err2); 193 | return callback(err2); 194 | } 195 | 196 | return callback(null, data); 197 | }); 198 | }); 199 | } 200 | 201 | function updateUser(user, callback) { 202 | 203 | validateUser(user, (errValdation, u) => { 204 | if (errValdation) return callback (errValdation); 205 | 206 | User.findOneAndUpdate({ _id: u.id }, u, (err, data) => { 207 | if (err) return callback(err); 208 | 209 | return callback(null, data); 210 | }); 211 | }); 212 | } 213 | 214 | function validateUser(user, callback) { 215 | 216 | if (typeof user.name === 'string') { 217 | user.name = user.name.trim(); 218 | if (user.name.length === 0) 219 | return callback(new Error('user.name length is 0')); 220 | } else { 221 | return callback(new Error('user.name is required')); 222 | } 223 | 224 | if (typeof user.email === 'string') { 225 | user.email = user.email.trim(); 226 | if (!(validations.email.regex.value).test(user.email)) 227 | return callback(new Error(validations.email.regex.message)); 228 | } else { 229 | return callback(new Error('user.email is required')); 230 | } 231 | 232 | if (typeof user.role === 'string') { 233 | user.role = user.role.trim(); 234 | if (!Roles.isValidRole(user.role)) 235 | return callback(new Error(`user.role '${user.role}' is not a valid role`)); 236 | } else { 237 | return callback(new Error('user.role is required')); 238 | } 239 | 240 | return callback(null, user); 241 | } 242 | -------------------------------------------------------------------------------- /server/config/index.js: -------------------------------------------------------------------------------- 1 | const validations = require('../../src/shared/model-validations'); 2 | 3 | module.exports = { 4 | 5 | // DB 6 | dbUri: 'mongodb://localhost/node_auth', 7 | 8 | // jsonwebtoken secret 9 | jwtSecret: '!!secret phrase!!', 10 | 11 | // Model validations 12 | validations // :validations 13 | }; 14 | -------------------------------------------------------------------------------- /server/main/common/auth-header.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const config = require('../../config'); 3 | 4 | exports.getId = (authHeader, callback) => { 5 | if (!authHeader) return callback(new Error('authorization header not found')); 6 | 7 | // Get the last part from a authorization header string like "bearer token-value" 8 | const token = authHeader.split(' ')[1]; 9 | 10 | // Decode the token using a secret key-phrase 11 | return jwt.verify(token, config.jwtSecret, (err, decoded) => { 12 | if (err) { 13 | console.log(err); 14 | return callback(err); 15 | } 16 | 17 | return callback(null, decoded.sub); 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /server/main/common/utils.js: -------------------------------------------------------------------------------- 1 | // const bcrypt = require('bcrypt'); // Use bcryptjs for Windows, bcrypt for Linux 2 | const bcrypt = require('bcryptjs'); 3 | 4 | /** 5 | * Hash encrypt a clear string 6 | * 7 | * @param {string} clearString The string to hash 8 | * @param {function} callback (err, data) 9 | The function that is called after a service call 10 | error {object}: null if no error 11 | data {object}: The data set of a succesful call 12 | */ 13 | exports.hash = (clearString, callback) => { 14 | return bcrypt.genSalt((saltError, salt) => { 15 | if (saltError) { return callback(saltError); } 16 | 17 | return bcrypt.hash(clearString, salt, (hashError, hash) => { 18 | if (hashError) { return callback(hashError); } 19 | 20 | return callback(null, hash); 21 | }); 22 | }); 23 | }; 24 | 25 | /** 26 | * Check if clear string matches the bycrpt hash 27 | */ 28 | exports.compareHash = (hash, clearString, callback) => { 29 | bcrypt.compare(hash, clearString, callback); 30 | }; 31 | -------------------------------------------------------------------------------- /server/main/controllers/authController.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport'); 2 | const { validations } = require('../../config'); 3 | 4 | // POST /auth/signup 5 | exports.postSignup = function(req, res, next) { 6 | const validationResult = validateSignupForm(req.body); 7 | if (!validationResult.success) { 8 | return res.status(400).json({ 9 | success: false, 10 | message: validationResult.message, 11 | errors: validationResult.errors 12 | }); 13 | } 14 | 15 | return passport.authenticate('local-signup', (err) => { 16 | if (err) { 17 | console.log(JSON.stringify(err)); 18 | 19 | if (err.name === 'MongoError' && err.code === 11000) { 20 | // 11000 Mongo code is for a duplication email error 21 | // 409 HTTP status code is for conflict error 22 | return res.status(409).json({ 23 | success: false, 24 | message: 'Check the form for errors.', 25 | errors: { 26 | email: 'This email is already taken.' 27 | } 28 | }); 29 | } 30 | 31 | return res.status(400).json({ 32 | success: false, 33 | message: 'Could not process the form.' 34 | }); 35 | } 36 | 37 | return res.status(200).json({ 38 | success: true, 39 | message: 'Sign up success.' 40 | }); 41 | })(req, res, next); 42 | }; 43 | 44 | 45 | // POST /auth/login 46 | exports.postLogin = function(req, res, next) { 47 | const validationResult = validateLoginForm(req.body); 48 | if (!validationResult.success) { 49 | return res.status(400).json({ 50 | success: false, 51 | message: validationResult.message, 52 | errors: validationResult.errors 53 | }); 54 | } 55 | 56 | return passport.authenticate('local-login', (err, token, userData) => { 57 | if (err) { 58 | if (err.name === 'IncorrectCredentialsError') { 59 | return res.status(400).json({ 60 | success: false, 61 | message: err.message 62 | }); 63 | } 64 | 65 | return res.status(400).json({ 66 | success: false, 67 | message: 'Could not process the form.' 68 | }); 69 | } 70 | 71 | return res.json({ 72 | success: true, 73 | message: 'Login success.', 74 | token, 75 | user: userData 76 | }); 77 | })(req, res, next); 78 | }; 79 | 80 | /** 81 | * Validate the sign up form 82 | * 83 | * @param {object} payload - the HTTP body message 84 | * @returns {object} The result of validation. Object contains a boolean validation result, 85 | * errors tips, and a global message for the whole form. 86 | */ 87 | function validateSignupForm(payload) { 88 | const errors = {}; 89 | let isFormValid = true; 90 | let message = ''; 91 | 92 | if (!payload || typeof payload.email !== 'string' || 93 | !(validations.email.regex.value).test(payload.email.trim())) { 94 | isFormValid = false; 95 | errors.email = validations.email.regex.message; 96 | } 97 | 98 | if (!payload || typeof payload.password !== 'string' || 99 | payload.password.trim().length < validations.password.minLength.value) { 100 | isFormValid = false; 101 | errors.password = validations.password.minLength.message; 102 | } 103 | 104 | if (!payload || typeof payload.name !== 'string' || payload.name.trim().length === 0) { 105 | isFormValid = false; 106 | errors.name = 'Please provide your name.'; 107 | } 108 | 109 | if (!isFormValid) { 110 | message = 'Check the form for errors.'; 111 | } 112 | 113 | return { 114 | success: isFormValid, 115 | message, 116 | errors 117 | }; 118 | } 119 | 120 | /** 121 | * Validate the login form 122 | * 123 | * @param {object} payload - the HTTP body message 124 | * @returns {object} The result of validation. Object contains a boolean validation result, 125 | * errors tips, and a global message for the whole form. 126 | */ 127 | function validateLoginForm(payload) { 128 | const errors = {}; 129 | let isFormValid = true; 130 | let message = ''; 131 | 132 | if (!payload || typeof payload.email !== 'string' || 133 | !(validations.email.regex.value).test(payload.email.trim())) { 134 | isFormValid = false; 135 | errors.email = validations.email.regex.message; 136 | } 137 | 138 | if (!payload || typeof payload.password !== 'string' || 139 | payload.password.trim().length < validations.password.minLength.value) { 140 | isFormValid = false; 141 | errors.password = validations.password.minLength.message; 142 | } 143 | 144 | if (!isFormValid) { 145 | message = 'Check the form for errors.'; 146 | } 147 | 148 | return { 149 | success: isFormValid, 150 | message, 151 | errors 152 | }; 153 | } 154 | -------------------------------------------------------------------------------- /server/middleware/auth-check.js: -------------------------------------------------------------------------------- 1 | const User = require('mongoose').model('User'); 2 | const jwt = require('jsonwebtoken'); 3 | const config = require('../config'); 4 | 5 | /** 6 | * Auth Checker middleware function. 7 | * 8 | * @param {array} roles User roles to grant permision for a route 9 | * If undefined, any user with login token can access 10 | */ 11 | module.exports = function(roles) { 12 | 13 | // Return middleware 14 | return (req, res, next) => { 15 | if (!req.headers.authorization) { 16 | return res.status(401).end(); 17 | } 18 | 19 | // Get the last part from a authorization header string like "bearer token-value" 20 | const token = req.headers.authorization.split(' ')[1]; 21 | 22 | // Decode the token using a secret key-phrase 23 | return jwt.verify(token, config.jwtSecret, (err, decoded) => { 24 | 25 | // 401 not unauthorized 26 | if (err) return res.status(401).end(); 27 | 28 | const userId = decoded.sub; 29 | 30 | // Check if user exists 31 | return User.findById(userId, (err2, user) => { 32 | if (err2 || !user) return res.status(401).end(); 33 | 34 | if (roles) { 35 | if (roles.indexOf(user.role) > -1) return next(); 36 | else return res.status(401).end(); 37 | } 38 | 39 | return next(); 40 | 41 | }); 42 | }); 43 | 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /server/models/index.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | module.exports.connect = (uri) => { 4 | mongoose.connect(uri); 5 | // plug in the promise library: 6 | mongoose.Promise = global.Promise; 7 | 8 | mongoose.connection.on('error', (err) => { 9 | console.error(`Mongoose connection error: ${err}`); 10 | process.exit(1); 11 | }); 12 | 13 | // load models 14 | require('./user'); 15 | }; 16 | -------------------------------------------------------------------------------- /server/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | // // const bcrypt = require('bcrypt'); // Use bcryptjs for Windows, bcrypt for Linux 3 | // const bcrypt = require('bcryptjs'); 4 | const utils = require('../main/common/utils'); 5 | const mongoosePaginate = require('mongoose-paginate'); 6 | const timestamps = require('mongoose-timestamp'); 7 | 8 | const Roles = require('../../src/shared/roles'); 9 | 10 | // define the User model schema 11 | const UserSchema = new mongoose.Schema({ 12 | name: { 13 | type: String, 14 | required: true 15 | }, 16 | email: { 17 | type: String, 18 | index: { unique: true } 19 | }, 20 | role: { 21 | type: String, 22 | default: Roles.user, 23 | enum: [Roles.user, Roles.siteAdmin] // Accept only these roles 24 | }, 25 | password: String 26 | }); 27 | 28 | UserSchema.plugin(mongoosePaginate); 29 | UserSchema.plugin(timestamps); 30 | 31 | /** 32 | * Override default toJSON, remove password field and __v version 33 | */ 34 | UserSchema.methods.toJSON = function() { 35 | var obj = this.toObject(); 36 | delete obj.password; 37 | delete obj.__v; 38 | obj.id = obj._id; 39 | delete obj._id; 40 | return obj; 41 | }; 42 | 43 | 44 | /** 45 | * Compare the passed password with the value in the database. A model method. 46 | * 47 | * @param {string} password 48 | * @returns {object} callback 49 | */ 50 | UserSchema.methods.comparePassword = function comparePassword(password, callback) { 51 | utils.compareHash(password, this.password, callback); 52 | }; 53 | 54 | 55 | /** 56 | * The pre-save hook method. 57 | * 58 | * NOTE: pre & post hooks are not executed on update() and findeOneAndUpdate() 59 | * http://mongoosejs.com/docs/middleware.html 60 | */ 61 | UserSchema.pre('save', function saveHook(next) { 62 | const user = this; 63 | 64 | // Proceed further only if the password is modified or the user is new 65 | if (!user.isModified('password')) return next(); 66 | 67 | return utils.hash(user.password, (err, hash) => { 68 | if (err) { return next (err); } 69 | 70 | // Replace the password string with hash value 71 | user.password = hash; 72 | 73 | return next(); 74 | }); 75 | }); 76 | 77 | module.exports = mongoose.model('User', UserSchema); 78 | -------------------------------------------------------------------------------- /server/passport/local-login.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const User = require('mongoose').model('User'); 3 | const PassportLocalStrategy = require('passport-local').Strategy; 4 | const config = require('../config'); 5 | 6 | 7 | /** 8 | * Return the Passport Local Strategy object. 9 | */ 10 | module.exports = new PassportLocalStrategy({ 11 | usernameField: 'email', 12 | passwordField: 'password', 13 | session: false, 14 | passReqToCallback: true 15 | }, (req, email, password, done) => { 16 | const userData = { 17 | email: email.trim(), 18 | password: password.trim() 19 | }; 20 | 21 | // find a user by email address 22 | return User.findOne({ email: userData.email }, (err, user) => { 23 | if (err) { return done(err); } 24 | 25 | if (!user) { 26 | const error = new Error('Incorrect email or password'); 27 | error.name = 'IncorrectCredentialsError'; 28 | 29 | return done(error); 30 | } 31 | 32 | // check if a hashed user's password is equal to a value saved in the database 33 | return user.comparePassword(userData.password, (passwordErr, isMatch) => { 34 | if (err) { return done(err); } 35 | 36 | if (!isMatch) { 37 | const error = new Error('Incorrect email or password'); 38 | error.name = 'IncorrectCredentialsError'; 39 | 40 | return done(error); 41 | } 42 | 43 | const payload = { 44 | sub: user._id 45 | }; 46 | 47 | // create a token string 48 | const token = jwt.sign(payload, config.jwtSecret); 49 | const data = { 50 | id: user.id, 51 | name: user.name, 52 | email: user.email, 53 | role: user.role 54 | }; 55 | 56 | return done(null, token, data); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /server/passport/local-signup.js: -------------------------------------------------------------------------------- 1 | const User = require('mongoose').model('User'); 2 | const PassportLocalStrategy = require('passport-local').Strategy; 3 | 4 | 5 | /** 6 | * Return the Passport Local Strategy object. 7 | */ 8 | module.exports = new PassportLocalStrategy({ 9 | usernameField: 'email', 10 | passwordField: 'password', 11 | session: false, 12 | passReqToCallback: true 13 | }, (req, email, password, done) => { 14 | const userData = { 15 | email: email.trim(), 16 | password: password.trim(), 17 | name: req.body.name.trim() 18 | }; 19 | 20 | const newUser = new User(userData); 21 | newUser.save((err) => { 22 | if (err) { return done(err); } 23 | 24 | return done(null); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /server/routes/api.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const authCheck = require('../middleware/auth-check'); 4 | const Roles = require('../../src/shared/roles'); 5 | 6 | const messageController = require('../api/controllers/messageController'); 7 | const userController = require('../api/controllers/userController'); 8 | 9 | // GET /api/messages/public1 10 | router.get('/messages/public1', messageController.getPublicMessage1); 11 | 12 | // GET /api/messages/private1 13 | router.get('/messages/private1', authCheck(), messageController.getPrivateMessage1); 14 | 15 | // GET /api/messages/admin1 16 | router.get('/messages/admin1', authCheck([Roles.admin,Roles.siteAdmin]), messageController.getAdminMessage1); 17 | 18 | 19 | // GET /api/users 20 | router.get('/users', authCheck([Roles.admin,Roles.siteAdmin]), userController.list); 21 | 22 | // GET /api/users/:id 23 | router.get('/users/:id', authCheck([Roles.siteAdmin]), userController.find); 24 | 25 | // DELETE /api/users/:id 26 | router.delete('/users/:id', authCheck([Roles.siteAdmin]), userController.destroy); 27 | 28 | // PUT /api/users 29 | router.put('/users', authCheck([Roles.siteAdmin]), userController.updateUser); 30 | 31 | // PUT /api/users/password 32 | router.put('/users/password', authCheck([Roles.siteAdmin]), userController.updatePassword); 33 | 34 | // PUT /api/users/profile 35 | router.put('/users/profile', authCheck(), userController.updateProfile); 36 | 37 | // PUT /api/users/profile/password 38 | router.put('/users/profile/password', authCheck(), userController.updateProfilePassword); 39 | 40 | module.exports = router; 41 | -------------------------------------------------------------------------------- /server/routes/auth.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | const authController = require('../main/controllers/authController'); 5 | 6 | // POST /auth/signup 7 | router.post('/signup', authController.postSignup); 8 | 9 | // POST /auth/login 10 | router.post('/login', authController.postLogin); 11 | 12 | module.exports = router; 13 | -------------------------------------------------------------------------------- /server/views/error.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /server/views/layout.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(charset='utf-8') 5 | meta(http-equiv='X-UA-Compatible', content='IE=edge') 6 | meta(name='description', content='Boiler plate, ES6 and Bootstrap, built using Webpack') 7 | meta(name='viewport', content='width=device-width, initial-scale=1') 8 | 9 | title= title 10 | 11 | // Disable tap highlight on IE 12 | meta(name='msapplication-tap-highlight', content='no') 13 | 14 | // Web Application Manifest 15 | link(rel='manifest', href='manifest.json') 16 | 17 | // Add to homescreen for Chrome on Android 18 | meta(name='mobile-web-app-capable', content='yes') 19 | meta(name='application-name', content='Static Website Boilerplate') 20 | link(rel='icon', sizes='192x192', href='/img/ico/chrome-touch-icon-192x192.png') 21 | 22 | // Add to homescreen for Safari on iOS 23 | meta(name='apple-mobile-web-app-capable', content='yes') 24 | meta(name='apple-mobile-web-app-status-bar-style', content='black') 25 | meta(name='apple-mobile-web-app-title', content='Static Website Boilerplate') 26 | link(rel='apple-touch-icon', href='/img/ico/apple-touch-icon.png') 27 | 28 | // Tile icon for Win8 (144x144 + tile color) 29 | meta(name='msapplication-TileImage', content='/img/ico/mstile-150x150.png') 30 | meta(name='msapplication-TileColor', content='#263238') 31 | 32 | // Color the status bar on mobile devices 33 | meta(name='theme-color', content='#263238') 34 | 35 | link(rel='stylesheet', href='/css/style.css') 36 | body 37 | 38 | block content 39 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtimmermann/Boilerplate-Role-Based-Permissions-Nodejs/4f86649a4a2a62bec83e5ec1fbc05d02b43c4686/src/favicon.ico -------------------------------------------------------------------------------- /src/img/ico/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtimmermann/Boilerplate-Role-Based-Permissions-Nodejs/4f86649a4a2a62bec83e5ec1fbc05d02b43c4686/src/img/ico/apple-touch-icon.png -------------------------------------------------------------------------------- /src/img/ico/chrome-touch-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtimmermann/Boilerplate-Role-Based-Permissions-Nodejs/4f86649a4a2a62bec83e5ec1fbc05d02b43c4686/src/img/ico/chrome-touch-icon-192x192.png -------------------------------------------------------------------------------- /src/img/ico/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtimmermann/Boilerplate-Role-Based-Permissions-Nodejs/4f86649a4a2a62bec83e5ec1fbc05d02b43c4686/src/img/ico/icon-128x128.png -------------------------------------------------------------------------------- /src/img/ico/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtimmermann/Boilerplate-Role-Based-Permissions-Nodejs/4f86649a4a2a62bec83e5ec1fbc05d02b43c4686/src/img/ico/mstile-150x150.png -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Boilerplate Role Based Permissions React Nodejs 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | 41 |
42 | 92 |
93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /src/jsx/common/imports.js: -------------------------------------------------------------------------------- 1 | // import 'bootstrap/dist/js/bootstrap.js'; 2 | import 'bootstrap/dist/js/bootstrap'; 3 | import 'bootstrap/dist/css/bootstrap.css'; 4 | import 'font-awesome/css/font-awesome.css'; 5 | import '../../scss/style.scss'; 6 | -------------------------------------------------------------------------------- /src/jsx/common/main-routes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Route, Switch } from 'react-router-dom'; 4 | 5 | import Auth from '../modules/auth'; 6 | import PrivateRoutes from './private-routes'; 7 | 8 | import Home from '../pages/home'; 9 | import SignIn from '../pages/signin'; 10 | import SignUp from '../pages/signup'; 11 | import Public1 from '../pages/public1'; 12 | 13 | import Profile from '../pages/private/profile'; 14 | import ProfilePassword from '../pages/private/profile-password'; 15 | import Private1 from '../pages/private/private1'; 16 | import Admin1 from '../pages/private/admin1'; 17 | 18 | import Users from '../pages/private/site-admin/users'; 19 | import UserEdit from '../pages/private/site-admin/user-edit'; 20 | import UserDelete from '../pages/private/site-admin/user-delete'; 21 | import UserPassword from '../pages/private/site-admin/user-password'; 22 | 23 | class MainRoutes extends React.Component { 24 | 25 | render() { 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {/* eslint-disable arrow-body-style, arrow-parens */} 36 | ()} /> 37 | ()} /> 38 | {/* eslint-enable arrow-body-style */} 39 | 40 | 41 | 42 | 43 | 44 | {/* */} 45 | {/* eslint-disable arrow-body-style, arrow-parens */} 46 | ()} /> 47 | {/* eslint-enable arrow-body-style */} 48 | 49 | 50 | 51 | {/* */} 52 | 53 | ); 54 | } 55 | } 56 | MainRoutes.propTypes = { 57 | isAuthenticated: PropTypes.bool.isRequired 58 | }; 59 | 60 | export default MainRoutes; 61 | -------------------------------------------------------------------------------- /src/jsx/common/navbar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Auth from '../modules/auth'; 4 | import { NavLink } from 'react-router-dom'; 5 | import Roles from '../../shared/roles'; 6 | 7 | class NavBar extends React.Component { 8 | 9 | // eslint-disable-next-line class-methods-use-this 10 | signOut() { 11 | Auth.deauthenticateUser(); 12 | } 13 | 14 | render() { 15 | const isAuthenticated = this.props.isAuthenticated; 16 | const user = this.props.user; 17 | return ( 18 | 78 | ); 79 | } 80 | } 81 | NavBar.propTypes = { 82 | isAuthenticated: PropTypes.bool.isRequired, 83 | user: PropTypes.object.isRequired // eslint-disable-line react/forbid-prop-types 84 | }; 85 | 86 | export default NavBar; 87 | -------------------------------------------------------------------------------- /src/jsx/common/private-routes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Redirect } from 'react-router-dom'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import NotFound from '../pages/not-found'; 6 | 7 | const PrivateRoutes = ({ isAuthenticated, role, children }) => { 8 | 9 | let isFound = false; 10 | let hasPermissions = false; 11 | children.props.children.forEach(function(route) { // eslint-disable-line prefer-arrow-callback 12 | 13 | // Check if current path matches a route, and it has role permissions 14 | const routePath = route.props.path.toLowerCase(); 15 | const routeMatcher = new RegExp(routePath.replace(/:[^\s/]+/g, '([\\w-]+)')); 16 | const url = location.pathname.toLowerCase(); 17 | const match = url.match(routeMatcher); 18 | if (match && match.input === match[0]) { 19 | isFound = true; 20 | if (!route.props.userRoles) hasPermissions = true; 21 | else if (route.props.userRoles) { 22 | let roles = route.props.userRoles.replace(/\s/g, ''); 23 | roles = roles.split(','); 24 | if (roles.indexOf(role) > -1) hasPermissions = true; 25 | } 26 | } 27 | }); 28 | 29 | return ( 30 | // eslint-disable-next-line no-nested-ternary 31 | isAuthenticated && hasPermissions && isFound ? ( 32 |
33 | {children} 34 |
35 | ) : ( 36 | !isFound ? ( 37 | 38 | ) : ( 39 | 40 | ) 41 | ) 42 | ); 43 | }; 44 | PrivateRoutes.propTypes = { 45 | isAuthenticated: PropTypes.bool.isRequired, 46 | role: PropTypes.string.isRequired, 47 | children: PropTypes.object.isRequired // eslint-disable-line react/forbid-prop-types 48 | // children: PropTypes.instanceOf(Route).isRequired 49 | }; 50 | 51 | export default PrivateRoutes; 52 | -------------------------------------------------------------------------------- /src/jsx/common/utils.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable wrap-iife */ 2 | /* eslint-disable consistent-return */ 3 | /* eslint-disable no-bitwise */ 4 | 5 | const Utils = 6 | (function () { 7 | 8 | return { 9 | 10 | /** 11 | * Alternative to setTimeout, will execute callback in true time based on a timestamp; 12 | * as some browsers timing varies w/ setTimeout 13 | * @param {number} interval Time to wait in milliseconds 14 | * @param {function} callback() The callback function 15 | */ 16 | timeout: (interval, callback) => { 17 | const start = Date.now(); 18 | (function f() { 19 | // eslint-disable-next-line one-var 20 | // eslint-disable-next-line indent 21 | const diff = Date.now() - start; 22 | // const ns = (((interval - diff)/1e3) >> 0); 23 | // const m = (ns/60) >> 0; 24 | // const s = ns - m*60; 25 | // console.log('Callback in '+ m +':'+ ((''+s).length>1?'':'0')+s); 26 | if (diff > interval) { 27 | callback(); 28 | return void 0; // eslint-disable-line no-void 29 | } 30 | // setTimeout(f,1e3); 31 | setTimeout(f, 10); // Pass the function in to window.setTimeout 32 | })(); 33 | }, 34 | 35 | /** 36 | * Find the first input to set focus to 37 | */ 38 | focusFirstInput: () => { 39 | $('form:first *:input[type!=hidden]:enabled:first').focus(); 40 | } 41 | }; 42 | 43 | }()); 44 | 45 | export default Utils; 46 | -------------------------------------------------------------------------------- /src/jsx/components/form-submit-errors.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; 3 | import PropTypes from 'prop-types'; 4 | 5 | class FormSubmitErrors extends Component { 6 | render() { 7 | const errors = []; 8 | this.props.errors.forEach((error) => { 9 | errors.push(
  • {error}
  • ); 10 | }); 11 | const divs = []; 12 | if (errors.length > 0) { 13 | divs.push( 14 |
    15 |
      16 | {errors} 17 |
    18 |
    19 | ); 20 | } 21 | 22 | return ( 23 | 27 | {divs} 28 | 29 | ); 30 | } 31 | } 32 | FormSubmitErrors.propTypes = { 33 | errors: PropTypes.arrayOf(PropTypes.string).isRequired 34 | }; 35 | 36 | export default FormSubmitErrors; 37 | -------------------------------------------------------------------------------- /src/jsx/components/form-validation-errors.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; 3 | import PropTypes from 'prop-types'; 4 | 5 | class FormValidationErrors extends Component { 6 | render() { 7 | const validation = this.props.validation; 8 | const errors = []; 9 | Object.keys(validation).forEach((key) => { 10 | if (typeof validation[key].valid === 'boolean' && 11 | typeof validation[key].touched === 'boolean' && 12 | typeof validation[key].message === 'string' && 13 | !validation[key].valid && 14 | validation[key].touched) { 15 | errors.push(
  • {validation[key].message}
  • ); 16 | } 17 | }); 18 | 19 | const divs = []; 20 | if (errors.length > 0) { 21 | divs.push( 22 |
    23 |
      24 | 28 | {errors} 29 | 30 |
    31 |
    32 | ); 33 | } 34 | 35 | return ( 36 | 40 | {divs} 41 | 42 | ); 43 | } 44 | } 45 | FormValidationErrors.propTypes = { 46 | validation: PropTypes.object.isRequired // eslint-disable-line react/forbid-prop-types 47 | }; 48 | 49 | export default FormValidationErrors; 50 | -------------------------------------------------------------------------------- /src/jsx/components/password-change-form.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Utils from '../common/utils'; 5 | import ModelValidations from '../../shared/model-validations'; 6 | import FormValidationErrors from './form-validation-errors'; 7 | import FormSubmitErrors from './form-submit-errors'; 8 | 9 | class PasswordChangeForm extends Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | validation: { 15 | password: { 16 | valid: false, 17 | touched: false, 18 | message: 'Password must be a mininum of 8 characters' 19 | }, 20 | passwordConfirm: { 21 | valid: false, 22 | touched: false, 23 | message: ModelValidations.password.minLength.message 24 | }, 25 | formValid: false 26 | } 27 | }; 28 | 29 | this.changeInput = this.changeInput.bind(this); 30 | this.cancel = this.cancel.bind(this); 31 | } 32 | 33 | componentDidMount() { 34 | Utils.focusFirstInput(); 35 | } 36 | componentDidUpdate(prevProps) { 37 | if (prevProps.isFetching !== this.props.isFetching) { 38 | Utils.focusFirstInput(); 39 | } 40 | } 41 | 42 | changeInput(evt) { 43 | const field = evt.target.name; 44 | const value = evt.target.value; 45 | const user = this.props.user; 46 | user[field] = value; 47 | 48 | this.setState({ 49 | errors: [] 50 | }); 51 | this.validate(field, value); 52 | } 53 | 54 | validate(field, value) { 55 | const validation = this.state.validation; 56 | if (validation[field]) validation[field].touched = true; 57 | 58 | switch (field) { 59 | case 'password': 60 | validation.password.valid = value.length >= ModelValidations.password.minLength.value; 61 | break; 62 | case 'passwordConfirm': 63 | validation.passwordConfirm.valid = value === this.props.user.password; 64 | break; 65 | } 66 | 67 | validation.formValid = true; 68 | Object.keys(validation).forEach((key) => { 69 | if (typeof validation[key].valid === 'boolean' && !validation[key].valid) { 70 | validation.formValid = false; 71 | } 72 | }); 73 | 74 | this.setState({ validation: validation }); // eslint-disable-line object-shorthand 75 | } 76 | 77 | cancel() { 78 | this.props.history.goBack(); 79 | } 80 | 81 | render() { 82 | const validation = this.state.validation; 83 | return ( 84 |
    85 |
    86 |
    87 |

    Change Password

    88 |
    89 |
    90 |
    91 |
    92 |
    93 |
    94 |
    95 | 96 |
    97 | 98 |
    99 |
    100 |
    101 | 102 |
    103 | 104 |
    105 |
    106 |
    107 | 108 |
    109 | 110 |
    111 |
    112 |
    113 | 114 |
    115 | 116 |
    117 |
    118 |
    119 |
    120 | 121 | 122 |
    123 |
    124 |
    125 |
    126 |
    127 |
    128 | 129 | 130 |
    131 |
    132 |
    133 | ); 134 | } 135 | } 136 | PasswordChangeForm.propTypes = { 137 | user: PropTypes.shape({ 138 | name: PropTypes.string.isRequired, 139 | email: PropTypes.string.isRequired, 140 | password: PropTypes.string 141 | }).isRequired, 142 | submit: PropTypes.func.isRequired, 143 | errors: PropTypes.arrayOf(PropTypes.string).isRequired, 144 | history: PropTypes.shape({ 145 | goBack: PropTypes.func.isRequired 146 | }).isRequired, 147 | isFetching: PropTypes.bool.isRequired 148 | }; 149 | 150 | export default PasswordChangeForm; 151 | -------------------------------------------------------------------------------- /src/jsx/config/index.js: -------------------------------------------------------------------------------- 1 | // Client configuration 2 | 3 | module.exports = { 4 | 5 | /** 6 | * Set the token expire age in milliseconds. 7 | */ 8 | session: { 9 | maxAge: 2 * 60 * 60 * 1000 // 2 hours 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/jsx/index.jsx: -------------------------------------------------------------------------------- 1 | import './common/imports'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { BrowserRouter as Router } from 'react-router-dom'; 6 | 7 | import Auth from './modules/auth'; 8 | 9 | import MainRoutes from './common/main-routes'; 10 | import NavBar from './common/navbar'; 11 | 12 | class App extends React.Component { 13 | render() { 14 | const isAuthenticated = Auth.isAuthenticated(); 15 | return ( 16 |
    17 | 18 |
    19 | 20 |
    21 |
    22 | ); 23 | } 24 | } 25 | 26 | $(() => { 27 | ReactDOM.render( 28 | 29 | 30 | , 31 | document.querySelector('#container-main') 32 | ); 33 | }); 34 | -------------------------------------------------------------------------------- /src/jsx/modules/auth.jsx: -------------------------------------------------------------------------------- 1 | import { session } from '../config'; 2 | 3 | function isExpired() { 4 | const exp = parseInt(localStorage.getItem('exp')); // eslint-disable-line radix 5 | return Date.now() > exp; 6 | } 7 | 8 | function setExpire() { 9 | const exp = new Date().getTime() + session.maxAge; 10 | localStorage.setItem('exp', new Date(exp).getTime()); 11 | } 12 | 13 | const Auth = { 14 | 15 | /** 16 | * Check if a user is authenticated - check if a token is saved in Local Storage 17 | * 18 | * @returns {boolean} 19 | */ 20 | isAuthenticated: () => { 21 | if (localStorage.getItem('token') !== null && !isExpired()) { 22 | setExpire(); 23 | return true; 24 | } 25 | 26 | Auth.deauthenticateUser(); // On expire, de-auth user 27 | return false; 28 | }, 29 | 30 | /** 31 | * Authenticate a user. Save a token string in Local Storage 32 | * 33 | * @param {string} token 34 | * @param {object} user 35 | */ 36 | authenticateUser: (token, user) => { 37 | localStorage.setItem('token', token); 38 | localStorage.setItem('utoken', btoa(JSON.stringify(user))); 39 | setExpire(); 40 | }, 41 | 42 | /** 43 | * Update user in local Storage 44 | * 45 | * @param {object} user 46 | */ 47 | updateUser: (user) => { 48 | localStorage.setItem('utoken', btoa(JSON.stringify(user))); 49 | }, 50 | 51 | /** 52 | * Deauthenticate a user. Remove a token from Local Storage. 53 | */ 54 | deauthenticateUser: () => { 55 | localStorage.removeItem('token'); 56 | localStorage.removeItem('utoken'); 57 | localStorage.removeItem('exp'); 58 | }, 59 | 60 | /** 61 | * Get a token value. 62 | * @returns {string} 63 | */ 64 | getToken: () => { 65 | return localStorage.getItem('token'); 66 | }, 67 | 68 | /** 69 | * Get user role 70 | * @returns {string} 71 | */ 72 | getRole: () => { 73 | const user = Auth.getUser(); 74 | return user && user.role ? user.role : ''; 75 | }, 76 | 77 | /** 78 | * Get user object 79 | * @returns {object} 80 | */ 81 | getUser: () => { 82 | const utoken = localStorage.getItem('utoken'); 83 | return utoken !== null ? JSON.parse(atob(utoken)) : null; 84 | } 85 | 86 | }; 87 | 88 | export default Auth; 89 | -------------------------------------------------------------------------------- /src/jsx/pages/home.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class Home extends Component { 4 | render() { 5 | return (

    Home Page

    ); 6 | } 7 | } 8 | 9 | export default Home; 10 | -------------------------------------------------------------------------------- /src/jsx/pages/not-found.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class NotFound extends Component { 4 | render() { 5 | return (

    404 Page Not Found

    ); 6 | } 7 | } 8 | 9 | export default NotFound; 10 | -------------------------------------------------------------------------------- /src/jsx/pages/private/admin1.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; 3 | 4 | import MessageService from '../../services/message-service'; 5 | 6 | class Admin1 extends Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | message: '', 12 | error: '' 13 | }; 14 | 15 | this.componentWillMount = this.componentWillMount.bind(this); 16 | } 17 | 18 | componentWillMount() { 19 | MessageService.getAdminMessage1((err, data) => { 20 | if (err) { 21 | this.setState({ message: '', error: err }); 22 | } else { 23 | this.setState({ message: data.message, error: '' }); 24 | } 25 | }); 26 | } 27 | 28 | render() { 29 | const divs = []; 30 | if (this.state.message || this.state.error) { 31 | divs.push( 32 |
    33 | {this.state.message} 34 | {this.state.error} 35 |
    36 | ); 37 | } 38 | 39 | return ( 40 |
    41 |
    42 |
    43 |

    Admin Page 1

    44 | 48 | {divs} 49 | 50 |
    51 |
    52 |
    53 |
    54 |
      55 |
    • React route
    • 56 | {/* eslint-disable react/no-unescaped-entities */} 57 |
        58 |
      • /admin1
      • 59 |
      • Requires auth token. User roles of 'Admin' or 'SiteAdmin'
      • 60 |
      • See src/jsx/common/main-routes.jsx
      • 61 |
      62 |
    • Api route
    • 63 |
        64 |
      • /api/messages/admin1
      • 65 |
      • Requires auth token. User roles of 'Admin' or 'SiteAdmin'
      • 66 |
      • 67 | See server/routes/api.js 68 | {/* eslint-disable max-len */} 69 | {/* eslint-disable react/no-unescaped-entities, react/jsx-no-comment-textnodes */} 70 |
        71 |                     const authCheck = require('../middleware/auth-check'); 

        72 | 73 | // GET /api/messages/admin1
        74 | router.get('/messages/admin1', authCheck([Roles.admin,Roles.siteAdmin]), messageController.getAdminMessage1); 75 |
        76 | {/* eslint-enable react/no-unescaped-entities, react/jsx-no-comment-textnodes, max-len */} 77 |
      • 78 |
      79 | {/* eslint-enable react/no-unescaped-entities */} 80 |
    81 |
    82 |
    83 |
    84 | ); 85 | } 86 | } 87 | 88 | export default Admin1; 89 | -------------------------------------------------------------------------------- /src/jsx/pages/private/private1.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; 3 | 4 | import MessageService from '../../services/message-service'; 5 | 6 | class Private1 extends Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | message: '', 12 | error: '' 13 | }; 14 | 15 | this.componentWillMount = this.componentWillMount.bind(this); 16 | } 17 | 18 | componentWillMount() { 19 | MessageService.getPrivateMessage1((err, data) => { 20 | if (err) { 21 | this.setState({ message: '', error: err }); 22 | } else { 23 | this.setState({ message: data.message, error: '' }); 24 | } 25 | }); 26 | } 27 | 28 | render() { 29 | const divs = []; 30 | if (this.state.message || this.state.error) { 31 | divs.push( 32 |
    33 | {this.state.message} 34 | {this.state.error} 35 |
    36 | ); 37 | } 38 | 39 | return ( 40 |
    41 |
    42 |
    43 |

    Private Page 1

    44 | 48 | {divs} 49 | 50 |
    51 |
    52 |
    53 |
    54 |
      55 |
    • React route
    • 56 |
        57 |
      • /private1
      • 58 |
      • Requires auth token, any user role
      • 59 |
      • See src/jsx/common/main-routes.jsx
      • 60 |
      61 |
    • Api route
    • 62 |
        63 |
      • /api/messages/message1
      • 64 |
      • Requires auth token, any user role
      • 65 |
      • 66 | See server/routes/api.js 67 | {/* eslint-disable max-len */} 68 | {/* eslint-disable react/no-unescaped-entities, react/jsx-no-comment-textnodes */} 69 |
        70 |                     const authCheck = require('../middleware/auth-check'); 

        71 | 72 | // GET /api/messages/private1
        73 | router.get('/messages/private1', authCheck(), messageController.getPrivateMessage1); 74 |
        75 | {/* eslint-enable react/no-unescaped-entities, react/jsx-no-comment-textnodes, max-len */} 76 |
      • 77 |
      78 |
    79 |
    80 |
    81 |
    82 | ); 83 | } 84 | } 85 | 86 | export default Private1; 87 | -------------------------------------------------------------------------------- /src/jsx/pages/private/profile-password.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import UserService from '../../services/user-service'; 5 | import PasswordChangeForm from '../../components/password-change-form'; 6 | 7 | class ProfilePassword extends Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | errors: [], 13 | user: this.props.user 14 | }; 15 | 16 | this.submit = this.submit.bind(this); 17 | } 18 | 19 | submit(evt) { 20 | evt.preventDefault(); 21 | 22 | UserService.profileUserPassword(this.state.user, (err, data) => { 23 | if (err || (data && !data.success)) { 24 | this.setState({ errors: data && data.errors ? data.errors : [err.message] }); 25 | } else if (data && data.success) { 26 | this.props.history.push('/'); 27 | } 28 | }); 29 | } 30 | 31 | render() { 32 | return ( 33 |
    34 | 40 |
    41 | ); 42 | } 43 | } 44 | ProfilePassword.propTypes = { 45 | user: PropTypes.shape({ 46 | name: PropTypes.string.isRequired, 47 | email: PropTypes.string.isRequired 48 | }).isRequired, 49 | history: PropTypes.shape({ 50 | push: PropTypes.func.isRequired 51 | }).isRequired 52 | }; 53 | 54 | export default ProfilePassword; 55 | -------------------------------------------------------------------------------- /src/jsx/pages/private/profile.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { NavLink } from 'react-router-dom'; 4 | 5 | import Utils from '../../common/utils'; 6 | import Auth from '../../modules/auth'; 7 | import UserService from '../../services/user-service'; 8 | 9 | import FormValidationErrors from '../../components/form-validation-errors'; 10 | import FormSubmitErrors from '../../components/form-submit-errors'; 11 | 12 | class Profile extends Component { 13 | constructor(props) { 14 | super(props); 15 | 16 | this.state = { 17 | errors: [], 18 | user: this.props.user, 19 | validation: { 20 | name: { 21 | valid: false, 22 | touched: false, 23 | message: 'Name is required' 24 | }, 25 | email: { 26 | valid: false, 27 | touched: false, 28 | message: 'Email is invalid' 29 | }, 30 | formValid: false 31 | } 32 | }; 33 | 34 | this.submit = this.submit.bind(this); 35 | this.changeInput = this.changeInput.bind(this); 36 | this.cancel = this.cancel.bind(this); 37 | } 38 | 39 | componentDidMount() { 40 | // Validate form after initial render 41 | Object.keys(this.state.user).forEach((key) => { 42 | this.validate(key, this.state.user[key]); 43 | }); 44 | 45 | Utils.focusFirstInput(); 46 | } 47 | 48 | submit(evt) { 49 | evt.preventDefault(); 50 | 51 | UserService.updateProfile(this.state.user, (err, data) => { 52 | if (err) { 53 | this.setState({ errors: data && data.errors ? data.errors : [err.message] }); 54 | } else if (data && data.success) { 55 | Auth.updateUser(this.state.user); 56 | this.props.history.goBack(); 57 | } 58 | }); 59 | } 60 | 61 | changeInput(evt) { 62 | const field = evt.target.name; 63 | const value = evt.target.value; 64 | const user = this.state.user; 65 | user[field] = value; 66 | 67 | this.setState({ 68 | errors: [], 69 | user: user // eslint-disable-line object-shorthand 70 | }); 71 | this.validate(field, value); 72 | } 73 | 74 | validate(field, value) { 75 | const validation = this.state.validation; 76 | if (validation[field]) validation[field].touched = true; 77 | 78 | switch (field) { 79 | case 'name': 80 | validation.name.valid = value.length > 0; 81 | break; 82 | case 'email': 83 | validation.email.valid = (/^([\w.%+-]+)@([\w-]+\.)+([\w]{2,})$/i).test(value); 84 | break; 85 | } 86 | 87 | validation.formValid = true; 88 | Object.keys(validation).forEach((key) => { 89 | if (typeof validation[key].valid === 'boolean' && !validation[key].valid) { 90 | validation.formValid = false; 91 | } 92 | }); 93 | 94 | this.setState({ validation: validation }); // eslint-disable-line object-shorthand 95 | } 96 | 97 | cancel() { 98 | this.props.history.goBack(); 99 | } 100 | 101 | render() { 102 | const validation = this.state.validation; 103 | return ( 104 |
    105 |
    106 |
    107 |

    Edit Profile

    108 |
    109 |
    110 |
    111 |
    112 |
    113 |
    114 | 115 |
    116 | 117 |
    118 |
    119 |
    120 | 121 |
    122 | 123 |
    124 |
    125 |
    126 |
    127 |
    128 | 129 | 132 |
    133 | 134 | 135 |
    136 |
    137 |
    138 |
    139 |
    140 | 141 | 142 |
    143 |
    144 |
    145 | ); 146 | } 147 | } 148 | Profile.propTypes = { 149 | user: PropTypes.shape({ 150 | name: PropTypes.string.isRequired, 151 | email: PropTypes.string.isRequired 152 | }).isRequired, 153 | history: PropTypes.shape({ 154 | goBack: PropTypes.func.isRequired 155 | }).isRequired 156 | }; 157 | 158 | export default Profile; 159 | -------------------------------------------------------------------------------- /src/jsx/pages/private/site-admin/user-delete.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import UserService from '../../../services/user-service'; 5 | import FormSubmitErrors from '../../../components/form-submit-errors'; 6 | 7 | class UserDelete extends Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | id: props.match.params.id, 13 | errors: [], 14 | user: { name: '', email: '', role: '' } 15 | }; 16 | 17 | this.delete = this.delete.bind(this); 18 | this.cancel = this.cancel.bind(this); 19 | } 20 | 21 | componentWillMount() { 22 | UserService.getUser(this.state.id, (err, data) => { 23 | if (data && data.success) { 24 | this.setState({ user: data.data }); 25 | } else if (err) { 26 | this.setState({ errors: [err.message] }); 27 | } 28 | }); 29 | } 30 | 31 | delete() { 32 | UserService.deleteUser(this.state.id, (err, data) => { 33 | if (err || (data && !data.success)) { 34 | this.setState({ errors: data && data.errors ? data.errors : [err.message] }); 35 | } else if (data && data.success) { 36 | this.props.history.goBack(); 37 | } 38 | }); 39 | } 40 | 41 | cancel() { 42 | this.props.history.goBack(); 43 | } 44 | 45 | render() { 46 | const user = this.state.user; 47 | return ( 48 |
    49 |
    50 |
    51 |
    52 |

    Delete User

    53 |
    54 |
    55 |
    56 |
    57 |
    58 |
    59 | Name 60 |
    {user.name}
    61 |
    62 |
    63 | Email 64 |
    {user.email}
    65 |
    66 |
    67 | Role 68 |
    {user.role}
    69 |
    70 |
    71 |
    72 | Are you sure you want to delete user?
    73 |
    74 | 75 | 76 |
    77 |
    78 |
    79 |
    80 |
    81 | 82 |
    83 |
    84 |
    85 | ); 86 | } 87 | } 88 | UserDelete.propTypes = { 89 | match: PropTypes.shape({ 90 | params: PropTypes.shape({ 91 | id: PropTypes.string.isRequired 92 | }) 93 | }).isRequired, 94 | history: PropTypes.shape({ 95 | goBack: PropTypes.func.isRequired 96 | }).isRequired 97 | }; 98 | 99 | export default UserDelete; 100 | -------------------------------------------------------------------------------- /src/jsx/pages/private/site-admin/user-edit.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { NavLink } from 'react-router-dom'; 4 | 5 | import Utils from '../../../common/utils'; 6 | import Roles from '../../../../shared/roles'; 7 | import UserService from '../../../services/user-service'; 8 | 9 | import FormValidationErrors from '../../../components/form-validation-errors'; 10 | import FormSubmitErrors from '../../../components/form-submit-errors'; 11 | 12 | class UserEdit extends Component { 13 | constructor(props) { 14 | super(props); 15 | 16 | this.state = { 17 | id: props.match.params.id, 18 | errors: [], 19 | user: { name: '', email: '' }, 20 | validation: { 21 | name: { 22 | valid: false, 23 | touched: false, 24 | message: 'Name is required' 25 | }, 26 | email: { 27 | valid: false, 28 | touched: false, 29 | message: 'Email is invalid' 30 | }, 31 | formValid: false 32 | }, 33 | isFetching: true 34 | }; 35 | 36 | this.submit = this.submit.bind(this); 37 | this.changeInput = this.changeInput.bind(this); 38 | this.cancel = this.cancel.bind(this); 39 | } 40 | 41 | componentWillMount() { 42 | UserService.getUser(this.state.id, (err, data) => { 43 | if (data && data.success) { 44 | this.setState({ user: data.data, isFetching: false }); 45 | 46 | // Validate form after inputs are loaded 47 | Object.keys(this.state.user).forEach((key) => { 48 | this.validate(key, this.state.user[key]); 49 | }); 50 | } else if (err) { 51 | this.setState({ errors: [err.message] }); 52 | } 53 | }); 54 | } 55 | 56 | componentDidUpdate(prevProps, prevState) { 57 | if (prevState.isFetching !== this.state.isFetching) { 58 | Utils.focusFirstInput(); 59 | } 60 | } 61 | 62 | submit(evt) { 63 | evt.preventDefault(); 64 | 65 | UserService.updateUser(this.state.user, (err, data) => { 66 | if (err || (data && !data.success)) { 67 | this.setState({ errors: data && data.errors ? data.errors : [err.message] }); 68 | } else if (data && data.success) { 69 | this.props.history.goBack(); 70 | } 71 | }); 72 | } 73 | 74 | changeInput(evt) { 75 | const field = evt.target.name; 76 | const value = evt.target.value; 77 | const user = this.state.user; 78 | user[field] = value; 79 | 80 | this.setState({ 81 | errors: [], 82 | user: user // eslint-disable-line object-shorthand 83 | }); 84 | this.validate(field, value); 85 | } 86 | 87 | validate(field, value) { 88 | const validation = this.state.validation; 89 | if (validation[field]) validation[field].touched = true; 90 | 91 | switch (field) { 92 | case 'name': 93 | validation.name.valid = value.length > 0; 94 | break; 95 | case 'email': 96 | validation.email.valid = (/^([\w.%+-]+)@([\w-]+\.)+([\w]{2,})$/i).test(value); 97 | break; 98 | } 99 | 100 | validation.formValid = true; 101 | Object.keys(validation).forEach((key) => { 102 | if (typeof validation[key].valid === 'boolean' && !validation[key].valid) { 103 | validation.formValid = false; 104 | } 105 | }); 106 | 107 | this.setState({ validation: validation }); // eslint-disable-line object-shorthand 108 | } 109 | 110 | cancel() { 111 | this.props.history.goBack(); 112 | } 113 | 114 | render() { 115 | const validation = this.state.validation; 116 | const roleOptions = []; 117 | Roles.map().forEach((role) => { 118 | roleOptions.push(); 119 | }); 120 | return ( 121 |
    122 |
    123 |
    124 |

    Edit Profile

    125 |
    126 |
    127 |
    128 |
    129 |
    130 |
    131 |
    132 | 133 |
    134 | 135 |
    136 |
    137 |
    138 | 139 |
    140 | 141 |
    142 |
    143 |
    144 | 145 |
    146 | 149 |
    150 |
    151 |
    152 |
    153 |
    154 | 155 | 158 |
    159 | 160 | 161 |
    162 |
    163 |
    164 |
    165 |
    166 |
    167 | 168 | 169 |
    170 |
    171 |
    172 | ); 173 | } 174 | } 175 | UserEdit.propTypes = { 176 | match: PropTypes.shape({ 177 | params: PropTypes.shape({ 178 | id: PropTypes.string.isRequired 179 | }) 180 | }).isRequired, 181 | history: PropTypes.shape({ 182 | goBack: PropTypes.func.isRequired 183 | }).isRequired 184 | }; 185 | 186 | export default UserEdit; 187 | -------------------------------------------------------------------------------- /src/jsx/pages/private/site-admin/user-password.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import UserService from '../../../services/user-service'; 5 | import PasswordChangeForm from '../../../components/password-change-form'; 6 | 7 | class UserPassword extends Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | id: props.match.params.id, 13 | errors: [], 14 | user: { name: '', email: '', password: '' }, 15 | isFetching: true 16 | }; 17 | 18 | this.submit = this.submit.bind(this); 19 | } 20 | 21 | componentWillMount() { 22 | UserService.getUser(this.state.id, (err, data) => { 23 | if (data && data.success) { 24 | this.setState({ user: data.data, isFetching: false }); 25 | } else if (err) { 26 | this.setState({ errors: [err.message] }); 27 | } 28 | }); 29 | } 30 | 31 | submit(evt) { 32 | evt.preventDefault(); 33 | 34 | UserService.adminUserPassword(this.state.user, (err, data) => { 35 | if (err || (data && !data.success)) { 36 | this.setState({ errors: data && data.errors ? data.errors : [err.message] }); 37 | } else if (data && data.success) { 38 | this.props.history.push('/admin/users'); 39 | } 40 | }); 41 | } 42 | 43 | render() { 44 | return ( 45 |
    46 | 52 |
    53 | ); 54 | } 55 | } 56 | UserPassword.propTypes = { 57 | match: PropTypes.shape({ 58 | params: PropTypes.shape({ 59 | id: PropTypes.string.isRequired 60 | }) 61 | }).isRequired, 62 | history: PropTypes.shape({ 63 | push: PropTypes.func.isRequired 64 | }).isRequired 65 | }; 66 | 67 | export default UserPassword; 68 | -------------------------------------------------------------------------------- /src/jsx/pages/private/site-admin/users.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { NavLink } from 'react-router-dom'; 4 | import ReactTable from 'react-table'; 5 | import Roles from '../../../../shared/roles'; 6 | import FormSubmitErrors from '../../../components/form-submit-errors'; 7 | import UserService from '../../../services/user-service'; 8 | 9 | class Users extends Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | errors: [], 15 | data: [], 16 | pages: null, 17 | loading: true 18 | }; 19 | 20 | this.fetchData = this.fetchData.bind(this); 21 | } 22 | 23 | fetchData(state, /* instance */) { 24 | 25 | let sort = ''; 26 | state.sorted.forEach((item) => { 27 | const dir = item.desc ? '-' : ''; 28 | sort += dir + item.id +' '; 29 | }); 30 | 31 | const query = `?page=${state.page+1}&limit=${state.pageSize}&sort=${sort}&filter=${JSON.stringify(state.filtered)}`; 32 | 33 | UserService.getUsers(query, (err, data) => { 34 | if (err) { 35 | this.setState({ errors: [err.message] }); 36 | } else { 37 | this.setState({ 38 | errors: [], 39 | data: data.docs, 40 | pages: data.pages, 41 | loading: false 42 | }); 43 | } 44 | }); 45 | } 46 | 47 | render() { 48 | const { data, pages, loading } = this.state; 49 | const role = this.props.role; 50 | const columns = [ 51 | { 52 | Header: 'Name', 53 | accessor: 'name' 54 | }, 55 | { 56 | Header: 'Email', 57 | accessor: 'email' 58 | }, 59 | { 60 | Header: 'role', 61 | accessor: 'role' 62 | }, 63 | { 64 | Header: 'CreatedAt', 65 | accessor: 'createdAt', 66 | filterable: false 67 | }, 68 | { 69 | Header: 'UpdatedAt', 70 | accessor: 'updatedAt', 71 | filterable: false 72 | } 73 | ]; 74 | if (role === Roles.siteAdmin) { 75 | columns.push( 76 | { 77 | Header: 'Action', 78 | accessor: 'id', 79 | sortable: false, 80 | filterable: false, 81 | /* eslint-disable arrow-body-style */ 82 | Cell: row => ( 83 |
    84 | 85 | 88 | 89 | 92 |
    93 | ) 94 | /* eslint-enable arrow-body-style */ 95 | } 96 | ); 97 | } 98 | 99 | return ( 100 |
    101 | 102 | 113 |
    114 | ); 115 | } 116 | } 117 | Users.propTypes = { 118 | role: PropTypes.string.isRequired 119 | }; 120 | 121 | export default Users; 122 | -------------------------------------------------------------------------------- /src/jsx/pages/public1.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; 3 | 4 | import MessageService from '../services/message-service'; 5 | 6 | class Public1 extends Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | message: '', 12 | error: '' 13 | }; 14 | 15 | this.componentWillMount = this.componentWillMount.bind(this); 16 | } 17 | 18 | componentWillMount() { 19 | MessageService.getPublicMessage1((err, data) => { 20 | if (err) { 21 | this.setState({ message: '', error: err }); 22 | } else { 23 | this.setState({ message: data.message, error: '' }); 24 | } 25 | }); 26 | } 27 | 28 | render() { 29 | const divs = []; 30 | if (this.state.message || this.state.error) { 31 | divs.push( 32 |
    33 | {this.state.message} 34 | {this.state.error} 35 |
    36 | ); 37 | } 38 | 39 | return ( 40 |
    41 |
    42 |
    43 |

    Public Page 1

    44 | 48 | {divs} 49 | 50 |
    51 |
    52 |
    53 |
    54 |
      55 |
    • React route
    • 56 |
        57 |
      • /public1
      • 58 |
      • No auth token or user role required
      • 59 |
      • See src/jsx/common/main-routes.jsx
      • 60 |
      61 |
    • Api route
    • 62 |
        63 |
      • /api/messages/public1
      • 64 |
      • No auth token or user role required
      • 65 |
      • 66 | See server/routes/api.js 67 | {/* eslint-disable max-len */} 68 | {/* eslint-disable react/no-unescaped-entities, react/jsx-no-comment-textnodes */} 69 |
        70 |                     // GET /api/messages/public1 
        71 | router.get('/messages/public1', messageController.getPublicMessage1); 72 |
        73 | {/* eslint-enable react/no-unescaped-entities, react/jsx-no-comment-textnodes, max-len */} 74 |
      • 75 |
      76 |
    77 |
    78 |
    79 |
    80 | ); 81 | } 82 | } 83 | 84 | export default Public1; 85 | -------------------------------------------------------------------------------- /src/jsx/pages/signin.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | import PropTypes from 'prop-types'; 4 | import Auth from '../modules/auth'; 5 | 6 | import Utils from '../common/utils'; 7 | import ModelValidations from '../../shared/model-validations'; 8 | import FormValidationErrors from '../components/form-validation-errors'; 9 | import FormSubmitErrors from '../components/form-submit-errors'; 10 | 11 | class SignIn extends Component { 12 | constructor(props) { 13 | super(props); 14 | 15 | this.state = { 16 | errors: [], 17 | user: { 18 | email: '', 19 | password: '' 20 | }, 21 | validation: { 22 | email: { 23 | valid: false, 24 | touched: false, 25 | message: ModelValidations.email.regex.message 26 | }, 27 | password: { 28 | valid: false, 29 | touched: false, 30 | message: ModelValidations.password.minLength.message 31 | }, 32 | formValid: false 33 | } 34 | }; 35 | 36 | this.submit = this.submit.bind(this); 37 | this.changeInput = this.changeInput.bind(this); 38 | } 39 | 40 | componentWillMount() { 41 | if (Auth.isAuthenticated()) { 42 | Auth.deauthenticateUser(); 43 | this.props.history.push('/signin'); // Trigger navbar re-render 44 | } 45 | } 46 | 47 | componentDidMount() { 48 | Utils.focusFirstInput(); 49 | } 50 | 51 | submit(evt) { 52 | evt.preventDefault(); 53 | 54 | // create a string for an HTTP body message 55 | const email = encodeURIComponent(this.state.user.email); 56 | const password = encodeURIComponent(this.state.user.password); 57 | const formData = `email=${email}&password=${password}`; 58 | 59 | // create an AJAX request 60 | const xhr = new XMLHttpRequest(); 61 | xhr.open('post', '/auth/login'); 62 | xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); 63 | xhr.responseType = 'json'; 64 | xhr.addEventListener('load', () => { 65 | if (xhr.status === 200) { 66 | 67 | this.setState({ errors: [] }); 68 | 69 | // Save the token 70 | Auth.authenticateUser(xhr.response.token, xhr.response.user); 71 | 72 | // Redirect to home page 73 | this.props.history.push('/'); 74 | } else { 75 | 76 | const errors = []; 77 | if (xhr.response && xhr.response.message) { 78 | errors.push(xhr.response.message); // Summary 79 | const errObj = xhr.response.errors ? xhr.response.errors : {}; 80 | Object.keys(errObj).forEach((key) => { 81 | errors.push(errObj[key]); 82 | }); 83 | } else { 84 | errors.push(`${xhr.status} ${xhr.statusText}`); 85 | } 86 | 87 | this.setState({ errors: errors }); // eslint-disable-line object-shorthand 88 | } 89 | }); 90 | xhr.send(formData); 91 | } 92 | 93 | changeInput(evt) { 94 | const field = evt.target.name; 95 | const value = evt.target.value; 96 | const user = this.state.user; 97 | user[field] = value; 98 | 99 | this.setState({ 100 | errors: [], 101 | user: user // eslint-disable-line object-shorthand 102 | }); 103 | this.validate(field, value); 104 | } 105 | 106 | validate(field, value) { 107 | const validation = this.state.validation; 108 | if (validation[field]) validation[field].touched = true; 109 | 110 | switch (field) { 111 | case 'email': 112 | validation.email.valid = (ModelValidations.email.regex.value).test(value); 113 | break; 114 | case 'password': 115 | validation.password.valid = value.length >= ModelValidations.password.minLength.value; 116 | break; 117 | } 118 | 119 | validation.formValid = true; 120 | Object.keys(validation).forEach((key) => { 121 | if (typeof validation[key].valid === 'boolean' && !validation[key].valid) { 122 | validation.formValid = false; 123 | } 124 | }); 125 | 126 | this.setState({ validation: validation }); // eslint-disable-line object-shorthand 127 | } 128 | 129 | render() { 130 | const validation = this.state.validation; 131 | return ( 132 |
    133 |
    134 |
    135 |

    Sign In

    136 |
    137 | Please enter email and password. Or Sign up for a new account 138 |
    139 |
    140 |
    141 |
    142 |
    143 |
    144 |
    145 | 146 |
    147 | 148 |
    149 |
    150 |
    151 | 152 |
    153 | 154 |
    155 |
    156 |
    157 |
    158 | 159 |
    160 |
    161 |
    162 |
    163 |
    164 | 165 | 166 |
    167 |
    168 |
    169 | ); 170 | } 171 | } 172 | SignIn.propTypes = { 173 | history: PropTypes.shape({ 174 | push: PropTypes.func.isRequired 175 | }).isRequired 176 | }; 177 | 178 | export default SignIn; 179 | -------------------------------------------------------------------------------- /src/jsx/pages/signup.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Utils from '../common/utils'; 5 | import ModelValidations from '../../shared/model-validations'; 6 | import FormValidationErrors from '../components/form-validation-errors'; 7 | import FormSubmitErrors from '../components/form-submit-errors'; 8 | 9 | class SignUp extends Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | errors: [], 15 | user: { 16 | name: '', 17 | email: '', 18 | password: '' 19 | }, 20 | validation: { 21 | name: { 22 | valid: false, 23 | touched: false, 24 | message: 'Name is required' 25 | }, 26 | email: { 27 | valid: false, 28 | touched: false, 29 | message: ModelValidations.email.regex.message 30 | }, 31 | password: { 32 | valid: false, 33 | touched: false, 34 | message: ModelValidations.password.minLength.message 35 | }, 36 | passwordConfirm: { 37 | valid: false, 38 | touched: false, 39 | message: 'Password confirm does not match.' 40 | }, 41 | formValid: false 42 | } 43 | }; 44 | 45 | this.submit = this.submit.bind(this); 46 | this.changeInput = this.changeInput.bind(this); 47 | } 48 | 49 | componentDidMount() { 50 | Utils.focusFirstInput(); 51 | } 52 | 53 | submit(evt) { 54 | evt.preventDefault(); 55 | 56 | // create a string for an HTTP body message 57 | const name = encodeURIComponent(this.state.user.name); 58 | const email = encodeURIComponent(this.state.user.email); 59 | const password = encodeURIComponent(this.state.user.password); 60 | const formData = `name=${name}&email=${email}&password=${password}`; 61 | 62 | const xhr = new XMLHttpRequest(); 63 | xhr.open('post', '/auth/signup'); 64 | xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); 65 | xhr.responseType = 'json'; 66 | xhr.addEventListener('load', () => { 67 | if (xhr.status === 200) { 68 | 69 | this.setState({ errors: [] }); 70 | 71 | // Redirect to login 72 | this.props.history.push('/signin'); 73 | } else { 74 | 75 | const errors = []; 76 | if (xhr.response && xhr.response.message) { 77 | errors.push(xhr.response.message); // Summary 78 | const errObj = xhr.response.errors ? xhr.response.errors : {}; 79 | Object.keys(errObj).forEach((key) => { 80 | errors.push(errObj[key]); 81 | }); 82 | } else { 83 | errors.push(`${xhr.status} ${xhr.statusText}`); 84 | } 85 | 86 | this.setState({ errors: errors }); // eslint-disable-line object-shorthand 87 | } 88 | }); 89 | xhr.send(formData); 90 | } 91 | 92 | changeInput(evt) { 93 | const field = evt.target.name; 94 | const value = evt.target.value; 95 | const user = this.state.user; 96 | user[field] = value; 97 | 98 | this.setState({ 99 | errors: [], 100 | user: user // eslint-disable-line object-shorthand 101 | }); 102 | this.validate(field, value); 103 | } 104 | 105 | validate(field, value) { 106 | const validation = this.state.validation; 107 | if (validation[field]) validation[field].touched = true; 108 | 109 | switch (field) { 110 | case 'name': 111 | validation.name.valid = value.length > 0; 112 | break; 113 | case 'email': 114 | validation.email.valid = (ModelValidations.email.regex.value).test(value); 115 | break; 116 | case 'password': 117 | validation.password.valid = value.length >= ModelValidations.password.minLength.value; 118 | break; 119 | case 'passwordConfirm': 120 | validation.passwordConfirm.valid = value === this.state.user.password; 121 | break; 122 | } 123 | 124 | validation.formValid = true; 125 | Object.keys(validation).forEach((key) => { 126 | if (typeof validation[key].valid === 'boolean' && !validation[key].valid) { 127 | validation.formValid = false; 128 | } 129 | }); 130 | 131 | this.setState({ validation: validation }); // eslint-disable-line object-shorthand 132 | } 133 | 134 | render() { 135 | const validation = this.state.validation; 136 | return ( 137 |
    138 |
    139 |
    140 |

    Sign Up

    141 |
    142 |
    143 |
    144 |
    145 |
    146 |
    147 | 148 |
    149 | 150 |
    151 |
    152 |
    153 | 154 |
    155 | 156 |
    157 |
    158 |
    159 | 160 |
    161 | 162 |
    163 |
    164 |
    165 | 166 |
    167 | 168 |
    169 |
    170 |
    171 |
    172 | 173 |
    174 |
    175 |
    176 |
    177 |
    178 | 179 | 180 |
    181 |
    182 |
    183 | ); 184 | } 185 | } 186 | SignUp.propTypes = { 187 | history: PropTypes.shape({ 188 | push: PropTypes.func.isRequired 189 | }).isRequired 190 | }; 191 | 192 | export default SignUp; 193 | -------------------------------------------------------------------------------- /src/jsx/services/message-service.js: -------------------------------------------------------------------------------- 1 | import Request from './request'; 2 | 3 | const MessageService = { 4 | 5 | /** 6 | * Get public message 1 7 | * @param {function} callback (err, data) 8 | The function that is called after a service call 9 | error {object}: null if no error 10 | data {object}: The data set of a succesful call 11 | */ 12 | getPublicMessage1: (callback) => { 13 | if (!$.isFunction(callback)) throw new Error('callback function is required'); 14 | Request.get('/api/messages/public1', callback); 15 | }, 16 | 17 | /** 18 | * Get private message 1 19 | * @param {function} callback (err, data) 20 | The function that is called after a service call 21 | error {object}: null if no error 22 | data {object}: The data set of a succesful call 23 | */ 24 | getPrivateMessage1: (callback) => { 25 | if (!$.isFunction(callback)) throw new Error('callback function is required'); 26 | Request.get('/api/messages/private1', callback); 27 | }, 28 | 29 | /** 30 | * Get admin message 1 31 | * @param {function} callback (err, data) 32 | The function that is called after a service call 33 | error {object}: null if no error 34 | data {object}: The data set of a succesful call 35 | */ 36 | getAdminMessage1: (callback) => { 37 | if (!$.isFunction(callback)) throw new Error('callback function is required'); 38 | Request.get('/api/messages/admin1', callback); 39 | } 40 | 41 | }; 42 | 43 | export default MessageService; 44 | -------------------------------------------------------------------------------- /src/jsx/services/request.js: -------------------------------------------------------------------------------- 1 | import Auth from '../modules/auth'; 2 | 3 | const Request = 4 | (function () { 5 | 6 | function setHeader(xhr) { 7 | xhr.setRequestHeader('Authorization', `bearer ${Auth.getToken()}`); 8 | } 9 | 10 | return { 11 | 12 | /** 13 | * Ajax get request 14 | * 15 | * @param {string} url 16 | * @param {function} callback (err, data) 17 | The function that is called after a service call 18 | error {object}: null if no error 19 | data {object}: The data set of a succesful call 20 | */ 21 | get: (url, callback) => { 22 | $.ajax({ 23 | url, // :url 24 | type: 'GET', 25 | dataType: 'json', 26 | beforeSend: setHeader 27 | }).done((result) => { 28 | return callback(null, result); 29 | }).fail((jqxhr, textStatus, error) => { 30 | return callback(error, jqxhr && jqxhr.responseJSON ? jqxhr.responseJSON : null); 31 | }); 32 | }, 33 | 34 | /** 35 | * Ajax delete request 36 | * 37 | * @param {string} url 38 | * @param {function} callback (err, data) 39 | The function that is called after a service call 40 | error {object}: null if no error 41 | data {object}: The data set of a succesful call 42 | */ 43 | delete: (url, callback) => { 44 | $.ajax({ 45 | url, // :url 46 | type: 'DELETE', 47 | dataType: 'json', 48 | beforeSend: setHeader 49 | }).done((result) => { 50 | return callback(null, result); 51 | }).fail((jqxhr, textStatus, error) => { 52 | return callback(error, jqxhr && jqxhr.responseJSON ? jqxhr.responseJSON : null); 53 | }); 54 | }, 55 | 56 | /** 57 | * Ajax put request 58 | * 59 | * @param {string} url 60 | * @param {string} data Stringified json data 61 | * @param {function} callback (err, data) 62 | The function that is called after a service call 63 | error {object}: null if no error 64 | data {object}: The data set of a succesful call 65 | */ 66 | put: (url, data, callback) => { 67 | $.ajax({ 68 | url, // :url 69 | type: 'PUT', 70 | data, // :data 71 | contentType: 'application/json; charset=utf-8', 72 | dataType: 'json', 73 | beforeSend: setHeader 74 | }).done((result) => { 75 | return callback(null, result); 76 | }).fail((jqxhr, textStatus, error) => { 77 | return callback(error, jqxhr && jqxhr.responseJSON ? jqxhr.responseJSON : null); 78 | }); 79 | }, 80 | 81 | /** 82 | * Ajax post request 83 | * 84 | * @param {string} url 85 | * @param {string} data Stringified json data 86 | * @param {function} callback (err, data) 87 | The function that is called after a service call 88 | error {object}: null if no error 89 | data {object}: The data set of a succesful call 90 | */ 91 | post: (url, data, callback) => { 92 | $.ajax({ 93 | url, // :url 94 | type: 'POST', 95 | data, // :data 96 | contentType: 'application/json; charset=utf-8', 97 | dataType: 'json', 98 | beforeSend: setHeader 99 | }).done((result) => { 100 | return callback(null, result); 101 | }).fail((jqxhr, textStatus, error) => { 102 | return callback(error, jqxhr && jqxhr.responseJSON ? jqxhr.responseJSON : null); 103 | }); 104 | } 105 | }; 106 | }()); 107 | 108 | export default Request; 109 | -------------------------------------------------------------------------------- /src/jsx/services/user-service.js: -------------------------------------------------------------------------------- 1 | import Request from './request'; 2 | 3 | const UserService = { 4 | 5 | /** 6 | * Get Users 7 | * 8 | * @param {string} query query string with paging options 9 | * @param {function} callback (err, data) 10 | The function that is called after a service call 11 | error {object}: null if no error 12 | data {object}: The data set of a succesful call 13 | */ 14 | getUsers: (query, callback) => { 15 | if (!$.isFunction(callback)) throw new Error('callback function is required'); 16 | Request.get(`/api/users${query}`, callback); 17 | }, 18 | 19 | /** 20 | * Get User by id 21 | * 22 | * @param {string} id user id 23 | * @param {function} callback (err, data) 24 | The function that is called after a service call 25 | error {object}: null if no error 26 | data {object}: The data set of a succesful call 27 | */ 28 | getUser: (id, callback) => { 29 | if (!$.isFunction(callback)) throw new Error('callback function is required'); 30 | Request.get(`/api/users/${id}`, callback); 31 | }, 32 | 33 | /** 34 | * Delete a user 35 | * 36 | * @param {string} id user id 37 | * @param {function} callback (err, data) 38 | The function that is called after a service call 39 | error {object}: null if no error 40 | data {object}: The data set of a succesful call 41 | */ 42 | deleteUser: (id, callback) => { 43 | if (!$.isFunction(callback)) throw new Error('callback function is required'); 44 | Request.delete(`/api/users/${id}`, callback); 45 | }, 46 | 47 | /** 48 | * Update a user 49 | * 50 | * @param {object} user user object to update 51 | * @param {function} callback (err, data) 52 | The function that is called after a service call 53 | error {object}: null if no error 54 | data {object}: The data set of a succesful call 55 | */ 56 | updateUser: (user, callback) => { 57 | if (!$.isFunction(callback)) throw new Error('callback function is required'); 58 | Request.put('/api/users', JSON.stringify({ user /* :user */ }), callback); 59 | }, 60 | 61 | /** 62 | * Update a user profile 63 | * 64 | * @param {object} user user object to update 65 | * @param {function} callback (err, data) 66 | The function that is called after a service call 67 | error {object}: null if no error 68 | data {object}: The data set of a succesful call 69 | */ 70 | updateProfile: (user, callback) => { 71 | if (!$.isFunction(callback)) throw new Error('callback function is required'); 72 | Request.put('/api/users/profile', JSON.stringify({ user /* :user */ }), callback); 73 | }, 74 | 75 | /** 76 | * User changes thier own profile password 77 | * 78 | * @param {object} user user object to update 79 | * @param {function} callback (err, data) 80 | The function that is called after a service call 81 | error {object}: null if no error 82 | data {object}: The data set of a succesful call 83 | */ 84 | profileUserPassword: (user, callback) => { 85 | if (!$.isFunction(callback)) throw new Error('callback function is required'); 86 | Request.put('/api/users/profile/password', JSON.stringify({ user /* :user */ }), callback); 87 | }, 88 | 89 | /** 90 | * Admin change user password 91 | * 92 | * @param {object} user user object to update 93 | * @param {function} callback (err, data) 94 | The function that is called after a service call 95 | error {object}: null if no error 96 | data {object}: The data set of a succesful call 97 | */ 98 | adminUserPassword: (user, callback) => { 99 | if (!$.isFunction(callback)) throw new Error('callback function is required'); 100 | Request.put('/api/users/password', JSON.stringify({ user /* :user */ }), callback); 101 | } 102 | }; 103 | 104 | export default UserService; 105 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Boilerplate Role Based Permissions Nodejs", 3 | "short_name": "Boilerplate", 4 | "icons": [{ 5 | "src": "/img/ico/icon-128x128.png", 6 | "sizes": "128x128", 7 | "type": "image/png" 8 | }, { 9 | "src": "/img/ico/apple-touch-icon.png", 10 | "sizes": "180x180", 11 | "type": "image/png" 12 | }, { 13 | "src": "/img/ico/mstile-150x150.png", 14 | "sizes": "150x150", 15 | "type": "image/png" 16 | }, { 17 | "src": "/img/ico/chrome-touch-icon-192x192.png", 18 | "sizes": "192x192", 19 | "type": "image/png" 20 | }], 21 | "start_url": "/index.html", 22 | "display": "standalone", 23 | "theme_color": "#263238" 24 | } 25 | -------------------------------------------------------------------------------- /src/manifest.webapp: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "name": "Boilerplate Role Based Permissions Nodejs", 4 | "launch_path": "/index.html", 5 | "description": "Boilerplate role based permissions, local passport authentication", 6 | "icons": { 7 | "128": "/img/ico/icon-128x128.png" 8 | }, 9 | "developer": { 10 | "name": "", 11 | "url": "" 12 | }, 13 | "installs_allowed_from": [ 14 | "*" 15 | ], 16 | "default_locale": "en", 17 | "permissions": { 18 | }, 19 | "locales": { 20 | "en": { 21 | "name": "Boilerplate Role Based Permissions Nodejs", 22 | "description": "Boilerplate role based permissions, local passport authentication" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/robots.txt: -------------------------------------------------------------------------------- 1 | # www.robotstxt.org/ 2 | 3 | # Allow crawling of all content 4 | User-agent: * 5 | Disallow: 6 | -------------------------------------------------------------------------------- /src/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin border-radius($radius) { 2 | -webkit-border-radius: $radius; 3 | -moz-border-radius: $radius; 4 | -ms-border-radius: $radius; 5 | border-radius: $radius; 6 | } 7 | -------------------------------------------------------------------------------- /src/scss/bootstrap-overrides.scss: -------------------------------------------------------------------------------- 1 | /* Overriding bootstrap defaults */ 2 | 3 | .navbar-nav > li > a.active { 4 | background-color: #e7e7e7; 5 | font-weight: bold; 6 | } 7 | 8 | .dropdown-menu { 9 | min-width: 60px !important; 10 | } 11 | -------------------------------------------------------------------------------- /src/scss/margin-padding.scss: -------------------------------------------------------------------------------- 1 | /* MARGINS & PADDINGS */ 2 | .p-xxs { 3 | padding: 5px !important; 4 | } 5 | .p-xs { 6 | padding: 10px !important; 7 | } 8 | .p-sm { 9 | padding: 15px !important; 10 | } 11 | .p-m { 12 | padding: 20px !important; 13 | } 14 | .p-md { 15 | padding: 25px !important; 16 | } 17 | .p-lg { 18 | padding: 30px !important; 19 | } 20 | .p-xl { 21 | padding: 40px !important; 22 | } 23 | .m-xxs { 24 | margin: 2px 4px; 25 | } 26 | .m-xs { 27 | margin: 5px; 28 | } 29 | .m-sm { 30 | margin: 10px; 31 | } 32 | .m { 33 | margin: 15px; 34 | } 35 | .m-md { 36 | margin: 20px; 37 | } 38 | .m-lg { 39 | margin: 30px; 40 | } 41 | .m-xl { 42 | margin: 50px; 43 | } 44 | .m-n { 45 | margin: 0 !important; 46 | } 47 | .m-l-none { 48 | margin-left: 0; 49 | } 50 | .m-l-xs { 51 | margin-left: 5px; 52 | } 53 | .m-l-sm { 54 | margin-left: 10px; 55 | } 56 | .m-l { 57 | margin-left: 15px; 58 | } 59 | .m-l-md { 60 | margin-left: 20px; 61 | } 62 | .m-l-lg { 63 | margin-left: 30px; 64 | } 65 | .m-l-xl { 66 | margin-left: 40px; 67 | } 68 | .m-l-n-xxs { 69 | margin-left: -1px; 70 | } 71 | .m-l-n-xs { 72 | margin-left: -5px; 73 | } 74 | .m-l-n-sm { 75 | margin-left: -10px; 76 | } 77 | .m-l-n { 78 | margin-left: -15px; 79 | } 80 | .m-l-n-md { 81 | margin-left: -20px; 82 | } 83 | .m-l-n-lg { 84 | margin-left: -30px; 85 | } 86 | .m-l-n-xl { 87 | margin-left: -40px; 88 | } 89 | .m-t-none { 90 | margin-top: 0; 91 | } 92 | .m-t-xxs { 93 | margin-top: 1px; 94 | } 95 | .m-t-xs { 96 | margin-top: 5px; 97 | } 98 | .m-t-sm { 99 | margin-top: 10px; 100 | } 101 | .m-t { 102 | margin-top: 15px; 103 | } 104 | .m-t-md { 105 | margin-top: 20px; 106 | } 107 | .m-t-lg { 108 | margin-top: 30px; 109 | } 110 | .m-t-xl { 111 | margin-top: 40px; 112 | } 113 | .m-t-xxl { 114 | margin-top: 50px; 115 | } 116 | .m-t-xxxl { 117 | margin-top: 60px; 118 | } 119 | .m-t-n-xxs { 120 | margin-top: -1px; 121 | } 122 | .m-t-n-xs { 123 | margin-top: -5px; 124 | } 125 | .m-t-n-sm { 126 | margin-top: -10px; 127 | } 128 | .m-t-n { 129 | margin-top: -15px; 130 | } 131 | .m-t-n-md { 132 | margin-top: -20px; 133 | } 134 | .m-t-n-lg { 135 | margin-top: -30px; 136 | } 137 | .m-t-n-xl { 138 | margin-top: -40px; 139 | } 140 | .m-r-none { 141 | margin-right: 0; 142 | } 143 | .m-r-xxs { 144 | margin-right: 1px; 145 | } 146 | .m-r-xs { 147 | margin-right: 5px; 148 | } 149 | .m-r-sm { 150 | margin-right: 10px; 151 | } 152 | .m-r { 153 | margin-right: 15px; 154 | } 155 | .m-r-md { 156 | margin-right: 20px; 157 | } 158 | .m-r-lg { 159 | margin-right: 30px; 160 | } 161 | .m-r-xl { 162 | margin-right: 40px; 163 | } 164 | .m-r-n-xxs { 165 | margin-right: -1px; 166 | } 167 | .m-r-n-xs { 168 | margin-right: -5px; 169 | } 170 | .m-r-n-sm { 171 | margin-right: -10px; 172 | } 173 | .m-r-n { 174 | margin-right: -15px; 175 | } 176 | .m-r-n-md { 177 | margin-right: -20px; 178 | } 179 | .m-r-n-lg { 180 | margin-right: -30px; 181 | } 182 | .m-r-n-xl { 183 | margin-right: -40px; 184 | } 185 | .m-b-none { 186 | margin-bottom: 0; 187 | } 188 | .m-b-xxs { 189 | margin-bottom: 1px; 190 | } 191 | .m-b-xs { 192 | margin-bottom: 5px; 193 | } 194 | .m-b-sm { 195 | margin-bottom: 10px; 196 | } 197 | .m-b { 198 | margin-bottom: 15px; 199 | } 200 | .m-b-md { 201 | margin-bottom: 20px; 202 | } 203 | .m-b-lg { 204 | margin-bottom: 30px; 205 | } 206 | .m-b-xl { 207 | margin-bottom: 40px; 208 | } 209 | .m-b-n-xxs { 210 | margin-bottom: -1px; 211 | } 212 | .m-b-n-xs { 213 | margin-bottom: -5px; 214 | } 215 | .m-b-n-sm { 216 | margin-bottom: -10px; 217 | } 218 | .m-b-n { 219 | margin-bottom: -15px; 220 | } 221 | .m-b-n-md { 222 | margin-bottom: -20px; 223 | } 224 | .m-b-n-lg { 225 | margin-bottom: -30px; 226 | } 227 | .m-b-n-xl { 228 | margin-bottom: -40px; 229 | } 230 | .space-15 { 231 | margin: 15px 0; 232 | } 233 | .space-20 { 234 | margin: 20px 0; 235 | } 236 | .space-25 { 237 | margin: 25px 0; 238 | } 239 | .space-30 { 240 | margin: 30px 0; 241 | } 242 | -------------------------------------------------------------------------------- /src/scss/site-branding.scss: -------------------------------------------------------------------------------- 1 | 2 | .branding-table-user-list { 3 | background-color: #FFF; 4 | } 5 | -------------------------------------------------------------------------------- /src/scss/style.scss: -------------------------------------------------------------------------------- 1 | @import '_mixins'; 2 | @import 'bootstrap-overrides'; 3 | @import 'margin-padding'; 4 | @import 'site-branding'; 5 | 6 | body { 7 | font-family: Circular,"Helvetica Neue",Helvetica,Arial,sans-serif; 8 | font-size: 14px; 9 | line-height: 1.43; 10 | color: #484848; 11 | } 12 | 13 | .form-inline { 14 | .form-group { 15 | margin-right: 5px; 16 | } 17 | } 18 | 19 | .modal-content { 20 | .modal-footer { 21 | @include border-radius(6px); 22 | background-color: #f5f5f5; 23 | } 24 | } 25 | 26 | .form-header { 27 | h4 { 28 | font-weight: bold; 29 | } 30 | text-align: center; 31 | padding-bottom: 10px; 32 | } 33 | 34 | /** 35 | * Fade in/out transitions 36 | */ 37 | .transition-enter { 38 | opacity: 0.01; 39 | } 40 | .transition-enter.transition-enter-active { 41 | opacity: 1; 42 | transition: 700ms; 43 | } 44 | .example-leave { 45 | opacity: 1; 46 | } 47 | .transition-leave.transition-leave-active { 48 | opacity: 0.01; 49 | transition: 700ms; 50 | } 51 | 52 | // React Table (react-table) tweaks 53 | // Fixed table body not displaying in Chrome etc. 54 | // and the paging buttons filling up height 100% 55 | .ReactTable { 56 | display: block !important; 57 | .-pagination .-btn { 58 | height: auto !important; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/shared/model-validations.js: -------------------------------------------------------------------------------- 1 | // Model property validations 2 | 3 | module.exports = { 4 | password: { 5 | minLength: { 6 | value: 8, 7 | message: 'Password must be a mininum of 8 characters' 8 | } 9 | }, 10 | email: { 11 | regex: { 12 | value: /^([\w.%+-]+)@([\w-]+\.)+([\w]{2,})$/i, 13 | message: 'Email is not valid.' 14 | } 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/shared/roles.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable key-spacing */ 2 | 3 | /** 4 | * Role permissions enum 5 | * Shared between client and server side code base 6 | */ 7 | const Roles = { 8 | siteAdmin: 'SiteAdmin', 9 | admin: 'Admin', 10 | user: 'User', 11 | 12 | // eslint-disable-next-line object-shorthand 13 | map: () => { 14 | return [ 15 | { value: Roles.user }, 16 | { value: Roles.admin }, 17 | { value: Roles.siteAdmin } 18 | ]; 19 | }, 20 | 21 | isValidRole: (role) => { 22 | let valid = false; 23 | Roles.map().forEach((item) => { 24 | if (item.value === role) valid = true; 25 | }); 26 | return valid; 27 | } 28 | }; 29 | 30 | module.exports = Roles; 31 | -------------------------------------------------------------------------------- /webpack.common.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack common/base config 3 | */ 4 | 5 | const path = require('path'); 6 | const webpack = require('webpack'); 7 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 8 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 9 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 11 | 12 | module.exports = { 13 | context: path.resolve('./src'), 14 | // entry: './jsx/main.jsx', 15 | entry: './jsx/index.jsx', 16 | output: { 17 | path: path.resolve('./dist/'), 18 | filename: 'js/main.js', 19 | publicPath: '/' 20 | }, 21 | resolve: { 22 | extensions: ['.js', '.jsx'] 23 | }, 24 | devtool: 'source-map', 25 | module: { 26 | loaders: [ 27 | { 28 | // test: /\.js$|\.jsx$/, 29 | test: /\.jsx?$/, // Both .js and .jsx 30 | enforce: 'pre', 31 | exclude: /node_modules/, 32 | loader: 'eslint-loader' 33 | // options: { 34 | // eslintPath: path.join(__dirname, '.eslintrc.js'), 35 | // // emitWarning: true, 36 | // // emitError: true, 37 | // // failOnWarning: true, 38 | // // failOnError: true 39 | // } 40 | }, 41 | { 42 | test: /\.js$/, 43 | loader: 'babel-loader', 44 | exclude: /node_modules/, 45 | query: { presets: ['es2015'] } 46 | }, 47 | { 48 | test: /\.jsx$/, 49 | loader: 'babel-loader', 50 | query: { presets: ['es2015', 'react'] } 51 | }, 52 | 53 | { 54 | test: /\.css$/, 55 | use: ExtractTextPlugin.extract({ 56 | use: ['css-loader'] 57 | }) 58 | }, 59 | 60 | { 61 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, 62 | loader: 'url-loader?limit=10000&mimetype=application/font-woff&name=fonts/[name].[ext]' 63 | }, 64 | { 65 | test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, 66 | loader: 'file-loader?name=fonts/[name].[ext]' 67 | }, 68 | { 69 | test: /\.(jpe?g|png|gif)$/, 70 | loader: 'file-loader?name=img/[name].[ext]' 71 | }, 72 | { 73 | test: /\.html$/, 74 | loader: 'html-loader' 75 | } 76 | ] 77 | }, 78 | plugins: [ 79 | new CleanWebpackPlugin(['dist'], 80 | { 81 | verbose: true 82 | // exclude: ['img/**/*'] // <- exclude does not work 83 | } 84 | ), 85 | new CopyWebpackPlugin([ 86 | { from: './manifest.json' }, 87 | { from: './manifest.webapp' }, 88 | { from: './robots.txt' }, 89 | { from: './favicon.ico' }, 90 | { from: './img/**/*', to: './' }, 91 | { from: '../node_modules/react-table/react-table.css', to: './css' } 92 | ]), 93 | new webpack.ProvidePlugin({ 94 | $: 'jquery', 95 | jQuery: 'jquery' 96 | }), 97 | new HtmlWebpackPlugin({ 98 | template: './index.html', 99 | 100 | // true is default. True will automatically inject 101 | // the built.js script into the html 102 | inject: false 103 | }), 104 | new ExtractTextPlugin({ 105 | filename: 'css/style.css', 106 | allChunks: true 107 | }) 108 | ] 109 | }; 110 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack development config 3 | */ 4 | 5 | /* eslint-disable import/no-extraneous-dependencies */ 6 | /* eslint-disable global-require */ 7 | 8 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 9 | // const BrowserSyncPlugin = require('browser-sync-webpack-plugin'); 10 | 11 | const Merge = require('webpack-merge'); 12 | const CommonConfig = require('./webpack.common.config.js'); 13 | 14 | module.exports = Merge(CommonConfig, { 15 | module: { 16 | loaders: [ 17 | { 18 | test: /\.scss$/, 19 | use: ExtractTextPlugin.extract({ 20 | fallback: 'style-loader', 21 | use: [ 22 | { 23 | loader: 'css-loader', 24 | options: { 25 | sourceMap: true, 26 | minimize: false, 27 | discardComments: { 28 | removeAll: false 29 | } 30 | } 31 | }, 32 | { 33 | loader: 'postcss-loader', 34 | options: { 35 | plugins: () => [require('autoprefixer')] 36 | } 37 | }, 38 | { 39 | // options: { sourceMap: true }, 40 | loader: 'sass-loader' 41 | } 42 | ] 43 | }) 44 | } 45 | ] 46 | }, 47 | plugins: [ 48 | // new BrowserSyncPlugin({ 49 | // server: { 50 | // baseDir: ['dist'] 51 | // }, 52 | // port: 3000, 53 | // host: 'localhost', 54 | // open: false, 55 | 56 | // // Set to true if you want this served up on external ip, 57 | // // however, requires network connection and hangs when set to true 58 | // online: false 59 | // }) 60 | ] 61 | }); 62 | -------------------------------------------------------------------------------- /webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack production config 3 | */ 4 | 5 | /* eslint-disable import/no-extraneous-dependencies */ 6 | /* eslint-disable global-require */ 7 | 8 | const webpack = require('webpack'); 9 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 10 | 11 | const Merge = require('webpack-merge'); 12 | const CommonConfig = require('./webpack.common.config.js'); 13 | 14 | module.exports = Merge(CommonConfig, { 15 | module: { 16 | loaders: [ 17 | { 18 | test: /\.scss$/, 19 | use: ExtractTextPlugin.extract({ 20 | fallback: 'style-loader', 21 | use: [ 22 | { 23 | loader: 'css-loader', 24 | options: { 25 | sourceMap: true, 26 | minimize: true, 27 | discardComments: { 28 | removeAll: true 29 | } 30 | } 31 | }, 32 | { 33 | loader: 'postcss-loader', 34 | options: { 35 | plugins: () => [require('autoprefixer')] 36 | } 37 | }, 38 | { 39 | // options: { sourceMap: true }, 40 | loader: 'sass-loader' 41 | } 42 | ] 43 | }) 44 | } 45 | ] 46 | }, 47 | plugins: [ 48 | new webpack.LoaderOptionsPlugin({ 49 | minimize: true, 50 | debug: false 51 | }), 52 | 53 | // When using new UglifyJsPlugin and --opimize-minimize (or -p) the 54 | // UglifyJsPlugin is running twice. Omit the CLI option. 55 | // https://github.com/webpack/webpack/issues/1385 56 | new webpack.optimize.UglifyJsPlugin({ 57 | sourceMap: true, // Default is false 58 | beautify: false, 59 | mangle: { 60 | screw_ie8: true, 61 | keep_fnames: true 62 | }, 63 | compress: { 64 | drop_console: true, 65 | screw_ie8: true 66 | }, 67 | comments: false 68 | }) 69 | ] 70 | }); 71 | --------------------------------------------------------------------------------