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:
@@ -9,7 +9,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
|||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { trpc } from "@/lib/trpc";
|
import { trpc } from "@/lib/trpc";
|
||||||
import { Calendar, Edit, Eye, Plus, Trash2, X, Bell, Maximize2, Minimize2 } from "lucide-react";
|
import { Calendar, Edit, Eye, Plus, Trash2, X, Bell, Maximize2, Minimize2, Send } from "lucide-react";
|
||||||
import FormateurCombobox from "@/components/FormateurCombobox";
|
import FormateurCombobox from "@/components/FormateurCombobox";
|
||||||
import CapacityProgressBar from "@/components/CapacityProgressBar";
|
import CapacityProgressBar from "@/components/CapacityProgressBar";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -87,6 +87,15 @@ export default function AdminSequences() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const testerRappelMutation = trpc.rappels.testerRappel.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Email de test envoyé avec succès");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(`Erreur lors de l'envoi du test : ${error.message}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
setFormData({
|
setFormData({
|
||||||
formationId: "",
|
formationId: "",
|
||||||
@@ -821,6 +830,21 @@ export default function AdminSequences() {
|
|||||||
>
|
>
|
||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (sequence.dates && sequence.dates.length > 0) {
|
||||||
|
testerRappelMutation.mutate({ sequenceId: sequence.id });
|
||||||
|
} else {
|
||||||
|
toast.error("Cette séquence n'a pas de dates configurées");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={testerRappelMutation.isPending}
|
||||||
|
title="Tester l'envoi d'un rappel"
|
||||||
|
>
|
||||||
|
<Send className="h-4 w-4 text-green-600" />
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -38,9 +38,14 @@ export type InsertUser = typeof users.$inferInsert;
|
|||||||
*/
|
*/
|
||||||
export const passwordResetTokens = mysqlTable("passwordResetTokens", {
|
export const passwordResetTokens = mysqlTable("passwordResetTokens", {
|
||||||
id: int("id").autoincrement().primaryKey(),
|
id: int("id").autoincrement().primaryKey(),
|
||||||
userId: int("userId").notNull(),
|
/** Email de l'utilisateur qui demande la réinitialisation */
|
||||||
|
email: varchar("email", { length: 320 }).notNull(),
|
||||||
|
/** Token unique généré pour la réinitialisation */
|
||||||
token: varchar("token", { length: 255 }).notNull().unique(),
|
token: varchar("token", { length: 255 }).notNull().unique(),
|
||||||
|
/** Date d'expiration du token (24h après création) */
|
||||||
expiresAt: timestamp("expiresAt").notNull(),
|
expiresAt: timestamp("expiresAt").notNull(),
|
||||||
|
/** Indique si le token a déjà été utilisé */
|
||||||
|
used: boolean("used").default(false).notNull(),
|
||||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -553,3 +558,4 @@ export const historiqueAttestations = mysqlTable("historiqueAttestations", {
|
|||||||
|
|
||||||
export type HistoriqueAttestation = typeof historiqueAttestations.$inferSelect;
|
export type HistoriqueAttestation = typeof historiqueAttestations.$inferSelect;
|
||||||
export type InsertHistoriqueAttestation = typeof historiqueAttestations.$inferInsert;
|
export type InsertHistoriqueAttestation = typeof historiqueAttestations.$inferInsert;
|
||||||
|
|
||||||
|
|||||||
@@ -50,9 +50,11 @@
|
|||||||
"@trpc/client": "^11.6.0",
|
"@trpc/client": "^11.6.0",
|
||||||
"@trpc/react-query": "^11.6.0",
|
"@trpc/react-query": "^11.6.0",
|
||||||
"@trpc/server": "^11.6.0",
|
"@trpc/server": "^11.6.0",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
"@types/multer": "^2.0.0",
|
"@types/multer": "^2.0.0",
|
||||||
"@types/pdfkit": "^0.17.4",
|
"@types/pdfkit": "^0.17.4",
|
||||||
"axios": "^1.12.0",
|
"axios": "^1.12.0",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|||||||
34
pnpm-lock.yaml
generated
34
pnpm-lock.yaml
generated
@@ -127,6 +127,9 @@ importers:
|
|||||||
'@trpc/server':
|
'@trpc/server':
|
||||||
specifier: ^11.6.0
|
specifier: ^11.6.0
|
||||||
version: 11.6.0(typescript@5.9.3)
|
version: 11.6.0(typescript@5.9.3)
|
||||||
|
'@types/bcrypt':
|
||||||
|
specifier: ^6.0.0
|
||||||
|
version: 6.0.0
|
||||||
'@types/multer':
|
'@types/multer':
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
@@ -136,6 +139,9 @@ importers:
|
|||||||
axios:
|
axios:
|
||||||
specifier: ^1.12.0
|
specifier: ^1.12.0
|
||||||
version: 1.12.2
|
version: 1.12.2
|
||||||
|
bcrypt:
|
||||||
|
specifier: ^6.0.0
|
||||||
|
version: 6.0.0
|
||||||
bcryptjs:
|
bcryptjs:
|
||||||
specifier: ^3.0.3
|
specifier: ^3.0.3
|
||||||
version: 3.0.3
|
version: 3.0.3
|
||||||
@@ -2610,6 +2616,9 @@ packages:
|
|||||||
'@types/babel__traverse@7.28.0':
|
'@types/babel__traverse@7.28.0':
|
||||||
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
|
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
|
||||||
|
|
||||||
|
'@types/bcrypt@6.0.0':
|
||||||
|
resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==}
|
||||||
|
|
||||||
'@types/bcryptjs@3.0.0':
|
'@types/bcryptjs@3.0.0':
|
||||||
resolution: {integrity: sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==}
|
resolution: {integrity: sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==}
|
||||||
deprecated: This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed.
|
deprecated: This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed.
|
||||||
@@ -2969,6 +2978,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-vAPMQdnyKCBtkmQA6FMCBvU9qFIppS3nzyXnEM+Lo2IAhG4Mpjv9cCxMudhgV3YdNNJv6TNqXy97dfRVL2LmaQ==}
|
resolution: {integrity: sha512-vAPMQdnyKCBtkmQA6FMCBvU9qFIppS3nzyXnEM+Lo2IAhG4Mpjv9cCxMudhgV3YdNNJv6TNqXy97dfRVL2LmaQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
bcrypt@6.0.0:
|
||||||
|
resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==}
|
||||||
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
bcryptjs@3.0.3:
|
bcryptjs@3.0.3:
|
||||||
resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==}
|
resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -4523,6 +4536,10 @@ packages:
|
|||||||
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||||
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||||
|
|
||||||
|
node-addon-api@8.5.0:
|
||||||
|
resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==}
|
||||||
|
engines: {node: ^18 || ^20 || >= 21}
|
||||||
|
|
||||||
node-domexception@1.0.0:
|
node-domexception@1.0.0:
|
||||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||||
engines: {node: '>=10.5.0'}
|
engines: {node: '>=10.5.0'}
|
||||||
@@ -4537,6 +4554,10 @@ packages:
|
|||||||
encoding:
|
encoding:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
node-gyp-build@4.8.4:
|
||||||
|
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
node-releases@2.0.23:
|
node-releases@2.0.23:
|
||||||
resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==}
|
resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==}
|
||||||
|
|
||||||
@@ -8504,6 +8525,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.28.4
|
'@babel/types': 7.28.4
|
||||||
|
|
||||||
|
'@types/bcrypt@6.0.0':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 24.7.0
|
||||||
|
|
||||||
'@types/bcryptjs@3.0.0':
|
'@types/bcryptjs@3.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
bcryptjs: 3.0.3
|
bcryptjs: 3.0.3
|
||||||
@@ -8931,6 +8956,11 @@ snapshots:
|
|||||||
|
|
||||||
baseline-browser-mapping@2.8.12: {}
|
baseline-browser-mapping@2.8.12: {}
|
||||||
|
|
||||||
|
bcrypt@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
node-addon-api: 8.5.0
|
||||||
|
node-gyp-build: 4.8.4
|
||||||
|
|
||||||
bcryptjs@3.0.3: {}
|
bcryptjs@3.0.3: {}
|
||||||
|
|
||||||
big-integer@1.6.52: {}
|
big-integer@1.6.52: {}
|
||||||
@@ -10772,12 +10802,16 @@ snapshots:
|
|||||||
react: 19.2.0
|
react: 19.2.0
|
||||||
react-dom: 19.2.0(react@19.2.0)
|
react-dom: 19.2.0(react@19.2.0)
|
||||||
|
|
||||||
|
node-addon-api@8.5.0: {}
|
||||||
|
|
||||||
node-domexception@1.0.0: {}
|
node-domexception@1.0.0: {}
|
||||||
|
|
||||||
node-fetch@2.7.0:
|
node-fetch@2.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
whatwg-url: 5.0.0
|
whatwg-url: 5.0.0
|
||||||
|
|
||||||
|
node-gyp-build@4.8.4: {}
|
||||||
|
|
||||||
node-releases@2.0.23: {}
|
node-releases@2.0.23: {}
|
||||||
|
|
||||||
nodemailer@7.0.11: {}
|
nodemailer@7.0.11: {}
|
||||||
|
|||||||
30
server/db.ts
30
server/db.ts
@@ -611,12 +611,15 @@ export async function deleteUser(id: number) {
|
|||||||
|
|
||||||
// ==================== GESTION DES TOKENS DE RÉINITIALISATION ====================
|
// ==================== 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();
|
const db = await getDb();
|
||||||
if (!db) throw new Error("Database not available");
|
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({
|
const result = await db.insert(passwordResetTokens).values({
|
||||||
userId,
|
email,
|
||||||
token,
|
token,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
used: false,
|
used: false,
|
||||||
@@ -1266,3 +1269,26 @@ export async function getAdminUsers() {
|
|||||||
|
|
||||||
return await db.select().from(users).where(eq(users.role, 'admin'));
|
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'),
|
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 * as analyticsDb from "./analyticsDb";
|
||||||
import { generateAnalyticsExcel, generateAnalyticsPDF } from "./analyticsExportService";
|
import { generateAnalyticsExcel, generateAnalyticsPDF } from "./analyticsExportService";
|
||||||
import { TRPCError } from "@trpc/server";
|
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 { logNotification } from "./notificationLogsDb";
|
||||||
import { generateExcelExport, generatePDFExport, generateFeuillePresence } from "./exportService";
|
import { generateExcelExport, generatePDFExport, generateFeuillePresence } from "./exportService";
|
||||||
import { parseExcelFile, validateImportData, importToDatabase } from "./importExcel";
|
import { parseExcelFile, validateImportData, importToDatabase } from "./importExcel";
|
||||||
|
import crypto from "crypto";
|
||||||
|
import bcrypt from "bcrypt";
|
||||||
|
|
||||||
// Procédure admin uniquement
|
// Procédure admin uniquement
|
||||||
const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
|
const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
|
||||||
@@ -32,6 +34,94 @@ export const appRouter = router({
|
|||||||
success: true,
|
success: true,
|
||||||
} as const;
|
} 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 =====
|
// ===== FORMATIONS =====
|
||||||
|
|||||||
10
todo.md
10
todo.md
@@ -807,3 +807,13 @@
|
|||||||
- [x] Corriger l'erreur de type dans la modification des séquences (formateurId string au lieu de number)
|
- [x] Corriger l'erreur de type dans la modification des séquences (formateurId string au lieu de number)
|
||||||
- [x] Corriger la conservation de la date de blocage lors de la modification des séquences (ajout du champ dans le schéma Drizzle)
|
- [x] Corriger la conservation de la date de blocage lors de la modification des séquences (ajout du champ dans le schéma Drizzle)
|
||||||
- [x] Corriger les rappels envoyés plusieurs fois et l'historique vide (nettoyage de la table logsRappels)
|
- [x] Corriger les rappels envoyés plusieurs fois et l'historique vide (nettoyage de la table logsRappels)
|
||||||
|
|
||||||
|
## Nouvelles fonctionnalités à implémenter
|
||||||
|
- [ ] Créer la table passwordResetTokens dans le schéma Drizzle
|
||||||
|
- [ ] Implémenter la route backend pour demander une réinitialisation de mot de passe
|
||||||
|
- [ ] Implémenter la route backend pour valider le token et réinitialiser le mot de passe
|
||||||
|
- [ ] Créer la page frontend de demande de réinitialisation
|
||||||
|
- [ ] Créer la page frontend de réinitialisation avec token
|
||||||
|
- [x] Ajouter le bouton "Tester le rappel" dans AdminSequences.tsx
|
||||||
|
- [ ] Corriger les erreurs TypeScript dans les fonctions de gestion des présences
|
||||||
|
- [ ] Corriger les erreurs TypeScript dans les fonctions de gestion des logs
|
||||||
|
|||||||
Reference in New Issue
Block a user