Browse Source

Initial commit for publishing on gitlab

master
Fabrice PENHOËT 1 year ago
commit
85aef66e35
  1. 26
      .gitignore
  2. 5
      .sequelizerc
  3. 107
      app.js
  4. 29
      config/database.js
  5. 18
      config/illustrations.js
  6. 63
      config/instance-demo.js
  7. 65
      config/instance-example.js
  8. 63
      config/instance-prod.js
  9. 11
      config/links.js
  10. 20
      config/log4js.js
  11. 20
      config/mail.js
  12. 16
      config/main.js
  13. 56
      config/questionnaires.js
  14. 11
      config/tags.js
  15. 36
      config/users.js
  16. 273
      controllers/answer.js
  17. 142
      controllers/choice.js
  18. 237
      controllers/illustration.js
  19. 144
      controllers/link.js
  20. 129
      controllers/pause.js
  21. 132
      controllers/payment.js
  22. 179
      controllers/question.js
  23. 728
      controllers/questionnaire.js
  24. 391
      controllers/subscription.js
  25. 321
      controllers/tag.js
  26. 1184
      controllers/user.js
  27. 27
      example.env
  28. 6672
      front/package-lock.json
  29. 31
      front/package.json
  30. 50
      front/public/.htaccess
  31. 56
      front/public/404.html
  32. 1463
      front/public/JS/accountUser.app.js
  33. 1294
      front/public/JS/connection.app.js
  34. 1113
      front/public/JS/deconnection.app.js
  35. 1168
      front/public/JS/deleteValidation.app.js
  36. 1278
      front/public/JS/homeManager.app.js
  37. 1518
      front/public/JS/homeUser.app.js
  38. 864
      front/public/JS/index.app.js
  39. 1210
      front/public/JS/loginLink.app.js
  40. 872
      front/public/JS/loginlink.app.js
  41. 2070
      front/public/JS/manageQuestionnaires.app.js
  42. 1608
      front/public/JS/manageUsers.app.js
  43. 1174
      front/public/JS/newLoginValidation.app.js
  44. 11169
      front/public/JS/polyfill.app.js
  45. 1458
      front/public/JS/questionnaire.app.js
  46. 1104
      front/public/JS/quiz.app.js
  47. 1328
      front/public/JS/subscribe.app.js
  48. 1186
      front/public/JS/subscribeValidation.app.js
  49. 1235
      front/public/JS/unsubscribe.app.js
  50. 868
      front/public/JS/validation.app.js
  51. 138
      front/public/a-propos.html
  52. 79
      front/public/accueil.html
  53. 46
      front/public/aurevoir.html
  54. 168
      front/public/compte.html
  55. 85
      front/public/connexion.html
  56. 53
      front/public/contact.html
  57. 59
      front/public/credits.html
  58. 101
      front/public/donnees.html
  59. 261
      front/public/gestion-quizs.html
  60. 206
      front/public/gestion-utilisateurs.html
  61. 78
      front/public/gestion.html
  62. BIN
      front/public/img/404-notfound.png
  63. BIN
      front/public/img/android-icon-192x192.png
  64. BIN
      front/public/img/apple-icon-57x57.png
  65. BIN
      front/public/img/favicon-32x32.png
  66. BIN
      front/public/img/favicon.ico
  67. 94
      front/public/inscription.html
  68. 45
      front/public/login.html
  69. 45
      front/public/newlogin.html
  70. 2
      front/public/robots.txt
  71. 46
      front/public/sortie.html
  72. 45
      front/public/stop-mail.html
  73. 80
      front/public/themes/default/404.html
  74. 7
      front/public/themes/default/CSS/grids-responsive-min.css
  75. 286
      front/public/themes/default/CSS/layout.css
  76. 11
      front/public/themes/default/CSS/pure-min.css
  77. 352
      front/public/themes/default/CSS/wikilerni.css
  78. 161
      front/public/themes/default/a-propos.html
  79. 100
      front/public/themes/default/accueil.html
  80. 79
      front/public/themes/default/aurevoir.html
  81. 176
      front/public/themes/default/compte.html
  82. 108
      front/public/themes/default/connexion.html
  83. 266
      front/public/themes/default/gestion-quizs.html
  84. 211
      front/public/themes/default/gestion-utilisateurs.html
  85. 83
      front/public/themes/default/gestion.html
  86. 1
      front/public/themes/default/index.html
  87. 115
      front/public/themes/default/inscription.html
  88. 79
      front/public/themes/default/login.html
  89. 79
      front/public/themes/default/newlogin.html
  90. 78
      front/public/themes/default/sortie.html
  91. 78
      front/public/themes/default/stop-mail.html
  92. 79
      front/public/themes/default/validation.html
  93. 67
      front/public/themes/wikilerni/css/account-mobile.css
  94. 49
      front/public/themes/wikilerni/css/account.css
  95. 308
      front/public/themes/wikilerni/css/common-mobile.css
  96. 247
      front/public/themes/wikilerni/css/common.css
  97. 160
      front/public/themes/wikilerni/css/home-mobile.css
  98. 131
      front/public/themes/wikilerni/css/home.css
  99. 28
      front/public/themes/wikilerni/css/links-page-mobile.css
  100. 27
      front/public/themes/wikilerni/css/links-page.css

26
.gitignore

@ -0,0 +1,26 @@
# BACKEND :
datas/
logs/
temp/
node_modules/
nodemon.json
*.env
!example.env
/config/instance.*
!/config/instance-example.js
# FRONT END :
/front/node_modules/
/front/webpack.config-*.js
!/front/webpack.config.js
/front/public/img/quizs/
/front/public/JS/*/
/front/public/quiz/
/front/public/quizs/
/front/public/index.html
/front/public/CGV-CGU.html
/front/public/mentions-legales.html
/front/public/robots-*.txt
/front/public/WikiLerni-pub.asc

5
.sequelizerc

@ -0,0 +1,5 @@
const path = require('path');
module.exports = {
'config': path.resolve('config', 'database.js')
};

107
app.js

@ -0,0 +1,107 @@
require("dotenv").config();
const express = require("express");
const bodyParser = require("body-parser");
const path = require("path");
const log4js = require("log4js");
const timeInitialise = require("./middleware/initialise");
const cronRoutes = require("./routes/cron");
const userRoutes = require("./routes/user");
const userPausesRoutes = require("./routes/pause");
const userPaymentsRoutes = require("./routes/payment");
const questionnairesRoutes = require("./routes/questionnaire");
const questionsRoutes = require("./routes/question");
const choicesRoutes = require("./routes/choice");
const illustrationRoutes = require("./routes/illustration");
const linkRoutes = require("./routes/link");
const tagRoutes = require("./routes/tag");
const config = require("./config/main");
const confLog4js=require("./config/log4js");
const txt = require("./lang/"+config.adminLang+"/general");
const tool = require("./tools/main");
const app = express();
app.use(timeInitialise);
app.use((req, res, next) =>
{
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content, Accept, Content-Type, Authorization");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
next();
});
//app.use(express.static(path.join(__dirname, "front/public")));
app.use(bodyParser.json());
app.use("/api/user", userRoutes);
app.use("/api/pause", userPausesRoutes);
app.use("/api/payment", userPaymentsRoutes);
app.use("/api/questionnaire", questionnairesRoutes);
app.use("/api/questionnaire", tagRoutes);
app.use("/api/question", questionsRoutes);
app.use("/api/question", choicesRoutes);
app.use("/api/illustration", illustrationRoutes);
app.use("/api/link", linkRoutes);
app.use("/api/cron", cronRoutes);
// Évalue de la durée de la réponse (!= durée script, car fonctions asynchrones continuent). Mettre next() après réponse des contrôleurs... à contrôler !
// Capture aussi les url inconnues en retournant une erreur 404.
// Je peux aussi recevoir des messages à afficher dans les logs venant des "cron".
app.use((req, res, next) =>
{
try
{
if(res.headersSent)
{
log4js.configure(confLog4js);// ici, car pas pris en compte si je le fais avant le middleware (?).
const myLogs = log4js.getLogger(config.env);
let timeEnd=Date.now(), maxTime=config.responseTimingAlertInSeconde;
if(req.url.startsWith("/api/cron"))
maxTime=config.cronTimingAlertInSeconde;
const mapMessage =
{
SCRIPT_TIMING: timeEnd-req.timeBegin,
SCRIPT_URL: req.url
};
if(config.env === "development")
{
myLogs.info(tool.replaceAll(txt.scriptTimingInfo, mapMessage));
if(res.alerte)
myLogs.warn(res.alerte);
}
else if((timeEnd-req.timeBegin) > maxTime*1000)
{
myLogs.warn(tool.replaceAll(txt.scriptTimingAlert, mapMessage));
if(res.message)
myLogs.info(res.message);
}
next();
}
else
{
const err = new Error(txt.badUrl);
err.status=404;
next(err);
}
}
catch(e)
{
next(e);
}
});
// Capture et traitement des erreurs
app.use((err, req, res, next) =>
{
const status = (err.status) ? err.status : 500;
if(!res.headersSent)
res.status(status).json({ errors: txt.serverError });
log4js.configure(confLog4js);// même remarque que + haut
const myLogs = log4js.getLogger(config.env);
myLogs.error(txt.serverErrorAdmin, { message : err.message, url: req.url });
});
log4js.shutdown();
module.exports = app;

29
config/database.js

@ -0,0 +1,29 @@
require('dotenv').config();
module.exports =
{
"development":
{
"username": process.env.DB_USER,
"password": process.env.DB_PASS,
"database": process.env.DB_NAME,
"host": process.env.DB_HOST,
"dialect": "mysql"
},
"test":
{
"username": process.env.DB_USER,
"password": process.env.DB_PASS,
"database": process.env.DB_NAME,
"host": process.env.DB_HOST,
"dialect": "mysql"
},
"production":
{
"username": process.env.DB_USER,
"password": process.env.DB_PASS,
"database": process.env.DB_NAME,
"host": process.env.DB_HOST,
"dialect": "mysql"
}
};

18
config/illustrations.js

@ -0,0 +1,18 @@
const instance = require("./instance");
module.exports =
{
// API'routes (after "apiUrl" defined in instance.js)
illustrationsRoute: "/illustration/",
// forms : à compléter avec valeurs par défaut, etc. cf modèle
Illustration :
{
alt: { maxlength: 255 },
title: { maxlength: 255 },
caption: { maxlength: 255 },
image: { required: true, accept: instance.mimeTypesForIllustration.join(",") }
},
// files upload tempory dir
dirIllustrationsTmp : "temp",
dirIllustrations: "front/public/img/quizs"
};

63
config/instance-demo.js

@ -0,0 +1,63 @@
const users = require("./users");
const questionnaires = require("./questionnaires");
module.exports =
{
apiUrl: "https://apitest.wikilerni.com/api",
siteUrl: "https://test.wikilerni.com",
adminName: "Fabrice",
adminEmail: "dev@wikilerni.com",
senderName: "WikiLerni (démo)",
senderEmail: "bonjour@wikilerni.com",
adminLang: "fr",
theme: "wikilerni", // le thème utilisé (dans /views) pour générer les pages HTML. Contient ses propres fichiers de configuration.
availableLangs: ["fr"],// Languages in which the site is available. The first one is the default one.
siteName: "WikiLerni (démo)",
beginCodeGodfather: "WL", // case-sensitive and can't contain "@" !
cronTimingAlertInSeconde: 120, // for logs
responseTimingAlertInSeconde: 3, // idem
tokenSignupValidationTimeInHours: "48h", // https://github.com/zeit/ms
tokenLoginLinkTimeInHours: "1h",
tokenConnexionMinTimeInHours: "24h",
tokenConnexionMaxTimeInDays: "180 days",
tokenLoginChangingTimeInHours: "1h",// for email & password changing
tokenDeleteUserTimeInHours: "1h",
tokenUnsubscribeLinkTimeInDays: "7 days", // token send with subscription's emails
freeAccountTimingInDays: 15,
freeAccountExpirationNotificationInDays: 2,
accountExpirationFirstNotificationInDays: 15,
accountExpirationSecondNotificationInDays: 3,
inactiveAccountTimeToDeleteInDays: 180,
// Questionnaires:
nbQuestionsMin: 1, // minimum number of questions for the questionnaire to be publishable
nbQuestionsMax: 0, // if 0 = not maximum
nbChoicesMax: 10,
nbNewQuestionnaires: 10,// for RSS, etc.
hourGiveNewQuestionnaireBegin: 3, // in user local time
hourGiveNewQuestionnaireEnd: 8, // idem
numberNewQuestionnaireAtSameTime: 50, // for mass mailing
minSearchQuestionnaires: 3,
// Illustrations:
nbIllustrationsMin: 0,
nbIllustrationsMax: 1,
maxIllustrationSizeinOctet: 1000000,// pas contrôlé pour l'instant. À revoir.
mimeTypesForIllustration: [ "image/jpg", "image/jpeg", "image/png", "image/gif", "image/png" ],
// -- Upload and resize:
illustrationsWidthMaxInPx: 400,
illustrationsMiniaturesWidthMaxInPx: 200,
// Links:
nbLinksMin: 1,
nbLinksMax: 1,
// à supprimer quand tous les "require" à jour:
nbQuestionsMin: questionnaires.nbQuestionsMin,
nbQuestionsMax: questionnaires.nbQuestionsMax,
nbChoicesMax: questionnaires.nbChoicesMax,
passwordMinLength: users.password.minlength,
dirCacheUsers: users.dirCacheUsers,
dirCacheUsersAnswers: users.dirCacheUsersAnswers,
dirCacheQuestionnaires: questionnaires.dirCacheQuestionnaires,
dirCacheQuestions: questionnaires.dirCacheQuestions,
dirCacheUsersQuestionnaires: questionnaires.dirCacheUsersQuestionnaires,
dirHTMLQuestionnaire: questionnaires.dirHTMLQuestionnaire,
dirWebQuestionnaire: questionnaires.dirWebQuestionnaire
};

65
config/instance-example.js

@ -0,0 +1,65 @@
/// À ADAPTER ET RENOMMER : instance.js.
const users = require("./users");
const questionnaires = require("./questionnaires");
module.exports =
{
apiUrl: "https://...",
siteUrl: "https://...",
adminName: "bob",
adminEmail: "bob@example.tld",
senderName: "bob",
senderEmail: "bob@example.tld",
adminLang: "fr",
theme: "wikilerni", // le thème utilisé (dans /views) pour générer les pages HTML. Contient ses propres fichiers de configuration.
availableLangs: ["fr"],// Languages in which the site is available. The first one is the default one.
siteName: "WikiLerni",
beginCodeGodfather: "WL", // case-sensitive and can't contain "@" !
cronTimingAlertInSeconde: 120, // for logs
responseTimingAlertInSeconde: 3, // idem
tokenSignupValidationTimeInHours: "48h", // see : https://github.com/zeit/ms
tokenLoginLinkTimeInHours: "1h",
tokenConnexionMinTimeInHours: "24h",
tokenConnexionMaxTimeInDays: "180 days",
tokenLoginChangingTimeInHours: "1h",// for email & password changing
tokenDeleteUserTimeInHours: "1h",
tokenUnsubscribeLinkTimeInDays: "7 days", // token send with subscription's emails
freeAccountTimingInDays: 15,
freeAccountExpirationNotificationInDays: 3,
accountExpirationFirstNotificationInDays: 10,
accountExpirationSecondNotificationInDays: 3,
inactiveAccountTimeToDeleteInDays: 180,
// Questionnaires:
nbQuestionsMin: 1, // minimum number of questions for the questionnaire to be publishable
nbQuestionsMax: 0, // if 0 = not maximum
nbChoicesMax: 10,
nbNewQuestionnaires: 10, // for RSS, etc.
hourGiveNewQuestionnaireBegin: 3, // in user local time
hourGiveNewQuestionnaireEnd: 8, // idem
numberNewQuestionnaireAtSameTime: 50, // for mass mailing sending new quiz
minSearchQuestionnaires: 3,
// Illustrations:
nbIllustrationsMin: 0,
nbIllustrationsMax: 1,
maxIllustrationSizeinOctet: 1000000,// Not checked yet. To be continued.
mimeTypesForIllustration: [ "image/jpg", "image/jpeg", "image/png", "image/gif", "image/png" ],
// -- Upload and resize:
illustrationsWidthMaxInPx: 400,
illustrationsMiniaturesWidthMaxInPx: 200,
// Links:
nbLinksMin: 1,
nbLinksMax: 1,
// à supprimer quand tous les "require" à jour:
nbQuestionsMin: questionnaires.nbQuestionsMin,
nbQuestionsMax: questionnaires.nbQuestionsMax,
nbChoicesMax: questionnaires.nbChoicesMax,
passwordMinLength: users.password.minlength,
dirCacheUsers: users.dirCacheUsers,
dirCacheUsersAnswers: users.dirCacheUsersAnswers,
dirCacheQuestionnaires: questionnaires.dirCacheQuestionnaires,
dirCacheQuestions: questionnaires.dirCacheQuestions,
dirCacheUsersQuestionnaires: questionnaires.dirCacheUsersQuestionnaires,
dirHTMLQuestionnaire: questionnaires.dirHTMLQuestionnaire,
dirWebQuestionnaire: questionnaires.dirWebQuestionnaire
};

63
config/instance-prod.js

@ -0,0 +1,63 @@
const users = require("./users");
const questionnaires = require("./questionnaires");
module.exports =
{
apiUrl: "https://api.wikilerni.com/api",
siteUrl: "https://www.wikilerni.com",
adminName: "Fab",
adminEmail: "dev@wikilerni.com",
senderName: "WikiLerni",
senderEmail: "bonjour@wikilerni.com",
adminLang: "fr",
theme: "wikilerni", // le thème utilisé (dans /views) pour générer les pages HTML. Contient ses propres fichiers de configuration.
availableLangs: ["fr"],// Languages in which the site is available. The first one is the default one.
siteName: "WikiLerni",
beginCodeGodfather: "WL", // case-sensitive and can't contain "@" !
cronTimingAlertInSeconde: 120, // for logs
responseTimingAlertInSeconde: 3, // idem
tokenSignupValidationTimeInHours: "48h", // https://github.com/zeit/ms
tokenLoginLinkTimeInHours: "1h",
tokenConnexionMinTimeInHours: "24h",
tokenConnexionMaxTimeInDays: "180 days",
tokenLoginChangingTimeInHours: "1h",// for email & password changing
tokenDeleteUserTimeInHours: "1h",
tokenUnsubscribeLinkTimeInDays: "7 days", // token send with subscription's emails
freeAccountTimingInDays: 15,
freeAccountExpirationNotificationInDays: 3,
accountExpirationFirstNotificationInDays: 10,
accountExpirationSecondNotificationInDays: 3,
inactiveAccountTimeToDeleteInDays: 180,
// Questionnaires:
nbQuestionsMin: 1, // minimum number of questions for the questionnaire to be publishable
nbQuestionsMax: 0, // if 0 = not maximum
nbChoicesMax: 10,
nbNewQuestionnaires: 10,// for RSS, etc.
hourGiveNewQuestionnaireBegin: 3, // in user local time
hourGiveNewQuestionnaireEnd: 8, // idem
numberNewQuestionnaireAtSameTime: 50, // for mass mailing
minSearchQuestionnaires: 3,
// Illustrations:
nbIllustrationsMin: 0,
nbIllustrationsMax: 1,
maxIllustrationSizeinOctet: 1000000,// pas contrôlé pour l'instant. À revoir.
mimeTypesForIllustration: [ "image/jpg", "image/jpeg", "image/png", "image/gif", "image/png" ],
// -- Upload and resize:
illustrationsWidthMaxInPx: 400,
illustrationsMiniaturesWidthMaxInPx: 200,
// Links:
nbLinksMin: 1,
nbLinksMax: 1,
// à supprimer quand tous les "require" à jour:
nbQuestionsMin: questionnaires.nbQuestionsMin,
nbQuestionsMax: questionnaires.nbQuestionsMax,
nbChoicesMax: questionnaires.nbChoicesMax,
passwordMinLength: users.password.minlength,
dirCacheUsers: users.dirCacheUsers,
dirCacheUsersAnswers: users.dirCacheUsersAnswers,
dirCacheQuestionnaires: questionnaires.dirCacheQuestionnaires,
dirCacheQuestions: questionnaires.dirCacheQuestions,
dirCacheUsersQuestionnaires: questionnaires.dirCacheUsersQuestionnaires,
dirHTMLQuestionnaire: questionnaires.dirHTMLQuestionnaire,
dirWebQuestionnaire: questionnaires.dirWebQuestionnaire
};

11
config/links.js

@ -0,0 +1,11 @@
module.exports =
{
// API'routes (after "apiUrl" defined in instance.js)
linksRoute: "/link/",
// forms : à compléter avec valeurs par défaut, etc. cf modèle
Link :
{
url: { maxlength: 255, required: true },
anchor: { maxlength: 150, required: true }
}
};

20
config/log4js.js

@ -0,0 +1,20 @@
module.exports =
{
"appenders":
{
"fileLogs":
{
"type": "dateFile",
"filename": "logs/day.log",
"alwaysIncludePattern" : true,
"numBackups": 7,
"keepFileExt": true
},
"console": { "type": "console" }
},
"categories":
{
"production": { "appenders": ["fileLogs"], "level": "trace" },
"default": { "appenders": [ "console" ], "level": "trace" }
}
}

20
config/mail.js

@ -0,0 +1,20 @@
require('dotenv').config();
const instance = require("./instance");
module.exports =
{
"SMTP" :
{
"names": process.env.SMTP_NAMES.split(","),
"hosts": process.env.SMTP_HOSTS.split(","),
"ports": process.env.SMTP_PORTS.split(","),
"secures" : process.env.SMTP_SECURES.split(","),
"logins" : process.env.SMTP_LOGINS.split(","),
"passwords" : process.env.SMTP_PASSWORDS.split(",")
},
"SENDER" :
{
"name" : instance.senderName,
"email" : instance.senderEmail
}
};

16
config/main.js

@ -0,0 +1,16 @@
require('dotenv').config();
const instance = require("./instance");
instance.env=process.env.NODE_ENV;
instance.bcryptSaltRounds=parseInt(process.env.BCRYPT_SALT_ROUNDS,10);
instance.cronToken=process.env.CRON_TOKEN;
instance.tokenPrivateKey=process.env.TOKEN_PRIVATE_KEY;
instance.maxLoginFail=parseInt(process.env.MAX_LOGIN_FAILS,10);
instance.loginFailTimeInMinutes=parseInt(process.env.LOGIN_FAIL_TIME_IN_MINUTES,10);
instance.dirCache="datas";
instance.dirHTML="front/public";
instance.dirTmp="datas/tmp";
instance.dirTmpLogin="datas/tmp/logins";
module.exports = instance;

56
config/questionnaires.js

@ -0,0 +1,56 @@
module.exports =
{
// API'routes (after "apiUrl" defined in instance.js)
questionnaireRoutes: "/questionnaire",
getQuestionnaireRoutes: "/get",
previewQuestionnaireRoutes: "/preview",
publishedQuestionnaireRoutes: "/quiz/",
saveAnswersRoute: "/answer/",
getStatsQuestionnaires : "/stats/",
searchQuestionnairesRoute : "/search",
getRandomQuestionnairesRoute : "/getrandom",
searchAdminQuestionnairesRoute : "/searchadmin",
getListNextQuestionnaires: "/getlistnextquestionnaires/",
regenerateHTML: "/htmlregenerated",
// -- questions & choices :
questionsRoute: "/question/",
// -- tags :
tagsSearchRoute: "/tags/search/",
// -- answers :
getQuestionnairesWithoutAnswer: "/withoutanswer/user/",
getPreviousAnswers: "/user/answers/",
getStatsAnswers : "/user/anwswers/stats/",
getAdminStats: "/getadminstats/",
// forms : à compléter avec valeurs par défaut, etc. cf modèle
Questionnaire :
{
title: { maxlength: 255, required: true },
slug: { maxlength: 150 }, // champ requis mais calculé à partir du titre qd vide
introduction: { required: true }
},
searchQuestionnaires : { minlength: 3, required: true },
Question :
{
text: { maxlength: 255, required: true },
rank: { required: true, min:1, defaultValue:1 }
},
Choice :
{
text: { maxlength: 255, required: true }
},
nbQuestionsMin: 1,
nbQuestionsMax: 0,
nbChoicesMax: 10,
nbTagsMin: 0,
nbTagsMax: 0, // 0 = not max
// JSON and HTML dir
dirCacheQuestionnaires : "datas/questionnaires",
dirCacheQuestions : "datas/questionnaires/questions",
dirCacheUsersQuestionnaires : "datas/users/questionnaires",
dirCacheTags : "datas/questionnaires/tags",
dirHTMLQuestionnaire : "front/public/quiz",
dirHTMLTags : "front/public/quizs",
dirWebQuestionnaire : "quiz",//pour url page
dirWebTags : "quizs",// idem
nbRandomResults : 3// limite les résultat du moteur de recherche quand demande de résultats au hasard
};

11
config/tags.js

@ -0,0 +1,11 @@
// fichier à supprimer une fois tous les "require" ok
const questionnaires = require("./questionnaires");
module.exports =
{
dirCacheTags : questionnaires.dirCacheTags,
dirHTMLTags : questionnaires.dirHTMLTags,
dirWebTags : questionnaires.dirWebTags,
nbTagsMin: questionnaires.nbTagsMin,
nbTagsMax: questionnaires.nbTagsMax
};

36
config/users.js

@ -0,0 +1,36 @@
module.exports =
{
// API'routes (after "apiUrl" defined in instance.js)
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/",
getAdminStats: "/getadminstats/",
// forms : à compléter avec valeurs par défaut, etc. cf modèle
name: { maxlength: 70, required: true },
email: { maxlength: 255, required: true },
password: { minlength: 8, maxlength:72, required: true }, // https://www.npmjs.com/package/bcrypt#security-issues-and-concerns
newPassword: { minlength: 8, maxlength:72 },
codeGodfather: { maxlength: 255 },
cguOk: { value: "true", required: true },
timeDifferenceMin: -720,
timeDifferenceMax:840,
// JSON dir
dirCacheUsers : "datas/users",
dirCacheUsersAnswers : "datas/users/questionnaires/answers"
};

273
controllers/answer.js

@ -0,0 +1,273 @@
const { QueryTypes } = require("sequelize");
const config = require("../config/main.js");
const configTpl = require("../views/"+config.theme+"/config/"+config.availableLangs[0]+".js");
const tool = require("../tools/main");
const toolFile = require("../tools/file");
const subscriptionCtrl = require("./subscription");
const questionnaireCtrl = require("./questionnaire");
const txt = require("../lang/"+config.adminLang+"/answer");
const txtGeneral = require("../lang/"+config.adminLang+"/general");
// Enregistrement d'une réponse à un questionnaire
exports.create = async (req, res, next) =>
{
try
{
const db = require("../models/index");
const checkQuestionnaireAccess=await subscriptionCtrl.checkQuestionnaireAccess(req.connectedUser.User.id, req.body.QuestionnaireId);
req.body.UserId=req.connectedUser.User.id;
if(checkQuestionnaireAccess) // l'utilisateur a déjà accès à ce questionnaire
await db["Answer"].create({ ...req.body }, { fields: ["nbQuestions", "nbCorrectAnswers", "duration", "QuestionnaireId", "UserId"] });
else
{
await Promise.all([
db["QuestionnaireAccess"].create({ ...req.body }, { fields: ["QuestionnaireId", "UserId"] }),
db["Answer"].create({ ...req.body }, { fields: ["nbQuestions", "nbCorrectAnswers", "duration", "QuestionnaireId", "UserId"] })
]);
}
// j'en profite pour remettre les pendules à l'heure !
db["User"].update({ timeDifference: req.body.timeDifference }, { where: { id : req.connectedUser.User.id }, limit:1 });
creaUserStatsAnwsersJson(req.body.UserId);
creaUserQuestionnairesWithoutAnswerJson(req.body.UserId);
creaUserAnswersJson(req.body.UserId);
res.status(201).json({ message: txt.responseSavedMessage });
next();
}
catch(e)
{ // à priori, l'utilisateur ne peut pas avoir envoyé de données incorrectes, donc erreur application pour admin
next(e);
}
}
// Retourne les réponses d'un utilisateur pour un questionnaire donné
// Si fichier réponses devient trop gros, passer par sql ?
exports.getAnswersByQuestionnaire = async(req, res, next) =>
{
try
{
const answers=await getUserAnswersByQuestionnaire(req.params.userId, req.params.questionnaireId);
res.status(200).json(answers);
next();
}
catch(e)
{
next(e);
}
}
// Retourne les statistiques de l'utilisateur
exports.getStatsByUser = async(req, res, next) =>
{
try
{
const stats=await getUserStatsAnswers(req.params.userId);
// J'ajoute les stats générales des questionnaires pour comparaison :
stats.general=await questionnaireCtrl.getStatsQuestionnaires();
res.status(200).json(stats);
}
catch(e)
{
next(e);
}
}
// Retourne la liste des questionnaires auxquels un utilisateur a accès, mais n'a pas répondu
// Ils sont listés par ordre de fraîcheur, les + récents étant en début de liste
// Un questionnaire de début et un nombre de questionnaires à retourner doivent être fournis (pagination).
exports.getQuestionnairesWithouAnswerByUser = async(req, res, next) =>
{
try
{
let datas;
if(req.params.id===undefined || req.params.begin===undefined || req.params.nb===undefined)
{
const err=new Error;
err.message=txtGeneral.neededParams;
throw err;
}
else
datas=await getUserQuestionnairesWithoutAnswer(req.params.id, req.params.begin, req.params.nb);
if(datas!==false)
{
if(req.params.output!==undefined && req.params.output=="html")
{
if(datas.questionnaires.length!=0)
{
const pug = require("pug");
const striptags = require("striptags");
const txtIllustration= require("../lang/"+config.adminLang+"/illustration");
const compiledFunction = pug.compileFile("./views/"+config.theme+"/includes/listing-questionnaires.pug");
const pageDatas=
{
tool: tool,
striptags: striptags,
txtGeneral: txtGeneral,
txtIllustration: txtIllustration,
questionnaires: datas.questionnaires,
nbQuestionnairesList:configTpl.nbQuestionnairesUserHomePage
}
datas.html=await compiledFunction(pageDatas);
}
else
datas.html="";
res.status(200).json(datas);
}
else
res.status(200).json(datas);
}
else
res.status(404).json(txtQuestionnaire.notFound);
next();
}
catch(e)
{
next(e);
}
}
// FONCTIONS UTILITAIRES
// Créer la liste des réponses d'un utilisateur
const creaUserAnswersJson = async (UserId) =>
{
const db = require("../models/index");
const userAnswers=await db.sequelize.query("SELECT `QuestionnaireId`,`nbQuestions`,`nbCorrectAnswers`,`duration`,`createdAt` FROM `Answers` WHERE `UserId`=:id ORDER BY `QuestionnaireId` DESC, `createdAt` DESC", { replacements: { id: UserId }, type: QueryTypes.SELECT });
if(userAnswers)
{
await toolFile.createJSON(config.dirCacheUsersAnswers, UserId, userAnswers);// à surveiller car fichier pouvant devenir gros ! mais utile pour SVG côté client
return userAnswers;
}
else
return false;
}
exports.creaUserAnswersJson = creaUserAnswersJson;
// Retourne les réponses d'un utilisateurs à un questionnaire
const getUserAnswersByQuestionnaire = async (UserId, QuestionnaireId) =>
{
let userAnswers=await toolFile.readJSON(config.dirCacheUsersAnswers, UserId);
if(!userAnswers)
userAnswers=await creaUserAnswersJson(UserId);
if(!userAnswers)
return false;
const answers=[];
for(let i in userAnswers)
{
if(userAnswers[i].QuestionnaireId==QuestionnaireId)// pas "===" car type de données pouvant être différents
answers.push(userAnswers[i]);
else if(answers.length!==0)// les réponses étant classées par QuestionnaireId, je peux sortir de la boucle
break;
}
return answers;
}
// À combien de questionnaire l'utilisateur a-t'il répondu, quelle est son résultat moyen ?
const creaUserStatsAnwsersJson = async (UserId) =>
{
const db = require("../models/index");
const getUserAnswers = await db["Answer"].findAll({ where: { UserId : UserId }, attributes: ["id"] });
const getUserQuestionnaires = await db["Answer"].findAll({ attributes: [[db.sequelize.fn('DISTINCT', db.sequelize.col('QuestionnaireId')), 'id']], where: { UserId : UserId }});
const getUserStats = await db.sequelize.query("SELECT ROUND(AVG(nbCorrectAnswers/nbQuestions) *100) as avgCorrectAnswers, ROUND(AVG(duration)) as avgDuration FROM Answers GROUP BY UserId HAVING UserId=:id", { replacements: { id: UserId }, type: QueryTypes.SELECT });
if(getUserAnswers && getUserQuestionnaires)
{
const stats =
{
nbAnswers : getUserAnswers.length,
nbQuestionnaires : getUserQuestionnaires.length
}
if(getUserStats && getUserAnswers.length!=0)
{
stats.avgCorrectAnswers=getUserStats[0].avgCorrectAnswers;
stats.avgDuration=getUserStats[0].avgDuration;
}
await toolFile.createJSON(config.dirCacheUsersAnswers, "stats"+UserId, stats);
return stats;
}
else
return false;
}
exports.creaUserStatsAnwsersJson = creaUserStatsAnwsersJson;
// Retourne les données créées par la fonction précédente
const getUserStatsAnswers = async (UserId) =>
{
let userStats=await toolFile.readJSON(config.dirCacheUsersAnswers, "stats"+UserId);
if(!userStats)
userStats=await creaUserStatsAnwsersJson(UserId);
if(!userStats)
return false;
else
return userStats;
}
// À combien de questionnaire les utilisateurs ont-ils répondu ces dernières 24 ? depuis le début ?
const getStatsAnswers = async () =>
{
const db = require("../models/index");
const getAnswers24H = await db.sequelize.query("SELECT `id` FROM `Answers` WHERE `createdAt` > ADDDATE(NOW(), -1)", { type: QueryTypes.SELECT });
const getAnswersTot = await db.sequelize.query("SELECT `id` FROM `Answers`", { type: QueryTypes.SELECT });
if(getAnswers24H && getAnswersTot)
{
const stats =
{
nbAnswers24H : getAnswers24H.length,
nbAnswersTot : getAnswersTot.length
}
return stats;
}
else
return false;
}
exports.getStatsAnswers = getStatsAnswers;
// Créer la liste des questionnaires proposés à l'utilisateur, mais auxquels il n'a pas encore répondu
const creaUserQuestionnairesWithoutAnswerJson = async (UserId) =>
{
UserId=tool.trimIfNotNull(UserId);
if(UserId===null)
return false;
const db = require("../models/index");
const userQuestionnaires=await db.sequelize.query("SELECT `QuestionnaireId` FROM `QuestionnaireAccesses` WHERE `UserId`=:id AND `QuestionnaireId` NOT IN (SELECT DISTINCT `QuestionnaireId` FROM Answers WHERE `UserId`=:id) ORDER BY `createdAt` DESC ", { replacements: { id: UserId }, type: QueryTypes.SELECT });
if(userQuestionnaires)
{
const questionnairesId=[];// les ids suffisent et allègent le fichier
for(i in userQuestionnaires)
questionnairesId.push(userQuestionnaires[i].QuestionnaireId);
await toolFile.createJSON(config.dirCacheUsersQuestionnaires+"/without", UserId, { ids: questionnairesId });
return { ids: questionnairesId };
}
else
return false;
}
exports.creaUserQuestionnairesWithoutAnswerJson = creaUserQuestionnairesWithoutAnswerJson;
// Retourne les données créées par la fonction précédente
const getUserQuestionnairesWithoutAnswer = async (UserId, begin=0, nb=10) =>
{
UserId=tool.trimIfNotNull(UserId);
if(UserId===null)
return false;
let userQuestionnaires=await toolFile.readJSON(config.dirCacheUsersQuestionnaires+"/without", UserId);
if(!userQuestionnaires)
userQuestionnaires=await creaUserQuestionnairesWithoutAnswerJson(UserId);
if(!userQuestionnaires)
return false;
let questionnaire, Questionnaires=[], i=begin;
const nbTot=userQuestionnaires.ids.length;
if(nb===0)
nb=nbTot;// peut être = 0 si l'utilisateur est à jour
while(i<nb && userQuestionnaires.ids[i])
{
questionnaire=await questionnaireCtrl.searchQuestionnaireById(userQuestionnaires.ids[i]);
if(questionnaire)
Questionnaires.push(questionnaire);
i++;
}
return { nbTot: nbTot, questionnaires: Questionnaires };
}
exports.getUserQuestionnairesWithoutAnswer = getUserQuestionnairesWithoutAnswer;

142
controllers/choice.js

@ -0,0 +1,142 @@
const config = require("../config/main.js");
const tool = require("../tools/main");
const toolError = require("../tools/error");
const questionCtrl = require("./question");
const questionnaireCtrl = require("./questionnaire");
const txt = require("../lang/"+config.adminLang+"/choice");
const txtQuestion = require("../lang/"+config.adminLang+"/question");
// J'arrive aux deux contrôleurs suivants après être passé par les contrôleurs de "question" qui leur passe la main via next()
exports.create = async (req, res, next) =>
{
try
{
const db = require("../models/index");
let question=await questionCtrl.searchQuestionById(req.body.QuestionId);
if(!question)
throw { message: txt.needQuestionForChoices+req.body.QuestionId };
let choices=[], i=0, oneIsCorrect=false;
while(!tool.isEmpty(req.body["choiceText"+i]))
{
if(req.body["choiceIsCorrect"+i]=="true")
{
isCorrect=true;
oneIsCorrect=true;
}
else
isCorrect=false;
choices.push({ text:req.body["choiceText"+i], isCorrect:isCorrect, QuestionId:req.body.QuestionId });
i++;
}
if(!oneIsCorrect)
{
questionCtrl.deleteQuestionById(req.body.QuestionId);
res.status(400).json({ errors: [txt.needOneGoodChoice] });
}
else if(choices.length < 2)
{
questionCtrl.deleteQuestionById(req.body.QuestionId);
res.status(400).json({ errors: [txt.needMinChoicesForQuestion] });
}
else if(config.nbChoicesMax!==0 && choices.length>config.nbChoicesMax)
{
questionCtrl.deleteQuestionById(req.body.QuestionId);
res.status(400).json({ errors: [txt.needMaxChoicesForQuestion+config.nbChoicesMax] });
}
else
{
for(let i in choices)
await db["Choice"].create(choices[i], { fields: ["text", "isCorrect", "QuestionId"] });
question=await questionCtrl.creaQuestionJson(req.body.QuestionId);// besoin de ces données pour la réponse
await questionnaireCtrl.creaQuestionnaireJson(req.body.QuestionnaireId,true);// pour le cache + HTML
questionnaire=await questionnaireCtrl.searchQuestionnaireById(req.body.QuestionnaireId,true);// nécessaire réaffichage après ajout
res.status(201).json({ message: txtQuestion.addOkMessage , questionnaire: questionnaire });
}
next();
}
catch(e)
{
const returnAPI=toolError.returnSequelize(e);
if(returnAPI.messages)
{
res.status(returnAPI.status).json({ errors : returnAPI.messages });
next();
}
else
next(e);
}
}
exports.modify = async (req, res, next) =>
{
try
{
const db = require("../models/index");
let question=await questionCtrl.searchQuestionById(req.params.id);
if(!question)
throw { message: txt.needQuestionForChoices+req.params.id };
let choicesUpdated=[], choicesAdded=[], i=0, isCorrect, oneIsCorrect=false;
while(!tool.isEmpty(req.body["choiceText"+i]))
{
if(req.body["choiceIsCorrect"+i]=="true")
{
isCorrect=true;
oneIsCorrect=true;
}
else
isCorrect=false;
if(!tool.isEmpty(req.body["idChoice"+i]))
choicesUpdated.push({ text:req.body["choiceText"+i], isCorrect:isCorrect, id:req.body["idChoice"+i] });
else
choicesAdded.push({ text:req.body["choiceText"+i], isCorrect:isCorrect, QuestionId:req.params.id });
i++;
}
if(!oneIsCorrect)
res.status(400).json({ errors: [txt.needOneGoodChoice] });
else if(i<2)
res.status(400).json({ errors: [txt.needMinChoicesForQuestion] });
else if(config.nbChoicesMax!==0 && i>config.nbChoicesMax)
res.status(400).json({ errors: [txt.needMaxChoicesForQuestion+config.nbChoicesMax] });
else
{
let finded=false;
for(let i in question.Choices)// = les réponses actuellement enregistrées
{
for(let j in choicesUpdated)
{
if(choicesUpdated[j].id==question.Choices[i].id)
{
finded=true;
break;
}
}
if(!finded)
await db["Choice"].destroy( { where: { id : question.Choices[i].id }, limit:1 }); // ce choix n'a pas été gardé
finded=false;
}
for(let i in choicesUpdated)
await db["Choice"].update(choicesUpdated[i], { where: { id: choicesUpdated[i].id } , fields: ["text", "isCorrect"], limit:1 });
for(let i in choicesAdded)
await db["Choice"].create(choicesAdded[i], { fields: ["text", "isCorrect", "QuestionId"] });
question=await questionCtrl.creaQuestionJson(req.params.id);// attendre pour pouvoir tout retourner
await questionnaireCtrl.creaQuestionnaireJson(req.body.QuestionnaireId,true);// pour le cache + HTML
questionnaire=await questionnaireCtrl.searchQuestionnaireById(req.body.QuestionnaireId, true);// nécessaire réaffichage après enregistrement
res.status(200).json({ message: txtQuestion.updateOkMessage , questionnaire: questionnaire });
}
next();
}
catch(e)
{
const returnAPI=toolError.returnSequelize(e);
if(returnAPI.messages)
{
res.status(returnAPI.status).json({ errors : returnAPI.messages });
next();
}
else
next(e);
}
}

237
controllers/illustration.js

@ -0,0 +1,237 @@
const sharp = require("sharp");
const config = require("../config/main.js");
const configIllustrations = require("../config/illustrations.js");
const tool = require("../tools/main");
const toolError = require("../tools/error");
const toolFile = require("../tools/file");
const questionnaireCtrl = require("./questionnaire");
const txt = require("../lang/"+config.adminLang+"/illustration");
const txtGeneral = require("../lang/"+config.adminLang+"/general");
exports.create = async (req, res, next) =>
{
try
{
const db = require("../models/index");
const illustrationDatas = await checkHasFile(req);
// Le fichier peut avoir été bloqué par multer :
if(!illustrationDatas.url)
res.status(400).json({ errors: [txt.needGoodFile] });
else
{
const questionnaire=await questionnaireCtrl.searchQuestionnaireById(illustrationDatas.QuestionnaireId);
if(!questionnaire)
{
toolFile.deleteFile(configIllustrations.dirIllustrations, illustrationDatas.url);
throw { message: txt.needQuestionnaireForIllustration };
}
else if(configIllustrations.nbIllustrationsMax!==0 && questionnaire.Illustrations.length >= configIllustrations.nbIllustrationsMax)
{
toolFile.deleteFile(configIllustrations.dirIllustrations, illustrationDatas.url);
res.status(400).json({ errors: [txt.needMaxIllustrationsForQuestionnaire] });
}
else
{
const illustration=await db["Illustration"].create({ ...illustrationDatas }, { fields: ["url", "alt", "title", "caption", "QuestionnaireId"] });
const questionnaireDatas=await questionnaireCtrl.creaQuestionnaireJson(illustrationDatas.QuestionnaireId);// me permet de retourner en réponse les infos actualisées pour les afficher
res.status(201).json({ message: txt.addedOkMessage, questionnaire: questionnaireDatas });
}
}
next();
}
catch(e)
{
const returnAPI=toolError.returnSequelize(e);
if(returnAPI.messages)
{
res.status(returnAPI.status).json({ errors : returnAPI.messages });
next();
}
else
next(e);
}
}
exports.modify = async (req, res, next) =>
{
try
{
const db = require("../models/index");
const illustration=await searchIllustrationById(req.params.id);
if(!illustration)
res.status(404).json({ errors: txt.notFound });
else
{
const questionnaire=await questionnaireCtrl.searchQuestionnaireById(illustration.QuestionnaireId);
if(!questionnaire)
{
if(illustrationDatas.url)
toolFile.deleteFile(configIllustrations.dirIllustrations, illustrationDatas.url);
throw { message: txt.needQuestionnaireForIllustration };
}
else if(req.connectedUser.User.status==="creator" && req.connectedUser.User.id!==questionnaire.Questionnaire.CreatorId)
{
if(illustrationDatas.url)
toolFile.deleteFile(configIllustrations.dirIllustrations, illustrationDatas.url);
res.status(401).json({ errors: txtGeneral.notAllowed });
}
else
{
if(!tool.isEmpty(req.body.fileError))
res.status(400).json({ errors: [req.body.fileError] });
else
{
const illustrationDatas = await checkHasFile(req);
// Lors d'une mise à jour, un nouveau fichier n'a pas forcément été envoyé ou peut avoir été bloqué par multer
// Mais si c'est le cas, on supprime l'ancien fichier :
if(illustrationDatas.url)
{
toolFile.deleteFile(configIllustrations.dirIllustrations+"/min", illustration.url);
}
await db["Illustration"].update({ ...illustrationDatas }, { where: { id : req.params.id } , fields: ["url", "alt", "title", "caption"], limit:1 });
const questionnaireDatas=await questionnaireCtrl.creaQuestionnaireJson(illustrationDatas.QuestionnaireId);// me permet de retourner en réponse les infos actualisées pour les afficher
res.status(200).json({ message: txt.updatedOkMessage, questionnaire: questionnaireDatas });
}
}
}
next();
}
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 illustration=await searchIllustrationById(req.params.id);
if(!illustration)
throw { message: txt.notFound+req.params.id };
else
{
const questionnaire=await questionnaireCtrl.searchQuestionnaireById(illustration.QuestionnaireId);
if(!questionnaire)
throw { message: txt.needQuestionnaireForIllustration };
else if(req.connectedUser.User.status==="creator" && req.connectedUser.User.id!==questionnaire.Questionnaire.CreatorId)
res.status(401).json({ errors: txtGeneral.notAllowed });
else
{
const delIllus=await deleteIllustrationById(req.params.id);
if(delIllus)
{
const questionnaireDatas=await questionnaireCtrl.creaQuestionnaireJson(illustration.QuestionnaireId);// me permet de retourner en réponse les infos actualisées pour les afficher
res.status(200).json({ message: txt.deletedOkMessage, questionnaire: questionnaireDatas });
}
else
res.status(400).json({ errors: [txtGeneral.serverError] });
}
}
next();
}
catch(e)
{
next(e);
}
}
exports.getOneIllustrationById = async (req, res, next) =>
{
try
{
const illustration=await searchIllustrationById(req.params.id);
if(illustration)
res.status(200).json(illustration);
else
res.status(404).json(null);
}
catch(e)
{
next(e);
}
}
// cron de nettoyage des fichiers illustrations n'existant plus dans la bd
exports.deleteOldFiles= async (req, res, next) =>
{
try
{
const db = require("../models/index");
const illustrations=await db["Illustration"].findAll({ attributes: ["url"] });
let saveFiles=[];
for(let i in illustrations)
saveFiles.push(illustrations[i].url);
await toolFile.deleteFilesInDirectory(configIllustrations.dirIllustrations, saveFiles);
await toolFile.deleteFilesInDirectory(configIllustrations.dirIllustrations+"/min", saveFiles);
// + le répertoire temporaire où rien ne devrait traîner :
const fileExpiration=new Date().getTime()-1000;
await toolFile.deleteOldFilesInDirectory(configIllustrations.dirIllustrationsTmp, fileExpiration);
res.status(200).json(deleteFiles);
next();
}
catch(e)
{
next(e);
}
}
// FONCTIONS UTILITAIRES
// Gère le redimensionnement de l'image si un fichier est envoyé.
// c'est multer qui vérifie dans le middleware précédent que l'image a le bon format, puis lui donne un nom (filename) si c'est ok
const checkHasFile = async (req) =>
{
if(req.file)
{ // à revoir ? : là l'image est aggrandie si + petite que demandé
await sharp(req.file.path).resize(configIllustrations.illustrationsWidthMaxInPx).toFile(configIllustrations.dirIllustrations+"/"+req.file.filename);
await sharp(req.file.path).resize(configIllustrations.illustrationsMiniaturesWidthMaxInPx).toFile(configIllustrations.dirIllustrations+"/min/"+req.file.filename);
await toolFile.deleteFile(configIllustrations.dirIllustrationsTmp, req.file.filename);
}
// La gestion du téléchargement du fichier de l'illustration fait que les données sont envoyées sous forme de chaîne de caractères (form-data), qu'il faut transformer en json
const datas=req.file ? { ...req.body, url: req.file.filename } : { ...req.body };
return datas;
}
const searchIllustrationById = async (id) =>
{
const db = require("../models/index");
const illustration=await db["Illustration"].findByPk(id);
if(illustration)
return illustration;
else
return false;
}
const deleteIllustrationById = async (id) =>
{
const db = require("../models/index");
const illustration=await searchIllustrationById(id);
if(!illustration)
throw { message: txt.notFound+id };
else
{
const nb=await db["Illustration"].destroy( { where: { id : id }, limit:1 });
if(nb===1)
{
toolFile.deleteFile(configIllustrations.dirIllustrations, illustration.url);
toolFile.deleteFile(configIllustrations.dirIllustrations+"/min", illustration.url);
questionnaireCtrl.creaQuestionnaireJson(illustration.QuestionnaireId);
}
return true;
}
}
exports.deleteIllustrationById = deleteIllustrationById;

144
controllers/link.js

@ -0,0 +1,144 @@
const config = require("../config/main.js");
const configLinks = require("../config/links.js");
const tool = require("../tools/main");
const toolError = require("../tools/error");
const questionnaireCtrl = require("./questionnaire");
const txt = require("../lang/"+config.adminLang+"/link");
const txtGeneral = require("../lang/"+config.adminLang+"/general");
exports.create = async (req, res, next) =>
{
try
{
const db = require("../models/index");
const questionnaire=await questionnaireCtrl.searchQuestionnaireById(req.body.QuestionnaireId);
if(!questionnaire)
throw { message: txt.needQuestionnaire };
else if(configLinks.nbLinksMax!==0 && questionnaire.Links.length>=configLinks.nbLinksMax)
res.status(400).json({ errors: txt.needMaxLinksForQuestionnaire });
else
{
const link=await db["Link"].create({ ...req.body }, { fields: ["url","anchor", "QuestionnaireId"] });
questionnaireCtrl.creaQuestionnaireJson(req.body.QuestionnaireId);
const questionnaireDatas=await questionnaireCtrl.creaQuestionnaireJson(req.body.QuestionnaireId);// me permet de retourner en réponse les infos actualisées pour les afficher
res.status(201).json({ message: txt.addedOkMessage, questionnaire: questionnaireDatas });
}
next();
}
catch(e)
{
const returnAPI=toolError.returnSequelize(e);
if(returnAPI.messages)
{
res.status(returnAPI.status).json({ errors : returnAPI.messages });
next();
}
else
next(e);
}
}
exports.modify = async (req, res, next) =>
{
try
{
const db = require("../models/index");
const link=await searchLinkById(req.params.id);
if(!link)
res.status(404).json({ errors: txt.notFound });
else
{
const questionnaire=await questionnaireCtrl.searchQuestionnaireById(link.QuestionnaireId);
if(!questionnaire)
throw { message: txt.needQuestionnaire };
else if(req.connectedUser.User.status==="creator" && req.connectedUser.User.id!==questionnaire.Questionnaire.CreatorId)
res.status(401).json({ errors: txtGeneral.notAllowed });
else
{
await db["Link"].update({ ...req.body }, { where: { id : req.params.id } , fields: ["url","anchor"], limit:1 });
const questionnaireDatas=await questionnaireCtrl.creaQuestionnaireJson(link.QuestionnaireId);// me permet de retourner en réponse les infos actualisées pour les afficher
res.status(200).json({ message: txt.updatedOkMessage, questionnaire: questionnaireDatas });
}
}
next();
}
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 link=await searchLinkById(req.params.id);
if(!link)
res.status(404).json({ errors: txt.notFound });
else
{
const questionnaire=await questionnaireCtrl.searchQuestionnaireById(link.QuestionnaireId);
if(!questionnaire)
throw { message: txt.needQuestionnaire };
else if(req.connectedUser.User.status==="creator" && req.connectedUser.User.id!==questionnaire.Questionnaire.CreatorId)
res.status(401).json({ errors: txtGeneral.notAllowed });
else
{
const nb=await db["Link"].destroy( { where: { id : req.params.id }, limit:1 });
if(nb===1)
{
const questionnaireDatas=await questionnaireCtrl.creaQuestionnaireJson(link.QuestionnaireId);
res.status(200).json({ message: txt.deletedOkMessage, questionnaire: questionnaireDatas });
}
else // ne devrait pas être possible, car déjà testé + haut !
throw { message: txt.needQuestionnaire };
}
}
next();
}
catch(e)
{
next(e);
}
}
exports.getOneById = async (req, res, next) =>
{
try
{
const link=await searchLinkById(req.params.id);
if(link)
res.status(200).json(link);
else
res.status(404).json(null);
next();
}
catch(e)
{
next(e);
}
}
// FONCTIONS UTILITAIRES
const searchLinkById = async (id) =>
{
const db = require("../models/index");
const link = await db["Link"].findByPk(id);
if(link)
return link;
else
return false;
}

129
controllers/pause.js

@ -0,0 +1,129 @@
const { QueryTypes } = require("sequelize");
const config = require("../config/main.js");
const tool = require("../tools/main");
const toolError = require("../tools/error");
const userCtrl=require("./user");
const txt = require("../lang/"+config.adminLang+"/pause");
const txtGeneral = require("../lang/"+config.adminLang+"/general");
exports.create = async (req, res, next) =>
{
try
{
const db = require("../models/index");
const connectedUser=req.connectedUser;
req.body.SubscriptionId=connectedUser.Subscription.id;
await db["Pause"].create({ ...req.body });
userCtrl.creaUserJson(connectedUser.User.id);
res.status(201).json({ message: txt.createdOkMessage });
next();
}
catch(e)
{
const returnAPI=toolError.returnSequelize(e);
if(returnAPI.messages)
{
res.status(returnAPI.status).json({ errors : returnAPI.messages });
next();
}
else
next(e);
}
}
exports.modify = async (req, res, next) =>
{
try
{
const db = require("../models/index");
const connectedUser=req.connectedUser;
if(!checkPauseIsOk(req.params.id, connectedUser))
res.status(404).json({ errors: txtGeneral.serverError });
else
{
await db["Pause"].update({ ...req.body }, { where: { id : req.params.id } , fields: ["name", "startingAt", "endingAt"], limit:1 }),
userCtrl.creaUserJson(connectedUser.User.id);
res.status(201).json({ message: txt.updatedOkMessage });
}
next();
}
catch(e)
{
const returnAPI=toolError.returnSequelize(e);
if(returnAPI.length!==0)
{
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(!checkPauseIsOk(req.params.id, connectedUser))
res.status(404).json({ errors: txtGeneral.serverError });
else
{
await db["Pause"].destroy({ where: { id : req.params.id }, limit:1 });
userCtrl.creaUserJson(connectedUser.User.id);
res.status(200).json({ message: txt.deletedOkMessage });
}
next();
}
catch(e)
{
next(e);
}
}
// Cron
exports.deleteOldPauses= async(req, res, next) =>
{
try