ajout envoi smtp (paramètres, test envois, envoi à 1 participant). protège vue mot de passe

- ajout filtre page statistiques
This commit is contained in:
Yannick Le Duc
2025-08-25 18:28:14 +02:00
parent caed358661
commit b0a945f07b
21 changed files with 3523 additions and 30 deletions

101
src/lib/email.ts Normal file
View File

@@ -0,0 +1,101 @@
import { SmtpSettings } from '@/types';
export const emailService = {
/**
* Teste la connectivité réseau de base
*/
async testNetworkConnectivity(host: string): Promise<{ success: boolean; error?: string }> {
try {
// Test simple de connectivité avec un ping DNS
const response = await fetch(`https://dns.google/resolve?name=${host}`, {
method: 'GET',
headers: {
'Accept': 'application/dns-json',
},
});
if (!response.ok) {
return { success: false, error: 'Impossible de résoudre le nom d\'hôte' };
}
const data = await response.json();
if (data.Status !== 0 || !data.Answer || data.Answer.length === 0) {
return { success: false, error: 'Nom d\'hôte non trouvé' };
}
return { success: true };
} catch (error) {
return {
success: false,
error: 'Erreur de connectivité réseau'
};
}
},
/**
* Teste la connexion SMTP via API route
*/
async testConnection(smtpSettings: SmtpSettings): Promise<{ success: boolean; error?: string }> {
try {
// Test de connectivité réseau d'abord
const networkTest = await this.testNetworkConnectivity(smtpSettings.host);
if (!networkTest.success) {
return {
success: false,
error: `Problème de connectivité : ${networkTest.error}. Vérifiez votre connexion internet et le nom du serveur SMTP.`
};
}
const response = await fetch('/api/test-smtp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ smtpSettings }),
});
const result = await response.json();
return result;
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur de connexion SMTP'
};
}
},
/**
* Envoie un email de test via API route
*/
async sendTestEmail(
smtpSettings: SmtpSettings,
toEmail: string
): Promise<{ success: boolean; error?: string; messageId?: string }> {
try {
const response = await fetch('/api/test-email', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ smtpSettings, toEmail }),
});
const result = await response.json();
return result;
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur lors de l\'envoi de l\'email'
};
}
},
/**
* Valide une adresse email
*/
validateEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
};

73
src/lib/encryption.ts Normal file
View File

@@ -0,0 +1,73 @@
import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync } from 'crypto';
// Clé de chiffrement dérivée de la clé Supabase
const deriveKey = (): Buffer => {
const salt = process.env.SUPABASE_ANON_KEY || 'default-salt';
return pbkdf2Sync(salt, 'mes-budgets-participatifs', 100000, 32, 'sha256');
};
export const encryptionService = {
/**
* Chiffre une valeur avec AES-256-GCM
*/
encrypt(value: string): string {
if (!value) return '';
const key = deriveKey();
const iv = randomBytes(16);
const cipher = createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(value, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
// Format: iv:authTag:encryptedData
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
},
/**
* Déchiffre une valeur chiffrée avec AES-256-GCM
*/
decrypt(encryptedValue: string): string {
if (!encryptedValue) return '';
try {
const parts = encryptedValue.split(':');
if (parts.length !== 3) {
throw new Error('Format de chiffrement invalide');
}
const [ivHex, authTagHex, encryptedData] = parts;
const key = deriveKey();
const iv = Buffer.from(ivHex, 'hex');
const authTag = Buffer.from(authTagHex, 'hex');
const decipher = createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
} catch (error) {
console.error('Erreur lors du déchiffrement:', error);
return '';
}
},
/**
* Vérifie si une valeur est chiffrée
*/
isEncrypted(value: string): boolean {
return value && value.includes(':') && value.split(':').length === 3;
},
/**
* Masque une valeur pour l'affichage
*/
mask(value: string, maskChar: string = '•'): string {
if (!value) return '';
return maskChar.repeat(Math.min(value.length, 8));
}
};

View File

@@ -1,5 +1,7 @@
import { supabase } from './supabase';
import { Campaign, Proposition, Participant, Vote, ParticipantWithVoteStatus } from '@/types';
import { Campaign, Proposition, Participant, Vote, ParticipantWithVoteStatus, Setting, SmtpSettings } from '@/types';
import { encryptionService } from './encryption';
import { emailService } from './email';
// Services pour les campagnes
export const campaignService = {
@@ -279,3 +281,175 @@ export const voteService = {
});
}
};
// Services pour les paramètres
export const settingsService = {
async getAll(): Promise<Setting[]> {
const { data, error } = await supabase
.from('settings')
.select('*')
.order('category', { ascending: true })
.order('key', { ascending: true });
if (error) throw error;
return data || [];
},
async getByCategory(category: string): Promise<Setting[]> {
const { data, error } = await supabase
.from('settings')
.select('*')
.eq('category', category)
.order('key', { ascending: true });
if (error) throw error;
return data || [];
},
async getByKey(key: string): Promise<Setting | null> {
const { data, error } = await supabase
.from('settings')
.select('*')
.eq('key', key)
.single();
if (error) {
if (error.code === 'PGRST116') return null; // No rows returned
throw error;
}
return data;
},
async getValue(key: string, defaultValue: string = ''): Promise<string> {
const setting = await this.getByKey(key);
return setting?.value || defaultValue;
},
async getBooleanValue(key: string, defaultValue: boolean = false): Promise<boolean> {
const value = await this.getValue(key, defaultValue.toString());
return value === 'true';
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async create(setting: any): Promise<Setting> {
const { data, error } = await supabase
.from('settings')
.insert(setting)
.select()
.single();
if (error) throw error;
return data;
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async update(key: string, updates: any): Promise<Setting> {
const { data, error } = await supabase
.from('settings')
.update(updates)
.eq('key', key)
.select()
.single();
if (error) throw error;
return data;
},
async setValue(key: string, value: string): Promise<Setting> {
const existing = await this.getByKey(key);
if (existing) {
return this.update(key, { value });
} else {
return this.create({ key, value, category: 'general' });
}
},
async setBooleanValue(key: string, value: boolean): Promise<Setting> {
return this.setValue(key, value.toString());
},
async delete(key: string): Promise<void> {
const { error } = await supabase
.from('settings')
.delete()
.eq('key', key);
if (error) throw error;
},
// Méthodes spécifiques pour les paramètres SMTP
async getSmtpSettings(): Promise<SmtpSettings> {
const smtpKeys = [
'smtp_host', 'smtp_port', 'smtp_username', 'smtp_password',
'smtp_secure', 'smtp_from_email', 'smtp_from_name'
];
const settings = await Promise.all(
smtpKeys.map(key => this.getByKey(key))
);
return {
host: settings[0]?.value || '',
port: parseInt(settings[1]?.value || '587'),
username: settings[2]?.value || '',
password: encryptionService.isEncrypted(settings[3]?.value || '')
? encryptionService.decrypt(settings[3]?.value || '')
: settings[3]?.value || '',
secure: settings[4]?.value === 'true',
from_email: settings[5]?.value || '',
from_name: settings[6]?.value || ''
};
},
async setSmtpSettings(smtpSettings: SmtpSettings): Promise<void> {
const settingsToUpdate = [
{ key: 'smtp_host', value: smtpSettings.host, category: 'email', description: 'Serveur SMTP' },
{ key: 'smtp_port', value: smtpSettings.port.toString(), category: 'email', description: 'Port SMTP' },
{ key: 'smtp_username', value: smtpSettings.username, category: 'email', description: 'Nom d\'utilisateur SMTP' },
{ key: 'smtp_password', value: encryptionService.encrypt(smtpSettings.password), category: 'email', description: 'Mot de passe SMTP (chiffré)' },
{ key: 'smtp_secure', value: smtpSettings.secure.toString(), category: 'email', description: 'Connexion sécurisée SSL/TLS' },
{ key: 'smtp_from_email', value: smtpSettings.from_email, category: 'email', description: 'Adresse email d\'expédition' },
{ key: 'smtp_from_name', value: smtpSettings.from_name, category: 'email', description: 'Nom d\'expédition' }
];
await Promise.all(
settingsToUpdate.map(setting => this.setValue(setting.key, setting.value))
);
},
async testSmtpConnection(smtpSettings: SmtpSettings): Promise<{ success: boolean; error?: string }> {
try {
// Validation basique des paramètres
if (!smtpSettings.host || !smtpSettings.port || !smtpSettings.username || !smtpSettings.password) {
return { success: false, error: 'Paramètres SMTP incomplets' };
}
if (smtpSettings.port < 1 || smtpSettings.port > 65535) {
return { success: false, error: 'Port SMTP invalide' };
}
if (!smtpSettings.from_email.includes('@')) {
return { success: false, error: 'Adresse email d\'expédition invalide' };
}
// Test de connexion via API route
return await emailService.testConnection(smtpSettings);
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : 'Erreur inconnue' };
}
},
async sendTestEmail(smtpSettings: SmtpSettings, toEmail: string): Promise<{ success: boolean; error?: string; messageId?: string }> {
try {
// Validation de l'email de destination
if (!emailService.validateEmail(toEmail)) {
return { success: false, error: 'Adresse email de destination invalide' };
}
// Envoi de l'email de test via API route
return await emailService.sendTestEmail(smtpSettings, toEmail);
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : 'Erreur lors de l\'envoi de l\'email de test' };
}
}
};