TP - création de comptes

Désormais nous savons nous connecter et nous déconnecter grâce à notre page de login, mais il serait également intéressant de pouvoir créer des comptes.

A la fin de ce travail pratique se présentera obtenir la page suivante :

_images/18.png

En réduisant la largeur de la fenêtre de votre navigateur, la version mobile s’affiche :

_images/26.png

Ce formulaire est interactif. Si l’utilisateur entre du texte dans un champ, le texte est évalué et la page change en conséquence.

Si un texte non valide est entré, un message d’erreur s’affiche et la zone de texte devient rouge :

_images/43.png

A l’inverse, lorsque les données sont correctes, la zone de texte devient verte et le message d’erreur disparaît :

_images/52.png

Si l’utilisateur appuie sur le bouton Créer un compte et que tous les champs sont en vert, l’application vérifie dans la base de données si le nom d’utilisateur existe déjà. Si c’est le cas, un message d’erreur s’affiche alors sur la page.

_images/62.png

Sinon, le compte est créé et enregistré dans la base de données, et l’utilisateur est directement connecté. Il est alors redirigé vers la page d’accueil.

Pistes

  • Les champs sont évalués instantanément (lorsque l’utilisateur entre du texte) sur la page web. Ainsi, de nombreux aller-retour entre le client et le serveur sont évités.
  • Le texte entré est vérifié au moyen d’expressions régulières
  • Le fichier login.js doit être également appelé dans cette page, car nous aurons besoin de la fonction verifier_enter() et de la gestion de l’événement socket redirection.
  • Lors de la création d’un nouveau compte, un nombre qui se trouve entre 1 et 100 est choisi au hasard et intégré dans la colonne avatar de la table users (lorsque nous élaborerons la page de profil, nous aurons à notre disposition 100 images d’avatar)

Bon travail !

Correction

Code côté serveur

Cette fois-ci, le code côté serveur est relativement court. Comme d’habitude, il faut créer une route pour la page nouveau_compte :

./serveur/routes.js
app.get('/nouveau_compte', function(req, res) {
  res.render('nouveau_compte.ejs', {
    nom_utilisateur: session.session_active(req.session, req)
  });
});

Il reste encore la gestion de l’événement socket nouveau_compte dans socket.js:

./serveur/socket.js
socket.on('nouveau_compte', function(compte) {
  var requete_sql = 'SELECT'+
    // nous utilisons la fonction SQL COUNT()
    // pour déterminer si l'utilisateur existe
    +'COUNT(nom_utilisateur) AS "user_exists"\
    FROM users WHERE nom_utilisateur = ??';
  var inserts = [compte.nom_utilisateur];
  requete_sql = sql.preparer(mysql, requete_sql, inserts);
  sql.requete(mysql, sql, requete_sql, function(result) {
    if (result[0].user_exists !== 0) {
      socket.emit('utilisateur_existant');
    } else {
      var requete_sql = '\
        INSERT INTO users(nom_utilisateur, \
        mot_de_passe, email, nom, prenom, avatar) \
        VALUES(??, ??, ??, ??, ??, ?)';
      var inserts = [
        compte.nom_utilisateur,
        compte.password,
        compte.mail,
        compte.nom,
        compte.prenom,
        // nombre au hasard entre 1 et 100 pour l'avatar
        Math.floor((Math.random() * 100) + 1)
      ];
      requete_sql = sql.preparer(mysql, requete_sql, inserts);
      sql.requete(mysql, sql, requete_sql);
      session.creation_jeton(mysql, sql, base64url,
        crypto, socket, compte.nom_utilisateur);
    }
  });
});

Code côté client

HTML et EJS

Commençons par la page principale, nouveau_compte.ejs, qui contient le squelette de notre page :

./views/nouveau_compte.ejs
<!DOCTYPE html>
<html>
  <head>
    <% include balise_head %>
    <!-- fonctions verifier_enter() et événement socket "redirection" -->
    <script src="static/js/login.js"></script>
    <!-- fonction nouveau_compte() et événement socket "utilisateur_existant" -->
    <script src="static/js/nouveau_compte.js"></script>
    <!-- fonction verifier() qui vérifie les données du formulaire et affiche
    les messages d'erreurs si nécessaire -->
    <script src="static/js/verification_form.js"></script>
  </head>
  <body>
    <% include barre_navigation %>
    <div class="container">
      <div class="row">
        <div class="col-lg-offset-2 col-md-offset-1 col-md-10 col-lg-8 blanc">
        <center class="alert alert-danger" id="utilisateur_existant"
        style="display: none;">
          Ce nom d'utilisateur existe déjà. Veuillez en choisir un autre.
        </center>
        <form class="form-horizontal" style="padding-right:20px;">
          <div class="form-group">
            <h4 class="gros_titre">Créer un compte</h4>
          </div>
          <% include form_nouveau_compte %>
          <div class="form-group">
            <span class="pull-right btn btn-primary btn-nouveau_compte"
              onclick="nouveau_compte();">
                Créer un compte
            </span>
          </div>
        </form>
        <div class="col-lg-offset-2 col-md-offset-1"></div>
      </div>
    </div>
  </body>
</html>

Comme vous le constatez, le formulaire est ici séparé dans un autre fichier EJS du nom de form_nouveau_compte.ejs. Voici le contenu de ce fichier :

./views/form_nouveau_compte.ejs
<div class="row">
  <div class="form-group" id="form_nc_utilisateur">
    <label for="nc_utilisateur" class="col-lg-3 col-sm-3 control-label">
      Nom d'utilisateur
    </label>
    <div class="col-lg-9 col-sm-9">
      <input type="text" class="form-control" id="nc_utilisateur"
      onkeyup="verifier('utilisateur');">
      <span class="help-block message_erreur" id="utilisateur_incorrect">
        Le nom d'utilisateur doit contenir au minimum 4 caractères,
        au maximum 10 caractères, et peut être uniquement composé de chiffres
        et de lettres non-accentuées.
      </span>
    </div>
  </div>
</div>
<div class="row desktop">
  <div class="form-group">
    <div id="form_nc_prenom">
     <label for="nc_prenom" class="col-sm-3 control-label">Prénom</label>
     <div class="col-lg-4 col-sm-4">
       <input type="text" class="form-control" id="nc_prenom"
       onkeyup="verifier('prenom');">
       <span class="help-block message_erreur" id="prenom_incorrect">
          Ce prénom est invalide.
       </span>
     </div>
    </div>
    <div id="form_nc_nom">
      <label for="nc_nom" class="col-sm-1 control-label">Nom</label>
      <div class="col-lg-4 col-sm-4">
        <input type="text" class="form-control" id="nc_nom"
        onkeyup="verifier('nom');">
        <span class="help-block message_erreur" id="nom_incorrect">
          Ce nom est invalide.
        </span>
      </div>
    </div>
  </div>
</div>
<div class="row mobile">
  <div class="form-group" id="form_nc_prenom_mobile">
    <label for="nc_prenom_mobile" class="col-sm-4 control-label">Prénom</label>
    <div class="col-lg-8 col-sm-8">
      <input type="text" class="form-control" id="nc_prenom_mobile"
      onkeyup="verifier('prenom_mobile');">
      <span class="help-block message_erreur" id="prenom_mobile_incorrect">
        Ce prénom est invalide.
      </span>
    </div>
  </div>
</div>
<div class="row mobile">
  <div class="form-group" id="form_nc_nom_mobile">
    <label for="nc_nom_mobile" class="col-sm-4 control-label">Nom</label>
    <div class="col-lg-8 col-sm-8">
      <input type="text" class="form-control" id="nc_nom_mobile"
      onkeyup="verifier('nom_mobile');">
      <span class="help-block message_erreur" id="nom_mobile_incorrect">
        Ce nom est invalide.
      </span>
    </div>
  </div>
</div>
<div class="row">
  <div class="form-group" id="form_nc_mail">
    <label for="nc_mail" class="col-lg-2 col-sm-2 control-label">Mail</label>
    <div class="col-lg-10 col-sm-10">
      <input type="text" class="form-control" id="nc_mail"
      onkeyup="verifier('mail');">
      <span class="help-block message_erreur" id="mail_incorrect">
        Cette adresse e-mail est invalide.
      </span>
    </div>
  </div>
</div>
<div class="row">
  <div class="form-group" id="form_nc_password">
    <label for="nc_password" class="col-lg-3 col-sm-3 control-label">
      Mot de passe
    </label>
    <div class="col-lg-9 col-sm-9">
      <input type="password" class="form-control" id="nc_password"
      onkeypress="verifier_enter(event, this, nouveau_compte);"
      onkeyup="verifier('password');">
      <span class="help-block message_erreur" id="password_incorrect">
        Le mot de passe doit contenir au minimum 4 caractères, au maximum
        20 caractères, et peut être uniquement composé de chiffres et de lettres.
      </span>
    </div>
  </div>
</div>

Au premier abord, ce code peut paraître complexe, mais en réalité, les différents éléments de cette page sont presque identiques. Pour mieux comprendre le fonctionnement général de ce code, voici quelques explications :

  • Les balises dont l’attribut id commence par form_nc_ servent à être reconnues pour colorier les balises <input> en rouge ou en vert selon le texte tapé dans les balises <input>
  • Les balises <input> ont un attribut id qui commence par nc_, qui permet ensuite de récupérer facilement le texte que l’utilisateur a écrit
  • Ces mêmes balises ont un attribut onkeyup, assigné à la fonction verifier() qui permet de lancer la vérification du texte chaque fois que l’utilisateur tape une lettre sur son clavier
  • Chaque message d’erreur possède un attribut id qui se termine par _incorrect. Il pourra être identifié pour que son attribut style soit modifié par du code javascript afin de l’afficher ou le cacher
  • Les entrées nom et prénom sont écrites à double, une fois pour les appareils mobiles et une fois pour les ordinateurs. Comme il n’est pas possible d’attribuer deux id similaires dans une page HTML, les identifiants des balises concernant les appareils mobiles se terminent par _mobile
  • Pour éviter d’afficher deux fois les champs nom et prénom, ceux-ci sont pourvus d’une nouvelle classe, desktop ou mobile, qui servira à afficher ces balises seulement sur les appareils concernés.

Feuilles de style

Pour améliorer l’apparence du formulaire, nous allons modifier le fichier style.css

./static/style.css
.help-block {
  position: relative;
    top: -8px;
}
.message_erreur {
  position: relative;
    top: 2px;
  margin-bottom: -15px;
  /* le message d'erreur ne s'affiche pas au chargement de la page */
  display: none;
}

Il nous faut également une media query pour que la classe mobile s’affiche seulement sur les fenêtres d’une petite largeur et que la classe desktop sur les fenêtres d’une plus grande largeur. Comme ceci concerne directement le responsive design, nous écrivons dans responsive.css :

./static/responsive.css
@media(max-width: 767px) {
  .desktop {
    display: none;
  }
}
@media(min-width: 768px) {
  .mobile {
    display: none;
  }
}

Javascript

Nous arrivons maintenant à la partie importante du côté client : la gestion du formulaire interactif. Les fonctions utilisées pour ce formulaires étant uniques, nous les mettons dans un fichier séparé, nouveau_compte.js :

./static/js/nouveau_compte.js
function get_regex(data) {
  switch(data) {
    case 'utilisateur':
      return /^[a-z0-9]{4,10}$/i;
    case 'password':
      return /^.{4,20}$/i;
    case 'prenom': case 'prenom_mobile': case 'nom': case 'nom_mobile':
      return /^[a-zâäàéèùêëîïôöçñ\ ]{2,50}$/i;
    case 'mail':
      return /^[a-z0-9\.-_]+\@[a-z0-9\.-_]+\.[a-z]{2,10}$/i;
  }
}

function get_html_class(data, type) {
  if (data === 'prenom' || data === 'nom') {
    switch(type) {
      case 'success':
        return 'has-success';
      case 'nothing':
        return '';
      case 'error':
        return 'has-error';
    }
  } else {
    switch(type) {
      case 'success':
        return 'form-group has-success';
      case 'nothing':
        return 'form-group';
      case 'error':
        return 'form-group has-error';
    }
  }
}

function verifier(data, stop) {
  var return_value;
  if (get_regex(data).test(byId('nc_'+data).value)) {
    byId('form_nc_'+data).className = get_html_class(data, 'success');
    byId(data+'_incorrect').style.display = 'none';
    return_value = true;
  } else if (byId('nc_'+data).value === '') {
    byId('form_nc_'+data).className = get_html_class(data, 'nothing');
    byId(data+'_incorrect').style.display = 'none';
    return_value = false;
  } else {
    byId('form_nc_'+data).className = get_html_class(data, 'error');
    byId(data+'_incorrect').style.display = 'inline';
    return_value = false;
  }
  if ((data === 'prenom' || data == 'nom') && !stop) {
    byId('nc_'+data+'_mobile').value = byId('nc_'+data).value;
    verifier(data+'_mobile', true);
  }
  if ((data === 'prenom_mobile' || data == 'nom_mobile') && !stop) {
    byId('nc_'+data.replace(/_mobile/, '')).value = byId('nc_'+data).value;
    verifier(data.replace(/_mobile/, ''), true);
  }
  return return_value;
}

Comme vu précédemment, les balises HTML écrites ont toutes des identifiants. Ainsi, il est plus aisé de paramétrer notre formulaire. C’est pourquoi la fonction verifier() peut être appliquée à toutes les balises. Cela évite des redondances, puisque nous n’avons pas à vérifier chaque élément du formulaire individuellement.

Gestion des noms et prénoms

Les balises <input> pour les prénoms et les noms ont deux versions : une mobile et une desktop, qui dépendent de la taille de la fenêtre. Pour éviter des problèmes, la fonction verifier() s’occupe d’actualiser les champs à chaque fois qu’on l’appelle, donc à chaque fois que l’utilisateur entre une lettre avec son clavier grâce aux instructions suivantes :

// lorsque le texte est tapé dans la classe "desktop"
byId('nc_'+data+'_mobile').value = byId('nc_'+data).value;

// lorsque le texte est tapé dans la classe "mobile"
byId('nc_'+data.replace(/_mobile/, '')).value = byId('nc_'+data).value;

Une fois le texte actualisé dans les deux balises <input>, nous vérifions qu’il se trouve dans la zone de texte invisible. C’est pourquoi nous appelons à nouveau la fonction verifier() une fois le texte actualisé. Cependant, un appel successif de fonctions dans elles-mêmes crée une récursion. C’est pour cela que la fonction verifier() a besoin d’un autre argument, stop. Lorsque nous appelons cette fonction alors que nous sommes déjà en train d’exécuter son code, ce paramètre prend la valeur true. Ainsi, même si les conditions data === 'prenom_mobile' ou data == 'nom_mobile' sont validées, la condition !stop stoppe l’exécution de la fonction, et ainsi, la récursion n’a pas lieu.

Astuce

L’argument stop n’a pas de valeur par défaut. Et pourtant, lorsqu’il est testé dans les conditions de verifier(), il ne crée pas d’erreur. En effet, !stop peut vouloir dire stop === false, mais aussi typeof(stop) === 'undefined'. Et comme l’argument stop est dans une fonction, il est déclaré (l’équivalent de var stop;) même si il n’a aucune valeur. Ainsi, la fonction peut s’exécuter sans encombres.

Avertissement

Si vous n’êtes pas dans une fonction, vous ne pouvez pas utiliser cette technique, car une variable inexistante ne serait pas déclarée.

Il reste encore un souci à régler pour le formulaire côté desktop. En effet, les entrées nom et prénom se trouvent sur la même ligne, ce qui implique qu’elles soient dans une balise de classe form-group. Or, lors de l’ajout d’une classe has-success ou has-error à cette balise, form-group serait supprimé si nous ne traitions pas les cas nom et prénom séparément. C’est ce à quoi sert la fonction get_html_class(), qui s’occupe de retourner les classes HTML appropriées dans les conditions success (si le texte est correct), nothing (s’il n’y a pas de texte) et error (si le texte est incorrect).

Expressions régulières

La fonction get_regex() permet d’obtenir l’expression régulière en fonction du type de donnée entré. Pour retourner facilement l’expression régulière, elle utilise une condition switch.

Note

Normalement, les instructions des conditions switch se terminent par break;. Mais, dans ce cas, les instructions qui s’y trouvent permettent toutes de terminer la fonction et de renvoyer une valeur avec le mot return. Par conséquent, l’instruction break; est inutile, puisqu’elle ne sera de toute façon pas exécutée.

Pour être certain que vous comprenez toutes les expressions régulières de cette fonction, analysons-les rapidement :

  • le nom d’utilisateur doit contenir de quatre à dix ({4,10}) caractères alphanumériques ([a-z0-9])
  • le mot de passe peut contenir n’importe quel caractère (.) répété de quatre à vingt fois ({4,20})
  • le prénom et le nom sont composés de n’importe quelle lettre et d’espaces (dans le cas où un utilisateur a plusieurs noms), et doivent avoir entre deux et cinquante caractères {2,50}
  • l’adresse e-mail doit être composée de caractères alphanumériques, de tirets et de points ([a-z0-9\.-_]+), suivi d’un arobase, puis d’un nom de domaine qui comporte aussi des caractères alphanumériques, des tirets et des points ([a-z0-9\.-_]+), puis d’un point et d’une extension (.com, .net, etc.).
Evénement socket

A présent, le formulaire est interactif, mais les données du formulaire ne sont pas enregistrées dans la base de données. Comme nous avons un fichier dédié à la vérification d’un nouveau formulaire, nous allons créer un nouveau fichier, nouveau_compte.js qui gérera l’envoi des données du formlaire avec socket.io :

./static/js/nouveau_compte.js
function nouveau_compte() {
  if (verifier('utilisateur') &&
    verifier('prenom') &&
    verifier('nom') &&
    verifier('mail') &&
    verifier('password'))
  {

    socket.emit('nouveau_compte', {
      nom_utilisateur : byId('nc_utilisateur').value,
      password : byId('nc_password').value,
      prenom : byId('nc_prenom').value,
      nom : byId('nc_nom').value,
      mail : byId('nc_mail').value
    });
  }
}

Note

Si vous vous demandiez à quoi servaient les return true; et les return false; dans la fonction verifier(), vous avez maintenant votre réponse. Ils permettent d’éviter l’envoi de requêtes socket.io si le contenu des champs du formulaire ne correspond pas aux expressions régulières.

Dans ce fichier, nous ajouterons également la gestion de l’événement utilisateur_existant :

./static/js/nouveau_compte.js
// affiche un message d'erreur si l'utilisateur existe
socket.on('utilisateur_existant', function() {
  byId('utilisateur_existant').style.display = 'block';
});

Et voilà, vous pouvez maintenant créer des comptes d’utilisateur dans votre application avec un formulaire responsive et interactif !