#CestFacile : React Router v4 – Protected routes

Mon précédent post proposait de mettre en place un layout commun à l’essentiel de nos routes. Bien entendu il est toujours possible d’avoir des routes qui sortent de ce layout pour afficher, par exemple, un formulaire d’authentification ou une page spécifique.

Ici, on va s’intéresser à un autre type de routes, les routes que l’on souhaite « protéger » et qui nécessitent, par exemple, d’être inscrit pour pouvoir y accéder. On peut imaginer le même type de route qui ne seraient accessibles que pour certains groupes d’utilisateurs, ou pour des utilisateurs ayant remplis les premières étapes d’un formulaire. La logique est sensiblement la même.

Let’s go

L’approche naïve

L’idée ici, c’est de faire en sorte que lorsqu’un utilisateur non autorisé chercher à accéder à une url, il soit redirigé vers une autre page.

On pourrait contrôler chaque container accessible dans le router, et ça fonctionnerait sans doute tout aussi bien, mais dès que vous aurez plusieurs pages, voir plusieurs dizaines de pages nécessitant un contrôle d’accès, vous risquez vite de déchanter.

On va quand même voir à quoi ressemble un contrôle d’accès au niveau des containers. On refactorisera ensuite pour obtenir quelque chose qui tienne la route et qui soit future-proof (oui, aujourd’hui je me la pète avec les termes).

Fondamentalement, on a besoin de :

  • Un système d’authentification qui permette de savoir si l’utilisateur est loggé ou non
  • Eventuellement de stocker un token JWT en localStorage même si on ne l’utilisera pas pour le moment
  • D’une nouvelle Saga et d’un nouveau reducer qui vont gérer la partie auth de notre app
  • De <Redirect />, de chez React Router Dom, qui va se charger de rediriger nos utilisateurs d’une page à une autre selon qu’ils soient loggés ou non.

Vous pouvez choper ça sur GitHub :

https://github.com/GregoryBabonaux/react-c-est-facile/tree/805f80a8518244e6e6b3d18c10a96c67d4e8bc31

Tout d’abord on créé notre reducer, nos actions et nos actionTypes dans /Services/Auth.

Rien de particulier si ce n’est que l’on va également modifier notre initialState :

const initialState = {
 stuff: [],
 auth: {
   loading: false,
   logged: false,
 }
}
export default initialState

loading pour placer un spinner quand on tente de se logger, logged pour identifier si l’utilisateur est loggé. Rien de fou fou.

Notre Saga simulera un appel à une API avec un timeout de 3 secondes, et renverra un token qu’on stockera en localStorage. On ne l’utilisera pas, c’est juste pour illustrer le fonctionnement.

import { call, put, takeEvery } from 'redux-saga/effects'
import {loggedIn} from './actions'

const someAPICalls = {
 tryLogin(payload){
   return new Promise( (resolve, reject) => {
     setTimeout(resolve, 3000, 'faketokenforfun')
   })
 },
 setToken(token){
   return localStorage.setItem('token', token)
 }
}

function* authenticate(action){
 const token = yield call(someAPICalls.tryLogin, action.payload)
 yield call(someAPICalls.setToken, token)
 yield put(loggedIn())
}

function* watchAuth(){
 yield takeEvery('LOGIN', authenticate)
}

export default watchAuth;

Rien de fou fou non plus ici. On dispatch une action logged quand le processus est terminé, ce qui va permettre d’identifier que l’utilisateur est loggé.

On a plus qu’à checker ensuite si logged est à true dans nos containers, et agir en conséquence :

Login :

See the Pen ddQdjV by daibai (@Daibai) on CodePen.

On check simplement si logged est true.

Si oui, on redirige vers notre home (App).

Si non, on affiche notre form de login qui dispatch une action au submit.

App :

 

See the Pen gvQvdj by daibai (@Daibai) on CodePen.

On fait le contraire, si l’utilisateur n’est pas loggé, on le redirige vers /login

On se posera rapidement la question de savoir si toute cette logique doit être gérée dans nos containers.

La réponse est bien évidemment non, et on aurait tout intérêt à identifier en amont les routes qui nécessitent d’être loggé.

Ca tombe bien, c’est ce qu’on va faire tout de suite.

L’approche plus pérenne

Avant tout, remember :

On a créé un layout en utilisant le principe de render props associé à React Router. On veut conserver ce principe en ajoutant une dimension supplémentaire : un check sur la possibilité d’accéder ou pas à une route.

Fondamentalement, on va juste aller plus loin avec les render props :

  • On va render ProtectedRoutes qui va lui même render notre Layout qui va render le composant que l’on souhaite afficher.
  • ProtectedRoutes va contenir la logique associée au check de l’authentification
  • Layout va ajouter notre… layout
import React from 'react'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import * as stuffActions from '../../Services/Stuff/actions'
import {Redirect} from 'react-router-dom'
import Layout from '../Layout/Layout'

const ProtectedRoutes = ({component: Component, ...rest}) => {
 const {path} = rest;
 const {logged} = rest.auth

if( !logged ){
  return <Redirect to="/login" />
}

return (
   <Layout exact path={path} component={Component} />
 )
}

const mapStateToProps = ({auth}) => ({
 auth
})

export default connect(mapStateToProps)(ProtectedRoutes);

Dans /Components/ProtectedRoutes/ProtectedRoutes.js

Plutôt simple, notre composant s’occupe de vérifier si l’user est loggé et redirige si ce n’est pas le cas.

Dans nos Routes, on aura alors :

import React from 'react';
import { BrowserRouter, Switch } from 'react-router-dom';
import App from './Scenes/App/App';
import Login from './Scenes/Login/Login';
import ProtectedRoutes from './Components/ProtectedRoutes/ProtectedRoutes'
import Layout from './Components/Layout/Layout'

export default () => {
 return (
   <BrowserRouter>
     <Switch>
       <ProtectedRoutes exact path='/' component={App}/>
       <Layout exact path='/login' component={Login}/>
     </Switch>
   </BrowserRouter>
 )
}

Dans ./Routes.js

On passe les mêmes props, avec le component que l’on veut afficher en bout de course. On a juste ajouté une nouvelle dimension.

Ca nous permet du même coup de refacto un peu <App> en retirant le check sur logged.

Quid des « non protected routes »

On pourrait se demander si ça ne vaut le coup de créer des NonProtectedRoutes. Après tout, un utilisateur loggé ne devrait ni pouvoir accéder au formulaire d’authentification, ni au formulaire d’inscription, ni à la récupération de mot de passe.

Mais par contre un utilisateur loggé devrait quand même pouvoir continuer à accéder à des routes non protégées, par exemple nos mentions légales, la liste des posts de notre blog…

On ne va pas parler de « non protected routes » puisque, fondamentalement, elles existent déjà. Il est très tôt (merci la planification de posts WordPress !), je suis pas spécialement inspiré, alors on a qu’à appeler ça « AuthenticationRoutes », mais libre à vous de trouver une nomenclature qui vous convient.

Devinez quoi ?

Ca reprend le même concept que nos ProtectedRoutes, si ce n’est que cette fois, l’utilisateur ne doit pas être loggé pour pouvoir accéder à ces pages. C’est précisément ce qu’on a fait dans notre approche naïve, plus haut.

import React from 'react'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import * as stuffActions from '../../Services/Stuff/actions'
import {Redirect} from 'react-router-dom'
import Layout from '../Layout/Layout'

const ProtectedRoutes = ({component: Component, ...rest}) => {
 const {path} = rest;
 const {logged} = rest.auth

if( logged ){
 return <Redirect to="/" />
}

return (
 <Layout exact path={path} component={Component} />
 )
}

const mapStateToProps = ({auth}) => ({
 auth
})

export default connect(mapStateToProps)(ProtectedRoutes);

Dans ./Components/AuthenticationRoutes/AuthenticationRoutes

Un petit update de Routes :

import React from 'react';
import { BrowserRouter, Switch } from 'react-router-dom';
import App from './Scenes/App/App';
import Login from './Scenes/Login/Login';

import ProtectedRoutes from './Components/ProtectedRoutes/ProtectedRoutes'
import AuthenticationRoutes from './Components/AuthenticationRoutes/AuthenticationRoutes'
import Layout from './Components/Layout/Layout'

export default () => {
 return (
   <BrowserRouter>
     <Switch>
       <ProtectedRoutes exact path='/' component={App}/>
       <AuthenticationRoutes exact path='/login' component={Login}/>
     </Switch>
   </BrowserRouter>
 )
}

Dans ./Routes.js

Vous retrouverez le code ici :

https://github.com/GregoryBabonaux/react-c-est-facile/tree/fd90ed20fec908dcccf97956f74d2d0d8a3b6853

A partir de là on peut faire à peu près ce qu’on veut et gérer les edge cases comme ils se présentent en terme de condition d’accès au contenu.

Et rien n’empêche d’effectuer des contrôles d’accès supplémentaires dans nos composants.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *