├── 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 |
6 |
7 | @2017 Haichao Yu
8 |
9 |
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 |
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 |
21 | You need to enable JavaScript to run this app.
22 |
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 |
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 | {field.label}
22 |
30 |
31 | );
32 |
33 | renderTextEditor = (field) => (
34 |
35 | {field.label}
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 |
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 | {field.label}
26 |
34 |
35 | );
36 |
37 | renderTextEditor = (field) => (
38 |
39 | {field.label}
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 |
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 | { /*{field.label} */ }
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 |
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 | { /*{label} */ }
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 |
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 |
{this.props.username}
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 |
47 |
48 |
49 |
50 |
51 |
Haichao's Blog System
52 |
53 |
54 |
55 |
56 | Posts
57 |
58 |
59 | GitHub
60 |
61 |
62 |
66 |
67 | {this.renderLinks()}
68 |
69 |
70 |
71 |
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 | {field.label}
27 |
34 |
35 | );
36 |
37 | renderOptions = (field) => (
38 |
39 | {field.label}
40 |
41 |
42 | Male
43 | Female
44 |
45 |
46 | );
47 |
48 | renderTextarea = (field) => (
49 |
50 | {field.label}
51 |
52 |
53 | );
54 |
55 | renderAlert() {
56 |
57 | const { state } = this.props.history.location;
58 | const { action } = this.props.history;
59 |
60 | if (state && action === 'REPLACE') {
61 |
62 | return (
63 |
64 | {`[${state.time}] --- `} {state.status === 'success' ? 'Congratulations!' : 'Oops!'} {state.message}
65 |
66 | );
67 | }
68 | }
69 |
70 | render() {
71 |
72 | if (!this.props.initialValues) { // if the initialValues is null, render Loading...
73 | return Loading...
74 | }
75 |
76 | const { handleSubmit } = this.props;
77 |
78 | return (
79 |
80 | { this.renderAlert() }
81 |
95 |
96 | );
97 | }
98 | }
99 |
100 | /*
101 | function validate(formProps) {
102 | console.log(formProps);
103 | }
104 | */
105 |
106 | Profile = reduxForm({
107 | form: 'profile', // name of the form
108 | // validate: validate,
109 | })(Profile);
110 |
111 | function mapStateToProps(state) {
112 | return { initialValues: state.profile.user }; // set initial values for the form
113 | }
114 |
115 | export default connect(mapStateToProps, { fetchProfile, clearProfile, updateProfile })(Profile);
--------------------------------------------------------------------------------
/client/src/components/blog/post_detail/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import NoMatch from '../../nomatch';
5 | import PostBody from './post_body';
6 | import Comments from './comments';
7 | import CommentNew from './comment_new';
8 | import PostEdit from './post_edit';
9 |
10 | import { fetchPost, checkAuthority, deletePost } from '../../../actions';
11 |
12 | class PostDetail extends Component {
13 |
14 | constructor(props) {
15 | super(props);
16 | this.state = { // component state: being read or being edited
17 | beingEdit: false
18 | };
19 | }
20 |
21 | componentDidMount() {
22 |
23 | // By default, we set beingEdit as false (Since when the user first click the post, the post detail is read, rather than edited)
24 | this.setState({
25 | beingEdit: false
26 | });
27 |
28 | // Get post id
29 | const { id } = this.props.match.params;
30 |
31 | // Fetch post detail
32 | if (!this.props.post) {
33 | this.props.fetchPost(id);
34 | }
35 |
36 | // Check whether current authenticated user has authority to make change to this post
37 | this.props.checkAuthority(id);
38 | }
39 |
40 | handleEditSuccess() {
41 | this.setState({
42 | beingEdit: false
43 | });
44 | }
45 |
46 | onEditClick() {
47 | this.setState({
48 | beingEdit: true
49 | });
50 | }
51 |
52 | onDeleteClick() {
53 | const { id } = this.props.match.params;
54 | this.props.deletePost(id, (path) => {
55 | this.props.history.push(path);
56 | });
57 | }
58 |
59 | renderDeleteConfirmModal() { // used for delete confirmation
60 | return (
61 |
62 |
63 |
64 |
65 |
Confirm Delete
66 |
67 | ×
68 |
69 |
70 |
71 |
Are you sure you want to delete this post with its comments? Attention! This delete operation cannot be undone.
72 |
73 |
74 | Cancel
75 | Delete
76 |
77 |
78 |
79 |
80 | );
81 | }
82 |
83 | renderUpdateAndDeleteButton() {
84 | if (this.props.allowChange) {
85 | return (
86 |
87 | Edit
88 | Delete
89 |
90 | );
91 | }
92 | }
93 |
94 | render() {
95 |
96 | // If there is no post match the given post ID, render NoMatch page
97 | if (!this.props.post) {
98 | return ;
99 | }
100 |
101 | // If the component state 'beingEdit' is true, we render the post edit page
102 | if (this.state.beingEdit) {
103 | return (
104 |
111 | );
112 | }
113 |
114 | // Render the regular post detail page for reading
115 | return (
116 |
117 |
118 | {this.renderUpdateAndDeleteButton()}
119 |
120 |
126 |
127 | {this.renderDeleteConfirmModal()}
128 |
129 |
130 | );
131 | }
132 | }
133 |
134 | function mapStateToProps({ posts, auth }, ownProps) {
135 | return {
136 | post: posts[ownProps.match.params.id],
137 | allowChange: auth.allowChange,
138 | };
139 | }
140 |
141 | export default connect(mapStateToProps, { fetchPost, checkAuthority, deletePost })(PostDetail);
--------------------------------------------------------------------------------
/server/controllers/blog.js:
--------------------------------------------------------------------------------
1 | let _ = require('lodash');
2 |
3 | const Post = require('../models/post');
4 | const Comment = require('../models/comment');
5 |
6 | /**
7 | * ------- Post APIs -------
8 | */
9 |
10 | /**
11 | * Get a list of posts
12 | *
13 | * @param req
14 | * @param res
15 | * @param next
16 | */
17 | exports.fetchPosts = function(req, res, next) {
18 | Post
19 | .find({})
20 | .select({})
21 | .limit(100)
22 | .sort({
23 | time: -1
24 | })
25 | .exec(function(err, posts) {
26 | if (err) {
27 | console.log(err);
28 | return res.status(422).json({
29 | message: 'Error! Could not retrieve posts.'
30 | });
31 | }
32 | res.json(posts);
33 | });
34 | };
35 |
36 | /**
37 | * Create a new post
38 | *
39 | * @param req
40 | * @param res
41 | * @param next
42 | */
43 | exports.createPost = function(req, res, next) {
44 |
45 | // Require auth
46 | const user = req.user;
47 |
48 | const title = req.body.title;
49 | const categories = req.body.categories;
50 | const content = req.body.content;
51 | const authorId = user._id;
52 | const authorName = user.firstName + ' ' + user.lastName;
53 | const time = Date.now();
54 |
55 | // Make sure title, categories and content are not empty
56 | if (!title || !categories || !content) {
57 | return res.status(422).json({
58 | message: 'Title, categories and content are all required.'
59 | });
60 | }
61 |
62 | // Create a new post
63 | const post = new Post({
64 | title: title,
65 | categories: _.uniq(categories.split(',').map((item) => item.trim())), // remove leading and trailing spaces, remove duplicate categories
66 | content: content,
67 | authorId: authorId,
68 | authorName: authorName,
69 | time: time,
70 | });
71 |
72 | // Save the post
73 | post.save(function(err, post) { // callback function
74 | if (err) {
75 | return next(err);
76 | }
77 | res.json(post); // return the created post
78 | });
79 | };
80 |
81 | /**
82 | * Fetch a single post by post ID
83 | *
84 | * @param req
85 | * @param res
86 | * @param next
87 | */
88 | exports.fetchPost = function(req, res, next) {
89 | Post.findById({
90 | _id: req.params.id
91 | }, function(err, post) {
92 | if (err) {
93 | console.log(err);
94 | return res.status(422).json({
95 | message: 'Error! Could not retrieve the post with the given post ID.'
96 | });
97 | }
98 | if (!post) {
99 | return res.status(404).json({
100 | message: 'Error! The post with the given ID is not exist.'
101 | });
102 | }
103 | res.json(post); // return the single blog post
104 | });
105 | };
106 |
107 | /**
108 | * Check if current post can be updated or deleted by the authenticated user: The author can only make change to his/her own posts
109 | *
110 | * @param req
111 | * @param res
112 | * @param next
113 | */
114 | exports.allowUpdateOrDelete = function(req, res, next) {
115 |
116 | // Require auth
117 | const user = req.user;
118 |
119 | // Find the post by post ID
120 | Post.findById({
121 | _id: req.params.id
122 | }, function(err, post) {
123 |
124 | if (err) {
125 | console.log(err);
126 | return res.status(422).json({
127 | message: 'Error! Could not retrieve the post with the given post ID.'
128 | });
129 | }
130 |
131 | // Check if the post exist
132 | if (!post) {
133 | return res.status(404).json({
134 | message: 'Error! The post with the given ID is not exist.'
135 | });
136 | }
137 |
138 | console.log(user._id);
139 | console.log(post.authorId);
140 |
141 | // Check if the user ID is equal to the author ID
142 | if (!user._id.equals(post.authorId)) {
143 | return res.send({allowChange: false});
144 | }
145 | res.send({allowChange: true});
146 | });
147 | };
148 |
149 | /**
150 | * Edit/Update a post
151 | *
152 | * @param req
153 | * @param res
154 | * @param next
155 | */
156 | exports.updatePost = function(req, res, next) {
157 |
158 | // Require auth
159 | const user = req.user;
160 |
161 | // Find the post by post ID
162 | Post.findById({
163 | _id: req.params.id
164 | }, function(err, post) {
165 |
166 | if (err) {
167 | console.log(err);
168 | return res.status(422).json({
169 | message: 'Error! Could not retrieve the post with the given post ID.'
170 | });
171 | }
172 |
173 | // Check if the post exist
174 | if (!post) {
175 | return res.status(404).json({
176 | message: 'Error! The post with the given ID is not exist.'
177 | });
178 | }
179 |
180 | // Make sure the user ID is equal to the author ID (Cause only the author can edit the post)
181 | // console.log(user._id);
182 | // console.log(post.authorId);
183 | if (!user._id.equals(post.authorId)) {
184 | return res.status(422).json({
185 | message: 'Error! You have no authority to modify this post.'
186 | });
187 | }
188 |
189 | // Make sure title, categories and content are not empty
190 | const title = req.body.title;
191 | const categories = req.body.categories;
192 | const content = req.body.content;
193 |
194 | if (!title || !categories || !content) {
195 | return res.status(422).json({
196 | message: 'Title, categories and content are all required.'
197 | });
198 | }
199 |
200 | // Update user
201 | post.title = title;
202 | post.categories = _.uniq(categories.split(',').map((item) => item.trim())), // remove leading and trailing spaces, remove duplicate categories;
203 | post.content = content;
204 |
205 | // Save user
206 | post.save(function(err, post) { // callback function
207 | if (err) {
208 | return next(err);
209 | }
210 | res.json(post); // return the updated post
211 | });
212 | });
213 | };
214 |
215 | /**
216 | * Delete a post by post ID
217 | *
218 | * @param req
219 | * @param res
220 | * @param next
221 | */
222 | exports.deletePost = function(req, res, next) {
223 |
224 | // Require auth
225 |
226 | // Delete the post
227 | Post.findByIdAndRemove(req.params.id, function(err, post) {
228 | if (err) {
229 | return next(err);
230 | }
231 | if (!post) {
232 | return res.status(422).json({
233 | message: 'Error! The post with the given ID is not exist.'
234 | });
235 | }
236 |
237 | // Delete comments correspond to this post
238 | Comment.remove({ postId: post._id }, function(err) {
239 | if (err) {
240 | return next(err);
241 | }
242 | });
243 |
244 | // Return a success message
245 | res.json({
246 | message: 'The post has been deleted successfully!'
247 | });
248 | });
249 | };
250 |
251 | /**
252 | * Fetch posts by author ID
253 | *
254 | * @param req
255 | * @param res
256 | * @param next
257 | */
258 | exports.fetchPostsByAuthorId = function(req, res, next) {
259 |
260 | // Require auth
261 | const user = req.user;
262 |
263 | // Fetch posts by author ID
264 | Post
265 | .find({
266 | authorId: user._id
267 | })
268 | .select({})
269 | .limit(100)
270 | .sort({
271 | time: -1
272 | })
273 | .exec(function(err, posts) {
274 | if (err) {
275 | console.log(err);
276 | return res.status(422).json({
277 | message: 'Error! Could not retrieve posts.'
278 | });
279 | }
280 | res.json(posts);
281 | });
282 | };
283 |
284 | /**
285 | * ------- Comment APIs -------
286 | */
287 |
288 | /**
289 | * Create a new comment (post ID and user ID are both needed)
290 | *
291 | * @param req
292 | * @param res
293 | * @param next
294 | */
295 | exports.createComment = function(req, res, next) {
296 |
297 | // Require auth
298 | const user = req.user;
299 |
300 | if (!user) {
301 | return res.status(422).json({
302 | message: 'You must sign in before you can post new comment.'
303 | });
304 | }
305 |
306 | // Get post ID
307 | const postId = req.params.postId;
308 |
309 | // Get content and make sure it is not empty
310 | const content = req.body.content;
311 | if (!content) {
312 | return res.status(422).json({
313 | message: 'Comment cannot be empty.'
314 | });
315 | }
316 |
317 | // Create a new comment
318 | const comment = new Comment({
319 | content: content,
320 | authorId: user._id,
321 | authorName: user.firstName + ' ' + user.lastName,
322 | postId: postId,
323 | time: Date.now(),
324 | });
325 |
326 | // Save the comment
327 | comment.save(function(err, comment) { // callback function
328 | if (err) {
329 | return next(err);
330 | }
331 | res.json(comment); // return the created comment
332 | });
333 | };
334 |
335 | /**
336 | * Fetch comments for a specific blog post (post ID is needed)
337 | *
338 | * @param req
339 | * @param res
340 | * @param next
341 | */
342 | exports.fetchCommentsByPostId = function(req, res, next) {
343 | Comment
344 | .find({
345 | postId: req.params.postId
346 | })
347 | .select({})
348 | .limit(100)
349 | .sort({
350 | time: 1
351 | })
352 | .exec(function(err, comments) {
353 | if (err) {
354 | console.log(err);
355 | return res.status(422).json({
356 | message: 'Error! Could not retrieve comments.'
357 | });
358 | }
359 | res.json(comments);
360 | });
361 | };
--------------------------------------------------------------------------------
/client/src/actions/index.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { reset } from 'redux-form';
3 | import {
4 | AUTH_USER,
5 | UNAUTH_USER,
6 |
7 | FETCH_PROFILE,
8 | CLEAR_PROFILE,
9 | UPDATE_PROFILE,
10 |
11 | FETCH_POSTS,
12 | CREATE_POST,
13 | FETCH_POST,
14 | UPDATE_POST,
15 | DELETE_POST,
16 |
17 | CHECK_AUTHORITY,
18 |
19 | CREATE_COMMENT,
20 | FETCH_COMMENTS,
21 | } from './types';
22 |
23 | const ROOT_URL = '/api';
24 |
25 | /**
26 | * Authentication
27 | */
28 |
29 | export function signinUser({ email, password }, historyPush, historyReplace) {
30 |
31 | // Using redux-thunk (instead of returning an object, return a function)
32 | // All redux-thunk doing is giving us arbitrary access to the dispatch function, and allow us to dispatch our own actions at any time we want
33 | return function(dispatch) {
34 |
35 | // Submit email/password to the server
36 | axios.post(`${ROOT_URL}/signin`, { email, password }) // axios returns a promise
37 | .then(response => { // If request is good (sign in succeeded) ...
38 |
39 | // - Save the JWT token (use local storage)
40 | localStorage.setItem('token', response.data.token);
41 |
42 | // - Update state to indicate user is authenticated
43 | dispatch({
44 | type: AUTH_USER,
45 | payload: response.data.username,
46 | });
47 |
48 | // - Redirect (PUSH) to the route '/posts'
49 | historyPush('/posts');
50 | })
51 | .catch(() => { // If request is bad (sign in failed) ...
52 |
53 | // - Redirect (REPLACE) to the route '/signin', then show an error to the user
54 | historyReplace('/signin', {
55 | time: new Date().toLocaleString(),
56 | message: 'The email and/or password are incorrect.'
57 | });
58 | });
59 | }
60 | }
61 |
62 | export function signupUser({ email, password, firstName, lastName }, historyPush, historyReplace) {
63 |
64 | return function(dispatch) {
65 |
66 | axios.post(`${ROOT_URL}/signup`, { email, password, firstName, lastName }) // axios returns a promise
67 | .then(response => { // If request is good (sign up succeeded) ...
68 |
69 | // - Redirect (PUSH) to the route '/signin', then show a success message to the user
70 | historyPush('/signin', { time: new Date().toLocaleString(), message: response.data.message });
71 | })
72 | .catch(({response}) => { // If request is bad (sign up failed) ...
73 |
74 | // - Redirect (REPLACE) to the route '/signup', then show an error to the user
75 | historyReplace('/signup', { time: new Date().toLocaleString(), message: response.data.message });
76 | });
77 | }
78 | }
79 |
80 | export function signoutUser() {
81 |
82 | // - Delete the JWT token from local storage
83 | localStorage.removeItem('token');
84 |
85 | // - Update state to indicate the user is not authenticated
86 | return { type: UNAUTH_USER };
87 | }
88 |
89 | export function verifyJwt() {
90 |
91 | return function(dispatch) {
92 | axios.get(`${ROOT_URL}/verify_jwt`, {
93 | headers: { authorization: localStorage.getItem('token') }
94 | }).then((response) => {
95 | dispatch({
96 | type: AUTH_USER,
97 | payload: response.data.username,
98 | });
99 | });
100 | }
101 | }
102 |
103 | /**
104 | * User information
105 | */
106 |
107 | export function fetchProfile() {
108 |
109 | return function(dispatch) {
110 | axios.get(`${ROOT_URL}/profile`, {
111 | headers: { authorization: localStorage.getItem('token') }
112 | }).then(response => {
113 | dispatch({
114 | type: FETCH_PROFILE,
115 | payload: response.data.user,
116 | });
117 | });
118 | }
119 | }
120 |
121 | export function clearProfile() {
122 | return { type: CLEAR_PROFILE };
123 | }
124 |
125 | export function updateProfile({ firstName, lastName, birthday, sex, phone, address, occupation, description }, historyReplace) {
126 |
127 | return function(dispatch) {
128 | axios.put(`${ROOT_URL}/profile`, { // req.body (2nd parameter)
129 | firstName,
130 | lastName,
131 | birthday,
132 | sex,
133 | phone,
134 | address,
135 | occupation,
136 | description,
137 | }, { // header (3rd parameter)
138 | headers: {authorization: localStorage.getItem('token')}, // require auth
139 | }
140 | )
141 | .then((response) => { // Update profile success
142 | // - Update profile
143 | dispatch({
144 | type: UPDATE_PROFILE,
145 | payload: response.data.user,
146 | });
147 | // - Update username for header
148 | dispatch({
149 | type: AUTH_USER,
150 | payload: response.data.user.firstName + ' ' + response.data.user.lastName,
151 | });
152 | // - history.replace
153 | historyReplace('/profile', {
154 | status: 'success',
155 | time: new Date().toLocaleString(),
156 | message: 'You have successfully updated your profile.',
157 | });
158 | })
159 | .catch(() => { // Update profile failed
160 | historyReplace('/profile', {
161 | status: 'fail',
162 | time: new Date().toLocaleString(),
163 | message: 'Update profile failed. Please try again.',
164 | });
165 | });
166 | }
167 | }
168 |
169 | export function changePassword({ oldPassword, newPassword }, historyReplace) {
170 |
171 | return function(dispatch) {
172 | axios.put(`${ROOT_URL}/password`, {
173 | oldPassword,
174 | newPassword,
175 | }, {
176 | headers: {authorization: localStorage.getItem('token')}, // require auth
177 | })
178 | .then((response) => {
179 | dispatch(reset('settings')); // Clear the form if success
180 | historyReplace('/settings', {
181 | status: 'success',
182 | time: new Date().toLocaleString(),
183 | message: response.data.message,
184 | });
185 | })
186 | .catch(({response}) => {
187 | historyReplace('/settings', {
188 | status: 'fail',
189 | time: new Date().toLocaleString(),
190 | message: response.data.message,
191 | });
192 | });
193 | }
194 | }
195 |
196 | /**
197 | * Blog Post
198 | */
199 |
200 | export function fetchPosts() {
201 |
202 | return function(dispatch) {
203 | axios.get(`${ROOT_URL}/posts`).then((response) => {
204 | dispatch({
205 | type: FETCH_POSTS,
206 | payload: response.data,
207 | });
208 | });
209 | }
210 | }
211 |
212 | export function createPost({ title, categories, content }, historyPush, historyReplace) {
213 |
214 | return function(dispatch) {
215 | axios.post(`${ROOT_URL}/posts`, {
216 | title,
217 | categories,
218 | content,
219 | }, {
220 | headers: {authorization: localStorage.getItem('token')}, // require auth
221 | })
222 | .then((response) => { // If create post succeed, navigate to the post detail page
223 | dispatch({
224 | type: CREATE_POST,
225 | payload: response.data,
226 | });
227 | historyPush(`/posts/${response.data._id}`);
228 | })
229 | .catch(({response}) => { // If create post failed, alert failure message
230 | historyReplace('/posts/new', {
231 | time: new Date().toLocaleString(),
232 | message: response.data.message,
233 | });
234 | });
235 | }
236 | }
237 |
238 | export function fetchPost(id) {
239 |
240 | return function(dispatch) {
241 | axios.get(`${ROOT_URL}/posts/${id}`).then(response => {
242 | // console.log(response);
243 | dispatch({
244 | type: FETCH_POST,
245 | payload: response.data,
246 | });
247 | });
248 | }
249 | }
250 |
251 | export function updatePost({ _id, title, categories, content }, onEditSuccess, historyReplace) {
252 |
253 | return function(dispatch) {
254 | axios.put(`${ROOT_URL}/posts/${_id}`, {
255 | _id,
256 | title,
257 | categories,
258 | content,
259 | }, {
260 | headers: {authorization: localStorage.getItem('token')}, // require auth
261 | })
262 | .then((response) => {
263 | dispatch({
264 | type: UPDATE_POST,
265 | payload: response.data,
266 | });
267 | onEditSuccess(); // set beingEdit to false
268 | historyReplace(`/posts/${_id}`, null);
269 | })
270 | .catch(({response}) => {
271 | historyReplace(`/posts/${_id}`, {
272 | time: new Date().toLocaleString(),
273 | message: response.data.message,
274 | });
275 | });
276 | }
277 | }
278 |
279 | export function deletePost(id, historyPush) {
280 |
281 | return function(dispatch) {
282 | axios.delete(`${ROOT_URL}/posts/${id}`, {
283 | headers: {authorization: localStorage.getItem('token')}, // require auth
284 | }).then((response) => {
285 | dispatch({
286 | type: DELETE_POST,
287 | payload: id,
288 | });
289 | historyPush('/posts');
290 | })
291 | }
292 | }
293 |
294 | export function fetchPostsByUserId() {
295 |
296 | return function(dispatch) {
297 | axios.get(`${ROOT_URL}/my_posts`, {
298 | headers: {authorization: localStorage.getItem('token')}, // require auth
299 | })
300 | .then((response) => {
301 | dispatch({
302 | type: FETCH_POSTS,
303 | payload: response.data,
304 | });
305 | });
306 | }
307 | }
308 |
309 | /**
310 | * Blog Comments
311 | */
312 |
313 | export function createComment({ comment, postId }, clearTextEditor, historyReplace) {
314 |
315 | return function(dispatch) {
316 | axios.post(`${ROOT_URL}/comments/${postId}`, { content: comment }, {
317 | headers: {authorization: localStorage.getItem('token')}, // require auth
318 | })
319 | .then((response) => { // If success, clear the text editor
320 | dispatch({
321 | type: CREATE_COMMENT,
322 | payload: response.data,
323 | });
324 | dispatch(reset('comment_new')); // - Clear form value (data)
325 | clearTextEditor(); // - Clear text editor (UI)
326 | historyReplace(`/posts/${postId}`, null); // - clear alert message
327 | })
328 | .catch(({response}) => { // If fail, render alert message
329 |
330 | // failure reason: un-authenticated
331 | if (!response.data.message) {
332 | return historyReplace(`/posts/${postId}`, {
333 | time: new Date().toLocaleString(),
334 | message: 'You must sign in before you can post new comment.',
335 | });
336 | }
337 |
338 | // failure reason: comment is empty
339 | historyReplace(`/posts/${postId}`, {
340 | time: new Date().toLocaleString(),
341 | message: response.data.message,
342 | });
343 | });
344 | }
345 | }
346 |
347 | export function fetchComments(postId) {
348 |
349 | return function(dispatch) {
350 | axios.get(`${ROOT_URL}/comments/${postId}`).then((response) => {
351 | dispatch({
352 | type: FETCH_COMMENTS,
353 | payload: response.data,
354 | });
355 | });
356 | }
357 | }
358 |
359 | /**
360 | * Check authority: Check if the user has the authority to make change to a specific post
361 | */
362 | export function checkAuthority(postId) {
363 |
364 | return function(dispatch) {
365 | axios.get(`${ROOT_URL}/allow_edit_or_delete/${postId}`, {
366 | headers: {authorization: localStorage.getItem('token')}, // require auth
367 | }).then((response) => {
368 | dispatch({
369 | type: CHECK_AUTHORITY,
370 | payload: response.data.allowChange,
371 | });
372 | }).catch(() => { // If an user is un-authorized, he/she cannot make change to any posts
373 | dispatch({
374 | type: CHECK_AUTHORITY,
375 | payload: false,
376 | })
377 | });
378 | }
379 | }
--------------------------------------------------------------------------------