Checkpoint: Ajout de l'interface de configuration SMTP avec Resend : formulaire de configuration (API key, email expéditeur, nom), basculement simulation/production, test d'envoi immédiat, stockage sécurisé en base de données, et guide de configuration intégré
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
|
||||
import { ENV } from "./env";
|
||||
import { notifyOwner } from "./notification";
|
||||
import { getActiveEmailConfig } from "../db";
|
||||
|
||||
interface EmailParams {
|
||||
to: string;
|
||||
@@ -37,9 +38,11 @@ interface ResendEmailRequest {
|
||||
/**
|
||||
* Envoie un email via Resend API
|
||||
*/
|
||||
async function sendViaResend(params: EmailParams): Promise<boolean> {
|
||||
const resendApiKey = process.env.RESEND_API_KEY;
|
||||
const fromEmail = process.env.RESEND_FROM_EMAIL || 'noreply@manusvm.computer';
|
||||
async function sendViaResend(params: EmailParams, config?: { apiKey: string; fromEmail: string; fromName: string }): Promise<boolean> {
|
||||
// Utiliser la config fournie ou les variables d'environnement
|
||||
const resendApiKey = config?.apiKey || process.env.RESEND_API_KEY;
|
||||
const fromEmail = config?.fromEmail || process.env.RESEND_FROM_EMAIL || 'noreply@manusvm.computer';
|
||||
const fromName = config?.fromName || 'Formation Manager Itinova';
|
||||
|
||||
if (!resendApiKey) {
|
||||
console.warn('[Email] RESEND_API_KEY non configurée, email simulé');
|
||||
@@ -48,7 +51,7 @@ async function sendViaResend(params: EmailParams): Promise<boolean> {
|
||||
|
||||
try {
|
||||
const payload: ResendEmailRequest = {
|
||||
from: fromEmail,
|
||||
from: `${fromName} <${fromEmail}>`,
|
||||
to: [params.to],
|
||||
subject: params.subject,
|
||||
html: params.html,
|
||||
@@ -126,15 +129,32 @@ ${params.attachments ? `Pièces jointes: ${params.attachments.map(a => a.filenam
|
||||
* Envoie un email (réel ou simulé selon la configuration)
|
||||
*/
|
||||
export async function sendEmail(params: EmailParams): Promise<boolean> {
|
||||
// Tenter d'envoyer via Resend
|
||||
const sent = await sendViaResend(params);
|
||||
// Récupérer la configuration depuis la base de données
|
||||
const config = await getActiveEmailConfig();
|
||||
|
||||
// Si l'envoi échoue (pas de clé API ou erreur), simuler
|
||||
if (!sent) {
|
||||
// Si mode simulation ou pas de config, simuler
|
||||
if (!config || config.mode === 'simulation') {
|
||||
return await simulateEmail(params);
|
||||
}
|
||||
|
||||
return true;
|
||||
// Si mode production, tenter d'envoyer via Resend
|
||||
if (config.mode === 'production' && config.apiKey) {
|
||||
const sent = await sendViaResend(params, {
|
||||
apiKey: config.apiKey,
|
||||
fromEmail: config.fromEmail,
|
||||
fromName: config.fromName,
|
||||
});
|
||||
|
||||
// Si l'envoi échoue, simuler en fallback
|
||||
if (!sent) {
|
||||
return await simulateEmail(params);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback: simuler
|
||||
return await simulateEmail(params);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
60
server/db.ts
60
server/db.ts
@@ -10,16 +10,18 @@ import {
|
||||
inscriptions,
|
||||
passwordResetTokens,
|
||||
InsertPasswordResetToken,
|
||||
InsertFormation,
|
||||
InsertApprenant,
|
||||
emailTemplates,
|
||||
emailConfig,
|
||||
InsertEmailConfig,
|
||||
InsertSequence,
|
||||
InsertDateFormation,
|
||||
InsertInscription,
|
||||
InsertFormation,
|
||||
InsertApprenant,
|
||||
Sequence,
|
||||
DateFormation,
|
||||
Apprenant,
|
||||
Formation,
|
||||
emailTemplates,
|
||||
EmailTemplate,
|
||||
InsertEmailTemplate
|
||||
} from "../drizzle/schema";
|
||||
@@ -602,3 +604,55 @@ export async function initializeDefaultEmailTemplates() {
|
||||
|
||||
console.log("[DB] Templates d'emails par défaut initialisés");
|
||||
}
|
||||
|
||||
|
||||
// ==================== Email Config ====================
|
||||
|
||||
/**
|
||||
* Récupère la configuration email active
|
||||
*/
|
||||
export async function getActiveEmailConfig() {
|
||||
const db = await getDb();
|
||||
if (!db) return undefined;
|
||||
|
||||
const result = await db
|
||||
.select()
|
||||
.from(emailConfig)
|
||||
.where(eq(emailConfig.active, true))
|
||||
.limit(1);
|
||||
|
||||
return result.length > 0 ? result[0] : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée ou met à jour la configuration email
|
||||
*/
|
||||
export async function upsertEmailConfig(data: InsertEmailConfig) {
|
||||
const db = await getDb();
|
||||
if (!db) return;
|
||||
|
||||
// Désactiver toutes les configurations existantes
|
||||
await db.update(emailConfig).set({ active: false });
|
||||
|
||||
// Créer la nouvelle configuration
|
||||
await db.insert(emailConfig).values({
|
||||
...data,
|
||||
active: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour la configuration email
|
||||
*/
|
||||
export async function updateEmailConfig(id: number, data: Partial<InsertEmailConfig>) {
|
||||
const db = await getDb();
|
||||
if (!db) return;
|
||||
|
||||
await db
|
||||
.update(emailConfig)
|
||||
.set({
|
||||
...data,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(emailConfig.id, id));
|
||||
}
|
||||
|
||||
@@ -666,6 +666,51 @@ export const appRouter = router({
|
||||
return { success: true };
|
||||
}),
|
||||
}),
|
||||
|
||||
emailConfig: router({
|
||||
get: adminProcedure.query(async () => {
|
||||
return db.getActiveEmailConfig();
|
||||
}),
|
||||
|
||||
upsert: adminProcedure
|
||||
.input(z.object({
|
||||
provider: z.string(),
|
||||
apiKey: z.string().nullable(),
|
||||
fromEmail: z.string().email(),
|
||||
fromName: z.string(),
|
||||
mode: z.enum(["simulation", "production"]),
|
||||
domainVerified: z.boolean(),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
await db.upsertEmailConfig(input);
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
testEmail: adminProcedure
|
||||
.input(z.object({
|
||||
toEmail: z.string().email(),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
// Importer le service d'envoi
|
||||
const { sendEmail } = await import("./_core/emailSender");
|
||||
|
||||
try {
|
||||
const success = await sendEmail({
|
||||
to: input.toEmail,
|
||||
subject: "Test d'envoi d'email - Formation Manager Itinova",
|
||||
html: `
|
||||
<h1>Test réussi !</h1>
|
||||
<p>Cet email de test a été envoyé avec succès depuis votre configuration SMTP.</p>
|
||||
<p>Votre configuration d'envoi d'emails fonctionne correctement.</p>
|
||||
`,
|
||||
});
|
||||
|
||||
return { success, message: success ? "Email envoyé avec succès" : "Échec de l'envoi" };
|
||||
} catch (error: any) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export type AppRouter = typeof appRouter;
|
||||
|
||||
Reference in New Issue
Block a user