使用Node.JS,React,Redux和Redux-saga Part3:身份验证构建Retrogames存档

Here we are at the last part of the Retrogames Archive tutorial. We have now a full working app written in JavaScript and it's pretty much what we were looking to achieve.

这里是Retrogames Archive教程的最后一部分。 现在,我们已经有了一个使用JavaScript编写的功能全面的应用程序,这几乎就是我们想要实现的目标。

However, at present all the users have full access to the common operations on the archive: They can view, create and delete games entries.

但是,目前,所有用户都可以完全访问存档中的常规操作:他们可以查看,创建和删除游戏条目。

In this last part of the tutorial we want to limit the operations of creating and deleting a game to allow only Authenticated user to perform them. Plus, we are going to improve the UI with friendly notifications.

在本教程的最后一部分中,我们希望限制创建和删除游戏的操作,以仅允许经过身份验证的用户执行游戏。 另外,我们将通过友好的通知来改进用户界面。

The project is always available on my github, the master branch contains the complete code. Don't forget to copy the up-to-date css in your project!

该项目始终在我的github上可用,master分支包含完整的代码。 不要忘记在您的项目中复制最新CSS!

先决条件 ( Prerequisites )

The only prerequisite for this very last part of the tutorial is familiarity with JSON web tokens. I take for granted all the other prerequisites described in the previous tutorials, part1 and part2.

本教程最后一部分的唯一先决条件是熟悉JSON Web令牌。 我理所当然地理解了先前教程part1和part2中描述的所有其他先决条件。

NB: Since we are going to edit/update existing files I am going to highlight the changes with multi-line comments:

注意 :由于我们将要编辑/更新现有文件,因此我将使用多行注释突出显示更改:

/* 
 * The new code starts after this comment...
 */ 

目录 ( Table of Contents )

资料夹结构 ( Folder Structure )

That's the final folder structure:

那是最终的文件夹结构:

--app
 ----models
 ------game.js
 ------user.js
 ----routes
 ------game.js
 ------user.js
 --config
 ----index.js
 --client
 ----dist
 ------css
 --------style.css
 ------fonts
 --------PressStart2p.ttf
 ------index.html
 ------bundle.js
 ----src
 ------actions
 --------filestack.js
 --------games.js
 --------auth.js
 ------components
 --------About.jsx
 --------AddGamePanel.jsx
 --------Archive.jsx
 --------Contact.jsx
 --------Form.jsx
 --------Game.jsx
 --------GamesListManager.jsx
 --------Home.jsx
 --------index.js
 --------Login.jsx
 --------Modal.jsx
 --------Signup.jsx
 --------Welcome.jsx
 ------constants
 --------auth.js
 --------filestack.js
 --------games.js
 ------containers
 --------AddGameContainer.jsx
 --------GamesContainer.jsx
 --------reducers
 ----------auth.js
 ----------filestack.js
 ----------games.js
 ----------index.js
 ----------routing.js
 --------sagas
 ----------auth.js
 ----------filestack.js
 ----------games.js
 ----------index.js
 ------index.js
 ------routes.js
 ------store.js
 ------utils
 --------authWrapper.js
 --------authentication.js
 --.babelrc
 --package.json
 --server.js
 --webpack-loaders.js
 --webpack-paths.js
 --webpack.config.js
 --yarn.lock

If you compare this folder structure with the last one you should notice we mostly added files to handle the user authentication on both the server side (new routes and a model) and the client side (new components, reducers, sagas, actions etc.).

如果将此文件夹结构与最后一个文件夹结构进行比较,您应该注意到我们主要在服务器端(新路径和模型)和客户端(新组件,reducer,sagas,action等)上添加了用于处理用户身份验证的文件。 。

认证方式 ( Authentication )

Authentication is handled on both parts of your application, the server and the client, so let's start with the server.

身份验证在应用程序的两个部分(服务器和客户端)上都进行处理,因此让我们从服务器开始。

服务器和JSON令牌 (Server and JSON Token)

We are using JSON web tokens to add a security layer to the app. Users should be able to sign up or login to modify the games archive.

我们正在使用JSON Web令牌向应用程序添加安全层。 用户应该能够注册或登录以修改游戏档案。

First of all, open server.js and add two new routes:

首先,打开server.js并添加两条新路由:

import express from 'express';
import bodyParser from 'body-parser';
import mongoose from 'mongoose';
import morgan from 'morgan';

import { getGames, getGame, postGame, deleteGame } from './app/routes/game';
// New routes and middleware to manage the authentication
import { signup, login, verifyAuth } from './app/routes/user';

const app = express();
const port = process.env.PORT || 8080;

const options = {
  server: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } },
  replset: { socketOptions: { keepAlive: 1, connectTimeoutMS : 30000 } }
};
mongoose.Promise = global.Promise;
mongoose.connect(YOUR_MONGODB_URL', options);

const db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));

app.use(bodyParser.urlencoded({ extended: true}));
app.use(bodyParser.json());
app.use(morgan('dev'));

app.use(express.static(__dirname + '/client/dist'));

app.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*");
  res.header('Access-Control-Allow-Methods', 'GET,POST,DELETE');
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, x-access-token");
  next();
});

/*
 * New routes to handle Authentication
 */
app.post('/auth/login', login);
app.post('/auth/signup', signup);

app.route('/games')
    // verifyAuth is the security middleware to check the authentication
    .post(verifyAuth, postGame)
    .get(getGames);
app.route('/games/:id')
    .get(getGame)
    // Again delete requests pass through the security middleware
    .delete(verifyAuth, deleteGame);

app.route("*").get((req, res) => {
    res.sendFile('client/dist/index.html', { root: __dirname });
});

app.listen(port);

console.log(`listening on port ${port}`);
  • In the server main file we included two new routes in charge of handling the authentication: A POST requests to /auth/signup inserts a new user into the database and return the token while /auth/login checks the credentials and returns a token as well.

    在服务器主文件中,我们包括两个用于处理身份验证的新路由:对/ auth / signup的 POST请求将一个新用户插入数据库并返回令牌,而/ auth / login检查凭据并还返回令牌。
  • In addition, the route for posting a new game and the route for deleting a game are protected by verifyAuth middleware. Whenever the server receives a request to one of those routes, it first retrieves the token from the request header and verify it. Then, if the verification succeed, a call to next runs the function in charge of posting or deleting a game, otherwise the server simply returns a HTTP 403 forbidden status.

    另外,发布新游戏的路线和删除游戏的路线均由verifyAuth中间件保护。 每当服务器收到对这些路由之一的请求时,它首先会从请求标头中检索令牌并进行验证。 然后,如果验证成功,则对next的调用将运行负责发布或删除游戏的功能,否则服务器仅返回HTTP 403禁止状态。

用户模型 (User Model)

We now need to create the user model to store users in the database so we can go very simple: We need e-mail, password and name fields. In particular, the password will be hashed and to do so we let's add bcryptjs, a small library to help hashing passwords:

现在,我们需要创建用户模型以将用户存储在数据库中,这样我们可以变得非常简单:我们需要电子邮件密码名称字段。 特别是,密码将被哈希处理,为此,我们添加bcryptjs ,这是一个帮助哈希密码的小型库:

yarn add bcryptjs

Let's add a new fileuser.js in /app/models and paste the following code:

让我们在/app/models添加一个新文件user.js并粘贴以下代码:

// Require some dependencies
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var bcrypt = require('bcryptjs');

// Our schema defines 3 fields, notice email must be unique
var userSchema = new mongoose.Schema({
  email: { type: String, unique: true, lowercase: true },
  password: { type: String, select: false },
  name: String
});

userSchema.pre('save', function (next) {
  var user = this;
  // before saving a hashed version of the password is created and saved into the db
  bcrypt.genSalt(10, function (err, salt) {
    bcrypt.hash(user.password, salt, function (err, hash) {
      user.password = hash;
      next();
    });
  });
});

// This utility function comes handy during authentication
userSchema.methods.comparePwd = function(password, done) {
  // Compare the password sent by the user with the one stored in the db
  bcrypt.compare(password, this.password, (err, isMatch) => {
    done(err, isMatch);
  });
};

// Export the model
module.exports = mongoose.model('User', userSchema);

身份验证路由和中间件 (Authentication Routes and Middleware)

The logic to authenticate the user is also in charge to create the token to be sent as response: To create the token we can define payload with some useful information:

验证用户身份的逻辑还负责创建作为响应发送的令牌:要创建令牌,我们可以使用一些有用的信息定义有效负载:

  1. sub: The subject is the user name. In the client the token can be decoded and the user name shown in the app along with a welcome message.

    sub :主题是用户名。 在客户端中,可以对令牌进行解码,并在应用程序中显示用户名以及欢迎消息。
  2. exp: The expiration date is set to 1 day and to easily set it we are gonna install moment, very useful JavaScript library to work with dates.

    exp :到期日期设置为1天,为了方便地进行设置,我们将安装moment ,这是一个非常有用JavaScript库来处理日期。
jsonwebtoken and jsonwebtokenmoment: moment
yarn add jsonwebtoken moment

In server.js we defined two new routes and a middleware all residing in the same file /app/routes/user.js. Let's create it and past the following code:

server.js我们定义了两个新路由和一个中间件,它们都位于同一文件/app/routes/user.js 。 让我们创建它,并通过以下代码:

// Our new dependencies
import jwt from 'jsonwebtoken';
import moment from 'moment';
// We import the User model we have just defined
import User from '../models/user';
// The config file contains the secret to sign the token
import config from '../../config';

// Utility function to create and return the token, it requires TOKEN_SECRET from config
const createToken = name => {
  var payload = {
    sub: name,
    exp: moment().add(1, 'day').unix()
  };
  return jwt.sign(payload, config.TOKEN_SECRET);
}

// signup function for the /auth/signup route
const signup = (req, res) => {
  // query the database to make sure the e-mail is not taken already
  User.findOne({ email: req.body.email }, (err, existingUser) => {
    if (existingUser) {
    // HTTP 409 status is sent in case the e-mail is taken
      return res.status(409).json({ message: 'Email is already taken' });
    }

    // A new user is created with the information sent by the client
    const user = Object.assign(new User(), req.body);
    user.save((err, result) => {
      if (err) {
        res.send(err);
      }
      // Notice we also send the token as we want the user to be immediately logged in
      res.json({
        message: 'Welcome to Retrogames, you are now logged in',
        token: createToken(result.name)
      });
    });
  });
};

// Login function for /auth/login
const login = (req, res) => {
  // Query the database for user with that specific e-mail
  User.findOne({ email: req.body.email }, '+password', (err, user) => {
    if (!user) {
    // If the user doesn't exist just send a HTTP 401 status
      return res.status(401).json({ message: 'Invalid email/password' });
    }
    /* If the user exists, the password sent by the client is compared with the one in the db
    with the utilily function comparePwd
   */
    user.comparePwd(req.body.password, (err, isMatch) => {
      if (!isMatch) {
    // In case of wrong password, we send another HTTP 401 status
        return res.status(401).send({ message: 'Invalid email/password' });
      }
      // Correct information from the client, a token is sent
      res.json({ message: 'You are now logged in', token: createToken(user.name) });
    });
  });
};

// verifyAuth middleware to protect post and delete routes
const verifyAuth = (req, res, next) => {
  // Get the token from the header x-access-token
  const token = req.headers['x-access-token'];
  if (token) {
    // Verifies the token and the expiration
    jwt.verify(token, config.TOKEN_SECRET, function(err, payload) {
      // If the verification fails it returns http status 403
      if (err) {
        return res.status(403).send({
          message: 'Failed to authenticate token.'
        });
      } else {
        // Goes to the next route since there are no errors
        next();
      }
    });
  } else {
    // Requests without token return http status 403
    return res.status(403).send({
        message: 'No token provided.'
    });
  }
};

// Export the functions for server.js
export {
  signup,
  login,
  verifyAuth
};

There are 4 functions we created and 3 of them are exported for further usage in server.js.

我们创建了4个函数,其中3个已导出,以便在server.js进一步使用。

  • createToken: It's an utility function in charge to create and return a valid token.

    createToken :它是一个实用程序函数,负责创建和返回有效令牌。
  • signup: The function receives a new user's information to create a new user entry in the database and returns the token. Before that, it checks if the e-mail was taken by another user before.

    signup :该函数接收新用户的信息以在数据库中创建新的用户条目并返回令牌。 在此之前,它将检查该电子邮件之前是否已被其他用户接收。
  • login: Whenever a user try to authenticate to /auth/login, the function first retrieves the correct user from the database (given the e-mail) and then verify the password. If all goes right, it sends the token back to the client.

    login :每当用户尝试通过/ auth / login进行身份验证时,该函数都会首先从数据库(通过电子邮件)中检索正确的用户,然后验证密码。 如果一切正常,它将令牌发送回客户端。
  • verifyAuth: This is the middleware in charge to protect the archive: Only authenticated user can create or delete games.

    verifyAuth :这是负责保护存档的中间件:只有经过身份验证的用户才能创建或删除游戏。

Take a look at createToken: To sign the token we need a secret string that we imported from a config file, let's create /config/index.js and paste the following code:

看一下createToken :要对令牌进行签名 ,我们需要一个从配置文件中导入的秘密字符串,让我们创建/config/index.js并粘贴以下代码:

const TOKEN_SECRET = process.env.TOKEN_SECRET || 'YOUR_SECRET_STRING';

export default {
  TOKEN_SECRET
};

Replace YOUR_SECRET_STRING with a string of your choice.

用您选择的字符串替换YOUR_SECRET_STRING

The server side is done, let's test it with Postman!

服务器端已完成,让我们用Postman进行测试!

测试服务器 (Test the Server)

The first thing we can do is to try to add a new game with no token in the header to verify whether the server actually refuses your request or not:

我们可以做的第一件事是尝试添加一个新游戏,并且标题中没有令牌,以验证服务器是否实际上拒绝了您的请求:

POST请求到/ games (POST Request to /games )

in Postman, send a new game to localhost:8080/games and you should receive a HTTP 403 status with message "No token provided.":

在Postman中,将一个新游戏发送到localhost:8080 / games ,您应该收到HTTP 403状态,并显示消息“未提供令牌”:

The middleware seems to be working fine! Let's create a user now:

中间件似乎运行良好! 现在创建一个用户:

POST请求到/ auth / signup (POST Request to /auth/signup )

We need to send an e-mail, name and password to the server at /auth/signup, let's see if it works:

我们需要通过/ auth / signup向服务器发送电子邮件,名称和密码,让我们看看它是否有效:

As you can see we received a token, let's now use it for creating a new game!

如您所见,我们收到了令牌,现在让我们使用它来创建新游戏!

经过验证的POST请求到/ games (Authenticated POST Request to /games )

We copy the token in the header tab for the post request. Here is the result:

我们在发布请求的标题标签中复制令牌。 结果如下:

And the game was successfully created!

游戏成功创建!

Finally, we want to simulate the login request.

最后,我们要模拟登录请求。

POST请求/ auth / login (POST Request to /auth/login)

Let's login to the server by sending e-mail and password:

让我们通过发送电子邮件和密码登录服务器:

And this works too, we can now go working on the client-side!

这也可行,我们现在可以在客户端上工作了!

客户 ( Client )

In the client-side of the app we want show the buttons to login, sign-up or logout so let's take a look at these two screenshots:

在应用程序的客户端,我们希望显示用于登录,注册或注销的按钮,因此让我们看一下以下两个屏幕截图:

  1. The user is not authenticated:

    用户未通过身份验证:

  • The users can login or sign-up.

    用户可以登录或注册。
  • Since the user is not authenticated, he/she cannot add/delete any games.

    由于用户未通过身份验证,因此他/她无法添加/删除任何游戏。
  1. The user is authenticated:

    用户已通过身份验证:

  • The user is now authenticated so the buttons to login/sign-up don't show.

    现在,用户已通过身份验证,因此不会显示用于登录/注册的按钮。
  • The user has now the right to add/delete a game. Notice on the top right the welcome message along with the logout button.

    用户现在有权添加/删除游戏。 请注意右上角的欢迎消息以及注销按钮。

Finally, the views to login/sign-up are pretty straightforward:

最后,登录/注册的视图非常简单:

使用Redux管理路由 (Manage the Routing with Redux)

Before writing the authentication logic, let's think about the steps to login/signup: Once the user clicks on the login/register button, the client communicates with the server and if all goes well it receives back the token. At this point, a redirection the user to /games should happen. I find react-router-redux package very useful because we can control the routing in the state and we have a handy action-creator push to dispatch actions to the reducer and change view.

在编写身份验证逻辑之前,让我们考虑一下登录/注册的步骤:用户单击“登录/注册”按钮后,客户端与服务器通信,如果一切顺利,它将接收回令牌。 此时,应该将用户重定向到/ games 。 我发现react-router-redux软件包非常有用,因为我们可以控制状态下的路由,并且我们有一个方便的动作创建者push可以将动作分派给reducer并更改视图。

Let's add it by running:

让我们通过运行添加它:

yarn add react-router-redux

To use the push action-creator we need to add a new middleware when configuring the store:

要使用push动作创建者,我们需要在配置商店时添加新的中间件:

In /client/src/store.js paste the following code:

/client/src/store.js粘贴以下代码:

import {
  createStore,
  applyMiddleware,
  compose
} from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootSaga from './sagas';
import reducer from './reducers';
/* 
 * Here we imported the routerMiddleware
 */ 
import { routerMiddleware } from 'react-router-redux';
import { hashHistory } from 'react-router';

const configureStore = () => {
  const sagaMiddleware = createSagaMiddleware();
  /*
   * It requires the app history as parameter
   */ 
  const routeMiddleware = routerMiddleware(hashHistory);
  const store = createStore(
    reducer,
    /* 
     * we add it right after sagaMiddleware 
     */ 
    applyMiddleware(sagaMiddleware, routeMiddleware)
  );
  sagaMiddleware.run(rootSaga);

  return store;
}
export default configureStore;

Since we are using Immutable for the state we need to define the routing reducer by ourselves as well as pass a selector to access the payload state and convert it to a JavaScript object.

由于我们将Immutable用于状态,因此我们需要自己定义路由缩减器,并传递选择器以访问有效负载状态并将其转换为JavaScript对象。

NB: These steps are not necessary with a mutable state.

注意 :对于可变状态,这些步骤不是必需的。

Let's create the reducer in /client/src/reducers/routing.js:

让我们在/client/src/reducers/routing.js创建reducer:

// This is a standard definition for the routing reducer
import Immutable from 'immutable';
import { LOCATION_CHANGE } from 'react-router-redux';

const initialState = Immutable.fromJS({
  locationBeforeTransitions: null
});

export default (state = initialState, action) => {
  if (action.type === LOCATION_CHANGE) {
    return state.set('locationBeforeTransitions', action.payload);
  }

  return state;
};

This is pretty straightforward, there is just an action to interact with the reducer and it comes directly from react-router-redux. We also gotta edit the index.js in /client/src/reducers:

这非常简单,只有一个动作与reducer交互,它直接来自react-router-redux 。 我们还必须在/client/src/reducers编辑index.js

import { combineReducers } from 'redux-immutable';
import { reducer as form } from 'redux-form/immutable';
import games from './games';
import filestack from './filestack';
/* 
 * Here we imported the routing reducer
 */ 
import routing from './routing';

export default combineReducers({
  games,
  form,
  filestack,
  /* 
   * Combine routing as well
   */ 
  routing,
});

The last thing to do is to call syncHistoryWithStore to get the routing part of the state and convert it to an object.

最后要做的是调用syncHistoryWithStore以获取状态的路由部分并将其转换为对象。

In /client/src/routes.js paste the following code:

/client/src/routes.js粘贴以下代码:

import React from 'react';
import { Provider } from 'react-redux';
import configureStore from './store';
import { Router, Route, hashHistory, IndexRoute } from 'react-router';
import { AddGameContainer, GamesContainer } from './containers';
import { Home, Archive, Welcome, About, Contact } from './components';
/* 
 * Here we imported syncHistoryWithStore
 */ 
import { syncHistoryWithStore } from 'react-router-redux';

const store = configureStore();
/* 
 * Sync navigation events with the store
 */
const history = syncHistoryWithStore(hashHistory, store, {
  selectLocationState (state) {
    return state.get('routing').toObject();
  }
});

const routes = (
  <Provider store={store}>
    <Router history={history}>
      <Route path="/" component={Home}>
        <IndexRoute component={Welcome} />
        <Route path="/about" component={About} />
        <Route path="/contact" component={Contact} />
      </Route>
      <Route path="/games" component={Archive}>
        <IndexRoute component={GamesContainer} />
        <Route path="add" component={AddGameContainer} />
      </Route>
    </Router>
  </Provider>
);

export default routes;

That's all, the navigation is now synchronized with the store and we can dispatch actions to change the views. If you are interested in digging more into redux and immutability I suggest you to take a look at redux-immutable documentation where it also further explains what we have just done.

如此,导航现在已与商店同步,我们可以调度动作来更改视图。 如果您有兴趣进一步研究redux和不变性,建议您看一看redux不可变的文档,该文档还将进一步说明我们所做的工作。

Time to write the login.

是时候写登录了。

用户登录 (User Login)

Let's start by the view, create a Login component in /client/src/components/Login.jsx and paste the following code:

让我们从视图开始,在/client/src/components/Login.jsx创建一个Login组件,并粘贴以下代码:

// We import a bunch of dependencies
import React, { PureComponent } from 'react';
import { Link } from 'react-router';
import { Field, reduxForm } from 'redux-form/immutable';
// Some action-creators we are going to write later
import * as authActionCreators from '../actions/auth';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

class Login extends PureComponent {
  login () {
    // dispatch action to the redux-saga
    this.props.authActions.loginUser(this.props.location.query.next || '/games');
  }

  render () {
    const { picture, uploadPicture } = this.props;
    return (
      <div className="row scrollable">
        <div className="col-md-offset-2 col-md-8">
          <div className="text-left">
            <Link to="/games" className="btn btn-info">Back</Link>
          </div>
       <div className="panel panel-default">
         <div className="panel-heading">
        <h2 className="panel-title text-center">Login</h2>
         </div>
         <div className="panel-body">
           <form>
                <div className="form-group text-left">
                  <label htmlFor="name">Name</label>
                  <Field
                    name="email"
                    type="text"
                    className="form-control"
                    component="input"
                    placeholder="Enter the name"
                  />
                </div>
                <div className="form-group text-left">
                  <label htmlFor="password">Password</label>
                  <Field
                    name="password"
                    component="textarea"
                    className="form-control"
                    placeholder="Enter the password"
                    rows="5"
                  />
                </div>
          <button 
            type="button" 
            className="btn btn-submit btn-block" 
            onClick={() => this.login()}
          >
          Login
          </button>
          </form>
        </div>
      </div>
       </div>
     </div>
    );
  }
}
// Bind the action-creators so that we can call them as props
function mapDispatchToProps (dispatch) {
  return {
    authActions: bindActionCreators(authActionCreators, dispatch)
  };
}
// Wrap the login into a reduxForm HOC
export default reduxForm({ form: 'login' })(connect(null, mapDispatchToProps)(Login));

The Login component is nothing but a redux-form asking for e-mail and password (remember the test with Postman?). The login button is responsible for calling the login function which dispatches the action described by loginUser.

登录组件不过是一个要求电子邮件和密码的redux表单(还记得邮递员的测试吗?)。 登录按钮负责调用login功能,该功能分派loginUser描述的操作。

Let's make the Login available through /client/src/components/index.js:

让我们通过/client/src/components/index.js使登录可用:

import About from './About';
import Contact from './Contact';
import Form from './Form';
import Game from './Game';
import GamesListManager from './GamesListManager';
import Home from './Home';
import Archive from './Archive';
import Modal from './Modal';
import Welcome from './Welcome';
/* 
 * Here we import Login.jsx
 */ 
import Login from './Login';

export {
  About,
  Contact,
  Form,
  Game,
  GamesListManager,
  Home,
  Archive,
  Modal,
  Welcome,
  Login // Export Login
};

What about the login route? We wanto to show the login view at localhost:8080/auth/login so let's edit /client/src/routes.js:

那登录路线呢? 我们想在localhost:8080 / auth / login上显示登录视图,所以让我们编辑/client/src/routes.js

import React from 'react';
import { Provider } from 'react-redux';
import configureStore from './store';
import { Router, Route, hashHistory, IndexRoute } from 'react-router';
import { AddGameContainer, GamesContainer } from './containers';
/* 
 * We can conveniently import Login with all the other components
 */ 
import { Home, Archive, Welcome, About, Contact, Login } from './components';
/* 
 * Here we imported syncHistoryWithStore
 */ 
import { syncHistoryWithStore } from 'react-router-redux';

const store = configureStore();
/* 
 * Sync navigation events with the store
 */ 
const history = syncHistoryWithStore(hashHistory, store, {
  selectLocationState (state) {
    return state.get('routing').toObject();
  }
});

const routes = (
  <Provider store={store}>
    <Router history={history}>
      <Route path="/" component={Home}>
        <IndexRoute component={Welcome} />
        <Route path="/about" component={About} />
        <Route path="/contact" component={Contact} />
      </Route>
      <Route path="/games" component={Archive}>
        <IndexRoute component={GamesContainer} />
        <Route path="add" component={AddGameContainer} />
      </Route>
      {
      /* 
       * The Archive component defines the layout that works for both Login and Signup
       */
    }
      <Route path="/auth" component={Archive}>
        <Route path="login" component={Login} />
       </Route>
    </Router>
  </Provider>
);

export default routes;

Notice the new Route with path /auth has component Archive, in fact the layout of the login page (and as we will see later, of the signup too) is very similar to the AddGame view so we can reuse it.

请注意,路径为/ auth的新Route具有组件Archive ,实际上,登录页面(以及我们稍后将看到的注册页面)的布局与AddGame视图非常相似,因此我们可以重复使用它。

We haven't defined the action-creators yet, let's do it.

我们尚未定义动作创建者,让我们开始吧。

In /client/src/actions create a file auth.js and paste the following code:

/client/src/actions创建一个文件auth.js并粘贴以下代码:

// We always define constants
import {
  LOGIN_USER,
  LOGIN_USER_SUCCESS,
  LOGIN_USER_FAILURE,
} from '../constants/auth';

// Intercepted by a redux-saga
function loginUser (redirection) {
  return {
    type: LOGIN_USER,
    redirection
  };
}

// In case of successful response from the server
function loginUserSuccess (token) { // It carries the token!
  return {
    type: LOGIN_USER_SUCCESS,
    token
  };
}

// In case of failure
function loginUserFailure () {
  return {
    type: LOGIN_USER_FAILURE
  };
}

export {
  loginUser,
  loginUserSuccess,
  loginUserFailure
};
  • The loginUser action-creator dispatch a LOGIN_USER action which is intercepted by a saga we are writing later.

    loginUser操作创建者调度LOGIN_USER操作,该操作被我们稍后编写的传奇拦截。
  • The saga sends user credentials to the server and wait for a token, then call loginUserSuccesful or loginUserFailure according to the response.

    传奇将用户凭据发送到服务器并等待令牌,然后根据响应调用loginUserSuccesfulloginUserFailure

You should be already familiar with the pattern of writing 3 action-creators.

您应该已经熟悉编写3个动作创建者的模式。

Let's now define the constants, create a file auth.js in /src/client/constants:

现在让我们定义常量,在/src/client/constants创建一个文件auth.js

// New constants for the login
const LOGIN_USER = 'LOGIN_USER';
const LOGIN_USER_SUCCESS = 'LOGIN_USER_SUCCESS';
const LOGIN_USER_FAILURE = 'LOGIN_USER_FAILURE';

export {
  LOGIN_USER,
  LOGIN_USER_SUCCESS,
  LOGIN_USER_FAILURE,
};

The code is pretty straightforward, we export the constants to be used throughout the project.

该代码非常简单,我们导出要在整个项目中使用的常量。

Then, let's create the reducer in a new file in /client/src/reducers/auth.js:

然后,让我们在/client/src/reducers/auth.js中的新文件中创建reducer:

import Immutable from 'immutable';
// We neeed jwt-decode to take the user name from the token and store it in the state
import jwtDecode from 'jwt-decode';
import {
  LOGIN_USER_SUCCESS,
  LOGIN_USER_FAILURE,
} from '../constants/auth';

// The initial state has no token hence no name and isAuthenticated is false
const initialState = Immutable.Map({
  isAuthenticated: false,
  token: null,
  name: null
});

export default (state = initialState, action) => {
  switch (action.type) {
    // Once the server sent a token, the saga dispatches loginUserSuccess 
    case LOGIN_USER_SUCCESS: { 
      return state.merge({
        isAuthenticated: true,
        token: action.token,
        name: jwtDecode(action.token).sub
      });
    }
    // In case of failure the state goes back to the initial one
    case LOGIN_USER_FAILURE: return state.merge(initialState);
    default: return state;
  }
}
  • isAuthenticated is a very important field, later we will use to query the state and see whether the user is authenticated or not.

    isAuthenticated是一个非常重要的字段,稍后我们将使用它来查询状态并查看用户是否已通过身份验证。
  • Moreover, we decoded the token to get the user name and include it in the welcome message.

    此外,我们对令牌进行了解码以获取用户名并将其包含在欢迎消息中。

Have you noticed we imported jwt-decode? That's a simple library to help us decode the token in the frontend side of the app. Let's install it:

您是否注意到我们导入了jwt-decode ? 这是一个简单的库,可帮助我们在应用程序前端解码令牌。 让我们安装它:

yarn add jwt-decode

Let's update /client/src/reducers/index.js:

让我们更新/client/src/reducers/index.js

import { combineReducers } from 'redux-immutable';
import { reducer as form } from 'redux-form/immutable';
import games from './games';
import filestack from './filestack';
/* 
 * Import the auth reducer
 */ 
import auth from './auth';
import routing from './routing';

export default combineReducers({
  games,
  form,
  filestack,
  auth, // Combine it with the other reducers
  routing
});

Finally, we need a new saga to communicate with the server. So create auth.js in /client/src/sagas and paste the following code:

最后,我们需要一个新的传奇来与服务器通信。 因此,在/client/src/sagas创建auth.js并粘贴以下代码:

import { takeLatest } from 'redux-saga';
import { put, call, select } from 'redux-saga/effects';
// We import the constant to use it in the watcher
import { LOGIN_USER } from '../constants/auth';
import {
  loginUserSuccess,
  loginUserFailure
} from '../actions/auth';
// push action-creators to change the view
import { push } from 'react-router-redux';
// We want to show a notification to the user once logged in
import {actions as toastrActions} from 'react-redux-toastr';

// Selector to get the credential from the form
const getForm = (state, form) => {
  return state.getIn(['form', form]).toJS();
}

// Fetch sends the credentials to the server
const sendCredentials = (route, credentials) => {
  return fetch(`http://localhost:8080/auth/${route}`, {
    headers: new Headers({
      'Content-Type': 'application/json'
    }),
    method: 'POST',
    body: JSON.stringify(credentials)
  })
  .then(response => {
    if (response.status === 200) {
      return response.json(); // This contains the token!
    }
    throw response;
  });
};

function* loginUser (action) {
  // The redirection changes the view to the main page
  const { redirection } = action;
  try {
    const credentials = yield select(getForm, 'login');
    const result = yield call(sendCredentials, 'login', credentials.values);
    // Redux-toastr shows the users nice notifications
    yield put(toastrActions.add({
       type: 'success', // success is a green notification
       title: 'Retrogames Archive',
       message: result.message
    }));
    // We also save the token in the local storage
    localStorage.setItem('token', result.token); 
    // We send the token to the reducer
    yield put(loginUserSuccess(result.token));
    // Redirect to the main page!
    yield put(push(redirection));
  } catch (e) {
    // The status 401 has a personalized message to show in a notification
    let message = '';
    if(e.status === 401) {
      message = 'Invalid email/password';
    } else {
      message = 'Sorry, an error occured!';
    }
    // Set the state to initial state
    yield put(loginUserFailure());
    yield put(toastrActions.add({
       type: 'error', // Red notification
       title: 'Retrogames Archive',
       message: message
     }));
  }
}

// Saga watcher to intercept LOGIN_USER
export function* watchLoginUser () {
  yield takeLatest(LOGIN_USER, loginUser);
}

Although the loginUser seems more complicated than the others we wrote in the past, it is actually very easy, the comments in the code highlight the steps. So, you must have notice redux-toastr that, as the name says, implement toastrs notifications to be used with redux.

尽管loginUser似乎比我们过去编写的其他用户更为复杂,但实际上非常简单,代码中的注释突出了步骤。 因此,您必须注意到redux-toastr ,顾名思义,它实现了与redux一起使用的toastrs通知。

We haven't added them yet, so let install it:

我们尚未添加它们,因此请安装它:

yarn add redux-toastr

Redux-toastr requires to add its own reducer, edit /client/src/reducers/index.js:

Redux-toastr需要添加自己的reducer,编辑/client/src/reducers/index.js

import { combineReducers } from 'redux-immutable';
import { reducer as form } from 'redux-form/immutable';
import games from './games';
import filestack from './filestack';
import auth from './auth';
import routing from './routing';
/* 
 * Import redux-toastr reducer
 */ 
import { reducer as toastr} from 'react-redux-toastr'

export default combineReducers({
  games,
  form,
  filestack,
  auth,
  routing,
  toastr // Combine toastr
});

Then, it requires its own css, edit /client/dist/index.html:

然后,它需要自己CSS,编辑/client/dist/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Retrogames Archive</title>
    <link rel="icon" href="https://cdn.filestackcontent.com/S0zeyXxRem6pL6tHq9pz">
<!-- redux-toastr css to style the notifications -->
    <link href="http://diegoddox.github.io/react-redux-toastr/4.4/react-redux-toastr.min.css" rel="stylesheet" type="text/css">
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">

  </head>
  <body>
    <div id="content"></div>
    <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
    <script src="https://api.filestackapi.com/filestack.js"></script>
    <script src="./bundle.js"></script>
  </body>
</html>

The last step is to add ReduxToastr component at the root of the app, let's edit /client/src/routes.js:

最后一步是在应用程序的根目录添加ReduxToastr组件,让我们编辑/client/src/routes.js

import React from 'react';
import { Provider } from 'react-redux';
import configureStore from './store';
import { Router, Route, hashHistory, IndexRoute } from 'react-router';
import { AddGameContainer, GamesContainer } from './containers';
import { Home, Archive, Welcome, About, Contact, Login } from './components';
import { syncHistoryWithStore } from 'react-router-redux';
/* 
 * Import ReduxToastr
 */ 
import ReduxToastr from 'react-redux-toastr';

const store = configureStore();
const history = syncHistoryWithStore(hashHistory, store, {
  selectLocationState (state) {
    return state.get('routing').toObject();
  }
});

const routes = (
  <Provider store={store}>
  {
  /* 
   * We add the div wrapper as Route and ReduxToastr are on the same level 
   */
   }
    <div className="wrapper">
    <Router history={history}>
      <Route path="/" component={Home}>
        <IndexRoute component={Welcome} />
        <Route path="/about" component={About} />
        <Route path="/contact" component={Contact} />
       </Route>
       <Route path="/games" component={Archive}>
         <IndexRoute component={GamesContainer} />
         <Route path="add" component={AddGameContainer} />
       </Route>
       <Route path="/auth" component={Archive}>
         <Route path="login" component={Login} />
       </Route>
    </Router>
    {
      /* 
       * we can customize it's behavior and look through props 
      /*
    }
    <ReduxToastr
        timeOut={2000}
        newestOnTop={false}
        preventDuplicates={true}
        position="top-right"
        transitionIn="fadeIn"
        transitionOut="fadeOut"
      />
   </div>
  </Provider>
);

export default routes;

For more information regarding Redux-toastr options, check the documentation.

有关Redux-toastr选项的更多信息,请参阅文档

The login logic is done, let's do the signup that you will see it's very similar.

登录逻辑已完成,让我们进行注册,您将看到它非常相似。

用户注册 (User Signup)

Let's create Signup.jsx in /client/src/components and paste the following code:

让我们在/client/src/components创建Signup.jsx并粘贴以下代码:

import React, { PureComponent } from 'react';
import { Link } from 'react-router';
import { Field, reduxForm } from 'redux-form/immutable';
import * as authActionCreators from '../actions/auth';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

class Signup extends PureComponent {
  // signupUser dispatches SIGNUP_USER to be intercepted by a redux-saga
  register () {
    this.props.authActions.signupUser();
  }

  render () {
    const { picture, uploadPicture } = this.props;
    return (
      <div className="row scrollable">
    <div className="col-md-offset-2 col-md-8">
         <div className="text-left">
            <Link to="/games" className="btn btn-info">Back</Link>
         </div>
      <div className="panel panel-default">
        <div className="panel-heading">
          <h2 className="panel-title text-center">
        Sign Up
          </h2>
        </div>
       <div className="panel-body">
        <form onSubmit={this.props.handleSubmit}>
                <div className="form-group text-left">
                  <label htmlFor="email">E-mail</label>
                  <Field
                    name="email"
                    type="text"
                    className="form-control"
                    component="input"
                    placeholder="Enter the e-mail"
                  />
                </div>
                <div className="form-group text-left">
                  <label htmlFor="name">Name</label>
                  <Field
                    name="name"
                    type="text"
                    className="form-control"
                    component="input"
                    placeholder="Enter the name"
                  />
                </div>
                <div className="form-group text-left">
                  <label htmlFor="password">Password</label>
                  <Field
                    name="password"
                    component="textarea"
                    className="form-control"
                    placeholder="Enter the password"
                    rows="5"
                  />
                </div>
        <button 
          type="button" 
          className="btn btn-submit btn-block" 
          onClick={() => this.register()}
        >
        Register
        </button>
        </form>
       </div>
      </div>
    </div>
     </div>
    );
  }
}
// Bint the action-creators to be used as props
function mapDispatchToProps (dispatch) {
  return {
    authActions: bindActionCreators(authActionCreators, dispatch)
  };
}
// redux-form HOC to wrap the component
export default reduxForm({ form: 'signup' })(connect(null, mapDispatchToProps)(Signup));

It's very similar to Login so let's move on and edit /client/src/components/index.js to make the component available:

它与Login非常相似,因此让我们继续并编辑/client/src/components/index.js以使该组件可用:

import About from './About';
import Contact from './Contact';
import Form from './Form';
import Game from './Game';
import GamesListManager from './GamesListManager';
import Home from './Home';
import Archive from './Archive';
import Modal from './Modal';
import Welcome from './Welcome';
import Login from './Login';
/* 
 * Import Signup
 */ 
import Signup from './Signup';

export {
  About,
  Contact,
  Form,
  Game,
  GamesListManager,
  Home,
  Archive,
  Modal,
  Welcome,
  Login,
  Signup // Export Signup
};

And then we create its own route at /auth/signup. Let's edit /client/src/routes.js and paste the following:

然后在/ auth / signup创建自己的路由。 让我们编辑/client/src/routes.js并粘贴以下内容:

import React from 'react';
import { Provider } from 'react-redux';
import configureStore from './store';
import { Router, Route, hashHistory, IndexRoute } from 'react-router';
import { AddGameContainer, GamesContainer } from './containers';
/* 
 * We also import Signup
 */ 
import { Home, Archive, Welcome, About, Contact, Login, Signup } from './components';
import { syncHistoryWithStore } from 'react-router-redux';
import ReduxToastr from 'react-redux-toastr';

const store = configureStore();
const history = syncHistoryWithStore(hashHistory, store, {
  selectLocationState (state) {
    return state.get('routing').toObject();
  }
});

const routes = (
  <Provider store={store}>
    <div className="wrapper">
    <Router history={history}>
      <Route path="/" component={Home}>
        <IndexRoute component={Welcome} />
        <Route path="/about" component={About} />
        <Route path="/contact" component={Contact} />
       </Route>
       <Route path="/games" component={Archive}>
         <IndexRoute component={GamesContainer} />
         <Route path="add" component={AddGameContainer} />
       </Route>
       <Route path="/auth" component={Archive}>
         {
         /* 
          * Signup Route 
          */
           }
         <Route path="signup" component={Signup} />
         <Route path="login" component={Login} />
       </Route>
    </Router>
    <ReduxToastr
        timeOut={2000}
        newestOnTop={false}
        preventDuplicates={true}
        position="top-right"
        transitionIn="fadeIn"
        transitionOut="fadeOut"
      />
   </div>
  </Provider>
);

export default routes;

To define the action-creators we are going to edit /client/src/actions/auth.js:

为了定义动作创建者,我们将编辑/client/src/actions/auth.js

import {
  LOGIN_USER,
  LOGIN_USER_SUCCESS,
  LOGIN_USER_FAILURE,
  /*
   * New constants to be imported
   */ 
  SIGNUP_USER,
  SIGNUP_USER_SUCCESS,
  SIGNUP_USER_FAILURE
} from '../constants/auth';

function loginUser (redirection) {
  return {
    type: LOGIN_USER,
    redirection
  };
}

function loginUserSuccess (token) {
  return {
    type: LOGIN_USER_SUCCESS,
    token
  };
}

function loginUserFailure () {
  return {
    type: LOGIN_USER_FAILURE
  };
}
/* 
 * signupUser dispatched from Signup component
 */ 
function signupUser () {
  return {
    type: SIGNUP_USER
  };
}
/* 
 * SignupUserSuccess send the token to be added to the state
 */ 
function signupUserSuccess (token) { // It carries the token!
  return {
    type: SIGNUP_USER_SUCCESS,
    token
  };
}
/* 
 * In case of server failure
 */
function signupUserFailure () {
  return {
    type: SIGNUP_USER_FAILURE
  };
}

export {
  loginUser,
  loginUserSuccess,
  loginUserFailure,
  signupUser,
  signupUserSuccess,
  signupUserFailure
};

As we did for login, let's add the constants in /client/src/constants/auth.js:

就像登录一样,让我们​​在/client/src/constants/auth.js添加常量:

const LOGIN_USER = 'LOGIN_USER';
const LOGIN_USER_SUCCESS = 'LOGIN_USER_SUCCESS';
const LOGIN_USER_FAILURE = 'LOGIN_USER_FAILURE';
/* 
 * New constants
 */
const SIGNUP_USER = 'SIGNUP_USER';
const SIGNUP_USER_SUCCESS = 'SIGNUP_USER_SUCCESS';
const SIGNUP_USER_FAILURE = 'SIGNUP_USER_FAILURE';

export {
  LOGIN_USER,
  LOGIN_USER_SUCCESS,
  LOGIN_USER_FAILURE,
  SIGNUP_USER,
  SIGNUP_USER_SUCCESS,
  SIGNUP_USER_FAILURE
};

And now we are going to edit the reducer in /src/clients/reducers/auth.js:

现在,我们将在/src/clients/reducers/auth.js编辑reducer:

import Immutable from 'immutable';
import jwtDecode from 'jwt-decode';
import {
  LOGIN_USER_SUCCESS,
  LOGIN_USER_FAILURE,
  /* 
   * New constants
   */ 
  SIGNUP_USER_SUCCESS,
  SIGNUP_USER_FAILURE
} from '../constants/auth';

const initialState = Immutable.Map({
  isAuthenticated: false,
  token: null,
  name: null
});
// The actions dispatched by the signup logic do exactly the same of login ones
export default (state = initialState, action) => {
  switch (action.type) {
    case SIGNUP_USER_SUCCESS:
    case LOGIN_USER_SUCCESS: {
      return state.merge({
        isAuthenticated: true,
        token: action.token,
        name: jwtDecode(action.token).sub
      });
    }
    case SIGNUP_USER_FAILURE: // All the failures simply return the initial state
    case LOGIN_USER_FAILURE: return state.merge(initialState);
    default: return state;
  }
}

Someone may wonder why we should define different constants for login and signup as they practically do the same: This is actually a personal choice, in my case I want to make it easier to read and understand that the action belongs to the signup logic instead of the login one.

有人可能会奇怪,为什么我们应该为登录和注册定义不同的常量,因为它们实际上是一样的:这实际上是个人选择,在我的情况下,我想更容易阅读和理解该操作属于注册逻辑,而不是登录一个。

Finally, let's edit /client/src/sagas/auth:

最后,让我们编辑/client/src/sagas/auth

import { takeLatest } from 'redux-saga';
import { put, call, select } from 'redux-saga/effects';
import { LOGIN_USER, SIGNUP_USER } from '../constants/auth';
import {
  loginUserSuccess,
  loginUserFailure,
  /* 
   * We import the signup action-creators
   */
  signupUserSuccess,
  signupUserFailure
} from '../actions/auth';
import { push } from 'react-router-redux';
import {actions as toastrActions} from 'react-redux-toastr';

const getForm = (state, form) => {
  return state.getIn(['form', form]).toJS();
}

const sendCredentials = (route, credentials) => {
  return fetch(`http://localhost:8080/auth/${route}`, {
    headers: new Headers({
      'Content-Type': 'application/json'
    }),
    method: 'POST',
    body: JSON.stringify(credentials)
  })
  .then(response => {
    if (response.status === 200) {
      return response.json();
    }
    throw response;
  });
};

function* loginUser (action) {
  const { redirection } = action;
  try {
    const credentials = yield select(getForm, 'login');
    const result = yield call(sendCredentials, 'login', credentials.values);
    yield put(toastrActions.add({
       type: 'success',
       title: 'Retrogames Archive',
       message: result.message
    }));
    localStorage.setItem('token', result.token);
    yield put(loginUserSuccess(result.token));
    yield put(push(redirection));
  } catch (e) {
    let message = '';
    if(e.status === 401) {
      message = 'Invalid email/password';
    } else {
      message = 'Sorry, an error occured!';
    }
    yield put(loginUserFailure());
    yield put(toastrActions.add({
       type: 'error',
       title: 'Retrogames Archive',
       message: message
     }));
  }
}
/* 
 * the new sagas to handle signup
 */ 
function* signupUser () {
  try {
    // We get the credentials from the form in the state
    const credentials = yield select(getForm, 'signup');
    const result = yield call(sendCredentials, 'signup', credentials.values);
    // Show a notification in the browser 
    yield put(toastrActions.add({
       type: 'success',
       title: 'Retrogames Archive',
       message: result.message
    }));
    // Set the token in the local storage
    localStorage.setItem('token', result.token);
    // Update the state with the token
    yield put(signupUserSuccess(result.token));
    // Redirect to /games
    yield put(push('/games'));
  } catch (e) {
    // As we did for loginUser, we show a personalized message according to the error status
    let message = '';
    if(e.status === 409) {
      message = 'Email is already taken';
    } else {
      message = 'Sorry, an error occured!';
    }
    // Set the auth portion of the state to the initial value
    yield put(signupUserFailure());
    yield put(toastrActions.add({
       type: 'error',
       title: 'Retrogames Archive',
       message: message
     }));
  }
}

export function* watchLoginUser () {
  yield takeLatest(LOGIN_USER, loginUser);
}

/* 
 * Signup watcher
 */
export function* watchSignupUser () {
  yield takeLatest(SIGNUP_USER, signupUser);
}

And we are done for the sign-up logic! We need to run the two sagas watchLoginUser and watchSignupUser. Edit /client/src/sagas/index.js and paste the following code:

我们已经完成了注册逻辑! 我们需要运行两个sagas watchLoginUser和watchSignupUser。 编辑/client/src/sagas/index.js并粘贴以下代码:

import {
  watchGetGames,
  watchDeleteGame,
  watchPostGame
} from './games';
import { watchUploadPicture } from './filestack';
/* 
 * The new watchers in charge of the authentication
 */ 
import { watchLoginUser, watchSignupUser } from './auth';

export default function* rootSaga () {
  yield [
    watchGetGames(),
    watchDeleteGame(),
    watchPostGame(),
    watchUploadPicture(),
    watchLoginUser(),
    watchSignupUser()
  ];
}

Now you can create a user and login by manually type /auth/signup or /auth/login.However the app has no buttons to login/sign-up, plus users who actually are not authenticated can still create or delete games. It's now time to protect the app and this concludes our tutorial.

现在您可以通过手动输入/ auth / signup/ auth / login来创建用户并登录。但是,该应用程序没有用于登录/注册的按钮,而且未经身份验证的用户仍然可以创建或删除游戏。 现在该保护该应用了,我们的教程到此结束。

认证包装 (Authentication Wrapper)

So we want to protect some routes in our app and possibly hide/show the buttons to delete or add a game. To do so we are going to rely on redux-auth-wrapper. So what is this authentication wrapper? The idea is that we can decouple Authentication and Authorization with high order components that wrap other components, this is kinda useful to protect routes too!

因此,我们希望保护应用程序中的某些路线,并可能隐藏/显示按钮以删除或添加游戏。 为此,我们将依赖redux-auth-wrapper 。 那么这个认证包装器是什么呢? 这个想法是,我们可以将身份验证和授权与包装其他组件的高阶组件分离,这对于保护路由也很有用!

Let's go on and install it:

让我们继续安装它:

yarn add redux-auth-wrapper

if you open /client/src/routes.js you should know that on /games/add the router is going to show AddGameContainer, however this should not happen when the user is not authenticated. Otherwise he/shes is going to be redirected to the login page.

如果打开/client/src/routes.js您应该知道/ games / add上的路由器将显示AddGameContainer ,但是当用户未通过身份验证时不会发生这种情况。 否则,他/她将被重定向到登录页面。

Let's see it in action, we can create authWrapper.js file in /client/src/utils and paste the following code:

让我们来看看它的作用,我们可以在/client/src/utils创建authWrapper.js文件,并粘贴以下代码:

// We import the wrapper component
 import { UserAuthWrapper } from 'redux-auth-wrapper';

// We export a simple function which receives some options and return the wrapper
export default (options) => UserAuthWrapper(options);

The options we provide are useful to define customized rules for the app.

我们提供的选项对于定义应用程序的自定义规则很有用。

Now in /client/routes.js paste the following code:

现在在/client/routes.js粘贴以下代码:

import React from 'react';
import { Provider } from 'react-redux';
import configureStore from './store';
import { Router, Route, browserHistory, hashHistory, IndexRoute } from 'react-router';
import { AddGameContainer, GamesContainer } from './containers';
import { Home, Archive, Welcome, About, Contact, Login, Signup } from './components';
import { UserAuthWrapper } from 'redux-auth-wrapper';
import { push } from 'react-router-redux';
import { syncHistoryWithStore } from 'react-router-redux';
import ReduxToastr from 'react-redux-toastr';
/* 
 * We imported the utility function
 */ 
import userAuthenticated from './utils/authWrapper';

const store = configureStore();
const history = syncHistoryWithStore(hashHistory, store, {
  selectLocationState (state) {
    return state.get('routing').toObject();
  }
});
/* 
 * Here we set the rules for the wrapper
 */ 
const options = {
  authSelector: state => state.get('auth'),
  predicate: auth => auth.get('isAuthenticated'),
  redirectAction: ({ pathname, query }) => {
    if(query.redirect) {
    // If the user is not logged in go to /auth/login
      return push(`auth${pathname}?next=${query.redirect}`);
    } 
  },
  wrapperDisplayName: 'UserIsJWTAuthenticated'
};
const requireAuthentication = userAuthenticated(options);

const routes = (
  <Provider store={store}>
    <div className="wrapper">
      <Router history={history}>
        <Route path="/" component={Home}>
          <IndexRoute component={Welcome} />
          <Route path="/about" component={About} />
          <Route path="/contact" component={Contact} />
        </Route>
        <Route path="/games" component={Archive}>
          <IndexRoute component={GamesContainer} />
       {
         /* 
          * you can see that AddGameContainer is now literally wrapped 
          */
          }
          <Route path="add" component={requireAuthentication(AddGameContainer)} />
        </Route>
        <Route path="/auth" component={Archive}>
          <Route path="signup" component={Signup} />
          <Route path="login" component={Login} />
        </Route>
      </Router>
      <ReduxToastr
        timeOut={2000}
        newestOnTop={false}
        preventDuplicates={true}
        position="top-right"
        transitionIn="fadeIn"
        transitionOut="fadeOut"
      />
    </div>
  </Provider>
);

export default routes;

So what have we actually done?

那么我们实际上做了什么?

  • authSelector receives a selector of part of the state, in our case the auth has it's contain isAuthenticated

    authSelector接收状态的一部分的选择器,在本例中,auth具有它的包含isAuthenticated
  • predicate receives the result from authSelector and according to the boolean balue of isAuthenticated it will either go to games/add or to auth/login.

    谓词从authSelector接收结果,根据isAuthenticated的布尔值,它将进入游戏/添加或进入auth /登录
  • As you can redirectAction is a redux action-creator, we check the redirection and in case push to /auth/login?next=query.redirect. query.redirect is actually /games/add so when the user logged in the view automatically shows the form to add a new game.

    如您所料, redirectAction是一个Redux操作创建者,我们检查重定向,以防推送到/auth/login?next=query.redirectquery.redirect实际上是/ games / add,因此当用户登录视图时,它会自动显示添加新游戏的表单。
  • wrapperDisplayName is a name describing the authorization.

    wrapperDisplayName是描述授权的名称。

It's great, in few lines of code we have now a wrapper we can reuse to protect all the other routes that require authentication. However, we can do more than this, redux-auth-wrapper can actually hide/show any component, which is what we want to achieve: Show/hide the delete game button, or show the add game button when the user is authenticated while replacing it with login and sign-up buttons when not authenticated. Let's see how easy it is!

太好了,在几行代码中,我们现在有了一个包装器,可以重复使用该包装器来保护所有其他需要身份验证的路由。 但是,我们可以做更多的事情, redux-auth-wrapper实际上可以隐藏/显示任何组件,这是我们想要实现的:显示/隐藏“删除游戏”按钮,或者在用户通过身份验证时显示“添加游戏”按钮。未经身份验证时将其替换为登录和注册按钮。 让我们看看它有多简单!

Let's start from the delete button, to hide/show it we simply wrap it and pass the right options! Edit /client/src/components/Game.jsx and paste the following code:

让我们从删除按钮开始,要隐藏/显示它,我们只需要包装它并传递正确的选项即可! 编辑/client/src/components/Game.jsx并粘贴以下代码:

import React, { PureComponent } from 'react';
import { Link } from 'react-router';
/* 
 * We import our utility function
 */ 
import userAuthenticated from '../utils/authWrapper';

/* 
 * Here we set the rules to hide/show the delete button
 */ 
const options = {
  authSelector: state => state.get('auth'),
  predicate: auth => auth.get('isAuthenticated'),
  wrapperDisplayName: 'authDeleteGame',
  FailureComponent: null
};

/* 
 * We define the 'wrapper' version of the delete button
 */ 
const DeleteButton = userAuthenticated(options)(
  (props) => <button className="btn btn-danger" role="button" onClick={() => props.deleteGame(props.id)}>Delete</button>
);

export default class Game extends PureComponent {
  render () {
    const { _id, i, name, description, picture, toggleModal, deleteGame } = this.props;
    return (
      <div className="col-md-4">
        <div className="thumbnail">
          <div className="thumbnail-frame">
            <img src={picture} alt="..." className="img-responsive thumbnail-pic" />
          </div>
          <div className="caption">
            <h5>{name}</h5>
            <p className="description-thumbnail">{`${description.substring(0, 150)}...`}</p>
            <div className="btn-group" role="group" aria-label="...">
              <button className="btn btn-success" role="button" onClick={() => toggleModal(i)}>View</button>
           {
           /* 
            * the new DeleteButton 
            */
           }
              <DeleteButton deleteGame={deleteGame} id={_id} />
            </div>
          </div>
        </div>
      </div>
    );
  }
}
  • As we have done before to protect the rule, we check inside the app state and if isAuthenticated the button will show up in the page.

    正如我们之前为保护规则所做的那样,我们在应用状态内部进行检查,如果已验证为isAuthenticated ,则该按钮将显示在页面中。
  • FailureComponent is just a component we can render in the page when the user is not authorized. In this case it's set to null because there is no need but it comes handy with the sign-up/login panel instead.

    FailureComponent只是当用户未经授权时可以在页面中呈现的组件。 在这种情况下,将其设置为null是因为没有必要,但是它在注册/登录面板中很方便。

Finally, we want to show the add game button when the user is authenticated, plus a welcome message and logout button. But, when the user is not logged in, then show the buttons sign-in and login.

最后,我们希望在用户通过身份验证时显示添加游戏按钮,以及欢迎消息和注销按钮。 但是,当用户未登录时,请显示按钮登录和登录。

Let's have another look at the UI:

让我们再看一下UI:

  1. User Authenticated

    用户认证
  2. User not Authenticated

    用户未认证

Let's create another component to achieve this, in /client/src/components create AddGamePanel.jsx and paste the following code:

让我们创建另一个组件来实现此目的,在/client/src/components创建AddGamePanel.jsx并粘贴以下代码:

import React, { PureComponent } from 'react';
import { Link } from 'react-router';
/* 
 * Import the utility function
 */ 
import userAuthenticated from '../utils/authWrapper';

class AddGamePanel extends PureComponent {
  render () {
    /* 
     * userName comes from the state while logout 
     * is an action creator we are going to define later 
     */
    const { userName, logout } = this.props;
    return (
      <div className="add-game-panel">
        <h5>Welcome back {userName}, <span onClick={logout}>Logout</span></h5>
        <Link to="/games/add" className="btn btn-danger">add a new Game!</Link>
      </div>
    );
  }
}
/* 
 * Auth-wrapper options
 */ 
const options = {
  authSelector: state => state.get('auth'),
  predicate: auth => auth.get('isAuthenticated'),
  wrapperDisplayName: 'authAddGame',
  /* 
   * This time the failure component are the buttons 
   * to authenticate the user or register a new one 
   */
  FailureComponent: () => {
    return (
      <div className="btn-group" role="group" aria-label="...">
        <Link to="/auth/signup" className="btn btn-primary">Sign Up</Link>
        <Link to="/auth/login" className="btn btn-danger">Login</Link>
      </div>
    );
  }
};

// We export it 
export default userAuthenticated(options)(AddGamePanel);

The options are totally the same as the delete buttons ones but this time we render FailureComponent when the user is not authenticated.

这些选项与删除按钮的选项完全相同,但是这一次,我们在未通过用户身份验证时呈现FailureComponent

Now, let's import it inside GamesListManager, edit /client/src/components/GamesListManager.jsx:

现在,让我们将其导入GamesListManager内 ,编辑/client/src/components/GamesListManager.jsx

import React, { PureComponent } from 'react';
import { Link } from 'react-router';
import Game from './Game';
/* 
 * Import the new component
 */
import AddGamePanel from './AddGamePanel';

export default class GamesListManager extends PureComponent {

  render () {
    const {
      games,
      searchBar,
      setSearchBar,
      toggleModal,
      deleteGame,
      userName,
      /*
    * The new action-creator to be defined
    */ 
      logout
    } = this.props;

    return (
      <div className="container scrollable">
        <div className="row text-left">
        {
        /* 
         * we add the component 
         */
        }
          <AddGamePanel logout={logout} userName={userName}/>
        </div>
        <div className="row">
          <input
            type="search" placeholder="Search by Name" className="form-control search-bar" onKeyUp={setSearchBar} />
        </div>
        <div className="row">
        {
          games
            .filter(game => game.name.toLowerCase().includes(searchBar))
            .map((game, i) => {
              return (
                <Game  {...game}
                  key={game._id}
                  i={i}
                  toggleModal={toggleModal}
                  deleteGame={deleteGame}
                />
              );
            })
        }
        </div>
        <hr />
      </div>

    );
  }
}

Nothing exotic, we just render the component inside its parent GamesListManager.

没什么奇怪的 ,我们只是在其父GamesListManager中渲染该组件。

The userName and logout props come from the GamesContainer so let's edit /client/src/containers/GamesContainer.jsx:

userNamelogout道具来自GamesContainer,所以让我们编辑/client/src/containers/GamesContainer.jsx

import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Immutable from 'immutable';
import { Modal, GamesListManager } from '../components';
import * as gamesActionCreators from '../actions/games';
import * as authActionCreators from '../actions/auth';
/* 
 * Add toastr to show notifcations
 */ 
import { toastr } from 'react-redux-toastr';

class GamesContainer extends PureComponent {
  constructor (props) {
    super(props);
    this.toggleModal = this.toggleModal.bind(this);
    this.deleteGame = this.deleteGame.bind(this);
    this.setSearchBar = this.setSearchBar.bind(this);
    // Bind logout to this
    this.logout = this.logout.bind(this);
  }

  componentDidMount () {
    this.getGames();
  }

  toggleModal (index) {
    this.props.gamesActions.showSelectedGame(this.props.games[index]);
    $('#game-modal').modal();
  }

  getGames () {
    this.props.gamesActions.getGames();
  }

  deleteGame (id) {
    this.props.gamesActions.deleteGame(id);
  }

  setSearchBar (event) {
    this.props.gamesActions.setSearchBar(event.target.value.toLowerCase());
  }
  /* 
   * The function calls an action to remove the user 
   * from the state, show a notification and    delete the token from the local storage 
   */
  logout () {
    this.props.authActions.logoutUser();
    toastr.success('Retrogames archive', 'Your are now logged out');
    localStorage.removeItem('token');
  }

  render () {
    const { games, selectedGame, searchBar, userName, authActions } = this.props;
    return (
      <div>
        <Modal game={selectedGame} />
        <GamesListManager
          games={games}
          searchBar={searchBar}
          setSearchBar={this.setSearchBar}
          toggleModal={this.toggleModal}
          deleteGame={this.deleteGame}
       {
       /* 
        * the new props to be passed 
        */
       }
          userName={userName}
          logout={this.logout}
        />
      </div>
    );
  }
}

function mapStateToProps (state) {
  return {
    games: state.getIn(['games', 'list'], Immutable.List()).toJS(),
    searchBar: state.getIn(['games', 'searchBar'], ''),
    selectedGame: state.getIn(['games', 'selectedGame'], Immutable.List()).toJS(),
    /* 
     * The name comes from token after being decoded
     */ 
    userName: state.getIn(['auth', 'name'])
  }
}

function mapDispatchToProps (dispatch) {
  return {
    gamesActions: bindActionCreators(gamesActionCreators, dispatch),
    authActions: bindActionCreators(authActionCreators, dispatch)
  };
}
export default connect(mapStateToProps, mapDispatchToProps)(GamesContainer);

As the container contains the logic it's a connected component, this is where we can get the props for AddGamePanel.

由于容器包含逻辑,它是一个连接的组件,因此可以在此获取AddGamePanel的道具。

To conclude, let's create the logout action-creators in /client/src/actions/auth.js:

最后,让我们在/client/src/actions/auth.js创建注销操作创建者:

import {
  LOGIN_USER,
  LOGIN_USER_SUCCESS,
  LOGIN_USER_FAILURE,
  /* 
   * Import a new constant
   */ 
  LOGOUT_USER,
  SIGNUP_USER,
  SIGNUP_USER_SUCCESS,
  SIGNUP_USER_FAILURE
} from '../constants/auth';

function loginUser (redirection) {
  return {
    type: LOGIN_USER,
    redirection
  };
}

function loginUserSuccess (token) {
  return {
    type: LOGIN_USER_SUCCESS,
    token
  };
}

function loginUserFailure () {
  return {
    type: LOGIN_USER_FAILURE
  };
}
/* 
 * The action-creator
 */ 
function logoutUser () {
  return {
    type: LOGOUT_USER
  };
}

function signupUser () {
  return {
    type: SIGNUP_USER
  };
}

function signupUserSuccess (token) {
  return {
    type: SIGNUP_USER_SUCCESS,
    token
  };
}

function signupUserFailure () {
  return {
    type: SIGNUP_USER_FAILURE
  };
}

export {
  loginUser,
  loginUserSuccess,
  loginUserFailure,
  logoutUser,
  signupUser,
  signupUserSuccess,
  signupUserFailure
};

And then add the constant in /client/src/constants/auth.js:

然后在/client/src/constants/auth.js添加常量:

const LOGIN_USER = 'LOGIN_USER';
const LOGIN_USER_SUCCESS = 'LOGIN_USER_SUCCESS';
const LOGIN_USER_FAILURE = 'LOGIN_USER_FAILURE';
/* 
 * New constant
 */ 
const LOGOUT_USER = 'LOGOUT_USER';
const SIGNUP_USER = 'SIGNUP_USER';
const SIGNUP_USER_SUCCESS = 'SIGNUP_USER_SUCCESS';
const SIGNUP_USER_FAILURE = 'SIGNUP_USER_FAILURE';

export {
  LOGIN_USER,
  LOGIN_USER_SUCCESS,
  LOGIN_USER_FAILURE,
  LOGOUT_USER,
  SIGNUP_USER,
  SIGNUP_USER_SUCCESS,
  SIGNUP_USER_FAILURE
};

Finally, the auth reducer is going to intercept the action LOGOUT_USER, edit /client/src/reducers/auth:

最后,auth reducer将截获LOGOUT_USER动作,编辑/client/src/reducers/auth

import Immutable from 'immutable';
import jwtDecode from 'jwt-decode';
import {
  LOGIN_USER_SUCCESS,
  LOGIN_USER_FAILURE,
  /* 
   * Import the logout constant
   */ 
  LOGOUT_USER,
  SIGNUP_USER_SUCCESS,
  SIGNUP_USER_FAILURE
} from '../constants/auth';

const initialState = Immutable.Map({
  isAuthenticated: false,
  token: null,
  name: null
});

export default (state = initialState, action) => {
  switch (action.type) {
    case SIGNUP_USER_SUCCESS:
    case LOGIN_USER_SUCCESS: {
      return state.merge({
        isAuthenticated: true,
        token: action.token,
        name: jwtDecode(action.token).sub
      });
    }
    case SIGNUP_USER_FAILURE:
    case LOGIN_USER_FAILURE: // initial state is naturally the logout state
    case LOGOUT_USER: return state.merge(initialState);
    default: return state;
  }
}

And that's all, we added authentication to our Archive app!

就是这样,我们向“存档”应用程序添加了身份验证!

There is only one last thing we can do: Whenever a user refreshes the page we should first look for the token in the local storage, in case it's valid we can directly authenticate him/her. createStore function receives an object parameter to define an initial state so we can take advantage of it: A function isAuthenticated looks for the token from the local storage and if the token exists the initial state sends the store the user data during its creation. Create authentication.js in client/src/utils and paste the following code:

我们只能做的最后一件事:每当用户刷新页面时,我们都应该首先在本地存储中查找令牌,如果有效,我们可以直接对其进行身份验证。 createStore函数接收一个对象参数来定义初始状态,因此我们可以利用它: isAuthenticated函数从本地存储中查找令牌,如果令牌存在,则初始状态会在创建过程中向存储发送用户数据。 在client/src/utils创建authentication.js并粘贴以下代码:

import jwtDecode from 'jwt-decode';

const isAuthenticated = () => {
  const token = localStorage.getItem('token');
  // Default initialState
  let initialState = {
    isAuthenticated: false,
    token: null,
    name: null
  };
  if (token) { 
    // This is the same result as LOGIN_USER_SUCCESS
    initialState = {
      isAuthenticated: true,
      token: token,
      name: jwtDecode(token).sub
    };
  }

  return initialState;
};

export default isAuthenticated;

And finally we can edit /client/src/store.js:

最后,我们可以编辑/client/src/store.js

import {
  createStore,
  applyMiddleware,
  compose
} from 'redux';
/*
 * Wrap initialState in an immutable data-structure
 */
import Immutable from 'immutable';
import createSagaMiddleware from 'redux-saga';
import rootSaga from './sagas';
import reducer from './reducers';
import { routerMiddleware } from 'react-router-redux';
import { hashHistory } from 'react-router';
/*
 * Import isAuthenticated to define initialState
 */
import isAuthenticated from './utils/authentication';

/*
 * We define the initialState with only information regarding the authentication
 */ 
const initialState = Immutable.fromJS({
  auth: isAuthenticated()
});

const configureStore = () => {
  const sagaMiddleware = createSagaMiddleware();
  const routeMiddleware = routerMiddleware(hashHistory);
  const store = createStore(
    reducer,
    initialState, // We pass the initialState for user authentication
    compose(
      applyMiddleware(sagaMiddleware, routeMiddleware),
      window.devToolsExtension ? window.devToolsExtension() : (f) => f
    )
  );
  sagaMiddleware.run(rootSaga);

  return store;
}
export default configureStore;

And this is the end of the tutorial, Congratulations!

恭喜,这就是本教程的结尾!

结论 ( Conclusions )

In this part.3 of the tutorial to create a Retrogame Archive app we added a simple authentication logic to limit the actions users can do when not logged in: They can only view the games, to modify the database they must be authorized.

在本教程的第3部分中,创建了Retrogame Archive应用程序,我们添加了一个简单的身份验证逻辑,以限制用户未登录时可以执行的操作:他们只能查看游戏,修改必须经过授权的数据库。

To do so we first created two useful roues on our Node.js server to handle signup and login. Both returns a token that it's exchanged whenever the user wants to add or delete a game. To retrieve the token from HTTP requests we wrote a middleware.

为此,我们首先在Node.js服务器上创建了两个有用的路由来处理注册和登录。 当用户想要添加或删除游戏时,两者都返回交换令牌。 为了从HTTP请求中检索令牌,我们编写了一个中间件。

In the client-side we added redux-auth-wrapper which helps us to write high order components to wrap routes or components. By doing so we can show/hide buttons and protect views from unauthorized users.

在客户端,我们添加了redux-auth-wrapper ,它帮助我们编写高阶组件来包装路由或组件。 这样,我们可以显示/隐藏按钮并防止未经授权的用户查看。

进一步改进 ( Further Improvements )

It was definitely a long tutorial, divided in 3 parts, I hope you enjoyed it as I did while writing this fun app. However there are tons of optimization that can be done, first of all a general refactoring of the components: To name one, there may not be need separate components for signup and login views.

这绝对是一个漫长的教程,分为三个部分,希望您像我编写这个有趣的应用程序一样喜欢它。 但是,可以进行大量的优化工作,首先对组件进行一般的重构:举一个例子,注册和登录视图可能不需要单独的组件。

Here I summarizied a few suggestions:

在这里,我总结了一些建议:

  1. Writing constants and action-creators can become tedious (reducers included) so I suggest you to read the redux documentation to find strategies to reduce boilerplate.

    编写常量和动作创建者可能会变得乏味(包括缩减器),因此我建议您阅读redux 文档以找到减少样板的策略。
  2. The same can be said for Redux-saga, take a look at the advanced concepts and you should get some hints on how to refactor the sagas we wrote.

    对于Redux-saga来说也可以这么说,看看高级概念 ,您应该获得关于如何重构我们编写的sagas的一些提示。
  3. Finally, the webpack configuration to serve the bundle from Node.js is not optimized for production, which is why I set the NODE_ENV=build, not production. Also, Webpack 2 is at the time I wrote this tutorial very stable so I would suggest you all to start using it, the documentation to migrate from v1 to v2 is very complete.

    最后,用于Node.js捆绑软件的webpack配置并未针对生产进行优化,这就是为什么我设置NODE_ENV = build而非生产的原因。 另外,在我编写本教程时,Webpack 2非常稳定,因此建议大家开始使用它,从v1迁移到v2的文档非常完整。

翻译自: https://scotch.io/tutorials/build-a-retrogames-archive-with-node-js-react-redux-and-redux-saga-part3-authentication

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值