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:
Manus Sandbox
2025-11-18 02:00:48 -05:00
parent 20dc80cd74
commit 9d67724e83
11 changed files with 1298 additions and 13 deletions

View File

@@ -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);
}
/**

View File

@@ -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));
}

View File

@@ -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;