@programisto/edrm-exams 0.1.4 → 0.1.5
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 +9 -0
- package/dist/modules/edrm-exams/lib/openai/correctQuestion.txt +10 -0
- package/dist/modules/edrm-exams/lib/openai/createQuestion.txt +68 -0
- package/dist/modules/edrm-exams/lib/openai.d.ts +36 -0
- package/dist/modules/edrm-exams/lib/openai.js +82 -0
- package/dist/modules/edrm-exams/listeners/correct.listener.d.ts +2 -0
- package/dist/modules/edrm-exams/listeners/correct.listener.js +85 -0
- package/dist/modules/edrm-exams/models/candidate.models.d.ts +13 -0
- package/dist/modules/edrm-exams/models/candidate.models.js +59 -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/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-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 +52 -0
- package/dist/modules/edrm-exams/models/test.model.js +123 -0
- package/dist/modules/edrm-exams/models/user.model.d.ts +18 -0
- package/dist/modules/edrm-exams/models/user.model.js +64 -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 +299 -0
- package/dist/modules/edrm-exams/routes/exams.router.d.ts +7 -0
- package/dist/modules/edrm-exams/routes/exams.router.js +1012 -0
- package/dist/modules/edrm-exams/routes/result.router.d.ts +7 -0
- package/dist/modules/edrm-exams/routes/result.router.js +314 -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/package.json +73 -8
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { EnduranceRouter, EnduranceAuthMiddleware, enduranceEmitter, enduranceEventTypes } from 'endurance-core';
|
|
2
|
+
import CandidateModel from '../models/candidate.models.js';
|
|
3
|
+
import TestResult from '../models/test-result.model.js';
|
|
4
|
+
import Test from '../models/test.model.js';
|
|
5
|
+
import jwt from 'jsonwebtoken';
|
|
6
|
+
class CandidateRouter extends EnduranceRouter {
|
|
7
|
+
constructor() {
|
|
8
|
+
super(EnduranceAuthMiddleware.getInstance());
|
|
9
|
+
}
|
|
10
|
+
setupRoutes() {
|
|
11
|
+
const authenticatedOptions = {
|
|
12
|
+
requireAuth: false,
|
|
13
|
+
permissions: []
|
|
14
|
+
};
|
|
15
|
+
// Créer un nouveau candidat
|
|
16
|
+
this.post('/', authenticatedOptions, async (req, res) => {
|
|
17
|
+
const { firstName, lastName, email } = req.body;
|
|
18
|
+
console.log(req.body);
|
|
19
|
+
console.log(firstName, lastName, email);
|
|
20
|
+
if (!firstName || !lastName || !email) {
|
|
21
|
+
return res.status(400).json({ message: 'Error, firstName, lastName and email are required' });
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const newCandidate = new CandidateModel({ firstName, lastName, email });
|
|
25
|
+
await newCandidate.save();
|
|
26
|
+
res.status(201).json({ message: 'candidate created with success', candidate: newCandidate });
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
console.error('error when creating candidate : ', err);
|
|
30
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
// Lister tous les candidats
|
|
34
|
+
this.get('/', authenticatedOptions, async (req, res) => {
|
|
35
|
+
try {
|
|
36
|
+
const page = parseInt(req.query.page) || 1;
|
|
37
|
+
const limit = parseInt(req.query.limit) || 10;
|
|
38
|
+
const skip = (page - 1) * limit;
|
|
39
|
+
const search = req.query.search || '';
|
|
40
|
+
const sortBy = req.query.sortBy || 'lastName';
|
|
41
|
+
const sortOrder = req.query.sortOrder || 'asc';
|
|
42
|
+
// Construction de la requête de recherche
|
|
43
|
+
const query = {};
|
|
44
|
+
// Recherche sur firstName, lastName et email
|
|
45
|
+
if (search) {
|
|
46
|
+
query.$or = [
|
|
47
|
+
{ firstName: { $regex: search, $options: 'i' } },
|
|
48
|
+
{ lastName: { $regex: search, $options: 'i' } },
|
|
49
|
+
{ email: { $regex: search, $options: 'i' } }
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
// Construction du tri
|
|
53
|
+
const allowedSortFields = ['firstName', 'lastName', 'email'];
|
|
54
|
+
const sortField = allowedSortFields.includes(sortBy) ? sortBy : 'lastName';
|
|
55
|
+
const sortOptions = {
|
|
56
|
+
[sortField]: sortOrder === 'asc' ? 1 : -1
|
|
57
|
+
};
|
|
58
|
+
const [candidates, total] = await Promise.all([
|
|
59
|
+
CandidateModel.find(query)
|
|
60
|
+
.sort(sortOptions)
|
|
61
|
+
.skip(skip)
|
|
62
|
+
.limit(limit)
|
|
63
|
+
.exec(),
|
|
64
|
+
CandidateModel.countDocuments(query)
|
|
65
|
+
]);
|
|
66
|
+
const totalPages = Math.ceil(total / limit);
|
|
67
|
+
return res.json({
|
|
68
|
+
data: candidates,
|
|
69
|
+
pagination: {
|
|
70
|
+
currentPage: page,
|
|
71
|
+
totalPages,
|
|
72
|
+
totalItems: total,
|
|
73
|
+
itemsPerPage: limit,
|
|
74
|
+
hasNextPage: page < totalPages,
|
|
75
|
+
hasPreviousPage: page > 1
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
console.error('error when getting candidates : ', err);
|
|
81
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
// Obtenir un candidat par son ID
|
|
85
|
+
this.get('/:id', authenticatedOptions, async (req, res) => {
|
|
86
|
+
const { id } = req.params;
|
|
87
|
+
try {
|
|
88
|
+
const candidate = await CandidateModel.findById(id);
|
|
89
|
+
if (!candidate) {
|
|
90
|
+
return res.status(404).json({ message: 'no candidate found with this id' });
|
|
91
|
+
}
|
|
92
|
+
res.status(200).json({ message: 'candidate : ', data: candidate });
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
console.error('error when getting candidate : ', err);
|
|
96
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
// Obtenir un candidat par son email
|
|
100
|
+
this.get('/email/:email', authenticatedOptions, async (req, res) => {
|
|
101
|
+
try {
|
|
102
|
+
const email = req.params.email;
|
|
103
|
+
const candidate = await CandidateModel.findOne({ email });
|
|
104
|
+
if (!candidate) {
|
|
105
|
+
return res.status(404).json({ message: 'Candidat non trouvé' });
|
|
106
|
+
}
|
|
107
|
+
return res.json({
|
|
108
|
+
...candidate.toObject()
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
console.error('Erreur lors de la récupération du détail du candidat:', error);
|
|
113
|
+
res.status(500).send('Erreur interne du serveur');
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
// Générer un lien magique pour le candidat
|
|
117
|
+
this.post('/magic-link', { requireAuth: false }, async (req, res) => {
|
|
118
|
+
try {
|
|
119
|
+
const { email } = req.body;
|
|
120
|
+
if (!email) {
|
|
121
|
+
return res.status(400).json({ message: 'Email requis' });
|
|
122
|
+
}
|
|
123
|
+
const candidate = await CandidateModel.findOne({ email });
|
|
124
|
+
if (!candidate) {
|
|
125
|
+
return res.status(404).json({ message: 'Candidat non trouvé' });
|
|
126
|
+
}
|
|
127
|
+
// Générer le token JWT
|
|
128
|
+
const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes
|
|
129
|
+
const token = jwt.sign({
|
|
130
|
+
email,
|
|
131
|
+
expiresAt: expiresAt.toISOString()
|
|
132
|
+
}, process.env.JWT_SECRET || 'your-secret-key', { expiresIn: '10m' });
|
|
133
|
+
// Mettre à jour le candidat avec le token
|
|
134
|
+
candidate.magicLinkToken = token;
|
|
135
|
+
candidate.magicLinkExpiresAt = expiresAt;
|
|
136
|
+
await candidate.save();
|
|
137
|
+
// Envoyer l'email avec le lien magique
|
|
138
|
+
const magicLink = `${process.env.CANDIDATE_MAGIC_LINK}${token}`;
|
|
139
|
+
await enduranceEmitter.emit(enduranceEventTypes.SEND_EMAIL, {
|
|
140
|
+
template: 'candidate-magic-link-turing',
|
|
141
|
+
to: email,
|
|
142
|
+
from: process.env.EMAIL_USER_TURING,
|
|
143
|
+
emailUser: process.env.EMAIL_USER_TURING,
|
|
144
|
+
emailPassword: process.env.EMAIL_PASSWORD_TURING,
|
|
145
|
+
data: {
|
|
146
|
+
magicLink
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
return res.json({ message: 'Lien magique envoyé avec succès' });
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
console.error('Erreur lors de la génération du lien magique:', error);
|
|
153
|
+
res.status(500).send('Erreur interne du serveur');
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
// Vérifier et consommer le token magique
|
|
157
|
+
this.post('/verify-magic-link', { requireAuth: false }, async (req, res) => {
|
|
158
|
+
try {
|
|
159
|
+
const { token } = req.body;
|
|
160
|
+
if (!token) {
|
|
161
|
+
return res.status(400).json({ message: 'Token requis' });
|
|
162
|
+
}
|
|
163
|
+
// Vérifier le token JWT
|
|
164
|
+
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key');
|
|
165
|
+
// Vérifier si le token n'a pas expiré
|
|
166
|
+
if (new Date(decoded.expiresAt) < new Date()) {
|
|
167
|
+
return res.status(401).json({ message: 'Token expiré' });
|
|
168
|
+
}
|
|
169
|
+
// Trouver le candidat avec ce token
|
|
170
|
+
const candidate = await CandidateModel.findOne({
|
|
171
|
+
magicLinkToken: token,
|
|
172
|
+
magicLinkExpiresAt: { $gt: new Date() }
|
|
173
|
+
});
|
|
174
|
+
if (!candidate) {
|
|
175
|
+
return res.status(401).json({ message: 'Token invalide ou déjà utilisé' });
|
|
176
|
+
}
|
|
177
|
+
// Consommer le token en le supprimant
|
|
178
|
+
candidate.magicLinkToken = undefined;
|
|
179
|
+
candidate.magicLinkExpiresAt = undefined;
|
|
180
|
+
// Générer un nouveau token d'authentification valide 24h
|
|
181
|
+
const authExpiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 heures
|
|
182
|
+
const authToken = jwt.sign({
|
|
183
|
+
candidateId: candidate._id.toString(),
|
|
184
|
+
email: decoded.email,
|
|
185
|
+
type: 'candidate_auth'
|
|
186
|
+
}, process.env.JWT_SECRET || 'your-secret-key', { expiresIn: '24h' });
|
|
187
|
+
// Sauvegarder le nouveau token
|
|
188
|
+
candidate.authToken = authToken;
|
|
189
|
+
candidate.authTokenExpiresAt = authExpiresAt;
|
|
190
|
+
await candidate.save();
|
|
191
|
+
// Retourner les informations du candidat avec le nouveau token
|
|
192
|
+
return res.json({
|
|
193
|
+
message: 'Connexion réussie',
|
|
194
|
+
authToken,
|
|
195
|
+
candidate: {
|
|
196
|
+
id: candidate._id,
|
|
197
|
+
email: decoded.email,
|
|
198
|
+
firstName: candidate.firstName,
|
|
199
|
+
lastName: candidate.lastName
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
if (error instanceof jwt.JsonWebTokenError) {
|
|
205
|
+
return res.status(401).json({ message: 'Token invalide' });
|
|
206
|
+
}
|
|
207
|
+
console.error('Erreur lors de la vérification du token:', error);
|
|
208
|
+
res.status(500).send('Erreur interne du serveur');
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
// Lister tous les résultats de tests d'un candidat
|
|
212
|
+
this.get('/results/:candidateId', authenticatedOptions, async (req, res) => {
|
|
213
|
+
try {
|
|
214
|
+
const { candidateId } = req.params;
|
|
215
|
+
const page = parseInt(req.query.page) || 1;
|
|
216
|
+
const limit = parseInt(req.query.limit) || 10;
|
|
217
|
+
const skip = (page - 1) * limit;
|
|
218
|
+
const state = req.query.state || 'all';
|
|
219
|
+
const sortBy = req.query.sortBy || 'invitationDate';
|
|
220
|
+
const sortOrder = req.query.sortOrder || 'desc';
|
|
221
|
+
// Vérifier si le candidat existe
|
|
222
|
+
const candidate = await CandidateModel.findById(candidateId);
|
|
223
|
+
if (!candidate) {
|
|
224
|
+
return res.status(404).json({ message: 'Candidat non trouvé' });
|
|
225
|
+
}
|
|
226
|
+
// Construction de la requête
|
|
227
|
+
const query = { candidateId };
|
|
228
|
+
if (state !== 'all') {
|
|
229
|
+
query.state = state;
|
|
230
|
+
}
|
|
231
|
+
// Construction du tri
|
|
232
|
+
const allowedSortFields = ['invitationDate', 'state', 'score'];
|
|
233
|
+
const sortField = allowedSortFields.includes(sortBy) ? sortBy : 'invitationDate';
|
|
234
|
+
const sortOptions = {
|
|
235
|
+
[sortField]: sortOrder === 'asc' ? 1 : -1
|
|
236
|
+
};
|
|
237
|
+
const [results, total] = await Promise.all([
|
|
238
|
+
TestResult.find(query)
|
|
239
|
+
.sort(sortOptions)
|
|
240
|
+
.skip(skip)
|
|
241
|
+
.limit(limit)
|
|
242
|
+
.lean()
|
|
243
|
+
.exec(),
|
|
244
|
+
TestResult.countDocuments(query)
|
|
245
|
+
]);
|
|
246
|
+
// Récupérer les informations des tests associés
|
|
247
|
+
const testIds = results.map(result => result.testId);
|
|
248
|
+
const tests = await Test.find({ _id: { $in: testIds } }).lean();
|
|
249
|
+
const testsMap = new Map(tests.map(test => [test._id.toString(), test]));
|
|
250
|
+
// Récupérer tous les IDs de catégories utilisés dans les tests
|
|
251
|
+
const allCategoryIds = Array.from(new Set(tests.flatMap(test => (test.categories || []).map((cat) => cat.categoryId?.toString()))));
|
|
252
|
+
const TestCategory = (await import('../models/test-category.models.js')).default;
|
|
253
|
+
const categoriesDocs = await TestCategory.find({ _id: { $in: allCategoryIds } }).lean();
|
|
254
|
+
const categoriesMap = new Map(categoriesDocs.map(cat => [cat._id.toString(), cat.name]));
|
|
255
|
+
// Combiner les résultats avec les informations des tests et des catégories
|
|
256
|
+
const resultsWithTests = results.map(result => {
|
|
257
|
+
const test = testsMap.get(result.testId.toString());
|
|
258
|
+
let categoriesWithNames = [];
|
|
259
|
+
if (test && test.categories) {
|
|
260
|
+
categoriesWithNames = test.categories.map((cat) => ({
|
|
261
|
+
...cat,
|
|
262
|
+
categoryName: categoriesMap.get(cat.categoryId?.toString()) || 'Catégorie inconnue'
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
...result,
|
|
267
|
+
test: test
|
|
268
|
+
? {
|
|
269
|
+
title: test.title,
|
|
270
|
+
description: test.description,
|
|
271
|
+
targetJob: test.targetJob,
|
|
272
|
+
seniorityLevel: test.seniorityLevel,
|
|
273
|
+
categories: categoriesWithNames
|
|
274
|
+
}
|
|
275
|
+
: null
|
|
276
|
+
};
|
|
277
|
+
});
|
|
278
|
+
const totalPages = Math.ceil(total / limit);
|
|
279
|
+
return res.json({
|
|
280
|
+
data: resultsWithTests,
|
|
281
|
+
pagination: {
|
|
282
|
+
currentPage: page,
|
|
283
|
+
totalPages,
|
|
284
|
+
totalItems: total,
|
|
285
|
+
itemsPerPage: limit,
|
|
286
|
+
hasNextPage: page < totalPages,
|
|
287
|
+
hasPreviousPage: page > 1
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
console.error('Erreur lors de la récupération des résultats :', err);
|
|
293
|
+
res.status(500).json({ message: 'Erreur interne du serveur' });
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const router = new CandidateRouter();
|
|
299
|
+
export default router;
|