2. Ajouter un formulaire de connexion

Nous devons créer une nouvelle page de connexion, pour permettre aux utilisateurs autorisés d’accéder à notre application. Le formulaire de connexion présent sur cette page s’assurera de vérifier les identifiants saisis par l’utilisateur, et redirigera les « bons » utilisateurs sur la page avec la liste des pokémons.

Créez donc un nouveau fichier nommé login.tsx, dans le dossier pages. Je vous préviens, la quantité de code peut vous faire peur, pourtant il s’agit d’un simple formulaire de connexion avec deux champs username et password. Ne vous laissez pas impressionner donc, il n’y a pas de raisons de paniquer. Vous avez déjà le niveau pour comprendre tout ça ! 😉

import React, { FunctionComponent, useState } from 'react';
import { useHistory } from 'react-router-dom';
import AuthenticationService from '../services/authentication-service';

type Field = {
  value?: any,
  error?: string,
  isValid?: boolean
};

type Form = {
  username: Field,
  password: Field
}

const Login: FunctionComponent = () => {

  const history = useHistory();

  const [form, setForm] = useState<Form>({
    username: { value: '' },
    password: { value: '' },
  });

  const [message, setMessage] = useState<string>('Vous êtes déconnecté. (pikachu / pikachu)');

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
    const fieldName: string = e.target.name;
    const fieldValue: string = e.target.value;
    const newField: Field = { [fieldName]: { value: fieldValue } };

    setForm({ ...form, ...newField});
  }

  const validateForm = () => {
    let newForm: Form = form;

    // Validator username
    if(form.username.value.length < 3) {
      const errorMsg: string = 'Votre prénom doit faire au moins 3 caractères de long.';
      const newField: Field = { value: form.username.value, error: errorMsg, isValid: false };
      newForm = { ...newForm, ...{ username: newField } };
    } else {
      const newField: Field = { value: form.username.value, error: '', isValid: true };
      newForm = { ...newForm, ...{ username: newField } };
    }

    // Validator password
    if(form.password.value.length < 6) {
      const errorMsg: string = 'Votre mot de passe doit faire au moins 6 caractères de long.';
      const newField: Field = {value: form.password.value, error: errorMsg, isValid: false};
      newForm = { ...newForm, ...{ password: newField } };
    } else {
      const newField: Field = { value: form.password.value, error: '', isValid: true };
      newForm = { ...newForm, ...{ password: newField } };
    }

    setForm(newForm);

    return newForm.username.isValid && newForm.password.isValid;
  }

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const isFormValid = validateForm();
    if(isFormValid) {
      setMessage('👉 Tentative de connexion en cours ...');
      AuthenticationService.login(form.username.value, form.password.value).then(isAuthenticated => {
        if(!isAuthenticated) {
          setMessage('🔐 Identifiant ou mot de passe incorrect.');
          return;
        }
        
        history.push('/pokemons');
        
      });
    }
  }

  return (
    <form onSubmit={(e) => handleSubmit(e)}>
      <div className="row">
        <div className="col s12 m8 offset-m2">
          <div className="card hoverable">
            <div className="card-stacked">
              <div className="card-content">
                {/* Form message */}
                {message && <div className="form-group">
                  <div className="card-panel grey lighten-5">
                    {message}
                  </div>
                </div>}
                {/* Field username */}
                <div className="form-group">
                  <label htmlFor="username">Identifiant</label>
                  <input id="username" type="text" name="username" className="form-control" value={form.username.value} onChange={e => handleInputChange(e)}></input>
                  {/* error */}
                  {form.username.error &&
                  <div className="card-panel red accent-1"> 
                   {form.username.error} 
                  </div>} 
                </div>
                {/* Field password */}
                <div className="form-group">
                  <label htmlFor="password">Mot de passe</label>
                  <input id="password" type="password" name="password" className="form-control" value={form.password.value} onChange={e => handleInputChange(e)}></input>
                  {/* error */}
                  {form.password.error &&
                  <div className="card-panel red accent-1"> 
                   {form.password.error} 
                  </div>} 
                </div>
              </div>
              <div className="card-action center">
                {/* Submit button */}
                <button type="submit" className="btn">Valider</button>
              </div>
            </div>
          </div>
        </div>
      </div>
    </form>
  );
};
 
export default Login;

Bien que le code de ce composant semble interminable, je vous assure qu’il s’agit simplement d’une page affichant un formulaire de connexion. Si vous prenez le temps de regarder ce code, vous verrez qu’il n’y pas de concepts nouveaux et que vous êtes déjà capable de comprendre ce code. Cependant, je vous donne tout de même quelques explications pour vous présenter comment je vois les choses :

  • De la ligne 20 à 23, on définit un état pour notre formulaire, comme nous l’avions fait précédemment pour le formulaire des pokémons.
  • À la ligne 25, on sauvegarde dans un état un message d’information global pour le formulaire. Par exemple: « Tentative de connexion en cours », « Identifiant ou mot de passe incorrects », etc.
  • À la ligne 35, on définit une méthode pour valider les champs de notre formulaire. Pour le champ username, on vérifie simplement qu’il fait plus de 3 caractère de long, à la ligne 39. Et pour le mot de passe password, on vérifie qu’il fait au moins 6 caractères de long, à la ligne 49.
  • À la ligne 63, on gère la soumission de notre formulaire. Tout d’abord à la ligne 67, on affiche un message du type « Authentification en cours », car le délai que nous avons définis précédemment pour l’authentification d’un utilisateur est de 1000 millisecondes. Ensuite, à la ligne 69, on gère le cas ou les identifiants de l’utilisateurs sont incorrects, et on affiche le message d’erreur correspondant « Identifiant ou mot de passe incorrect ». Puis à la ligne 71, on gère le cas où l’utilisateur s’est connecté correctement, et on le redirige vers la liste de tous les pokémons, comme tout s’est bien passé.
  • Enfin, même si le template de composant est classique pour un formulaire, vous remarquez qu’on affiche notre message d’information du formulaire de la ligne 88 à 92, afin de prévenir l’utilisateur des traitements en cours.

Après s’être authentifié correctement, l’utilisateur sera redirigé vers la liste des pokémons. C’est parfait ! 👍

Pour terminer, il faut bien sûr ajouter notre nouvelle page de connexion login auprès de notre routeur React, pour qu’il soit au courant de cette nouvelle route. Ajoutez donc notre nouvelle route de connexion dans le composant racine App.tsx :

// Les autres importations.
import Login from './pages/login';
 
const App: FunctionComponent = () => {
 
  return (
    <Router>
      ...
      <Switch>
        <Route exact path="/" component={PokemonsList} />
        <Route exact path="/login" component={Login} />
        <Route exact path="/pokemons" component={PokemonsList} />
        <Route exact path="/pokemon/add" component={PokemonAdd} />
        <Route exact path="/pokemons/edit/:id" component={PokemonEdit} />
        <Route path="/pokemons/:id" component={PokemonsDetail} />
        <Route component={PageNotFound} />
      </Switch>
      </div>
    </Router>
  );
}
 
export default App;

Maintenant, essayer d’accéder à votre application.

Vous serez alors redirigé vers notre liste de pokémons, au lieu du formulaire de connexion … 🤔

En fait, c’est normal, car par défaut notre liste de pokémons n’est pas protégée. Donc que l’utilisateur soit connecté ou non ne change rien au fonctionnement de notre application. Pour accéder à notre formulaire de connexion, il faut saisir manuellement la route « /login » dans votre navigateur. Ce n’est pas vraiment ce que nous voulons.

Pour résumé, on a un service d’authentification fonctionnel, un formulaire de connexion qui tourne plutôt bien, mais… les autres routes de notre application ne sont pas encore protégées. Encore un peut de boulot donc !