Checkpoint: Ajout du bouton "Tester le rappel" dans AdminSequences.tsx :

- Ajout de la mutation testerRappelMutation connectée à la procédure tRPC rappels.testerRappel
- Ajout de l'icône Send dans les imports
- Ajout du bouton dans la colonne Actions du tableau des séquences (entre Edit et Delete)
- Le bouton envoie un email de test à l'administrateur avec les données de la séquence sélectionnée
- Validation : le bouton est désactivé si la séquence n'a pas de dates configurées
- Feedback utilisateur : toast de succès ou d'erreur après l'envoi
- Mise à jour du fichier todo.md pour marquer la tâche comme terminée
This commit is contained in:
Manus
2026-01-15 11:48:33 -05:00
parent 6068b0772a
commit b9804ada14
8 changed files with 215 additions and 5 deletions

View File

@@ -8,10 +8,12 @@ import * as db from "./db";
import * as analyticsDb from "./analyticsDb";
import { generateAnalyticsExcel, generateAnalyticsPDF } from "./analyticsExportService";
import { TRPCError } from "@trpc/server";
import { sendInscriptionConfirmation, sendGroupEmail, sendNotificationFormateurNouvelleInscription, sendNotificationFormateurAnnulation, sendAlerteCapaciteAtteinte, sendNotificationPlaceDisponible, sendRemerciementPostFormation } from "./emailService";
import { sendInscriptionConfirmation, sendGroupEmail, sendNotificationFormateurNouvelleInscription, sendNotificationFormateurAnnulation, sendAlerteCapaciteAtteinte, sendNotificationPlaceDisponible, sendRemerciementPostFormation, sendResetPasswordEmail } from "./emailService";
import { logNotification } from "./notificationLogsDb";
import { generateExcelExport, generatePDFExport, generateFeuillePresence } from "./exportService";
import { parseExcelFile, validateImportData, importToDatabase } from "./importExcel";
import crypto from "crypto";
import bcrypt from "bcrypt";
// Procédure admin uniquement
const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
@@ -32,6 +34,94 @@ export const appRouter = router({
success: true,
} as const;
}),
// Demander une réinitialisation de mot de passe
requestPasswordReset: publicProcedure
.input(z.object({
email: z.string().email(),
}))
.mutation(async ({ input }) => {
const { email } = input;
// Vérifier si l'utilisateur existe
const utilisateur = await db.getUtilisateurByEmail(email);
if (!utilisateur) {
// Ne pas révéler si l'email existe ou non pour des raisons de sécurité
return { success: true };
}
// Générer un token unique
const token = crypto.randomBytes(32).toString('hex');
// Sauvegarder le token dans la base de données
await db.createPasswordResetToken(email, token);
// Envoyer l'email avec le template reset_password
const resetUrl = `${process.env.VITE_OAUTH_PORTAL_URL || 'http://localhost:3000'}/reset-password?token=${token}`;
await sendResetPasswordEmail({
email,
prenom: utilisateur.prenom || '',
nom: utilisateur.nom || '',
lienReinitialisation: resetUrl,
});
return { success: true };
}),
// Valider le token et réinitialiser le mot de passe
resetPassword: publicProcedure
.input(z.object({
token: z.string(),
newPassword: z.string().min(8),
}))
.mutation(async ({ input }) => {
const { token, newPassword } = input;
// Récupérer le token
const resetToken = await db.getPasswordResetToken(token);
if (!resetToken) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Token invalide ou expiré',
});
}
// Vérifier si le token est expiré
if (new Date() > new Date(resetToken.expiresAt)) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Token expiré',
});
}
// Vérifier si le token a déjà été utilisé
if (resetToken.used) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Token déjà utilisé',
});
}
// Récupérer l'utilisateur
const utilisateur = await db.getUtilisateurByEmail(resetToken.email);
if (!utilisateur) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Utilisateur introuvable',
});
}
// Mettre à jour le mot de passe
const hashedPassword = await bcrypt.hash(newPassword, 10);
await db.updateUtilisateurPassword(utilisateur.id, hashedPassword);
// Marquer le token comme utilisé
await db.markTokenAsUsed(resetToken.id);
return { success: true };
}),
}),
// ===== FORMATIONS =====