@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.
Files changed (34) hide show
  1. package/README.md +135 -0
  2. package/dist/bin/www.d.ts +2 -0
  3. package/dist/bin/www.js +9 -0
  4. package/dist/modules/edrm-exams/lib/openai/correctQuestion.txt +10 -0
  5. package/dist/modules/edrm-exams/lib/openai/createQuestion.txt +68 -0
  6. package/dist/modules/edrm-exams/lib/openai.d.ts +36 -0
  7. package/dist/modules/edrm-exams/lib/openai.js +82 -0
  8. package/dist/modules/edrm-exams/listeners/correct.listener.d.ts +2 -0
  9. package/dist/modules/edrm-exams/listeners/correct.listener.js +85 -0
  10. package/dist/modules/edrm-exams/models/candidate.models.d.ts +13 -0
  11. package/dist/modules/edrm-exams/models/candidate.models.js +59 -0
  12. package/dist/modules/edrm-exams/models/company.model.d.ts +8 -0
  13. package/dist/modules/edrm-exams/models/company.model.js +34 -0
  14. package/dist/modules/edrm-exams/models/test-category.models.d.ts +7 -0
  15. package/dist/modules/edrm-exams/models/test-category.models.js +29 -0
  16. package/dist/modules/edrm-exams/models/test-question.model.d.ts +25 -0
  17. package/dist/modules/edrm-exams/models/test-question.model.js +70 -0
  18. package/dist/modules/edrm-exams/models/test-result.model.d.ts +26 -0
  19. package/dist/modules/edrm-exams/models/test-result.model.js +70 -0
  20. package/dist/modules/edrm-exams/models/test.model.d.ts +52 -0
  21. package/dist/modules/edrm-exams/models/test.model.js +123 -0
  22. package/dist/modules/edrm-exams/models/user.model.d.ts +18 -0
  23. package/dist/modules/edrm-exams/models/user.model.js +64 -0
  24. package/dist/modules/edrm-exams/routes/company.router.d.ts +7 -0
  25. package/dist/modules/edrm-exams/routes/company.router.js +108 -0
  26. package/dist/modules/edrm-exams/routes/exams-candidate.router.d.ts +7 -0
  27. package/dist/modules/edrm-exams/routes/exams-candidate.router.js +299 -0
  28. package/dist/modules/edrm-exams/routes/exams.router.d.ts +7 -0
  29. package/dist/modules/edrm-exams/routes/exams.router.js +1012 -0
  30. package/dist/modules/edrm-exams/routes/result.router.d.ts +7 -0
  31. package/dist/modules/edrm-exams/routes/result.router.js +314 -0
  32. package/dist/modules/edrm-exams/routes/user.router.d.ts +7 -0
  33. package/dist/modules/edrm-exams/routes/user.router.js +96 -0
  34. 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;
@@ -0,0 +1,7 @@
1
+ import { EnduranceRouter } from 'endurance-core';
2
+ declare class ExamsRouter extends EnduranceRouter {
3
+ constructor();
4
+ setupRoutes(): void;
5
+ }
6
+ declare const router: ExamsRouter;
7
+ export default router;