├── .gitignore
├── client
├── .babelrc
├── .eslintignore
├── .eslintrc
├── .gitignore
├── dist
│ ├── index.html
│ ├── scripts
│ │ ├── app.ae462822222611de71ef.js
│ │ └── app.ae462822222611de71ef.js.map
│ └── styles
│ │ ├── app.ae462822222611de71ef.css
│ │ └── app.ae462822222611de71ef.css.map
├── package.json
├── server.js
├── src
│ ├── app
│ │ ├── actions
│ │ │ ├── auth.js
│ │ │ ├── resetPassword.js
│ │ │ ├── types
│ │ │ │ └── index.js
│ │ │ └── users.js
│ │ ├── components
│ │ │ ├── App.jsx
│ │ │ ├── auth
│ │ │ │ ├── Signin.jsx
│ │ │ │ ├── Signout.jsx
│ │ │ │ ├── Signup.jsx
│ │ │ │ ├── SignupVerify.jsx
│ │ │ │ └── VerifyEmail.jsx
│ │ │ ├── bundle.scss
│ │ │ ├── common
│ │ │ │ ├── Footer.jsx
│ │ │ │ ├── Header.jsx
│ │ │ │ ├── footer.scss
│ │ │ │ ├── header.scss
│ │ │ │ └── styles
│ │ │ │ │ ├── form.scss
│ │ │ │ │ ├── global.scss
│ │ │ │ │ ├── mixins.scss
│ │ │ │ │ ├── normalize.scss
│ │ │ │ │ └── variables.scss
│ │ │ ├── hoc
│ │ │ │ ├── RequireAuth.js
│ │ │ │ └── RequireNotAuth.js
│ │ │ ├── resetPassword
│ │ │ │ ├── ResetPassword.jsx
│ │ │ │ ├── ResetPasswordNew.jsx
│ │ │ │ └── ResetPasswordVerify.jsx
│ │ │ └── users
│ │ │ │ ├── UserList.jsx
│ │ │ │ └── users.scss
│ │ ├── config.js
│ │ ├── index.js
│ │ ├── reducers
│ │ │ ├── authReducer.js
│ │ │ ├── index.js
│ │ │ ├── resetPasswordReducer.js
│ │ │ └── userReducer.js
│ │ └── routes.js
│ └── index.html
├── static
│ └── images
│ │ ├── github.png
│ │ └── screenshot.png
├── test
│ ├── components
│ │ └── app_test.js
│ └── test_helper.js
└── webpack
│ ├── webpack-dev.config.js
│ ├── webpack-prod.config.js
│ └── webpack.config.js
├── readme.md
└── server
├── .babelrc
├── .eslintrc
├── .gitignore
├── dist
├── config
│ └── example.index.js
├── controllers
│ ├── authController.js
│ ├── resetPasswordController.js
│ └── usersController.js
├── helpers
│ ├── email.js
│ └── token.js
├── index.js
├── models
│ └── user.js
├── router.js
└── services
│ └── passport.js
├── package.json
└── src
├── config
└── example.index.js
├── controllers
├── authController.js
├── resetPasswordController.js
└── usersController.js
├── helpers
├── email.js
└── token.js
├── index.js
├── models
└── user.js
├── router.js
└── services
└── passport.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
2 | .DS_Store
3 | npm-debug.log
--------------------------------------------------------------------------------
/client/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react", "es2015", "stage-1"]
3 | }
4 |
--------------------------------------------------------------------------------
/client/.eslintignore:
--------------------------------------------------------------------------------
1 | /src/app/reducers
--------------------------------------------------------------------------------
/client/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 | "rules": {
4 | "func-names": 0,
5 | "semi": 0,
6 | }
7 | }
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /.idea
3 | .DS_Store
4 | bundle.js
5 | npm-debug.log
--------------------------------------------------------------------------------
/client/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Token authentication system using Node, Mongo, React, Redux
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/client/dist/styles/app.ae462822222611de71ef.css:
--------------------------------------------------------------------------------
1 | @import url(https://fonts.googleapis.com/css?family=Nunito:300,400,500,600,700);/*! normalize.css v4.2.0 | MIT License | github.com/necolas/normalize.css */button,hr,input{overflow:visible}audio,canvas,progress,video{display:inline-block}progress,sub,sup{vertical-align:baseline}[type=checkbox],[type=radio],legend{box-sizing:border-box;padding:0}html{font-family:sans-serif;line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font:inherit;margin:0}optgroup{font-weight:700}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{color:inherit;display:table;max-width:100%;white-space:normal}textarea{overflow:auto}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}[hidden],template{display:none}body{font-family:Nunito,sans-serif;font-weight:lighter;letter-spacing:.5px;background-color:#f4f7f8;color:#4c5760;margin:0;padding:0;font-size:18px}@media (max-width:991px){body{font-size:14px}}a:active,a:after,a:checked,a:enabled,a:focus,a:link,a:visited{text-decoration:none!important}a,button,select,textarea{outline:none!important}input{outline:none}h1{margin:0 0 30px;text-align:center}.container{background-color:#fff;padding:30px 0 60px;max-width:640px;border:1px solid #c0c4c6;border-bottom:2px solid #c0c4c6;border-radius:5px;margin:60px auto 0}@media (max-width:991px){.container{width:90%}}.form-container{padding:0 100px;margin:0 auto}@media (max-width:767px){.form-container{padding:0 20px}}.content{text-align:center;padding:0 30px}.content h3{margin:40px 0}.resend{color:#288feb;cursor:pointer}.resend:hover{text-decoration:underline}.resended{color:#28cb75}.input-group{margin-bottom:14px}input[type=password],input[type=text]{border-radius:5px;border:1px solid #c0c4c6;height:40px;width:98%;padding-left:2%}input[type=password]:focus,input[type=text]:focus{border:1px solid #288feb}.has-error input[type=password],.has-error input[type=text]{border:1px solid #e15258}.btn{width:100%;height:40px;background-color:#28cb75;color:#fff;border:0;border-radius:5px;cursor:pointer;margin-top:20px;font-weight:500;font-size:18px}.btn:hover{background-color:#24b669}.form-bottom{text-align:center;font-size:14px;margin-top:20px;display:block}.form-bottom p{margin-bottom:8px}.form-bottom a{color:#288feb}.form-bottom a:hover{text-decoration:underline!important}.form-error{color:#e15258}.error-container{background-color:#e15258;color:#fff;padding:10px;border-radius:5px;font-size:16px;overflow:hidden}.error-container.signin-error{margin-top:44px}.password-forgot{float:right;font-size:14px;color:#288feb;width:100%;text-align:right}.password-forgot a{color:#288feb}.password-forgot a:hover{text-decoration:underline!important}header{height:70px;line-height:70px;background-color:#fff;border-bottom:1px solid #c0c4c6}header .logo{color:#288feb;margin-left:20px}header nav{float:right}header nav ul{margin:0;padding:0;float:right;margin-right:20px}header nav ul li{display:inline-block;margin-left:20px}header nav ul li a{color:#4c5760}header nav ul li a:hover{color:#288feb}footer{margin-top:40px;height:50px;line-height:50px;text-align:center;margin-bottom:100px}footer img{width:40px;display:block;margin:0 auto}footer a{display:block;color:#4c5760}footer a:hover{color:#288feb}.users ul{margin-top:30px;text-align:left}.users li{margin-bottom:10px}
2 | /*# sourceMappingURL=app.ae462822222611de71ef.css.map*/
--------------------------------------------------------------------------------
/client/dist/styles/app.ae462822222611de71ef.css.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":[],"names":[],"mappings":"","file":"/styles/app.ae462822222611de71ef.css","sourceRoot":""}
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node-redux-auth",
3 | "version": "1.0.0",
4 | "description": "Token authentication system using Node, Mongo, React, Redux",
5 | "scripts": {
6 | "dev": "webpack-dev-server --config ./webpack/webpack-dev.config.js --watch --colors",
7 | "build": "rm -rf dist && webpack --config ./webpack/webpack-prod.config.js --colors",
8 | "start": "NODE_ENV=production PORT=3000 pm2 start ./server.js",
9 | "test": "mocha --compilers js:babel-core/register --require ./test/test_helper.js --recursive ./test",
10 | "test:watch": "npm run test -- --watch",
11 | "lint": "eslint src test webpack"
12 | },
13 | "keywords": [
14 | "Express",
15 | "MongoDB",
16 | "React",
17 | "Redux",
18 | "Token Authentication",
19 | "Airbnb Eslint",
20 | "SCSS",
21 | "Babel",
22 | "Webpack Configuration"
23 | ],
24 | "repository": {
25 | "type": "git",
26 | "url": "git+https://github.com/DimitriMikadze/node-redux-auth"
27 | },
28 | "author": "Dimitri Mikadze",
29 | "license": "MIT",
30 | "devDependencies": {
31 | "babel-core": "^6.8.0",
32 | "babel-loader": "^6.2.4",
33 | "babel-preset-es2015": "^6.6.0",
34 | "babel-preset-react": "^6.5.0",
35 | "babel-preset-stage-1": "^6.5.0",
36 | "chai": "^3.5.0",
37 | "chai-jquery": "^2.0.0",
38 | "css-loader": "^0.25.0",
39 | "eslint": "^3.5.0",
40 | "eslint-config-airbnb": "^11.1.0",
41 | "eslint-plugin-import": "^1.8.0",
42 | "eslint-plugin-jsx-a11y": "^2.2.2",
43 | "eslint-plugin-react": "^6.2.1",
44 | "extract-text-webpack-plugin": "^1.0.1",
45 | "html-webpack-plugin": "^2.16.1",
46 | "jquery": "^3.1.0",
47 | "jsdom": "^9.0.0",
48 | "mocha": "^3.0.2",
49 | "node-sass": "^3.7.0",
50 | "react-addons-test-utils": "^15.0.2",
51 | "react-hot-loader": "^3.0.0-beta.3",
52 | "sass-loader": "^4.0.2",
53 | "style-loader": "^0.13.1",
54 | "url-loader": "^0.5.7",
55 | "webpack": "^1.13.0",
56 | "webpack-dev-server": "^1.14.1"
57 | },
58 | "dependencies": {
59 | "axios": "^0.12.0",
60 | "express": "^4.13.4",
61 | "react": "^15.0.2",
62 | "react-dom": "^15.0.2",
63 | "react-redux": "^4.4.5",
64 | "react-router": "^2.4.0",
65 | "redux": "^3.5.2",
66 | "redux-form": "^6.0.2",
67 | "redux-promise": "^0.5.3",
68 | "redux-thunk": "^2.1.0"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/client/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const app = express();
3 |
4 | app.use(express.static('./'));
5 | app.use(express.static('dist'));
6 |
7 | app.get('*', (req, res) => {
8 | res.sendFile(`${__dirname}/dist/index.html`);
9 | });
10 |
11 | const port = process.env.PORT || 3000;
12 |
13 | app.listen(port, () => {
14 | console.log('app listening on', port);
15 | });
--------------------------------------------------------------------------------
/client/src/app/actions/auth.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { browserHistory } from 'react-router';
3 | import { API_URL } from '../config';
4 | import {
5 | SIGNUP_SUCCESS,
6 | SIGNUP_FAILURE,
7 | SIGNUP_RESEND_FAILURE,
8 | VERIFY_EMAIL_ERROR,
9 | SIGNIN_FAILURE,
10 | AUTH_USER,
11 | UNAUTH_USER,
12 | } from './types/index';
13 |
14 | /**
15 | * Error helper
16 | */
17 | export function authError(CONST, error) {
18 | return {
19 | type: CONST,
20 | payload: error,
21 | };
22 | }
23 |
24 | /**
25 | * Sign up
26 | */
27 | export function signupUser(props) {
28 | return function (dispatch) {
29 | axios.post(`${API_URL}/signup`, props)
30 | .then(() => {
31 | dispatch({ type: SIGNUP_SUCCESS });
32 |
33 | browserHistory.push(`/reduxauth/signup/verify-email?email=${props.email}`);
34 | })
35 | .catch(response => dispatch(authError(SIGNUP_FAILURE, response.data.error)));
36 | }
37 | }
38 |
39 | /**
40 | * Sign in
41 | */
42 | export function signinUser(props) {
43 | const { email, password } = props;
44 |
45 | return function (dispatch) {
46 | axios.post(`${API_URL}/signin`, { email, password })
47 | .then(response => {
48 | localStorage.setItem('user', JSON.stringify(response.data));
49 |
50 | dispatch({ type: AUTH_USER });
51 |
52 | browserHistory.push('/reduxauth/users');
53 | })
54 | .catch(() => dispatch(authError(SIGNIN_FAILURE, "Email or password isn't right")));
55 | }
56 | }
57 |
58 |
59 | /**
60 | * Resend verification code
61 | */
62 | export function resendVerification(props) {
63 | return function (dispatch) {
64 | axios.post(`${API_URL}/resend-verify-code`, props)
65 | .then(() => {
66 | dispatch({ type: SIGNUP_SUCCESS });
67 | })
68 | .catch(response => dispatch(authError(SIGNUP_RESEND_FAILURE, response.data)));
69 | }
70 | }
71 |
72 |
73 | /**
74 | * Verify email
75 | */
76 | export function verifyEmail(props) {
77 | return function (dispatch) {
78 | axios.post(`${API_URL}/signup/verify-email`, props)
79 | .then(response => {
80 | localStorage.setItem('user', JSON.stringify(response.data));
81 |
82 | dispatch({ type: AUTH_USER });
83 |
84 | browserHistory.push('/reduxauth/users');
85 | })
86 | .catch(response => dispatch(authError(VERIFY_EMAIL_ERROR, response.data.error)));
87 | }
88 | }
89 |
90 | /**
91 | * Sign out
92 | */
93 | export function signoutUser() {
94 | localStorage.clear();
95 |
96 | return {
97 | type: UNAUTH_USER,
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/client/src/app/actions/resetPassword.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { browserHistory } from 'react-router';
3 | import { API_URL } from '../config';
4 | import {
5 | RESET_PASSWORD_SUCCESS,
6 | RESET_PASSWORD_FAILURE,
7 | VERIFY_RESET_PASSWORD_SUCCESS,
8 | VERIFY_RESET_PASSWORD_FAILURE,
9 | AUTH_USER,
10 | } from './types/index';
11 |
12 | /**
13 | * Error helper
14 | */
15 | export function authError(CONST, error) {
16 | return {
17 | type: CONST,
18 | payload: error,
19 | }
20 | }
21 |
22 | /**
23 | * Reset password
24 | */
25 | export function resetPassword(props) {
26 | return function (dispatch) {
27 | axios.post(`${API_URL}/reset-password`, props)
28 | .then(() => {
29 | dispatch({ type: RESET_PASSWORD_SUCCESS });
30 |
31 | browserHistory.push(`/reduxauth/reset-password/verify?email=${props.email}`);
32 | })
33 | .catch(response => {
34 | dispatch(authError(RESET_PASSWORD_FAILURE, response.data.error))
35 | });
36 | }
37 | }
38 |
39 | /**
40 | * Verify Reset password
41 | */
42 | export function verifyResetPassword(props) {
43 | return function (dispatch) {
44 | axios.post(`${API_URL}/reset-password/verify`, props)
45 | .then(() => {
46 | dispatch({ type: VERIFY_RESET_PASSWORD_SUCCESS });
47 | })
48 | .catch(response => {
49 | dispatch(authError(VERIFY_RESET_PASSWORD_FAILURE, response.data.error));
50 | });
51 | }
52 | }
53 |
54 | /**
55 | * Reset password new
56 | */
57 | export function resetPasswordNew(props) {
58 | return function (dispatch) {
59 | axios.post(`${API_URL}/reset-password/new`, props)
60 | .then(response => {
61 | localStorage.setItem('user', JSON.stringify(response.data));
62 |
63 | dispatch({ type: AUTH_USER });
64 |
65 | browserHistory.push('/reduxauth/users');
66 | })
67 | .catch(response => dispatch(authError(VERIFY_RESET_PASSWORD_FAILURE, response.data)));
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/client/src/app/actions/types/index.js:
--------------------------------------------------------------------------------
1 | export const SIGNUP_SUCCESS = 'SIGNUP_SUCCESS';
2 | export const SIGNUP_FAILURE = 'SIGNUP_FAILURE';
3 | export const SIGNUP_RESEND_FAILURE = 'SIGNUP_RESEND_FAILURE';
4 | export const VERIFY_EMAIL_ERROR = 'VERIFY_EMAIL_ERROR';
5 | export const SIGNIN_FAILURE = 'SIGNIN_FAILURE';
6 |
7 | export const RESET_PASSWORD_SUCCESS = 'RESET_PASSWORD';
8 | export const RESET_PASSWORD_FAILURE = 'RESET_PASSWORD_ERROR';
9 | export const VERIFY_RESET_PASSWORD_SUCCESS = 'VERIFY_RESET_PASSWORD_SUCCESS';
10 | export const VERIFY_RESET_PASSWORD_FAILURE = 'VERIFY_RESET_PASSWORD_FAILURE';
11 |
12 | export const AUTH_USER = 'AUTH_USER';
13 | export const UNAUTH_USER = 'UNAUTH_USER';
14 |
15 | export const FETCH_USERS = 'FETCH_USERS';
16 |
--------------------------------------------------------------------------------
/client/src/app/actions/users.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { API_URL } from '../config';
3 | import {
4 | FETCH_USERS,
5 | } from './types/index';
6 |
7 | /**
8 | * Fetch all users
9 | */
10 | export function fetchUsers() {
11 | const user = JSON.parse(localStorage.getItem('user'));
12 |
13 | return function (dispatch) {
14 | axios.get(API_URL, { headers: { authorization: user.token } })
15 | .then(response => {
16 | dispatch({
17 | type: FETCH_USERS,
18 | payload: response.data,
19 | });
20 | });
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/client/src/app/components/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Header from './common/Header';
3 | import Footer from './common/Footer';
4 |
5 | export default class App extends Component {
6 | render() {
7 | return (
8 |
9 |
10 |
11 | { this.props.children }
12 |
13 |
14 |
15 | );
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/client/src/app/components/auth/Signin.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { reduxForm, Field } from 'redux-form';
3 | import * as actions from '../../actions/auth';
4 | import { Link } from 'react-router';
5 | import { connect } from 'react-redux';
6 |
7 | const renderField = ({ input, type, placeholder, meta: { touched, error } }) => (
8 |
9 |
10 | { touched && error &&
{error}
}
11 |
12 | );
13 |
14 | class Signin extends Component {
15 | constructor(props) {
16 | super(props);
17 |
18 | this.handleFormSubmit = this.handleFormSubmit.bind(this);
19 | }
20 |
21 | handleFormSubmit(props) {
22 | this.props.signinUser(props);
23 | }
24 |
25 | render() {
26 | const { handleSubmit } = this.props;
27 |
28 | return (
29 |
58 | )
59 | }
60 | }
61 |
62 | function validate(formProps) {
63 | const errors = {};
64 |
65 | if(!formProps.email) {
66 | errors.email = 'Email is required'
67 | }
68 |
69 | if(!formProps.password) {
70 | errors.password = 'Password is required'
71 | }
72 |
73 | return errors;
74 | }
75 |
76 | function mapStateToProps(state) {
77 | return { errorMessage: state.auth.error }
78 | }
79 |
80 | Signin = reduxForm({ form: 'signin', validate })(Signin);
81 |
82 | export default connect(mapStateToProps, actions)(Signin);
83 |
--------------------------------------------------------------------------------
/client/src/app/components/auth/Signout.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import * as actions from '../../actions/auth';
4 |
5 | class Signout extends Component {
6 | componentWillMount() {
7 | this.props.signoutUser();
8 | }
9 |
10 | render() {
11 | return We hope to see you again soon...
12 | }
13 | }
14 |
15 | export default connect(null, actions)(Signout);
--------------------------------------------------------------------------------
/client/src/app/components/auth/Signup.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { reduxForm, Field } from 'redux-form';
3 | import * as actions from '../../actions/auth';
4 | import { Link } from 'react-router';
5 | import { connect } from 'react-redux';
6 |
7 | const renderField = ({ input, type, placeholder, meta: { touched, error } }) => (
8 |
9 |
10 | { touched && error &&
{error}
}
11 |
12 | );
13 |
14 | class Signup extends Component {
15 | constructor(props) {
16 | super(props);
17 |
18 | this.handleFormSubmit = this.handleFormSubmit.bind(this);
19 | }
20 |
21 | handleFormSubmit(formProps) {
22 | this.props.signupUser(formProps);
23 | }
24 |
25 | render() {
26 | const { handleSubmit } = this.props;
27 |
28 | return (
29 |
64 | )
65 | }
66 | }
67 |
68 | const validate = props => {
69 | const errors = {};
70 | const fields = ['firstname', 'lastname', 'email', 'password', 'repassword'];
71 |
72 | fields.forEach((f) => {
73 | if(!(f in props)) {
74 | errors[f] = `${f} is required`;
75 | }
76 | });
77 |
78 | if(props.firstname && props.firstname.length < 3) {
79 | errors.firstname = "minimum of 4 characters";
80 | }
81 |
82 | if(props.firstname && props.firstname.length > 20) {
83 | errors.firstname = "maximum of 20 characters";
84 | }
85 |
86 | if(props.lastname && props.lastname.length < 3) {
87 | errors.lastname = "minimum of 4 characters";
88 | }
89 |
90 | if(props.lastname && props.lastname.length > 20) {
91 | errors.lastname = "maximum of 20 characters";
92 | }
93 |
94 | if(props.email && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(props.email)) {
95 | errors.email = "please provide valid email";
96 | }
97 |
98 | if(props.password && props.password.length < 6) {
99 | errors.password = "minimum 6 characters";
100 | }
101 |
102 | if(props.password !== props.repassword) {
103 | errors.repassword = "passwords doesn't match";
104 | }
105 |
106 | return errors;
107 | };
108 |
109 |
110 | function mapStateToProps(state) {
111 | return { errorMessage: state.auth.error };
112 | }
113 |
114 | Signup = reduxForm({ form: 'signup', validate })(Signup);
115 |
116 | export default connect(mapStateToProps, actions)(Signup);
117 |
--------------------------------------------------------------------------------
/client/src/app/components/auth/SignupVerify.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { browserHistory } from 'react-router';
4 | import * as actions from '../../actions/auth';
5 |
6 | class SignupVerify extends Component {
7 | constructor(props) {
8 | super(props);
9 |
10 | this.state = { resend: false };
11 | }
12 |
13 | componentWillMount() {
14 | this.email = this.props.location.query.email;
15 |
16 | if(!this.props.signup || !this.email) {
17 | browserHistory.push('/reduxauth/signup');
18 | }
19 | }
20 |
21 | resendEmail(props) {
22 | this.setState({ resend: true });
23 | this.props.resendVerification(props);
24 | }
25 |
26 | render() {
27 | return (
28 |
29 |
Activate account
30 |
Please confirm the verification code we've just emailed you at { this.email && this.email }
31 | {
32 | !this.state.resend ?
33 |
Resend email verification code
:
34 |
Email verification code has been resended
35 | }
36 | {
37 | this.props.errorMessage && this.props.errorMessage.signupResend &&
38 |
{ this.props.errorMessage.signupResend }
39 | }
40 |
41 | )
42 | }
43 | }
44 |
45 | function mapStateToProps(state) {
46 | return { errorMessage: state.auth.error, signup: state.auth.signup };
47 | }
48 |
49 | export default connect(mapStateToProps, actions)(SignupVerify);
--------------------------------------------------------------------------------
/client/src/app/components/auth/VerifyEmail.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import * as actions from '../../actions/auth';
4 |
5 | class VerifyEmail extends Component {
6 | constructor(props) {
7 | super(props);
8 |
9 | this.state = { resend: false };
10 | }
11 |
12 | componentWillMount() {
13 | const { email, token } = this.props.location.query;
14 |
15 | this.user = {};
16 | this.user.email = email;
17 | this.user.token = token;
18 |
19 | this.props.verifyEmail({ email, token });
20 | }
21 |
22 | resendEmail(props) {
23 | this.setState({ resend: true });
24 | this.props.resendVerification(props);
25 | }
26 |
27 | render() {
28 | return (
29 |
30 | {
31 | this.props.errorMessage && this.props.errorMessage.verifyEmail &&
32 |
33 |
Failure
34 |
35 |
{ this.props.errorMessage.verifyEmail.message }
36 |
37 | }
38 | {
39 | this.props.errorMessage && this.props.errorMessage.verifyEmail && this.props.errorMessage.verifyEmail.resend && !this.state.resend &&
40 |
41 | Resend verification code
42 |
43 | }
44 | {
45 | this.state.resend &&
46 |
47 | Email verification code has been resended
48 |
49 | }
50 |
51 | )
52 | }
53 | }
54 |
55 | function mapStateToProps(state) {
56 | return { errorMessage: state.auth.error };
57 | }
58 |
59 | export default connect(mapStateToProps, actions)(VerifyEmail);
--------------------------------------------------------------------------------
/client/src/app/components/bundle.scss:
--------------------------------------------------------------------------------
1 | @import './common/styles/normalize';
2 | @import './common/styles/variables';
3 | @import './common/styles/mixins';
4 | @import './common/styles/global';
5 | @import './common/styles/form';
6 | @import './common/header';
7 | @import './common/footer';
8 | @import './users/users';
--------------------------------------------------------------------------------
/client/src/app/components/common/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router';
3 |
4 | function Footer() {
5 | return (
6 |
12 | )
13 | };
14 |
15 | export default Footer;
--------------------------------------------------------------------------------
/client/src/app/components/common/Header.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { Link } from 'react-router';
4 |
5 | class Header extends Component {
6 | render() {
7 | return (
8 |
9 | Redux Auth
10 |
11 | {
12 | this.props.authenticated ?
13 |
14 |
15 | Users
16 |
17 |
18 | Signout
19 |
20 |
21 | :
22 |
23 |
24 | Sign in
25 |
26 |
27 | Sign up
28 |
29 |
30 | }
31 |
32 |
33 | )
34 | }
35 | }
36 |
37 | function mapStateToProps(state) {
38 | return { authenticated: state.auth.authenticated };
39 | }
40 |
41 | export default connect(mapStateToProps)(Header);
--------------------------------------------------------------------------------
/client/src/app/components/common/footer.scss:
--------------------------------------------------------------------------------
1 | footer {
2 | margin-top: 40px;
3 | height: 50px;
4 | line-height: 50px;
5 | text-align: center;
6 | margin-bottom: 100px;
7 |
8 | img {
9 | width: 40px;
10 | display: block;
11 | margin: 0 auto;
12 | }
13 |
14 | a {
15 | display: block;
16 | color: $light-black;
17 |
18 | &:hover {
19 | color: $blue;
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/client/src/app/components/common/header.scss:
--------------------------------------------------------------------------------
1 | header {
2 | height: 70px;
3 | line-height: 70px;
4 | background-color: $white;
5 | border-bottom: 1px solid $gray;
6 |
7 | .logo {
8 | color: $blue;
9 | margin-left: 20px;
10 | }
11 |
12 | nav {
13 | float: right;
14 |
15 | ul {
16 | margin: 0;
17 | padding: 0;
18 | float: right;
19 | margin-right: 20px;
20 |
21 | li {
22 | display: inline-block;
23 | margin-left: 20px;
24 |
25 | a {
26 | color: $light-black;
27 |
28 | &:hover {
29 | color: $blue;
30 | }
31 | }
32 | }
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/client/src/app/components/common/styles/form.scss:
--------------------------------------------------------------------------------
1 | .input-group {
2 | margin-bottom: 14px;
3 | }
4 |
5 | input[type='text'], input[type='password'] {
6 | border-radius: 5px;
7 | border: 1px solid $gray;
8 | height: 40px;
9 | width: 98%;
10 | padding-left: 2%;
11 |
12 | &:focus {
13 | border: 1px solid $blue;
14 | }
15 | }
16 |
17 | .has-error {
18 | input[type='text'], input[type='password'] {
19 | border: 1px solid $red;
20 | }
21 | }
22 |
23 | .btn {
24 | width: 100%;
25 | height: 40px;
26 | background-color: $green;
27 | color: $white;
28 | border: 0;
29 | border-radius: 5px;
30 | cursor: pointer;
31 | margin-top: 20px;
32 | font-weight: 500;
33 | font-size: 18px;
34 |
35 | &:hover {
36 | background-color: darken($green, 5);
37 | }
38 | }
39 |
40 | .form-bottom {
41 | text-align: center;
42 | font-size: 14px;
43 | margin-top: 20px;
44 | display: block;
45 |
46 | p {
47 | margin-bottom: 8px;
48 | }
49 |
50 | a {
51 | color: $blue;
52 |
53 | &:hover {
54 | text-decoration: underline !important;
55 | }
56 | }
57 | }
58 |
59 | .form-error {
60 | color: $red;
61 | }
62 |
63 | .error-container {
64 | background-color: $red;
65 | color: $white;
66 | padding: 10px;
67 | border-radius: 5px;
68 | font-size: 16px;
69 | overflow: hidden;
70 |
71 | &.signin-error {
72 | margin-top: 44px;
73 | }
74 | }
75 |
76 | .password-forgot {
77 | float: right;
78 | font-size: 14px;
79 | color: $blue;
80 | width: 100%;
81 | text-align: right;
82 |
83 | a {
84 | color: $blue;
85 |
86 | &:hover {
87 | text-decoration: underline !important;
88 | }
89 | }
90 | }
--------------------------------------------------------------------------------
/client/src/app/components/common/styles/global.scss:
--------------------------------------------------------------------------------
1 | @import url(https://fonts.googleapis.com/css?family=Nunito:300,400,500,600,700);
2 |
3 | body {
4 | font-family: 'Nunito', sans-serif;
5 | font-weight: lighter;
6 | letter-spacing: .5px;
7 | background-color: $light-gray;
8 | color: $light-black;
9 | margin: 0;
10 | padding: 0;
11 | font-size: 18px;
12 | @include mobile-ipad {
13 | font-size: 14px;
14 | }
15 | }
16 |
17 | a {
18 | &:visited,
19 | &:after,
20 | &:active,
21 | &:focus,
22 | &:checked,
23 | &:enabled,
24 | &:link {
25 | text-decoration: none !important;
26 | }
27 | }
28 |
29 | a,
30 | button,
31 | select,
32 | textarea {
33 | outline: none !important;
34 | }
35 |
36 | input {
37 | outline: none;
38 | }
39 |
40 | h1 {
41 | margin: 0 0 30px 0;
42 | text-align: center;
43 | }
44 |
45 | .container {
46 | background-color: $white;
47 | padding: 30px 0 60px 0;
48 | max-width: 640px;
49 | border: 1px solid $gray;
50 | border-bottom: 2px solid $gray;
51 | border-radius: 5px;
52 | margin: 60px auto 0 auto;
53 |
54 | @include mobile-ipad {
55 | width: 90%;
56 | }
57 | }
58 |
59 | .form-container {
60 | padding: 0 100px;
61 |
62 | @include mobile {
63 | padding: 0 20px;
64 |
65 | }
66 | margin: 0 auto;
67 | }
68 |
69 | .content {
70 | text-align: center;
71 | padding: 0 30px;
72 |
73 | h3 {
74 | margin: 40px 0;
75 | }
76 | }
77 |
78 | .resend {
79 | color: $blue;
80 | cursor: pointer;
81 |
82 | &:hover {
83 | text-decoration: underline;
84 | }
85 | }
86 |
87 | .resended {
88 | color: $green;
89 | }
--------------------------------------------------------------------------------
/client/src/app/components/common/styles/mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin mobile {
2 | @media (max-width: $screen-xs-max) {
3 | @content;
4 | }
5 | }
6 |
7 | @mixin ipad {
8 | @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
9 | @content;
10 | }
11 | }
12 |
13 | @mixin mobile-ipad {
14 | @media (max-width: $screen-sm-max) {
15 | @content;
16 | }
17 | }
--------------------------------------------------------------------------------
/client/src/app/components/common/styles/normalize.scss:
--------------------------------------------------------------------------------
1 | /*! normalize.css v4.2.0 | MIT License | github.com/necolas/normalize.css */button,hr,input{overflow:visible}audio,canvas,progress,video{display:inline-block}progress,sub,sup{vertical-align:baseline}[type=checkbox],[type=radio],legend{box-sizing:border-box;padding:0}html{font-family:sans-serif;line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font:inherit;margin:0}optgroup{font-weight:700}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:ButtonText dotted 1px}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{color:inherit;display:table;max-width:100%;white-space:normal}textarea{overflow:auto}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}[hidden],template{display:none}
--------------------------------------------------------------------------------
/client/src/app/components/common/styles/variables.scss:
--------------------------------------------------------------------------------
1 | $white: #fff;
2 | $black: #000;
3 | $purple: #288feb;
4 | $light-black: #4c5760;
5 | $light-gray: #f4f7f8;
6 | $gray: #c0c4c6;
7 | $green: #28cb75;
8 | $red: #c25975;
9 | $js-yellow: #f8dc3d;
10 | $red: #e15258;
11 | $purple: #7d669e;
12 | $orange: #eb7728;
13 | $blue: #288feb;
14 | $light-blue: #39ADD1;
15 |
16 | $screen-xs-max: "767px";
17 | $screen-sm-min: "768px";
18 | $screen-sm-max: "991px";
19 | $screen-md-min: "992px";
20 | $screen-md-max: "1199px";
21 | $screen-lg-min: "1200px";
--------------------------------------------------------------------------------
/client/src/app/components/hoc/RequireAuth.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import { browserHistory } from 'react-router';
4 |
5 | export default function (ComposedComponent) {
6 | class Authentication extends Component {
7 | componentWillMount() {
8 | if (!this.props.authenticated) {
9 | browserHistory.push('/reduxauth/signup');
10 | }
11 | }
12 |
13 | componentWillUpdate(nextProps) {
14 | if (!nextProps.authenticated) {
15 | browserHistory.push('/reduxauth/signup');
16 | }
17 | }
18 |
19 | render() {
20 | return
21 | }
22 | }
23 |
24 | Authentication.propTypes = { authenticated: PropTypes.bool };
25 |
26 | function mapStateToProps(state) {
27 | return { authenticated: state.auth.authenticated };
28 | }
29 |
30 | return connect(mapStateToProps)(Authentication);
31 | }
32 |
--------------------------------------------------------------------------------
/client/src/app/components/hoc/RequireNotAuth.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import { browserHistory } from 'react-router';
4 |
5 | export default function (ComposedComponent) {
6 | class NotAuthentication extends Component {
7 | componentWillMount() {
8 | if (this.props.authenticated) {
9 | browserHistory.push('/reduxauth/users');
10 | }
11 | }
12 |
13 | componentWillUpdate(nextProps) {
14 | if (nextProps.authenticated) {
15 | browserHistory.push('/reduxauth/users');
16 | }
17 | }
18 |
19 | render() {
20 | return
21 | }
22 | }
23 |
24 | NotAuthentication.propTypes = { authenticated: PropTypes.bool };
25 |
26 | function mapStateToProps(state) {
27 | return { authenticated: state.auth.authenticated };
28 | }
29 |
30 | return connect(mapStateToProps)(NotAuthentication);
31 | }
32 |
--------------------------------------------------------------------------------
/client/src/app/components/resetPassword/ResetPassword.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { reduxForm, Field } from 'redux-form';
3 | import * as actions from '../../actions/resetPassword';
4 | import { connect } from 'react-redux';
5 |
6 | const renderInput = field =>
7 |
8 |
9 |
10 |
11 | class ResetPassword extends Component {
12 | constructor(props) {
13 | super(props);
14 |
15 | this.handleFormSubmit = this.handleFormSubmit.bind(this);
16 | }
17 |
18 | handleFormSubmit(props) {
19 | this.props.resetPassword(props);
20 | }
21 |
22 | render() {
23 | const { handleSubmit } = this.props;
24 |
25 | return (
26 |
27 |
Reset Password
28 |
44 |
45 | )
46 | }
47 | }
48 |
49 | function mapStateToProps(state) {
50 | return { errorMessage: state.resetPass.error };
51 | }
52 |
53 | ResetPassword = reduxForm({ form: 'resetpassword' })(ResetPassword);
54 |
55 | export default connect(mapStateToProps, actions)(ResetPassword);
56 |
--------------------------------------------------------------------------------
/client/src/app/components/resetPassword/ResetPasswordNew.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { reduxForm, Field } from 'redux-form';
3 | import * as actions from '../../actions/resetPassword';
4 | import { Link } from 'react-router';
5 | import { connect } from 'react-redux';
6 |
7 | const renderField = ({ input, type, placeholder, meta: { touched, error } }) => (
8 |
9 |
10 | { touched && error &&
{error}
}
11 |
12 | );
13 |
14 | class ResetPasswordNew extends Component {
15 | constructor(props) {
16 | super(props);
17 |
18 | this.handleFormSubmit = this.handleFormSubmit.bind(this);
19 | }
20 |
21 | componentWillMount() {
22 | const { email, token } = this.props.location.query;
23 |
24 | this.props.verifyResetPassword({ email, token });
25 | }
26 |
27 | handleFormSubmit(props) {
28 | const { email, token } = this.props.location.query;
29 |
30 | props.email = email;
31 | props.token = token;
32 |
33 | this.props.resetPasswordNew(props);
34 | }
35 |
36 | render() {
37 | const { handleSubmit } = this.props;
38 |
39 | return (
40 |
41 |
Reset Password
42 | {
43 | /* Landing error message */
44 | this.props.errorMessage && this.props.errorMessage.verifyResetPassword ?
45 |
46 |
{ this.props.errorMessage.verifyResetPassword.message }
47 | {
48 | this.props.errorMessage.verifyResetPassword.resend &&
49 | Reset Password Again
50 | }
51 |
52 | :
53 | /* New password form */
54 |
70 | }
71 |
72 | )
73 | }
74 | }
75 |
76 | function validate(props) {
77 | const errors = {};
78 | const fields = ['newpassword', 'renewpassword'];
79 |
80 | fields.forEach((f) => {
81 | if(!(f in props)) {
82 | errors[f] = `${f} is required`;
83 | }
84 | });
85 |
86 | if(props.newpassword && props.newpassword.length < 6) {
87 | errors.newpassword = "minimum 6 characters";
88 | }
89 |
90 | if(props.newpassword !== props.renewpassword) {
91 | errors.renewpassword = "passwords doesn't match";
92 | }
93 |
94 | return errors;
95 | }
96 |
97 | function mapStateToProps(state) {
98 | return { errorMessage: state.resetPass.error };
99 | }
100 |
101 | ResetPasswordNew = reduxForm({ form: 'resetnewpassword', validate })(ResetPasswordNew);
102 |
103 | export default connect(mapStateToProps, actions)(ResetPasswordNew);
104 |
--------------------------------------------------------------------------------
/client/src/app/components/resetPassword/ResetPasswordVerify.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { browserHistory } from 'react-router';
4 | import * as actions from '../../actions/resetPassword';
5 |
6 | class ResetPasswordVerify extends Component {
7 | constructor(props) {
8 | super(props);
9 |
10 | this.state = { resend: false };
11 | }
12 |
13 | componentWillMount() {
14 | this.email = this.props.location.query.email;
15 |
16 | if(!this.props.resetPasswordProgress || !this.email) {
17 | browserHistory.push('/reduxauth/signup');
18 | }
19 | }
20 |
21 | resendEmail(props) {
22 | this.setState({ resend: true });
23 | this.props.resetPassword(props);
24 | }
25 |
26 | render() {
27 | return (
28 |
29 |
Reset Password
30 |
We've just emailed you password reset instructions at { this.email && this.email }
31 | {
32 | !this.state.resend ?
33 |
Resend instructions
:
34 |
Reset password instructions has been resended
35 | }
36 | {
37 | this.props.errorMessage && this.props.errorMessage.resetPassword &&
38 |
{ this.props.errorMessage.resetPassword }
39 | }
40 |
41 | )
42 | }
43 | }
44 |
45 | function mapStateToProps(state) {
46 | return { resetPasswordProgress: state.resetPass.resetPassword, errorMessage: state.resetPass.error };
47 | }
48 |
49 | export default connect(mapStateToProps, actions)(ResetPasswordVerify);
--------------------------------------------------------------------------------
/client/src/app/components/users/UserList.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import * as actions from '../../actions/users';
3 | import { connect } from 'react-redux';
4 |
5 | class Feature extends Component {
6 | componentWillMount() {
7 | this.props.fetchUsers();
8 |
9 | this.user = JSON.parse(localStorage.getItem('user'));
10 | }
11 |
12 | renderUsers() {
13 | const users = this.props.users || [];
14 |
15 | return users.map((user, i) => {
16 | return { user.firstname }
17 | })
18 | }
19 |
20 | render() {
21 | return (
22 |
23 |
Hello { this.user.firstname }
24 |
Here are auth protected user firstnames! :)
25 |
26 | { this.renderUsers() }
27 |
28 |
29 | )
30 | }
31 | }
32 |
33 | function mapStateToProps(state) {
34 | return { users: state.user.list };
35 | }
36 |
37 | export default connect(mapStateToProps, actions)(Feature);
--------------------------------------------------------------------------------
/client/src/app/components/users/users.scss:
--------------------------------------------------------------------------------
1 | .users {
2 | ul {
3 | margin-top: 30px;
4 | text-align: left;
5 | }
6 |
7 | li {
8 | margin-bottom: 10px;
9 | }
10 | }
--------------------------------------------------------------------------------
/client/src/app/config.js:
--------------------------------------------------------------------------------
1 | export const API_URL = process.env.NODE_ENV === 'production' ? 'http://dimitrimikadze.com:3333' : 'http://localhost:3333';
2 |
--------------------------------------------------------------------------------
/client/src/app/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 | import { createStore, applyMiddleware } from 'redux';
5 | import { Router, browserHistory } from 'react-router';
6 | import reduxThunk from 'redux-thunk';
7 | import { AUTH_USER } from './actions/types/index';
8 |
9 | import reducers from './reducers';
10 | import routes from './routes';
11 |
12 | import './components/bundle.scss';
13 |
14 | const createStoreWithMiddleware = applyMiddleware(reduxThunk)(createStore);
15 | const store = createStoreWithMiddleware(reducers);
16 |
17 | const user = JSON.parse(localStorage.getItem('user'));
18 |
19 | if (user && user.token) {
20 | store.dispatch({ type: AUTH_USER });
21 | }
22 |
23 | ReactDOM.render(
24 |
25 | window.scrollTo(0, 0)} history={browserHistory} routes={routes} />
26 |
27 | , document.getElementById('react-root'));
28 |
--------------------------------------------------------------------------------
/client/src/app/reducers/authReducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | SIGNUP_SUCCESS,
3 | SIGNUP_FAILURE,
4 | SIGNUP_RESEND_FAILURE,
5 | VERIFY_EMAIL_ERROR,
6 | SIGNIN_FAILURE,
7 | AUTH_USER,
8 | UNAUTH_USER,
9 | } from '../actions/types/index';
10 |
11 | export default function(state = {}, action) {
12 | switch(action.type) {
13 | case SIGNUP_SUCCESS:
14 | return { ...state, signup: true, error: {} };
15 | case SIGNUP_FAILURE:
16 | return { ...state, signup: false, error: { signup: action.payload } };
17 | case SIGNUP_RESEND_FAILURE:
18 | return { ...state, signup: true, error: { signupResend: action.payload } };
19 | case VERIFY_EMAIL_ERROR:
20 | return { ...state, signup: true, error: { verifyEmail: action.payload } };
21 | case SIGNIN_FAILURE:
22 | return { ...state, error: { signin: action.payload } };
23 | case AUTH_USER:
24 | return { ...state, authenticated: true, error: {} };
25 | case UNAUTH_USER:
26 | return { ...state, authenticated: false, error: {} };
27 | }
28 |
29 | return state;
30 | }
--------------------------------------------------------------------------------
/client/src/app/reducers/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import { combineReducers } from 'redux';
3 | import { reducer as form } from 'redux-form';
4 | import authReducer from './authReducer';
5 | import resetPasswordReducer from './resetPasswordReducer';
6 | import userReducer from './userReducer';
7 |
8 | const rootReducer = combineReducers({
9 | form,
10 | auth: authReducer,
11 | resetPass: resetPasswordReducer,
12 | user: userReducer
13 | });
14 |
15 | export default rootReducer;
16 |
--------------------------------------------------------------------------------
/client/src/app/reducers/resetPasswordReducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | RESET_PASSWORD_SUCCESS,
3 | RESET_PASSWORD_FAILURE,
4 | VERIFY_RESET_PASSWORD_SUCCESS,
5 | VERIFY_RESET_PASSWORD_FAILURE,
6 | } from '../actions/types/index';
7 |
8 | export default function(state = {}, action) {
9 | switch(action.type) {
10 | case RESET_PASSWORD_SUCCESS:
11 | return { ...state, resetPassword: true, error: {} };
12 | case RESET_PASSWORD_FAILURE:
13 | return { ...state, resetPassword: false, error: { resetPassword: action.payload } };
14 | case VERIFY_RESET_PASSWORD_SUCCESS:
15 | return { ...state, verifyResetPassword: true, error: {}, resetPassword: false };
16 | case VERIFY_RESET_PASSWORD_FAILURE:
17 | return { ...state, verifyResetPassword: false, error: { verifyResetPassword: action.payload } };
18 | }
19 |
20 | return state;
21 | }
--------------------------------------------------------------------------------
/client/src/app/reducers/userReducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | FETCH_USERS,
3 | } from '../actions/types/index';
4 |
5 | export default function(state = {}, action) {
6 | switch(action.type) {
7 | case FETCH_USERS:
8 | return { list: action.payload, ...state };
9 | }
10 |
11 | return state;
12 | }
--------------------------------------------------------------------------------
/client/src/app/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, IndexRoute } from 'react-router';
3 |
4 | import App from './components/App';
5 | import UserList from './components/users/UserList';
6 | import Signin from './components/auth/Signin';
7 | import Signout from './components/auth/Signout';
8 | import Signup from './components/auth/Signup';
9 | import VerifyEmail from './components/auth/VerifyEmail';
10 | import SignupVerify from './components/auth/SignupVerify';
11 | import ResetPassword from './components/resetPassword/ResetPassword';
12 | import ResetPasswordVerify from './components/resetPassword/ResetPasswordVerify';
13 | import ResetPasswordNew from './components/resetPassword/ResetPasswordNew';
14 |
15 | import requireAuth from './components/hoc/RequireAuth';
16 | import requireNotAuth from './components/hoc/RequireNotAuth';
17 |
18 | export default (
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | )
32 |
--------------------------------------------------------------------------------
/client/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Token authentication system using Node, Mongo, React, Redux
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/client/static/images/github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DimiMikadze/node-redux-auth/388d5985a89df83a86c5fa6c036a8a88145fb1a9/client/static/images/github.png
--------------------------------------------------------------------------------
/client/static/images/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DimiMikadze/node-redux-auth/388d5985a89df83a86c5fa6c036a8a88145fb1a9/client/static/images/screenshot.png
--------------------------------------------------------------------------------
/client/test/components/app_test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef, no-unused-expressions */
2 | import { renderComponent, expect } from '../test_helper';
3 | import App from '../../src/app/components/App';
4 |
5 | describe('App', () => {
6 | let component;
7 |
8 | beforeEach(() => {
9 | component = renderComponent(App);
10 | });
11 |
12 | it('renders something', () => {
13 | expect(component).to.exist;
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/client/test/test_helper.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable func-names */
2 | import _$ from 'jquery';
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 | import TestUtils from 'react-addons-test-utils';
6 | import jsdom from 'jsdom';
7 | import chai, { expect } from 'chai';
8 | import chaiJquery from 'chai-jquery';
9 | import { Provider } from 'react-redux';
10 | import { createStore } from 'redux';
11 | import reducers from '../src/app/reducers';
12 |
13 | global.document = jsdom.jsdom('');
14 | global.window = global.document.defaultView;
15 | global.navigator = {
16 | userAgent: 'node.js',
17 | };
18 |
19 | const $ = _$(window);
20 |
21 | chaiJquery(chai, chai.util, $);
22 |
23 | function renderComponent(ComponentClass, props = {}, state = {}) {
24 | const componentInstance = TestUtils.renderIntoDocument(
25 |
26 |
27 |
28 | );
29 |
30 | return $(ReactDOM.findDOMNode(componentInstance));
31 | }
32 |
33 | $.fn.simulate = function (eventName, value) {
34 | if (value) {
35 | this.val(value);
36 | }
37 | TestUtils.Simulate[eventName](this[0]);
38 | };
39 |
40 | export { renderComponent, expect };
41 |
--------------------------------------------------------------------------------
/client/webpack/webpack-dev.config.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./webpack.config.js')({
2 | isProduction: false,
3 | devtool: 'cheap-eval-source-map',
4 | jsFileName: 'app.js',
5 | cssFileName: 'app.css',
6 | port: 3000,
7 | });
8 |
--------------------------------------------------------------------------------
/client/webpack/webpack-prod.config.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./webpack.config.js')({
2 | isProduction: true,
3 | devtool: 'source-map',
4 | jsFileName: 'app.[hash].js',
5 | cssFileName: 'app.[hash].css',
6 | });
7 |
--------------------------------------------------------------------------------
/client/webpack/webpack.config.js:
--------------------------------------------------------------------------------
1 | const Path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 | const Webpack = require('webpack');
4 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
5 |
6 | module.exports = (options) => {
7 | const ExtractSASS = new ExtractTextPlugin(`/styles/${options.cssFileName}`);
8 |
9 | const webpackConfig = {
10 | devtool: options.devtool,
11 | entry: [
12 | `webpack-dev-server/client?http://localhost:${+ options.port}`,
13 | 'webpack/hot/dev-server',
14 | Path.join(__dirname, '../src/app/index'),
15 | ],
16 | output: {
17 | path: Path.join(__dirname, '../dist'),
18 | filename: `/scripts/${options.jsFileName}`,
19 | },
20 | resolve: {
21 | extensions: ['', '.js', '.jsx'],
22 | },
23 | module: {
24 | loaders: [{
25 | test: /.jsx?$/,
26 | include: Path.join(__dirname, '../src/app'),
27 | loader: 'babel',
28 | }],
29 | },
30 | plugins: [
31 | new Webpack.DefinePlugin({
32 | 'process.env': {
33 | NODE_ENV: JSON.stringify(options.isProduction ? 'production' : 'development'),
34 | },
35 | }),
36 | new HtmlWebpackPlugin({
37 | template: Path.join(__dirname, '../src/index.html'),
38 | }),
39 | ],
40 | };
41 |
42 | if (options.isProduction) {
43 | webpackConfig.entry = [Path.join(__dirname, '../src/app/index')];
44 |
45 | webpackConfig.plugins.push(
46 | new Webpack.optimize.OccurenceOrderPlugin(),
47 | new Webpack.optimize.UglifyJsPlugin({
48 | compressor: {
49 | warnings: false,
50 | },
51 | }),
52 | ExtractSASS
53 | );
54 |
55 | webpackConfig.module.loaders.push({
56 | test: /\.scss$/,
57 | loader: ExtractSASS.extract(['css', 'sass']),
58 | });
59 | } else {
60 | webpackConfig.plugins.push(
61 | new Webpack.HotModuleReplacementPlugin()
62 | );
63 |
64 | webpackConfig.module.loaders.push({
65 | test: /\.scss$/,
66 | loaders: ['style', 'css', 'sass'],
67 | });
68 |
69 | webpackConfig.devServer = {
70 | contentBase: Path.join(__dirname, '../'),
71 | hot: true,
72 | port: options.port,
73 | inline: true,
74 | progress: true,
75 | historyApiFallback: true,
76 | stats: 'errors-only',
77 | };
78 | }
79 |
80 | return webpackConfig;
81 | };
82 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Token authentication system using Node, Mongo, React, Redux
2 |
3 | ## Features
4 |
5 | - Signin, Signup, Email verification, Password reset
6 | - Client side forms validation
7 | - Node/Express rest api
8 | - Nodemailer configuration, Email templates
9 | - Webpack configuration for server and client
10 | - SCSS configuration
11 | - Linting with Airbnb eslint configuration
12 |
13 | ## Screenshot
14 |
15 | 
16 |
17 | ## Getting Started
18 |
19 | Clone Repo
20 |
21 | ````
22 | git clone https://github.com/DimiMikadze/node-redux-auth.git
23 | ````
24 |
25 | # Server
26 |
27 | npm install dependencies
28 |
29 | ````
30 | cd node-redux-auth/server
31 |
32 | npm install
33 | ````
34 |
35 | Create index.js file inside src/config folder.
36 |
37 | example index.js:
38 |
39 | ````
40 | export const dbConfig = {
41 | secret: 'SomeRandomSecretString',
42 | db: 'mongodb://localhost:auth/auth',
43 | };
44 |
45 | export const emailConfig = {
46 | service: 'Gmail',
47 | auth: {
48 | user: 'reduxauth@gmail.com',
49 | pass: 'Password',
50 | },
51 | };
52 |
53 | export const ROOT_URL = process.env.NODE_ENV === 'production' ? 'http://dimimikadze.com:3000' : 'http://localhost:3000';
54 |
55 | ````
56 |
57 | Start Mongodb
58 |
59 | ````
60 | mongod
61 | ````
62 |
63 | # Client
64 |
65 | npm install dependencies
66 |
67 | ````
68 | cd node-redux-auth/client
69 |
70 | npm install
71 | ````
72 |
73 | Commands
74 | --------
75 |
76 | Open the terminal and go to the folder server/ and run `npm run dev`. The server is gonna start and listen in the port 3333.
77 |
78 | Open a new terminal and go to the folder client/ and run `npm run dev`. The client is gonna start and listen in the port 3000.
79 |
80 | The client is reachable on `localhost:3000/reduxauth`.
81 |
82 | |Script|Description|
83 | |---|---|
84 | |`npm run dev`| Run development server |
85 | |`npm run dev`| Run development client |
86 | |`npm run build`| build the application to `./dist`|
87 | |`npm start`| Start production server with pm2 from `./dist`|
88 |
89 | ### Contributing
90 |
91 | contributions are welcome!
92 |
93 | ### License
94 |
95 | MIT
96 |
--------------------------------------------------------------------------------
/server/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015"]
3 | }
--------------------------------------------------------------------------------
/server/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 | "rules": {
4 | "consistent-return": 0,
5 | "no-shadow": 0,
6 | "no-console": 0,
7 | "no-unused-vars": 0,
8 | "func-names": 0,
9 | "max-len": 0,
10 | "quotes": 0,
11 | }
12 | }
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /.idea
3 | /src/config/index.js
4 | /dist/config/index.js
5 | .DS_Store
6 | npm-debug.log
--------------------------------------------------------------------------------
/server/dist/config/example.index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | var dbConfig = exports.dbConfig = {
7 | secret: 'SomeRandomSecretString',
8 | db: 'mongodb://localhost:auth/auth'
9 | };
10 |
11 | var emailConfig = exports.emailConfig = {
12 | service: 'Gmail',
13 | auth: {
14 | user: 'email@gmail.com',
15 | pass: 'Password'
16 | }
17 | };
18 |
19 | var ROOT_URL = exports.ROOT_URL = process.env.NODE_ENV === 'production' ? 'http://dimitrimikadze.com:3000' : 'http://localhost:3000';
--------------------------------------------------------------------------------
/server/dist/controllers/authController.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.verifiEmail = exports.resendVerification = exports.signup = exports.signin = undefined;
7 |
8 | var _nodemailer = require('nodemailer');
9 |
10 | var _nodemailer2 = _interopRequireDefault(_nodemailer);
11 |
12 | var _bcryptNodejs = require('bcrypt-nodejs');
13 |
14 | var _bcryptNodejs2 = _interopRequireDefault(_bcryptNodejs);
15 |
16 | var _user = require('../models/user');
17 |
18 | var _user2 = _interopRequireDefault(_user);
19 |
20 | var _email = require('../helpers/email');
21 |
22 | var _token = require('../helpers/token');
23 |
24 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
25 |
26 | /**
27 | * Sign in
28 | */
29 | var signin = exports.signin = function signin(req, res) {
30 | var _req$user = req.user;
31 | var firstname = _req$user.firstname;
32 | var lastname = _req$user.lastname;
33 | var email = _req$user.email;
34 |
35 |
36 | res.json({ token: (0, _token.tokenForUser)(req.user), firstname: firstname, lastname: lastname, email: email });
37 | };
38 |
39 | /**
40 | * Sign up
41 | */
42 | var signup = exports.signup = function signup(req, res, next) {
43 | var _req$body = req.body;
44 | var firstname = _req$body.firstname;
45 | var lastname = _req$body.lastname;
46 | var email = _req$body.email;
47 | var password = _req$body.password;
48 |
49 |
50 | if (!firstname || !lastname || !email || !password) {
51 | return res.status(422).send({ error: "all fields are required" });
52 | }
53 |
54 | _user2.default.findOne({ email: email }, function (err, existingUser) {
55 | if (err) {
56 | return next(err);
57 | }
58 |
59 | if (existingUser) {
60 | return res.status(422).send({ error: "Email is in use" });
61 | }
62 |
63 | var user = new _user2.default({ firstname: firstname, lastname: lastname, email: email, password: password });
64 |
65 | user.save(function (err) {
66 | if (err) {
67 | return next(err);
68 | }
69 |
70 | (0, _email.sendVerificationEmail)(email, firstname, user.auth.token);
71 |
72 | res.json({ firstname: firstname, lastname: lastname, email: email });
73 | });
74 | });
75 | };
76 |
77 | /**
78 | * Resend verification code
79 | */
80 | var resendVerification = exports.resendVerification = function resendVerification(req, res, next) {
81 | var email = req.body.email;
82 |
83 |
84 | _user2.default.findOne({ email: email }, function (err, user) {
85 | if (err) {
86 | return next(err);
87 | }
88 |
89 | var tomorrow = new Date();
90 | tomorrow.setDate(tomorrow.getDate() + 1);
91 |
92 | _user2.default.findByIdAndUpdate(user.id, { auth: { used: false, token: user.auth.token, expires: tomorrow } }, function (err) {
93 | if (err) {
94 | return next(err);
95 | }
96 |
97 | var firstname = user.firstname;
98 | var email = user.email;
99 |
100 |
101 | (0, _email.sendVerificationEmail)(email, firstname, user.auth.token);
102 |
103 | res.json({ success: true });
104 | });
105 | });
106 | };
107 |
108 | /**
109 | * Verify email
110 | */
111 | var verifiEmail = exports.verifiEmail = function verifiEmail(req, res, next) {
112 | var _req$body2 = req.body;
113 | var email = _req$body2.email;
114 | var token = _req$body2.token;
115 |
116 |
117 | _user2.default.findOne({ email: email }, function (err, user) {
118 | if (err) {
119 | return next(err);
120 | }
121 |
122 | if (!user) {
123 | return res.status(422).send({ error: { message: "User doesnt exists", resend: false } });
124 | }
125 |
126 | if (user.auth.used) {
127 | return res.status(422).send({ error: { message: "link already used", resend: false } });
128 | }
129 |
130 | if (new Date() > user.auth.expires) {
131 | return res.status(422).send({ error: { message: "link already expired", resend: true } });
132 | }
133 |
134 | if (token !== user.auth.token) {
135 | return res.status(422).send({ error: { message: "something has gone wrong, please sign up again", resend: false } });
136 | }
137 |
138 | _user2.default.findByIdAndUpdate(user.id, { role: 1, auth: { used: true } }, function (err) {
139 | if (err) {
140 | return next(err);
141 | }
142 |
143 | var email = user.email;
144 | var firstname = user.firstname;
145 | var lastname = user.lastname;
146 |
147 |
148 | res.json({ token: (0, _token.tokenForUser)(user), email: email, firstname: firstname, lastname: lastname });
149 | });
150 | });
151 | };
--------------------------------------------------------------------------------
/server/dist/controllers/resetPasswordController.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.resetPasswordNew = exports.verifyResetPassword = exports.resetPassword = undefined;
7 |
8 | var _nodemailer = require('nodemailer');
9 |
10 | var _nodemailer2 = _interopRequireDefault(_nodemailer);
11 |
12 | var _bcryptNodejs = require('bcrypt-nodejs');
13 |
14 | var _bcryptNodejs2 = _interopRequireDefault(_bcryptNodejs);
15 |
16 | var _user = require('../models/user');
17 |
18 | var _user2 = _interopRequireDefault(_user);
19 |
20 | var _config = require('../config');
21 |
22 | var _email = require('../helpers/email');
23 |
24 | var _token = require('../helpers/token');
25 |
26 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
27 |
28 | /**
29 | * Reset password
30 | */
31 | var resetPassword = exports.resetPassword = function resetPassword(req, res, next) {
32 | var email = req.body.email;
33 |
34 | _user2.default.findOne({ email: email }, function (err, user) {
35 | if (err) {
36 | return next(err);
37 | }
38 |
39 | if (!user) {
40 | return res.status(422).send({ error: "email doesn't exists" });
41 | }
42 |
43 | var token = (0, _token.tokenForUser)(user);
44 |
45 | var afterOneHour = new Date();
46 | afterOneHour.setHours(afterOneHour.getHours() + 1);
47 |
48 | _user2.default.findByIdAndUpdate(user.id, { resetPassword: { token: token, used: 0, expires: afterOneHour } }, function (err) {
49 | if (err) {
50 | return next(err);
51 | }
52 |
53 | (0, _email.sendResetPassword)(email, user.firstname, token);
54 |
55 | res.json({ success: true });
56 | });
57 | });
58 | };
59 |
60 | /**
61 | * Verify reset password
62 | */
63 | var verifyResetPassword = exports.verifyResetPassword = function verifyResetPassword(req, res, next) {
64 | var _req$body = req.body;
65 | var email = _req$body.email;
66 | var token = _req$body.token;
67 |
68 |
69 | _user2.default.findOne({ email: email }, function (err, user) {
70 | if (err) {
71 | return next(err);
72 | }
73 |
74 | if (!user) {
75 | return res.status(422).send({ error: { message: "email doesn't exists", resend: false } });
76 | }
77 |
78 | if (user.resetPassword.used) {
79 | return res.status(422).send({ error: { message: "link already used, please request reset password again", resend: true } });
80 | }
81 |
82 | if (new Date() > user.resetPassword.expires) {
83 | return res.status(422).send({ error: { message: "link already expired, please request reset password again", resend: true } });
84 | }
85 |
86 | if (token !== user.resetPassword.token) {
87 | return res.status(422).send({ error: { message: "something has gone wrong, please request reset password again", resend: true } });
88 | }
89 |
90 | res.json({ success: true });
91 | });
92 | };
93 |
94 | /**
95 | * Reset password, new password
96 | */
97 | var resetPasswordNew = exports.resetPasswordNew = function resetPasswordNew(req, res, next) {
98 | var _req$body2 = req.body;
99 | var email = _req$body2.email;
100 | var newpassword = _req$body2.newpassword;
101 | var token = _req$body2.token;
102 |
103 |
104 | _user2.default.findOne({ email: email }, function (err, user) {
105 | if (!user) {
106 | return res.status(422).send({ error: { message: "email doesn't exists", resend: false } });
107 | }
108 |
109 | if (user.resetPassword.used) {
110 | return res.status(422).send({ error: { message: "link already used, please request reset password again", resend: true } });
111 | }
112 |
113 | if (new Date() > user.resetPassword.expires) {
114 | return res.status(422).send({ error: { message: "link already expired, please request reset password again", resend: true } });
115 | }
116 |
117 | if (token !== user.resetPassword.token) {
118 | return res.status(422).send({ error: { message: "something has gone wrong, please request reset password again", resend: true } });
119 | }
120 |
121 | _bcryptNodejs2.default.genSalt(10, function (err, salt) {
122 | if (err) {
123 | return next(err);
124 | }
125 |
126 | _bcryptNodejs2.default.hash(newpassword, salt, null, function (err, hash) {
127 | if (err) {
128 | return next(err);
129 | }
130 |
131 | _user2.default.findByIdAndUpdate(user.id, { password: hash, resetPassword: {} }, function (err) {
132 | if (err) {
133 | return next(err);
134 | }
135 |
136 | var firstname = user.firstname;
137 | var lastname = user.lastname;
138 | var email = user.email;
139 |
140 |
141 | res.json({ firstname: firstname, lastname: lastname, email: email, token: (0, _token.tokenForUser)(user) });
142 | });
143 | });
144 | });
145 | });
146 | };
--------------------------------------------------------------------------------
/server/dist/controllers/usersController.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.fetchUsers = undefined;
7 |
8 | var _user = require('../models/user');
9 |
10 | var _user2 = _interopRequireDefault(_user);
11 |
12 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
13 |
14 | /**
15 | * Fetch user firstnames
16 | */
17 | var fetchUsers = exports.fetchUsers = function fetchUsers(req, res, next) {
18 | _user2.default.find({}, 'firstname', function (err, users) {
19 | if (err) {
20 | return next(err);
21 | }
22 |
23 | res.json(users);
24 | });
25 | };
--------------------------------------------------------------------------------
/server/dist/helpers/email.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.sendVerificationEmail = sendVerificationEmail;
7 | exports.sendResetPassword = sendResetPassword;
8 |
9 | var _nodemailer = require('nodemailer');
10 |
11 | var _nodemailer2 = _interopRequireDefault(_nodemailer);
12 |
13 | var _config = require('../config');
14 |
15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
16 |
17 | /* eslint-disable prefer-template */
18 |
19 | var transporter = _nodemailer2.default.createTransport(_config.emailConfig);
20 |
21 | var from = 'Redux Auth Team';
22 |
23 | function sendVerificationEmail(email, firstName, token) {
24 | var html = "" + "
" + "
" + "
Hi, " + firstName + " " + "
Click the big button below to activate your account.
" + "
Activate Account " + "
Redux Auth Team ";
25 |
26 | transporter.sendMail({
27 | from: from,
28 | to: email,
29 | subject: 'Verify Email',
30 | html: html
31 | }, function (err) {
32 | if (err) {
33 | return err;
34 | }
35 | });
36 | }
37 |
38 | function sendResetPassword(email, firstName, token) {
39 | var html = "" + "
" + "
" + "
Hi, " + firstName + " " + "
We've received a request to reset your password. if you didn't make the request, just ignore this email. Otherwise, you can reset your password using this link
" + "
Click here to reset your password " + "
Redux Auth Team ";
40 |
41 | transporter.sendMail({
42 | from: from,
43 | to: email,
44 | subject: 'Password Reset',
45 | html: html
46 | }, function (err) {
47 | if (err) {
48 | return err;
49 | }
50 | });
51 | }
--------------------------------------------------------------------------------
/server/dist/helpers/token.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.tokenForUser = tokenForUser;
7 |
8 | var _jwtSimple = require('jwt-simple');
9 |
10 | var _jwtSimple2 = _interopRequireDefault(_jwtSimple);
11 |
12 | var _config = require('../config');
13 |
14 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
15 |
16 | function tokenForUser(user) {
17 | var timestamp = new Date().getTime();
18 |
19 | return _jwtSimple2.default.encode({ sub: user.id, iat: timestamp }, _config.dbConfig.secret);
20 | }
--------------------------------------------------------------------------------
/server/dist/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _express = require('express');
4 |
5 | var _express2 = _interopRequireDefault(_express);
6 |
7 | var _http = require('http');
8 |
9 | var _http2 = _interopRequireDefault(_http);
10 |
11 | var _bodyParser = require('body-parser');
12 |
13 | var _bodyParser2 = _interopRequireDefault(_bodyParser);
14 |
15 | var _morgan = require('morgan');
16 |
17 | var _morgan2 = _interopRequireDefault(_morgan);
18 |
19 | var _mongoose = require('mongoose');
20 |
21 | var _mongoose2 = _interopRequireDefault(_mongoose);
22 |
23 | var _cors = require('cors');
24 |
25 | var _cors2 = _interopRequireDefault(_cors);
26 |
27 | var _compression = require('compression');
28 |
29 | var _compression2 = _interopRequireDefault(_compression);
30 |
31 | var _router = require('./router');
32 |
33 | var _router2 = _interopRequireDefault(_router);
34 |
35 | var _config = require('./config');
36 |
37 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
38 |
39 | var app = (0, _express2.default)();
40 |
41 | _mongoose2.default.connect(_config.dbConfig.db);
42 | _mongoose2.default.set('debug', true);
43 |
44 | app.use((0, _compression2.default)());
45 | app.use((0, _morgan2.default)('combined'));
46 | app.use((0, _cors2.default)());
47 | app.use(_bodyParser2.default.json({ type: '*/*' }));
48 | (0, _router2.default)(app);
49 |
50 | var port = process.env.PORT || 3333;
51 | var server = _http2.default.createServer(app);
52 | server.listen(port);
53 | console.log('server listening on:', port);
--------------------------------------------------------------------------------
/server/dist/models/user.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _mongoose = require('mongoose');
8 |
9 | var _mongoose2 = _interopRequireDefault(_mongoose);
10 |
11 | var _bcryptNodejs = require('bcrypt-nodejs');
12 |
13 | var _bcryptNodejs2 = _interopRequireDefault(_bcryptNodejs);
14 |
15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
16 |
17 | var Schema = _mongoose2.default.Schema;
18 |
19 | var userSchema = new Schema({
20 | firstname: String,
21 | lastname: String,
22 | email: { type: String, lowercase: true, unique: true },
23 | password: String,
24 | role: { type: Number, default: 0 },
25 | auth: {
26 | token: String,
27 | used: Boolean,
28 | expires: Date
29 | },
30 | resetPassword: {
31 | token: String,
32 | used: Boolean,
33 | expires: Date
34 | }
35 | });
36 |
37 | userSchema.pre('save', function (next) {
38 | var user = this;
39 |
40 | _bcryptNodejs2.default.genSalt(10, function (err, salt) {
41 | if (err) {
42 | return next(err);
43 | }
44 |
45 | _bcryptNodejs2.default.hash(user.password, salt, null, function (err, hash) {
46 | if (err) {
47 | return next(err);
48 | }
49 |
50 | var tomorrow = new Date();
51 | tomorrow.setDate(tomorrow.getDate() + 1);
52 |
53 | user.password = hash;
54 | user.auth = { token: salt, used: 0, expires: tomorrow };
55 | next();
56 | });
57 | });
58 | });
59 |
60 | userSchema.methods.comparePassword = function (candidatePassword, callback) {
61 | _bcryptNodejs2.default.compare(candidatePassword, this.password, function (err, isMatch) {
62 | if (err) {
63 | return callback(err);
64 | }
65 |
66 | callback(null, isMatch);
67 | });
68 | };
69 |
70 | exports.default = _mongoose2.default.model('user', userSchema);
--------------------------------------------------------------------------------
/server/dist/router.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _passport = require('passport');
8 |
9 | var _passport2 = _interopRequireDefault(_passport);
10 |
11 | var _authController = require('./controllers/authController');
12 |
13 | var _resetPasswordController = require('./controllers/resetPasswordController');
14 |
15 | var _usersController = require('./controllers/usersController');
16 |
17 | var _passport3 = require('./services/passport');
18 |
19 | var _passport4 = _interopRequireDefault(_passport3);
20 |
21 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
22 |
23 | var requireAuth = _passport2.default.authenticate('jwt', { session: false });
24 | var requireSignin = _passport2.default.authenticate('local', { session: false });
25 |
26 | var router = function router(app) {
27 | app.get('/', requireAuth, _usersController.fetchUsers);
28 | app.post('/signup', _authController.signup);
29 | app.post('/signup/verify-email', _authController.verifiEmail);
30 | app.post('/resend-verify-code', _authController.resendVerification);
31 | app.post('/signin', requireSignin, _authController.signin);
32 | app.post('/reset-password', _resetPasswordController.resetPassword);
33 | app.post('/reset-password/verify', _resetPasswordController.verifyResetPassword);
34 | app.post('/reset-password/new', _resetPasswordController.resetPasswordNew);
35 | };
36 |
37 | exports.default = router;
--------------------------------------------------------------------------------
/server/dist/services/passport.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _passport = require('passport');
4 |
5 | var _passport2 = _interopRequireDefault(_passport);
6 |
7 | var _user = require('../models/user');
8 |
9 | var _user2 = _interopRequireDefault(_user);
10 |
11 | var _config = require('../config');
12 |
13 | var _passportLocal = require('passport-local');
14 |
15 | var _passportLocal2 = _interopRequireDefault(_passportLocal);
16 |
17 | var _passportJwt = require('passport-jwt');
18 |
19 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
20 |
21 | var localOptions = { usernameField: 'email' };
22 | var localLogin = new _passportLocal2.default(localOptions, function (email, password, done) {
23 | _user2.default.findOne({ email: email }, function (err, user) {
24 | if (err) {
25 | return done(err);
26 | }
27 |
28 | if (!user) {
29 | return done(null, false);
30 | }
31 |
32 | user.comparePassword(password, function (err, isMatch) {
33 | if (err) {
34 | return done(err);
35 | }
36 |
37 | if (!isMatch) {
38 | return done(null, false);
39 | }
40 |
41 | if (user.role < 1) {
42 | return done(null, false);
43 | }
44 |
45 | return done(null, user);
46 | });
47 | });
48 | });
49 |
50 | var jwtOptions = {
51 | jwtFromRequest: _passportJwt.ExtractJwt.fromHeader('authorization'),
52 | secretOrKey: _config.dbConfig.secret
53 | };
54 |
55 | var jwtLogin = new _passportJwt.Strategy(jwtOptions, function (payload, done) {
56 | _user2.default.findById(payload.sub, function (err, user) {
57 | if (err) {
58 | return done(err, false);
59 | }
60 |
61 | if (user) {
62 | done(null, user);
63 | } else {
64 | done(null, false);
65 | }
66 | });
67 | });
68 |
69 | _passport2.default.use(jwtLogin);
70 | _passport2.default.use(localLogin);
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node-redux-auth",
3 | "version": "1.0.0",
4 | "description": "Token authentication system using Node, Mongo, React, Redux",
5 | "scripts": {
6 | "dev": "nodemon --exec babel-node src/index.js",
7 | "build": "rm -rf dist && babel src --out-dir dist",
8 | "start": "NODE_ENV=production PORT=3333cs pm2 start dist/index.js",
9 | "lint": "eslint src"
10 | },
11 | "keywords": [
12 | "Express",
13 | "MongoDB",
14 | "React",
15 | "Redux",
16 | "Token Authentication",
17 | "Airbnb Eslint",
18 | "SCSS",
19 | "Babel",
20 | "Webpack Configuration"
21 | ],
22 | "repository": {
23 | "type": "git",
24 | "url": "git+https://github.com/DimitriMikadze/node-redux-auth"
25 | },
26 | "author": "Dimtiri Mikadze",
27 | "license": "MIT",
28 | "devDependencies": {
29 | "babel-cli": "^6.9.0",
30 | "babel-preset-es2015": "^6.9.0",
31 | "eslint": "^2.11.1",
32 | "eslint-config-airbnb": "^9.0.1",
33 | "eslint-plugin-import": "^1.8.1",
34 | "eslint-plugin-jsx-a11y": "^1.3.0",
35 | "eslint-plugin-react": "^5.1.1",
36 | "nodemon": "^1.9.2"
37 | },
38 | "dependencies": {
39 | "bcrypt-nodejs": "0.0.3",
40 | "body-parser": "^1.15.1",
41 | "compression": "^1.6.2",
42 | "cors": "^2.7.1",
43 | "express": "^4.13.4",
44 | "jwt-simple": "^0.5.0",
45 | "mongoose": "^4.4.16",
46 | "morgan": "^1.7.0",
47 | "nodemailer": "^2.4.2",
48 | "passport": "^0.3.2",
49 | "passport-jwt": "^2.0.0",
50 | "passport-local": "^1.0.0"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/server/src/config/example.index.js:
--------------------------------------------------------------------------------
1 | export const dbConfig = {
2 | secret: 'SomeRandomSecretString',
3 | db: 'mongodb://localhost:auth/auth',
4 | };
5 |
6 | export const emailConfig = {
7 | service: 'Gmail',
8 | auth: {
9 | user: 'email@gmail.com',
10 | pass: 'Password',
11 | },
12 | };
13 |
14 | export const ROOT_URL = process.env.NODE_ENV === 'production' ? 'http://dimitrimikadze.com:3000' : 'http://localhost:3000';
15 |
--------------------------------------------------------------------------------
/server/src/controllers/authController.js:
--------------------------------------------------------------------------------
1 | import nodemailer from 'nodemailer';
2 | import bcrypt from 'bcrypt-nodejs';
3 | import User from '../models/user';
4 | import { sendVerificationEmail } from '../helpers/email';
5 | import { tokenForUser } from '../helpers/token';
6 |
7 | /**
8 | * Sign in
9 | */
10 | export const signin = (req, res) => {
11 | const { firstname, lastname, email } = req.user;
12 |
13 | res.json({ token: tokenForUser(req.user), firstname, lastname, email });
14 | };
15 |
16 | /**
17 | * Sign up
18 | */
19 | export const signup = (req, res, next) => {
20 | const { firstname, lastname, email, password } = req.body;
21 |
22 | if (!firstname || !lastname || !email || !password) {
23 | return res.status(422).send({ error: "all fields are required" });
24 | }
25 |
26 | User.findOne({ email }, (err, existingUser) => {
27 | if (err) { return next(err); }
28 |
29 | if (existingUser) {
30 | return res.status(422).send({ error: "Email is in use" });
31 | }
32 |
33 | const user = new User({ firstname, lastname, email, password });
34 |
35 | user.save((err) => {
36 | if (err) { return next(err); }
37 |
38 | sendVerificationEmail(email, firstname, user.auth.token);
39 |
40 | res.json({ firstname, lastname, email });
41 | });
42 | });
43 | };
44 |
45 | /**
46 | * Resend verification code
47 | */
48 | export const resendVerification = (req, res, next) => {
49 | const { email } = req.body;
50 |
51 | User.findOne({ email }, (err, user) => {
52 | if (err) { return next(err); }
53 |
54 | const tomorrow = new Date();
55 | tomorrow.setDate(tomorrow.getDate() + 1);
56 |
57 | User.findByIdAndUpdate(user.id, { auth: { used: false, token: user.auth.token, expires: tomorrow } }, (err) => {
58 | if (err) { return next(err); }
59 |
60 | const { firstname, email } = user;
61 |
62 | sendVerificationEmail(email, firstname, user.auth.token);
63 |
64 | res.json({ success: true });
65 | });
66 | });
67 | };
68 |
69 | /**
70 | * Verify email
71 | */
72 | export const verifiEmail = (req, res, next) => {
73 | const { email, token } = req.body;
74 |
75 | User.findOne({ email }, (err, user) => {
76 | if (err) { return next(err); }
77 |
78 | if (!user) {
79 | return res.status(422).send({ error: { message: "User doesnt exists", resend: false } });
80 | }
81 |
82 | if (user.auth.used) {
83 | return res.status(422).send({ error: { message: "link already used", resend: false } });
84 | }
85 |
86 | if (new Date() > user.auth.expires) {
87 | return res.status(422).send({ error: { message: "link already expired", resend: true } });
88 | }
89 |
90 | if (token !== user.auth.token) {
91 | return res.status(422).send({ error: { message: "something has gone wrong, please sign up again", resend: false } });
92 | }
93 |
94 | User.findByIdAndUpdate(user.id, { role: 1, auth: { used: true } }, (err) => {
95 | if (err) { return next(err); }
96 |
97 | const { email, firstname, lastname } = user;
98 |
99 | res.json({ token: tokenForUser(user), email, firstname, lastname });
100 | });
101 | });
102 | };
103 |
--------------------------------------------------------------------------------
/server/src/controllers/resetPasswordController.js:
--------------------------------------------------------------------------------
1 | import nodemailer from 'nodemailer';
2 | import bcrypt from 'bcrypt-nodejs';
3 | import User from '../models/user';
4 | import { dbConfig, emailConfig } from '../config';
5 | import { sendResetPassword } from '../helpers/email';
6 | import { tokenForUser } from '../helpers/token';
7 |
8 | /**
9 | * Reset password
10 | */
11 | export const resetPassword = (req, res, next) => {
12 | const email = req.body.email;
13 |
14 | User.findOne({ email }, (err, user) => {
15 | if (err) { return next(err); }
16 |
17 | if (!user) {
18 | return res.status(422).send({ error: "email doesn't exists" });
19 | }
20 |
21 | const token = tokenForUser(user);
22 |
23 | const afterOneHour = new Date();
24 | afterOneHour.setHours(afterOneHour.getHours() + 1);
25 |
26 | User.findByIdAndUpdate(user.id, { resetPassword: { token, used: 0, expires: afterOneHour } }, (err) => {
27 | if (err) { return next(err); }
28 |
29 | sendResetPassword(email, user.firstname, token);
30 |
31 | res.json({ success: true });
32 | });
33 | });
34 | };
35 |
36 | /**
37 | * Verify reset password
38 | */
39 | export const verifyResetPassword = (req, res, next) => {
40 | const { email, token } = req.body;
41 |
42 | User.findOne({ email }, (err, user) => {
43 | if (err) { return next(err); }
44 |
45 | if (!user) {
46 | return res.status(422).send({ error: { message: "email doesn't exists", resend: false } });
47 | }
48 |
49 | if (user.resetPassword.used) {
50 | return res.status(422).send({ error: { message: "link already used, please request reset password again", resend: true } });
51 | }
52 |
53 | if (new Date() > user.resetPassword.expires) {
54 | return res.status(422).send({ error: { message: "link already expired, please request reset password again", resend: true } });
55 | }
56 |
57 | if (token !== user.resetPassword.token) {
58 | return res.status(422).send({ error: { message: "something has gone wrong, please request reset password again", resend: true } });
59 | }
60 |
61 | res.json({ success: true });
62 | });
63 | };
64 |
65 | /**
66 | * Reset password, new password
67 | */
68 | export const resetPasswordNew = (req, res, next) => {
69 | const { email, newpassword, token } = req.body;
70 |
71 | User.findOne({ email }, (err, user) => {
72 | if (!user) {
73 | return res.status(422).send({ error: { message: "email doesn't exists", resend: false } });
74 | }
75 |
76 | if (user.resetPassword.used) {
77 | return res.status(422).send({ error: { message: "link already used, please request reset password again", resend: true } });
78 | }
79 |
80 | if (new Date() > user.resetPassword.expires) {
81 | return res.status(422).send({ error: { message: "link already expired, please request reset password again", resend: true } });
82 | }
83 |
84 | if (token !== user.resetPassword.token) {
85 | return res.status(422).send({ error: { message: "something has gone wrong, please request reset password again", resend: true } });
86 | }
87 |
88 | bcrypt.genSalt(10, (err, salt) => {
89 | if (err) { return next(err); }
90 |
91 | bcrypt.hash(newpassword, salt, null, (err, hash) => {
92 | if (err) { return next(err); }
93 |
94 | User.findByIdAndUpdate(user.id, { password: hash, resetPassword: {} }, (err) => {
95 | if (err) { return next(err); }
96 |
97 | const { firstname, lastname, email } = user;
98 |
99 | res.json({ firstname, lastname, email, token: tokenForUser(user) });
100 | });
101 | });
102 | });
103 | });
104 | };
105 |
--------------------------------------------------------------------------------
/server/src/controllers/usersController.js:
--------------------------------------------------------------------------------
1 | import User from '../models/user';
2 |
3 | /**
4 | * Fetch user firstnames
5 | */
6 | export const fetchUsers = (req, res, next) => {
7 | User.find({}, 'firstname', (err, users) => {
8 | if (err) { return next(err); }
9 |
10 | res.json(users);
11 | });
12 | };
13 |
--------------------------------------------------------------------------------
/server/src/helpers/email.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable prefer-template */
2 | import nodemailer from 'nodemailer';
3 | import { emailConfig, ROOT_URL } from '../config';
4 | const transporter = nodemailer.createTransport(emailConfig);
5 |
6 | const from = 'Redux Auth Team';
7 |
8 | export function sendVerificationEmail(email, firstName, token) {
9 | const html = "" +
10 | "
" +
11 | "
" +
12 | "
Hi, " + firstName + " " +
13 | "
Click the big button below to activate your account.
" +
14 | "
Activate Account " +
15 | "
Redux Auth Team ";
16 |
17 | transporter.sendMail({
18 | from,
19 | to: email,
20 | subject: 'Verify Email',
21 | html,
22 | }, (err) => { if (err) { return err; } });
23 | }
24 |
25 | export function sendResetPassword(email, firstName, token) {
26 | const html = "" +
27 | "
" +
28 | "
" +
29 | "
Hi, " + firstName + " " +
30 | "
We've received a request to reset your password. if you didn't make the request, just ignore this email. Otherwise, you can reset your password using this link
" +
31 | "
Click here to reset your password " +
32 | "
Redux Auth Team ";
33 |
34 | transporter.sendMail({
35 | from,
36 | to: email,
37 | subject: 'Password Reset',
38 | html,
39 | }, (err) => { if (err) { return err; } });
40 | }
41 |
--------------------------------------------------------------------------------
/server/src/helpers/token.js:
--------------------------------------------------------------------------------
1 | import jwt from 'jwt-simple';
2 | import { dbConfig } from '../config';
3 |
4 | export function tokenForUser(user) {
5 | const timestamp = new Date().getTime();
6 |
7 | return jwt.encode({ sub: user.id, iat: timestamp }, dbConfig.secret);
8 | }
9 |
--------------------------------------------------------------------------------
/server/src/index.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import http from 'http';
3 | import bodyParser from 'body-parser';
4 | import morgan from 'morgan';
5 | import mongoose from 'mongoose';
6 | import cors from 'cors';
7 | import compression from 'compression';
8 | import router from './router';
9 | import { dbConfig } from './config';
10 |
11 | const app = express();
12 |
13 | mongoose.connect(dbConfig.db);
14 | mongoose.set('debug', true);
15 |
16 | app.use(compression());
17 | app.use(morgan('combined'));
18 | app.use(cors());
19 | app.use(bodyParser.json({ type: '*/*' }));
20 | router(app);
21 |
22 | const port = process.env.PORT || 3333;
23 | const server = http.createServer(app);
24 | server.listen(port);
25 | console.log('server listening on:', port);
26 |
--------------------------------------------------------------------------------
/server/src/models/user.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 | import bcrypt from 'bcrypt-nodejs';
3 |
4 | const Schema = mongoose.Schema;
5 |
6 | const userSchema = new Schema({
7 | firstname: String,
8 | lastname: String,
9 | email: { type: String, lowercase: true, unique: true },
10 | password: String,
11 | role: { type: Number, default: 0 },
12 | auth: {
13 | token: String,
14 | used: Boolean,
15 | expires: Date,
16 | },
17 | resetPassword: {
18 | token: String,
19 | used: Boolean,
20 | expires: Date,
21 | },
22 | });
23 |
24 | userSchema.pre('save', function (next) {
25 | const user = this;
26 |
27 | bcrypt.genSalt(10, (err, salt) => {
28 | if (err) { return next(err); }
29 |
30 | bcrypt.hash(user.password, salt, null, (err, hash) => {
31 | if (err) { return next(err); }
32 |
33 | const tomorrow = new Date();
34 | tomorrow.setDate(tomorrow.getDate() + 1);
35 |
36 | user.password = hash;
37 | user.auth = { token: salt, used: 0, expires: tomorrow };
38 | next();
39 | });
40 | });
41 | });
42 |
43 | userSchema.methods.comparePassword = function (candidatePassword, callback) {
44 | bcrypt.compare(candidatePassword, this.password, (err, isMatch) => {
45 | if (err) { return callback(err); }
46 |
47 | callback(null, isMatch);
48 | });
49 | };
50 |
51 | export default mongoose.model('user', userSchema);
52 |
--------------------------------------------------------------------------------
/server/src/router.js:
--------------------------------------------------------------------------------
1 | import passport from 'passport';
2 | import { signin, signup, verifiEmail, resendVerification } from './controllers/authController';
3 | import { resetPassword, verifyResetPassword, resetPasswordNew } from './controllers/resetPasswordController';
4 | import { fetchUsers } from './controllers/usersController';
5 | import passportService from './services/passport';
6 |
7 | const requireAuth = passport.authenticate('jwt', { session: false });
8 | const requireSignin = passport.authenticate('local', { session: false });
9 |
10 | const router = (app) => {
11 | app.get('/', requireAuth, fetchUsers);
12 | app.post('/signup', signup);
13 | app.post('/signup/verify-email', verifiEmail);
14 | app.post('/resend-verify-code', resendVerification);
15 | app.post('/signin', requireSignin, signin);
16 | app.post('/reset-password', resetPassword);
17 | app.post('/reset-password/verify', verifyResetPassword);
18 | app.post('/reset-password/new', resetPasswordNew);
19 | };
20 |
21 | export default router;
22 |
--------------------------------------------------------------------------------
/server/src/services/passport.js:
--------------------------------------------------------------------------------
1 | import passport from 'passport';
2 | import User from '../models/user';
3 | import { dbConfig } from '../config';
4 | import LocalStrategy from 'passport-local';
5 | import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';
6 |
7 | const localOptions = { usernameField: 'email' };
8 | const localLogin = new LocalStrategy(localOptions, (email, password, done) => {
9 | User.findOne({ email }, (err, user) => {
10 | if (err) { return done(err); }
11 |
12 | if (!user) { return done(null, false); }
13 |
14 | user.comparePassword(password, (err, isMatch) => {
15 | if (err) { return done(err); }
16 |
17 | if (!isMatch) { return done(null, false); }
18 |
19 | if (user.role < 1) { return done(null, false); }
20 |
21 | return done(null, user);
22 | });
23 | });
24 | });
25 |
26 | const jwtOptions = {
27 | jwtFromRequest: ExtractJwt.fromHeader('authorization'),
28 | secretOrKey: dbConfig.secret,
29 | };
30 |
31 | const jwtLogin = new JwtStrategy(jwtOptions, (payload, done) => {
32 | User.findById(payload.sub, (err, user) => {
33 | if (err) { return done(err, false); }
34 |
35 | if (user) {
36 | done(null, user);
37 | } else {
38 | done(null, false);
39 | }
40 | });
41 | });
42 |
43 | passport.use(jwtLogin);
44 | passport.use(localLogin);
45 |
--------------------------------------------------------------------------------