Initial commit for publishing on gitlab
This commit is contained in:
commit
85aef66e35
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@ -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
Normal file
5
.sequelizerc
Normal file
@ -0,0 +1,5 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
'config': path.resolve('config', 'database.js')
|
||||
};
|
107
app.js
Normal file
107
app.js
Normal file
@ -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
Normal file
29
config/database.js
Normal file
@ -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
Normal file
18
config/illustrations.js
Normal file
@ -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
Normal file
63
config/instance-demo.js
Normal file
@ -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
Normal file
65
config/instance-example.js
Normal file
@ -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
Normal file
63
config/instance-prod.js
Normal file
@ -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
Normal file
11
config/links.js
Normal file
@ -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
Normal file
20
config/log4js.js
Normal file
@ -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
Normal file
20
config/mail.js
Normal file
@ -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
Normal file
16
config/main.js
Normal file
@ -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
Normal file
56
config/questionnaires.js
Normal file
@ -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
Normal file
11
config/tags.js
Normal file
@ -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
Normal file
36
config/users.js
Normal file
@ -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
Normal file
273
controllers/answer.js
Normal file
@ -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
Normal file
142
controllers/choice.js
Normal file
@ -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
Normal file
237
controllers/illustration.js
Normal file
@ -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
Normal file
144
controllers/link.js
Normal file
@ -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
Normal file
129
controllers/pause.js
Normal file
@ -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);
|
||||