feat: introduce Resend email provider and add admin email configuration page.
This commit is contained in:
@@ -0,0 +1,29 @@
|
|||||||
|
# Implementazione Configurazione Email in Amministrazione
|
||||||
|
|
||||||
|
## Obiettivo
|
||||||
|
Rendere disponibile la configurazione dell'invio email del modulo Comunicazioni nella sezione Amministrazione dell'interfaccia grafica.
|
||||||
|
|
||||||
|
## Stato Attuale
|
||||||
|
- Il backend ha già gli endpoint per la configurazione SMTP (`api/communications/config`).
|
||||||
|
- Esiste già una pagina `SettingsPage` nel modulo Comunicazioni (`src/frontend/src/apps/communications/pages/SettingsPage.tsx`) che gestisce il form di configurazione.
|
||||||
|
- Il modulo Comunicazioni non è attualmente visibile nel menu principale se non attivo/acquistato, ma la configurazione email è un setting globale che dovrebbe essere accessibile.
|
||||||
|
|
||||||
|
## Piano di Lavoro
|
||||||
|
1. **Aggiornamento Route**: Aggiungere una route `/admin/email-config` in `App.tsx` che punta alla pagina di configurazione esistente (o un wrapper).
|
||||||
|
2. **Aggiornamento Menu**: Aggiungere la voce "Configurazione Email" nel menu "Amministrazione" in `Sidebar.tsx`.
|
||||||
|
3. **Traduzioni**: Aggiungere le chiavi di traduzione per la nuova voce di menu in `it/translation.json` e `en/translation.json`.
|
||||||
|
4. **Test**: Avviare l'applicazione e verificare che la pagina sia accessibile e funzionante.
|
||||||
|
|
||||||
|
## Dettagli Tecnici
|
||||||
|
- Riutilizzare `src/frontend/src/apps/communications/pages/SettingsPage.tsx`.
|
||||||
|
- La route sarà protetta se necessario, ma accessibile come parte dell'amministrazione.
|
||||||
|
|
||||||
|
## Stato Finale
|
||||||
|
- [x] Aggiunta route `/admin/email-config` in `App.tsx`.
|
||||||
|
- [x] Aggiunta voce menu "Configurazione Email" in `Sidebar.tsx`.
|
||||||
|
- [x] Aggiunte traduzioni IT ed EN.
|
||||||
|
- [x] Installato .NET 9.0 SDK via script locale (`~/.dotnet`).
|
||||||
|
- [x] Installato `dotnet-ef` tool.
|
||||||
|
- [x] Creata migrazione `UpdateCommunicationsModule` e aggiornato il database.
|
||||||
|
- [x] Backend avviato su porta 5000.
|
||||||
|
- [x] Frontend avviato su porta 5173.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Integrazione Supporto Resend per Invio Email
|
||||||
|
|
||||||
|
## Obiettivo
|
||||||
|
Abilitare l'invio di email tramite servizi terzi (Resend) oltre al già presente SMTP, con configurazione via interfaccia grafica.
|
||||||
|
|
||||||
|
## Stato Attuale
|
||||||
|
- Backend: `SmtpEmailSender` gestisce solo SMTP.
|
||||||
|
- Frontend: `SettingsPage` gestisce solo campi SMTP.
|
||||||
|
- DTO: `SmtpConfigDto` limitato a SMTP.
|
||||||
|
|
||||||
|
## Piano di Lavoro
|
||||||
|
1. **Backend DTO**: Aggiornare `SmtpConfigDto` con campi `Provider` e `ResendApiKey`.
|
||||||
|
2. **Backend Controller**: Aggiornare `CommunicationsController` per leggere/salvare le nuove configurazioni (`EMAIL_PROVIDER`, `RESEND_API_KEY`).
|
||||||
|
3. **Backend Service**: Modificare `SmtpEmailSender` (o rinominarlo in `UnifiedEmailSender`) per supportare la logica condizionale (SMTP vs Resend). Implementare l'invio tramite HTTP Client per Resend.
|
||||||
|
4. **Frontend Service**: Aggiornare le definizioni di tipo TypeScript.
|
||||||
|
5. **Frontend UI**: Modificare `SettingsPage` per aggiungere un selettore di provider (SMTP/Resend) e mostrare i campi pertinenti dinamicamente.
|
||||||
|
6. **Traduzioni**: Aggiungere le nuove etichette.
|
||||||
|
|
||||||
|
## Dettagli Tecnici
|
||||||
|
- **API Resend**: Richiesta POST a `https://api.resend.com/emails` con Bearer Token.
|
||||||
|
- **Provider Enum**: "smtp", "resend".
|
||||||
|
- **Defaut**: SMTP per retrocompatibilità.
|
||||||
|
|
||||||
|
## Avanzamento
|
||||||
|
- [x] Backend DTO Update (`SmtpConfigDto`)
|
||||||
|
- [x] Backend Controller Update (`CommunicationsController`)
|
||||||
|
- [x] Backend Service Logic (`SmtpEmailSender` now handles Resend via HTTP)
|
||||||
|
- [x] Frontend Types Update
|
||||||
|
- [x] Frontend UI Update (`SettingsPage.tsx` with Provider selector)
|
||||||
|
- [x] Dependencies (Added `Microsoft.Extensions.Http` to Infrastructure)
|
||||||
|
|
||||||
|
## Note Finali
|
||||||
|
- L'integrazione supporta ora la selezione dinamica tra SMTP e Resend.
|
||||||
|
- La configurazione viene salvata su database (`EMAIL_PROVIDER`, `RESEND_API_KEY`).
|
||||||
|
- Il backend utilizza `IHttpClientFactory` per le chiamate API verso Resend.
|
||||||
|
- UI aggiornata per mostrare campi condizionali.
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ public class CommunicationsController : ControllerBase
|
|||||||
public async Task<ActionResult<SmtpConfigDto>> GetConfig()
|
public async Task<ActionResult<SmtpConfigDto>> GetConfig()
|
||||||
{
|
{
|
||||||
var configs = await _context.Configurazioni
|
var configs = await _context.Configurazioni
|
||||||
.Where(c => c.Chiave.StartsWith("SMTP_"))
|
.Where(c => c.Chiave.StartsWith("SMTP_") || c.Chiave == "EMAIL_PROVIDER" || c.Chiave == "RESEND_API_KEY")
|
||||||
.ToDictionaryAsync(c => c.Chiave, c => c.Valore);
|
.ToDictionaryAsync(c => c.Chiave, c => c.Valore);
|
||||||
|
|
||||||
var dto = new SmtpConfigDto
|
var dto = new SmtpConfigDto
|
||||||
@@ -36,7 +36,9 @@ public class CommunicationsController : ControllerBase
|
|||||||
Password = GetValue(configs, "SMTP_PASS"),
|
Password = GetValue(configs, "SMTP_PASS"),
|
||||||
EnableSsl = bool.Parse(GetValue(configs, "SMTP_SSL", "false")),
|
EnableSsl = bool.Parse(GetValue(configs, "SMTP_SSL", "false")),
|
||||||
FromEmail = GetValue(configs, "SMTP_FROM_EMAIL"),
|
FromEmail = GetValue(configs, "SMTP_FROM_EMAIL"),
|
||||||
FromName = GetValue(configs, "SMTP_FROM_NAME")
|
FromName = GetValue(configs, "SMTP_FROM_NAME"),
|
||||||
|
Provider = GetValue(configs, "EMAIL_PROVIDER", "smtp"),
|
||||||
|
ResendApiKey = GetValue(configs, "RESEND_API_KEY")
|
||||||
};
|
};
|
||||||
|
|
||||||
return Ok(dto);
|
return Ok(dto);
|
||||||
@@ -53,6 +55,9 @@ public class CommunicationsController : ControllerBase
|
|||||||
await SetConfig("SMTP_FROM_EMAIL", dto.FromEmail);
|
await SetConfig("SMTP_FROM_EMAIL", dto.FromEmail);
|
||||||
await SetConfig("SMTP_FROM_NAME", dto.FromName);
|
await SetConfig("SMTP_FROM_NAME", dto.FromName);
|
||||||
|
|
||||||
|
await SetConfig("EMAIL_PROVIDER", dto.Provider);
|
||||||
|
await SetConfig("RESEND_API_KEY", dto.ResendApiKey);
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,4 +9,8 @@ public class SmtpConfigDto
|
|||||||
public bool EnableSsl { get; set; } = false;
|
public bool EnableSsl { get; set; } = false;
|
||||||
public string FromEmail { get; set; } = string.Empty;
|
public string FromEmail { get; set; } = string.Empty;
|
||||||
public string FromName { get; set; } = string.Empty;
|
public string FromName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
// New fields for Resend support
|
||||||
|
public string Provider { get; set; } = "smtp"; // "smtp" or "resend"
|
||||||
|
public string ResendApiKey { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ builder.Services.AddDbContext<ZentralDbContext>(options =>
|
|||||||
options.UseSqlite(connectionString));
|
options.UseSqlite(connectionString));
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
|
builder.Services.AddHttpClient();
|
||||||
builder.Services.AddScoped<EventoCostiService>();
|
builder.Services.AddScoped<EventoCostiService>();
|
||||||
builder.Services.AddScoped<DemoDataService>();
|
builder.Services.AddScoped<DemoDataService>();
|
||||||
builder.Services.AddScoped<ReportGeneratorService>();
|
builder.Services.AddScoped<ReportGeneratorService>();
|
||||||
|
|||||||
4758
src/backend/Zentral.Infrastructure/Migrations/20251212105451_UpdateCommunicationsModule.Designer.cs
generated
Normal file
4758
src/backend/Zentral.Infrastructure/Migrations/20251212105451_UpdateCommunicationsModule.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,60 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Zentral.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class UpdateCommunicationsModule : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "EmailLogs",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
SentDate = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
Sender = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Recipient = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Subject = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Status = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
ErrorMessage = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
CreatedBy = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
UpdatedBy = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
CustomFieldsJson = table.Column<string>(type: "TEXT", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_EmailLogs", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_EmailLogs_Recipient",
|
||||||
|
table: "EmailLogs",
|
||||||
|
column: "Recipient");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_EmailLogs_SentDate",
|
||||||
|
table: "EmailLogs",
|
||||||
|
column: "SentDate");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_EmailLogs_Status",
|
||||||
|
table: "EmailLogs",
|
||||||
|
column: "Status");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "EmailLogs");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -418,6 +418,60 @@ namespace Zentral.Infrastructure.Migrations
|
|||||||
b.ToTable("CodiciCategoria");
|
b.ToTable("CodiciCategoria");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Zentral.Domain.Entities.Communications.EmailLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("CustomFieldsJson")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ErrorMessage")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Recipient")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Sender")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("SentDate")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Subject")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("UpdatedBy")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Recipient");
|
||||||
|
|
||||||
|
b.HasIndex("SentDate");
|
||||||
|
|
||||||
|
b.HasIndex("Status");
|
||||||
|
|
||||||
|
b.ToTable("EmailLogs", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Zentral.Domain.Entities.Configurazione", b =>
|
modelBuilder.Entity("Zentral.Domain.Entities.Configurazione", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using MimeKit;
|
using MimeKit;
|
||||||
using MailKit.Net.Smtp;
|
using MailKit.Net.Smtp;
|
||||||
using MailKit.Security;
|
using MailKit.Security;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Net.Http;
|
||||||
using Zentral.Domain.Entities.Communications;
|
using Zentral.Domain.Entities.Communications;
|
||||||
using Zentral.Domain.Interfaces;
|
using Zentral.Domain.Interfaces;
|
||||||
using Zentral.Infrastructure.Data;
|
using Zentral.Infrastructure.Data;
|
||||||
@@ -16,10 +19,12 @@ namespace Zentral.Infrastructure.Services;
|
|||||||
public class SmtpEmailSender : IEmailSender
|
public class SmtpEmailSender : IEmailSender
|
||||||
{
|
{
|
||||||
private readonly IServiceScopeFactory _scopeFactory;
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
|
||||||
public SmtpEmailSender(IServiceScopeFactory scopeFactory)
|
public SmtpEmailSender(IServiceScopeFactory scopeFactory, IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
_scopeFactory = scopeFactory;
|
_scopeFactory = scopeFactory;
|
||||||
|
_httpClientFactory = httpClientFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendEmailAsync(string to, string subject, string body, bool isHtml = true)
|
public async Task SendEmailAsync(string to, string subject, string body, bool isHtml = true)
|
||||||
@@ -34,14 +39,80 @@ public class SmtpEmailSender : IEmailSender
|
|||||||
|
|
||||||
// 1. Get Configuration
|
// 1. Get Configuration
|
||||||
var configs = await context.Configurazioni
|
var configs = await context.Configurazioni
|
||||||
.Where(c => c.Chiave.StartsWith("SMTP_"))
|
.Where(c => c.Chiave.StartsWith("SMTP_") || c.Chiave == "EMAIL_PROVIDER" || c.Chiave == "RESEND_API_KEY")
|
||||||
.ToDictionaryAsync(c => c.Chiave, c => c.Valore);
|
.ToDictionaryAsync(c => c.Chiave, c => c.Valore);
|
||||||
|
|
||||||
|
var provider = GetConfig(configs, "EMAIL_PROVIDER", "smtp");
|
||||||
|
|
||||||
|
if (provider.ToLower() == "resend")
|
||||||
|
{
|
||||||
|
await SendViaResendAsync(context, to, subject, body, attachments, isHtml, configs);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await SendViaSmtpAsync(context, to, subject, body, attachments, isHtml, configs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendViaResendAsync(ZentralDbContext context, string to, string subject, string body, List<string> attachments, bool isHtml, Dictionary<string, string?> configs)
|
||||||
|
{
|
||||||
|
var apiKey = GetConfig(configs, "RESEND_API_KEY");
|
||||||
|
var fromEmail = GetConfig(configs, "SMTP_FROM_EMAIL"); // Resend often requires a verified domain, but we reuse the field
|
||||||
|
var fromName = GetConfig(configs, "SMTP_FROM_NAME", "Zentral");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(apiKey))
|
||||||
|
{
|
||||||
|
await LogResultAsync(context, fromEmail, to, subject, "Failed", "Resend API Key not configured");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = _httpClientFactory.CreateClient();
|
||||||
|
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", apiKey);
|
||||||
|
|
||||||
|
var request = new
|
||||||
|
{
|
||||||
|
from = $"{fromName} <{fromEmail}>",
|
||||||
|
to = new[] { to },
|
||||||
|
subject = subject,
|
||||||
|
html = isHtml ? body : null,
|
||||||
|
text = !isHtml ? body : null,
|
||||||
|
attachments = attachments.Select(a => {
|
||||||
|
var bytes = System.IO.File.ReadAllBytes(a);
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
filename = System.IO.Path.GetFileName(a),
|
||||||
|
content = Convert.ToBase64String(bytes)
|
||||||
|
};
|
||||||
|
}).ToArray()
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await client.PostAsJsonAsync("https://api.resend.com/emails", request);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
await LogResultAsync(context, fromEmail, to, subject, "Success", "Via Resend");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var errorContent = await response.Content.ReadAsStringAsync();
|
||||||
|
await LogResultAsync(context, fromEmail, to, subject, "Failed", $"Resend Error: {errorContent}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await LogResultAsync(context, fromEmail, to, subject, "Failed", ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendViaSmtpAsync(ZentralDbContext context, string to, string subject, string body, List<string> attachments, bool isHtml, Dictionary<string, string?> configs)
|
||||||
|
{
|
||||||
var host = GetConfig(configs, "SMTP_HOST");
|
var host = GetConfig(configs, "SMTP_HOST");
|
||||||
var portStr = GetConfig(configs, "SMTP_PORT", "587");
|
var portStr = GetConfig(configs, "SMTP_PORT", "587");
|
||||||
var user = GetConfig(configs, "SMTP_USER");
|
var user = GetConfig(configs, "SMTP_USER");
|
||||||
var pass = GetConfig(configs, "SMTP_PASS");
|
var pass = GetConfig(configs, "SMTP_PASS");
|
||||||
var sslStr = GetConfig(configs, "SMTP_SSL", "false"); // StartTls is usually implied by port 587 but simpler handling here
|
var sslStr = GetConfig(configs, "SMTP_SSL", "false");
|
||||||
var fromEmail = GetConfig(configs, "SMTP_FROM_EMAIL", user);
|
var fromEmail = GetConfig(configs, "SMTP_FROM_EMAIL", user);
|
||||||
var fromName = GetConfig(configs, "SMTP_FROM_NAME", "Zentral");
|
var fromName = GetConfig(configs, "SMTP_FROM_NAME", "Zentral");
|
||||||
|
|
||||||
@@ -80,8 +151,6 @@ public class SmtpEmailSender : IEmailSender
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var client = new SmtpClient();
|
using var client = new SmtpClient();
|
||||||
// Use SecureSocketOptions.Auto for flexibility or StartTls based on config
|
|
||||||
// For now, simple logic:
|
|
||||||
if (port == 465)
|
if (port == 465)
|
||||||
await client.ConnectAsync(host, port, SecureSocketOptions.SslOnConnect);
|
await client.ConnectAsync(host, port, SecureSocketOptions.SslOnConnect);
|
||||||
else if (port == 587)
|
else if (port == 587)
|
||||||
@@ -102,9 +171,6 @@ public class SmtpEmailSender : IEmailSender
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
await LogResultAsync(context, fromEmail, to, subject, "Failed", ex.Message);
|
await LogResultAsync(context, fromEmail, to, subject, "Failed", ex.Message);
|
||||||
// We do not rethrow the exception to avoid breaking the application flow,
|
|
||||||
// but in some cases it might be useful to return a result.
|
|
||||||
// For now, logging to DB is the requirement.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
|
||||||
<PackageReference Include="MailKit" Version="4.3.0" />
|
<PackageReference Include="MailKit" Version="4.3.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|||||||
@@ -62,6 +62,7 @@
|
|||||||
"cycles": "Cycles",
|
"cycles": "Cycles",
|
||||||
"mrp": "MRP",
|
"mrp": "MRP",
|
||||||
"administration": "Administration",
|
"administration": "Administration",
|
||||||
|
"emailConfig": "Email Configuration",
|
||||||
"movements": "Movements",
|
"movements": "Movements",
|
||||||
"stock": "Stock",
|
"stock": "Stock",
|
||||||
"inventory": "Inventory"
|
"inventory": "Inventory"
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
"cycles": "Cicli",
|
"cycles": "Cicli",
|
||||||
"mrp": "MRP",
|
"mrp": "MRP",
|
||||||
"administration": "Amministrazione",
|
"administration": "Amministrazione",
|
||||||
|
"emailConfig": "Configurazione Email",
|
||||||
"movements": "Movimenti",
|
"movements": "Movimenti",
|
||||||
"stock": "Giacenze",
|
"stock": "Giacenze",
|
||||||
"inventory": "Inventario"
|
"inventory": "Inventario"
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { useRealTimeUpdates } from "./hooks/useRealTimeUpdates";
|
|||||||
import { CollaborationProvider } from "./contexts/CollaborationContext";
|
import { CollaborationProvider } from "./contexts/CollaborationContext";
|
||||||
import { AppProvider } from "./contexts/AppContext";
|
import { AppProvider } from "./contexts/AppContext";
|
||||||
import { TabProvider } from "./contexts/TabContext";
|
import { TabProvider } from "./contexts/TabContext";
|
||||||
|
import EmailConfigPage from "./apps/communications/pages/SettingsPage";
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@@ -82,6 +83,10 @@ function App() {
|
|||||||
path="admin/custom-fields"
|
path="admin/custom-fields"
|
||||||
element={<CustomFieldsAdminPage />}
|
element={<CustomFieldsAdminPage />}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="admin/email-config"
|
||||||
|
element={<EmailConfigPage />}
|
||||||
|
/>
|
||||||
{/* Warehouse Module */}
|
{/* Warehouse Module */}
|
||||||
<Route
|
<Route
|
||||||
path="warehouse/*"
|
path="warehouse/*"
|
||||||
|
|||||||
@@ -2,14 +2,16 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { useForm, Controller } from 'react-hook-form';
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
import {
|
import {
|
||||||
Box, Paper, Typography, TextField, Button, Grid,
|
Box, Paper, Typography, TextField, Button, Grid,
|
||||||
Switch, FormControlLabel, Divider, Alert, Snackbar
|
Switch, FormControlLabel, Divider, Alert, Snackbar,
|
||||||
|
FormControl, InputLabel, Select, MenuItem
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Save, Send, Email } from '@mui/icons-material';
|
import { Save, Send, Email } from '@mui/icons-material';
|
||||||
import { communicationsService } from '../services/communicationsService';
|
import { communicationsService } from '../services/communicationsService';
|
||||||
import { SmtpConfig, TestEmail } from '../types';
|
import { SmtpConfig, TestEmail } from '../types';
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const { control, handleSubmit, reset } = useForm<SmtpConfig>();
|
const { control, handleSubmit, reset, watch } = useForm<SmtpConfig>();
|
||||||
|
const provider = watch('provider') || 'smtp';
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [testMode, setTestMode] = useState(false);
|
const [testMode, setTestMode] = useState(false);
|
||||||
const [testData, setTestData] = useState<TestEmail>({ to: '', subject: 'Test Email', body: 'Test content' });
|
const [testData, setTestData] = useState<TestEmail>({ to: '', subject: 'Test Email', body: 'Test content' });
|
||||||
@@ -64,59 +66,94 @@ export default function SettingsPage() {
|
|||||||
return (
|
return (
|
||||||
<Box p={3}>
|
<Box p={3}>
|
||||||
<Typography variant="h4" gutterBottom display="flex" alignItems="center" gap={2}>
|
<Typography variant="h4" gutterBottom display="flex" alignItems="center" gap={2}>
|
||||||
<Email fontSize="large" color="primary" /> Configurazione SMTP
|
<Email fontSize="large" color="primary" /> Configurazione Email
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Paper sx={{ p: 3, mb: 3 }}>
|
<Paper sx={{ p: 3, mb: 3 }}>
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
<Grid item xs={12} md={8}>
|
|
||||||
<Controller
|
|
||||||
name="host"
|
|
||||||
control={control}
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field }) => <TextField {...field} label="SMTP Host" fullWidth required />}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12} md={4}>
|
<Grid item xs={12} md={4}>
|
||||||
<Controller
|
<FormControl fullWidth>
|
||||||
name="port"
|
<InputLabel>Provider</InputLabel>
|
||||||
control={control}
|
<Controller
|
||||||
defaultValue={587}
|
name="provider"
|
||||||
render={({ field }) => <TextField {...field} label="Port" type="number" fullWidth required />}
|
control={control}
|
||||||
/>
|
defaultValue="smtp"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select {...field} label="Provider">
|
||||||
|
<MenuItem value="smtp">SMTP</MenuItem>
|
||||||
|
<MenuItem value="resend">Resend</MenuItem>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} md={6}>
|
{provider === 'smtp' && (
|
||||||
<Controller
|
<>
|
||||||
name="user"
|
<Grid item xs={12} md={8}>
|
||||||
control={control}
|
<Controller
|
||||||
defaultValue=""
|
name="host"
|
||||||
render={({ field }) => <TextField {...field} label="Username" fullWidth />}
|
control={control}
|
||||||
/>
|
defaultValue=""
|
||||||
</Grid>
|
render={({ field }) => <TextField {...field} label="SMTP Host" fullWidth required />}
|
||||||
<Grid item xs={12} md={6}>
|
|
||||||
<Controller
|
|
||||||
name="password"
|
|
||||||
control={control}
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field }) => <TextField {...field} label="Password" type="password" fullWidth />}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid item xs={12} md={4}>
|
|
||||||
<Controller
|
|
||||||
name="enableSsl"
|
|
||||||
control={control}
|
|
||||||
defaultValue={false}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<FormControlLabel
|
|
||||||
control={<Switch checked={value} onChange={onChange} />}
|
|
||||||
label="Enable SSL/TLS"
|
|
||||||
/>
|
/>
|
||||||
)}
|
</Grid>
|
||||||
/>
|
<Grid item xs={12} md={4}>
|
||||||
</Grid>
|
<Controller
|
||||||
|
name="port"
|
||||||
|
control={control}
|
||||||
|
defaultValue={587}
|
||||||
|
render={({ field }) => <TextField {...field} label="Port" type="number" fullWidth required />}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<Controller
|
||||||
|
name="user"
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field }) => <TextField {...field} label="Username" fullWidth />}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<Controller
|
||||||
|
name="password"
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field }) => <TextField {...field} label="Password" type="password" fullWidth />}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} md={4}>
|
||||||
|
<Controller
|
||||||
|
name="enableSsl"
|
||||||
|
control={control}
|
||||||
|
defaultValue={false}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<FormControlLabel
|
||||||
|
control={<Switch checked={value} onChange={onChange} />}
|
||||||
|
label="Enable SSL/TLS"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{provider === 'resend' && (
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Controller
|
||||||
|
name="resendApiKey"
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field }) => <TextField {...field} label="Resend API Key" type="password" fullWidth required />}
|
||||||
|
/>
|
||||||
|
<Typography variant="caption" color="textSecondary" sx={{ mt: 1, display: 'block' }}>
|
||||||
|
Ottieni la tua API Key su <a href="https://resend.com/api-keys" target="_blank" rel="noopener noreferrer">resend.com</a>
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Divider sx={{ my: 2 }} />
|
<Divider sx={{ my: 2 }} />
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export interface SmtpConfig {
|
|||||||
enableSsl: boolean;
|
enableSsl: boolean;
|
||||||
fromEmail: string;
|
fromEmail: string;
|
||||||
fromName: string;
|
fromName: string;
|
||||||
|
provider?: 'smtp' | 'resend';
|
||||||
|
resendApiKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TestEmail {
|
export interface TestEmail {
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
Receipt as ReceiptIcon,
|
Receipt as ReceiptIcon,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
Email as EmailIcon,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { useLanguage } from '../contexts/LanguageContext';
|
import { useLanguage } from '../contexts/LanguageContext';
|
||||||
@@ -193,6 +194,7 @@ export default function Sidebar({ onClose, isCollapsed = false, onToggleCollapse
|
|||||||
{ id: 'autocodes', label: t('menu.autoCodes'), icon: <AutoCodeIcon />, path: '/admin/auto-codes', translationKey: 'menu.autoCodes' },
|
{ id: 'autocodes', label: t('menu.autoCodes'), icon: <AutoCodeIcon />, path: '/admin/auto-codes', translationKey: 'menu.autoCodes' },
|
||||||
{ id: 'customfields', label: t('menu.customFields'), icon: <AutoCodeIcon />, path: '/admin/custom-fields', translationKey: 'menu.customFields' },
|
{ id: 'customfields', label: t('menu.customFields'), icon: <AutoCodeIcon />, path: '/admin/custom-fields', translationKey: 'menu.customFields' },
|
||||||
{ id: 'reports', label: t('menu.reports'), icon: <PrintIcon />, path: '/report-designer', appCode: 'report-designer', translationKey: 'menu.reports' },
|
{ id: 'reports', label: t('menu.reports'), icon: <PrintIcon />, path: '/report-designer', appCode: 'report-designer', translationKey: 'menu.reports' },
|
||||||
|
{ id: 'email-config', label: t('menu.emailConfig'), icon: <EmailIcon />, path: '/admin/email-config', translationKey: 'menu.emailConfig' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user