@programisto/edrm-storage 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +135 -0
- package/dist/bin/www.d.ts +2 -0
- package/dist/bin/www.js +13 -0
- package/dist/modules/edrm-exams/lib/openai/correctQuestion.txt +9 -0
- package/dist/modules/edrm-exams/lib/openai/createQuestion.txt +6 -0
- package/dist/modules/edrm-exams/lib/openai.d.ts +37 -0
- package/dist/modules/edrm-exams/lib/openai.js +135 -0
- package/dist/modules/edrm-exams/listeners/correct.listener.d.ts +2 -0
- package/dist/modules/edrm-exams/listeners/correct.listener.js +167 -0
- package/dist/modules/edrm-exams/models/candidate.model.d.ts +21 -0
- package/dist/modules/edrm-exams/models/candidate.model.js +75 -0
- package/dist/modules/edrm-exams/models/candidate.models.d.ts +21 -0
- package/dist/modules/edrm-exams/models/candidate.models.js +75 -0
- package/dist/modules/edrm-exams/models/company.model.d.ts +8 -0
- package/dist/modules/edrm-exams/models/company.model.js +34 -0
- package/dist/modules/edrm-exams/models/contact.model.d.ts +14 -0
- package/dist/modules/edrm-exams/models/contact.model.js +60 -0
- package/dist/modules/edrm-exams/models/test-category.models.d.ts +7 -0
- package/dist/modules/edrm-exams/models/test-category.models.js +29 -0
- package/dist/modules/edrm-exams/models/test-job.model.d.ts +7 -0
- package/dist/modules/edrm-exams/models/test-job.model.js +29 -0
- package/dist/modules/edrm-exams/models/test-question.model.d.ts +25 -0
- package/dist/modules/edrm-exams/models/test-question.model.js +70 -0
- package/dist/modules/edrm-exams/models/test-result.model.d.ts +26 -0
- package/dist/modules/edrm-exams/models/test-result.model.js +70 -0
- package/dist/modules/edrm-exams/models/test.model.d.ts +47 -0
- package/dist/modules/edrm-exams/models/test.model.js +133 -0
- package/dist/modules/edrm-exams/models/user.model.d.ts +18 -0
- package/dist/modules/edrm-exams/models/user.model.js +73 -0
- package/dist/modules/edrm-exams/routes/company.router.d.ts +7 -0
- package/dist/modules/edrm-exams/routes/company.router.js +108 -0
- package/dist/modules/edrm-exams/routes/exams-candidate.router.d.ts +7 -0
- package/dist/modules/edrm-exams/routes/exams-candidate.router.js +448 -0
- package/dist/modules/edrm-exams/routes/exams.router.d.ts +8 -0
- package/dist/modules/edrm-exams/routes/exams.router.js +1343 -0
- package/dist/modules/edrm-exams/routes/result.router.d.ts +7 -0
- package/dist/modules/edrm-exams/routes/result.router.js +370 -0
- package/dist/modules/edrm-exams/routes/user.router.d.ts +7 -0
- package/dist/modules/edrm-exams/routes/user.router.js +96 -0
- package/dist/modules/edrm-storage/config/edrm-storage.config.d.ts +29 -0
- package/dist/modules/edrm-storage/config/edrm-storage.config.js +31 -0
- package/dist/modules/edrm-storage/config/environment.example.d.ts +54 -0
- package/dist/modules/edrm-storage/config/environment.example.js +130 -0
- package/dist/modules/edrm-storage/examples/usage.example.d.ts +52 -0
- package/dist/modules/edrm-storage/examples/usage.example.js +156 -0
- package/dist/modules/edrm-storage/index.d.ts +5 -0
- package/dist/modules/edrm-storage/index.js +8 -0
- package/dist/modules/edrm-storage/integration/edrm-storage-integration.d.ts +53 -0
- package/dist/modules/edrm-storage/integration/edrm-storage-integration.js +132 -0
- package/dist/modules/edrm-storage/interfaces/storage-provider.interface.d.ts +35 -0
- package/dist/modules/edrm-storage/interfaces/storage-provider.interface.js +1 -0
- package/dist/modules/edrm-storage/migrations/edrm-storage.migration.d.ts +6 -0
- package/dist/modules/edrm-storage/migrations/edrm-storage.migration.js +151 -0
- package/dist/modules/edrm-storage/models/file.model.d.ts +78 -0
- package/dist/modules/edrm-storage/models/file.model.js +190 -0
- package/dist/modules/edrm-storage/providers/s3-storage.provider.d.ts +18 -0
- package/dist/modules/edrm-storage/providers/s3-storage.provider.js +95 -0
- package/dist/modules/edrm-storage/routes/edrm-storage.router.d.ts +8 -0
- package/dist/modules/edrm-storage/routes/edrm-storage.router.js +155 -0
- package/dist/modules/edrm-storage/scripts/quick-start.d.ts +7 -0
- package/dist/modules/edrm-storage/scripts/quick-start.js +114 -0
- package/dist/modules/edrm-storage/services/edrm-storage.service.d.ts +29 -0
- package/dist/modules/edrm-storage/services/edrm-storage.service.js +188 -0
- package/dist/modules/edrm-storage/tests/edrm-storage.service.test.d.ts +1 -0
- package/dist/modules/edrm-storage/tests/edrm-storage.service.test.js +143 -0
- package/dist/modules/edrm-storage/tests/integration.test.d.ts +1 -0
- package/dist/modules/edrm-storage/tests/integration.test.js +141 -0
- package/package.json +81 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { EnduranceRouter, EnduranceAuthMiddleware, enduranceEmitter, enduranceEventTypes } from '@programisto/endurance-core';
|
|
2
|
+
import CandidateModel from '../models/candidate.model.js';
|
|
3
|
+
import TestResult, { TestState } from '../models/test-result.model.js';
|
|
4
|
+
import Test from '../models/test.model.js';
|
|
5
|
+
import TestJob from '../models/test-job.model.js';
|
|
6
|
+
// Fonction utilitaire pour récupérer le nom du job
|
|
7
|
+
async function getJobName(targetJob) {
|
|
8
|
+
// Si c'est déjà une string (ancien format), on la retourne directement
|
|
9
|
+
if (typeof targetJob === 'string') {
|
|
10
|
+
return targetJob;
|
|
11
|
+
}
|
|
12
|
+
// Si c'est un ObjectId, on récupère le job
|
|
13
|
+
if (targetJob && typeof targetJob === 'object' && targetJob._id) {
|
|
14
|
+
const job = await TestJob.findById(targetJob._id);
|
|
15
|
+
return job ? job.name : 'Job inconnu';
|
|
16
|
+
}
|
|
17
|
+
// Si c'est juste un ObjectId
|
|
18
|
+
if (targetJob && typeof targetJob === 'object' && targetJob.toString) {
|
|
19
|
+
const job = await TestJob.findById(targetJob);
|
|
20
|
+
return job ? job.name : 'Job inconnu';
|
|
21
|
+
}
|
|
22
|
+
return 'Job inconnu';
|
|
23
|
+
}
|
|
24
|
+
class ResultRouter extends EnduranceRouter {
|
|
25
|
+
constructor() {
|
|
26
|
+
super(EnduranceAuthMiddleware.getInstance());
|
|
27
|
+
}
|
|
28
|
+
setupRoutes() {
|
|
29
|
+
const authenticatedOptions = {
|
|
30
|
+
requireAuth: false,
|
|
31
|
+
permissions: []
|
|
32
|
+
};
|
|
33
|
+
// Lister tous les résultats de tests d'un candidat
|
|
34
|
+
this.get('/results/:candidateId', authenticatedOptions, async (req, res) => {
|
|
35
|
+
try {
|
|
36
|
+
const { candidateId } = req.params;
|
|
37
|
+
const page = parseInt(req.query.page) || 1;
|
|
38
|
+
const limit = parseInt(req.query.limit) || 10;
|
|
39
|
+
const skip = (page - 1) * limit;
|
|
40
|
+
const state = req.query.state || 'all';
|
|
41
|
+
const sortBy = req.query.sortBy || 'invitationDate';
|
|
42
|
+
const sortOrder = req.query.sortOrder || 'desc';
|
|
43
|
+
// Vérifier si le candidat existe
|
|
44
|
+
const candidate = await CandidateModel.findById(candidateId);
|
|
45
|
+
if (!candidate) {
|
|
46
|
+
return res.status(404).json({ message: 'Candidat non trouvé' });
|
|
47
|
+
}
|
|
48
|
+
// Construction de la requête
|
|
49
|
+
const query = { candidateId };
|
|
50
|
+
if (state !== 'all') {
|
|
51
|
+
query.state = state;
|
|
52
|
+
}
|
|
53
|
+
// Construction du tri
|
|
54
|
+
const allowedSortFields = ['invitationDate', 'state', 'score'];
|
|
55
|
+
const sortField = allowedSortFields.includes(sortBy) ? sortBy : 'invitationDate';
|
|
56
|
+
const sortOptions = {
|
|
57
|
+
[sortField]: sortOrder === 'asc' ? 1 : -1
|
|
58
|
+
};
|
|
59
|
+
const [results, total] = await Promise.all([
|
|
60
|
+
TestResult.find(query)
|
|
61
|
+
.sort(sortOptions)
|
|
62
|
+
.skip(skip)
|
|
63
|
+
.limit(limit)
|
|
64
|
+
.lean()
|
|
65
|
+
.exec(),
|
|
66
|
+
TestResult.countDocuments(query)
|
|
67
|
+
]);
|
|
68
|
+
// Récupérer les informations des tests associés
|
|
69
|
+
const testIds = results.map(result => result.testId);
|
|
70
|
+
const tests = await Test.find({ _id: { $in: testIds } }).lean();
|
|
71
|
+
const testsMap = new Map(tests.map(test => [test._id.toString(), test]));
|
|
72
|
+
// Récupérer tous les IDs de catégories utilisés dans les tests
|
|
73
|
+
const allCategoryIds = Array.from(new Set(tests.flatMap(test => (test.categories || []).map((cat) => cat.categoryId?.toString()))));
|
|
74
|
+
const TestCategory = (await import('../models/test-category.models.js')).default;
|
|
75
|
+
const categoriesDocs = await TestCategory.find({ _id: { $in: allCategoryIds } }).lean();
|
|
76
|
+
const categoriesMap = new Map(categoriesDocs.map(cat => [cat._id.toString(), cat.name]));
|
|
77
|
+
// Combiner les résultats avec les informations des tests et des catégories
|
|
78
|
+
const TestQuestion = (await import('../models/test-question.model.js')).default;
|
|
79
|
+
const resultsWithTests = await Promise.all(results.map(async (result) => {
|
|
80
|
+
const test = testsMap.get(result.testId.toString());
|
|
81
|
+
let categoriesWithNames = [];
|
|
82
|
+
let maxScore = 0;
|
|
83
|
+
if (test && test.categories) {
|
|
84
|
+
categoriesWithNames = test.categories.map((cat) => ({
|
|
85
|
+
...cat,
|
|
86
|
+
categoryName: categoriesMap.get(cat.categoryId?.toString()) || 'Catégorie inconnue'
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
if (test && test.questions && test.questions.length > 0) {
|
|
90
|
+
const questionIds = test.questions.map((q) => q.questionId || q);
|
|
91
|
+
const questions = await TestQuestion.find({ _id: { $in: questionIds } }).lean();
|
|
92
|
+
maxScore = questions.reduce((sum, q) => sum + (q.maxScore || 0), 0);
|
|
93
|
+
}
|
|
94
|
+
const { responses, ...resultWithoutResponses } = result;
|
|
95
|
+
return {
|
|
96
|
+
...resultWithoutResponses,
|
|
97
|
+
testResultId: result._id,
|
|
98
|
+
maxScore,
|
|
99
|
+
test: test
|
|
100
|
+
? {
|
|
101
|
+
title: test.title,
|
|
102
|
+
description: test.description,
|
|
103
|
+
targetJob: await getJobName(test.targetJob),
|
|
104
|
+
seniorityLevel: test.seniorityLevel,
|
|
105
|
+
categories: categoriesWithNames
|
|
106
|
+
}
|
|
107
|
+
: null
|
|
108
|
+
};
|
|
109
|
+
}));
|
|
110
|
+
const totalPages = Math.ceil(total / limit);
|
|
111
|
+
return res.json({
|
|
112
|
+
data: resultsWithTests,
|
|
113
|
+
pagination: {
|
|
114
|
+
currentPage: page,
|
|
115
|
+
totalPages,
|
|
116
|
+
totalItems: total,
|
|
117
|
+
itemsPerPage: limit,
|
|
118
|
+
hasNextPage: page < totalPages,
|
|
119
|
+
hasPreviousPage: page > 1
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
console.error('Erreur lors de la récupération des résultats :', err);
|
|
125
|
+
res.status(500).json({ message: 'Erreur interne du serveur' });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
// Obtenir les infos de base d'un test (sans les questions), avec categoryName et maxTime
|
|
129
|
+
this.get('/test/:id', authenticatedOptions, async (req, res) => {
|
|
130
|
+
try {
|
|
131
|
+
const { id } = req.params;
|
|
132
|
+
const TestCategory = (await import('../models/test-category.models.js')).default;
|
|
133
|
+
const TestQuestion = (await import('../models/test-question.model.js')).default;
|
|
134
|
+
// Récupérer le test sans les questions
|
|
135
|
+
const test = await Test.findById(id).lean();
|
|
136
|
+
if (!test) {
|
|
137
|
+
return res.status(404).json({ message: 'Test non trouvé' });
|
|
138
|
+
}
|
|
139
|
+
// Récupérer les noms des catégories
|
|
140
|
+
const categoryIds = (test.categories || []).map((cat) => cat.categoryId?.toString());
|
|
141
|
+
const categoriesDocs = await TestCategory.find({ _id: { $in: categoryIds } }).lean();
|
|
142
|
+
const categoriesMap = new Map(categoriesDocs.map(cat => [cat._id.toString(), cat.name]));
|
|
143
|
+
const categoriesWithNames = (test.categories || []).map((cat) => ({
|
|
144
|
+
...cat,
|
|
145
|
+
categoryName: categoriesMap.get(cat.categoryId?.toString()) || 'Catégorie inconnue'
|
|
146
|
+
}));
|
|
147
|
+
// Calculer la somme du temps de toutes les questions
|
|
148
|
+
const questions = await TestQuestion.find({ _id: { $in: (test.questions || []).map((q) => q.questionId) } }).lean();
|
|
149
|
+
const maxTime = questions.reduce((sum, q) => sum + (q.time || 0), 0);
|
|
150
|
+
const numberOfQuestions = questions.length;
|
|
151
|
+
// Récupérer le nom du job
|
|
152
|
+
const targetJobName = await getJobName(test.targetJob);
|
|
153
|
+
// Construire la réponse sans les questions
|
|
154
|
+
const { questions: _questions, // on retire les questions
|
|
155
|
+
...testWithoutQuestions } = test;
|
|
156
|
+
return res.json({
|
|
157
|
+
...testWithoutQuestions,
|
|
158
|
+
targetJobName,
|
|
159
|
+
categories: categoriesWithNames,
|
|
160
|
+
maxTime,
|
|
161
|
+
numberOfQuestions
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
console.error('Erreur lors de la récupération du test :', err);
|
|
166
|
+
res.status(500).json({ message: 'Erreur interne du serveur' });
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
// Obtenir l'ID de la prochaine question non répondue pour un résultat de test
|
|
170
|
+
this.get('/:id/nextQuestion', authenticatedOptions, async (req, res) => {
|
|
171
|
+
try {
|
|
172
|
+
const { id } = req.params;
|
|
173
|
+
const { currentQuestionId } = req.query;
|
|
174
|
+
// Récupérer le résultat de test
|
|
175
|
+
const result = await TestResult.findById(id);
|
|
176
|
+
if (!result) {
|
|
177
|
+
return res.status(404).json({ message: 'Résultat non trouvé' });
|
|
178
|
+
}
|
|
179
|
+
// Récupérer le test associé
|
|
180
|
+
const test = await Test.findById(result.testId).lean();
|
|
181
|
+
if (!test) {
|
|
182
|
+
return res.status(404).json({ message: 'Test non trouvé' });
|
|
183
|
+
}
|
|
184
|
+
// Liste des questions du test dans l'ordre
|
|
185
|
+
const questions = test.questions || [];
|
|
186
|
+
if (currentQuestionId) {
|
|
187
|
+
// Si on a un currentQuestionId, on cherche la question suivante dans l'ordre
|
|
188
|
+
const currentIndex = questions.findIndex(q => (q.questionId ? q.questionId.toString() : q.toString()) === currentQuestionId);
|
|
189
|
+
if (currentIndex === -1) {
|
|
190
|
+
return res.status(404).json({ message: 'Question courante non trouvée' });
|
|
191
|
+
}
|
|
192
|
+
// Si c'est la dernière question
|
|
193
|
+
if (currentIndex === questions.length - 1) {
|
|
194
|
+
// On est sur la dernière réponse, on met à jour la date de fin
|
|
195
|
+
result.endTime = new Date();
|
|
196
|
+
await result.save();
|
|
197
|
+
return res.json({ nextQuestionId: 'result' });
|
|
198
|
+
}
|
|
199
|
+
// Retourner la question suivante
|
|
200
|
+
const nextQuestion = questions[currentIndex + 1];
|
|
201
|
+
// Si c'est la première question (currentIndex === -1 avant), on met à jour la date de début
|
|
202
|
+
if (currentIndex === 0 && !result.startTime) {
|
|
203
|
+
result.startTime = new Date();
|
|
204
|
+
await result.save();
|
|
205
|
+
}
|
|
206
|
+
return res.json({
|
|
207
|
+
nextQuestionId: nextQuestion.questionId
|
|
208
|
+
? nextQuestion.questionId.toString()
|
|
209
|
+
: nextQuestion.toString()
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
// Comportement original : chercher la première question non répondue
|
|
214
|
+
const answeredIds = (result.responses || []).map((r) => r.questionId.toString());
|
|
215
|
+
let nextQuestionId = null;
|
|
216
|
+
for (const q of questions) {
|
|
217
|
+
const qid = (q.questionId ? q.questionId.toString() : q.toString());
|
|
218
|
+
if (!answeredIds.includes(qid)) {
|
|
219
|
+
nextQuestionId = qid;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (!nextQuestionId) {
|
|
224
|
+
// Plus de question à répondre, on met à jour la date de fin
|
|
225
|
+
result.endTime = new Date();
|
|
226
|
+
await result.save();
|
|
227
|
+
nextQuestionId = 'result';
|
|
228
|
+
}
|
|
229
|
+
else if (questions.length > 0 && nextQuestionId === (questions[0].questionId ? questions[0].questionId.toString() : questions[0].toString()) && !result.startTime) {
|
|
230
|
+
// Si c'est la première question, on met à jour la date de début
|
|
231
|
+
result.startTime = new Date();
|
|
232
|
+
await result.save();
|
|
233
|
+
}
|
|
234
|
+
return res.json({ nextQuestionId });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
console.error('Erreur lors de la récupération de la prochaine question :', err);
|
|
239
|
+
res.status(500).json({ message: 'Erreur interne du serveur' });
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
// Afficher une question par son ID (optionnellement vérifier la session)
|
|
243
|
+
this.get('/question/:idQuestion', authenticatedOptions, async (req, res) => {
|
|
244
|
+
try {
|
|
245
|
+
const { idQuestion } = req.params;
|
|
246
|
+
const { sessionId } = req.query;
|
|
247
|
+
const TestQuestion = (await import('../models/test-question.model.js')).default;
|
|
248
|
+
// Récupérer la question
|
|
249
|
+
const question = await TestQuestion.findById(idQuestion).lean();
|
|
250
|
+
if (!question) {
|
|
251
|
+
return res.status(404).json({ message: 'Question non trouvée' });
|
|
252
|
+
}
|
|
253
|
+
// Optionnel : vérifier que la question appartient bien au test de la session et n'a pas déjà été répondue
|
|
254
|
+
let test = null;
|
|
255
|
+
let questionPosition = -1;
|
|
256
|
+
let numberOfQuestions = 0;
|
|
257
|
+
if (sessionId) {
|
|
258
|
+
const result = await TestResult.findById(sessionId).lean();
|
|
259
|
+
if (!result) {
|
|
260
|
+
return res.status(404).json({ message: 'Session (résultat) non trouvée' });
|
|
261
|
+
}
|
|
262
|
+
test = await Test.findById(result.testId).lean();
|
|
263
|
+
if (!test) {
|
|
264
|
+
return res.status(404).json({ message: 'Test non trouvé' });
|
|
265
|
+
}
|
|
266
|
+
const questionIds = (test.questions || []).map((q) => q.questionId?.toString());
|
|
267
|
+
if (!questionIds.includes(idQuestion)) {
|
|
268
|
+
return res.status(403).json({ message: 'Question non autorisée pour cette session' });
|
|
269
|
+
}
|
|
270
|
+
// Vérifier que la question n'a pas déjà été répondue
|
|
271
|
+
const alreadyAnswered = (result.responses || []).some((r) => r.questionId?.toString() === idQuestion);
|
|
272
|
+
if (alreadyAnswered) {
|
|
273
|
+
return res.status(403).json({ message: 'Question déjà répondue pour cette session' });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
// Si pas de sessionId, on doit quand même récupérer le test pour avoir les infos
|
|
278
|
+
// Chercher dans tous les tests pour trouver celui qui contient cette question
|
|
279
|
+
const allTests = await Test.find({}).lean();
|
|
280
|
+
for (const t of allTests) {
|
|
281
|
+
const questionIds = (t.questions || []).map((q) => q.questionId?.toString());
|
|
282
|
+
if (questionIds.includes(idQuestion)) {
|
|
283
|
+
test = t;
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Calculer la position de la question et le nombre total de questions
|
|
289
|
+
if (test) {
|
|
290
|
+
numberOfQuestions = test.questions?.length || 0;
|
|
291
|
+
const questionIndex = test.questions?.findIndex((q) => q.questionId?.toString() === idQuestion);
|
|
292
|
+
questionPosition = questionIndex !== -1 ? questionIndex + 1 : -1; // +1 car les positions commencent à 1
|
|
293
|
+
}
|
|
294
|
+
return res.json({
|
|
295
|
+
question,
|
|
296
|
+
numberOfQuestions,
|
|
297
|
+
questionPosition
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
console.error('Erreur lors de la récupération de la question :', err);
|
|
302
|
+
res.status(500).json({ message: 'Erreur interne du serveur' });
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
// Enregistrer la réponse à une question pour un résultat de test
|
|
306
|
+
this.post('/response', authenticatedOptions, async (req, res) => {
|
|
307
|
+
try {
|
|
308
|
+
const { response, questionId, testResultId } = req.body;
|
|
309
|
+
// Récupérer le résultat de test
|
|
310
|
+
const result = await TestResult.findById(testResultId);
|
|
311
|
+
if (!result) {
|
|
312
|
+
return res.status(404).json({ message: 'Résultat non trouvé' });
|
|
313
|
+
}
|
|
314
|
+
// Récupérer le test associé
|
|
315
|
+
const test = await Test.findById(result.testId);
|
|
316
|
+
if (!test) {
|
|
317
|
+
return res.status(404).json({ message: 'Test non trouvé' });
|
|
318
|
+
}
|
|
319
|
+
// Vérifier que la question appartient bien au test
|
|
320
|
+
const questionIds = (test.questions || []).map((q) => q.questionId?.toString());
|
|
321
|
+
if (!questionIds.includes(questionId)) {
|
|
322
|
+
return res.status(403).json({ message: 'Question non autorisée pour ce test' });
|
|
323
|
+
}
|
|
324
|
+
// Vérifier que la question n'a pas déjà été répondue
|
|
325
|
+
const alreadyAnswered = (result.responses || []).some((r) => r.questionId?.toString() === questionId);
|
|
326
|
+
if (alreadyAnswered) {
|
|
327
|
+
return res.status(403).json({ message: 'Question déjà répondue pour cette session' });
|
|
328
|
+
}
|
|
329
|
+
// Enregistrer la réponse
|
|
330
|
+
result.responses = result.responses || [];
|
|
331
|
+
result.responses.push({
|
|
332
|
+
questionId,
|
|
333
|
+
response,
|
|
334
|
+
score: 0,
|
|
335
|
+
comment: ''
|
|
336
|
+
});
|
|
337
|
+
// Marquer explicitement le champ responses comme modifié
|
|
338
|
+
result.markModified('responses');
|
|
339
|
+
console.log('Avant sauvegarde - Responses:', result.responses);
|
|
340
|
+
// Vérifier si c'était la dernière question
|
|
341
|
+
const totalQuestions = test.questions.length;
|
|
342
|
+
const answeredQuestions = result.responses.length;
|
|
343
|
+
if (answeredQuestions === totalQuestions) {
|
|
344
|
+
result.state = TestState.Finish;
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
result.state = TestState.InProgress;
|
|
348
|
+
}
|
|
349
|
+
// Sauvegarder d'abord la réponse
|
|
350
|
+
const savedResult = await result.save();
|
|
351
|
+
console.log('Après sauvegarde - Responses:', savedResult.responses);
|
|
352
|
+
// Déclencher la correction automatique seulement après la sauvegarde
|
|
353
|
+
if (answeredQuestions === totalQuestions) {
|
|
354
|
+
await enduranceEmitter.emit(enduranceEventTypes.CORRECT_TEST, savedResult);
|
|
355
|
+
}
|
|
356
|
+
return res.status(200).json({
|
|
357
|
+
message: 'Réponse enregistrée',
|
|
358
|
+
response,
|
|
359
|
+
isLastQuestion: answeredQuestions === totalQuestions
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
console.error('Erreur lors de l\'enregistrement de la réponse :', err);
|
|
364
|
+
res.status(500).json({ message: 'Erreur interne du serveur' });
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const router = new ResultRouter();
|
|
370
|
+
export default router;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { EnduranceRouter, EnduranceAuthMiddleware } from '@programisto/endurance-core';
|
|
2
|
+
import UserExam from '../models/user.model.js';
|
|
3
|
+
class UserRouter extends EnduranceRouter {
|
|
4
|
+
constructor() {
|
|
5
|
+
super(EnduranceAuthMiddleware.getInstance());
|
|
6
|
+
}
|
|
7
|
+
setupRoutes() {
|
|
8
|
+
const authenticatedOptions = {
|
|
9
|
+
requireAuth: false,
|
|
10
|
+
permissions: []
|
|
11
|
+
};
|
|
12
|
+
// Lister tous les utilisateurs
|
|
13
|
+
this.get('/', authenticatedOptions, async (req, res) => {
|
|
14
|
+
try {
|
|
15
|
+
const users = await UserExam.find();
|
|
16
|
+
res.status(200).json({ array: users });
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
console.error('Error when retrieving users: ', err);
|
|
20
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
// Créer un utilisateur
|
|
24
|
+
this.post('/create', authenticatedOptions, async (req, res) => {
|
|
25
|
+
const { firstName, lastName, email, companyId } = req.body;
|
|
26
|
+
if (!firstName || !lastName || !email || !companyId) {
|
|
27
|
+
return res.status(400).json({ message: 'Error, firstName, lastName, email and companyId are required' });
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const newUser = new UserExam({ firstName, lastName, email, companyId });
|
|
31
|
+
await newUser.save();
|
|
32
|
+
res.status(201).json({ message: 'user created with sucess', user: newUser });
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
console.error('error when creating user : ', err);
|
|
36
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
// Obtenir un utilisateur par son ID
|
|
40
|
+
this.get('/:id', authenticatedOptions, async (req, res) => {
|
|
41
|
+
const { id } = req.params;
|
|
42
|
+
try {
|
|
43
|
+
const user = await UserExam.findById(id);
|
|
44
|
+
if (!user) {
|
|
45
|
+
return res.status(404).json({ message: 'no user founded with this id' });
|
|
46
|
+
}
|
|
47
|
+
res.status(200).json({ data: user });
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
console.error('error when geting user : ', err);
|
|
51
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
// Mettre à jour un utilisateur
|
|
55
|
+
this.put('/:id', authenticatedOptions, async (req, res) => {
|
|
56
|
+
const { id } = req.params;
|
|
57
|
+
const { firstName, lastName, email, companyId } = req.body;
|
|
58
|
+
try {
|
|
59
|
+
const user = await UserExam.findById(id);
|
|
60
|
+
if (!user) {
|
|
61
|
+
return res.status(404).json({ message: 'no user founded with this id' });
|
|
62
|
+
}
|
|
63
|
+
const updateData = {
|
|
64
|
+
firstName: firstName || user.firstName,
|
|
65
|
+
lastName: lastName || user.lastName,
|
|
66
|
+
email: email || user.email,
|
|
67
|
+
companyId: companyId || user.companyId
|
|
68
|
+
};
|
|
69
|
+
await UserExam.findByIdAndUpdate(id, updateData, { new: true });
|
|
70
|
+
const updatedUser = await UserExam.findById(id);
|
|
71
|
+
res.status(200).json({ message: 'user updated', user: updatedUser });
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
console.error('error when updating user : ', err);
|
|
75
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
// Supprimer un utilisateur
|
|
79
|
+
this.delete('/:id', authenticatedOptions, async (req, res) => {
|
|
80
|
+
const { id } = req.params;
|
|
81
|
+
try {
|
|
82
|
+
const user = await UserExam.findByIdAndDelete(id);
|
|
83
|
+
if (!user) {
|
|
84
|
+
return res.status(404).json({ message: 'no user founded with this id' });
|
|
85
|
+
}
|
|
86
|
+
res.status(200).json({ message: 'user deleted', user });
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
console.error('error when deleting user : ', err);
|
|
90
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const router = new UserRouter();
|
|
96
|
+
export default router;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface EdrmStorageConfig {
|
|
2
|
+
aws: {
|
|
3
|
+
region: string;
|
|
4
|
+
accessKeyId?: string;
|
|
5
|
+
secretAccessKey?: string;
|
|
6
|
+
bucket: string;
|
|
7
|
+
};
|
|
8
|
+
minio?: {
|
|
9
|
+
endpoint: string;
|
|
10
|
+
accessKey: string;
|
|
11
|
+
secretKey: string;
|
|
12
|
+
bucket: string;
|
|
13
|
+
useSSL: boolean;
|
|
14
|
+
};
|
|
15
|
+
general: {
|
|
16
|
+
defaultProvider: 'S3' | 'MINIO' | 'LOCAL';
|
|
17
|
+
maxFileSize: number;
|
|
18
|
+
allowedMimeTypes: string[];
|
|
19
|
+
uploadExpiresIn: number;
|
|
20
|
+
downloadExpiresIn: number;
|
|
21
|
+
};
|
|
22
|
+
events: {
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
emitFileStored: boolean;
|
|
25
|
+
emitFileDeleted: boolean;
|
|
26
|
+
emitFileAccessed: boolean;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export declare const defaultConfig: EdrmStorageConfig;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const defaultConfig = {
|
|
2
|
+
aws: {
|
|
3
|
+
region: process.env.AWS_REGION || 'us-east-1',
|
|
4
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
5
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
6
|
+
bucket: process.env.S3_BUCKET || 'edrm-storage'
|
|
7
|
+
},
|
|
8
|
+
general: {
|
|
9
|
+
defaultProvider: 'S3',
|
|
10
|
+
maxFileSize: 100 * 1024 * 1024, // 100MB
|
|
11
|
+
allowedMimeTypes: [
|
|
12
|
+
'application/pdf',
|
|
13
|
+
'application/msword',
|
|
14
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
15
|
+
'image/jpeg',
|
|
16
|
+
'image/png',
|
|
17
|
+
'image/gif',
|
|
18
|
+
'text/plain',
|
|
19
|
+
'application/zip',
|
|
20
|
+
'application/x-zip-compressed'
|
|
21
|
+
],
|
|
22
|
+
uploadExpiresIn: 3600, // 1 heure
|
|
23
|
+
downloadExpiresIn: 3600 // 1 heure
|
|
24
|
+
},
|
|
25
|
+
events: {
|
|
26
|
+
enabled: true,
|
|
27
|
+
emitFileStored: true,
|
|
28
|
+
emitFileDeleted: true,
|
|
29
|
+
emitFileAccessed: true
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration d'environnement pour le module EDRM Storage
|
|
3
|
+
* Copiez ce fichier vers .env et configurez vos valeurs
|
|
4
|
+
*/
|
|
5
|
+
export declare const EdrmStorageEnvironment: {
|
|
6
|
+
AWS: {
|
|
7
|
+
REGION: string;
|
|
8
|
+
ACCESS_KEY_ID: string;
|
|
9
|
+
SECRET_ACCESS_KEY: string;
|
|
10
|
+
BUCKET: string;
|
|
11
|
+
};
|
|
12
|
+
MINIO: {
|
|
13
|
+
ENDPOINT: string;
|
|
14
|
+
ACCESS_KEY: string;
|
|
15
|
+
SECRET_KEY: string;
|
|
16
|
+
BUCKET: string;
|
|
17
|
+
USE_SSL: boolean;
|
|
18
|
+
};
|
|
19
|
+
GENERAL: {
|
|
20
|
+
DEFAULT_PROVIDER: string;
|
|
21
|
+
MAX_FILE_SIZE: number;
|
|
22
|
+
UPLOAD_EXPIRES_IN: number;
|
|
23
|
+
DOWNLOAD_EXPIRES_IN: number;
|
|
24
|
+
ALLOWED_MIME_TYPES: string[];
|
|
25
|
+
};
|
|
26
|
+
EVENTS: {
|
|
27
|
+
ENABLED: boolean;
|
|
28
|
+
EMIT_FILE_STORED: boolean;
|
|
29
|
+
EMIT_FILE_DELETED: boolean;
|
|
30
|
+
EMIT_FILE_ACCESSED: boolean;
|
|
31
|
+
};
|
|
32
|
+
SECURITY: {
|
|
33
|
+
PRESIGNED_URL_EXPIRES_IN: number;
|
|
34
|
+
MAX_CONCURRENT_UPLOADS: number;
|
|
35
|
+
ENABLE_ANTIVIRUS: boolean;
|
|
36
|
+
ENABLE_WATERMARKING: boolean;
|
|
37
|
+
};
|
|
38
|
+
MONITORING: {
|
|
39
|
+
ENABLE_METRICS: boolean;
|
|
40
|
+
LOG_LEVEL: string;
|
|
41
|
+
ENABLE_AUDIT_LOG: boolean;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Validation de la configuration
|
|
46
|
+
*/
|
|
47
|
+
export declare function validateEdrmStorageConfig(): {
|
|
48
|
+
valid: boolean;
|
|
49
|
+
errors: string[];
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Exemple de fichier .env
|
|
53
|
+
*/
|
|
54
|
+
export declare const ENV_EXAMPLE = "\n# Configuration AWS S3\nAWS_REGION=us-east-1\nAWS_ACCESS_KEY_ID=your_access_key_here\nAWS_SECRET_ACCESS_KEY=your_secret_key_here\nS3_BUCKET=edrm-storage\n\n# Configuration MinIO (optionnel)\nMINIO_ENDPOINT=http://localhost:9000\nMINIO_ACCESS_KEY=your_minio_key\nMINIO_SECRET_KEY=your_minio_secret\nMINIO_BUCKET=edrm-storage\nMINIO_USE_SSL=false\n\n# Configuration g\u00E9n\u00E9rale\nEDRM_STORAGE_PROVIDER=S3\nEDRM_MAX_FILE_SIZE=104857600\nEDRM_UPLOAD_EXPIRES_IN=3600\nEDRM_DOWNLOAD_EXPIRES_IN=3600\nEDRM_ALLOWED_MIME_TYPES=application/pdf,image/jpeg,image/png\n\n# Configuration des \u00E9v\u00E9nements\nEDRM_EVENTS_ENABLED=true\nEDRM_EMIT_FILE_STORED=true\nEDRM_EMIT_FILE_DELETED=true\nEDRM_EMIT_FILE_ACCESSED=true\n\n# Configuration de s\u00E9curit\u00E9\nEDRM_PRESIGNED_URL_EXPIRES_IN=3600\nEDRM_MAX_CONCURRENT_UPLOADS=100\nEDRM_ENABLE_ANTIVIRUS=false\nEDRM_ENABLE_WATERMARKING=false\n\n# Configuration de monitoring\nEDRM_ENABLE_METRICS=true\nEDRM_LOG_LEVEL=info\nEDRM_ENABLE_AUDIT_LOG=false\n";
|