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:
30
server/db.ts
30
server/db.ts
@@ -611,12 +611,15 @@ export async function deleteUser(id: number) {
|
||||
|
||||
// ==================== GESTION DES TOKENS DE RÉINITIALISATION ====================
|
||||
|
||||
export async function createPasswordResetToken(userId: number, token: string, expiresAt: Date) {
|
||||
export async function createPasswordResetToken(email: string, token: string) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setHours(expiresAt.getHours() + 24); // Token valide 24h
|
||||
|
||||
const result = await db.insert(passwordResetTokens).values({
|
||||
userId,
|
||||
email,
|
||||
token,
|
||||
expiresAt,
|
||||
used: false,
|
||||
@@ -1266,3 +1269,26 @@ export async function getAdminUsers() {
|
||||
|
||||
return await db.select().from(users).where(eq(users.role, 'admin'));
|
||||
}
|
||||
|
||||
|
||||
|
||||
export async function getUtilisateurByEmail(email: string) {
|
||||
const db = await getDb();
|
||||
if (!db) return undefined;
|
||||
|
||||
const result = await db.select()
|
||||
.from(users)
|
||||
.where(eq(users.email, email))
|
||||
.limit(1);
|
||||
|
||||
return result.length > 0 ? result[0] : undefined;
|
||||
}
|
||||
|
||||
export async function updateUtilisateurPassword(userId: number, hashedPassword: string): Promise<void> {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
|
||||
await db.update(users)
|
||||
.set({ password: hashedPassword })
|
||||
.where(eq(users.id, userId));
|
||||
}
|
||||
|
||||
@@ -1021,3 +1021,21 @@ export async function sendAttestationEmail(params: {
|
||||
html: await getEmailTemplate(content, 'attestation'),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Alias pour sendPasswordResetEmail avec des paramètres plus simples
|
||||
*/
|
||||
export async function sendResetPasswordEmail(params: {
|
||||
email: string;
|
||||
prenom: string;
|
||||
nom: string;
|
||||
lienReinitialisation: string;
|
||||
}): Promise<boolean> {
|
||||
return sendPasswordResetEmail({
|
||||
apprenantEmail: params.email,
|
||||
apprenantNom: params.nom,
|
||||
apprenantPrenom: params.prenom,
|
||||
resetLink: params.lienReinitialisation,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 =====
|
||||
|
||||
Reference in New Issue
Block a user