WikiLerni/controllers/user.js

1194 lines
50 KiB
JavaScript
Raw Normal View History

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;