WikiLerni/controllers/user.js

1126 lines
48 KiB
JavaScript
Raw Permalink 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 configUsers = require("../config/users.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);
}
}
// Contrôleur traitant les données envoyées pour une inscription
// Les CGU doivent être acceptées et une adresse e-mail envoyée.
// Un champ email2 invisible ne devrait être envoyé que par des bots
// Le reste peut être adapté sur la page de validation de l'inscription.
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(tool.isEmpty(req.body.email))
res.status(400).json({ errors: [txt.needEmail] });
else if(!tool.isEmpty(req.body.email2))
{
res.status(400).json({ errors: [txt.failBotTest] });
console.log(txt.failBotTestLog+req.body.email2);
}
else
{
// Un mot de passe temporaire et non communiqué est généré :
req.body.passwordTemp=tool.getPassword(8, 12);
req.body.password=await bcrypt.hash(req.body.passwordTemp, config.bcryptSaltRounds);
// Un pseudo temporaire est créé en utilisant la partie de l'e-mail précédant le "@" :
const lastIndex=req.body.email.indexOf("@");
if(lastIndex === -1)// possible car validité de l'e-mail testé par le modèle lors de l'enregistrement
lastIndex=1;
req.body.name=req.body.email.substring(0, lastIndex);
const user=await db["User"].create({ ...req.body }, { fields: ["name", "email", "password", "timeDifference"] });
req.body.UserId=user.id;
// Si l'utilisateur a répondu à un quiz (ou groupe de quizs) avant de créer son compte, on enregistre son résultat :
if(req.body.QuestionnaireId)
await answerCtrl.saveAnswerToQuestionnaire(req.body);
else if(req.body.GroupId)
await answerCtrl.saveAnswerToGroup(req.body);
await sendValidationLink(user);
res.status(201).json({ message: txt.mailValidationMessage.replace("#EMAIL", req.body.email) });
next();
}
}
catch(e)
{
const returnAPI=toolError.returnSequelize(e);
if(returnAPI.messages)
{
res.status(returnAPI.status).json({ errors : returnAPI.messages });
next();
}
else
next(e);
}
}
// Contrôleur testant le lien de validation du compte et envoyant un message de bienvenue si tout est ok
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.replace("#URL", configTpl.connectionPage)] });
else
{
const now=new Date();
await Promise.all([
db["Subscription"].create({ numberOfDays: config.freeAccountTimingInDays, receiptDays: config.defaultReceiptDays, UserId: datas.User.id }),
db["User"].update({ connectedAt: now }, { where: { id : datas.User.id }, limit:1 })
]);
const newUser=await 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);
res.status(200).json(
{
newUser: newUser,
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);
}
}
// Contrôleur recevant les données envoyées par l'utilisateur lorsqu'il lui est demandé de complèter ses informations, juste après avoir validé son abonnement
// Il peut fournir ici un pseudo et un mot de passe de son choix ou encore un code parrain
// Il peut asussi choisir les jours de réception de son abonnement au site
exports.signUpCompletion = async (req, res, next) =>
{
try
{
const db = require("../models/index");
if(!tool.isEmpty(req.body.newPassword) && req.body.newPassword.length < config.passwordMinLength)
res.status(400).json({ errors: txt.needLongPassWord.replace("MIN_LENGTH", config.passwordMinLength) });
else
{
if(!tool.isEmpty(req.body.newPassword))// dans ce cas, l'utilisateur n'a pas choisi de mot de passe, mais pourra se connecter via les tokens de connexion
req.body.password=await bcrypt.hash(req.body.newPassword, config.bcryptSaltRounds);
req.body.GodfatherId=null;
if(req.body.codeGodfather !== "")
{
const godfather=await searchIdGodfather(req.body.codeGodfather);
if(godfather && godfather.id !== req.connectedUser.User.id) // des fois qu'il se serait désigné lui-même comme "parrain" :)
req.body.GodfatherId=godfather.id;
}
await Promise.all([
db["User"].update({ ...req.body }, { where: { id : req.connectedUser.User.id } , fields: ["name", "password", "GodfatherId"], limit:1 }),
db["Subscription"].update({ ...req.body }, { where: { UserId : req.connectedUser.User.id }, fields: ["receiptDays"], limit:1 })
]);
const user=await creaUserJson(req.connectedUser.User.id);
// Si un parrain a été désigné, on prévient l'heureux élu :) :
if(user!==false && req.body.GodfatherId !== null)
{
const godfather=await searchUserById(req.body.GodfatherId);
if(godfather)
{
const mapMail =
{
USER_NAME: godfather.User.name,
NOM_SITE : config.siteName,
EMAIL : user.User.email,
LINK_URL : config.siteUrl+"/"+configTpl.connectionPage
};
const mailDatas=
{
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), "", mailDatas);
}
}
const messageRetour=[txt.updatedOkMessage];
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.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();
// Si abonnement illimité, pas besoin de calculer le nbre de jours restants
const nbDaysOk=(datas.Subscription.numberOfDays===0) ? 1 : 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 });
await 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 l'utilisateur a répondu à un quiz (ou groupe de quizs) avant de se connecter, on enregistre son résultat.
// Uniquement pour les utilisateurs de base.
req.body.UserId=user.id;
if(req.body.QuestionnaireId && user.status === "user")
await answerCtrl.saveAnswerToQuestionnaire(req.body);
else if(req.body.GroupId && user.status === "user")
await answerCtrl.saveAnswerToGroup(req.body);
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 l'utilisateur a répondu à un quiz (ou groupe de quizs) avant de se connecter, on enregistre son résultat :
req.body.UserId=userDatas.User.id;
if(req.body.QuestionnaireId)
await answerCtrl.saveAnswerToQuestionnaire(req.body);
else if(req.body.GroupId)
await answerCtrl.saveAnswerToGroup(req.body);
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", "timeDifference"], limit:1 }),
db["Subscription"].update({ ...req.body }, { where: { UserId : req.connectedUser.User.id }, fields: ["receiptDays"], 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(req.body.newGodfatherId!=undefined)
{
const godFather=searchIdGodfather(req.body.newGodfatherId);
if(empty(godFather))
messageRetour.push(txt.updatedNeedGoodGodfather);
else
req.body.GodfatherId=godFather.id;
}
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", "GodfatherId"], limit:1 }),
db["Subscription"].update({ ...req.body }, { where: { UserId : req.params.id }, fields: ["numberOfDays", "receiptDays"], 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", "GodfatherId"] });
req.body.UserId=user.id;
await db["Subscription"].create({ ...req.body }, { fields: ["numberOfDays", "receiptDays", "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
// Supprime les fichiers stockant les erreurs de connexion ayant expirés
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);
}
}
// Suppression des fichiers pour des utilisateurs n'existant plus
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");
saveFiles.push("stats"+users[i].id+".json");
}
const deleteFiles = await Promise.all([
toolFile.deleteFilesInDirectory(configUsers.dirCacheUsers, saveFiles),
toolFile.deleteFilesInDirectory(configUsers.dirCacheUsersWithoutAnswers, saveFiles),
toolFile.deleteFilesInDirectory(configUsers.dirCacheUsersAnswers, saveFiles)
]);
res.status(200).json(deleteFiles);
next();
}
catch(e)
{
next(e);
}
}
// Suppression des comptes n'ayant pas été validé passé un certain délai
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)/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(let i in userUnvalided)
await db["UserDeleted"].create({ createdAt: userUnvalided[i].createdAt, deletedAt: now, wasValided: false });
console.log(metadata.affectedRows+txt.cronDeleteUnvalidedUsersMessage);
}
res.status(200).json(true);
next();
}
catch(e)
{
next(e);
}
}
// Suppression des comptes dont l'abonnement a expiré et ne s'étant pas connecté depuis à certains temps
exports.deleteInactiveAccounts= async(req, res, next) =>
{
try
{
const db = require("../models/index");
const usersInactive= await db.sequelize.query("SELECT createdAt FROM `Users` WHERE status='user' AND (ADDDATE(`connectedAt`, "+config.inactiveAccountTimeToDeleteInDays+") < NOW() OR `connectedAt` IS NULL) 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() OR `connectedAt` IS NULL) AND status='user' 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(let 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;