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 :

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

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 :

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

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.

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 fonctionverifier_enter()
et de la gestion de l’événement socketredirection
. - 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 tableusers
(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
:
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
:
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 :
<!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 :
<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 parform_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 attributid
qui commence parnc_
, 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 fonctionverifier()
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 attributstyle
soit modifié par du codejavascript
afin de l’afficher ou le cacher - Les entrées
nom
etprénom
sont écrites à double, une fois pour les appareils mobiles et une fois pour les ordinateurs. Comme il n’est pas possible d’attribuer deuxid
similaires dans une pageHTML
, les identifiants des balises concernant les appareils mobiles se terminent par_mobile
- Pour éviter d’afficher deux fois les champs
nom
etprénom
, ceux-ci sont pourvus d’une nouvelle classe,desktop
oumobile
, 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
.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
:
@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
:
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
:
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
:
// 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 !