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()
|
||||
{
|
||||
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);
|
||||
|
||||
var dto = new SmtpConfigDto
|
||||
@@ -36,7 +36,9 @@ public class CommunicationsController : ControllerBase
|
||||
Password = GetValue(configs, "SMTP_PASS"),
|
||||
EnableSsl = bool.Parse(GetValue(configs, "SMTP_SSL", "false")),
|
||||
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);
|
||||
@@ -53,6 +55,9 @@ public class CommunicationsController : ControllerBase
|
||||
await SetConfig("SMTP_FROM_EMAIL", dto.FromEmail);
|
||||
await SetConfig("SMTP_FROM_NAME", dto.FromName);
|
||||
|
||||
await SetConfig("EMAIL_PROVIDER", dto.Provider);
|
||||
await SetConfig("RESEND_API_KEY", dto.ResendApiKey);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@@ -9,4 +9,8 @@ public class SmtpConfigDto
|
||||
public bool EnableSsl { get; set; } = false;
|
||||
public string FromEmail { 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));
|
||||
|
||||
// Services
|
||||
builder.Services.AddHttpClient();
|
||||
builder.Services.AddScoped<EventoCostiService>();
|
||||
builder.Services.AddScoped<DemoDataService>();
|
||||
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");
|
||||
});
|
||||
|
||||
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 =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
||||
@@ -7,6 +7,9 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using MimeKit;
|
||||
using MailKit.Net.Smtp;
|
||||
using MailKit.Security;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Net.Http;
|
||||
using Zentral.Domain.Entities.Communications;
|
||||
using Zentral.Domain.Interfaces;
|
||||
using Zentral.Infrastructure.Data;
|
||||
@@ -16,10 +19,12 @@ namespace Zentral.Infrastructure.Services;
|
||||
public class SmtpEmailSender : IEmailSender
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public SmtpEmailSender(IServiceScopeFactory scopeFactory)
|
||||
public SmtpEmailSender(IServiceScopeFactory scopeFactory, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
public async Task SendEmailAsync(string to, string subject, string body, bool isHtml = true)
|
||||
@@ -34,14 +39,80 @@ public class SmtpEmailSender : IEmailSender
|
||||
|
||||
// 1. Get Configuration
|
||||
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);
|
||||
|
||||
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 portStr = GetConfig(configs, "SMTP_PORT", "587");
|
||||
var user = GetConfig(configs, "SMTP_USER");
|
||||
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 fromName = GetConfig(configs, "SMTP_FROM_NAME", "Zentral");
|
||||
|
||||
@@ -80,8 +151,6 @@ public class SmtpEmailSender : IEmailSender
|
||||
try
|
||||
{
|
||||
using var client = new SmtpClient();
|
||||
// Use SecureSocketOptions.Auto for flexibility or StartTls based on config
|
||||
// For now, simple logic:
|
||||
if (port == 465)
|
||||
await client.ConnectAsync(host, port, SecureSocketOptions.SslOnConnect);
|
||||
else if (port == 587)
|
||||
@@ -102,9 +171,6 @@ public class SmtpEmailSender : IEmailSender
|
||||
catch (Exception ex)
|
||||
{
|
||||
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 Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
|
||||
<PackageReference Include="MailKit" Version="4.3.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"cycles": "Cycles",
|
||||
"mrp": "MRP",
|
||||
"administration": "Administration",
|
||||
"emailConfig": "Email Configuration",
|
||||
"movements": "Movements",
|
||||
"stock": "Stock",
|
||||
"inventory": "Inventory"
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"cycles": "Cicli",
|
||||
"mrp": "MRP",
|
||||
"administration": "Amministrazione",
|
||||
"emailConfig": "Configurazione Email",
|
||||
"movements": "Movimenti",
|
||||
"stock": "Giacenze",
|
||||
"inventory": "Inventario"
|
||||
|
||||
@@ -25,6 +25,7 @@ import { useRealTimeUpdates } from "./hooks/useRealTimeUpdates";
|
||||
import { CollaborationProvider } from "./contexts/CollaborationContext";
|
||||
import { AppProvider } from "./contexts/AppContext";
|
||||
import { TabProvider } from "./contexts/TabContext";
|
||||
import EmailConfigPage from "./apps/communications/pages/SettingsPage";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
@@ -82,6 +83,10 @@ function App() {
|
||||
path="admin/custom-fields"
|
||||
element={<CustomFieldsAdminPage />}
|
||||
/>
|
||||
<Route
|
||||
path="admin/email-config"
|
||||
element={<EmailConfigPage />}
|
||||
/>
|
||||
{/* Warehouse Module */}
|
||||
<Route
|
||||
path="warehouse/*"
|
||||
|
||||
@@ -2,14 +2,16 @@ import React, { useEffect, useState } from 'react';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import {
|
||||
Box, Paper, Typography, TextField, Button, Grid,
|
||||
Switch, FormControlLabel, Divider, Alert, Snackbar
|
||||
Switch, FormControlLabel, Divider, Alert, Snackbar,
|
||||
FormControl, InputLabel, Select, MenuItem
|
||||
} from '@mui/material';
|
||||
import { Save, Send, Email } from '@mui/icons-material';
|
||||
import { communicationsService } from '../services/communicationsService';
|
||||
import { SmtpConfig, TestEmail } from '../types';
|
||||
|
||||
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 [testMode, setTestMode] = useState(false);
|
||||
const [testData, setTestData] = useState<TestEmail>({ to: '', subject: 'Test Email', body: 'Test content' });
|
||||
@@ -64,59 +66,94 @@ export default function SettingsPage() {
|
||||
return (
|
||||
<Box p={3}>
|
||||
<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>
|
||||
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<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}>
|
||||
<Controller
|
||||
name="port"
|
||||
control={control}
|
||||
defaultValue={587}
|
||||
render={({ field }) => <TextField {...field} label="Port" type="number" fullWidth required />}
|
||||
/>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Provider</InputLabel>
|
||||
<Controller
|
||||
name="provider"
|
||||
control={control}
|
||||
defaultValue="smtp"
|
||||
render={({ field }) => (
|
||||
<Select {...field} label="Provider">
|
||||
<MenuItem value="smtp">SMTP</MenuItem>
|
||||
<MenuItem value="resend">Resend</MenuItem>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
</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"
|
||||
{provider === 'smtp' && (
|
||||
<>
|
||||
<Grid item xs={12} md={8}>
|
||||
<Controller
|
||||
name="host"
|
||||
control={control}
|
||||
defaultValue=""
|
||||
render={({ field }) => <TextField {...field} label="SMTP Host" fullWidth required />}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<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}>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
@@ -6,6 +6,8 @@ export interface SmtpConfig {
|
||||
enableSsl: boolean;
|
||||
fromEmail: string;
|
||||
fromName: string;
|
||||
provider?: 'smtp' | 'resend';
|
||||
resendApiKey?: string;
|
||||
}
|
||||
|
||||
export interface TestEmail {
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
Receipt as ReceiptIcon,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Email as EmailIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
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: '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: 'email-config', label: t('menu.emailConfig'), icon: <EmailIcon />, path: '/admin/email-config', translationKey: 'menu.emailConfig' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user