@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.
Files changed (68) hide show
  1. package/README.md +135 -0
  2. package/dist/bin/www.d.ts +2 -0
  3. package/dist/bin/www.js +13 -0
  4. package/dist/modules/edrm-exams/lib/openai/correctQuestion.txt +9 -0
  5. package/dist/modules/edrm-exams/lib/openai/createQuestion.txt +6 -0
  6. package/dist/modules/edrm-exams/lib/openai.d.ts +37 -0
  7. package/dist/modules/edrm-exams/lib/openai.js +135 -0
  8. package/dist/modules/edrm-exams/listeners/correct.listener.d.ts +2 -0
  9. package/dist/modules/edrm-exams/listeners/correct.listener.js +167 -0
  10. package/dist/modules/edrm-exams/models/candidate.model.d.ts +21 -0
  11. package/dist/modules/edrm-exams/models/candidate.model.js +75 -0
  12. package/dist/modules/edrm-exams/models/candidate.models.d.ts +21 -0
  13. package/dist/modules/edrm-exams/models/candidate.models.js +75 -0
  14. package/dist/modules/edrm-exams/models/company.model.d.ts +8 -0
  15. package/dist/modules/edrm-exams/models/company.model.js +34 -0
  16. package/dist/modules/edrm-exams/models/contact.model.d.ts +14 -0
  17. package/dist/modules/edrm-exams/models/contact.model.js +60 -0
  18. package/dist/modules/edrm-exams/models/test-category.models.d.ts +7 -0
  19. package/dist/modules/edrm-exams/models/test-category.models.js +29 -0
  20. package/dist/modules/edrm-exams/models/test-job.model.d.ts +7 -0
  21. package/dist/modules/edrm-exams/models/test-job.model.js +29 -0
  22. package/dist/modules/edrm-exams/models/test-question.model.d.ts +25 -0
  23. package/dist/modules/edrm-exams/models/test-question.model.js +70 -0
  24. package/dist/modules/edrm-exams/models/test-result.model.d.ts +26 -0
  25. package/dist/modules/edrm-exams/models/test-result.model.js +70 -0
  26. package/dist/modules/edrm-exams/models/test.model.d.ts +47 -0
  27. package/dist/modules/edrm-exams/models/test.model.js +133 -0
  28. package/dist/modules/edrm-exams/models/user.model.d.ts +18 -0
  29. package/dist/modules/edrm-exams/models/user.model.js +73 -0
  30. package/dist/modules/edrm-exams/routes/company.router.d.ts +7 -0
  31. package/dist/modules/edrm-exams/routes/company.router.js +108 -0
  32. package/dist/modules/edrm-exams/routes/exams-candidate.router.d.ts +7 -0
  33. package/dist/modules/edrm-exams/routes/exams-candidate.router.js +448 -0
  34. package/dist/modules/edrm-exams/routes/exams.router.d.ts +8 -0
  35. package/dist/modules/edrm-exams/routes/exams.router.js +1343 -0
  36. package/dist/modules/edrm-exams/routes/result.router.d.ts +7 -0
  37. package/dist/modules/edrm-exams/routes/result.router.js +370 -0
  38. package/dist/modules/edrm-exams/routes/user.router.d.ts +7 -0
  39. package/dist/modules/edrm-exams/routes/user.router.js +96 -0
  40. package/dist/modules/edrm-storage/config/edrm-storage.config.d.ts +29 -0
  41. package/dist/modules/edrm-storage/config/edrm-storage.config.js +31 -0
  42. package/dist/modules/edrm-storage/config/environment.example.d.ts +54 -0
  43. package/dist/modules/edrm-storage/config/environment.example.js +130 -0
  44. package/dist/modules/edrm-storage/examples/usage.example.d.ts +52 -0
  45. package/dist/modules/edrm-storage/examples/usage.example.js +156 -0
  46. package/dist/modules/edrm-storage/index.d.ts +5 -0
  47. package/dist/modules/edrm-storage/index.js +8 -0
  48. package/dist/modules/edrm-storage/integration/edrm-storage-integration.d.ts +53 -0
  49. package/dist/modules/edrm-storage/integration/edrm-storage-integration.js +132 -0
  50. package/dist/modules/edrm-storage/interfaces/storage-provider.interface.d.ts +35 -0
  51. package/dist/modules/edrm-storage/interfaces/storage-provider.interface.js +1 -0
  52. package/dist/modules/edrm-storage/migrations/edrm-storage.migration.d.ts +6 -0
  53. package/dist/modules/edrm-storage/migrations/edrm-storage.migration.js +151 -0
  54. package/dist/modules/edrm-storage/models/file.model.d.ts +78 -0
  55. package/dist/modules/edrm-storage/models/file.model.js +190 -0
  56. package/dist/modules/edrm-storage/providers/s3-storage.provider.d.ts +18 -0
  57. package/dist/modules/edrm-storage/providers/s3-storage.provider.js +95 -0
  58. package/dist/modules/edrm-storage/routes/edrm-storage.router.d.ts +8 -0
  59. package/dist/modules/edrm-storage/routes/edrm-storage.router.js +155 -0
  60. package/dist/modules/edrm-storage/scripts/quick-start.d.ts +7 -0
  61. package/dist/modules/edrm-storage/scripts/quick-start.js +114 -0
  62. package/dist/modules/edrm-storage/services/edrm-storage.service.d.ts +29 -0
  63. package/dist/modules/edrm-storage/services/edrm-storage.service.js +188 -0
  64. package/dist/modules/edrm-storage/tests/edrm-storage.service.test.d.ts +1 -0
  65. package/dist/modules/edrm-storage/tests/edrm-storage.service.test.js +143 -0
  66. package/dist/modules/edrm-storage/tests/integration.test.d.ts +1 -0
  67. package/dist/modules/edrm-storage/tests/integration.test.js +141 -0
  68. package/package.json +81 -0
@@ -0,0 +1,73 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { EnduranceSchema, EnduranceModelType } from '@programisto/endurance-core';
11
+ import Company from './company.model.js';
12
+ var UserRole;
13
+ (function (UserRole) {
14
+ // eslint-disable-next-line no-unused-vars
15
+ UserRole["Admin"] = "admin";
16
+ // eslint-disable-next-line no-unused-vars
17
+ UserRole["Recruiter"] = "recruiter";
18
+ // eslint-disable-next-line no-unused-vars
19
+ UserRole["Candidate"] = "candidate";
20
+ })(UserRole || (UserRole = {}));
21
+ let UserExam = class UserExam extends EnduranceSchema {
22
+ firstName;
23
+ lastName;
24
+ email;
25
+ password;
26
+ role;
27
+ companyId;
28
+ static getModel() {
29
+ return UserExamModel;
30
+ }
31
+ };
32
+ __decorate([
33
+ EnduranceModelType.prop({ required: true }),
34
+ __metadata("design:type", String)
35
+ ], UserExam.prototype, "firstName", void 0);
36
+ __decorate([
37
+ EnduranceModelType.prop({ required: true }),
38
+ __metadata("design:type", String)
39
+ ], UserExam.prototype, "lastName", void 0);
40
+ __decorate([
41
+ EnduranceModelType.prop({ required: true, unique: true }),
42
+ __metadata("design:type", String)
43
+ ], UserExam.prototype, "email", void 0);
44
+ __decorate([
45
+ EnduranceModelType.prop({ required: true }),
46
+ __metadata("design:type", String)
47
+ ], UserExam.prototype, "password", void 0);
48
+ __decorate([
49
+ EnduranceModelType.prop({ required: true, enum: UserRole }),
50
+ __metadata("design:type", String)
51
+ ], UserExam.prototype, "role", void 0);
52
+ __decorate([
53
+ EnduranceModelType.prop({ ref: () => Company }),
54
+ __metadata("design:type", Object)
55
+ ], UserExam.prototype, "companyId", void 0);
56
+ UserExam = __decorate([
57
+ EnduranceModelType.modelOptions({
58
+ schemaOptions: {
59
+ collection: 'users',
60
+ timestamps: true,
61
+ toObject: { virtuals: true },
62
+ toJSON: { virtuals: true },
63
+ _id: true,
64
+ validateBeforeSave: false,
65
+ strict: false
66
+ },
67
+ options: {
68
+ allowMixed: EnduranceModelType.Severity.ALLOW
69
+ }
70
+ })
71
+ ], UserExam);
72
+ const UserExamModel = EnduranceModelType.getModelForClass(UserExam);
73
+ export default UserExamModel;
@@ -0,0 +1,7 @@
1
+ import { EnduranceRouter } from '@programisto/endurance-core';
2
+ declare class CompanyRouter extends EnduranceRouter {
3
+ constructor();
4
+ setupRoutes(): void;
5
+ }
6
+ declare const router: CompanyRouter;
7
+ export default router;
@@ -0,0 +1,108 @@
1
+ import { EnduranceRouter, EnduranceAuthMiddleware } from '@programisto/endurance-core';
2
+ import Company from '../models/company.model.js';
3
+ import UserExam from '../models/user.model.js';
4
+ class CompanyRouter extends EnduranceRouter {
5
+ constructor() {
6
+ super(EnduranceAuthMiddleware.getInstance());
7
+ }
8
+ setupRoutes() {
9
+ const authenticatedOptions = {
10
+ requireAuth: false,
11
+ permissions: []
12
+ };
13
+ // Obtenir une entreprise par son ID
14
+ this.get('/:id', authenticatedOptions, async (req, res) => {
15
+ const { id } = req.params;
16
+ try {
17
+ const company = await Company.findById(id);
18
+ if (!company) {
19
+ return res.status(404).json({ message: 'Aucune entreprise trouvée avec cet ID' });
20
+ }
21
+ res.status(200).json({ data: company });
22
+ }
23
+ catch (err) {
24
+ console.error('Erreur lors de la récupération de l\'entreprise : ', err);
25
+ res.status(500).json({ message: 'Erreur interne du serveur' });
26
+ }
27
+ });
28
+ // Créer une nouvelle entreprise
29
+ this.post('/create', authenticatedOptions, async (req, res) => {
30
+ const { name, logo } = req.body;
31
+ if (!name || !logo) {
32
+ return res.status(400).json({ message: 'Erreur, le nom et le logo sont requis' });
33
+ }
34
+ try {
35
+ const newCompany = new Company({ name, logo });
36
+ await newCompany.save();
37
+ res.status(201).json({ message: 'Entreprise créée avec succès', company: newCompany });
38
+ }
39
+ catch (err) {
40
+ console.error('Erreur lors de la création de l\'entreprise : ', err);
41
+ res.status(500).json({ message: 'Erreur interne du serveur' });
42
+ }
43
+ });
44
+ // Mettre à jour une entreprise
45
+ this.put('/:id', authenticatedOptions, async (req, res) => {
46
+ const { id } = req.params;
47
+ const { name, logo } = req.body;
48
+ try {
49
+ const company = await Company.findById(id);
50
+ if (!company) {
51
+ return res.status(404).json({ message: 'Aucune entreprise trouvée avec cet ID' });
52
+ }
53
+ const updateData = {
54
+ name: name || company.name,
55
+ logo: logo || company.logo
56
+ };
57
+ await Company.findByIdAndUpdate(id, updateData, { new: true });
58
+ const updatedCompany = await Company.findById(id);
59
+ res.status(200).json({ message: 'Entreprise mise à jour', company: updatedCompany });
60
+ }
61
+ catch (err) {
62
+ console.error('Erreur lors de la mise à jour de l\'entreprise : ', err);
63
+ res.status(500).json({ message: 'Erreur interne du serveur' });
64
+ }
65
+ });
66
+ // Supprimer une entreprise
67
+ this.delete('/:id', authenticatedOptions, async (req, res) => {
68
+ const { id } = req.params;
69
+ try {
70
+ const company = await Company.findByIdAndDelete(id);
71
+ if (!company) {
72
+ return res.status(404).json({ message: 'Aucune entreprise trouvée avec cet ID' });
73
+ }
74
+ res.status(200).json({ message: 'Entreprise supprimée', company });
75
+ }
76
+ catch (err) {
77
+ console.error('Erreur lors de la suppression de l\'entreprise : ', err);
78
+ res.status(500).json({ message: 'Erreur interne du serveur' });
79
+ }
80
+ });
81
+ // Obtenir le nombre d'utilisateurs d'une entreprise
82
+ this.get('/numberOfUser/:id', authenticatedOptions, async (req, res) => {
83
+ const { id } = req.params;
84
+ try {
85
+ const users = await UserExam.find({ companyId: id });
86
+ const numberOfUser = users.length;
87
+ res.status(200).json({ data: numberOfUser });
88
+ }
89
+ catch (err) {
90
+ console.error('Erreur lors de la récupération du nombre d\'utilisateurs : ', err);
91
+ res.status(500).json({ message: 'Erreur interne du serveur' });
92
+ }
93
+ });
94
+ // Lister toutes les entreprises
95
+ this.get('/', authenticatedOptions, async (req, res) => {
96
+ try {
97
+ const companies = await Company.find();
98
+ res.status(200).json({ array: companies });
99
+ }
100
+ catch (err) {
101
+ console.error('Erreur lors de la récupération des entreprises : ', err);
102
+ res.status(500).json({ message: 'Erreur interne du serveur' });
103
+ }
104
+ });
105
+ }
106
+ }
107
+ const router = new CompanyRouter();
108
+ export default router;
@@ -0,0 +1,7 @@
1
+ import { EnduranceRouter } from '@programisto/endurance-core';
2
+ declare class CandidateRouter extends EnduranceRouter {
3
+ constructor();
4
+ setupRoutes(): void;
5
+ }
6
+ declare const router: CandidateRouter;
7
+ export default router;
@@ -0,0 +1,448 @@
1
+ import { EnduranceRouter, EnduranceAuthMiddleware, enduranceEmitter, enduranceEventTypes } from '@programisto/endurance-core';
2
+ import CandidateModel from '../models/candidate.model.js';
3
+ import ContactModel from '../models/contact.model.js';
4
+ import TestResult from '../models/test-result.model.js';
5
+ import Test from '../models/test.model.js';
6
+ import TestJob from '../models/test-job.model.js';
7
+ import jwt from 'jsonwebtoken';
8
+ // Fonction utilitaire pour récupérer le nom du job
9
+ async function getJobName(targetJob) {
10
+ // Si c'est déjà une string (ancien format), on la retourne directement
11
+ if (typeof targetJob === 'string') {
12
+ return targetJob;
13
+ }
14
+ // Si c'est un ObjectId, on récupère le job
15
+ if (targetJob && typeof targetJob === 'object' && targetJob._id) {
16
+ const job = await TestJob.findById(targetJob._id);
17
+ return job ? job.name : 'Job inconnu';
18
+ }
19
+ // Si c'est juste un ObjectId
20
+ if (targetJob && typeof targetJob === 'object' && targetJob.toString) {
21
+ const job = await TestJob.findById(targetJob);
22
+ return job ? job.name : 'Job inconnu';
23
+ }
24
+ return 'Job inconnu';
25
+ }
26
+ class CandidateRouter extends EnduranceRouter {
27
+ constructor() {
28
+ super(EnduranceAuthMiddleware.getInstance());
29
+ }
30
+ setupRoutes() {
31
+ const authenticatedOptions = {
32
+ requireAuth: false,
33
+ permissions: []
34
+ };
35
+ // Créer un nouveau candidat
36
+ this.post('/', authenticatedOptions, async (req, res) => {
37
+ const { firstname, lastname, email, phone, linkedin, city, experienceLevel, yearsOfExperience, skills } = req.body;
38
+ console.log(req.body);
39
+ if (!firstname || !lastname || !email || !city || !skills || skills.length === 0) {
40
+ return res.status(400).json({ message: 'Error, firstname, lastname, email, city and skills are required' });
41
+ }
42
+ try {
43
+ // Vérifier si un contact existe déjà avec cette adresse email
44
+ const existingContact = await ContactModel.findOne({ email });
45
+ if (existingContact) {
46
+ // Vérifier si un candidat existe déjà avec ce contact
47
+ const existingCandidate = await CandidateModel.findOne({ contact: existingContact._id });
48
+ if (existingCandidate) {
49
+ // Contact et candidat existent déjà
50
+ return res.status(200).json({
51
+ message: 'Contact et candidat existent déjà avec cette adresse email',
52
+ status: 'EXISTING',
53
+ candidate: {
54
+ ...existingCandidate.toObject(),
55
+ contact: existingContact.toObject()
56
+ }
57
+ });
58
+ }
59
+ else {
60
+ // Le contact existe mais pas de candidat, créer le candidat
61
+ const newCandidate = new CandidateModel({
62
+ contact: existingContact._id,
63
+ experienceLevel: experienceLevel || 'JUNIOR',
64
+ yearsOfExperience: yearsOfExperience || 0,
65
+ skills
66
+ });
67
+ await newCandidate.save();
68
+ return res.status(201).json({
69
+ message: 'Candidat créé avec succès en utilisant le contact existant',
70
+ status: 'CREATED_WITH_EXISTING_CONTACT',
71
+ candidate: {
72
+ ...newCandidate.toObject(),
73
+ contact: existingContact.toObject()
74
+ }
75
+ });
76
+ }
77
+ }
78
+ // Aucun contact existant, créer le contact et le candidat
79
+ const newContact = new ContactModel({
80
+ firstname,
81
+ lastname,
82
+ email,
83
+ phone,
84
+ linkedin,
85
+ city
86
+ });
87
+ await newContact.save();
88
+ // Créer ensuite le candidat avec la référence au contact
89
+ const newCandidate = new CandidateModel({
90
+ contact: newContact._id,
91
+ experienceLevel: experienceLevel || 'JUNIOR',
92
+ yearsOfExperience: yearsOfExperience || 0,
93
+ skills
94
+ });
95
+ await newCandidate.save();
96
+ // Récupérer le candidat et le contact séparément
97
+ const candidate = await CandidateModel.findById(newCandidate._id);
98
+ const contact = await ContactModel.findById(newContact._id);
99
+ if (!candidate || !contact) {
100
+ return res.status(500).json({ message: 'Erreur lors de la récupération des données' });
101
+ }
102
+ res.status(201).json({
103
+ message: 'Contact et candidat créés avec succès',
104
+ status: 'CREATED',
105
+ candidate: {
106
+ ...candidate.toObject(),
107
+ contact: contact.toObject()
108
+ }
109
+ });
110
+ }
111
+ catch (err) {
112
+ console.error('error when creating candidate : ', err);
113
+ res.status(500).json({ message: 'Internal server error' });
114
+ }
115
+ });
116
+ // Lister tous les candidats
117
+ this.get('/', authenticatedOptions, async (req, res) => {
118
+ try {
119
+ const page = parseInt(req.query.page) || 1;
120
+ const limit = parseInt(req.query.limit) || 10;
121
+ const skip = (page - 1) * limit;
122
+ const search = req.query.search || '';
123
+ const sortBy = req.query.sortBy || 'lastname';
124
+ const sortOrder = req.query.sortOrder || 'asc';
125
+ let contactIds = [];
126
+ let total = 0;
127
+ if (search) {
128
+ // Recherche dans les contacts
129
+ const contactQuery = {
130
+ $or: [
131
+ { firstname: { $regex: search, $options: 'i' } },
132
+ { lastname: { $regex: search, $options: 'i' } },
133
+ { email: { $regex: search, $options: 'i' } }
134
+ ]
135
+ };
136
+ const contacts = await ContactModel.find(contactQuery);
137
+ contactIds = contacts.map(contact => contact._id);
138
+ // Compter les candidats avec ces contacts
139
+ total = await CandidateModel.countDocuments({ contact: { $in: contactIds } });
140
+ }
141
+ else {
142
+ // Pas de recherche, compter tous les candidats
143
+ total = await CandidateModel.countDocuments();
144
+ }
145
+ // Construction du tri pour les contacts
146
+ const allowedSortFields = ['firstname', 'lastname', 'email'];
147
+ const sortField = allowedSortFields.includes(sortBy) ? sortBy : 'lastname';
148
+ let candidates;
149
+ if (search && contactIds.length > 0) {
150
+ // Récupérer les candidats avec les contacts trouvés
151
+ candidates = await CandidateModel.find({ contact: { $in: contactIds } })
152
+ .skip(skip)
153
+ .limit(limit)
154
+ .exec();
155
+ }
156
+ else if (!search) {
157
+ // Récupérer tous les candidats
158
+ candidates = await CandidateModel.find()
159
+ .skip(skip)
160
+ .limit(limit)
161
+ .exec();
162
+ }
163
+ else {
164
+ // Aucun contact trouvé pour la recherche
165
+ candidates = [];
166
+ }
167
+ // Récupérer les contacts pour tous les candidats
168
+ const candidateContactIds = candidates.map(candidate => candidate.contact);
169
+ const contacts = await ContactModel.find({ _id: { $in: candidateContactIds } });
170
+ const contactsMap = new Map(contacts.map(contact => [contact._id.toString(), contact]));
171
+ // Combiner les candidats avec leurs contacts et trier
172
+ const candidatesWithContacts = candidates.map(candidate => {
173
+ const contact = contactsMap.get(candidate.contact.toString());
174
+ return {
175
+ ...candidate.toObject(),
176
+ contact: contact ? contact.toObject() : null
177
+ };
178
+ });
179
+ // Trier les résultats côté serveur si nécessaire
180
+ if (sortField && candidatesWithContacts.length > 0) {
181
+ candidatesWithContacts.sort((a, b) => {
182
+ const aValue = a.contact ? a.contact[sortField] : '';
183
+ const bValue = b.contact ? b.contact[sortField] : '';
184
+ if (sortOrder === 'asc') {
185
+ return aValue.localeCompare(bValue);
186
+ }
187
+ else {
188
+ return bValue.localeCompare(aValue);
189
+ }
190
+ });
191
+ }
192
+ const totalPages = Math.ceil(total / limit);
193
+ return res.json({
194
+ data: candidatesWithContacts,
195
+ pagination: {
196
+ currentPage: page,
197
+ totalPages,
198
+ totalItems: total,
199
+ itemsPerPage: limit,
200
+ hasNextPage: page < totalPages,
201
+ hasPreviousPage: page > 1
202
+ }
203
+ });
204
+ }
205
+ catch (err) {
206
+ console.error('error when getting candidates : ', err);
207
+ res.status(500).json({ message: 'Internal server error' });
208
+ }
209
+ });
210
+ // Obtenir un candidat par son ID
211
+ this.get('/:id', authenticatedOptions, async (req, res) => {
212
+ const { id } = req.params;
213
+ try {
214
+ const candidate = await CandidateModel.findById(id);
215
+ if (!candidate) {
216
+ return res.status(404).json({ message: 'no candidate found with this id' });
217
+ }
218
+ // Récupérer le contact associé
219
+ const contact = await ContactModel.findById(candidate.contact);
220
+ if (!contact) {
221
+ return res.status(404).json({ message: 'contact not found for this candidate' });
222
+ }
223
+ res.status(200).json({
224
+ message: 'candidate : ',
225
+ data: {
226
+ ...candidate.toObject(),
227
+ contact: contact.toObject()
228
+ }
229
+ });
230
+ }
231
+ catch (err) {
232
+ console.error('error when getting candidate : ', err);
233
+ res.status(500).json({ message: 'Internal server error' });
234
+ }
235
+ });
236
+ // Obtenir un candidat par son email
237
+ this.get('/email/:email', authenticatedOptions, async (req, res) => {
238
+ try {
239
+ const email = req.params.email;
240
+ // Chercher d'abord le contact par email
241
+ const contact = await ContactModel.findOne({ email });
242
+ if (!contact) {
243
+ return res.status(404).json({ message: 'Contact non trouvé' });
244
+ }
245
+ // Puis chercher le candidat avec ce contact
246
+ const candidate = await CandidateModel.findOne({ contact: contact._id });
247
+ if (!candidate) {
248
+ return res.status(404).json({ message: 'Candidat non trouvé' });
249
+ }
250
+ return res.json({
251
+ ...candidate.toObject(),
252
+ contact: contact.toObject()
253
+ });
254
+ }
255
+ catch (error) {
256
+ console.error('Erreur lors de la récupération du détail du candidat:', error);
257
+ res.status(500).send('Erreur interne du serveur');
258
+ }
259
+ });
260
+ // Générer un lien magique pour le candidat
261
+ this.post('/magic-link', { requireAuth: false }, async (req, res) => {
262
+ try {
263
+ const { email } = req.body;
264
+ if (!email) {
265
+ return res.status(400).json({ message: 'Email requis' });
266
+ }
267
+ // Chercher d'abord le contact par email
268
+ const contact = await ContactModel.findOne({ email });
269
+ if (!contact) {
270
+ return res.status(404).json({ message: 'Contact non trouvé' });
271
+ }
272
+ // Puis chercher le candidat avec ce contact
273
+ const candidate = await CandidateModel.findOne({ contact: contact._id });
274
+ if (!candidate) {
275
+ return res.status(404).json({ message: 'Candidat non trouvé' });
276
+ }
277
+ // Générer le token JWT
278
+ const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes
279
+ const token = jwt.sign({
280
+ email,
281
+ expiresAt: expiresAt.toISOString()
282
+ }, process.env.JWT_SECRET || 'your-secret-key', { expiresIn: '10m' });
283
+ // Mettre à jour le candidat avec le token
284
+ candidate.magicLinkToken = token;
285
+ candidate.magicLinkExpiresAt = expiresAt;
286
+ await candidate.save();
287
+ // Envoyer l'email avec le lien magique
288
+ const magicLink = `${process.env.CANDIDATE_MAGIC_LINK}${token}`;
289
+ await enduranceEmitter.emit(enduranceEventTypes.SEND_EMAIL, {
290
+ template: 'candidate-magic-link',
291
+ to: email,
292
+ from: process.env.EMAIL_USER,
293
+ emailUser: process.env.EMAIL_USER,
294
+ emailPassword: process.env.EMAIL_PASSWORD,
295
+ data: {
296
+ magicLink
297
+ }
298
+ });
299
+ return res.json({ message: 'Lien magique envoyé avec succès' });
300
+ }
301
+ catch (error) {
302
+ console.error('Erreur lors de la génération du lien magique:', error);
303
+ res.status(500).send('Erreur interne du serveur');
304
+ }
305
+ });
306
+ // Vérifier et consommer le token magique
307
+ this.post('/verify-magic-link', { requireAuth: false }, async (req, res) => {
308
+ try {
309
+ const { token } = req.body;
310
+ if (!token) {
311
+ return res.status(400).json({ message: 'Token requis' });
312
+ }
313
+ // Vérifier le token JWT
314
+ const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key');
315
+ // Vérifier si le token n'a pas expiré
316
+ if (new Date(decoded.expiresAt) < new Date()) {
317
+ return res.status(401).json({ message: 'Token expiré' });
318
+ }
319
+ // Trouver le candidat avec ce token
320
+ const candidate = await CandidateModel.findOne({
321
+ magicLinkToken: token,
322
+ magicLinkExpiresAt: { $gt: new Date() }
323
+ });
324
+ if (!candidate) {
325
+ return res.status(401).json({ message: 'Token invalide ou déjà utilisé' });
326
+ }
327
+ // Consommer le token en le supprimant
328
+ candidate.magicLinkToken = undefined;
329
+ candidate.magicLinkExpiresAt = undefined;
330
+ // Générer un nouveau token d'authentification valide 24h
331
+ const authExpiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 heures
332
+ const authToken = jwt.sign({
333
+ candidateId: candidate._id.toString(),
334
+ email: decoded.email,
335
+ type: 'candidate_auth'
336
+ }, process.env.JWT_SECRET || 'your-secret-key', { expiresIn: '24h' });
337
+ // Sauvegarder le nouveau token
338
+ candidate.authToken = authToken;
339
+ candidate.authTokenExpiresAt = authExpiresAt;
340
+ await candidate.save();
341
+ // Retourner les informations du candidat avec le nouveau token
342
+ return res.json({
343
+ message: 'Connexion réussie',
344
+ authToken,
345
+ candidate: {
346
+ id: candidate._id,
347
+ email: decoded.email,
348
+ contact: candidate.contact
349
+ }
350
+ });
351
+ }
352
+ catch (error) {
353
+ if (error instanceof jwt.JsonWebTokenError) {
354
+ return res.status(401).json({ message: 'Token invalide' });
355
+ }
356
+ console.error('Erreur lors de la vérification du token:', error);
357
+ res.status(500).send('Erreur interne du serveur');
358
+ }
359
+ });
360
+ // Lister tous les résultats de tests d'un candidat
361
+ this.get('/results/:candidateId', authenticatedOptions, async (req, res) => {
362
+ try {
363
+ const { candidateId } = req.params;
364
+ const page = parseInt(req.query.page) || 1;
365
+ const limit = parseInt(req.query.limit) || 10;
366
+ const skip = (page - 1) * limit;
367
+ const state = req.query.state || 'all';
368
+ const sortBy = req.query.sortBy || 'invitationDate';
369
+ const sortOrder = req.query.sortOrder || 'desc';
370
+ // Vérifier si le candidat existe
371
+ const candidate = await CandidateModel.findById(candidateId);
372
+ if (!candidate) {
373
+ return res.status(404).json({ message: 'Candidat non trouvé' });
374
+ }
375
+ // Construction de la requête
376
+ const query = { candidateId };
377
+ if (state !== 'all') {
378
+ query.state = state;
379
+ }
380
+ // Construction du tri
381
+ const allowedSortFields = ['invitationDate', 'state', 'score'];
382
+ const sortField = allowedSortFields.includes(sortBy) ? sortBy : 'invitationDate';
383
+ const sortOptions = {
384
+ [sortField]: sortOrder === 'asc' ? 1 : -1
385
+ };
386
+ const [results, total] = await Promise.all([
387
+ TestResult.find(query)
388
+ .sort(sortOptions)
389
+ .skip(skip)
390
+ .limit(limit)
391
+ .lean()
392
+ .exec(),
393
+ TestResult.countDocuments(query)
394
+ ]);
395
+ // Récupérer les informations des tests associés
396
+ const testIds = results.map(result => result.testId);
397
+ const tests = await Test.find({ _id: { $in: testIds } }).lean();
398
+ const testsMap = new Map(tests.map(test => [test._id.toString(), test]));
399
+ // Récupérer tous les IDs de catégories utilisés dans les tests
400
+ const allCategoryIds = Array.from(new Set(tests.flatMap(test => (test.categories || []).map((cat) => cat.categoryId?.toString()))));
401
+ const TestCategory = (await import('../models/test-category.models.js')).default;
402
+ const categoriesDocs = await TestCategory.find({ _id: { $in: allCategoryIds } }).lean();
403
+ const categoriesMap = new Map(categoriesDocs.map(cat => [cat._id.toString(), cat.name]));
404
+ // Combiner les résultats avec les informations des tests et des catégories
405
+ const resultsWithTests = await Promise.all(results.map(async (result) => {
406
+ const test = testsMap.get(result.testId.toString());
407
+ let categoriesWithNames = [];
408
+ if (test && test.categories) {
409
+ categoriesWithNames = test.categories.map((cat) => ({
410
+ ...cat,
411
+ categoryName: categoriesMap.get(cat.categoryId?.toString()) || 'Catégorie inconnue'
412
+ }));
413
+ }
414
+ return {
415
+ ...result,
416
+ test: test
417
+ ? {
418
+ title: test.title,
419
+ description: test.description,
420
+ targetJob: await getJobName(test.targetJob),
421
+ seniorityLevel: test.seniorityLevel,
422
+ categories: categoriesWithNames
423
+ }
424
+ : null
425
+ };
426
+ }));
427
+ const totalPages = Math.ceil(total / limit);
428
+ return res.json({
429
+ data: resultsWithTests,
430
+ pagination: {
431
+ currentPage: page,
432
+ totalPages,
433
+ totalItems: total,
434
+ itemsPerPage: limit,
435
+ hasNextPage: page < totalPages,
436
+ hasPreviousPage: page > 1
437
+ }
438
+ });
439
+ }
440
+ catch (err) {
441
+ console.error('Erreur lors de la récupération des résultats :', err);
442
+ res.status(500).json({ message: 'Erreur interne du serveur' });
443
+ }
444
+ });
445
+ }
446
+ }
447
+ const router = new CandidateRouter();
448
+ export default router;
@@ -0,0 +1,8 @@
1
+ import { EnduranceRouter } from '@programisto/endurance-core';
2
+ declare class ExamsRouter extends EnduranceRouter {
3
+ constructor();
4
+ private generateAndSaveQuestion;
5
+ setupRoutes(): void;
6
+ }
7
+ declare const router: ExamsRouter;
8
+ export default router;