fix: corriger le flux de réinitialisation par e-mail
Some checks failed
Validation applicative / TypeScript, tests et build (push) Failing after 2m22s
Some checks failed
Validation applicative / TypeScript, tests et build (push) Failing after 2m22s
This commit is contained in:
@@ -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 l’environnement 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(),
|
||||
|
||||
Reference in New Issue
Block a user