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) =>
{
try
{
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 user=await db["User"].findOne({ attributes: ["id", "password", "email", "name", "status"], where: { email: emailSend } });
if(!user)
res.status(404).json({ errors: [txt.emailNotFound] });
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 } });
if(!subscription)
{
@ -356,30 +359,35 @@ exports.login = async (req, res, next) =>
else
{
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));
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)] });
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);
if (!valid)
{
res.status(401).json({ errors: [txt.badPassword] });
// On comptabilise l'erreur :
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;
await toolFile.createJSON(config.dirTmpLogin, slugify(emailSend), newCountLogin);
}
else
{
// Si tout est ok, on enregistre la date de connexion + retourne un token de connexion.
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 });
creaUserJson(user.id);
// Connexion à rallonge uniquement possible pour utilisateur de base :
let loginTime=config.tokenConnexionMinTimeInHours;
if((req.body.keepConnected==="true") && (user.status==="user"))
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;
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) =>
{
try
{
// Est-ce qu'un compte existe pour l'adresse e-mail envoyée ?
const emailSend=tool.trimIfNotNull(req.body.email);
const userDatas=await searchUserByEmail(emailSend);
if(!userDatas)
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] });
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)
{
await sendValidationLink(userDatas.User);
@ -447,7 +458,7 @@ exports.getLoginLink = async (req, res, next) =>
mailRecipientAddress: userDatas.User.email
}
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();

View File

@ -30,7 +30,7 @@
<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>
<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>
<footer class="cardboard">

View File

@ -1,10 +1,9 @@
// -- GESTION DU FORMULAIRE PERMETTANT DE SE CONNECTER
/// 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
/// 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.
/// 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
/// 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.
// Fichier de configuration tirés du backend :
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";
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 { addElement } from "./tools/dom.js";
import { helloDev } from "./tools/everywhere.js";
import { getDatasFromInputs } from "./tools/forms.js";
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 :
const txt = require("../../lang/"+lang+"/general");
const txtUsers = require("../../lang/"+lang+"/user");
const txtServerError = require("../../lang/"+lang+"/general").serverError;
const txtAlreadyConnected = require("../../lang/"+lang+"/user").alreadyConnected;
const txtNeedChooseLoginWay = require("../../lang/"+lang+"/user").needChooseLoginWay;
// Principaux éléments du DOM manipulés :
const myForm = document.getElementById("connection");
@ -40,11 +40,10 @@ const initialise = async () =>
const isConnected=await checkSession();
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 homePage=user.status+"HomePage";
window.location.assign("/"+configTemplate[homePage]);
addElement(divResponse, "p", txtUsers.alreadyConnected, "", ["information"]);// au cas où blocage redirection
}
else
{
@ -58,7 +57,7 @@ const initialise = async () =>
}
catch(e)
{
addElement(divResponse, "p", txt.serverError, "", ["error"]);
addElement(divResponse, "p", txtServerError, "", ["error"]);
console.error(e);
}
}
@ -73,7 +72,7 @@ myForm.addEventListener("submit", function(e)
divResponse.innerHTML="";// efface d'éventuels messages déjà affichés
let datas=getDatasFromInputs(myForm);
if(isEmpty(datas.password) && isEmpty(datas.getLoginLink))
addElement(divResponse, "div", txtUsers.needChooseLoginWay, "", ["error"]);
addElement(divResponse, "div", txtNeedChooseLoginWay, "", ["error"]);
else
{
const xhr = new XMLHttpRequest();
@ -89,21 +88,20 @@ myForm.addEventListener("submit", function(e)
if (this.status === 200)
{
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";
addElement(divResponse, "p", response.message, "", ["success"]);
}
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();
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;
else
connexionMaxTime+=parseInt(response.connexionTime,10)*3600*1000;
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";
//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 :
let url=getLocaly("url", true);
if(!isEmpty(url) && url.href.indexOf(siteUrl)!==-1)
@ -112,29 +110,26 @@ myForm.addEventListener("submit", function(e)
removeLocaly("url");
}
else
url=configTemplate[response.status+"HomePage"]
url=configTemplate[response.status+"HomePage"];
window.location.assign(url);
}
else
addElement(divResponse, "p", txt.serverError, "", ["error"]);
addElement(divResponse, "p", txtServerError, "", ["error"]);
}
else if (response.errors)
{
if(Array.isArray(response.errors))
response.errors = response.errors.join("<br>");
else
response.errors = txt.serverError;
response.errors = response.errors.join("<br>");
addElement(divResponse, "p", response.errors, "", ["error"]);
}
else
addElement(divResponse, "p", txt.serverError, "", ["error"]);
addElement(divResponse, "p", txtServerError, "", ["error"]);
}
}
xhr.setRequestHeader("Content-Type", "application/json");
if(datas)
{
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);
xhr.send(JSON.stringify(datas));
}
@ -142,7 +137,7 @@ myForm.addEventListener("submit", function(e)
}
catch(e)
{
addElement(divResponse, "p", txt.serverError, "", ["error"]);
addElement(divResponse, "p", txtServerError, "", ["error"]);
console.error(e);
}
});

View File

@ -15,7 +15,7 @@ import { getLocaly, removeLocaly, saveLocaly } from "./tools/clientstorage.js";
import { addElement } from "./tools/dom.js";
import { helloDev } from "./tools/everywhere.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 :
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.",
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.",
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.",
badPassword: "Aucun compte utilisateur ne correspond aux informations saisies.",
mailLoginLinkSubject : "Votre lien de connexion.",
mailLoginLinkTxt : "Me connecter.",
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>",
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.",
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.",