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;