WikiLerni/controllers/user.js

1194 lines
50 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const slugify = require('slugify');
const { Op, QueryTypes } = require("sequelize");// pour certaines requêtes sql
const config = require("../config/main.js");
const configTpl = require("../views/"+config.theme+"/config/"+config.availableLangs[0]+".js");
const tool = require("../tools/main");
const toolError = require("../tools/error");
const toolFile = require("../tools/file");
const toolMail = require("../tools/mail");
const txt = require("../lang/"+config.adminLang+"/user");
const txtGeneral = require("../lang/"+config.adminLang+"/general");
const answerCtrl = require("./answer");
const subscriptionCtrl = require("./subscription");
// Retourne certaines configurations utiles aux formulaires où vues
exports.checkEmailIsFree = async (req, res, next) =>
{
try
{
const user=await searchUserByEmail(req.body.emailTest);
const response={ };
if(user)
{
response.free=false;
if(user.Subscription)
response.validated=true;
else
response.validated=false;
}
else
response.free=true;
res.status(200).json(response);
next();
}
catch(e)
{
next(e);
}
}
exports.getGodfatherId = async (req, res, next) =>
{
try
{
const godfather=await searchIdGodfather(req.body.codeTest);
if(godfather)
res.status(200).json(godfather.id);
else
res.status(204).json({ errors: txt.godfatherNotFound });
next();
}
catch(e)
{
next(e);
}
}
// retourne toutes les infos utiles côté client pour un utilisateur
// revoir : placer dans un autre contrôler dédié à communication avec le front-end
exports.getConfig = (req, res, next) =>
{
try
{
//const configLinks = require("../config/links.js");
//const configIllustrations = require("../config/illustrations.js");
const configUsers =
{
// les routes :
siteUrl: config.siteUrl,
apiUrl: config.apiUrl,
// -- users
userRoutes: "/user",
subscribeRoute: "/signup",
getGodfatherRoute: "/getgodfatherid",
checkIfIsEmailfreeRoute: "/isemailfree",
checkSubscribeTokenRoute: "/validation/",
checkLoginRoute: "/checklogin/",
connectionRoute: "/login",
getLoginLinkRoute: "/getloginlink",
connectionWithLinkRoute: "/checkloginlink",
getUserInfos: "/get/",
createUserRoute: "/create",
validateUserRoute: "/validate/",
updateUserInfos: "/modify/",
searchUserRoute: "/search/",
getGodChilds: "/getgodchilds/",
checkNewLoginLinkRoute: "/confirmnewlogin/",
checkDeleteLinkRoute: "/confirmdelete/",
getPayments: "/payment/getforoneuser/",
unsubscribeRoute: "/subscription/stop/",
// -- questionnaires :
questionnaireRoutes: "/questionnaire",
getQuestionnaireRoutes: "/get",
previewQuestionnaireRoutes: "/preview",
publishedQuestionnaireRoutes: "/quiz/",
saveAnswersRoute: "/answer/",
getStatsQuestionnaires : "/stats/",
searchQuestionnairesRoute : "/search",
searchAdminQuestionnairesRoute : "/searchAdmin",
getListNextQuestionnaires: "/getListNextQuestionnaires/",
// -- tags :
tagsSearchRoute: "/tags/search/",
// -- links :
linksRoute: "/link/",
// -- illustrations :
illustrationsRoute: "/illustration/",
// -- questions & choices :
questionsRoute: "/question/",
// -- answers :
getQuestionnairesWithoutAnswer: "/withoutanswer/user/",
getPreviousAnswers: "/user/answers/",
getStatsAnswers : "/user/anwswers/stats/",
getAdminStats: "/getadminstats/",
// configuration des champs de saisie
// revoir : toutes ces données devraient venir de fichiers de configuration
// -- users :
name: { maxlength: 70, required: true },
email: { maxlength: 255, required: true },
password: { minlength: config.passwordMinLength, required: true },
newPassword: { minlength: config.passwordMinLength },
codeGodfather: { maxlength: 255 },
cguOk: { value: "true", required: true },
timeDifferenceMin:-720,
timeDifferenceMax:840,
beginCodeGodfather: config.beginCodeGodfather,
// -- questionnaires :
Questionnaire :
{
title: { maxlength: 255, required: true },
slug: { maxlength: 150 }, // champ requis mais calculé à partir du titre qd vide
introduction: { required: true }
},
searchQuestionnaires : { minlength: config.minSearchQuestionnaires, required: true },
// -- links :
Link :
{
url: { maxlength: 255, required: true },
anchor: { maxlength: 150, required: true }
},
nbLinksMin: 1,
nbLinksMax: 1,
// -- illustrations :
Illustration :
{
alt: { maxlength: 255 },
title: { maxlength: 255 },
caption: { maxlength: 255 },
image: { required: true, accept: "'image/jpg', 'image/jpeg', 'image/png', 'image/gif', 'image/png'" } // si possibilité de sélectionner une image déjà chargée, required != true, mais oblige à revoir la suppression des fichiers image
},
nbIllustrationsMin: 0,
nbIllustrationsMax: 1,
// -- questions & choice :
Question :
{
text: { maxlength: 255, required: true },
rank: { required: true, min:1, defaultValue:1 }
},
Choice :
{
text: { maxlength: 255, required: true }
},
nbQuestionsMin: config.nbQuestionsMin,
nbQuestionsMax: config.nbQuestionsMax,
nbChoicesMax: config.nbChoicesMax,
// durée des token :
signupValidationTimeInHours: config.tokenSignupValidationTimeInHours,
loginLinkTimeInHours: config.tokenLoginLinkTimeInHours,
connexionMinTimeInHours: config.tokenConnexionMinTimeInHours,
connexionMaxTimeInHours: config.tokenConnexionMaxTimeInDays,
loginChangingTimeInHours: config.tokenLoginChangingTimeInHours,
deleteUserTimeInHours: config.tokenDeleteUserTimeInHours,
unsubscribeLinkTimeInDays: config.tokenUnsubscribeLinkTimeInDays
}
res.status(200).json(configUsers);
next();
}
catch(e)
{
next(e);
}
}
exports.signup = async (req, res, next) =>
{
try
{
const db = require("../models/index");
if(req.body.cguOk !== "true")
res.status(400).json({ errors: [txt.needUGCOk] });
else if(req.body.password.length < config.passwordMinLength) // si le champ est supprimé du formulaire d'inscription, générer un mot de passe ok ici
res.status(400).json({ errors: [txt.needLongPassWord.replace("MIN_LENGTH", config.passwordMinLength)] });
else
{
req.body.GodfatherId=null;
if(req.body.codeGodfather!=="")
{
const godfather=await searchIdGodfather(req.body.codeGodfather);
if(godfather)
req.body.GodfatherId=godfather.id;
}
req.body.password=await bcrypt.hash(req.body.password, config.bcryptSaltRounds);
const user=await db["User"].create({ ...req.body }, { fields: ["name", "email", "password", "newsletterOk", "GodfatherId", "timeDifference"] });
req.body.UserId=user.id;
// si l'utilisateur a répondu à un quiz avant de créer son compte, on enregistre son résultats.
if(req.body.QuestionnaireId)
{
await Promise.all([
db["QuestionnaireAccess"].create({ ...req.body }, { fields: ["QuestionnaireId", "UserId"] }),
db["Answer"].create({ ...req.body }, { fields: ["nbQuestions", "nbCorrectAnswers", "duration", "QuestionnaireId", "UserId"] })
]); // pas nécessaire ici de créer le cache JSON, car il sera créé lors de la première connexion au compte.
}
await sendValidationLink(user);
res.status(201).json({ message: txt.mailValidationMessage });
next();
}
}
catch(e)
{
const returnAPI=toolError.returnSequelize(e);
if(returnAPI.messages)
{
res.status(returnAPI.status).json({ errors : returnAPI.messages });
next();
}
else
next(e);
}
}
exports.signupValidation = async (req, res, next) =>
{
try
{
const db = require("../models/index");
const datas=await checkTokenUser(req.params.token);
if(datas)
{
if(datas.Subscription)
res.status(200).json({ errors: [txt.validationAlreadyMessage] });
else
{
const now=new Date();
await Promise.all([
db["Subscription"].create({ numberOfDays: config.freeAccountTimingInDays, noticeOk: datas.User.newsletterOk, UserId: datas.User.id }),
db["User"].update({ connectedAt: now }, { where: { id : datas.User.id }, limit:1 })
]);
creaUserJson(datas.User.id);
const mapMail =
{
USER_NAME: datas.User.name,
NOM_SITE : config.siteName,
EMAIL : config.senderEmail,
LINK_URL : config.siteUrl+"/"+configTpl.connectionPage
};
const mailDatas =
{
mailSubject: txt.mailWelcomeSubject,
mailPreheader: txt.mailWelcomeSubject,
mailTitle: txt.mailWelcomeSubject,
mailHeaderLinkUrl: config.siteUrl+"/"+configTpl.connectionPage,
mailHeaderLinkTxt: txt.mailWelcomeLinkTxt,
mailMainContent: tool.replaceAll(txt.mailWelcomeBodyHTML, mapMail),
linksCTA: [{ url:config.siteUrl+"/"+configTpl.connectionPage, txt:txt.mailWelcomeLinkTxt }],
mailRecipientAddress: datas.User.email
}
toolMail.sendMail(0, datas.User.email, txt.mailWelcomeSubject, tool.replaceAll(txt.mailWelcomeBodyTxt, mapMail), "", mailDatas);
if(datas.User.GodfatherId!==null)
{
const godfather=await searchUserById(datas.User.GodfatherId);
if(godfather)
{
mapMail.USER_NAME=godfather.User.name;
mapMail.EMAIL=datas.User.email;
const mailDatas2=
{
mailSubject: txt.mailThankGodfatherSubject,
mailPreheader: txt.mailThankGodfatherSubject,
mailTitle: txt.mailThankGodfatherSubject,
mailHeaderLinkUrl: config.siteUrl+"/"+configTpl.connectionPage,
mailHeaderLinkTxt: txt.mailThankGodfatherLinkTxt,
mailMainContent: tool.replaceAll(txt.mailThankGodfatherBodyHTML, mapMail),
mailRecipientAddress: godfather.User.email
}
toolMail.sendMail(godfather.User.smtp, godfather.User.email, txt.mailThankGodfatherSubject, tool.replaceAll(txt.mailThankGodfatherBodyTxt, mapMail), "", mailDatas2);
}
}
res.status(200).json(
{
userId: datas.User.id,
token: jwt.sign({ userId: datas.User.id }, config.tokenPrivateKey, { expiresIn: config.tokenConnexionMinTimeInHours })
});
}
}
else
res.status(401).json({ errors: txt.badLinkValidationMessage });
next();
}
catch(e)
{
next(e);
}
}
exports.checkToken = async (req, res, next) =>
{
try
{
const datas=await checkTokenUser(req.params.token);
if(datas && datas.Subscription)
{
const beginSubTS=new Date(datas.Subscription.createdAt).getTime();
const nbDaysOk=datas.Subscription.numberOfDays-Math.round((Date.now()-beginSubTS)/1000/3600/24);
res.status(200).json(
{
isValid: true,
id: datas.User.id,
name: datas.User.name,
language: datas.User.language,
timeDifference : datas.User.timeDifference,
status: datas.User.status,
nbDaysOk : nbDaysOk
});
}
else
res.status(200).json({ isValid: false });
next();
}
catch(e)
{
next(e);
}
}
// 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)
{
await sendValidationLink(user);
res.status(400).json({ errors: [txt.needValidationToLogin] });
}
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*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*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;
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, on les enregistre ici :
req.body.UserId=user.id;
if(req.body.QuestionnaireId)
{
const access=await subscriptionCtrl.checkQuestionnaireAccess(user.id, req.body.QuestionnaireId);
if(!access)
await db["QuestionnaireAccess"].create({ ...req.body }, { fields: ["QuestionnaireId", "UserId"] });
await db["Answer"].create({ ...req.body }, { fields: ["nbQuestions", "nbCorrectAnswers", "duration", "QuestionnaireId", "UserId"] });
answerCtrl.creaUserAnswersJson(req.body.UserId);
answerCtrl.creaUserQuestionnairesWithoutAnswerJson(req.body.UserId);
answerCtrl.creaUserStatsAnwsersJson(req.body.UserId);
}
res.status(200).json(
{
userId: user.id,
status: user.status,
connexionTime: loginTime,
token: jwt.sign({ userId: user.id }, config.tokenPrivateKey, { expiresIn: loginTime })
});
}
}
}
}
next();
}
catch(e)
{
next(e);
}
}
// 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") // 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);
res.status(401).json({ errors: [txt.needValidationToLogin] });
}
else
{
const token=jwt.sign({ userId:userDatas.User.id, keepConnected:req.body.keepConnected }, config.tokenPrivateKey, { expiresIn: config.tokenLoginLinkTimeInHours });
const mapMail =
{
USER_NAME: userDatas.User.name,
LINK_URL : config.siteUrl+"/"+configTpl.loginLinkPage+token
};
const mailDatas=
{
mailSubject: txt.mailLoginLinkSubject,
mailPreheader: txt.mailLoginLinkSubject,
mailTitle: txt.mailLoginLinkSubject,
mailHeaderLinkUrl: config.siteUrl+"/"+configTpl.loginLinkPage+token,
mailHeaderLinkTxt: txt.mailLoginLinkTxt,
mailMainContent: tool.replaceAll(txt.mailLoginLinkBodyHTML, mapMail),
linksCTA: [{ url:config.siteUrl+"/"+configTpl.loginLinkPage+token, txt:txt.mailLoginLinkTxt }],
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.replace("*TIMING*", config.tokenLoginLinkTimeInHours) });
}
}
next();
}
catch(e)
{
next(e);
}
}
exports.checkLoginLink = async (req, res, next) =>
{
try
{
const db = require("../models/index");
const userDatas= await checkTokenUser(req.body.t);
if(userDatas.User.status!=="user")
res.status(403).json({ errors: [txtGeneral.notAllowed] });
else if(userDatas.Subscription)
{
const now=new Date();
const timeDifference=req.body.timeDifference;
db["User"].update({ connectedAt: now, timeDifference: timeDifference }, { where: { id : userDatas.User.id }, limit:1 });
creaUserJson(userDatas.User.id);
let loginTime=config.tokenConnexionMinTimeInHours;
if(userDatas.decodedToken.keepConnected===true)
loginTime=config.tokenConnexionMaxTimeInDays;
// si des données concernant un quiz ont été transmises, je les enregistre ici :
if(req.body.QuestionnaireId)
{
req.body.UserId=userDatas.User.id;
const access=await subscriptionCtrl.checkQuestionnaireAccess(userDatas.User.id, req.body.QuestionnaireId);
if(!access)
await db["QuestionnaireAccess"].create({ ...req.body }, { fields: ["QuestionnaireId", "UserId"] });
await db["Answer"].create({ ...req.body }, { fields: ["nbQuestions", "nbCorrectAnswers", "duration", "QuestionnaireId", "UserId"] });
answerCtrl.creaUserAnswersJson(req.body.UserId);
answerCtrl.creaUserStatsAnwsersJson(req.body.UserId);
answerCtrl.creaUserQuestionnairesWithoutAnswerJson(req.body.UserId);
}
res.status(200).json(
{
userId: userDatas.User.id,
connexionTime: loginTime,
token: jwt.sign({ userId: userDatas.User.id }, config.tokenPrivateKey, { expiresIn: loginTime })
});
}
else // ne devrait pas être possible...
{
await sendValidationLink(userDatas.User);
res.status(401).json({ errors: [txt.needValidationToLogin] });
}
next();
}
catch(e)
{
next(e);
}
}
exports.modify = async (req, res, next) =>
{
try
{
const db = require("../models/index");
if(req.body.newPassword && req.body.newPassword.length < config.passwordMinLength)
res.status(400).json({ errors: txt.needLongPassWord.replace("MIN_LENGTH", config.passwordMinLength) });
else
{
if(req.connectedUser.User.status==="user")
{
await Promise.all([
db["User"].update({ ...req.body }, { where: { id : req.connectedUser.User.id } , fields: ["name", "language", "newsletterOk", "timeDifference"], limit:1 }),
db["Subscription"].update({ ...req.body }, { where: { UserId : req.connectedUser.User.id }, fields: ["receiptDays", "noticeOk"], limit:1 })
]);
creaUserJson(req.connectedUser.User.id);
const messageRetour=[txt.updatedOkMessage];
if(req.body.newPassword || (req.body.email != req.connectedUser.User.email))
{
const newLogin={ userId: req.connectedUser.User.id };
req.body.email=tool.trimIfNotNull(req.body.email);
if(req.body.email != req.connectedUser.User.email)
{
if (! /[A-Za-z0-9_.-]+@[A-Za-z0-9_.-]+\.[A-Za-z]{2,6}/.test(req.body.email))
messageRetour.push(txt.updatedNeedGoodEmail);
else
{
const emailFree=await searchUserByEmail(req.body.email);
if(emailFree!==false)
messageRetour.push(txt.updatedNeedUniqueEmail.replace("NEW_EMAIL", req.body.email));
else
newLogin.email=req.body.email;
}
}
if(req.body.newPassword)
newLogin.password=await bcrypt.hash(req.body.newPassword, config.bcryptSaltRounds);
if(newLogin.email || newLogin.password)
{
const token=jwt.sign(newLogin, config.tokenPrivateKey, { expiresIn: config.tokenLoginChangingTimeInHours });
const mapMail =
{
USER_NAME: req.body.name,
LINK_URL : config.siteUrl+"/"+configTpl.newLoginLinkPage+token
};
const mailDatas=
{
mailSubject: txt.mailUpdateLoginSubject,
mailPreheader: txt.mailUpdateLoginSubject,
mailTitle: txt.mailUpdateLoginSubject,
mailHeaderLinkUrl: config.siteUrl+"/"+configTpl.newLoginLinkPage+token,
mailHeaderLinkTxt: txt.mailUpdateLoginLinkTxt,
mailMainContent: tool.replaceAll(txt.mailUpdateLoginBodyHTML, mapMail),
linksCTA: [{ url:config.siteUrl+"/"+configTpl.newLoginLinkPage+token, txt:txt.mailUpdateLoginLinkTxt }],
mailRecipientAddress: req.body.email
}
await toolMail.sendMail(req.connectedUser.User.smtp, req.body.email, txt.mailUpdateLoginSubject, tool.replaceAll(txt.mailUpdateLoginBodyTxt, mapMail), "", mailDatas);
messageRetour.push(txt.mailUpdateLoginLinkMessage.replace("NEW_EMAIL", req.body.email));
}
}
res.status(200).json({ message: messageRetour });
}
else
{
let userToUpdating=await searchUserById(req.params.id);
if(!userToUpdating || !userToUpdating.Subscription)
res.status(400).json({ errors: [txt.updatedNeedValidatedUser] });
else
{
const messageRetour=[txtGeneral.updateOkMessage];
if(req.body.newPassword)
req.body.password=await bcrypt.hash(req.body.newPassword, config.bcryptSaltRounds);
if(tool.isEmpty(req.body.newGodfatherId))
req.body.GodfatherId=null;
else if(req.body.newGodfatherId && req.body.newGodfatherId!==req.params.id && await searchUserById(req.body.newGodfatherId))
req.body.GodfatherId=req.body.newGodfatherId;
else
messageRetour.push(txt.updatedNeedGoodGodfather);
switch(req.connectedUser.User.status)
{
case "admin":
await Promise.all([
db["User"].update({ ...req.body }, { where: { id : req.params.id }, limit:1 }),
db["Subscription"].update({ ...req.body }, { where: { UserId : req.params.id }, limit:1 })
]);
break;
case "manager":
await Promise.all([
db["User"].update({ ...req.body }, { where: { id : req.params.id }, fields: ["name", "email", "password", "language", "adminComments", "smtp", "newsletterOk", "GodfatherId"], limit:1 }),
db["Subscription"].update({ ...req.body }, { where: { UserId : req.params.id }, fields: ["numberOfDays", "receiptDays", "noticeOk"], limit:1 })
]);
break;
case "creator":
await Promise.all([
db["User"].update({ ...req.body }, { where: { id : req.params.id } , fields: ["name", "email", "password", "language", "adminComments"], limit:1 }),
db["Subscription"].update({ ...req.body }, { where: { UserId : req.params.id }, fields: ["numberOfDays", "receiptDays"], limit:1 })
]);
break;
default:
throw { message: txtGeneral.serverError };
}
await creaUserJson(req.params.id);// besoin d'attendre car utiliser pour rafraîchir l'affichage
res.status(200).json({ message: messageRetour });
}
}
}
next();
}
catch(e)
{
const returnAPI=toolError.returnSequelize(e);
if(returnAPI.messages)
{
res.status(returnAPI.status).json({ errors : returnAPI.messages });
next();
}
else
next(e);
}
}
exports.checkNewLoginLink = async (req, res, next) =>
{
try
{
const db = require("../models/index");
const userDatas=await checkTokenUser(req.params.token);
if(userDatas)
{
if(userDatas.User.id!==req.connectedUser.User.id)
res.status(401).json({ errors: [txtGeneral.notAllowed] });
else
{
await db["User"].update({ ...userDatas.decodedToken }, { where: { id : userDatas.User.id } , fields: ["email", "password"], limit:1 });
creaUserJson(userDatas.User.id);
res.status(200).json({ message: txt.mailUpdateLoginOkMessage });
}
}
else
res.status(404).json({ errors: [txtGeneral.notAllowed] });
next();
}
catch(e)
{
next(e);
}
}
exports.create = async (req, res, next) =>
{
try
{
const db = require("../models/index");
const connectedUser=req.connectedUser;
if(connectedUser.User.status==="user")
res.status(403).json({ errors: [txtGeneral.notAllowed] });
else
{
if(!tool.isEmpty(req.body.GodfatherId) && ! await searchUserById(req.body.GodfatherId))
res.status(400).json({ errors: [txt.updatedFailedGodfatherNotFound] });
else
{
req.body.password=await bcrypt.hash(req.body.password, config.bcryptSaltRounds);
if(req.body.GodfatherId==="")
req.body.GodfatherId=null;
let user;
switch(connectedUser.User.status)
{
case "admin":
user=await db["User"].create({ ...req.body });
req.body.UserId=user.id;
await db["Subscription"].create({ ...req.body });
break;
case "manager":
user=await db["User"].create({ ...req.body }, { fields: ["name", "email", "password", "language", "adminComments", "smtp", "newsletterOk", "GodfatherId"] });
req.body.UserId=user.id;
await db["Subscription"].create({ ...req.body }, { fields: ["numberOfDays", "receiptDays", "noticeOk", "UserId"] });
break;
case "creator":
user=await db["User"].create({ ...req.body }, { fields: ["name", "email", "password", "language", "adminComments"] });
req.body.UserId=user.id;
await db["Subscription"].create({ ...req.body }, { fields: ["numberOfDays", "receiptDays", "UserId"] });
break;
default:
throw { message: [txtGeneral.serverError] };
}
creaUserJson(req.body.UserId);
res.status(201).json({ message: [txt.creationOkMessage], id:req.body.UserId });
}
}
next();
}
catch(e)
{
const returnAPI=toolError.returnSequelize(e);
if(returnAPI.messages)
{
res.status(returnAPI.status).json({ errors : [returnAPI.messages] });
next();
}
else
next(e);
}
}
exports.validate = async (req, res, next) =>
{
try
{
const db = require("../models/index");
const connectedUser=req.connectedUser;
if(["admin", "manager"].indexOf(connectedUser.User.status) === -1)
res.status(403).json({ errors: [txtGeneral.notAllowed] });
else
{
const userToValidating=await searchUserById(req.params.id);
if(!userToValidating)
res.status(404).json({ errors: [txt.notFound] });
else if(userToValidating.Subscription)
res.status(400).json({ errors: [txt.validationAlreadyMessageAdmin] });
else
{
await db["Subscription"].create({ numberOfDays: config.freeAccountTimingInDays, UserId: req.params.id });
creaUserJson(req.params.id);
res.status(200).json({ message: [txt.validationMessageAdmin] });
}
}
}
catch(e)
{
const returnAPI=toolError.returnSequelize(e);
if(returnAPI.messages)
{
res.status(returnAPI.status).json({ errors : returnAPI.messages });
next();
}
else
next(e);
}
}
exports.delete = async (req, res, next) =>
{
try
{
const db = require("../models/index");
const connectedUser=req.connectedUser;
if(connectedUser.User.status==="user")
{
const userDatas=await searchUserById(connectedUser.User.id);
const token=jwt.sign({ userId: connectedUser.User.id }, config.tokenPrivateKey, { expiresIn: config.tokenDeleteUserTimeInHours });
const mapMail =
{
USER_NAME: userDatas.User.name,
LINK_URL : config.siteUrl+"/"+configTpl.deleteLinkPage+token
};
const mailDatas=
{
mailSubject: txt.mailDeleteSubject,
mailPreheader: txt.mailDeleteSubject,
mailTitle: txt.mailDeleteSubject,
mailHeaderLinkUrl: config.siteUrl+"/"+configTpl.deleteLinkPage+token,
mailHeaderLinkTxt: txt.mailDeleteLinkTxt,
mailMainContent: tool.replaceAll(txt.mailDeleteBodyHTML, mapMail),
linksCTA: [{ url:config.siteUrl+"/"+configTpl.deleteLinkPage+token, txt:txt.mailDeleteLinkTxt }],
mailRecipientAddress: connectedUser.User.email
}
await toolMail.sendMail(connectedUser.User.smtp, connectedUser.User.email, txt.mailDeleteSubject, tool.replaceAll(txt.mailDeleteBodyTxt, mapMail), "", mailDatas);
res.status(200).json({ message: txt.mailDeleteLinkMessage });
}
else if (["admin","manager","creator"].indexOf(connectedUser.User.status) !== -1)
{
const userDeleted=await searchUserById(req.params.id);
if(!userDeleted)
throw { message: txt.notFound };
const nb=await db["User"].destroy({ where: { id : req.params.id }, limit:1 });
if(nb===1)
{
await toolFile.deleteJSON(config.dirCacheUsers, req.params.id);
toolFile.deleteJSON(config.dirCacheUsersQuestionnaires+"/without", req.params.id);
toolFile.deleteJSON(config.dirCacheUsersQuestionnaires+"/answers", req.params.id);
toolFile.deleteJSON(config.dirCacheUsersQuestionnaires+"/answers", "stats"+req.params.id);
let now=new Date(), wasValided=false, wasPremium=false;
if(userDeleted.Subscription)
{
wasValided=true;
if(userDeleted.Subscription.numberOfDay > config.freeAccountTimingInDays)
wasPremium=true;
}
db["UserDeleted"].create({ createdAt: userDeleted.User.createdAt, deletedAt: now, wasValided: wasValided, wasPremium: wasPremium });
res.status(200).json({ message: [txt.deleteOkMessage] });
}
else
{
res.status(400).json({ errors: txtGeneral.serverError });
throw { message: txt.deleteFailMessage+req.params.id };
}
}
next();
}
catch(e)
{
next(e);
}
}
exports.checkDeleteLink = async (req, res, next) =>
{
try
{
const db = require("../models/index");
const connectedUser=req.connectedUser;
const userDatas=await checkTokenUser(req.params.token);
if(userDatas)
{
if(connectedUser.User.id!=userDatas.User.id)
res.status(403).json({ errors: [txtGeneral.notAllowed] });
else
{
const nb=await db["User"].destroy({ where: { id : connectedUser.User.id }, limit:1 });
if(nb===1)
{
await toolFile.deleteJSON(config.dirCacheUsers, connectedUser.User.id);
res.status(200).json({ message: txt.mailDeleteLinkOkMessage });
toolFile.deleteJSON(config.dirCacheUsersQuestionnaires+"/without", connectedUser.User.id);
toolFile.deleteJSON(config.dirCacheUsersQuestionnaires+"/answers", connectedUser.User.id);
toolFile.deleteJSON(config.dirCacheUsersQuestionnaires+"/answers", "stats"+connectedUser.User.id);
const now=new Date(); wasPremium=false;
if(userDatas.Subscription.numberOfDay > config.freeAccountTimingInDays)
wasPremium=true;
db["UserDeleted"].create({ createdAt: userDatas.User.createdAt, deletedAt: now, wasValided:true, wasPremium: wasPremium });
}
else
res.status(400).json({ errors: [txt.mailDeleteLinkAlreadyMessage] });
}
}
else
res.status(400).json({ errors: [txt.mailDeleteLinkFailMessage] });
next();
}
catch(e)
{
next(e);
}
}
exports.getOneUserById = async (req, res, next) =>
{
try
{
const connectedUser=req.connectedUser;
if(connectedUser.User.status==="user")
{
if(connectedUser.User.id!=req.params.id)
res.status(403).json({ errors: txtGeneral.notAllowed });
else
{
const datas=await searchUserById(connectedUser.User.id);
if(datas)
res.status(200).json(datas);
else
res.status(404).json({ message:txt.notFound });
}
}
else if (["admin","manager","creator"].indexOf(connectedUser.User.status) !== -1)
{// dans le cas des "creator" il faudra peut-être limité l'accès à "ses" utilisateurs ?
const datas=await searchUserById(req.params.id);
if(datas)
res.status(200).json(datas);
else
res.status(404).json({ message:txt.notFound });
}
next();
}
catch(e)
{
next(e);
}
}
exports.getOneUserGodChilds = async (req, res, next) =>
{
try
{
const db = require("../models/index");
const connectedUser=req.connectedUser;
let godchilds;
if(["admin","manager","creator"].indexOf(connectedUser.User.status) === -1)
godchilds=await db["User"].findAll({ where: { GodfatherId: connectedUser.User.id }, attributes: ["id", "name", "email"] });
else
godchilds=await db["User"].findAll({ where: { GodfatherId: req.params.id }, attributes: ["id", "name", "email"] });
res.status(200).json(godchilds);
next();
}
catch(e)
{
next(e);
}
}
exports.searchUsers = async (req, res, next) =>
{
try
{
const connectedUser=req.connectedUser;
if(["admin", "manager", "creator"].indexOf(connectedUser.User.status) === -1)
res.status(403).json({ errors: [txtGeneral.notAllowed] });
else
{
const db = require("../models/index");
const Users=await db.sequelize.query("SELECT `id`,`name`,`email` FROM `Users` WHERE `name` LIKE :search OR `adminComments` LIKE :search OR `email` LIKE :search OR `id` = :id", { replacements: { search: "%"+req.body.search+"%", id: req.body.search }, type: QueryTypes.SELECT });
res.status(200).json(Users);
}
next();
}
catch(e)
{
next(e);
}
}
// Retourne des statistiques concernant les utilisateurs
// Utile tableau de bord gestionnaires
exports.getStats= async (req, res, next) =>
{
try
{
let stats=await getStatsUsers();
stats.Answers=await answerCtrl.getStatsAnswers();
stats.Subscriptions=await subscriptionCtrl.getStatsSubscriptions();
res.status(200).json(stats);
next();
}
catch(e)
{
next(e);
}
}
// CRONS
exports.deleteLoginFail = async (req, res, next) =>
{
try
{
const fileExpiration=new Date().getTime()-config.loginFailTimeInMinutes*60*1000;
const deleteFiles = await toolFile.deleteOldFilesInDirectory(config.dirTmpLogin, fileExpiration);
res.status(200).json(deleteFiles);
next();
}
catch(e)
{
next(e);
}
}
exports.deleteJsonFiles= async (req, res, next) =>
{
try
{
const db = require("../models/index");
const users=await db["User"].findAll({ attributes: ["id"] });
let saveFiles=[];
for(let i in users)
saveFiles.push(users[i].id+".json");
const deleteFiles = await Promise.all([
toolFile.deleteFilesInDirectory(config.dirCacheUsers, saveFiles),
toolFile.deleteFilesInDirectory(config.dirCacheUsersQuestionnaires, saveFiles)
]);
res.status(200).json(deleteFiles);
next();
}
catch(e)
{
next(e);
}
}
exports.deleteUnvalided= async (req, res, next) =>
{
try
{
const db = require("../models/index");
const nowTS=Date.now();
const timeExpiration=nowTS-parseInt(config.tokenSignupValidationTimeInHours,10)*3600*1000;
const userUnvalided= await db.sequelize.query("SELECT createdAt FROM `Users` WHERE UNIX_TIMESTAMP(createdAt) < "+timeExpiration+" AND `id` NOT IN (SELECT `UserId` FROM `Subscriptions`)", { type: QueryTypes.SELECT });
if(userUnvalided.length!==0)
{
const [results, metadata] = await db.sequelize.query("DELETE FROM `Users` WHERE UNIX_TIMESTAMP(createdAt) < "+timeExpiration+" AND `id` NOT IN (SELECT `UserId` FROM `Subscriptions`)");
const now=new Date();
for(i in userUnvalided)
await db["UserDeleted"].create({ createdAt: userUnvalided[i].createdAt, deletedAt: now, wasValided: false });
res.message=metadata.affectedRows+txt.cronDeleteUnvalidedUsersMessage;
}
res.status(200).json(true);
next();
}
catch(e)
{
next(e);
}
}
exports.deleteInactiveAccounts= async(req, res, next) =>
{
try
{
const db = require("../models/index");
// compte inactif = abonnement expiré + non connecté depuis un certains nombres de jours
const usersInactive= await db.sequelize.query("SELECT createdAt FROM `Users` WHERE status='user' and ADDDATE(`connectedAt`, "+config.inactiveAccountTimeToDeleteInDays+") < NOW() AND `id` IN (SELECT `UserId` FROM `Subscriptions` WHERE ADDDATE(`createdAt`, `numberOfDays`)< NOW())", { type: QueryTypes.SELECT });
if(usersInactive.length!==0)
{
const [results, metadata] = await db.sequelize.query("DELETE FROM `Users` WHERE ADDDATE(`connectedAt`, "+config.inactiveAccountTimeToDeleteInDays+") < NOW() AND `id` IN (SELECT `UserId` FROM `Subscriptions` WHERE ADDDATE(`createdAt`, `numberOfDays`)< NOW())");
if(metadata.affectedRows!==0)
res.message=metadata.affectedRows+txt.deleteInactiveUsersMessage;
const now=new Date();
for(i in usersInactive)
await db["UserDeleted"].create({ createdAt: usersInactive[i].createdAt, deletedAt: now, wasValided: true });
}
res.status(200).json(true);
next();
}
catch(e)
{
next(e);
}
}
// FONCTIONS UTILITAIRES
// Création du fichier des données de l'utilisateur + son abonnement et ses pauses
const creaUserJson = async (id) =>
{
id=tool.trimIfNotNull(id);
if(id===null)
return false;
const db = require("../models/index");
const User=await db["User"].findByPk(id, { attributes: { exclude: ['password'] } });
if(User)
{
let datas={ User };
const Subscription=await db["Subscription"].findOne({ where: { UserId: id } });
if(Subscription) // pas de cache pour utilisateurs non validés
{
datas.Subscription=Subscription;
const Pauses=await db["Pause"].findAll({ where: { SubscriptionId: Subscription.id } });
if(Pauses)
datas.Pauses=Pauses;
await toolFile.createJSON(config.dirCacheUsers, id, datas);
}
return datas;
}
else
return false;
}
exports.creaUserJson = creaUserJson;
const searchUserById = async (id) =>
{
id=tool.trimIfNotNull(id);
if(id===null)
return false;
const user=await toolFile.readJSON(config.dirCacheUsers, id);
if(user)
return user;
else
return await creaUserJson(id);
}
exports.searchUserById = searchUserById;
const searchUserByEmail = async (email) =>
{
const db = require("../models/index");
const emailSend=tool.trimIfNotNull(email);
const userEmail=await db["User"].findOne({ attributes: ["id"], where: { email : emailSend } });
if(userEmail)
return await searchUserById(userEmail.id);// permet de récupérer les autres infos (Subscription, etc.)
else
return false;
}
// Recherche le parrain dont le "code" fourni peut être l'identifiant préfixé ou bien l'adresse e-mail
const searchIdGodfather = async (code) =>
{
let godfatherInfos="";
code=tool.trimIfNotNull(code);
if(code===null)
return false;
else if(code.indexOf("@")!==-1) // tester en premier, car une adresse peut débuter par le préfixe de l'appli...
godfatherInfos=await searchUserByEmail(code);
else if (code.startsWith(config.beginCodeGodfather))
godfatherInfos=await searchUserById(code.substring(config.beginCodeGodfather.length));
if(godfatherInfos.Subscription && godfatherInfos.User.status==="user") // le parrain doit avoir validé son compte et doit être un simple "user"
return godfatherInfos.User;
else
return null;
}
// Envoi le lien permettant au nouvel utilisateur de valider son compte
const sendValidationLink = async (user) =>
{
const token=jwt.sign({ userId: user.id }, config.tokenPrivateKey, { expiresIn: config.tokenSignupValidationTimeInHours });
const mapMail =
{
USER_NAME: user.name,
LINK_URL: config.siteUrl+"/"+configTpl.validationLinkPage+token,
};
const mailDatas=
{
mailSubject: txt.mailValidationLinkSubject,
mailPreheader: txt.mailValidationLinkSubject,
mailTitle: txt.mailValidationLinkSubject,
mailHeaderLinkUrl: config.siteUrl+"/"+configTpl.validationLinkPage+token,
mailHeaderLinkTxt: txt.mailValidationLinkTxt,
mailMainContent: tool.replaceAll(txt.mailValidationLinkSBodyHTML, mapMail),
linksCTA: [{ url:config.siteUrl+"/"+configTpl.validationLinkPage+token, txt:txt.mailValidationLinkTxt }],
mailRecipientAddress: user.email
}
await toolMail.sendMail(0, user.email, txt.mailValidationLinkSubject, tool.replaceAll(txt.mailValidationLinkSBodyTxt, mapMail), "", mailDatas);
return true;
}
// Permet de vérifier un token de connexion : est-il valide et est-ce qu'il correspond à un utilisateur ?
// Si oui, on peut aussi enregistrer une nouvelle connexion pour l'utilisateur (une fois / jour)
const checkTokenUser = async (token) =>
{
const db = require("../models/index");
const decodedToken=jwt.verify(token, config.tokenPrivateKey);
const userId=decodedToken.userId;
const datas=await searchUserById(userId);
if(datas.User)
{
const now=new Date();
const nowTS=Date.now()
const lastConnection = Date.parse(datas.User.connectedAt);
if(lastConnection+24*3600*1000 < nowTS) // une fois par jour suffit.
{
await db["User"].update({ connectedAt: now }, { where: { id : userId }, limit:1 });
creaUserJson(userId);
}
datas.decodedToken=decodedToken;
return datas;
}
else
return false;
}
exports.checkTokenUser = checkTokenUser;
// Stats sur les créations de compte, suppression.. durant les dernières 24H ou depuis le début
const getStatsUsers = async () =>
{
const db = require("../models/index");
const getNewUsers24H = await db.sequelize.query("SELECT `id` FROM `Users` WHERE `createdAt` > ADDDATE(NOW(), -1)", { type: QueryTypes.SELECT });
const getNewUsersTot = await db.sequelize.query("SELECT `id` FROM `Users`", { type: QueryTypes.SELECT });
const getDeletedUsers24H = await db.sequelize.query("SELECT `id` FROM `UserDeleteds` WHERE `deletedAt` > ADDDATE(NOW(), -1)", { type: QueryTypes.SELECT });
const getDeletedUsersTot = await db.sequelize.query("SELECT `id` FROM `UserDeleteds`", { type: QueryTypes.SELECT });
const getDeletedUsersWasValided = await db.sequelize.query("SELECT `id` FROM `UserDeleteds` WHERE `wasValided`= 1", { type: QueryTypes.SELECT });
const getDeletedUsersTotWasPremium = await db.sequelize.query("SELECT `id` FROM `UserDeleteds` WHERE `wasPremium`= 1", { type: QueryTypes.SELECT });
if(getNewUsers24H && getNewUsersTot && getDeletedUsers24H && getDeletedUsersWasValided && getDeletedUsersTotWasPremium && getDeletedUsersTot )
{
const stats =
{
nbNewUsers24H : getNewUsers24H.length,
nbNewUsersTot : getNewUsersTot.length,
nbDeletedUsers24H : getDeletedUsers24H.length,
nbDeletedUsersTot : getDeletedUsersTot.length,
nbDeletedUsersWasValided : getDeletedUsersWasValided.length,
nbDeletedUsersTotWasPremium : getDeletedUsersTotWasPremium.length
}
return stats;
}
else
return false;
}
exports.getStatsUsers = getStatsUsers;