├── .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 |
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 |
30 |

Sign in

31 |
32 | 33 | {/* Email */} 34 | 35 | 36 | {/* Password */} 37 | 38 | 39 | {/* Forgot password */} 40 |
41 | I forgot my password 42 |
43 | 44 | {/* Server error message */} 45 | { this.props.errorMessage && this.props.errorMessage.signin && 46 |
Oops! { this.props.errorMessage.signin }
} 47 | 48 | {/* Signin button */} 49 | 50 | 51 | {/* Signup button */} 52 |
53 |

Don't have an account?

54 | Click here to sign up 55 |
56 | 57 |
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 |
30 |

Sign up

31 |
32 | 33 | {/* Firstname */} 34 | 35 | 36 | {/* Lastname */} 37 | 38 | 39 | {/* Email */} 40 | 41 | 42 | {/* Password */} 43 | 44 | 45 | {/* Email */} 46 | 47 | 48 | {/* Server error message */} 49 |
50 | { this.props.errorMessage && this.props.errorMessage.signup && 51 |
Oops! { this.props.errorMessage.signup }
} 52 |
53 | 54 | {/* Submit button */} 55 | 56 | 57 | {/* Sign in button */} 58 |
59 |

Already signed up?

60 | Click here to sign in 61 |
62 | 63 |
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 | 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 |
29 | 30 | {/* Email */} 31 |
32 | 33 |
34 | 35 | {/* Server error message */} 36 |
37 | { this.props.errorMessage && this.props.errorMessage.resetPassword && 38 |
{ this.props.errorMessage.resetPassword }
} 39 |
40 | 41 | {/* Submit button */} 42 | 43 |
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 |
55 | {/* New password */} 56 | 57 | 58 | {/* Repeat new password */} 59 | 60 | 61 | { 62 | /* Server error message */ 63 | this.props.errorMessage && this.props.errorMessage.verifyResetPassword && 64 |
{ this.props.errorMessage.verifyResetPassword.message }
65 | } 66 | 67 | {/* Submit button */} 68 | 69 | 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 | ![Screenshot](/client/static/images/screenshot.png) 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 | --------------------------------------------------------------------------------