Archives Wild Code School
React - L'Odyssey d'Homer

090 - Redux Chili Peppers

Quête 10 sur 11 · 1 image archivée

On y est ! les Redux sont là !

Maintenant que tu peux t'authentifier auprès du serveur, tu vas devoir gérer la session coté client dans React. Pour cela tu vas devoir gérer ton token :

  • le garder dans ta session afin de naviguer dans ton application correctement
  • l'insérer dans l'entête de chacune de requête auprès du serveur

Objectifs

  • Avoir une application React qui communique avec le serveur de façon authentifié !

Récupération du JWT

Afin d'être synchro, dans le fichier SiginIn.jsx tu dois avoir une fonction de soumission de formulaire ressemblant à :

javascript
handleSubmit=(e)=> {
    e.preventDefault();  
    fetch("/auth/signin", {
        method:  'POST',
        headers:  new  Headers({
            'Content-Type':  'application/json'
        }),
        body:  JSON.stringify(this.state),
    })
    .then(res  => {
        if (res.ok)
            return  res.json()
        else
            throw  new  Error(res.statusText);
    })
    .then(res  =>  this.setState({ "flash":  res.message }))
    .catch(err  =>  this.setState({ "flash":  err.message }))
}

Vérifie et modifie ton backend pour que message soit bien remonté. Normalement tu dois avoir la snackbar qui te notifie si tu es authentifié ou non.

Si tu loggues le résultat du fetch dans ta console tu verras que l'objet obtenu du serveur ressemble à :

javascript
{
  "user": {
    "email": "homer@wildcodeschool.fr"
  },
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRvdG9AdGF0YS5mciIsImlhdCI6MTUyODQ0NDYxMX0.c0L_l0in5O1FNGRCgT-Gv6Rn6wX_bRBZTDk9VS_p5LE",
  "message": "User sign in!"
}

Le serveur retourne des informations sur l'utilisateur authentifié, un token JWT, et un message.

Cool, tu as récupéré le token !

↗
fetch
developer.mozilla.org

JWT et Redux

Le jwt est l'identifiant de l'utilisateur. Chacune des requêtes au serveur doit 'porter' - bearer - ce jeton pour que le serveur comprenne quel utilisateur est derrière celle-ci.

On comprend bien que l'information va être utiliser à plusieurs endroit dans l'application React : au niveau des accès de page, au niveau de la création des sessions, au niveau des générations des requêtes AJAX... Le token va devoir être stoker dans un store global à l'application. Ainsi l'information pourra être accessible de n'importe où.

C'est ici que Redux rentre en scène et ca va envoyer du lourd !

Redux est une librairie qui va permettre de gérer des états globaux de l'application qui vont être stockés dans un store. Nous allons parler de :

  • reducer : qui va, à partir de l'ancien état et de nouvelles données, établir un nouvel état
  • store : états issus de l'ensemble des reducers
  • container : composant React qui est connecté à Redux
  • actions : que nous verrons plus tard

Tout d'abord du doit installer Redux :

bash
npm install --save redux react-redux

On va rentrer dans le détail de Redux dans les prochaines étapes.

↗
Redux
redux.js.org

Redux - Mise en place de l'environnement

Avant d'aller plus loin, il est nécessaire de préparer l'environnement Redux. En l'occurence modifier l'arborescence du projet. Celle-ci est assez standard dans l'univers Redux... il est préférable de la respecter.

Crée les dossiers afin de suivre l'arborescence ci-dessous. On distingue les components des containers car tous les composants React ne seront pas connectés à Redux et donc ne seront pas des conteneurs. Dans notre cas les trois composants vont devenir des conteneurs par la suite donc tu peux déplacer les fichiers jsx dans containers comme indiqué. Attention il va falloir revoir les import pour que tout le monde se retrouve.

bash
/src
    /actions
    /components
    /containers
        SignIn.jsx
        SignUp.jsx
        Profile.jsx
    /reducers
    App.js
    App.css
    index.js
    index.css
↗
Redux
redux.js.org

La fonction Redux Connect où comment devenir container

Maintenant, reprend ton composant SignIn où tu récupères ton token. L'idée est de stocker le jeton dans Redux. Pour cela nous allons transformer le component en container !

Redux propose la fonction connect pour cela. Tout simplement on remplace :

javascript
export  default  SignIn;

par :

javascript
import { connect } from  'react-redux';
{...}
export  default  connect()(SignIn);

⇒ SignIn est maintenant connecté à Redux.

La fonction connect passe la fonction dispatch en tant que props du composant. dispatch (spatch !) permet de transmettre un JSON action aux reducers. action doit au moins intégrer une clé type qui représente le nom de l'action à considérer. Dans le composant SignIn, l'appel à dispatch ressemblera à :

javascript
this.props.dispatch(
    {
        type : "CREATE_SESSION",
        user: res.user,
        token : res.token,
        message : res.message
    }
)

La fonction doit être appelée à la reception du token. Je te laisse le mettre au bon endroit :).

↗
Redux
redux.js.org

Pas de reducer pas de Redux

Ton code n'est pas encore exploitable. On envoie aux reducers une action de type "CREATE_SESSION". Ok mais as-tu créé au moins un reducer ? Donc tu envoies une action à personne !

Tu vas créer ton premier reducer. Pour cela dans le dossier /reducers tu vas créer 2 fichiers :

bash
/reducers
    index.js
    authReducer.js

Le fichier index.js est le point d'entré des reducers. Il ressemble à :

javascript
import {combineReducers} from  'redux'
import  authReducer  from  './authReducer'

const  allReducers  =  combineReducers({
    auth:  authReducer,
});
export  default  allReducers;

On référence le reducer authReducer et on le stock dans l'état auth! Les états de ton application son dans allReducer et l'un des états est auth.

Le fichier authReducer.js est le reducer responsable de l'état auth. Il ressemble ici dans ton cas :

javascript
export  default  function (state  = {}, action) {
    switch (action.type) {
        case  "CREATE_SESSION":
            return { ...state, token:  action.token}
        default:
            return  state;
    }
}

Comme tu peux le voir un reducer prend le state précédent (qu'on peut initialiser comme on le souhaite state = {}) et l'action du dispatcher. De manière assez transparente, suivant le typede l'action, on produit un nouvel état ⇒ ici en définissant le token : { ...state, token: action.token}. Une fois le return exécuté, l'état auth sera mis à jour.

Attention de toujours mettre la condition default dans le switch afin de garantir que le reducer renvoie toujours un état !

↗
Redux - reducer
redux.js.org

Définition du store

Afin de proposer Redux à l'ensemble de l'application, il est nécessaire de configurer un provider dans l'index.js de React. Il faut réaliser les modifications comme suit :

javascript
import { createStore } from  'redux';
import { Provider } from  'react-redux';
{...}
const  store  =  createStore(allReducers)  
ReactDOM.render(
    <Provider  store={store}>
        <App  />
    </Provider>,
    document.getElementById('root')
);

Tu dois encapsuler ton App par un Provider auquel on doit spécifier un store. Le store est créé à partir des reducers récupérés de /reducers/index.js et grâce à la fonction createStore.

Voilà tu as ton store...

qui est mis à jour par les reducers qui sont mis à jour par les dispatch qui sont passer dans les props du composant à l'aide de connect qui transforme ainsi le composant en container

... qui est accessible au travers de ton application.

↗
Redux - store
redux.js.org

Mapper du state c'est du props

Le store est définit et lorsque tu réussis une authentification, ton état global stocké ressemble à :

json
{
    auth: {
        token: "le token"
    }
}

Pour notifier que l'utilisateur a bien été authentifié, tu vas devoir le lui notifier si le token devient non vide.

Il faut pour cela récupérer l'état global dans ton composant SignIn. Redux propose, à l'aide de Connect de passer l'état du store en props de ton composant. On passe généralement par une fonction mapStateToProps :

javascript
export  default  connect(mapStateToProps)(SignIn);

Ici connect passe le store (state) Redux à mapStateToProps. On peut donc implémenter la fonction comme ceci :

javascript
function  mapStateToProps(state) {
    return {
        flash:  state.auth.token,
    }
};

Tu peux ainsi passer le token stocké dans l'état auth du store Redux et le passer dans la props flash ⇒ this.props.flash.

Je te laisse afficher le token dans la snackbar :).

↗
Redux
redux.js.org

You shall not pass !

Tu arrives à d'identifier ? top !

Mais ce qui pourrait être encore pluuuus mieux serait :

  • d'être redirigé vers /signin si on n'est pas encore authentifié

  • d'être redirigé vers /profile si on a un token

Pour cela tu vas devoir implémenter des HOC - Higher Order Component. Un HOC est une fonction qui prend un composant en argument et le retourne (passe plat) si les conditions sont remplies sinon elle redirige l'utilisateur vers une autre route.

Attention Ne jamais oublier que le front n'est pas sécurisable ! L'utilisateur ayant potentiellement accès au code source, il peut hacker l'application et accéder à l'ensemble des pages. Seul le back est sécurisé. Par exemple, un hacker pourra accéder à /profile mais ne pourra pas récupérer les informations provenant du backend... la page restera donc vierge. Donc le blocage des routes n'a qu'une valeur d'ergonomie. Par exemple, cela ne sert à rien de donner libre accès à /profile si l'utilisateur n'est pas authentifier car il aura une page vierge.

Tout d'abord tu vas donc bloquer l'accès à /profile au personne non authentifiée. On encapsule le composant dans la Route avec une fonction comme ceci :

javascript
<Route  exact  path="/profile"  component={requireAuth(Profile)}  />

Tu vas implémenter un hoc au nom de requireAuth. Tu vas créer un dossier hoc/ et y mettre un fichier requireAuth.js.

Ce fichier exporte une fonction avec un composant en argument. Cette fonction renvoie un composant qui sera dans un premier temps celui en argument. Cela donne :

javascript
export  default  function (ComposedComponent) {
    return ComposedComponent;
}

Mais ce que l'on souhaite c'est que l'on retourne le composant ou bien que l'on soit redirigé vers /signin. Pour cela le principe est de créer un composant parent qui va gérer tout ça.

javascript
import  React, { Component } from  'react';
import { browserHistory } from  'react-router';

export  default  function (ComposedComponent) {
    class  Authentication  extends  Component {  
        componentWillMount() {
            if (!this.props.authenticated) 
                this.props.history.push('/signin');
        } 
        componentWillUpdate() {
            if (!this.props.authenticated) 
                this.props.history.push('/signin');
        }
        render() {
            return  <ComposedComponent  {...this.props}  />
        }
    }     
    return  Authentication;
}

Ici on crée un composant Authentification qui va rendre ComposedComponent et ses props. Cependant, avec la mise à jour du DOM on va vérifier que la personne est authentifiée. On regarde la nouvelle props authenticated, si elle est fausse alors on redirige via l'historique de notre routeur.

Mais d'où provient ce nextProps.authenticated ? De Redux bien sûr !

javascript
import  React, { Component } from  'react';
import { connect } from  'react-redux';
import { browserHistory } from  'react-router';

export  default  function (ComposedComponent) {
    class  Authentication  extends  Component {  
        componentWillMount() {
            if (!this.props.authenticated) 
                this.props.history.push('/signin');
        } 
        componentWillUpdate() {
            if (!this.props.authenticated) 
                this.props.history.push('/signin');
        }
        render() {
            return  <ComposedComponent  {...this.props}  />
        }
    }

    function  mapStateToProps(state) {
        return { authenticated:  state.auth.token?true:false };
    }

    return  connect(mapStateToProps)(Authentication);
}

Si tout se passe bien, maintenant sans token on est redirigé vers /signin.

↗
Redux
redux.js.org

Don't authenticate identified people dude !

Dans le même principe que pour l'hoc requireAuth, tu vas créer un hoc requireNotAuth qui va rediriger vers /profile un utilisateur authentifié qui souhaite accéder à /signin.

Je ne peux pas t'aider j'ai piscine. Bon courage

↗
Google
google.fr

Un redirect du gauche

Je te propose de mettre au bon endroit, dans SignIn la ligne de redirection suivante :

javascript
this.props.history.replace("/");

Et de corriger le Switch dans App :

javascript
<Switch>
    <Redirect  exact  from='/'  to='/profile'  />
    <Route  exact  path="/profile"  component={requireAuth(Profile)}  />
    <Route  exact  path="/signin"  component={requireNotAuth(SignIn)}  />
    <Route  exact  path="/signup"  component={requireNotAuth(SignUp)}  />
</Switch>
↗

La requête ça mâche !

Mais ton formulaire doit être vide, ou du moins avec les mauvaises données. Effectivement ta requête récupérant tes données ne peut pas aboutir car :

  • tu n'as toujours pas dit qui tu étais au serveur

  • le serveur attend une personne authentifiée

Il faut donc récupérer le token provenant du store ! Il y a du container là dessous... Je te laisse faire !

Il faut ensuite modifier la requête fetch afin d'y ajouter le token dans le hearder :

javascript
fetch('/profile', {
    headers: {
        'Authorization':  'Bearer '  +  this.props.token,
    }
})
.then(res  => {
    if (res.ok)
        return  res.json()
    else
        throw  new  Error(res.statusText)
})
.then(res  => {this.setState({ profile:  res })})

Je te laisse mettre à jour ta classe Profile.

Si tout se passe bien tu vas observer les données de l'utilisateur connecté dans la page.

Challenge

Chat lounge...

  • Réaliser cette quête avec du Red Hot dans les oreilles !

Critères de validation

  • Le token doit être stocké dans Redux
  • Une fois authentifié on doit être redirigé sur son profil
  • Le profil doit afficher les données de l'utilisateur authentifié