Update configuration and mailing service functionality

- Added HTTP client configuration for SendGrid in Program.cs.
- Cleaned up appsettings files for development and production environments.
- Modified MailingService to use IHttpClientFactory for SendGrid.
- Enhanced NewMailings component with cancel functionality and confirmation dialog.
- Updated project dependencies in .csproj and created package-lock.json.
This commit is contained in:
David Headrick 2025-04-09 11:57:32 -05:00
parent d977b3701b
commit 9703517974
9 changed files with 91 additions and 46 deletions

View File

@ -4,9 +4,19 @@ using Surge365.MassEmailReact.Domain.Entities;
using Surge365.MassEmailReact.Infrastructure.DapperMaps; using Surge365.MassEmailReact.Infrastructure.DapperMaps;
using Surge365.MassEmailReact.Infrastructure.Repositories; using Surge365.MassEmailReact.Infrastructure.Repositories;
using Surge365.MassEmailReact.Infrastructure.Services; using Surge365.MassEmailReact.Infrastructure.Services;
using System.Net;
using System.Security.Authentication;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient("SendGridClient", client =>
{
client.BaseAddress = new Uri("https://api.sendgrid.com/"); // Optional, for clarity
}).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13
});
// Add services to the container. // Add services to the container.
builder.Services.AddControllers(); builder.Services.AddControllers();

View File

@ -9,18 +9,10 @@
"Jwt": { "Jwt": {
"Secret": "Z9R5aFml+eRMeb7tyf8N9wCq3tZpS/EM6nGqOxlXPtOw4cJ3zS1AByczrIlD5F9d" "Secret": "Z9R5aFml+eRMeb7tyf8N9wCq3tZpS/EM6nGqOxlXPtOw4cJ3zS1AByczrIlD5F9d"
}, },
"AppCode": "MassEmailReactApi",
"AuthAppCode": "MassEmailWeb",
"EnvironmentCode": "UAT", "EnvironmentCode": "UAT",
"ConnectionStrings": { "ConnectionStrings": {
"Marketing.ConnectionString": "data source=uat.surge365.com;initial catalog=Marketing;User ID=ytb;Password=YTB()nl!n3;Encrypt=yes;TrustServerCertificate=yes;Connection Timeout=3;Application Name=##application_name##;", //TODO: Move this to development.json, on server should go somewhere secure. GET IT OUT OF GIT "Marketing.ConnectionString": "data source=uat.surge365.com;initial catalog=Marketing;User ID=ytb;Password=YTB()nl!n3;Encrypt=yes;TrustServerCertificate=yes;Connection Timeout=3;Application Name=##application_name##;", //TODO: Move this to development.json, on server should go somewhere secure. GET IT OUT OF GIT
"MassEmail.ConnectionString": "data source=uat.surge365.com;initial catalog=MassEmail;User ID=ytb;Password=YTB()nl!n3;Encrypt=yes;TrustServerCertificate=yes;Connection Timeout=3;Application Name=##application_name##;" //TODO: Move this to development.json, on server should go somewhere secure. GET IT OUT OF GIT "MassEmail.ConnectionString": "data source=uat.surge365.com;initial catalog=MassEmail;User ID=ytb;Password=YTB()nl!n3;Encrypt=yes;TrustServerCertificate=yes;Connection Timeout=3;Application Name=##application_name##;" //TODO: Move this to development.json, on server should go somewhere secure. GET IT OUT OF GIT
}, },
"TestTargetSql": "CREATE TABLE #columns\r\n(\r\n primary_key INT NOT NULL IDENTITY(1,1) PRIMARY KEY,\r\n name VARCHAR(255),\r\n data_type CHAR(1)\r\n)\r\nSELECT TOP 10 *\r\nINTO #list\r\nFROM ##database_name##..##view_name##\r\n##filter##\r\n\r\nDECLARE @row_count INT\r\nSELECT @row_count = COUNT(*)\r\nFROM ##database_name##..##view_name##\r\n##filter##\r\n\r\nDECLARE c_curs CURSOR FOR \r\nSELECT c.name AS column_name, t.name AS data_type\r\nFROM tempdb.sys.columns c\r\nINNER JOIN tempdb.sys.types t ON c.user_type_id = t.user_type_id\r\n AND t.name NOT IN ('text','ntext','image','binary','varbinary','image','cursor','timestamp','hierarchyid','sql_variant','xml','table')\r\nWHERE object_id = object_id('tempdb..#list') \r\n AND ((t.name IN ('char','varchar') AND c.max_length <= 255)\r\n OR (t.name IN ('nchar','nvarchar') AND c.max_length <= 510)\r\n OR (t.name NOT IN ('char','varchar','nchar','nvarchar')))\r\n \r\nOPEN c_curs\r\nDECLARE @column_name VARCHAR(255), @column_type VARCHAR(255)\r\n\r\nFETCH NEXT FROM c_curs INTO @column_name, @column_type\r\nWHILE(@@FETCH_STATUS = 0)\r\nBEGIN \r\n DECLARE @data_type CHAR(1) = 'S'\r\n IF(@column_type IN ('date','datetime','datetime2','datetimeoffset','smalldatetime','time'))\r\n BEGIN\r\n SET @data_type = 'D'\r\n END\r\n ELSE IF(@column_type IN ('bit'))\r\n BEGIN\r\n SET @data_type = 'B'\r\n END\r\n ELSE IF(@column_type IN ('bigint','numeric','smallint','decimal','smallmoney','int','tinyint','money','float','real'))\r\n BEGIN\r\n SET @data_type = 'N'\r\n END\r\n INSERT INTO #columns(name, data_type) VALUES(@column_name, @data_type)\r\n FETCH NEXT FROM c_curs INTO @column_name, @column_type\r\nEND\r\nCLOSE c_curs\r\nDEALLOCATE c_curs\r\nSELECT * FROM #columns ORDER BY primary_key\r\nSELECT * FROM #list\r\nSELECT @row_count AS row_count\r\nDROP TABLE #columns\r\nDROP TABLE #list", "DefaultUnsubscribeUrl": "https://uat.emailopentracking.surge365.com/unsubscribe.htm"
"ConnectionStringTemplate": "data source=##server_name##,##port##;initial catalog=##database_name##;User ID=##username##;Password=##password##;persist security info=False;packet size=4096;TrustServerCertificate=True;",
"DefaultUnsubscribeUrl": "https://uat.emailopentracking.surge365.com/unsubscribe.htm",
"SendGrid_TestMode": false,
"RegularExpression_Email": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
"SendGrid_Url": "smtp.sendgrid.net",
"SendGrid_Port": "587"
} }

View File

@ -10,9 +10,9 @@
"Secret": "1bXgXk7v/W9XksGoNiqWvM7+9/BERZonShxqoCVvdi8Ew47M1VFzJGA9sPMgkmn/HRmuZ83iytNsHXI6GkAb8g==" "Secret": "1bXgXk7v/W9XksGoNiqWvM7+9/BERZonShxqoCVvdi8Ew47M1VFzJGA9sPMgkmn/HRmuZ83iytNsHXI6GkAb8g=="
}, },
"EnvironmentCode": "UAT", "EnvironmentCode": "UAT",
"DefaultUnsubscribeUrl": "https://uat.emailopentracking.surge365.com/unsubscribe.htm", "ConnectionStrings": {
"SendGrid_TestMode": true, "Marketing.ConnectionString": "data source=localhost;initial catalog=Marketing;User ID=ytb;Password=YTB()nl!n3;Encrypt=yes;TrustServerCertificate=yes;Connection Timeout=3;Application Name=##application_name##;", //TODO: Move this to development.json, on server should go somewhere secure. GET IT OUT OF GIT
"RegularExpression_Email": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", "MassEmail.ConnectionString": "data source=localhost;initial catalog=MassEmail;User ID=ytb;Password=YTB()nl!n3;Encrypt=yes;TrustServerCertificate=yes;Connection Timeout=3;Application Name=##application_name##;" //TODO: Move this to development.json, on server should go somewhere secure. GET IT OUT OF GIT
"SendGrid_Url": "smtp.sendgrid.net", },
"SendGrid_Port": "587" "DefaultUnsubscribeUrl": "https://uat.emailopentracking.surge365.com/unsubscribe.htm"
} }

View File

@ -11,7 +11,7 @@
}, },
"AppCode": "MassEmailReactApi", "AppCode": "MassEmailReactApi",
"AuthAppCode": "MassEmailWeb", "AuthAppCode": "MassEmailWeb",
"EnvironmentCode": "UAT", "EnvironmentCode": "PRODUCTION",
"ConnectionStrings": { "ConnectionStrings": {
"Marketing.ConnectionString": "data source=localhost;initial catalog=Marketing;User ID=ytb;Password=YTB()nl!n3;Encrypt=yes;TrustServerCertificate=yes;Connection Timeout=3;Application Name=##application_name##;", //TODO: Move this to development.json, on server should go somewhere secure. GET IT OUT OF GIT "Marketing.ConnectionString": "data source=localhost;initial catalog=Marketing;User ID=ytb;Password=YTB()nl!n3;Encrypt=yes;TrustServerCertificate=yes;Connection Timeout=3;Application Name=##application_name##;", //TODO: Move this to development.json, on server should go somewhere secure. GET IT OUT OF GIT
"MassEmail.ConnectionString": "data source=localhost;initial catalog=MassEmail;User ID=ytb;Password=YTB()nl!n3;Encrypt=yes;TrustServerCertificate=yes;Connection Timeout=3;Application Name=##application_name##;" //TODO: Move this to development.json, on server should go somewhere secure. GET IT OUT OF GIT "MassEmail.ConnectionString": "data source=localhost;initial catalog=MassEmail;User ID=ytb;Password=YTB()nl!n3;Encrypt=yes;TrustServerCertificate=yes;Connection Timeout=3;Application Name=##application_name##;" //TODO: Move this to development.json, on server should go somewhere secure. GET IT OUT OF GIT
@ -20,7 +20,5 @@
"ConnectionStringTemplate": "data source=##server_name##,##port##;initial catalog=##database_name##;User ID=##username##;Password=##password##;persist security info=False;packet size=4096;TrustServerCertificate=True;", "ConnectionStringTemplate": "data source=##server_name##,##port##;initial catalog=##database_name##;User ID=##username##;Password=##password##;persist security info=False;packet size=4096;TrustServerCertificate=True;",
"DefaultUnsubscribeUrl": "https://emailopentracking.surge365.com/unsubscribe.htm", "DefaultUnsubscribeUrl": "https://emailopentracking.surge365.com/unsubscribe.htm",
"SendGrid_TestMode": false, "SendGrid_TestMode": false,
"RegularExpression_Email": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", "RegularExpression_Email": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
"SendGrid_Url": "smtp.sendgrid.net",
"SendGrid_Port": "587"
} }

View File

@ -0,0 +1,6 @@
{
"name": "Surge365.MassEmailReact.API",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View File

@ -15,6 +15,7 @@ namespace Surge365.MassEmailReact.Infrastructure.Services
{ {
public class MailingService : IMailingService public class MailingService : IMailingService
{ {
private readonly IHttpClientFactory _httpClientFactory;
private readonly ITargetService _targetService; private readonly ITargetService _targetService;
private readonly ITemplateService _templateService; private readonly ITemplateService _templateService;
private readonly IEmailDomainService _emailDomainService; private readonly IEmailDomainService _emailDomainService;
@ -41,22 +42,9 @@ namespace Surge365.MassEmailReact.Infrastructure.Services
return _config["RegularExpression_Email"] ?? ""; return _config["RegularExpression_Email"] ?? "";
} }
} }
private string SendGridUrl public MailingService(IHttpClientFactory httpClientFactory, IMailingRepository mailingRepository, ITargetService targetService, ITemplateService templateService, IEmailDomainService emailDomainService, IConfiguration config)
{
get
{
return _config["SendGrid_Url"] ?? "";
}
}
private int SendGridPort
{
get
{
return _config.GetValue<int>("SendGrid_Port");
}
}
public MailingService(IMailingRepository mailingRepository, ITargetService targetService, ITemplateService templateService, IEmailDomainService emailDomainService, IConfiguration config)
{ {
_httpClientFactory = httpClientFactory;
_mailingRepository = mailingRepository; _mailingRepository = mailingRepository;
_targetService = targetService; _targetService = targetService;
_templateService = templateService; _templateService = templateService;
@ -269,7 +257,8 @@ namespace Surge365.MassEmailReact.Infrastructure.Services
//string url = SendGridUrl; //string url = SendGridUrl;
//int port = SendGridPort; //int port = SendGridPort;
var client = new SendGridClient(password); var httpClient = _httpClientFactory.CreateClient("SendGridClient");
var client = new SendGridClient(httpClient, password);
var message = new SendGridMessage() { var message = new SendGridMessage() {
From = new EmailAddress(msg.From.Address, msg.From.DisplayName), From = new EmailAddress(msg.From.Address, msg.From.DisplayName),
Subject = msg.Subject, Subject = msg.Subject,

View File

@ -14,13 +14,14 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Dapper.FluentMap" Version="2.0.0" /> <PackageReference Include="Dapper.FluentMap" Version="2.0.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.1" /> <PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.2" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.3" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.2" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.2" /> <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.4" />
<PackageReference Include="SendGrid" Version="9.29.3" /> <PackageReference Include="SendGrid" Version="9.29.3" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.5.0" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.8.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -3,11 +3,13 @@ import { useSetupData, SetupData } from "@/context/SetupDataContext";
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
import RefreshIcon from '@mui/icons-material/Refresh'; import RefreshIcon from '@mui/icons-material/Refresh';
import CancelIcon from '@mui/icons-material/Cancel';
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton } from '@mui/material'; import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton } from '@mui/material';
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid'; import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
import Mailing from '@/types/mailing'; import Mailing from '@/types/mailing';
//import Template from '@/types/template'; //import Template from '@/types/template';
import MailingEdit from "@/components/modals/MailingEdit"; import MailingEdit from "@/components/modals/MailingEdit";
import ConfirmationDialog from "@/components/modals/ConfirmationDialog";
function NewMailings() { function NewMailings() {
const theme = useTheme(); const theme = useTheme();
@ -19,17 +21,24 @@ function NewMailings() {
const [mailings, setMailings] = useState<Mailing[]>([]); const [mailings, setMailings] = useState<Mailing[]>([]);
const [selectedRow, setSelectedRow] = useState<Mailing | null>(null); const [selectedRow, setSelectedRow] = useState<Mailing | null>(null);
const [open, setOpen] = useState<boolean>(false); const [open, setOpen] = useState<boolean>(false);
const [confirmDialogOpen, setConfirmDialogOpen] = useState<boolean>(false);
const [mailingToCancel, setMailingToCancel] = useState<Mailing | null>(null);
const columns: GridColDef<Mailing>[] = [ const columns: GridColDef<Mailing>[] = [
{ {
field: "actions", field: "actions",
headerName: "", headerName: "",
sortable: false, sortable: false,
width: 60, width: 100,
renderCell: (params: GridRenderCellParams<Mailing>) => ( renderCell: (params: GridRenderCellParams<Mailing>) => (
<IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(params.row); }}> <>
<EditIcon /> <IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(params.row); }}>
</IconButton> <EditIcon />
</IconButton>
<IconButton color="secondary" onClick={(e) => { e.stopPropagation(); handleCancelClick(params.row); }}>
<CancelIcon />
</IconButton>
</>
), ),
}, },
{ field: "id", headerName: "ID", width: 80 }, { field: "id", headerName: "ID", width: 80 },
@ -76,6 +85,34 @@ function NewMailings() {
updateMailings(updatedRow); updateMailings(updatedRow);
}; };
const handleCancelClick = (row: Mailing) => {
setMailingToCancel(row);
setConfirmDialogOpen(true);
};
const handleCancelConfirm = async () => {
if (!mailingToCancel) return;
try {
const response = await fetch(`/api/mailings/${mailingToCancel.id}/cancel`, { method: 'POST' });
if (response.ok) {
setMailings((prev) => prev.filter(m => m.id !== mailingToCancel.id));
} else {
console.error("Failed to cancel mailing");
}
} catch (error) {
console.error("Error cancelling mailing:", error);
} finally {
setConfirmDialogOpen(false);
setMailingToCancel(null);
}
};
const handleCancelDialogClose = () => {
setConfirmDialogOpen(false);
setMailingToCancel(null);
};
useEffect(() => { useEffect(() => {
reloadMailings(); reloadMailings();
}, []); }, []);
@ -117,6 +154,9 @@ function NewMailings() {
<IconButton onClick={(e) => { e.stopPropagation(); handleEdit(row); }}> <IconButton onClick={(e) => { e.stopPropagation(); handleEdit(row); }}>
<EditIcon /> <EditIcon />
</IconButton> </IconButton>
<IconButton color="secondary" onClick={() => handleCancelClick(row)}>
<CancelIcon />
</IconButton>
</Box> </Box>
</Card> </Card>
))} ))}
@ -171,6 +211,15 @@ function NewMailings() {
onSave={handleUpdateRow} onSave={handleUpdateRow}
/> />
)} )}
{confirmDialogOpen && (
<ConfirmationDialog
open={confirmDialogOpen}
title="Cancel Mailing"
message={`Are you sure you want to cancel the mailing "${mailingToCancel?.name}"? This action cannot be undone.`}
onConfirm={handleCancelConfirm}
onCancel={handleCancelDialogClose}
/>
)}
</Box> </Box>
); );
} }

View File

@ -7,7 +7,7 @@ import { Box, useTheme, useMediaQuery, CircularProgress, IconButton, List, Card,
import { DataGrid, GridColDef, GridRenderCellParams, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid'; import { DataGrid, GridColDef, GridRenderCellParams, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
import Mailing from '@/types/mailing'; import Mailing from '@/types/mailing';
import MailingEdit from "@/components/modals/MailingEdit"; import MailingEdit from "@/components/modals/MailingEdit";
import MailingView from "@/components/modals/MailingView"; // Assume this is a new read-only view component import MailingView from "@/components/modals/MailingView";
import ConfirmationDialog from "@/components/modals/ConfirmationDialog"; import ConfirmationDialog from "@/components/modals/ConfirmationDialog";
function ScheduleMailings() { function ScheduleMailings() {