##### 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 : .. image:: tp3/1.png En réduisant la largeur de la fenêtre de votre navigateur, la version mobile s'affiche : .. image:: tp3/2.png :height: 10000 px :width: 600 px :scale: 55 % 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 : .. image:: tp3/4.png A l'inverse, lorsque les données sont correctes, la zone de texte devient verte et le message d'erreur disparaît : .. image:: tp3/5.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. .. image:: tp3/6.png :height: 10000 px :width: 700 px :scale: 55 % 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`` : .. captionup:: ./serveur/routes.js .. code-block:: javascript 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``: .. captionup:: ./serveur/socket.js .. code-block:: javascript 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 : .. captionup:: ./views/nouveau_compte.ejs .. code-block:: guess <% include balise_head %> <% include barre_navigation %>

Créer un compte

<% include form_nouveau_compte %>
Créer un compte
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 : .. captionup:: ./views/form_nouveau_compte.ejs .. code-block:: guess
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.
Ce prénom est invalide.
Ce nom est invalide.
Ce prénom est invalide.
Ce nom est invalide.
Cette adresse e-mail est invalide.
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.
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 ```` en rouge ou en vert selon le texte tapé dans les balises ```` - Les balises ```` 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`` .. captionup:: ./static/style.css .. code-block:: 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`` : .. captionup:: ./static/responsive.css .. code-block:: 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`` : .. captionup:: ./static/js/nouveau_compte.js .. code-block:: javascript 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 ```` 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 : .. code-block:: javascript // 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 ````, 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. .. tip:: 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. .. warning:: 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`` : .. captionup:: ./static/js/nouveau_compte.js .. code-block:: javascript 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`` : .. captionup:: ./static/js/nouveau_compte.js .. code-block:: javascript // 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 !