fix: corriger le flux de réinitialisation par e-mail
Some checks failed
Validation applicative / TypeScript, tests et build (push) Failing after 2m22s

This commit is contained in:
manus-admin
2026-09-02 11:30:37 +02:00
parent 166729bdbc
commit 2cac56b8c6
8 changed files with 247 additions and 58 deletions

View File

@@ -19,6 +19,23 @@ import { sql } from "drizzle-orm";
import { catalogueRouter } from "./routers/catalogue";
import { planFormationRouter } from "./routers/planFormation";
/**
* Construit le lien de réinitialisation avec le domaine réellement utilisé.
* Cela permet au même code de fonctionner en local, recette et production.
*/
function buildPasswordResetUrl(req: { protocol: string; get: (name: string) => string | undefined }, token: string) {
const forwardedProtocol = req.get("x-forwarded-proto")?.split(",")[0]?.trim();
const protocol = forwardedProtocol || req.protocol;
const forwardedHost = req.get("x-forwarded-host")?.split(",")[0]?.trim();
const host = forwardedHost || req.get("host");
if (!host) {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "URL de réinitialisation indisponible" });
}
return `${protocol}://${host}/reset-password?token=${encodeURIComponent(token)}`;
}
// Procédure admin uniquement
const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
if (ctx.user.role !== 'admin') {
@@ -52,7 +69,7 @@ export const appRouter = router({
.input(z.object({
email: z.string().email(),
}))
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const { email } = input;
// Vérifier si l'utilisateur existe
@@ -66,10 +83,10 @@ export const appRouter = router({
const token = crypto.randomBytes(32).toString('hex');
// Sauvegarder le token dans la base de données
await db.createPasswordResetToken(email, token);
await db.createPasswordResetToken(utilisateur.id, token);
// Envoyer l'email avec le template reset_password
const resetUrl = `${process.env.VITE_OAUTH_PORTAL_URL || 'http://localhost:3000'}/reset-password?token=${token}`;
const resetUrl = buildPasswordResetUrl(ctx.req, token);
await sendResetPasswordEmail({
email,
@@ -117,7 +134,7 @@ export const appRouter = router({
}
// Récupérer l'utilisateur
const utilisateur = await db.getUtilisateurByEmail(resetToken.email);
const utilisateur = await db.getUserById(resetToken.userId);
if (!utilisateur) {
throw new TRPCError({
code: 'NOT_FOUND',
@@ -667,7 +684,8 @@ export const appRouter = router({
// Vérifier si la séquence est bloquée
const now = new Date();
if (sequence.statut === 'bloquee' || now >= new Date(sequence.dateBlocage)) {
// Une date de blocage est optionnelle : ne jamais transformer `null` en 01/01/1970.
if (sequence.statut === 'bloquee' || (sequence.dateBlocage && now >= new Date(sequence.dateBlocage))) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'Les inscriptions sont fermées pour cette séquence (J-15 dépassé)'
@@ -685,7 +703,7 @@ export const appRouter = router({
// Vérifier la capacité
const nbInscrits = await db.countInscriptionsBySequence(input.sequenceId, 'confirmee');
const statut = nbInscrits >= sequence.capaciteMax ? 'en_attente' : 'confirmee';
const statut = nbInscrits >= sequence.capaciteMax ? 'liste_attente' : 'confirmee';
await db.createInscription({
apprenantId: input.apprenantId,
@@ -785,7 +803,7 @@ export const appRouter = router({
const admins = await db.getAdminUsers();
const adminEmails = admins.filter(a => a.email).map(a => a.email!);
if (adminEmails.length > 0) {
const nbListeAttente = await db.countInscriptionsBySequence(input.sequenceId, 'en_attente');
const nbListeAttente = await db.countInscriptionsBySequence(input.sequenceId, 'liste_attente');
let formateurNom: string | undefined;
if (inscriptionSequence.formateurId) {
const formateur = await db.getFormateurById(inscriptionSequence.formateurId);
@@ -850,7 +868,7 @@ export const appRouter = router({
// Vérifier si la séquence est bloquée
const now = new Date();
if (sequence.statut === 'bloquee' || now >= new Date(sequence.dateBlocage)) {
if (sequence.statut === 'bloquee' || (sequence.dateBlocage && now >= new Date(sequence.dateBlocage))) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'Les désinscriptions sont fermées pour cette séquence (J-15 dépassé)'
@@ -927,7 +945,7 @@ export const appRouter = router({
// Notifier le premier en liste d'attente qu'une place s'est libérée
const inscriptionsListeAttente = await db.getInscriptionsBySequence(input.sequenceId);
const premierEnAttente = inscriptionsListeAttente
.filter(i => i.inscription.statut === 'en_attente')
.filter(i => i.inscription.statut === 'liste_attente')
.sort((a, b) => new Date(a.inscription.dateInscription).getTime() - new Date(b.inscription.dateInscription).getTime())[0];
if (premierEnAttente && premierEnAttente.apprenant) {
@@ -973,7 +991,7 @@ export const appRouter = router({
updateStatut: adminProcedure.input(z.object({
id: z.number(),
statut: z.enum(['confirmee', 'en_attente', 'annulee']),
statut: z.enum(['confirmee', 'liste_attente', 'annulee']),
})).mutation(async ({ input }) => {
// Récupérer l'inscription avant modification
const inscriptionAvant = await db.getInscriptionById(input.id);
@@ -1293,7 +1311,7 @@ export const appRouter = router({
await db.createInscription({
apprenantId: input.apprenantId,
sequenceId: input.sequenceId,
statut: input.statut === "confirme" ? "confirmee" : "en_attente",
statut: input.statut === "confirme" ? "confirmee" : "liste_attente",
});
return { success: true };
@@ -1384,7 +1402,7 @@ export const appRouter = router({
return { success: true };
}),
requestPasswordReset: adminProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => {
requestPasswordReset: adminProcedure.input(z.object({ id: z.number() })).mutation(async ({ input, ctx }) => {
const user = await db.getUserById(input.id);
if (!user) {
throw new Error("Utilisateur introuvable");
@@ -1399,11 +1417,10 @@ export const appRouter = router({
const token = crypto.randomBytes(32).toString('hex');
// Sauvegarder le token en base (la fonction gère l'expiration automatiquement)
await db.createPasswordResetToken(user.email, token);
await db.createPasswordResetToken(user.id, token);
// Générer le lien de réinitialisation
// TODO: Remplacer par l'URL réelle de votre application en production
const resetLink = `https://votre-domaine.com/reset-password?token=${token}`;
// Générer le lien de réinitialisation sur lenvironnement réellement appelé.
const resetLink = buildPasswordResetUrl(ctx.req, token);
// Envoyer l'email
const emailService = await import('./emailService');
@@ -2057,8 +2074,8 @@ export const appRouter = router({
texte: z.string(),
typeQuestion: z.enum(["choix_multiple", "echelle", "texte_libre", "oui_non"]),
options: z.string().optional(),
echelleMin: z.number().optional(),
echelleMax: z.number().optional(),
valeurMin: z.number().optional(),
valeurMax: z.number().optional(),
echelleLabelMin: z.string().optional(),
echelleLabelMax: z.string().optional(),
obligatoire: z.boolean().default(false),
@@ -2076,8 +2093,8 @@ export const appRouter = router({
texte: z.string().optional(),
typeQuestion: z.enum(["choix_multiple", "echelle", "texte_libre", "oui_non"]).optional(),
options: z.string().optional(),
echelleMin: z.number().optional(),
echelleMax: z.number().optional(),
valeurMin: z.number().optional(),
valeurMax: z.number().optional(),
echelleLabelMin: z.string().optional(),
echelleLabelMax: z.string().optional(),
obligatoire: z.boolean().optional(),