├── .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 | 
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 |
43 |
44 |
45 |
49 |
50 |
Boilerplate Role Based Permissions
51 |
52 |
Server
53 |
54 | - Node.js
55 | - Express 4
56 | - Passport local authentication strategy
57 |
58 | - JsonWebToken
59 | - Authorized token required for api routes, using middelware authorization check
60 |
61 | - MongoDB/Mongoose
62 |
63 |
Client JS
64 |
65 | - Webpack
66 |
69 | - React 15.6
70 |
71 | - React Router 4
72 |
73 | - Private and Public react routes
74 |
75 |
76 | - ES6
77 |
78 |
Client Styles
79 |
80 | - Bootstrap 3.3.7
81 | - Font Awesome 4.6.3
82 | - Sass
83 |
84 |
85 |
86 |
89 |
90 |
91 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
86 | Edit
87 |
88 |
89 |
90 | Delete
91 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------