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. // 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 { // 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 }); 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, numberOfDays: 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) 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(); 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 }); 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; 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 }); res.message=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;