@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,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,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,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;
|