Revue processus connexion compte avec mot de passe ou envoi de lien.

This commit is contained in:
Fabrice PENHOËT 2020-08-12 18:51:25 +02:00
parent 1c056db3bd
commit 7bafca1dc9
5 changed files with 42 additions and 36 deletions

View File

@ -336,17 +336,20 @@ exports.checkToken = async (req, res, next) =>
} }
} }
// Reçoit les données du formulaire de connexion avec mot de passe.
exports.login = async (req, res, next) => exports.login = async (req, res, next) =>
{ {
try try
{ {
const db = require("../models/index"); const db = require("../models/index");
// Est-ce qu'un compte existe pour l'adresse e-mail envoyée ?
const emailSend=tool.trimIfNotNull(req.body.email); const emailSend=tool.trimIfNotNull(req.body.email);
const user=await db["User"].findOne({ attributes: ["id", "password", "email", "name", "status"], where: { email: emailSend } }); const user=await db["User"].findOne({ attributes: ["id", "password", "email", "name", "status"], where: { email: emailSend } });
if(!user) if(!user)
res.status(404).json({ errors: [txt.emailNotFound] }); res.status(404).json({ errors: [txt.emailNotFound] });
else else
{ {
// Est-ce ce compte a déjà été validé par l'utilisateur ? Si non, on lui envoie un nouveau lien de validation.
const subscription=await db["Subscription"].findOne({ attributes: ["id"], where: { UserId: user.id } }); const subscription=await db["Subscription"].findOne({ attributes: ["id"], where: { UserId: user.id } });
if(!subscription) if(!subscription)
{ {
@ -356,30 +359,35 @@ exports.login = async (req, res, next) =>
else else
{ {
const nowTS=new Date().getTime(); const nowTS=new Date().getTime();
// L'utilisateur n'a-t-il pas testé de se connecter de trop nombreuses fois sans succès ?
const countLogin=await toolFile.readJSON(config.dirTmpLogin, slugify(emailSend)); const countLogin=await toolFile.readJSON(config.dirTmpLogin, slugify(emailSend));
if(countLogin && countLogin.nb >= config.maxLoginFail && countLogin.lastTime > (nowTS-config.loginFailTimeInMinutes*3600*1000)) if(countLogin && countLogin.nb >= config.maxLoginFail && countLogin.lastTime > (nowTS-config.loginFailTimeInMinutes*60*1000))
res.status(401).json({ errors: [txt.tooManyLoginFails.replace("MINUTES", config.loginFailTimeInMinutes)] }); res.status(401).json({ errors: [txt.tooManyLoginFails.replace("MINUTES", config.loginFailTimeInMinutes)] });
else else
{ {
// Le mot du passe envoyé est-il cohérent avec celui de la base de données après chiffrement ?
const valid = await bcrypt.compare(req.body.password, user.password); const valid = await bcrypt.compare(req.body.password, user.password);
if (!valid) if (!valid)
{ {
res.status(401).json({ errors: [txt.badPassword] }); res.status(401).json({ errors: [txt.badPassword] });
// On comptabilise l'erreur :
let newCountLogin={ nb:1, lastTime:nowTS }; let newCountLogin={ nb:1, lastTime:nowTS };
if(countLogin.nb && countLogin.lastTime > (nowTS-config.loginFailTimeInMinutes*3600*1000)) if(countLogin.nb && countLogin.lastTime > (nowTS-config.loginFailTimeInMinutes*60*1000))
newCountLogin.nb=countLogin.nb+1; newCountLogin.nb=countLogin.nb+1;
await toolFile.createJSON(config.dirTmpLogin, slugify(emailSend), newCountLogin); await toolFile.createJSON(config.dirTmpLogin, slugify(emailSend), newCountLogin);
} }
else else
{ {
// Si tout est ok, on enregistre la date de connexion + retourne un token de connexion.
const now=new Date(); const now=new Date();
const timeDifference=req.body.timeDifference;// permet d'actualiser en cas de déplacements/heures d'été, etc. const timeDifference=req.body.timeDifference;
db["User"].update({ connectedAt: now, timeDifference: timeDifference }, { where: { id : user.id }, limit:1 }); db["User"].update({ connectedAt: now, timeDifference: timeDifference }, { where: { id : user.id }, limit:1 });
creaUserJson(user.id); creaUserJson(user.id);
// Connexion à rallonge uniquement possible pour utilisateur de base :
let loginTime=config.tokenConnexionMinTimeInHours; let loginTime=config.tokenConnexionMinTimeInHours;
if((req.body.keepConnected==="true") && (user.status==="user")) if((req.body.keepConnected==="true") && (user.status==="user"))
loginTime=config.tokenConnexionMaxTimeInDays; loginTime=config.tokenConnexionMaxTimeInDays;
// si des données concernant un quiz ont été transmises, je les enregistre ici : // Si des données concernant un quiz ont été transmises, on les enregistre ici :
req.body.UserId=user.id; req.body.UserId=user.id;
if(req.body.QuestionnaireId) if(req.body.QuestionnaireId)
{ {
@ -410,18 +418,21 @@ exports.login = async (req, res, next) =>
} }
} }
// Reçoit les données du formulaire de connexion avec demande de recevoir un lien de connexion.
exports.getLoginLink = async (req, res, next) => exports.getLoginLink = async (req, res, next) =>
{ {
try try
{ {
// Est-ce qu'un compte existe pour l'adresse e-mail envoyée ?
const emailSend=tool.trimIfNotNull(req.body.email); const emailSend=tool.trimIfNotNull(req.body.email);
const userDatas=await searchUserByEmail(emailSend); const userDatas=await searchUserByEmail(emailSend);
if(!userDatas) if(!userDatas)
res.status(404).json({ errors: [txt.emailNotFound] }); res.status(404).json({ errors: [txt.emailNotFound] });
else if(userDatas.User.status!=="user") else if(userDatas.User.status!=="user") // seuls les utilisateurs de base peuvent se connecter de cette façon.
res.status(403).json({ errors: [txtGeneral.notAllowed] }); res.status(403).json({ errors: [txtGeneral.notAllowed] });
else else
{ {
// Est-ce ce compte a déjà été validé par l'utilisateur ? Si non, on lui envoie un nouveau lien de validation.
if(!userDatas.Subscription) if(!userDatas.Subscription)
{ {
await sendValidationLink(userDatas.User); await sendValidationLink(userDatas.User);
@ -447,7 +458,7 @@ exports.getLoginLink = async (req, res, next) =>
mailRecipientAddress: userDatas.User.email mailRecipientAddress: userDatas.User.email
} }
await toolMail.sendMail(userDatas.User.smtp, userDatas.User.email, txt.mailLoginLinkSubject, tool.replaceAll(txt.mailLoginLinkBodyTxt, mapMail), "", mailDatas); await toolMail.sendMail(userDatas.User.smtp, userDatas.User.email, txt.mailLoginLinkSubject, tool.replaceAll(txt.mailLoginLinkBodyTxt, mapMail), "", mailDatas);
res.status(200).json({ message: txt.mailLoginLinkMessage+config.tokenLoginLinkTimeInHours+"." }); res.status(200).json({ message: txt.mailLoginLinkMessage.replace("*TIMING*", config.tokenLoginLinkTimeInHours) });
} }
} }
next(); next();

View File

@ -30,7 +30,7 @@
<div id="prompt" class="cardboard"> <div id="prompt" class="cardboard">
<a href="/" title="Page d'accueil WikLerni"><img src="/themes/wikilerni/img/wikilerni-purple-2-512.png" alt="Logo WikiLerni" title="W I K I L E R N I" /></a> <a href="/" title="Page d'accueil WikLerni"><img src="/themes/wikilerni/img/wikilerni-purple-2-512.png" alt="Logo WikiLerni" title="W I K I L E R N I" /></a>
<p class="cardboard">Cultivons notre jardin !</p> <p class="cardboard">Cultivons notre jardin !</p>
<div id="response" class="error">Si vous voyez ce message, c'est que votre lien de connexion n'est pas valide. Vous pouvez <a href="/connexion.html">en demander un nouveau en cliquant ici</a>.</div> <div id="response">Si vous voyez ce message, c'est que votre lien de connexion n'est pas valide ou a expiré. Vous pouvez <a href="/connexion.html">en demander un nouveau en cliquant ici</a>.</div>
</div> </div>
<footer class="cardboard"> <footer class="cardboard">

View File

@ -1,10 +1,9 @@
// -- GESTION DU FORMULAIRE PERMETTANT DE SE CONNECTER // -- GESTION DU FORMULAIRE PERMETTANT DE SE CONNECTER
/// L'utilisateur peut avoir répondu à un quiz avant d'arriver sur la page de connexion /// L'utilisateur peut avoir répondu à un quiz avant d'arriver sur la page de connexion.
/// Dans ce cas il faut enregistrer son résultat en même temps, une fois la connexion validée /// Dans ce cas il faut enregistrer son résultat en même temps, une fois la connexion validée.
/// Le connexion peut se faire directement ici via la saisie d'un mot de passe /// Le connexion peut se faire directement ici via la saisie d'un mot de passe ou via l'envoi d'un token par e-mail.
/// Ou via l'envoi d'un token par e-mail
// Fichier de configuration tirés du backend : // Fichier de configuration tirés du backend :
import { apiUrl, availableLangs, siteUrl, theme } from "../../config/instance.js"; import { apiUrl, availableLangs, siteUrl, theme } from "../../config/instance.js";
@ -13,17 +12,18 @@ const lang=availableLangs[0];
import { connectionRoute, getLoginLinkRoute, userRoutes } from "../../config/users.js"; import { connectionRoute, getLoginLinkRoute, userRoutes } from "../../config/users.js";
const configTemplate = require("../../views/"+theme+"/config/"+lang+".js"); const configTemplate = require("../../views/"+theme+"/config/"+lang+".js");
// Importation des fonctions utile au script : // Importation des fonctions utiles au script :
import { getLocaly, removeLocaly, saveLocaly } from "./tools/clientstorage.js"; import { getLocaly, removeLocaly, saveLocaly } from "./tools/clientstorage.js";
import { addElement } from "./tools/dom.js"; import { addElement } from "./tools/dom.js";
import { helloDev } from "./tools/everywhere.js"; import { helloDev } from "./tools/everywhere.js";
import { getDatasFromInputs } from "./tools/forms.js"; import { getDatasFromInputs } from "./tools/forms.js";
import { isEmpty } from "../../tools/main"; import { isEmpty } from "../../tools/main";
import { checkAnswerDatas, checkSession, getConfig, getTimeDifference, setSession } from "./tools/users.js"; import { checkAnswerDatas, checkSession, getTimeDifference, setSession } from "./tools/users.js";
// Dictionnaires : // Dictionnaires :
const txt = require("../../lang/"+lang+"/general"); const txtServerError = require("../../lang/"+lang+"/general").serverError;
const txtUsers = require("../../lang/"+lang+"/user"); const txtAlreadyConnected = require("../../lang/"+lang+"/user").alreadyConnected;
const txtNeedChooseLoginWay = require("../../lang/"+lang+"/user").needChooseLoginWay;
// Principaux éléments du DOM manipulés : // Principaux éléments du DOM manipulés :
const myForm = document.getElementById("connection"); const myForm = document.getElementById("connection");
@ -40,11 +40,10 @@ const initialise = async () =>
const isConnected=await checkSession(); const isConnected=await checkSession();
if(isConnected) if(isConnected)
{ {
saveLocaly("message", { message: txtUsers.alreadyConnected, color:"information" });// pour l'afficher sur la page suivante saveLocaly("message", { message: txtAlreadyConnected, color:"info" });// pour l'afficher sur la page suivante
const user=getLocaly("user", true); const user=getLocaly("user", true);
const homePage=user.status+"HomePage"; const homePage=user.status+"HomePage";
window.location.assign("/"+configTemplate[homePage]); window.location.assign("/"+configTemplate[homePage]);
addElement(divResponse, "p", txtUsers.alreadyConnected, "", ["information"]);// au cas où blocage redirection
} }
else else
{ {
@ -58,7 +57,7 @@ const initialise = async () =>
} }
catch(e) catch(e)
{ {
addElement(divResponse, "p", txt.serverError, "", ["error"]); addElement(divResponse, "p", txtServerError, "", ["error"]);
console.error(e); console.error(e);
} }
} }
@ -73,7 +72,7 @@ myForm.addEventListener("submit", function(e)
divResponse.innerHTML="";// efface d'éventuels messages déjà affichés divResponse.innerHTML="";// efface d'éventuels messages déjà affichés
let datas=getDatasFromInputs(myForm); let datas=getDatasFromInputs(myForm);
if(isEmpty(datas.password) && isEmpty(datas.getLoginLink)) if(isEmpty(datas.password) && isEmpty(datas.getLoginLink))
addElement(divResponse, "div", txtUsers.needChooseLoginWay, "", ["error"]); addElement(divResponse, "div", txtNeedChooseLoginWay, "", ["error"]);
else else
{ {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
@ -89,21 +88,20 @@ myForm.addEventListener("submit", function(e)
if (this.status === 200) if (this.status === 200)
{ {
if(!isEmpty(response.message)) if(!isEmpty(response.message))
{ // cas d'une demande de lien de connexion { // cas d'une demande de lien de connexion avec succès.
myForm.style.display="none"; myForm.style.display="none";
addElement(divResponse, "p", response.message, "", ["success"]); addElement(divResponse, "p", response.message, "", ["success"]);
} }
else if(!isEmpty(response.userId) && !isEmpty(response.connexionTime) && !isEmpty(response.token)) else if(!isEmpty(response.userId) && !isEmpty(response.connexionTime) && !isEmpty(response.token))
{ // cas d'une connexion directe, on créé une session de connexion et redirige l'utilisateur { // cas d'une connexion via mot de passe avec succès : on crée une session de connexion et redirige l'utilisateur.
let connexionMaxTime=Date.now(); let connexionMaxTime=Date.now();
if(response.connexionTime.endsWith("days")) if(response.connexionTime.endsWith("days"))// l'utilisateur a demandé à rester connecté sur la durée.
connexionMaxTime+=parseInt(response.connexionTime,10)*24*3600*1000; connexionMaxTime+=parseInt(response.connexionTime,10)*24*3600*1000;
else else
connexionMaxTime+=parseInt(response.connexionTime,10)*3600*1000; connexionMaxTime+=parseInt(response.connexionTime,10)*3600*1000;
setSession(response.userId, response.token, connexionMaxTime); setSession(response.userId, response.token, connexionMaxTime);
removeLocaly("lastAnswer");// ! important pour ne pas enregister plusieurs fois le résultat removeLocaly("lastAnswer");// ! important pour ne pas enregister plusieurs fois son éventuel résultat au quiz.
myForm.style.display="none"; myForm.style.display="none";
//addElement(divResponse, "p", txtUsers.connectionOk, "", ["success"]);// au cas où blocage redirection
// l'utilisateur peut avoir tenté d'accéder à une autre page que sa page d'accueil : // l'utilisateur peut avoir tenté d'accéder à une autre page que sa page d'accueil :
let url=getLocaly("url", true); let url=getLocaly("url", true);
if(!isEmpty(url) && url.href.indexOf(siteUrl)!==-1) if(!isEmpty(url) && url.href.indexOf(siteUrl)!==-1)
@ -112,29 +110,26 @@ myForm.addEventListener("submit", function(e)
removeLocaly("url"); removeLocaly("url");
} }
else else
url=configTemplate[response.status+"HomePage"] url=configTemplate[response.status+"HomePage"];
window.location.assign(url); window.location.assign(url);
} }
else else
addElement(divResponse, "p", txt.serverError, "", ["error"]); addElement(divResponse, "p", txtServerError, "", ["error"]);
} }
else if (response.errors) else if (response.errors)
{ {
if(Array.isArray(response.errors)) response.errors = response.errors.join("<br>");
response.errors = response.errors.join("<br>");
else
response.errors = txt.serverError;
addElement(divResponse, "p", response.errors, "", ["error"]); addElement(divResponse, "p", response.errors, "", ["error"]);
} }
else else
addElement(divResponse, "p", txt.serverError, "", ["error"]); addElement(divResponse, "p", txtServerError, "", ["error"]);
} }
} }
xhr.setRequestHeader("Content-Type", "application/json"); xhr.setRequestHeader("Content-Type", "application/json");
if(datas) if(datas)
{ {
datas.timeDifference=getTimeDifference(); datas.timeDifference=getTimeDifference();
// si l'utilisateur a précédement répondu à un quiz, j'ajoute les infos de son résultat : // Si l'utilisateur a répondu à un quiz, j'ajoute les infos de son résultat aux données envoyées :
datas=checkAnswerDatas(datas); datas=checkAnswerDatas(datas);
xhr.send(JSON.stringify(datas)); xhr.send(JSON.stringify(datas));
} }
@ -142,7 +137,7 @@ myForm.addEventListener("submit", function(e)
} }
catch(e) catch(e)
{ {
addElement(divResponse, "p", txt.serverError, "", ["error"]); addElement(divResponse, "p", txtServerError, "", ["error"]);
console.error(e); console.error(e);
} }
}); });

View File

@ -15,7 +15,7 @@ import { getLocaly, removeLocaly, saveLocaly } from "./tools/clientstorage.js";
import { addElement } from "./tools/dom.js"; import { addElement } from "./tools/dom.js";
import { helloDev } from "./tools/everywhere.js"; import { helloDev } from "./tools/everywhere.js";
import { getDatasFromInputs, setAttributesToInputs } from "./tools/forms.js"; import { getDatasFromInputs, setAttributesToInputs } from "./tools/forms.js";
import { checkAnswerDatas, checkSession, getConfig, getPassword, getTimeDifference } from "./tools/users.js"; import { checkAnswerDatas, checkSession, getPassword, getTimeDifference } from "./tools/users.js";
// Dictionnaires : // Dictionnaires :
const txtServerError = require("../../lang/"+lang+"/general").serverError; const txtServerError = require("../../lang/"+lang+"/general").serverError;

View File

@ -44,14 +44,14 @@ module.exports =
needBeConnected: "Vous devez être connecté pour accéder à cette page.", needBeConnected: "Vous devez être connecté pour accéder à cette page.",
connectionOk: "Connexion réussie.", connectionOk: "Connexion réussie.",
needChooseLoginWay: "Vous devez soit saisir votre mot de passe, soit cocher la case vous permettant de recevoir un lien de connexion par e-mail.", needChooseLoginWay: "Vous devez soit saisir votre mot de passe, soit cocher la case vous permettant de recevoir un lien de connexion par e-mail.",
needValidationToLogin : "Vous devez d'abord valider votre compte avant de vous connecter. Pour ce faire, un lien vient de vous être envoyé par e-mail.", needValidationToLogin : "Vous devez d'abord valider votre compte avant de pouvoir vous connecter. Pour ce faire, un nouveau lien vient de vous être envoyé par e-mail.",
tooManyLoginFails : "Désolé mais il y a eu trop de tentatives de connexion infructueuses pour cette adresse e-mail. Vous devez attendre MINUTES minutes pour essayer de nouveau.", tooManyLoginFails : "Désolé mais il y a eu trop de tentatives de connexion infructueuses pour cette adresse e-mail. Vous devez attendre MINUTES minutes pour essayer de nouveau.",
badPassword: "Aucun compte utilisateur ne correspond aux informations saisies.", badPassword: "Aucun compte utilisateur ne correspond aux informations saisies.",
mailLoginLinkSubject : "Votre lien de connexion.", mailLoginLinkSubject : "Votre lien de connexion.",
mailLoginLinkTxt : "Me connecter.", mailLoginLinkTxt : "Me connecter.",
mailLoginLinkBodyTxt : "Bonjour USER_NAME,\n\nPour vous connecter à votre compte, cliquez sur le lien suivant sans tarder :\nLINK_URL", mailLoginLinkBodyTxt : "Bonjour USER_NAME,\n\nPour vous connecter à votre compte, cliquez sur le lien suivant sans tarder :\nLINK_URL",
mailLoginLinkBodyHTML : "<h3>Bonjour USER_NAME,</h3><p>Pour vous connecter à votre compte, cliquez sur le lien suivant sans tarder :</p>", mailLoginLinkBodyHTML : "<h3>Bonjour USER_NAME,</h3><p>Pour vous connecter à votre compte, cliquez sur le lien suivant sans tarder :</p>",
mailLoginLinkMessage : "Un lien de connexion vient de vous être envoyé sur votre adresse e-mail. Ne tardez pas à l'utiliser, car il n'est valable que durant ", mailLoginLinkMessage : "Un lien de connexion vient de vous être envoyé sur votre adresse e-mail. Ne tardez pas à l'utiliser, car il n'est valable que durant *TIMING* !",
updatedOkMessage: "Vos informations ont bien été mises à jour.", updatedOkMessage: "Vos informations ont bien été mises à jour.",
updatedNeedGoodEmail : "Mais la nouvelle adresse e-mail n'a pu être enregistrée, car elle n'a pas un format correct.", updatedNeedGoodEmail : "Mais la nouvelle adresse e-mail n'a pu être enregistrée, car elle n'a pas un format correct.",
updatedNeedUniqueEmail : "Mais la nouvelle adresse e-mail saisie (NEW_EMAIL) n'a pu être enregistrée, car elle est déjà utilisée pour un autre compte.", updatedNeedUniqueEmail : "Mais la nouvelle adresse e-mail saisie (NEW_EMAIL) n'a pu être enregistrée, car elle est déjà utilisée pour un autre compte.",