├── server ├── .gitignore ├── config.js ├── models │ ├── comment.js │ ├── post.js │ └── user.js ├── package.json ├── index.js ├── router.js ├── services │ └── passport.js └── controllers │ ├── authentication.js │ ├── userinfo.js │ └── blog.js ├── client ├── src │ ├── components │ │ ├── footer.js │ │ ├── nomatch.js │ │ ├── blog │ │ │ ├── post_detail │ │ │ │ ├── post_body.js │ │ │ │ ├── comments.js │ │ │ │ ├── comment_new.js │ │ │ │ ├── post_edit.js │ │ │ │ └── index.js │ │ │ ├── post_mine.js │ │ │ ├── post_list.js │ │ │ └── post_new.js │ │ ├── auth │ │ │ ├── require_auth.js │ │ │ ├── signin.js │ │ │ └── signup.js │ │ ├── welcome.js │ │ ├── userinfo │ │ │ ├── settings.js │ │ │ └── profile.js │ │ └── header.js │ ├── reducers │ │ ├── profile_reducer.js │ │ ├── index.js │ │ ├── comments_reducer.js │ │ ├── auth_reducer.js │ │ └── posts_reducer.js │ ├── actions │ │ ├── types.js │ │ └── index.js │ └── index.js ├── .gitignore ├── package.json └── public │ ├── style.css │ └── index.html ├── LICENSE └── README.md /server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /server/config.js: -------------------------------------------------------------------------------- 1 | // Hold application secret and config 2 | module.exports = { 3 | secret: 'DAnAkNmnk2jHONa4wVK0' 4 | }; -------------------------------------------------------------------------------- /client/src/components/footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => { 4 | return ( 5 | 10 | ); 11 | } -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /client/src/components/nomatch.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // 404 4 | export default () => ( 5 |
6 |
7 |

Page Not Found

8 |
9 |

We couldn't find the page you requested. Try access other content.

10 |
11 |
12 | ); -------------------------------------------------------------------------------- /server/models/comment.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | // Define our model 5 | const commentSchema = new Schema({ 6 | content: String, // html 7 | authorId: String, 8 | authorName: String, 9 | postId: String, 10 | time: Date, 11 | }); 12 | 13 | // Create the model class 14 | const ModelClass = mongoose.model('comment', commentSchema); 15 | 16 | // Export the model 17 | module.exports = ModelClass; -------------------------------------------------------------------------------- /server/models/post.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | // Define our model 5 | const postSchema = new Schema({ 6 | title: String, 7 | categories: [String], 8 | content: String, // html 9 | authorId: String, 10 | authorName: String, 11 | time: Date, 12 | }); 13 | 14 | // Create the model class 15 | const ModelClass = mongoose.model('post', postSchema); 16 | 17 | // Export the model 18 | module.exports = ModelClass; -------------------------------------------------------------------------------- /client/src/reducers/profile_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | FETCH_PROFILE, 3 | CLEAR_PROFILE, 4 | UPDATE_PROFILE, 5 | } from '../actions/types'; 6 | 7 | export default function(state={}, action) { 8 | // Attention!!! The state object here refers to state.profile, instead of the application state. 9 | 10 | switch(action.type) { 11 | case FETCH_PROFILE: 12 | return { ...state, user: action.payload }; 13 | case CLEAR_PROFILE: // clear the local redux state 14 | return {}; 15 | case UPDATE_PROFILE: 16 | return { ...state, user: action.payload }; 17 | default: 18 | return state; 19 | } 20 | } -------------------------------------------------------------------------------- /client/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { reducer as formReducer } from 'redux-form'; 3 | 4 | import authReducer from './auth_reducer'; 5 | import profileReducer from './profile_reducer'; 6 | import postsReducer from './posts_reducer'; 7 | import commentsReducer from './comments_reducer'; 8 | 9 | const rootReducer = combineReducers({ 10 | form: formReducer, // the form property of state is going to be produced by ReduxForm reducer 11 | auth: authReducer, 12 | profile: profileReducer, 13 | posts: postsReducer, 14 | comments: commentsReducer, 15 | }); 16 | 17 | export default rootReducer; -------------------------------------------------------------------------------- /client/src/reducers/comments_reducer.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { 3 | CREATE_COMMENT, 4 | FETCH_COMMENTS, 5 | } from '../actions/types'; 6 | 7 | export default function(state = {}, action) { 8 | // Attention!!! The state object here refers to state.comments, instead of the application state. 9 | 10 | switch(action.type) { 11 | case FETCH_COMMENTS: 12 | return _.mapKeys(action.payload, '_id'); 13 | case CREATE_COMMENT: 14 | return { ...state, [action.payload._id]: action.payload }; // [] here is not for creating array, is for key interpolation, i.e. newState[action.payload.id] = action.payload 15 | default: 16 | return state; 17 | } 18 | } -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "nodemon index.js", 9 | "start": "node index.js" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "bcrypt-nodejs": "0.0.3", 15 | "body-parser": "^1.17.2", 16 | "cors": "^2.8.3", 17 | "express": "^4.15.3", 18 | "jwt-simple": "^0.5.1", 19 | "mongoose": "^4.11.0", 20 | "morgan": "^1.8.2", 21 | "nodemon": "^1.11.0", 22 | "passport": "^0.4.0", 23 | "passport-jwt": "^3.0.0", 24 | "passport-local": "^1.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /client/src/actions/types.js: -------------------------------------------------------------------------------- 1 | export const AUTH_USER = 'auth_user'; 2 | export const UNAUTH_USER = 'unauth_user'; 3 | 4 | export const FETCH_PROFILE = 'fetch_profile'; 5 | export const CLEAR_PROFILE = 'clear_profile'; 6 | export const UPDATE_PROFILE = 'update_profile'; 7 | 8 | export const FETCH_POSTS = 'fetch_posts'; 9 | export const CREATE_POST = 'create_post'; 10 | export const FETCH_POST = 'fetch_post'; 11 | export const UPDATE_POST = 'update_post'; 12 | export const DELETE_POST = 'delete_post'; 13 | 14 | export const CHECK_AUTHORITY = 'check_authority'; // check if the user has the authority to make change to a specific post 15 | 16 | export const CREATE_COMMENT = 'create_comment'; 17 | export const FETCH_COMMENTS = 'fetch_comments'; -------------------------------------------------------------------------------- /client/src/reducers/auth_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | AUTH_USER, 3 | UNAUTH_USER, 4 | 5 | CHECK_AUTHORITY, 6 | } from '../actions/types'; 7 | 8 | export default function(state = {}, action) { 9 | // Attention!!! The state object here refers to state.auth, instead of the application state. 10 | 11 | switch(action.type) { 12 | case AUTH_USER: 13 | return { ...state, authenticated: true, username: action.payload }; 14 | case UNAUTH_USER: 15 | return { ...state, authenticated: false, username: '' }; 16 | 17 | case CHECK_AUTHORITY: // check if the user has the authority to make change to a specific post 18 | return { ...state, allowChange: action.payload }; 19 | 20 | default: 21 | return state; 22 | } 23 | } -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.16.2", 7 | "lodash": "^4.17.4", 8 | "prop-types": "^15.5.10", 9 | "react": "^15.6.1", 10 | "react-bootstrap": "^0.31.3", 11 | "react-dom": "^15.6.1", 12 | "react-redux": "^5.0.5", 13 | "react-router-dom": "^4.1.1", 14 | "react-scripts": "1.0.13", 15 | "redux": "^3.7.1", 16 | "redux-form": "^7.0.4", 17 | "redux-thunk": "^2.2.0" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test --env=jsdom", 23 | "eject": "react-scripts eject" 24 | }, 25 | "proxy": "http://localhost:3090/" 26 | } 27 | -------------------------------------------------------------------------------- /client/src/reducers/posts_reducer.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { 3 | FETCH_POSTS, 4 | CREATE_POST, 5 | FETCH_POST, 6 | UPDATE_POST, 7 | DELETE_POST, 8 | } from '../actions/types'; 9 | 10 | export default function(state = {}, action) { 11 | // Attention!!! The state object here refers to state.posts, instead of the application state. 12 | 13 | switch(action.type) { 14 | case FETCH_POSTS: 15 | return _.mapKeys(action.payload, '_id'); 16 | case CREATE_POST: 17 | return { ...state, [action.payload._id]: action.payload }; // [] here is not for creating array, is for key interpolation, i.e. newState[action.payload.id] = action.payload 18 | case FETCH_POST: 19 | return { ...state, [action.payload._id]: action.payload }; 20 | case UPDATE_POST: 21 | return { ...state, [action.payload._id]: action.payload }; 22 | case DELETE_POST: 23 | return _.omit(state, action.payload); 24 | default: 25 | return state; 26 | } 27 | } -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | // Main starting point of the application 2 | const express = require('express'); 3 | const http = require('http'); 4 | const bodyParser = require('body-parser'); 5 | const morgan = require('morgan'); 6 | const app = express(); 7 | const router = require('./router'); 8 | const mongoose = require('mongoose'); 9 | // const cors = require('cors'); // we don't need it anymore, because we use proxy server instead 10 | 11 | // DB Setup (connect mongoose and instance of mongodb) 12 | mongoose.connect('mongodb://localhost:blog/blog'); 13 | 14 | // App Setup (morgan and body-parser are middleware in Express) 15 | app.use(morgan('combined')); // middleware for logging 16 | app.use(bodyParser.json({ type: '*/*' })); // middleware for helping parse incoming HTTP requests 17 | // app.use(cors()); // middleware for circumventing (规避) cors error 18 | 19 | // Router Setup 20 | router(app); 21 | 22 | // Server Setup 23 | const port = process.env.PORT || 3090; 24 | const server = http.createServer(app); 25 | server.listen(port); 26 | console.log('Server listening on: ', port); -------------------------------------------------------------------------------- /client/src/components/blog/post_detail/post_body.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class PostBody extends Component { 4 | 5 | renderTags(tags) { 6 | return tags.map(tag => { 7 | return {tag}; 8 | }); 9 | } 10 | 11 | render() { 12 | 13 | const {post} = this.props; 14 | 15 | // for displaying inner html: https://facebook.github.io/react/docs/dom-elements.html 16 | return ( 17 |
18 |

{post.title}

19 | {this.renderTags(post.categories)} 20 | 21 | {post.authorName} 22 | 23 | {new Date(post.time).toLocaleString()} 24 |
25 |
26 |
27 |
28 | ); 29 | } 30 | } 31 | 32 | export default PostBody; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Haichao Yu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /client/src/components/auth/require_auth.js: -------------------------------------------------------------------------------- 1 | // An higher-order component to make sure user has logged in 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { connect } from 'react-redux'; 5 | 6 | export default function(ComposedComponent) { 7 | 8 | class Authentication extends Component { 9 | 10 | static contextTypes = { 11 | router: PropTypes.object 12 | }; 13 | 14 | componentWillMount() { 15 | if (!this.props.authenticated) { 16 | this.context.router.history.replace('/signin', { time: new Date().toLocaleString(), message: 'Please sign in first.'}); 17 | } 18 | } 19 | 20 | componentWillUpdate(nextProps) { 21 | if (!nextProps.authenticated) { 22 | this.context.router.history.replace('/signin', { time: new Date().toLocaleString(), message: 'Please sign in first.'}); 23 | } 24 | } 25 | 26 | render() { 27 | return 28 | } 29 | } 30 | 31 | function mapStateToProps(state) { 32 | return { authenticated: state.auth.authenticated }; 33 | } 34 | 35 | return connect(mapStateToProps)(Authentication); 36 | } -------------------------------------------------------------------------------- /client/src/components/blog/post_detail/comments.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React, { Component } from 'react'; 3 | import { connect } from 'react-redux'; 4 | import { fetchComments } from '../../../actions'; 5 | 6 | class Comments extends Component { 7 | 8 | componentDidMount() { 9 | this.props.fetchComments(this.props.postId); 10 | } 11 | 12 | renderComment(comment) { 13 | return ( 14 |
15 |
16 |
17 | {comment.authorName} 18 | 19 | {new Date(comment.time).toLocaleString()} 20 |
21 |
22 |
23 | ); 24 | } 25 | 26 | render() { 27 | return ( 28 |
29 |

Comments

30 | {_.map(this.props.comments, comment => { 31 | return this.renderComment(comment); 32 | })} 33 |
34 | ); 35 | } 36 | } 37 | 38 | function mapStateToProps(state) { 39 | return { comments: state.comments }; 40 | } 41 | 42 | export default connect(mapStateToProps, { fetchComments })(Comments); -------------------------------------------------------------------------------- /client/public/style.css: -------------------------------------------------------------------------------- 1 | /* Sticky footer styles 2 | -------------------------------------------------- */ 3 | html { 4 | position: relative; 5 | min-height: 100%; 6 | } 7 | body { 8 | /* Margin bottom by footer height */ 9 | margin-bottom: 60px; 10 | } 11 | .footer { 12 | position: absolute; 13 | bottom: 0; 14 | width: 100%; 15 | /* Set the fixed height of the footer here */ 16 | height: 60px; 17 | line-height: 60px; /* Vertically center the text there */ 18 | background-color: #f5f5f5; 19 | } 20 | /* Custom page CSS 21 | -------------------------------------------------- */ 22 | 23 | #content { 24 | margin-top: 90px; 25 | } 26 | 27 | .form-signin { 28 | max-width: 300px; 29 | padding: 19px 29px 29px; 30 | margin: 0 auto 20px; 31 | background-color: #fff; 32 | border: 1px solid #e5e5e5; 33 | border-radius: 5px; 34 | box-shadow: 0 1px 2px rgba(0,0,0,0.05) 35 | } 36 | 37 | .form-profile { 38 | max-width: 600px; 39 | padding: 19px 29px 29px; 40 | margin: 0 auto 20px; 41 | background-color: #fff; 42 | border: 1px solid #e5e5e5; 43 | border-radius: 5px; 44 | box-shadow: 0 1px 2px rgba(0,0,0,0.05) 45 | } 46 | 47 | .span-with-margin { 48 | margin-right: 5px; 49 | } 50 | 51 | .link-without-underline { 52 | text-decoration: none !important; 53 | } 54 | 55 | .post { 56 | max-width: 800px; 57 | padding: 19px 29px 29px; 58 | margin: 0 auto 20px; 59 | } 60 | 61 | .f6 { 62 | font-size: 12px !important; 63 | } 64 | 65 | .text-grey { 66 | color: #9199a1 !important; 67 | } -------------------------------------------------------------------------------- /client/src/components/blog/post_mine.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React, { Component } from 'react'; 3 | import { Link } from 'react-router-dom'; 4 | import { connect } from 'react-redux'; 5 | import { fetchPostsByUserId } from '../../actions/index'; 6 | 7 | class PostMine extends Component { 8 | 9 | componentDidMount() { 10 | this.props.fetchPostsByUserId(); 11 | } 12 | 13 | renderTags(tags) { 14 | return tags.map(tag => { 15 | return {tag}; 16 | }); 17 | } 18 | 19 | renderPostSummary(post) { 20 | return ( 21 |
22 |

23 | 24 | {post.title} 25 | 26 |

27 | {this.renderTags(post.categories)} 28 | 29 | {post.authorName} 30 | 31 | {new Date(post.time).toLocaleString()} 32 |
33 |
34 | ); 35 | } 36 | 37 | render() { 38 | return ( 39 |
40 |

My Blog Posts

41 | {_.map(this.props.posts, post => { 42 | return this.renderPostSummary(post); 43 | })} 44 |
45 | ); 46 | } 47 | } 48 | 49 | function mapStateToProps(state) { 50 | return { posts: state.posts }; 51 | } 52 | 53 | export default connect(mapStateToProps, { fetchPostsByUserId })(PostMine); -------------------------------------------------------------------------------- /client/src/components/blog/post_list.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React, { Component } from 'react'; 3 | import { Link } from 'react-router-dom'; 4 | import { connect } from 'react-redux'; 5 | import { fetchPosts } from '../../actions/index'; 6 | 7 | class PostList extends Component { 8 | 9 | componentDidMount() { 10 | this.props.fetchPosts(); 11 | } 12 | 13 | renderTags(tags) { 14 | return tags.map(tag => { 15 | return {tag}; 16 | }); 17 | } 18 | 19 | renderPostSummary(post) { 20 | return ( 21 |
22 |

23 | 24 | {post.title} 25 | 26 |

27 | {this.renderTags(post.categories)} 28 | 29 | {post.authorName} 30 | 31 | {new Date(post.time).toLocaleString()} 32 |
33 |
34 | ); 35 | } 36 | 37 | render() { 38 | // console.log(this.props.posts); 39 | return ( 40 |
41 | Publish A New Post 42 | {_.map(this.props.posts, post => { 43 | return this.renderPostSummary(post); 44 | })} 45 |
46 | ); 47 | } 48 | } 49 | 50 | function mapStateToProps(state) { 51 | return { posts: state.posts }; 52 | } 53 | 54 | export default connect(mapStateToProps, { fetchPosts })(PostList); -------------------------------------------------------------------------------- /client/src/components/welcome.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | export default () => ( 5 |
6 | 7 | { /*Main jumbotron for a primary marketing message or call to action*/ } 8 |
9 |

Welcome!

10 |

This is a MERN stack based fully functioning blog system. Here, you can share your experience and ideas with other people.

11 |

Look the blog posts »

12 |
13 | 14 | { /*Example row of columns*/ } 15 |
16 |
17 |

Front-end

18 |

The front-end client is built as a simple-page-application using React and Redux (for middlewares and reducers). Besides, React-Router is used for navigation. Redux-Thunk is used for processing asynchronous requests. Bootstrap 4 is used for page styling.

19 |
20 |
21 |

Back-end

22 |

The back-end server is built with Express.js and Node.js in MVC pattern, which provides completed REST APIs for data interaction. Passport.js is used as an authentication middleware in the sever. JSON Web Token (JWT) is used for signing in user and making authenticated requests.

23 |
24 |
25 |

Database

26 |

MongoDB is used as the back-end database, which include different data models/schemas (i.e., User, Post and Comment). Mongoose is used to access the MongoDB for CRUD actions (create, read, update and delete).

27 |
28 |
29 | 30 |
31 | ); -------------------------------------------------------------------------------- /client/src/components/blog/post_detail/comment_new.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { reduxForm, Field } from 'redux-form'; 4 | import { createComment } from '../../../actions'; 5 | 6 | class CommentNew extends Component { 7 | 8 | handleFormSubmit({ comment }) { 9 | const postId = this.props.postId; 10 | this.props.createComment({ comment, postId }, () => { // callback 1: clear text editor 11 | document.querySelector("trix-editor").value = "" 12 | }, (path, state) => { // callback 2: history replace 13 | this.props.history.replace(path, state); 14 | }); 15 | } 16 | 17 | renderTextEditor = (field) => ( 18 |
19 | 20 | 21 |
22 | ); 23 | 24 | renderAlert() { 25 | 26 | const { state } = this.props; 27 | const { action } = this.props; 28 | 29 | if (state && action === 'REPLACE') { 30 | return ( 31 |
32 | {`[${state.time}] --- `} Oops! {state.message} 33 |
34 | ); 35 | } 36 | } 37 | 38 | render() { 39 | 40 | const { handleSubmit } = this.props; 41 | 42 | return ( 43 |
44 |

New Comment

45 | {this.renderAlert()} 46 |
47 | 48 | 49 | 50 |
51 | ); 52 | } 53 | } 54 | 55 | CommentNew = reduxForm({ 56 | form: 'comment_new', // name of the form 57 | })(CommentNew); 58 | 59 | export default connect(null, { createComment })(CommentNew); -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Blog 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 |
24 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Redux Blog 2 | 3 | This is a MERN stack based fully functioning blog system, which supports features of signing up, signing up, making authenticated requests, updating profile, changing password, publishing/editing/deleting blog post, making comments, etc. 4 | 5 | Live App: https://www.haichaoy.com/ 6 | 7 | ## Tech Stack 8 | 9 | #### Front-end 10 | 11 | * The front-end client is built as a simple-page-application using React and Redux (for middlewares and reducers). 12 | * React-Router is used for navigation. 13 | * Redux-Thunk is used for processing asynchronous requests. 14 | * Bootstrap 4 is used for page styling. 15 | 16 | #### Back-end 17 | 18 | * The back-end server is built with Express.js and Node.js in MVC pattern, which provides completed REST APIs for data interaction. 19 | * Passport.js is used as an authentication middleware in the sever. 20 | * JSON Web Token (JWT) is used for signing in user and making authenticated requests. 21 | 22 | #### Database 23 | 24 | * MongoDB is used as the back-end database, which include different data models/schemas (i.e., User, Post and Comment). 25 | * Mongoose is used to access the MongoDB for CRUD actions (create, read, update and delete). 26 | 27 | ## Usage 28 | 29 | Running locally you need 3 terminals open: one for client, one for server, and another one for MongoDB back-end. Below are the steps: 30 | 31 | 1. Install Node.js; 32 | 2. Install MongoDB; 33 | 3. `git clone https://github.com/haichao-yu/react-redux-blog.git`; 34 | 4. Go to directory `client`, and run `npm install`; 35 | 5. Go to directory `server`, and run `npm install`; 36 | 6. In one terminal, run `mongod`; 37 | 7. In `server` directory, run `npm run dev`; 38 | 8. In `client` directory, run `npm run start`; 39 | 40 | Then you are all set. You can go to `http://localhost:3000/` to check you live application. 41 | 42 | ## Proxy 43 | 44 | Please refer to [this link](https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#proxying-api-requests-in-development). 45 | -------------------------------------------------------------------------------- /server/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | const bcrypt = require('bcrypt-nodejs'); 4 | 5 | // Define our model 6 | const userSchema = new Schema({ 7 | email: { type: String, unique: true, lowercase: true }, 8 | password: String, 9 | firstName: String, 10 | lastName: String, 11 | 12 | birthday: { type: String, default: '' }, 13 | sex: { type: String, default: '' }, // secrecy/male/female 14 | phone: { type: String, default: '' }, 15 | address: { type: String, default: '' }, 16 | occupation: { type: String, default: '' }, 17 | description: { type: String, default: '' }, 18 | }); 19 | 20 | // On Save Hook, encrypt the password 21 | userSchema.pre('save', function(next) { // before saving the model, run this funtion 22 | 23 | const user = this; // get access to the user model 24 | 25 | bcrypt.genSalt(10, function(err, salt) { // generate a salt, then run callback 26 | 27 | if (err) { 28 | return next(err); 29 | } 30 | 31 | // hash (encrypt) our password using the salt 32 | bcrypt.hash(user.password, salt, null, function(err, hash) { 33 | 34 | if (err) { 35 | return next(err); 36 | } 37 | 38 | // overwrite plain text password with encrypted password 39 | user.password = hash; 40 | 41 | // go ahead and save the model 42 | next(); 43 | }); 44 | }); 45 | }); 46 | 47 | // userSchema.methods: Whenever we create a user object, it's going to have access to any functions that we define on this methods property 48 | userSchema.methods.comparePassword = function(candidatePassword, callback) { // used in LocalStrategy 49 | 50 | // candidatePassword will be encrypted internally in this function 51 | bcrypt.compare(candidatePassword, this.password, function(err, isMatch) { 52 | if (err) { 53 | return callback(err); 54 | } 55 | callback(null, isMatch); 56 | }); 57 | }; 58 | 59 | // Create the model class 60 | const ModelClass = mongoose.model('user', userSchema); 61 | 62 | // Export the model 63 | module.exports = ModelClass; -------------------------------------------------------------------------------- /server/router.js: -------------------------------------------------------------------------------- 1 | const Authentication = require('./controllers/authentication'); 2 | const Profile = require('./controllers/userinfo'); 3 | const Blog = require('./controllers/blog'); 4 | 5 | // service 6 | const passport = require('passport'); 7 | const passportService = require('./services/passport'); 8 | 9 | // middleware in between Incoming Request and Route Handler 10 | const requireAuth = passport.authenticate('jwt', { session: false }); 11 | const requireSignin = passport.authenticate('local', { session: false }); 12 | 13 | module.exports = function(app) { 14 | 15 | /** 16 | * Authentication APIs 17 | */ 18 | 19 | app.get('/api/', requireAuth, function(req, res) { 20 | res.send({ message: 'Super secret code is ABC123' }); 21 | }); 22 | 23 | app.post('/api/signup', Authentication.signup); 24 | 25 | app.post('/api/signin', requireSignin, Authentication.signin); 26 | // app.post('/api/signin', Authentication.signin); 27 | 28 | app.get('/api/verify_jwt', requireAuth, Authentication.verifyJwt); 29 | 30 | /** 31 | * Profile APIs 32 | */ 33 | 34 | app.get('/api/profile', requireAuth, Profile.fetchProfile); 35 | 36 | app.put('/api/profile', requireAuth, Profile.updateProfile); 37 | 38 | app.put('/api/password', requireAuth, Profile.resetPassword); 39 | 40 | /** 41 | * Blog Post APIs 42 | */ 43 | 44 | app.get('/api/posts', Blog.fetchPosts); 45 | 46 | app.post('/api/posts', requireAuth, Blog.createPost); 47 | 48 | app.get('/api/posts/:id', Blog.fetchPost); 49 | 50 | app.get('/api/allow_edit_or_delete/:id', requireAuth, Blog.allowUpdateOrDelete); 51 | 52 | app.put('/api/posts/:id', requireAuth, Blog.updatePost); 53 | 54 | app.delete('/api/posts/:id', requireAuth, Blog.deletePost); 55 | 56 | app.get('/api/my_posts', requireAuth, Blog.fetchPostsByAuthorId); 57 | 58 | /** 59 | * Blog Comment APIs 60 | */ 61 | 62 | app.post('/api/comments/:postId', requireAuth, Blog.createComment); 63 | 64 | app.get('/api/comments/:postId', Blog.fetchCommentsByPostId); 65 | }; 66 | 67 | 68 | 69 | // CRUD: 70 | // - Create: http post request 71 | // - Read: http get request 72 | // - Update: http put request 73 | // - Delete: http delete request -------------------------------------------------------------------------------- /client/src/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 { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; 6 | import reduxThunk from 'redux-thunk'; 7 | 8 | import Header from './components/header'; 9 | import Footer from './components/footer'; 10 | import NoMatch from './components/nomatch'; 11 | import Welcome from './components/welcome'; 12 | import Signin from './components/auth/signin'; 13 | import Signup from './components/auth/signup'; 14 | import RequireAuth from './components/auth/require_auth'; 15 | import Profile from './components/userinfo/profile'; 16 | import Settings from './components/userinfo/settings'; 17 | import PostList from './components/blog/post_list'; 18 | import PostNew from './components/blog/post_new'; 19 | import PostDetail from './components/blog/post_detail/index'; 20 | import PostMine from './components/blog/post_mine'; 21 | 22 | import reducers from './reducers'; 23 | import { AUTH_USER } from './actions/types'; 24 | 25 | const createStoreWithMiddleware = applyMiddleware(reduxThunk)(createStore); 26 | const store = createStoreWithMiddleware(reducers); 27 | 28 | const token = localStorage.getItem('token'); 29 | // If we have a token, consider the user to be signed in 30 | if (token) { 31 | // We need to update application state 32 | store.dispatch({ type: AUTH_USER }); 33 | } 34 | 35 | ReactDOM.render( 36 | 37 | 38 |
39 |
40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
54 |
55 |
56 |
57 |
58 | , document.getElementById('root') 59 | ); -------------------------------------------------------------------------------- /client/src/components/userinfo/settings.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { reduxForm, Field } from 'redux-form'; 4 | import { changePassword } from '../../actions/index'; 5 | 6 | class Settings extends Component { 7 | 8 | handleFormSubmit({ oldPassword, newPassword }) { 9 | this.props.changePassword({ oldPassword, newPassword }, (path, state) => { // callback: history replace 10 | this.props.history.replace(path, state); 11 | }); 12 | } 13 | 14 | renderField = ({ label, input, type, meta: { touched, error, warning } }) => ( 15 |
16 | 17 | { touched && error && {error} } 18 |
19 | ); 20 | 21 | renderAlert() { 22 | 23 | const { state } = this.props.history.location; 24 | const { action } = this.props.history; 25 | 26 | if (state && action === 'REPLACE') { 27 | 28 | return ( 29 |
30 | {`[${state.time}] --- `} {state.status === 'success' ? 'Congratulations!' : 'Oops!'} {state.message} 31 |
32 | ); 33 | } 34 | } 35 | 36 | render() { 37 | // these properties comes from ReduxForm 38 | const { handleSubmit } = this.props; 39 | 40 | // when do we need .bind(this)? 41 | return ( 42 |
43 | {this.renderAlert()} 44 |
45 |

Change Password

46 |
47 | 48 | 49 | 50 | 51 | 52 |
53 | ); 54 | } 55 | } 56 | 57 | function validate(formProps) { 58 | 59 | // console.log(formProps); 60 | 61 | const errors = {}; 62 | 63 | if (formProps.newPassword !== formProps.newPasswordConfirm) { 64 | errors.newPasswordConfirm = 'New password must match!'; 65 | } 66 | 67 | return errors; 68 | } 69 | 70 | Settings = reduxForm({ 71 | form: 'settings', // name of the form 72 | validate: validate, 73 | })(Settings); 74 | 75 | export default connect(null, { changePassword })(Settings); -------------------------------------------------------------------------------- /client/src/components/blog/post_new.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { reduxForm, Field } from 'redux-form'; 4 | import { createPost } from '../../actions'; 5 | 6 | class PostNew extends Component { 7 | 8 | handleFormSubmit({ title, categories, content }) { 9 | // console.log(title); 10 | // console.log(categories); 11 | // console.log(content); 12 | this.props.createPost({ title, categories, content }, (path) => { // callback 1: history push 13 | this.props.history.push(path); 14 | }, (path, state) => { // callback 2: history replace 15 | this.props.history.replace(path, state); 16 | }); 17 | } 18 | 19 | renderInput = (field) => ( 20 |
21 | 22 | 30 |
31 | ); 32 | 33 | renderTextEditor = (field) => ( 34 |
35 | 36 | 37 | 38 |
39 | ); 40 | 41 | renderAlert() { 42 | 43 | const { state } = this.props.history.location; 44 | const { action } = this.props.history; 45 | 46 | if (state && action === 'REPLACE') { 47 | return ( 48 |
49 | {`[${state.time}] --- `} Oops! {state.message} 50 |
51 | ); 52 | } 53 | } 54 | 55 | render() { 56 | 57 | const { handleSubmit } = this.props; 58 | 59 | return ( 60 |
61 | {this.renderAlert()} 62 |

New Post

63 |
64 | 65 | 66 | 67 | 68 | 69 |
70 | ); 71 | } 72 | } 73 | 74 | PostNew = reduxForm({ 75 | form: 'post_new', // name of the form 76 | })(PostNew); 77 | 78 | export default connect(null, { createPost })(PostNew); -------------------------------------------------------------------------------- /client/src/components/blog/post_detail/post_edit.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { reduxForm, Field } from 'redux-form'; 4 | import { updatePost } from '../../../actions'; 5 | 6 | class PostEdit extends Component { 7 | 8 | componentDidMount() { 9 | const { content } = this.props.post; 10 | document.querySelector("trix-editor").value = content; 11 | } 12 | 13 | handleFormSubmit({ title, categories, content }) { 14 | 15 | const _id = this.props.post._id; 16 | categories = categories.toString(); 17 | 18 | this.props.updatePost({ _id, title, categories, content }, this.props.onEditSuccess, (path, state) => { 19 | this.props.history.replace(path, state); 20 | }); 21 | } 22 | 23 | renderInput = (field) => ( 24 |
25 | 26 | 34 |
35 | ); 36 | 37 | renderTextEditor = (field) => ( 38 |
39 | 40 | 41 | 42 |
43 | ); 44 | 45 | renderAlert() { 46 | 47 | const { state } = this.props; 48 | const { action } = this.props; 49 | 50 | if (state && action === 'REPLACE') { 51 | return ( 52 |
53 | {`[${state.time}] --- `} Oops! {state.message} 54 |
55 | ); 56 | } 57 | } 58 | 59 | render() { 60 | 61 | const { handleSubmit } = this.props; 62 | 63 | return ( 64 |
65 | {this.renderAlert()} 66 |

Edit Your Post

67 |
68 | 69 | 70 | 71 | 72 | 73 |
74 | ); 75 | } 76 | } 77 | 78 | PostEdit = reduxForm({ 79 | form: 'post_edit', // name of the form 80 | })(PostEdit); 81 | 82 | function mapStateToProps(state, ownProps) { 83 | return { initialValues: ownProps.post }; 84 | } 85 | 86 | export default connect(mapStateToProps, { updatePost })(PostEdit); -------------------------------------------------------------------------------- /client/src/components/auth/signin.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { reduxForm, Field } from 'redux-form'; 4 | import { signinUser } from '../../actions'; 5 | 6 | class Signin extends Component { 7 | 8 | componentWillMount() { 9 | if (this.props.authenticated) { // if the user already signed in, navigate to '/posts' 10 | this.props.history.replace('/posts'); 11 | } 12 | } 13 | 14 | handleFormSubmit({ email, password }) { 15 | // console.log(email, password); 16 | // need to do something to log user in 17 | this.props.signinUser({ email, password }, (path) => { // callback 1: history push 18 | this.props.history.push(path); 19 | }, (path, state) => { // callback 2: history replace 20 | this.props.history.replace(path, state); 21 | }); 22 | } 23 | 24 | renderField = (field) => ( 25 |
26 | { /**/ } 27 | 28 |
29 | ); 30 | 31 | renderAlert() { 32 | 33 | const { state } = this.props.history.location; 34 | const { action } = this.props.history; 35 | 36 | // message: successfully signed up, you can sign in 37 | if (state && action === 'PUSH') { 38 | return ( 39 |
40 | {`[${state.time}] --- `} Congratulations! {state.message} 41 |
42 | ); 43 | } 44 | 45 | // message: sign in failed 46 | if (state && action === 'REPLACE') { 47 | return ( 48 |
49 | {`[${state.time}] --- `} Oops! {state.message} 50 |
51 | ); 52 | } 53 | } 54 | 55 | render() { 56 | 57 | // these properties comes from ReduxForm 58 | const { handleSubmit } = this.props; 59 | 60 | // when do we need .bind(this)? 61 | return ( 62 |
63 | {this.renderAlert()} 64 |
65 |

Sign In

66 |
67 | 68 | 69 | 70 | 71 |
72 | ); 73 | } 74 | } 75 | 76 | Signin = reduxForm({ 77 | form: 'signin', // name of the form 78 | })(Signin); 79 | 80 | function mapStateToProps(state) { 81 | return { authenticated: state.auth.authenticated }; 82 | } 83 | 84 | export default connect(mapStateToProps, { signinUser })(Signin); 85 | 86 | // The new version of reduxForm (v6) also removes the ability to inject actions or props like the 'connect' helper from React Redux does. 87 | // To fix this, you'll need to wrap your component with both the 'connect' and 'reduxForm' helpers. -------------------------------------------------------------------------------- /server/services/passport.js: -------------------------------------------------------------------------------- 1 | // Configuration for passport (a middleware for authenticating user) 2 | 3 | // How it works? 4 | // Incoming Request ---> Passport ---> Router Handler 5 | 6 | // Passport can verity users with different strategies: 7 | // - Verify user with JWT (what we use here); 8 | // - Verify user with username and password; 9 | // - ... (check http://passportjs.org/) 10 | 11 | const passport = require('passport'); 12 | const User = require('../models/user'); 13 | const config = require('../config'); 14 | const JwtStrategy = require('passport-jwt').Strategy; 15 | const ExtractJwt = require('passport-jwt').ExtractJwt; 16 | const LocalStrategy = require('passport-local'); 17 | 18 | // Setup options for JWT strategy 19 | const jwtOptions = { 20 | jwtFromRequest: ExtractJwt.fromHeader('authorization'), 21 | secretOrKey: config.secret, // for decoding JWT 22 | }; 23 | 24 | // Create JWT strategy (authenticate user with JWT) 25 | const jwtLogin = new JwtStrategy(jwtOptions, function(payload, done) { 26 | // the function will be called whenever we need to authenticate a user with a JWT token 27 | // - payload: decoded JWT token ('sub' and 'iat', refer to jwt.encode in authentication.js) 28 | // - done: a callback function we need to call depending on whether or not we are able to successfully authenticate this user 29 | 30 | // See if the user ID in the payload exists in our database 31 | // If it does, call 'done' with that user 32 | // Otherwise, call 'done' without a user object 33 | User.findById(payload.sub, function(err, user) { 34 | 35 | if (err) { 36 | return done(err, false); 37 | } 38 | 39 | if (user) { 40 | done(null, user); 41 | } else { 42 | done(null, false); 43 | } 44 | }); 45 | }); 46 | 47 | // Setup options for local strategy 48 | const localOptions = { usernameField: 'email' }; // if you are looking for username, look for email (cause we use email here) 49 | 50 | // Create local strategy (authenticate user with email and password) 51 | const localLogin = new LocalStrategy(localOptions, function(email, password, done) { 52 | // Verify this email and password 53 | // Call done with the user if it is the correct email and password 54 | // Otherwise, call done with false 55 | User.findOne({ email: email }, function(err, user) { 56 | 57 | if (err) { 58 | return done(err); 59 | } 60 | 61 | if (!user) { 62 | return done(null, false, { message: 'Incorrect username.' }); // that last argument is the info argument to the authenticate callback 63 | } 64 | 65 | // Compare passwords - is `password` equal to user.password? 66 | user.comparePassword(password, function(err, isMatch) { 67 | 68 | if (err) { 69 | return done(err); 70 | } 71 | 72 | if (!isMatch) { 73 | return done(null, false, { message: 'Incorrect password.' }); // that last argument is the info argument to the authenticate callback 74 | } 75 | 76 | // Found the user (email and password are correct), then assign it to req.user, which then be used in signin() in authentication.js 77 | return done(null, user); 78 | }); 79 | }); 80 | }); 81 | 82 | // Tell passport to use these strategies 83 | passport.use(jwtLogin); // For making auth'd request 84 | passport.use(localLogin); // For signing in user -------------------------------------------------------------------------------- /server/controllers/authentication.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jwt-simple'); 2 | const passport = require('passport'); 3 | const User = require('../models/user'); 4 | const config = require('../config'); 5 | 6 | // utility function: User ID + Timestamp + Secret String = JSON Web Token (JWT) 7 | function tokenForUser(user) { 8 | const timestamp = new Date().getTime(); 9 | return jwt.encode({ sub: user._id, iat: timestamp }, config.secret); 10 | // sub: subject (the very specific user) 11 | // iat: issued at time 12 | } 13 | 14 | /** 15 | * Sign up a new user 16 | * 17 | * @param req 18 | * @param res 19 | * @param next 20 | */ 21 | exports.signup = function(req, res, next) { 22 | 23 | // console.log(req.body); 24 | const email = req.body.email; 25 | const password = req.body.password; 26 | const firstName = req.body.firstName; 27 | const lastName = req.body.lastName; 28 | 29 | if (!email || !password) { 30 | return res.status(422).send({ message: 'You must provide both email and password.' }); // 422 refers to unprocessable entity 31 | } 32 | 33 | // See if a user with given email exists 34 | User.findOne({ email: email }, function(err, existingUser) { 35 | 36 | if (err) { 37 | return next(err); 38 | } 39 | 40 | // If a user with email does exist, return an error 41 | if (existingUser) { 42 | return res.status(422).send({ message: 'This email is in use.' }); // 422 refers to unprocessable entity 43 | } 44 | 45 | // If a user with email does NOT exist, create and save user record 46 | const user = new User({ 47 | email: email, 48 | password: password, 49 | firstName: firstName, 50 | lastName: lastName, 51 | }); 52 | 53 | user.save(function(err) { // callback function 54 | if (err) { 55 | return next(err); 56 | } 57 | 58 | // Respond user request indicating the user was created 59 | res.json({ message: 'You have successfully signed up. You can sign in now.' }); 60 | }); 61 | }); 62 | }; 63 | 64 | /** 65 | * Sign in the user 66 | * 67 | * @param req 68 | * @param res 69 | * @param next 70 | */ 71 | exports.signin = function(req, res, next) { 72 | 73 | // Require auth 74 | 75 | // User has already had their email and password auth'd (through passport middleware [LocalStrategy]) 76 | // We just need to give them a token 77 | res.send({ 78 | token: tokenForUser(req.user), 79 | username: req.user.firstName + ' ' + req.user.lastName, 80 | }); 81 | }; 82 | 83 | /* 84 | // https://github.com/jaredhanson/passport-local/issues/4 85 | 86 | exports.signin = function(req, res, next) { 87 | passport.authenticate('local', { session: false }, function(err, user, info) { 88 | 89 | // console.log(err); 90 | // console.log(user); 91 | // console.log(info); // info contains customized error message 92 | 93 | if (err) { 94 | return next(err); 95 | } 96 | if (!user) { 97 | return res.status(401).send(info); 98 | } 99 | return res.send({ token: tokenForUser(user)}); 100 | })(req, res, next); 101 | }; 102 | */ 103 | 104 | /** 105 | * Verify if the JWT in local storage is valid 106 | * 107 | * @param req 108 | * @param res 109 | * @param next 110 | */ 111 | exports.verifyJwt = function(req, res, next) { 112 | 113 | // Require auth 114 | 115 | res.send({ 116 | username: req.user.firstName + ' ' + req.user.lastName 117 | }); 118 | }; -------------------------------------------------------------------------------- /client/src/components/auth/signup.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { reduxForm, Field } from 'redux-form'; // read new version of ReduxForm 4 | import { signupUser } from '../../actions'; 5 | 6 | class Signup extends Component { 7 | 8 | componentWillMount() { 9 | if (this.props.authenticated) { // if the user already signed in, navigate to '/posts' 10 | this.props.history.replace('/posts'); 11 | } 12 | } 13 | 14 | handleFormSubmit({ email, password, firstName, lastName }) { 15 | // Call action creator to sign up the user 16 | this.props.signupUser({ email, password, firstName, lastName }, (path, state) => { // callback 1: history push 17 | this.props.history.push(path, state); 18 | }, (path, state) => { // callback 2: history replace 19 | this.props.history.replace(path, state); 20 | }); 21 | } 22 | 23 | renderField = ({ label, input, type, meta: { touched, error, warning } }) => ( 24 |
25 | { /**/ } 26 | 27 | { touched && error && {error} } 28 |
29 | ); 30 | 31 | renderAlert() { 32 | 33 | const { state } = this.props.history.location; 34 | const { action } = this.props.history; 35 | 36 | // message: sign up failed 37 | if (state && action === 'REPLACE') { 38 | return ( 39 |
40 | {`[${state.time}] --- `} Oops! {state.message} 41 |
42 | ); 43 | } 44 | } 45 | 46 | render() { 47 | 48 | const { handleSubmit } = this.props; 49 | 50 | return ( 51 |
52 | {this.renderAlert()} 53 |
54 |

Sign Up

55 |
56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
64 | ); 65 | } 66 | } 67 | 68 | function validate(formProps) { 69 | 70 | // console.log(formProps); 71 | 72 | const errors = {}; 73 | 74 | /* 75 | if (!formProps.email) { 76 | errors.email = 'Please enter an email'; 77 | } 78 | 79 | if (!formProps.password) { 80 | errors.password = 'Please enter an password'; 81 | } 82 | 83 | if (!formProps.passwordConfirm) { 84 | errors.passwordConfirm = 'Please enter an password confirmation'; 85 | } 86 | */ 87 | 88 | if (formProps.password !== formProps.passwordConfirm) { 89 | errors.passwordConfirm = 'Password must match!'; 90 | } 91 | 92 | return errors; 93 | } 94 | 95 | Signup = reduxForm({ 96 | form: 'signup', 97 | validate: validate 98 | })(Signup); 99 | 100 | function mapStateToProps(state) { 101 | return { authenticated: state.auth.authenticated }; 102 | } 103 | 104 | export default connect(mapStateToProps, { signupUser })(Signup); -------------------------------------------------------------------------------- /client/src/components/header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router-dom'; 4 | import { verifyJwt, signoutUser } from '../actions'; 5 | 6 | class Header extends Component { 7 | 8 | componentWillMount() { 9 | if (this.props.authenticated && !this.props.user) { 10 | this.props.verifyJwt(); // fetch username 11 | } 12 | } 13 | 14 | renderLinks() { 15 | if (this.props.authenticated) { 16 | // show a dropdown menu for authenticated user 17 | return ( 18 |
19 | 20 |
21 | Your Posts 22 | Your Profile 23 |
24 | Settings 25 | Sign out 26 |
27 |
28 | ); 29 | } else { 30 | // show a link to sign in or sign up 31 | return ( 32 |
    33 |
  • 34 | Sign Up 35 |
  • 36 |
  • 37 | Sign In 38 |
  • 39 |
40 | ); 41 | } 42 | } 43 | 44 | render() { 45 | return ( 46 | 72 | ); 73 | } 74 | } 75 | 76 | function mapStateToProps(state) { 77 | return { 78 | authenticated: state.auth.authenticated, 79 | username: state.auth.username, 80 | }; 81 | } 82 | 83 | export default connect(mapStateToProps, { verifyJwt, signoutUser })(Header); 84 | 85 | 86 | 87 | /** 88 | * todo: A bug need to be fixed - After updating user profile, you navigate to another page (i.e., www.google.com). If you click the go back button on the browser, the username on header is incorrect. 89 | */ -------------------------------------------------------------------------------- /server/controllers/userinfo.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/user'); 2 | const Post = require('../models/post'); 3 | const Comment = require('../models/comment'); 4 | 5 | /** 6 | * Fetch profile information 7 | * 8 | * @param req 9 | * @param res 10 | * @param next 11 | */ 12 | exports.fetchProfile = function(req, res, next) { 13 | 14 | // Require auth 15 | 16 | // Return profile info 17 | const user = ({ 18 | email: req.user.email, 19 | firstName: req.user.firstName, 20 | lastName: req.user.lastName, 21 | birthday: req.user.birthday, 22 | sex: req.user.sex, 23 | phone: req.user.phone, 24 | address: req.user.address, 25 | occupation: req.user.occupation, 26 | description: req.user.description, 27 | }); 28 | res.send({ 29 | user: user 30 | }); 31 | }; 32 | 33 | /** 34 | * Update profile information 35 | * 36 | * @param req 37 | * @param res 38 | * @param next 39 | */ 40 | exports.updateProfile = function(req, res, next) { 41 | 42 | // Require auth 43 | 44 | // Get new profile info (user input) 45 | const firstName = req.body.firstName; 46 | const lastName = req.body.lastName; 47 | const birthday = req.body.birthday; 48 | const sex = req.body.sex; 49 | const phone = req.body.phone; 50 | const address = req.body.address; 51 | const occupation = req.body.occupation; 52 | const description = req.body.description; 53 | 54 | // Get user 55 | const user = req.user; 56 | 57 | // Update author name for post (updateMany(): MongoDB will update all documents that match criteria) 58 | Post.updateMany({ authorId: user._id }, { $set: { authorName: firstName + ' ' + lastName }}, function(err) { 59 | if (err) { 60 | next(err); 61 | } 62 | }); 63 | 64 | // Update author name for comment (updateMany(): MongoDB will update all documents that match criteria) 65 | Comment.updateMany({ authorId: user._id }, { $set: { authorName: firstName + ' ' + lastName }}, function(err) { 66 | if (err) { 67 | next(err); 68 | } 69 | }); 70 | 71 | // Update user profile 72 | User.findByIdAndUpdate(user._id, { $set: { 73 | firstName: firstName, 74 | lastName: lastName, 75 | birthday: birthday, 76 | sex: sex, 77 | phone: phone, 78 | address: address, 79 | occupation: occupation, 80 | description: description, 81 | } }, { new: true }, function(err, updatedUser) { 82 | if (err) { 83 | return next(err); 84 | } 85 | // Delete unused properties: _id, password, __v 86 | updatedUser = updatedUser.toObject(); 87 | delete updatedUser['_id']; 88 | delete updatedUser['password']; 89 | delete updatedUser['__v']; 90 | // Return updated user profile 91 | res.send({ user: updatedUser }); 92 | }) 93 | }; 94 | 95 | /** 96 | * Reset password 97 | * 98 | * @param req 99 | * @param res 100 | * @param next 101 | */ 102 | exports.resetPassword = function(req, res, next) { 103 | 104 | // Require auth 105 | 106 | const oldPassword = req.body.oldPassword; 107 | const newPassword = req.body.newPassword; 108 | const user = req.user; 109 | 110 | // Compare passwords - Does the user provide correct old password? 111 | user.comparePassword(oldPassword, function(err, isMatch) { 112 | 113 | if (err) { 114 | return next(err); 115 | } 116 | 117 | if (!isMatch) { 118 | return res.status(422).send({ message: 'You old password is incorrect! Please try again.' }) 119 | } 120 | 121 | if (oldPassword === newPassword) { 122 | return res.status(422).send({ message: 'Your new password must be different from your old password!' }); 123 | } 124 | 125 | // Update password 126 | user.password = newPassword; 127 | 128 | // Save to DB 129 | user.save(function(err) { 130 | if (err) { 131 | return next(err); 132 | } 133 | 134 | // Respond user request indicating that the password was updated successfully 135 | res.json({ message: 'You have successfully updated your password.' }); 136 | }); 137 | }); 138 | }; -------------------------------------------------------------------------------- /client/src/components/userinfo/profile.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { reduxForm, Field } from 'redux-form'; 4 | import { fetchProfile, clearProfile, updateProfile } from '../../actions/index'; 5 | 6 | class Profile extends Component { 7 | 8 | componentDidMount() { 9 | if (!this.props.initialValues) { 10 | this.props.fetchProfile(); // fetch profile 11 | } 12 | } 13 | 14 | componentWillUnmount() { 15 | this.props.clearProfile(); // clear the redux state (userinfo) when this component will unmount 16 | } 17 | 18 | handleFormSubmit({ firstName, lastName, birthday, sex, phone, address, occupation, description }) { 19 | this.props.updateProfile({ firstName, lastName, birthday, sex, phone, address, occupation, description }, (path, state) => { // callback: history replace 20 | this.props.history.replace(path, state); 21 | }); 22 | } 23 | 24 | renderInput = (field) => ( 25 |
26 | 27 | 34 |
35 | ); 36 | 37 | renderOptions = (field) => ( 38 |
39 | 40 | 45 |
46 | ); 47 | 48 | renderTextarea = (field) => ( 49 |
50 | 51 |