Files
peakskills/services/evaluation-service.ts
Julien Froidefond 5c71ce1a54 refactor: update authentication flow and cookie management
- Changed COOKIE_NAME from "peakSkills_userId" to "session_token" for better clarity.
- Updated AuthClient to handle login and registration with new data structures.
- Enhanced AuthWrapper to manage user sessions and display appropriate messages.
- Added error handling in LoginForm and RegisterForm for better user feedback.
- Refactored user service methods to streamline user creation and verification processes.
2025-08-25 16:19:31 +02:00

713 lines
19 KiB
TypeScript

import { getPool } from "./database";
import { userService } from "./user-service";
import { AuthService } from "./auth-service";
import {
UserEvaluation,
UserProfile,
CategoryEvaluation,
SkillEvaluation,
SkillLevel,
} from "../lib/types";
export class EvaluationService {
/**
* Charge une évaluation utilisateur directement par UUID
*/
async loadUserEvaluationByUuid(
userUuid: string
): Promise<UserEvaluation | null> {
if (!userUuid) {
return null;
}
const pool = getPool();
const client = await pool.connect();
try {
// Trouver l'utilisateur et son évaluation par UUID
const userQuery = `
SELECT u.*, ue.id as user_evaluation_id, ue.last_updated
FROM users u
LEFT JOIN user_evaluations ue ON u.uuid_id = ue.user_uuid
WHERE u.uuid_id = $1
`;
const userResult = await client.query(userQuery, [userUuid]);
if (
userResult.rows.length === 0 ||
!userResult.rows[0].user_evaluation_id
) {
return null;
}
const userData = userResult.rows[0];
const userEvaluationId = userData.user_evaluation_id;
// Charger directement les skills évaluées avec leurs catégories
const skillsQuery = `
SELECT
sc.name as category_name,
se.skill_id,
se.level,
se.can_mentor,
se.wants_to_learn,
se.is_selected
FROM skill_evaluations se
JOIN skills s ON se.skill_id = s.id
JOIN skill_categories sc ON s.category_id = sc.id
WHERE se.user_evaluation_id = $1
ORDER BY sc.name, s.name
`;
const skillsResult = await client.query(skillsQuery, [userEvaluationId]);
// Grouper par catégorie
const categoriesMap = new Map<string, CategoryEvaluation>();
for (const row of skillsResult.rows) {
const categoryName = row.category_name;
if (!categoriesMap.has(categoryName)) {
categoriesMap.set(categoryName, {
category: categoryName,
selectedSkillIds: [],
skills: [],
});
}
const catEval = categoriesMap.get(categoryName)!;
// Ajouter aux skills sélectionnées si is_selected = true
if (row.is_selected) {
catEval.selectedSkillIds.push(row.skill_id);
}
// Ajouter aux évaluations si elle est sélectionnée (donc évaluée)
if (row.is_selected) {
catEval.skills.push({
skillId: row.skill_id,
level: row.level,
canMentor: row.can_mentor,
wantsToLearn: row.wants_to_learn,
});
}
}
const evaluations: CategoryEvaluation[] = Array.from(
categoriesMap.values()
);
return {
profile: {
firstName: userData.first_name,
lastName: userData.last_name,
teamId: userData.team_id,
},
evaluations,
lastUpdated: userData.last_updated.toISOString(),
};
} finally {
client.release();
}
}
/**
* Sauvegarde une évaluation utilisateur complète (version UUID)
*/
async saveUserEvaluationUuid(evaluation: UserEvaluation): Promise<void> {
const pool = getPool();
const client = await pool.connect();
try {
await client.query("BEGIN");
// 1. Upsert user avec UUID
const userUuid = await AuthService.getUserUuidFromCookie();
// 2. Upsert user_evaluation avec user_uuid
const userEvalQuery = `
INSERT INTO user_evaluations (user_uuid, last_updated)
VALUES ($1, $2)
ON CONFLICT (user_uuid)
DO UPDATE SET last_updated = $2
RETURNING id
`;
const userEvalResult = await client.query(userEvalQuery, [
userUuid,
new Date(evaluation.lastUpdated),
]);
const userEvaluationId = userEvalResult.rows[0].id;
// 3. Sauvegarde chaque catégorie d'évaluation
for (const catEval of evaluation.evaluations) {
await this.saveSkillEvaluations(client, userEvaluationId, catEval);
}
await client.query("COMMIT");
} catch (error) {
await client.query("ROLLBACK");
throw error;
} finally {
client.release();
}
}
/**
* Sauvegarde une évaluation utilisateur complète (legacy)
*/
async saveUserEvaluation(evaluation: UserEvaluation): Promise<void> {
const pool = getPool();
const client = await pool.connect();
try {
await client.query("BEGIN");
// 1. Upsert user
const userId = await userService.upsertUser(evaluation.profile);
// 2. Upsert user_evaluation - d'abord supprimer l'ancienne si elle existe
await client.query("DELETE FROM user_evaluations WHERE user_id = $1", [
userId,
]);
const userEvalQuery = `
INSERT INTO user_evaluations (user_id, last_updated)
VALUES ($1, $2)
RETURNING id
`;
const userEvalResult = await client.query(userEvalQuery, [
userId,
new Date(evaluation.lastUpdated),
]);
const userEvaluationId = userEvalResult.rows[0].id;
// 4. Sauvegarder les nouvelles évaluations directement
for (const catEval of evaluation.evaluations) {
await this.saveSkillEvaluations(client, userEvaluationId, catEval);
}
await client.query("COMMIT");
} catch (error) {
await client.query("ROLLBACK");
throw error;
} finally {
client.release();
}
}
/**
* Sauvegarde les évaluations de skills directement
*/
private async saveSkillEvaluations(
client: any,
userEvaluationId: number,
catEval: CategoryEvaluation
): Promise<void> {
// Insérer les skills sélectionnées (sans évaluation)
for (const skillId of catEval.selectedSkillIds) {
const isEvaluated = catEval.skills.some(
(se) => se.skillId === skillId && se.level !== null
);
if (!isEvaluated) {
// Skill sélectionnée mais pas encore évaluée
await client.query(
`
INSERT INTO skill_evaluations
(user_evaluation_id, skill_id, level, is_selected, can_mentor, wants_to_learn)
VALUES ($1, $2, 'never'::skill_level_enum, true, false, false)
`,
[userEvaluationId, skillId]
);
}
}
// Insérer les évaluations de skills
for (const skillEval of catEval.skills) {
if (skillEval.level !== null) {
await client.query(
`
INSERT INTO skill_evaluations
(user_evaluation_id, skill_id, level, is_selected, can_mentor, wants_to_learn)
VALUES ($1, $2, $3, true, $4, $5)
`,
[
userEvaluationId,
skillEval.skillId,
skillEval.level,
skillEval.canMentor || false,
skillEval.wantsToLearn || false,
]
);
}
}
}
/**
* Charge une évaluation utilisateur
*/
async loadUserEvaluation(
profile: UserProfile
): Promise<UserEvaluation | null> {
const pool = getPool();
const client = await pool.connect();
try {
// Trouver l'utilisateur par firstName + lastName (équipe peut avoir changé)
const userQuery = `
SELECT u.*, ue.id as user_evaluation_id, ue.last_updated
FROM users u
LEFT JOIN user_evaluations ue ON u.uuid_id = ue.user_uuid
WHERE u.first_name = $1 AND u.last_name = $2
`;
const userResult = await client.query(userQuery, [
profile.firstName,
profile.lastName,
]);
if (
userResult.rows.length === 0 ||
!userResult.rows[0].user_evaluation_id
) {
return null;
}
const userData = userResult.rows[0];
const userEvaluationId = userData.user_evaluation_id;
// Charger directement les skills évaluées avec leurs catégories
const skillsQuery = `
SELECT
sc.name as category_name,
se.skill_id,
se.level,
se.can_mentor,
se.wants_to_learn,
se.is_selected
FROM skill_evaluations se
JOIN skills s ON se.skill_id = s.id
JOIN skill_categories sc ON s.category_id = sc.id
WHERE se.user_evaluation_id = $1
ORDER BY sc.name, s.name
`;
const skillsResult = await client.query(skillsQuery, [userEvaluationId]);
// Grouper par catégorie
const categoriesMap = new Map<string, CategoryEvaluation>();
for (const row of skillsResult.rows) {
const categoryName = row.category_name;
if (!categoriesMap.has(categoryName)) {
categoriesMap.set(categoryName, {
category: categoryName,
selectedSkillIds: [],
skills: [],
});
}
const catEval = categoriesMap.get(categoryName)!;
// Ajouter aux skills sélectionnées si is_selected = true
if (row.is_selected) {
catEval.selectedSkillIds.push(row.skill_id);
}
// Ajouter aux évaluations si elle est sélectionnée (donc évaluée)
if (row.is_selected) {
catEval.skills.push({
skillId: row.skill_id,
level: row.level,
canMentor: row.can_mentor,
wantsToLearn: row.wants_to_learn,
});
}
}
const evaluations: CategoryEvaluation[] = Array.from(
categoriesMap.values()
);
return {
profile,
evaluations,
lastUpdated: userData.last_updated.toISOString(),
};
} finally {
client.release();
}
}
/**
* Met à jour le niveau d'une skill (version UUID)
*/
async updateSkillLevelUuid(
profile: UserProfile,
category: string,
skillId: string,
level: SkillLevel
): Promise<void> {
await this.updateSkillPropertyUuid(
profile,
category,
skillId,
"level",
level
);
}
/**
* Met à jour le statut mentor d'une skill (version UUID)
*/
async updateSkillMentorStatusUuid(
profile: UserProfile,
category: string,
skillId: string,
canMentor: boolean
): Promise<void> {
await this.updateSkillPropertyUuid(
profile,
category,
skillId,
"can_mentor",
canMentor
);
}
/**
* Met à jour le statut apprentissage d'une skill (version UUID)
*/
async updateSkillLearningStatusUuid(
profile: UserProfile,
category: string,
skillId: string,
wantsToLearn: boolean
): Promise<void> {
await this.updateSkillPropertyUuid(
profile,
category,
skillId,
"wants_to_learn",
wantsToLearn
);
}
/**
* Méthode utilitaire pour mettre à jour une propriété de skill (version UUID)
*/
private async updateSkillPropertyUuid(
profile: UserProfile,
category: string,
skillId: string,
property: string,
value: any
): Promise<void> {
const pool = getPool();
const client = await pool.connect();
try {
await client.query("BEGIN");
// Trouver l'utilisateur existant au lieu d'en créer un nouveau
const existingUser = await userService.findUserByProfile(profile);
if (!existingUser) {
throw new Error(
"Utilisateur non trouvé. Veuillez vous connecter avec votre email et mot de passe."
);
}
const userUuid = existingUser.uuid;
// Upsert user_evaluation avec user_uuid
const userEvalResult = await client.query(
`
INSERT INTO user_evaluations (user_uuid, last_updated)
VALUES ($1, CURRENT_TIMESTAMP)
ON CONFLICT (user_uuid)
DO UPDATE SET last_updated = CURRENT_TIMESTAMP
RETURNING id
`,
[userUuid]
);
const userEvaluationId = userEvalResult.rows[0].id;
// Upsert skill evaluation avec gestion conditionnelle du level
let updateQuery: string;
let queryParams: any[];
if (property === "level") {
// Si on met à jour le level, utiliser la valeur fournie
updateQuery = `
INSERT INTO skill_evaluations (user_evaluation_id, skill_id, level, is_selected)
VALUES ($1, $2, $3, true)
ON CONFLICT (user_evaluation_id, skill_id)
DO UPDATE SET level = $3, is_selected = true
`;
queryParams = [userEvaluationId, skillId, value];
} else {
// Si on met à jour une autre propriété, level par défaut = 'never'
updateQuery = `
INSERT INTO skill_evaluations (user_evaluation_id, skill_id, level, ${property}, is_selected)
VALUES ($1, $2, 'never', $3, true)
ON CONFLICT (user_evaluation_id, skill_id)
DO UPDATE SET ${property} = $3, is_selected = true
`;
queryParams = [userEvaluationId, skillId, value];
}
await client.query(updateQuery, queryParams);
await client.query("COMMIT");
} catch (error) {
await client.query("ROLLBACK");
throw error;
} finally {
client.release();
}
}
/**
* Met à jour le niveau d'une skill (legacy)
*/
async updateSkillLevel(
profile: UserProfile,
category: string,
skillId: string,
level: SkillLevel
): Promise<void> {
await this.upsertSkillEvaluation(profile, skillId, { level });
}
/**
* Met à jour le statut de mentorat d'une skill
*/
async updateSkillMentorStatus(
profile: UserProfile,
category: string,
skillId: string,
canMentor: boolean
): Promise<void> {
await this.upsertSkillEvaluation(profile, skillId, { canMentor });
}
/**
* Met à jour le statut d'apprentissage d'une skill
*/
async updateSkillLearningStatus(
profile: UserProfile,
category: string,
skillId: string,
wantsToLearn: boolean
): Promise<void> {
await this.upsertSkillEvaluation(profile, skillId, { wantsToLearn });
}
/**
* Méthode utilitaire pour mettre à jour ou créer une évaluation de skill
*/
private async upsertSkillEvaluation(
profile: UserProfile,
skillId: string,
updates: {
level?: SkillLevel;
canMentor?: boolean;
wantsToLearn?: boolean;
isSelected?: boolean;
}
): Promise<void> {
const pool = getPool();
const client = await pool.connect();
try {
await client.query("BEGIN");
const userId = await userService.upsertUser(profile);
// Upsert user_evaluation
const userEvalResult = await client.query(
`
INSERT INTO user_evaluations (user_id, last_updated)
VALUES ($1, CURRENT_TIMESTAMP)
ON CONFLICT (user_id)
DO UPDATE SET last_updated = CURRENT_TIMESTAMP
RETURNING id
`,
[userId]
);
const userEvaluationId = userEvalResult.rows[0].id;
// Upsert skill evaluation avec valeurs par défaut
const query = `
INSERT INTO skill_evaluations (
user_evaluation_id,
skill_id,
level,
can_mentor,
wants_to_learn,
is_selected
)
VALUES ($1, $2,
COALESCE($3::skill_level_enum, 'never'::skill_level_enum),
COALESCE($4, false),
COALESCE($5, false),
COALESCE($6, true)
)
ON CONFLICT (user_evaluation_id, skill_id)
DO UPDATE SET
level = CASE WHEN $3 IS NOT NULL THEN $3::skill_level_enum ELSE skill_evaluations.level END,
can_mentor = CASE WHEN $4 IS NOT NULL THEN $4 ELSE skill_evaluations.can_mentor END,
wants_to_learn = CASE WHEN $5 IS NOT NULL THEN $5 ELSE skill_evaluations.wants_to_learn END,
is_selected = CASE WHEN $6 IS NOT NULL THEN $6 ELSE skill_evaluations.is_selected END,
updated_at = CURRENT_TIMESTAMP
`;
await client.query(query, [
userEvaluationId,
skillId,
updates.level || null,
updates.canMentor !== undefined ? updates.canMentor : null,
updates.wantsToLearn !== undefined ? updates.wantsToLearn : null,
updates.isSelected !== undefined ? updates.isSelected : null,
]);
await client.query("COMMIT");
} catch (error) {
await client.query("ROLLBACK");
throw error;
} finally {
client.release();
}
}
/**
* Ajoute une skill à l'évaluation (version UUID)
*/
async addSkillToEvaluationUuid(
profile: UserProfile,
category: string,
skillId: string
): Promise<void> {
await this.updateSkillPropertyUuid(
profile,
category,
skillId,
"level",
"never" // Valeur par défaut pour une skill nouvellement sélectionnée
);
}
/**
* Supprime une skill de l'évaluation (version UUID)
*/
async removeSkillFromEvaluationUuid(
profile: UserProfile,
category: string,
skillId: string
): Promise<void> {
const pool = getPool();
const client = await pool.connect();
try {
await client.query("BEGIN");
const userUuid = await AuthService.getUserUuidFromCookie();
// Supprimer directement la skill evaluation
const deleteQuery = `
DELETE FROM skill_evaluations
WHERE user_evaluation_id = (
SELECT id FROM user_evaluations WHERE user_uuid = $1
)
AND skill_id = $2
`;
await client.query(deleteQuery, [userUuid, skillId]);
await client.query("COMMIT");
} catch (error) {
await client.query("ROLLBACK");
throw error;
} finally {
client.release();
}
}
/**
* Ajoute une skill à l'évaluation (legacy)
*/
async addSkillToEvaluation(
profile: UserProfile,
category: string,
skillId: string
): Promise<void> {
await this.upsertSkillEvaluation(profile, skillId, {
isSelected: true,
level: "never" as SkillLevel, // Valeur par défaut pour une skill nouvellement sélectionnée
});
}
/**
* Supprime une skill de l'évaluation
*/
async removeSkillFromEvaluation(
profile: UserProfile,
category: string,
skillId: string
): Promise<void> {
const pool = getPool();
const client = await pool.connect();
try {
await client.query("BEGIN");
const userId = await userService.upsertUser(profile);
// Supprimer directement la skill evaluation
const deleteQuery = `
DELETE FROM skill_evaluations
WHERE user_evaluation_id = (
SELECT id FROM user_evaluations WHERE user_id = $1
)
AND skill_id = $2
`;
await client.query(deleteQuery, [userId, skillId]);
await client.query("COMMIT");
} catch (error) {
await client.query("ROLLBACK");
throw error;
} finally {
client.release();
}
}
/**
* Récupère l'évaluation complète de l'utilisateur côté serveur
* Combine la récupération du cookie et le chargement de l'évaluation
*/
async getServerUserEvaluation(userUuid: string) {
if (!userUuid) {
return null;
}
try {
return await this.loadUserEvaluationByUuid(userUuid);
} catch (error) {
console.error("Failed to get user evaluation:", error);
return null;
}
}
}
// Instance singleton
export const evaluationService = new EvaluationService();