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:
101
src/lib/email.ts
Normal file
101
src/lib/email.ts
Normal 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
73
src/lib/encryption.ts
Normal 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));
|
||||
}
|
||||
};
|
||||
@@ -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' };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user