Refactor authentication and restructure project architecture

This commit removes the `AuthenticationController.cs` and related DTOs, indicating a shift in authentication handling. The `BaseController.cs` has been updated to remove the authorization attribute, affecting access control. Multiple controllers have been restructured to reference `Surge365.Core.Controllers`.

Significant changes in `Program.cs` enhance security and service management with new middleware and JWT configurations. The project file now includes references to `Surge365.Core`, and the `IAuthService` interface has been updated accordingly.

React components have been modified to support the new authentication flow, including token refresh handling. The `customFetch.ts` utility has been improved for better session management. Mapping classes have been introduced or updated for improved entity mapping.

Overall, these changes enhance the application's architecture, security, and data handling processes.
This commit is contained in:
David Headrick 2025-06-28 09:26:41 -05:00
parent 7faac8b448
commit ba01cfcaf7
66 changed files with 1182 additions and 1925 deletions

View File

@ -1,97 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Surge365.MassEmailReact.API.Controllers;
using Surge365.MassEmailReact.Application.DTOs;
using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities;
namespace Surge365.MassEmailReact.API.Controllers
{
[AllowAnonymous]
public class AuthenticationController : BaseController
{
private readonly IAuthService _authService;
public AuthenticationController(IAuthService authService)
{
_authService = authService;
}
[HttpPost("logout")]
public IActionResult Logout()
{
Response.Cookies.Append("refreshToken", "", new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Strict,
Expires = DateTimeOffset.UtcNow.AddDays(-1) // Expire immediately
});
Response.Cookies.Append("accessToken", "", new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Strict,
Expires = DateTimeOffset.UtcNow.AddDays(-1) // Expire immediately
});
return Ok(new { message = "Logged out successfully" });
}
[HttpPost("authenticate")]
public async Task<IActionResult> Authenticate([FromBody] LoginRequest request)
{
var authResponse = await _authService.Authenticate(request.Username, request.Password);
if (!authResponse.authenticated)
return Unauthorized(new { message = authResponse.errorMessage });
else if(authResponse.data == null)
return Unauthorized(new { message = "Invalid credentials" });
//TODO: Store refresh token in DB
var cookieOptions = new CookieOptions
{
HttpOnly = true, // Prevents JavaScript access (mitigates XSS)
Secure = true, // Ensures cookie is only sent over HTTPS
SameSite = SameSiteMode.Strict, // Mitigates CSRF by restricting cross-site usage
Expires = DateTimeOffset.UtcNow.AddDays(7)
};
Response.Cookies.Append("refreshToken", authResponse.data.Value.refreshToken, cookieOptions);
Response.Cookies.Append("accessToken", authResponse.data.Value.accessToken, cookieOptions);
//TODO: Store user in session
return Ok(new { success = true, authResponse.data.Value.accessToken, authResponse.data.Value.user });
}
[HttpPost("refreshtoken")]
public async Task<IActionResult> RefreshToken()
{
var refreshToken = Request.Cookies["refreshToken"];
if (string.IsNullOrWhiteSpace(refreshToken))
return Unauthorized("Invalid refresh token");
var authResponse = await _authService.Authenticate(refreshToken);
if (!authResponse.authenticated)
return Unauthorized(new { message = authResponse.errorMessage });
else if (authResponse.data == null)
return Unauthorized(new { message = "Invalid credentials" });
var cookieOptions = new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Strict,
Expires = DateTimeOffset.UtcNow.AddDays(7)
};
Response.Cookies.Append("refreshToken", authResponse.data.Value.refreshToken, cookieOptions);
Response.Cookies.Append("accessToken", authResponse.data.Value.accessToken, cookieOptions);
return Ok(new { accessToken = authResponse.data.Value.accessToken });
}
[HttpPost("generatepasswordrecovery")]
public IActionResult GeneratePasswordRecovery([FromBody] GeneratePasswordRecoveryRequest request)
{
return Ok(new { });
}
}
}

View File

@ -1,13 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Surge365.MassEmailReact.API.Controllers
{
[Route("[controller]")]
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class BaseController : ControllerBase
{
}
}

View File

@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Surge365.Core.Controllers;
using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
using System.Net.Mail; using System.Net.Mail;

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Surge365.Core.Controllers;
using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
using System.Threading.Tasks; using System.Threading.Tasks;

View File

@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient;
using Surge365.Core.Controllers;
using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
using Surge365.MassEmailReact.Domain.Enums; using Surge365.MassEmailReact.Domain.Enums;

View File

@ -1,6 +1,7 @@
using Microsoft.AspNetCore.DataProtection.KeyManagement; using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Surge365.Core.Controllers;
using Surge365.MassEmailReact.Application.DTOs; using Surge365.MassEmailReact.Application.DTOs;
using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;

View File

@ -1,6 +1,7 @@
using Microsoft.AspNetCore.DataProtection.KeyManagement; using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Surge365.Core.Controllers;
using Surge365.MassEmailReact.Application.DTOs; using Surge365.MassEmailReact.Application.DTOs;
using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;

View File

@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Surge365.Core.Controllers;
using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;

View File

@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Surge365.Core.Controllers;
using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;

View File

@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Surge365.Core.Controllers;
using Surge365.MassEmailReact.Application.DTOs; using Surge365.MassEmailReact.Application.DTOs;
using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;

View File

@ -1,21 +1,24 @@
using Azure.Identity; using Azure.Identity;
using Microsoft.AspNetCore.Identity;
using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities;
using Surge365.MassEmailReact.Infrastructure;
using Surge365.MassEmailReact.Infrastructure.DapperMaps;
using Surge365.MassEmailReact.Infrastructure.Repositories;
using Surge365.MassEmailReact.Infrastructure.Services;
using Surge365.MassEmailReact.Infrastructure.Middleware;
using System.Net;
using System.Security.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Surge365.Core.Authentication;
using Surge365.Core.Extensions;
using Surge365.Core.Services;
using Surge365.Core.Middleware;
using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Infrastructure.Repositories;
using Surge365.MassEmailReact.Infrastructure.Services;
using System.Security.Authentication;
using System.Security.Claims;
using System.Text; using System.Text;
using Surge365.Core.Interfaces;
using Surge365.MassEmailReact.Infrastructure.EntityMaps;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.AddCustomConfigurationSources();
WebApplication? app = null; WebApplication? app = null;
try try
{ {
@ -50,21 +53,41 @@ try
IssuerSigningKey = new SymmetricSecurityKey(jwtKey), IssuerSigningKey = new SymmetricSecurityKey(jwtKey),
ClockSkew = TimeSpan.Zero // Optional: no grace period for expiration ClockSkew = TimeSpan.Zero // Optional: no grace period for expiration
}; };
// Read JWT from accessToken cookie
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var token = context.Request.Cookies["accessToken"];
if (!string.IsNullOrEmpty(token))
{
context.Token = token;
}
return Task.CompletedTask;
}
};
}); });
builder.Services.AddHttpContextAccessor();
Factory.RegisterDefaultServices(builder.Services,
adminContextProvider: provider =>
{
var httpContextAccessor = provider.GetRequiredService<IHttpContextAccessor>();
var httpContext = httpContextAccessor.HttpContext;
if (httpContext == null)
return new AdminContext();
string? adminId = null;
string? ipAddress = httpContext?.Connection?.RemoteIpAddress?.ToString();
string? appVersion = httpContext?.Request.Headers["app-version"].ToString();
string? deviceInfo = httpContext?.Request.Headers["User-Agent"].ToString();
if (httpContext?.User?.Identity?.IsAuthenticated == true)
{
adminId = httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? httpContext.User.FindFirst("sub")?.Value;
}
return new AdminContext
{
LoggedIn = !string.IsNullOrWhiteSpace(adminId),
AdminId = adminId,
IpAddress = ipAddress,
AppVersion = appVersion,
DeviceInfo = deviceInfo
};
}
);
builder.Services.AddHttpClient("SendGridClient", client => builder.Services.AddHttpClient("SendGridClient", client =>
{ {
client.BaseAddress = new Uri("https://api.sendgrid.com/"); // Optional, for clarity client.BaseAddress = new Uri("https://api.sendgrid.com/"); // Optional, for clarity
@ -74,10 +97,10 @@ try
}); });
// Add services to the container. // Add services to the container.
builder.Services.AddControllers(); builder.Services.AddControllers().AddApplicationPart(typeof(Surge365.Core.Controllers.AuthenticationController).Assembly);
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi(); builder.Services.AddOpenApi();
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<ITargetService, TargetService>(); builder.Services.AddScoped<ITargetService, TargetService>();
builder.Services.AddScoped<ITargetRepository, TargetRepository>(); builder.Services.AddScoped<ITargetRepository, TargetRepository>();
builder.Services.AddScoped<IServerService, ServerService>(); builder.Services.AddScoped<IServerService, ServerService>();
@ -95,10 +118,12 @@ try
builder.Services.AddScoped<IMailingService, MailingService>(); builder.Services.AddScoped<IMailingService, MailingService>();
builder.Services.AddScoped<IMailingRepository, MailingRepository>(); builder.Services.AddScoped<IMailingRepository, MailingRepository>();
EntityMapperConfiguration.ConfigureCustomMaps();
app = builder.Build(); app = builder.Build();
app.UseCustomExceptionHandler(); app.UseCustomExceptionHandler();
app.UseDefaultFiles(); app.UseDefaultFiles();
app.MapStaticAssets(); app.MapStaticAssets();
@ -109,17 +134,17 @@ try
} }
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.MapControllers(); app.MapControllers();
DapperConfiguration.ConfigureMappings();
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggingService appLoggingService = new LoggingService(builder.Configuration); Console.WriteLine($"Error during application startup: {ex.Message}");
LoggingService appLoggingService = new LoggingService(builder.Configuration, new DataAccessFactory(builder.Configuration));
appLoggingService.LogError(ex).Wait(); appLoggingService.LogError(ex).Wait();
return; return;
} }

View File

@ -18,6 +18,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\Core\Surge365.Core\Surge365.Core.csproj" />
<ProjectReference Include="..\Surge365.MassEmailReact.Application\Surge365.MassEmailReact.Application.csproj" /> <ProjectReference Include="..\Surge365.MassEmailReact.Application\Surge365.MassEmailReact.Application.csproj" />
<ProjectReference Include="..\Surge365.MassEmailReact.Infrastructure\Surge365.MassEmailReact.Infrastructure.csproj" /> <ProjectReference Include="..\Surge365.MassEmailReact.Infrastructure\Surge365.MassEmailReact.Infrastructure.csproj" />
</ItemGroup> </ItemGroup>

View File

@ -1,16 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Application.DTOs.AuthApi
{
public class AuthResponse
{
public string accessToken { get; set; } = "";
public string refreshToken { get; set; } = "";
public User? user { get; set; }
public string message { get; set; } = "";
}
}

View File

@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Application.DTOs.AuthApi
{
public class AuthenticateApiRequest
{
public required string appCode { get; set; }
public required string username { get; set; }
public required string password { get; set; }
}
}

View File

@ -1,14 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Application.DTOs.AuthApi
{
public class RefreshTokenApiRequest
{
public string appCode { get; set; } = "";
public string refreshToken { get; set; } = "";
}
}

View File

@ -1,22 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Application.DTOs.AuthApi
{
public class User
{
public int? UserKey { get; set; }
public Guid UserId { get; set; }
public string Username { get; set; } = "";
public string FirstName { get; set; } = "";
public string MiddleInitial { get; set; } = "";
public string LastName { get; set; } = "";
public bool IsActive { get; set; }
public List<string> Roles { get; set; } = new List<string>();
public User() { }
}
}

View File

@ -1,13 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Application.DTOs
{
public class GeneratePasswordRecoveryRequest
{
public required string Username { get; set; }
}
}

View File

@ -1,14 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Application.DTOs
{
public class LoginRequest
{
public required string Username { get; set; }
public required string Password { get; set; }
}
}

View File

@ -1,14 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Application.DTOs
{
public class RefreshTokenRequest
{
public required string RefreshToken { get; set; }
}
}

View File

@ -1,10 +0,0 @@
using Surge365.MassEmailReact.Application.DTOs.AuthApi;
namespace Surge365.MassEmailReact.Application.Interfaces
{
public interface IAuthService
{
Task<(bool authenticated, (User user, string accessToken, string refreshToken)? data, string errorMessage)> Authenticate(string username, string password);
Task<(bool authenticated, (User user, string accessToken, string refreshToken)? data, string errorMessage)> Authenticate(string refreshToken);
}
}

View File

@ -1,21 +0,0 @@
using Surge365.MassEmailReact.Application.DTOs.AuthApi;
namespace Surge365.MassEmailReact.Application.Interfaces
{
public enum LogLevels //TODO: Move all this to Surge365.Core (new project)
{
Fatal = 1,
Error,
Warn,
Info,
Debug
}
public interface ILoggingService
{
Task<bool> InsertLog(LogLevels level, string logger, int? userKey, string task, string message,
string exceptionStack, string exceptionMessage, string exceptionInnerMessage,
string customMessage1, string customMessage2, string customMessage3,
string customMessage4, string customMessage5);
Task<bool> LogError(Exception ex);
}
}

View File

@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Domain.Enums
{
public enum AuthResult
{
Success = 0,
LoginNotFound = 1,
LoginLocked = 2,
InvalidPassword = 3,
UnexpectedError = 4,
NotAllowed = 5
}
}

View File

@ -1,23 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Domain.Enums.Extensions
{
public static class AuthResultExtensions
{
public static string GetMessage(this AuthResult response)
{
return response switch
{
AuthResult.Success => "Login successful.",
AuthResult.InvalidPassword or AuthResult.LoginNotFound => "Invalid username and/or password.",
AuthResult.LoginLocked => "User has been locked out.",
AuthResult.NotAllowed => "User has not been granted access to this application.",
AuthResult.UnexpectedError or _ => "Unexpected error."
};
}
}
}

View File

@ -6,4 +6,8 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Folder Include="Enums\Extensions\" />
</ItemGroup>
</Project> </Project>

View File

@ -1,36 +0,0 @@
using Dapper;
using Dapper.FluentMap;
using Surge365.MassEmailReact.Domain.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Infrastructure.DapperMaps
{
public class DapperConfiguration
{
public static void ConfigureMappings()
{
SqlMapper.AddTypeHandler(new JsonListStringTypeHandler());
FluentMapper.Initialize(config =>
{
config.AddMap(new TargetMap());
config.AddMap(new TargetColumnMap());
config.AddMap(new ServerMap());
config.AddMap(new TestEmailListMap());
config.AddMap(new BouncedEmailMap());
config.AddMap(new UnsubscribeUrlMap());
config.AddMap(new TemplateMap());
config.AddMap(new EmailDomainMap());
config.AddMap(new MailingMap());
config.AddMap(new MailingEmailMap());
config.AddMap(new MailingTemplateMap());
config.AddMap(new MailingTargetMap());
config.AddMap(new MailingStatisticMap());
});
}
}
}

View File

@ -1,30 +0,0 @@
using Dapper;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Infrastructure.DapperMaps
{
public class JsonListStringTypeHandler : SqlMapper.TypeHandler<List<string>>
{
public override List<string> Parse(object value)
{
if (value == null || value == DBNull.Value)
return new List<string>();
string json = value.ToString() ?? "";
return string.IsNullOrEmpty(json)
? new List<string>()
: JsonSerializer.Deserialize<List<string>>(json) ?? new List<string>();
}
public override void SetValue(IDbDataParameter parameter, List<string> value)
{
parameter.Value = value != null ? JsonSerializer.Serialize(value) : DBNull.Value;
}
}
}

View File

@ -1,437 +0,0 @@
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection.PortableExecutable;
using System.Text;
using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Infrastructure
{
public class DataAccess
{
private string GetConnectionString(string connectionStringName)
{
if (_configuration == null)
return "";
return _configuration[$"ConnectionStrings:{connectionStringName}"] ?? "";
}
public DataAccess(IConfiguration configuration, string connectionStringName)
{
_configuration = configuration;
_connectionString = GetConnectionString(connectionStringName).Replace("##application_code##", Utilities.GetAppCode(configuration));
}
internal IConfiguration? _configuration;
internal Guid _sessionID = Guid.NewGuid();
internal string _connectionString;
internal int _timeout = 30;
internal SqlTransaction? _transaction;
internal SqlConnection? _connection;
internal bool _transactionStarted;
internal Object? _transactionStarter;
public string ConnectionString
{
get { return _connectionString; }
set { _connectionString = value; }
}
public int Timeout
{
get { return _timeout; }
set { _timeout = value; }
}
public bool TransactionStarted
{
get { return _transactionStarted; }
internal set { _transactionStarted = value; }
}
public Object? TransactionStarter
{
get { return _transactionStarter; }
internal set { _transactionStarter = value; }
}
#region Non-Async
internal void OpenConnection()
{
_connection = new SqlConnection(_connectionString);
_connection.Open();
}
internal void CloseConnection()
{
if (_connection == null)
return;
_connection.Close();
_connection.Dispose();
_connection = null;
}
public SqlTransaction BeginTransaction(Object sender)
{
if (_transaction != null)
return _transaction;
OpenConnection();
ArgumentNullException.ThrowIfNull(_connection);
_transaction = _connection.BeginTransaction();
_transactionStarted = true;
_transactionStarter = sender;
return _transaction;
}
public void RollbackTransaction(Object sender)
{
if (_transaction != null && sender == _transactionStarter)
{
_transaction.Rollback();
_transaction = null;
CloseConnection();
_transactionStarter = null;
_transactionStarted = false;
}
}
public void CommitTransaction(Object sender)
{
if (_transaction != null && sender == _transactionStarter)
{
_transaction.Commit();
_transaction = null;
CloseConnection();
_transactionStarter = null;
_transactionStarted = false;
}
}
public DataSet CallRetrievalProcedure(List<SqlParameter> parameters, string proc, int timeoutSeconds = 0)
{
DataSet ds = new DataSet();
bool createdConnection = false;
if (timeoutSeconds == 0)
timeoutSeconds = _timeout;
try
{
if (_connection == null)
{
createdConnection = true;
OpenConnection();
}
using SqlCommand cmd = new SqlCommand(proc, _connection);
cmd.Transaction = _transaction;
if (parameters != null)
{
foreach (SqlParameter p in parameters)
{
if (p != null)
cmd.Parameters.Add(p);
}
}
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandTimeout = timeoutSeconds;
using SqlDataAdapter da = new SqlDataAdapter(cmd);
try
{
da.Fill(ds);
}
catch (Exception ex)
{
string message = String.Format("Unable to retrieve data from proc: {0}", proc);
LogError(ex, message, "CallRetrievalProcedure");
throw;
}
if (createdConnection)
{
CloseConnection();
}
}
catch (Exception)
{
if (createdConnection)
{
CloseConnection();
}
throw;
}
return ds;
}
public int CallActionProcedure(List<SqlParameter> parameters, string proc, int timeoutSeconds = 0)
{
int iReturnVal = -1;
if (timeoutSeconds == 0)
timeoutSeconds = _timeout;
bool createdConnection = false;
try
{
if (_connection == null)
{
createdConnection = true;
OpenConnection();
}
using SqlCommand cmd = new SqlCommand(proc, _connection);
cmd.Transaction = _transaction;
if (parameters != null)
{
foreach (SqlParameter p in parameters)
{
if (p != null)
cmd.Parameters.Add(p);
}
}
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandTimeout = timeoutSeconds;
try
{
iReturnVal = cmd.ExecuteNonQuery();
}
catch (Exception ex)
{
string message = String.Format("Unable to execute proc: {0}", proc);
LogError(ex, message, "CallActionProcedure");
throw;
}
}
catch (Exception)
{
if (createdConnection)
{
CloseConnection();
}
throw;
}
if (createdConnection)
{
CloseConnection();
}
return iReturnVal;
}
private static void LogError(Exception ex, string message, string task)
{
Exception ex1 = ex;
if (ex == null)
ex1 = new Exception(message);
else if (!string.IsNullOrEmpty(message))
ex1 = new Exception(message, ex);
//Log
}
#endregion
#region Async
internal async Task OpenConnectionAsync()
{
//_connection = new SqlConnection(_connectionString);
//await _connection.OpenAsync();
await Task.Run(() => OpenConnectionWithRetry());
}
internal void OpenConnectionWithRetry(short maxRetries = 3, int totalTimeoutMs = 1000)
{
int attempt = 0;
while (attempt < maxRetries)
{
using (var cts = new CancellationTokenSource(totalTimeoutMs)) // Total cap, e.g., 3 seconds
{
try
{
Console.WriteLine($"Attempt {attempt + 1}...");
_connection = new SqlConnection(_connectionString);
_connection.OpenAsync(cts.Token).Wait(); // Use async with cancellation
return;
}
catch (Exception ex)
{
attempt++;
if (attempt == maxRetries)
{
Console.WriteLine($"Failed after {attempt} attempts: {ex.Message}");
throw;
}
Console.WriteLine($"Retrying after failure: {ex.Message}");
Thread.Sleep(1000); // Delay between retries
}
}
}
}
internal async Task CloseConnectionAsync()
{
if (_connection == null)
return;
await _connection.CloseAsync();
_connection.Dispose();
_connection = null;
}
public async Task<SqlTransaction> BeginTransactionAsync(Object sender)
{
if (_transaction != null)
return _transaction;
await OpenConnectionAsync();
ArgumentNullException.ThrowIfNull(_connection);
_transaction = _connection.BeginTransaction();
_transactionStarted = true;
_transactionStarter = sender;
return _transaction;
}
public async Task RollbackTransactionAsync(Object sender)
{
if (_transaction != null && sender == _transactionStarter)
{
await _transaction.RollbackAsync();
_transaction = null;
await CloseConnectionAsync();
_transactionStarter = null;
_transactionStarted = false;
}
}
public async Task CommitTransactionAsync(Object sender)
{
if (_transaction != null && sender == _transactionStarter)
{
await _transaction.CommitAsync();
_transaction = null;
await CloseConnectionAsync();
_transactionStarter = null;
_transactionStarted = false;
}
}
public async Task<DataSet> CallRetrievalProcedureAsync(List<SqlParameter> parameters, string proc, int timeoutSeconds = 0)
{
DataSet ds = new DataSet();
bool createdConnection = false;
if (timeoutSeconds == 0)
timeoutSeconds = _timeout;
try
{
if (_connection == null)
{
createdConnection = true;
await OpenConnectionAsync();
}
using SqlCommand cmd = new SqlCommand(proc, _connection);
cmd.Transaction = _transaction;
if (parameters != null)
{
foreach (SqlParameter p in parameters)
{
if (p != null)
cmd.Parameters.Add(p);
}
}
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandTimeout = timeoutSeconds;
try
{
//using var reader = await cmd.ExecuteReaderAsync();
using var adapter = new SqlDataAdapter(cmd);
await Task.Run(() => adapter.Fill(ds));
}
catch (Exception ex)
{
string message = String.Format("Unable to retrieve data from proc: {0}", proc);
await LogErrorAsync(ex, message, "CallRetrievalProcedure");
throw;
}
if (createdConnection)
{
await CloseConnectionAsync();
}
}
catch (Exception)
{
if (createdConnection)
{
await CloseConnectionAsync();
}
throw;
}
return ds;
}
public async Task<int> CallActionProcedureAsync(List<SqlParameter> parameters, string proc, int timeoutSeconds = 0)
{
int iReturnVal = -1;
if (timeoutSeconds == 0)
timeoutSeconds = _timeout;
bool createdConnection = false;
try
{
if (_connection == null)
{
createdConnection = true;
await OpenConnectionAsync();
}
using SqlCommand cmd = new SqlCommand(proc, _connection);
cmd.Transaction = _transaction;
if (parameters != null)
{
foreach (SqlParameter p in parameters)
{
if (p != null)
cmd.Parameters.Add(p);
}
}
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandTimeout = timeoutSeconds;
try
{
iReturnVal = await cmd.ExecuteNonQueryAsync();
}
catch (Exception ex)
{
string message = String.Format("Unable to execute proc: {0}", proc);
await LogErrorAsync(ex, message, "CallActionProcedure");
throw;
}
}
catch (Exception)
{
if (createdConnection)
{
await CloseConnectionAsync();
}
throw;
}
if (createdConnection)
{
await CloseConnectionAsync();
}
return iReturnVal;
}
private static async Task LogErrorAsync(Exception ex, string message, string task)
{
Exception ex1 = ex;
if (ex == null)
ex1 = new Exception(message);
else if (!string.IsNullOrEmpty(message))
ex1 = new Exception(message, ex);
try
{
using SqlConnection c = new SqlConnection();
await c.OpenAsync();
using SqlCommand cmd = new SqlCommand("lg_log_error", c);
await cmd.ExecuteNonQueryAsync();
}
catch (Exception iex) //Trap all errors, don't do any
{
Console.WriteLine($"Unhandled Exception occurred logging exception: exception={iex}; trying to log exception={ex}");
}
}
#endregion
}
}

View File

@ -3,10 +3,10 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dapper.FluentMap.Mapping; using Surge365.Core.Mapping;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
namespace Surge365.MassEmailReact.Infrastructure.DapperMaps namespace Surge365.MassEmailReact.Infrastructure.EntityMaps
{ {
public class BouncedEmailMap : EntityMap<BouncedEmail> public class BouncedEmailMap : EntityMap<BouncedEmail>
{ {

View File

@ -1,7 +1,7 @@
using Dapper.FluentMap.Mapping; using Surge365.Core.Mapping;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
namespace Surge365.MassEmailReact.Infrastructure.DapperMaps namespace Surge365.MassEmailReact.Infrastructure.EntityMaps
{ {
public class EmailDomainMap : EntityMap<EmailDomain> public class EmailDomainMap : EntityMap<EmailDomain>
{ {

View File

@ -0,0 +1,24 @@
using Surge365.Core.Mapping;
namespace Surge365.MassEmailReact.Infrastructure.EntityMaps
{
public static class EntityMapperConfiguration
{
public static void ConfigureCustomMaps()
{
QueryMapper.AddMap(new TargetMap());
QueryMapper.AddMap(new TargetColumnMap());
QueryMapper.AddMap(new ServerMap());
QueryMapper.AddMap(new TestEmailListMap());
QueryMapper.AddMap(new BouncedEmailMap());
QueryMapper .AddMap(new UnsubscribeUrlMap());
QueryMapper.AddMap(new TemplateMap());
QueryMapper.AddMap(new EmailDomainMap());
QueryMapper .AddMap(new MailingMap());
QueryMapper.AddMap(new MailingEmailMap());
QueryMapper.AddMap(new MailingTemplateMap());
QueryMapper.AddMap(new MailingTargetMap());
QueryMapper.AddMap(new MailingStatisticMap());
}
}
}

View File

@ -1,7 +1,7 @@
using Dapper.FluentMap.Mapping; using Surge365.Core.Mapping;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
namespace Surge365.MassEmailReact.Infrastructure.DapperMaps namespace Surge365.MassEmailReact.Infrastructure.EntityMaps
{ {
public class MailingEmailMap : EntityMap<MailingEmail> public class MailingEmailMap : EntityMap<MailingEmail>
{ {

View File

@ -1,7 +1,7 @@
using Dapper.FluentMap.Mapping; using Surge365.Core.Mapping;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
namespace Surge365.MassEmailReact.Infrastructure.DapperMaps namespace Surge365.MassEmailReact.Infrastructure.EntityMaps
{ {
public class MailingMap : EntityMap<Mailing> public class MailingMap : EntityMap<Mailing>
{ {

View File

@ -1,7 +1,7 @@
using Dapper.FluentMap.Mapping; using Surge365.Core.Mapping;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
namespace Surge365.MassEmailReact.Infrastructure.DapperMaps namespace Surge365.MassEmailReact.Infrastructure.EntityMaps
{ {
public class MailingStatisticMap : EntityMap<MailingStatistic> public class MailingStatisticMap : EntityMap<MailingStatistic>
{ {

View File

@ -3,10 +3,10 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dapper.FluentMap.Mapping; using Surge365.Core.Mapping;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
namespace Surge365.MassEmailReact.Infrastructure.DapperMaps namespace Surge365.MassEmailReact.Infrastructure.EntityMaps
{ {
public class MailingTargetMap : EntityMap<MailingTarget> public class MailingTargetMap : EntityMap<MailingTarget>
{ {

View File

@ -1,7 +1,7 @@
using Dapper.FluentMap.Mapping; using Surge365.Core.Mapping;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
namespace Surge365.MassEmailReact.Infrastructure.DapperMaps namespace Surge365.MassEmailReact.Infrastructure.EntityMaps
{ {
public class MailingTemplateMap : EntityMap<MailingTemplate> public class MailingTemplateMap : EntityMap<MailingTemplate>
{ {

View File

@ -3,10 +3,10 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dapper.FluentMap.Mapping; using Surge365.Core.Mapping;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
namespace Surge365.MassEmailReact.Infrastructure.DapperMaps namespace Surge365.MassEmailReact.Infrastructure.EntityMaps
{ {
public class ServerMap : EntityMap<Server> public class ServerMap : EntityMap<Server>
{ {

View File

@ -3,10 +3,10 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dapper.FluentMap.Mapping; using Surge365.Core.Mapping;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
namespace Surge365.MassEmailReact.Infrastructure.DapperMaps namespace Surge365.MassEmailReact.Infrastructure.EntityMaps
{ {
public class TargetColumnMap : EntityMap<TargetColumn> public class TargetColumnMap : EntityMap<TargetColumn>
{ {

View File

@ -3,10 +3,10 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dapper.FluentMap.Mapping; using Surge365.Core.Mapping;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
namespace Surge365.MassEmailReact.Infrastructure.DapperMaps namespace Surge365.MassEmailReact.Infrastructure.EntityMaps
{ {
public class TargetMap : EntityMap<Target> public class TargetMap : EntityMap<Target>
{ {

View File

@ -1,7 +1,7 @@
using Dapper.FluentMap.Mapping; using Surge365.Core.Mapping;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
namespace Surge365.MassEmailReact.Infrastructure.DapperMaps namespace Surge365.MassEmailReact.Infrastructure.EntityMaps
{ {
public class TemplateMap : EntityMap<Template> public class TemplateMap : EntityMap<Template>
{ {

View File

@ -1,4 +1,4 @@
using Dapper.FluentMap.Mapping; using Surge365.Core.Mapping;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -6,7 +6,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Infrastructure.DapperMaps namespace Surge365.MassEmailReact.Infrastructure.EntityMaps
{ {
public class TestEmailListMap : EntityMap<TestEmailList> public class TestEmailListMap : EntityMap<TestEmailList>
{ {

View File

@ -1,7 +1,7 @@
using Dapper.FluentMap.Mapping; using Surge365.Core.Mapping;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
namespace Surge365.MassEmailReact.Infrastructure.DapperMaps namespace Surge365.MassEmailReact.Infrastructure.EntityMaps
{ {
public class UnsubscribeUrlMap : EntityMap<UnsubscribeUrl> public class UnsubscribeUrlMap : EntityMap<UnsubscribeUrl>
{ {

View File

@ -1,36 +0,0 @@
using Azure.Extensions.AspNetCore.Configuration.Secrets;
using Azure.Security.KeyVault.Secrets;
namespace Surge365.MassEmailReact.Infrastructure
{
public class SurgeKeyVaultSecretManager : KeyVaultSecretManager
{
public override bool Load(SecretProperties secret)
{
return true;
}
public override string GetKey(KeyVaultSecret secret)
{
// Transform secret name back to configuration key
// Replace double hyphens (--) with colons (:) and single hyphens (-) with periods (.)
/*
* Example:
* AppSettings.json =
{
"AdminAuth": {
"ApiKey": "abcde"
},
"AdminAuth.ConnectionString":"abc"
}
* _config path = "AdminAuth:ApiKey"
* Environment Variable = "AdminAuth__ApiKey"
* Azure Key Vault = "AdminAuth--ApiKey"
*
* _config path = "AdminAuth.ConnectionString"
* Environment Variable = "AdminAuth.ConnectionString"
* Azure Key Vault = "AdminAuth-ConnectionString"
*/
return secret.Name.Replace("--", ":").Replace("-", ".");
}
}
}

View File

@ -1,66 +0,0 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Infrastructure;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Infrastructure.Middleware
{
public class CustomException
{
private readonly RequestDelegate _next;
private readonly ILoggingService _loggingService;
public CustomException(RequestDelegate next, ILoggingService loggingService)
{
_next = next;
_loggingService = loggingService;
}
/// <summary>
/// Invokes the middleware peforming session start
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
/// <returns></returns>
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch(Exception ex)
{
if (!(ex is ThreadAbortException))
{
/* Guid? loginId = null;
if (context.User.Identity?.IsAuthenticated == true)
{
// Adjust the claim type based on your JWT configuration (e.g., "sub", "nameid", or custom)
var loginIdClaim = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? context.User.FindFirst("sub")?.Value;
if (!string.IsNullOrEmpty(loginIdClaim) && Guid.TryParse(loginIdClaim, out var parsedUserId))
{
loginId = parsedUserId;
}
}*/
await _loggingService.LogError(ex);
}
throw;
}
}
}
public static class CustomExceptionMiddlewareExtensions
{
public static IApplicationBuilder UseCustomExceptionHandler(this IApplicationBuilder builder)
{
return builder.UseMiddleware<CustomException>();
}
}
}

View File

@ -1,12 +1,14 @@
using Dapper; using Microsoft.Data.SqlClient;
using Dapper.FluentMap;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Surge365.Core.Interfaces;
using Surge365.Core.Mapping;
using Surge365.Core.Services;
using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data; using System.Data;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Infrastructure.Repositories namespace Surge365.MassEmailReact.Infrastructure.Repositories
@ -14,22 +16,19 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
public class BouncedEmailRepository : IBouncedEmailRepository public class BouncedEmailRepository : IBouncedEmailRepository
{ {
private IConfiguration _config; private IConfiguration _config;
private const string _connectionStringName = "MassEmail.ConnectionString"; private DataAccessFactory _dataAccessFactory;
private string? ConnectionString private IQueryMapper _queryMapper;
{ public DataAccess GetDataAccess(string connectionStringName = "MassEmail") => _dataAccessFactory.Get(connectionStringName) ?? throw new ArgumentNullException(nameof(_dataAccessFactory), $"DataAccess context for '{connectionStringName}' not found.");
get
{
return _config.GetConnectionString(_connectionStringName);
}
}
public BouncedEmailRepository(IConfiguration config) public BouncedEmailRepository(IConfiguration config, DataAccessFactory dataAccessFactory, IQueryMapper queryMapper)
{ {
_config = config; _config = config;
_dataAccessFactory = dataAccessFactory ?? throw new ArgumentNullException(nameof(dataAccessFactory), "DataAccessFactory cannot be null.");
_queryMapper = queryMapper;
#if DEBUG #if DEBUG
if (!FluentMapper.EntityMaps.ContainsKey(typeof(BouncedEmail))) if (!_queryMapper.EntityMaps.ContainsKey(typeof(BouncedEmail)))
{ {
throw new InvalidOperationException("BouncedEmail dapper mapping is missing. Make sure ConfigureMappings() is called inside program.cs (program startup)."); throw new InvalidOperationException("BouncedEmail query mapping is missing. Make sure ConfigureCustomMaps() is called inside program.cs (program startup).");
} }
#endif #endif
} }
@ -37,21 +36,28 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
public async Task<BouncedEmail?> GetByEmailAsync(string emailAddress) public async Task<BouncedEmail?> GetByEmailAsync(string emailAddress)
{ {
ArgumentNullException.ThrowIfNull(_config); ArgumentNullException.ThrowIfNull(_config);
ArgumentNullException.ThrowIfNull(_connectionStringName);
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName)); var parameters = new List<SqlParameter>
{
new SqlParameter("@email_address", emailAddress)
};
return (await conn.QueryAsync<BouncedEmail>("mem_get_bounced_email_by_email", new { email_address = emailAddress }, commandType: CommandType.StoredProcedure)).FirstOrDefault(); DataAccess dataAccess = GetDataAccess();
var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_bounced_email_by_email");
var results = await _queryMapper.MapAsync<BouncedEmail>(dataSet);
return results.FirstOrDefault();
} }
public async Task<List<BouncedEmail>> GetAllAsync() public async Task<List<BouncedEmail>> GetAllAsync()
{ {
ArgumentNullException.ThrowIfNull(_config); var parameters = new List<SqlParameter>();
ArgumentNullException.ThrowIfNull(_connectionStringName);
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName)); DataAccess dataAccess = GetDataAccess();
var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_bounced_email_all");
return (await conn.QueryAsync<BouncedEmail>("mem_get_bounced_email_all", new { }, commandType: CommandType.StoredProcedure)).ToList(); var results = await _queryMapper.MapAsync<BouncedEmail>(dataSet);
return results.ToList();
} }
public async Task<int?> CreateAsync(BouncedEmail bouncedEmail) public async Task<int?> CreateAsync(BouncedEmail bouncedEmail)
@ -59,22 +65,30 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
ArgumentNullException.ThrowIfNull(bouncedEmail); ArgumentNullException.ThrowIfNull(bouncedEmail);
ArgumentNullException.ThrowIfNullOrEmpty(bouncedEmail.EmailAddress); ArgumentNullException.ThrowIfNullOrEmpty(bouncedEmail.EmailAddress);
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName)); SqlParameter pmBouncedEmailKey = new SqlParameter("@bounced_email_key", SqlDbType.Int)
{
Direction = ParameterDirection.Output
};
SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
{
Direction = ParameterDirection.Output
};
var parameters = new DynamicParameters(); List<SqlParameter> parameters = new List<SqlParameter>
parameters.Add("@email_address", bouncedEmail.EmailAddress, DbType.String); {
parameters.Add("@spam", bouncedEmail.Spam, DbType.Boolean); pmBouncedEmailKey,
parameters.Add("@unsubscribe", bouncedEmail.Unsubscribe, DbType.Boolean); new SqlParameter("@email_address", bouncedEmail.EmailAddress),
parameters.Add("@entered_by_admin", bouncedEmail.EnteredByAdmin, DbType.Boolean); new SqlParameter("@spam", bouncedEmail.Spam),
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output); new SqlParameter("@unsubscribe", bouncedEmail.Unsubscribe),
parameters.Add("@bounced_email_key", dbType: DbType.Int32, direction: ParameterDirection.Output); new SqlParameter("@entered_by_admin", bouncedEmail.EnteredByAdmin),
pmSuccess
};
await conn.ExecuteAsync("mem_save_bounced_email", parameters, commandType: CommandType.StoredProcedure); DataAccess dataAccess = GetDataAccess();
await dataAccess.CallActionProcedureAsync(parameters, "mem_save_bounced_email");
bool success = pmSuccess.Value != null && (bool)pmSuccess.Value;
bool success = parameters.Get<bool>("@success"); if (success) return (int?)pmBouncedEmailKey.Value;
if (success)
return parameters.Get<int>("@bounced_email_key");
return null; return null;
} }
@ -85,19 +99,24 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
ArgumentNullException.ThrowIfNullOrEmpty(originalEmailAddress); ArgumentNullException.ThrowIfNullOrEmpty(originalEmailAddress);
ArgumentNullException.ThrowIfNullOrEmpty(bouncedEmail.EmailAddress); ArgumentNullException.ThrowIfNullOrEmpty(bouncedEmail.EmailAddress);
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName)); SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
{
Direction = ParameterDirection.Output
};
var parameters = new DynamicParameters(); List<SqlParameter> parameters = new List<SqlParameter>
parameters.Add("@old_email_address", originalEmailAddress, DbType.String); {
parameters.Add("@email_address", bouncedEmail.EmailAddress, DbType.String); new SqlParameter("@old_email_address", originalEmailAddress),
parameters.Add("@spam", bouncedEmail.Spam, DbType.Boolean); new SqlParameter("@email_address", bouncedEmail.EmailAddress),
parameters.Add("@unsubscribe", bouncedEmail.Unsubscribe, DbType.Boolean); new SqlParameter("@spam", bouncedEmail.Spam),
parameters.Add("@entered_by_admin", bouncedEmail.EnteredByAdmin, DbType.Boolean); new SqlParameter("@unsubscribe", bouncedEmail.Unsubscribe),
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output); new SqlParameter("@entered_by_admin", bouncedEmail.EnteredByAdmin),
pmSuccess
};
await conn.ExecuteAsync("mem_save_bounced_email", parameters, commandType: CommandType.StoredProcedure); DataAccess dataAccess = GetDataAccess();
await dataAccess.CallActionProcedureAsync(parameters, "mem_save_bounced_email");
bool success = parameters.Get<bool>("@success"); bool success = pmSuccess.Value != null && (bool)pmSuccess.Value;
return success; return success;
} }
@ -106,15 +125,20 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
{ {
ArgumentNullException.ThrowIfNull(emailAddress); ArgumentNullException.ThrowIfNull(emailAddress);
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName)); SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
{
Direction = ParameterDirection.Output
};
var parameters = new DynamicParameters(); List<SqlParameter> parameters = new List<SqlParameter>
parameters.Add("@email_address", emailAddress, DbType.String); {
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output); new SqlParameter("@email_address", emailAddress),
pmSuccess
};
await conn.ExecuteAsync("mem_delete_bounced_email", parameters, commandType: CommandType.StoredProcedure); DataAccess dataAccess = GetDataAccess();
await dataAccess.CallActionProcedureAsync(parameters, "mem_delete_bounced_email");
bool success = parameters.Get<bool>("@success"); bool success = pmSuccess.Value != null && (bool)pmSuccess.Value;
return success; return success;
} }

View File

@ -1,14 +1,14 @@
using Dapper; using Microsoft.Data.SqlClient;
using Dapper.FluentMap;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Surge365.Core.Interfaces;
using Surge365.Core.Mapping;
using Surge365.Core.Services;
using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data; using System.Data;
using System.Linq; using System.Linq;
using System.Runtime.Intrinsics.Arm;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Infrastructure.Repositories namespace Surge365.MassEmailReact.Infrastructure.Repositories
@ -16,22 +16,19 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
public class EmailDomainRepository : IEmailDomainRepository public class EmailDomainRepository : IEmailDomainRepository
{ {
private readonly IConfiguration _config; private readonly IConfiguration _config;
private const string _connectionStringName = "MassEmail.ConnectionString"; private DataAccessFactory _dataAccessFactory;
private string? ConnectionString private IQueryMapper _queryMapper;
{ public DataAccess GetDataAccess(string connectionStringName = "MassEmail") => _dataAccessFactory.Get(connectionStringName) ?? throw new ArgumentNullException(nameof(_dataAccessFactory), $"DataAccess context for '{connectionStringName}' not found.");
get
{
return _config.GetConnectionString(_connectionStringName);
}
}
public EmailDomainRepository(IConfiguration config) public EmailDomainRepository(IConfiguration config, DataAccessFactory dataAccessFactory, IQueryMapper queryMapper)
{ {
_config = config; _config = config;
_dataAccessFactory = dataAccessFactory ?? throw new ArgumentNullException(nameof(dataAccessFactory), "DataAccessFactory cannot be null.");
_queryMapper = queryMapper;
#if DEBUG #if DEBUG
if (!FluentMapper.EntityMaps.ContainsKey(typeof(EmailDomain))) if (!_queryMapper.EntityMaps.ContainsKey(typeof(EmailDomain)))
{ {
throw new InvalidOperationException("EmailDomain dapper mapping is missing. Make sure ConfigureMappings() is called inside program.cs (program startup)."); throw new InvalidOperationException("EmailDomain query mapping is missing. Make sure ConfigureCustomMaps() is called inside program.cs (program startup).");
} }
#endif #endif
} }
@ -39,10 +36,18 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
public async Task<EmailDomain?> GetByIdAsync(int id, bool returnPassword = false) public async Task<EmailDomain?> GetByIdAsync(int id, bool returnPassword = false)
{ {
ArgumentNullException.ThrowIfNull(_config); ArgumentNullException.ThrowIfNull(_config);
ArgumentNullException.ThrowIfNull(_connectionStringName);
using SqlConnection conn = new SqlConnection(ConnectionString); var parameters = new List<SqlParameter>
EmailDomain? domain = (await conn.QueryAsync<EmailDomain>("mem_get_domain_by_id", new { domain_key = id }, commandType: CommandType.StoredProcedure)).FirstOrDefault(); {
new SqlParameter("@domain_key", id)
};
DataAccess dataAccess = GetDataAccess();
var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_domain_by_id");
var results = await _queryMapper.MapAsync<EmailDomain>(dataSet);
EmailDomain? domain = results.FirstOrDefault();
if (domain != null && !returnPassword) if (domain != null && !returnPassword)
{ {
domain.Password = ""; domain.Password = "";
@ -52,11 +57,17 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
public async Task<List<EmailDomain>> GetAllAsync(bool activeOnly = true, bool returnPassword = false) public async Task<List<EmailDomain>> GetAllAsync(bool activeOnly = true, bool returnPassword = false)
{ {
ArgumentNullException.ThrowIfNull(_config); var parameters = new List<SqlParameter>
ArgumentNullException.ThrowIfNull(_connectionStringName); {
new SqlParameter("@active_only", activeOnly)
};
DataAccess dataAccess = GetDataAccess();
var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_domain_all");
var results = await _queryMapper.MapAsync<EmailDomain>(dataSet);
List<EmailDomain> domains = results.ToList();
using SqlConnection conn = new SqlConnection(ConnectionString);
List<EmailDomain> domains = (await conn.QueryAsync<EmailDomain>("mem_get_domain_all", new { active_only = activeOnly }, commandType: CommandType.StoredProcedure)).ToList();
if(!returnPassword) if(!returnPassword)
{ {
foreach (var domain in domains) foreach (var domain in domains)
@ -73,21 +84,33 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
if (emailDomain.Id != null && emailDomain.Id > 0) if (emailDomain.Id != null && emailDomain.Id > 0)
throw new Exception("ID must be null"); throw new Exception("ID must be null");
using SqlConnection conn = new SqlConnection(ConnectionString); SqlParameter pmDomainKey = new SqlParameter("@domain_key", SqlDbType.Int)
var parameters = new DynamicParameters(); {
parameters.Add("@domain_key", dbType: DbType.Int32, direction: ParameterDirection.Output); Direction = ParameterDirection.Output
parameters.Add("@name", emailDomain.Name, DbType.String); };
parameters.Add("@email_address", emailDomain.EmailAddress, DbType.String); SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
parameters.Add("@username", emailDomain.Username, DbType.String); {
parameters.Add("@password", emailDomain.Password, DbType.String); Direction = ParameterDirection.Output
parameters.Add("@is_active", emailDomain.IsActive, DbType.Boolean); };
parameters.Add("@display_order", emailDomain.DisplayOrder, DbType.Int32);
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output); List<SqlParameter> parameters = new List<SqlParameter>
{
pmDomainKey,
new SqlParameter("@name", emailDomain.Name),
new SqlParameter("@email_address", emailDomain.EmailAddress),
new SqlParameter("@username", emailDomain.Username),
new SqlParameter("@password", emailDomain.Password),
new SqlParameter("@is_active", emailDomain.IsActive),
new SqlParameter("@display_order", emailDomain.DisplayOrder),
pmSuccess
};
DataAccess dataAccess = GetDataAccess();
await dataAccess.CallActionProcedureAsync(parameters, "mem_save_domain");
bool success = pmSuccess.Value != null && (bool)pmSuccess.Value;
await conn.ExecuteAsync("mem_save_domain", parameters, commandType: CommandType.StoredProcedure);
bool success = parameters.Get<bool>("@success");
if (success) if (success)
return parameters.Get<int>("@domain_key"); return (int?)pmDomainKey.Value;
return null; return null;
} }
@ -96,19 +119,26 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
ArgumentNullException.ThrowIfNull(emailDomain); ArgumentNullException.ThrowIfNull(emailDomain);
ArgumentNullException.ThrowIfNull(emailDomain.Id); ArgumentNullException.ThrowIfNull(emailDomain.Id);
using SqlConnection conn = new SqlConnection(ConnectionString); SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
var parameters = new DynamicParameters(); {
parameters.Add("@domain_key", emailDomain.Id, DbType.Int32); Direction = ParameterDirection.Output
parameters.Add("@name", emailDomain.Name, DbType.String); };
parameters.Add("@email_address", emailDomain.EmailAddress, DbType.String);
parameters.Add("@username", emailDomain.Username, DbType.String);
parameters.Add("@password", emailDomain.Password, DbType.String);
parameters.Add("@is_active", emailDomain.IsActive, DbType.Boolean);
parameters.Add("@display_order", emailDomain.DisplayOrder, DbType.Int32);
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output);
await conn.ExecuteAsync("mem_save_domain", parameters, commandType: CommandType.StoredProcedure); List<SqlParameter> parameters = new List<SqlParameter>
bool success = parameters.Get<bool>("@success"); {
new SqlParameter("@domain_key", emailDomain.Id),
new SqlParameter("@name", emailDomain.Name),
new SqlParameter("@email_address", emailDomain.EmailAddress),
new SqlParameter("@username", emailDomain.Username),
new SqlParameter("@password", emailDomain.Password),
new SqlParameter("@is_active", emailDomain.IsActive),
new SqlParameter("@display_order", emailDomain.DisplayOrder),
pmSuccess
};
DataAccess dataAccess = GetDataAccess();
await dataAccess.CallActionProcedureAsync(parameters, "mem_save_domain");
bool success = pmSuccess.Value != null && (bool)pmSuccess.Value;
return success; return success;
} }
} }

View File

@ -1,7 +1,8 @@
using Dapper; using Microsoft.Data.SqlClient;
using Dapper.FluentMap;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Surge365.Core.Interfaces;
using Surge365.Core.Mapping;
using Surge365.Core.Services;
using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
using System; using System;
@ -16,38 +17,43 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
public class MailingRepository : IMailingRepository public class MailingRepository : IMailingRepository
{ {
private readonly IConfiguration _config; private readonly IConfiguration _config;
private const string _connectionStringName = "MassEmail.ConnectionString"; private DataAccessFactory _dataAccessFactory;
private string? ConnectionString => _config.GetConnectionString(_connectionStringName); private IQueryMapper _queryMapper;
public DataAccess GetDataAccess(string connectionStringName = "MassEmail") => _dataAccessFactory.Get(connectionStringName) ?? throw new ArgumentNullException(nameof(_dataAccessFactory), $"DataAccess context for '{connectionStringName}' not found.");
public MailingRepository(IConfiguration config) public MailingRepository(IConfiguration config, DataAccessFactory dataAccessFactory, IQueryMapper queryMapper)
{ {
_config = config; _config = config;
_dataAccessFactory = dataAccessFactory ?? throw new ArgumentNullException(nameof(dataAccessFactory), "DataAccessFactory cannot be null.");
_queryMapper = queryMapper;
#if DEBUG #if DEBUG
if (!FluentMapper.EntityMaps.ContainsKey(typeof(Mailing))) if (!_queryMapper.EntityMaps.ContainsKey(typeof(Mailing)))
{ {
throw new InvalidOperationException("Mailing dapper mapping is missing. Make sure ConfigureMappings() is called inside program.cs (program startup)."); throw new InvalidOperationException("Mailing query mapping is missing. Make sure ConfigureCustomMaps() is called inside program.cs (program startup).");
} }
#endif #endif
} }
public async Task<Mailing?> GetByIdAsync(int id) public async Task<Mailing?> GetByIdAsync(int id)
{ {
ArgumentNullException.ThrowIfNull(ConnectionString); ArgumentNullException.ThrowIfNull(_config);
using SqlConnection conn = new SqlConnection(ConnectionString); var parameters = new List<SqlParameter>
{
new SqlParameter("@blast_key", id)
};
await conn.OpenAsync(); DataAccess dataAccess = GetDataAccess();
var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_blast_by_id");
using var multi = await conn.QueryMultipleAsync( // Handle multiple result sets
"mem_get_blast_by_id", var mailings = await _queryMapper.MapAsync<Mailing>(dataSet);
new { blast_key = id }, var mailing = mailings.FirstOrDefault();
commandType: CommandType.StoredProcedure);
var mailing = await multi.ReadSingleOrDefaultAsync<Mailing>();
if (mailing == null) return null; if (mailing == null) return null;
var template = await multi.ReadSingleOrDefaultAsync<MailingTemplate>(); var templates = await _queryMapper.MapAsync<MailingTemplate>(dataSet);
if (mailing != null) var template = templates.FirstOrDefault();
if (mailing != null && template != null)
mailing.Template = template; mailing.Template = template;
return mailing; return mailing;
@ -55,24 +61,23 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
public async Task<List<Mailing>> GetAllAsync(bool activeOnly = true) public async Task<List<Mailing>> GetAllAsync(bool activeOnly = true)
{ {
ArgumentNullException.ThrowIfNull(ConnectionString); var parameters = new List<SqlParameter>
{
new SqlParameter("@active_only", activeOnly)
};
using SqlConnection conn = new SqlConnection(ConnectionString); DataAccess dataAccess = GetDataAccess();
var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_blast_all");
await conn.OpenAsync(); var mailings = await _queryMapper.MapAsync<Mailing>(dataSet);
var mailingList = mailings.ToList();
if (!mailingList.Any()) return mailingList;
using var multi = await conn.QueryMultipleAsync( var templates = await _queryMapper.MapAsync<MailingTemplate>(dataSet);
"mem_get_blast_all", var templateList = templates.ToList();
new { active_only = activeOnly },
commandType: CommandType.StoredProcedure);
var mailings = (await multi.ReadAsync<Mailing>()).ToList(); var mailingDictionary = mailingList.ToDictionary(t => t.Id!.Value);
if (!mailings.Any()) return mailings; foreach (var template in templateList)
var templates = (await multi.ReadAsync<MailingTemplate>()).ToList();
var mailingDictionary = mailings.ToDictionary(t => t.Id!.Value);
foreach (var template in templates)
{ {
if (mailingDictionary.TryGetValue(template.MailingId, out var mailing)) if (mailingDictionary.TryGetValue(template.MailingId, out var mailing))
{ {
@ -80,27 +85,30 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
} }
} }
return mailings; return mailingList;
} }
public async Task<List<Mailing>> GetByStatusAsync(string codes, string? startDate, string? endDate) public async Task<List<Mailing>> GetByStatusAsync(string codes, string? startDate, string? endDate)
{ {
ArgumentNullException.ThrowIfNull(ConnectionString); var parameters = new List<SqlParameter>
{
new SqlParameter("@blast_status_codes", codes),
new SqlParameter("@start_date", startDate ?? (object)DBNull.Value),
new SqlParameter("@end_date", endDate ?? (object)DBNull.Value)
};
using SqlConnection conn = new SqlConnection(ConnectionString); DataAccess dataAccess = GetDataAccess();
await conn.OpenAsync(); var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_blast_by_status");
using var multi = await conn.QueryMultipleAsync( var mailings = await _queryMapper.MapAsync<Mailing>(dataSet);
"mem_get_blast_by_status", var mailingList = mailings.ToList();
new { blast_status_codes = codes, start_date = startDate, end_date = endDate }, if (!mailingList.Any()) return mailingList;
commandType: CommandType.StoredProcedure);
var mailings = (await multi.ReadAsync<Mailing>()).ToList(); var templates = await _queryMapper.MapAsync<MailingTemplate>(dataSet);
if (!mailings.Any()) return mailings; var templateList = templates.ToList();
var templates = (await multi.ReadAsync<MailingTemplate>()).ToList(); var mailingDictionary = mailingList.ToDictionary(t => t.Id!.Value);
foreach (var template in templateList)
var mailingDictionary = mailings.ToDictionary(t => t.Id!.Value);
foreach (var template in templates)
{ {
if (mailingDictionary.TryGetValue(template.MailingId, out var mailing)) if (mailingDictionary.TryGetValue(template.MailingId, out var mailing))
{ {
@ -108,138 +116,213 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
} }
} }
return mailings; return mailingList;
} }
public async Task<List<MailingStatistic>> GetStatisticsByStatusAsync(string codes, string? startDate, string? endDate) public async Task<List<MailingStatistic>> GetStatisticsByStatusAsync(string codes, string? startDate, string? endDate)
{ {
ArgumentNullException.ThrowIfNull(ConnectionString); var parameters = new List<SqlParameter>
{
new SqlParameter("@blast_status_codes", codes),
new SqlParameter("@start_date", startDate ?? (object)DBNull.Value),
new SqlParameter("@end_date", endDate ?? (object)DBNull.Value)
};
using SqlConnection conn = new SqlConnection(ConnectionString); DataAccess dataAccess = GetDataAccess();
return (await conn.QueryAsync<MailingStatistic>("mem_get_blast_statistic_by_status", new { blast_status_codes = codes, start_date = startDate, end_date = endDate }, commandType: CommandType.StoredProcedure)).ToList(); var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_blast_statistic_by_status");
var results = await _queryMapper.MapAsync<MailingStatistic>(dataSet);
return results.ToList();
} }
public async Task<MailingStatistic?> GetStatisticByIdAsync(int id) public async Task<MailingStatistic?> GetStatisticByIdAsync(int id)
{ {
ArgumentNullException.ThrowIfNull(ConnectionString); var parameters = new List<SqlParameter>
{
new SqlParameter("@blast_key", id)
};
using SqlConnection conn = new SqlConnection(ConnectionString); DataAccess dataAccess = GetDataAccess();
return (await conn.QueryAsync<MailingStatistic>("mem_get_blast_statistic_by_blast", new { blast_key = id }, commandType: CommandType.StoredProcedure)).FirstOrDefault(); var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_blast_statistic_by_blast");
var results = await _queryMapper.MapAsync<MailingStatistic>(dataSet);
return results.FirstOrDefault();
} }
public async Task<List<MailingEmail>> GetEmailsByIdAsync(int id) public async Task<List<MailingEmail>> GetEmailsByIdAsync(int id)
{ {
ArgumentNullException.ThrowIfNull(ConnectionString); var parameters = new List<SqlParameter>
{
new SqlParameter("@blast_key", id)
};
using SqlConnection conn = new SqlConnection(ConnectionString); DataAccess dataAccess = GetDataAccess();
return (await conn.QueryAsync<MailingEmail>("mem_get_blast_email_by_blast_id", new { blast_key = id }, commandType: CommandType.StoredProcedure)).ToList(); var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_blast_email_by_blast_id");
var results = await _queryMapper.MapAsync<MailingEmail>(dataSet);
return results.ToList();
} }
public async Task<MailingTemplate?> GetTemplateByIdAsync(int id) public async Task<MailingTemplate?> GetTemplateByIdAsync(int id)
{ {
ArgumentNullException.ThrowIfNull(ConnectionString); var parameters = new List<SqlParameter>
{
new SqlParameter("@blast_key", id)
};
using SqlConnection conn = new SqlConnection(ConnectionString); DataAccess dataAccess = GetDataAccess();
return (await conn.QueryAsync<MailingTemplate>("mem_get_blast_template_by_blast_id", new { blast_key = id }, commandType: CommandType.StoredProcedure)).FirstOrDefault(); var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_blast_template_by_blast_id");
var results = await _queryMapper.MapAsync<MailingTemplate>(dataSet);
return results.FirstOrDefault();
} }
public async Task<MailingTarget?> GetTargetByIdAsync(int id) public async Task<MailingTarget?> GetTargetByIdAsync(int id)
{ {
ArgumentNullException.ThrowIfNull(ConnectionString); var parameters = new List<SqlParameter>
{
new SqlParameter("@blast_key", id)
};
using SqlConnection conn = new SqlConnection(ConnectionString); DataAccess dataAccess = GetDataAccess();
return (await conn.QueryAsync<MailingTarget>("mem_get_blast_target_by_blast_id", new { blast_key = id }, commandType: CommandType.StoredProcedure)).FirstOrDefault(); var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_blast_target_by_blast_id");
var results = await _queryMapper.MapAsync<MailingTarget>(dataSet);
return results.FirstOrDefault();
} }
public async Task<bool> NameIsAvailableAsync(int? id, string name) public async Task<bool> NameIsAvailableAsync(int? id, string name)
{ {
ArgumentNullException.ThrowIfNull(ConnectionString); SqlParameter pmAvailable = new SqlParameter("@available", SqlDbType.Bit)
using var conn = new SqlConnection(ConnectionString); {
Direction = ParameterDirection.Output
};
var parameters = new DynamicParameters(); var parameters = new List<SqlParameter>
parameters.Add("@blast_key", id, DbType.Int32); {
parameters.Add("@blast_name", name, DbType.String); new SqlParameter("@blast_key", id ?? (object)DBNull.Value),
parameters.Add("@available", dbType: DbType.Boolean, direction: ParameterDirection.Output); new SqlParameter("@blast_name", name),
pmAvailable
};
await conn.ExecuteAsync("mem_is_blast_name_available", parameters, commandType: CommandType.StoredProcedure); DataAccess dataAccess = GetDataAccess();
await dataAccess.CallActionProcedureAsync(parameters, "mem_is_blast_name_available");
return parameters.Get<bool>("@available"); bool available = pmAvailable.Value != null && (bool)pmAvailable.Value;
return available;
} }
public async Task<string> GetNextAvailableNameAsync(int? id, string name) public async Task<string> GetNextAvailableNameAsync(int? id, string name)
{ {
ArgumentNullException.ThrowIfNull(ConnectionString); SqlParameter pmNextBlastName = new SqlParameter("@next_blast_name", SqlDbType.NVarChar, -1)
using var conn = new SqlConnection(ConnectionString); {
Direction = ParameterDirection.Output
};
var parameters = new DynamicParameters(); var parameters = new List<SqlParameter>
parameters.Add("@blast_key", id, DbType.Int32); {
parameters.Add("@blast_name", name, DbType.String); new SqlParameter("@blast_key", id ?? (object)DBNull.Value),
parameters.Add("@next_blast_name", dbType: DbType.String, size:-1, direction: ParameterDirection.Output); new SqlParameter("@blast_name", name),
pmNextBlastName
};
await conn.ExecuteAsync("mem_get_next_available_blast_name", parameters, commandType: CommandType.StoredProcedure); DataAccess dataAccess = GetDataAccess();
await dataAccess.CallActionProcedureAsync(parameters, "mem_get_next_available_blast_name");
return parameters.Get<string>("@next_blast_name"); return pmNextBlastName.Value?.ToString() ?? "";
} }
public async Task<int?> CreateAsync(Mailing mailing) public async Task<int?> CreateAsync(Mailing mailing)
{ {
ArgumentNullException.ThrowIfNull(ConnectionString);
ArgumentNullException.ThrowIfNull(mailing); ArgumentNullException.ThrowIfNull(mailing);
if (mailing.Id != null && mailing.Id > 0) if (mailing.Id != null && mailing.Id > 0)
throw new Exception("ID must be null"); throw new Exception("ID must be null");
using SqlConnection conn = new SqlConnection(ConnectionString); SqlParameter pmBlastKey = new SqlParameter("@blast_key", SqlDbType.Int)
var parameters = new DynamicParameters(); {
parameters.Add("@blast_key", dbType: DbType.Int32, direction: ParameterDirection.Output); Direction = ParameterDirection.Output
parameters.Add("@name", mailing.Name, DbType.String); };
parameters.Add("@description", mailing.Description, DbType.String); SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
parameters.Add("@template_key", mailing.TemplateId, DbType.Int32); {
parameters.Add("@target_key", mailing.TargetId, DbType.Int32); Direction = ParameterDirection.Output
parameters.Add("@blast_status_code", mailing.StatusCode, DbType.String); };
parameters.Add("@schedule_date", mailing.ScheduleDate, DbType.DateTime);
parameters.Add("@sent_date", mailing.SentDate, DbType.DateTime);
parameters.Add("@blast_recurring_type_code", mailing.RecurringTypeCode, DbType.String);
parameters.Add("@recurring_start_date", mailing.RecurringStartDate, DbType.DateTime);
parameters.Add("@template_json", JsonSerializer.Serialize(mailing.Template, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }), DbType.String);
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output);
await conn.ExecuteAsync("mem_save_blast", parameters, commandType: CommandType.StoredProcedure); List<SqlParameter> parameters = new List<SqlParameter>
{
pmBlastKey,
new SqlParameter("@name", mailing.Name),
new SqlParameter("@description", mailing.Description),
new SqlParameter("@template_key", mailing.TemplateId),
new SqlParameter("@target_key", mailing.TargetId),
new SqlParameter("@blast_status_code", mailing.StatusCode),
new SqlParameter("@schedule_date", mailing.ScheduleDate ?? (object)DBNull.Value),
new SqlParameter("@sent_date", mailing.SentDate ?? (object)DBNull.Value),
new SqlParameter("@blast_recurring_type_code", mailing.RecurringTypeCode ?? (object)DBNull.Value),
new SqlParameter("@recurring_start_date", mailing.RecurringStartDate ?? (object)DBNull.Value),
new SqlParameter("@template_json", JsonSerializer.Serialize(mailing.Template, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower })),
pmSuccess
};
bool success = parameters.Get<bool>("@success"); DataAccess dataAccess = GetDataAccess();
await dataAccess.CallActionProcedureAsync(parameters, "mem_save_blast");
bool success = pmSuccess.Value != null && (bool)pmSuccess.Value;
if (success) if (success)
return parameters.Get<int>("@blast_key"); return (int?)pmBlastKey.Value;
return null; return null;
} }
public async Task<bool> UpdateAsync(Mailing mailing) public async Task<bool> UpdateAsync(Mailing mailing)
{ {
ArgumentNullException.ThrowIfNull(ConnectionString);
ArgumentNullException.ThrowIfNull(mailing); ArgumentNullException.ThrowIfNull(mailing);
ArgumentNullException.ThrowIfNull(mailing.Id); ArgumentNullException.ThrowIfNull(mailing.Id);
using SqlConnection conn = new SqlConnection(ConnectionString); SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
var parameters = new DynamicParameters(); {
parameters.Add("@blast_key", mailing.Id, DbType.Int32); Direction = ParameterDirection.Output
parameters.Add("@name", mailing.Name, DbType.String); };
parameters.Add("@description", mailing.Description, DbType.String);
parameters.Add("@template_key", mailing.TemplateId, DbType.Int32);
parameters.Add("@target_key", mailing.TargetId, DbType.Int32);
parameters.Add("@blast_status_code", mailing.StatusCode, DbType.String);
parameters.Add("@schedule_date", mailing.ScheduleDate, DbType.DateTime);
parameters.Add("@sent_date", mailing.SentDate, DbType.DateTime);
parameters.Add("@blast_recurring_type_code", mailing.RecurringTypeCode, DbType.String);
parameters.Add("@recurring_start_date", mailing.RecurringStartDate, DbType.DateTime);
parameters.Add("@template_json", JsonSerializer.Serialize(mailing.Template, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }), DbType.String);
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output);
await conn.ExecuteAsync("mem_save_blast", parameters, commandType: CommandType.StoredProcedure); List<SqlParameter> parameters = new List<SqlParameter>
{
new SqlParameter("@blast_key", mailing.Id),
new SqlParameter("@name", mailing.Name),
new SqlParameter("@description", mailing.Description),
new SqlParameter("@template_key", mailing.TemplateId),
new SqlParameter("@target_key", mailing.TargetId),
new SqlParameter("@blast_status_code", mailing.StatusCode),
new SqlParameter("@schedule_date", mailing.ScheduleDate ?? (object)DBNull.Value),
new SqlParameter("@sent_date", mailing.SentDate ?? (object)DBNull.Value),
new SqlParameter("@blast_recurring_type_code", mailing.RecurringTypeCode ?? (object)DBNull.Value),
new SqlParameter("@recurring_start_date", mailing.RecurringStartDate ?? (object)DBNull.Value),
new SqlParameter("@template_json", JsonSerializer.Serialize(mailing.Template, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower })),
pmSuccess
};
return parameters.Get<bool>("@success"); DataAccess dataAccess = GetDataAccess();
await dataAccess.CallActionProcedureAsync(parameters, "mem_save_blast");
bool success = pmSuccess.Value != null && (bool)pmSuccess.Value;
return success;
} }
public async Task<bool> CancelMailingAsync(int id) public async Task<bool> CancelMailingAsync(int id)
{ {
SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
{
Direction = ParameterDirection.Output
};
using SqlConnection conn = new SqlConnection(ConnectionString); var parameters = new List<SqlParameter>
var parameters = new DynamicParameters(); {
parameters.Add("@blast_key", id, DbType.Int32); new SqlParameter("@blast_key", id),
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output); pmSuccess
};
await conn.ExecuteAsync("mem_cancel_blast", parameters, commandType: CommandType.StoredProcedure); DataAccess dataAccess = GetDataAccess();
await dataAccess.CallActionProcedureAsync(parameters, "mem_cancel_blast");
return parameters.Get<bool>("@success"); bool success = pmSuccess.Value != null && (bool)pmSuccess.Value;
return success;
} }
} }
} }

View File

@ -1,11 +1,10 @@
using Dapper; using Microsoft.Data.SqlClient;
using Dapper.FluentMap;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Surge365.Core.Interfaces;
using Surge365.Core.Mapping;
using Surge365.Core.Services;
using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
using Surge365.MassEmailReact.Domain.Enums;
using Surge365.MassEmailReact.Domain.Enums.Extensions;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data; using System.Data;
@ -18,32 +17,37 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
public class ServerRepository : IServerRepository public class ServerRepository : IServerRepository
{ {
private IConfiguration _config; private IConfiguration _config;
private const string _connectionStringName = "MassEmail.ConnectionString"; private DataAccessFactory _dataAccessFactory;
private string? ConnectionString private IQueryMapper _queryMapper;
{ public DataAccess GetDataAccess(string connectionStringName = "MassEmail") => _dataAccessFactory.Get(connectionStringName) ?? throw new ArgumentNullException(nameof(_dataAccessFactory), $"DataAccess context for '{connectionStringName}' not found.");
get public ServerRepository(IConfiguration config, DataAccessFactory dataAccessFactory, IQueryMapper queryMapper)
{
return _config.GetConnectionString(_connectionStringName);
}
}
public ServerRepository(IConfiguration config)
{ {
_config = config; _config = config;
_dataAccessFactory = dataAccessFactory ?? throw new ArgumentNullException(nameof(dataAccessFactory), "DataAccessFactory cannot be null.");
_queryMapper = queryMapper;
#if DEBUG #if DEBUG
if (!FluentMapper.EntityMaps.ContainsKey(typeof(Server))) if (!_queryMapper.EntityMaps.ContainsKey(typeof(Server)))
{ {
throw new InvalidOperationException("Server dapper mapping is missing. Make sure ConfigureMappings() is called inside program.cs (program startup)."); throw new InvalidOperationException("Server query mapping is missing. Make sure ConfigureCustomMaps() is called inside program.cs (program startup).");
} }
#endif #endif
} }
public async Task<Server?> GetByIdAsync(int serverKey, bool returnPassword = false) public async Task<Server?> GetByIdAsync(int serverKey, bool returnPassword = false)
{ {
ArgumentNullException.ThrowIfNull(_config); ArgumentNullException.ThrowIfNull(_config);
ArgumentNullException.ThrowIfNull(_connectionStringName);
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName)); var parameters = new List<SqlParameter>
{
new SqlParameter("@server_key", serverKey)
};
DataAccess dataAccess = GetDataAccess();
var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_server_by_id");
var results = await _queryMapper.MapAsync<Server>(dataSet);
Server? server = results.FirstOrDefault();
Server? server = (await conn.QueryAsync<Server>("mem_get_server_by_id", new { server_key = serverKey }, commandType: CommandType.StoredProcedure)).FirstOrDefault();
if (server != null && !returnPassword) if (server != null && !returnPassword)
{ {
server.Password = ""; server.Password = "";
@ -52,18 +56,15 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
} }
public async Task<List<Server>> GetAllAsync(bool activeOnly = true, bool returnPassword = false) public async Task<List<Server>> GetAllAsync(bool activeOnly = true, bool returnPassword = false)
{ {
ArgumentNullException.ThrowIfNull(_config); var parameters = new List<SqlParameter>
ArgumentNullException.ThrowIfNull(_connectionStringName);
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName));
List<Server> servers = (await conn.QueryAsync<Server>("mem_get_server_all", new { active_only = activeOnly }, commandType: CommandType.StoredProcedure)).ToList();
if (!returnPassword)
{ {
foreach (Server server in servers) new SqlParameter("@active_only", activeOnly)
server.Password = ""; };
} DataAccess dataAccess = GetDataAccess();
return servers; var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_server_all");
var results = await _queryMapper.MapAsync<Server>(dataSet);
return results.ToList();
} }
public async Task<int?> CreateAsync(Server server) public async Task<int?> CreateAsync(Server server)
@ -72,26 +73,30 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
if (server.Id != null && server.Id > 0) if (server.Id != null && server.Id > 0)
throw new Exception("ID must be null"); throw new Exception("ID must be null");
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName)); SqlParameter pmServerKey = new SqlParameter("@server_key", SqlDbType.Int)
{
Direction = ParameterDirection.Output
};
SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
{
Direction = ParameterDirection.Output
};
List<SqlParameter> parameters = new List<SqlParameter>
{
pmServerKey,
new SqlParameter("@name", server.Name),
new SqlParameter("@server_name", server.ServerName),
new SqlParameter("@port", server.Port),
new SqlParameter("@username", server.Username),
new SqlParameter("@password", server.Password),
pmSuccess
};
var parameters = new DynamicParameters(); DataAccess dataAccess = GetDataAccess();
parameters.Add("@server_key", dbType: DbType.Int32, direction: ParameterDirection.Output); await dataAccess.CallActionProcedureAsync(parameters, "mem_save_server");
parameters.Add("@name", server.Name, DbType.String); bool success = pmSuccess.Value != null && (bool)pmSuccess.Value;
parameters.Add("@server_name", server.ServerName, DbType.String);
parameters.Add("@port", server.Port, DbType.Int16);
parameters.Add("@username", server.Username, DbType.String);
parameters.Add("@password", server.Password, DbType.String);
// Output parameter if (success) return (int?)pmServerKey.Value;
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output);
await conn.ExecuteAsync("mem_save_server", parameters, commandType: CommandType.StoredProcedure);
// Retrieve the output parameter value
bool success = parameters.Get<bool>("@success");
if (success)
return parameters.Get<int>("@server_key");
return null; return null;
} }
@ -99,31 +104,27 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
{ {
ArgumentNullException.ThrowIfNull(server); ArgumentNullException.ThrowIfNull(server);
ArgumentNullException.ThrowIfNull(server.Id); ArgumentNullException.ThrowIfNull(server.Id);
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName));
var parameters = new DynamicParameters(); SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
parameters.Add("@server_key", server.Id, DbType.Int32); {
parameters.Add("@name", server.Name, DbType.String); Direction = ParameterDirection.Output
parameters.Add("@server_name", server.ServerName, DbType.String); };
parameters.Add("@port", server.Port, DbType.Int32);
parameters.Add("@username", server.Username, DbType.String);
parameters.Add("@password", server.Password, DbType.String);
// Output parameter List<SqlParameter> parameters = new List<SqlParameter>
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output); {
new SqlParameter("@server_key", server.Id),
await conn.ExecuteAsync("mem_save_server", parameters, commandType: CommandType.StoredProcedure); new SqlParameter("@name", server.Name),
new SqlParameter("@server_name", server.ServerName),
// Retrieve the output parameter value new SqlParameter("@port", server.Port),
bool success = parameters.Get<bool>("@success"); new SqlParameter("@username", server.Username),
new SqlParameter("@password", server.Password),
pmSuccess
};
DataAccess dataAccess = GetDataAccess();
await dataAccess.CallActionProcedureAsync(parameters, "mem_save_server");
bool success = pmSuccess.Value != null && (bool)pmSuccess.Value;
return success; return success;
} }
//public void Add(Server server)
//{
// Servers.Add(server);
//}
} }
} }

View File

@ -1,12 +1,11 @@
using Dapper; using Microsoft.Data.SqlClient;
using Dapper.FluentMap;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Surge365.Core.Interfaces;
using Surge365.Core.Mapping;
using Surge365.Core.Services;
using Surge365.MassEmailReact.Application.DTOs; using Surge365.MassEmailReact.Application.DTOs;
using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
using Surge365.MassEmailReact.Domain.Enums;
using Surge365.MassEmailReact.Domain.Enums.Extensions;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data; using System.Data;
@ -21,70 +20,69 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
public class TargetRepository : ITargetRepository public class TargetRepository : ITargetRepository
{ {
private IConfiguration _config; private IConfiguration _config;
private const string _connectionStringName = "MassEmail.ConnectionString"; private DataAccessFactory _dataAccessFactory;
private string? ConnectionString private IQueryMapper _queryMapper;
{ public DataAccess GetDataAccess(string connectionStringName = "MassEmail") => _dataAccessFactory.Get(connectionStringName) ?? throw new ArgumentNullException(nameof(_dataAccessFactory), $"DataAccess context for '{connectionStringName}' not found.");
get
{ public TargetRepository(IConfiguration config, DataAccessFactory dataAccessFactory, IQueryMapper queryMapper)
return _config.GetConnectionString(_connectionStringName);
}
}
public TargetRepository(IConfiguration config)
{ {
_config = config; _config = config;
_dataAccessFactory = dataAccessFactory ?? throw new ArgumentNullException(nameof(dataAccessFactory), "DataAccessFactory cannot be null.");
_queryMapper = queryMapper;
#if DEBUG #if DEBUG
if (!FluentMapper.EntityMaps.ContainsKey(typeof(Target))) if (!_queryMapper.EntityMaps.ContainsKey(typeof(Target)))
{ {
throw new InvalidOperationException("Target dapper mapping is missing. Make sure ConfigureMappings() is called inside program.cs (program startup)."); throw new InvalidOperationException("Target query mapping is missing. Make sure ConfigureCustomMaps() is called inside program.cs (program startup).");
} }
#endif #endif
} }
public async Task<Target?> GetByIdAsync(int targetKey) public async Task<Target?> GetByIdAsync(int targetKey)
{ {
ArgumentNullException.ThrowIfNull(_config); ArgumentNullException.ThrowIfNull(_config);
ArgumentNullException.ThrowIfNull(_connectionStringName);
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName)); var parameters = new List<SqlParameter>
await conn.OpenAsync(); {
new SqlParameter("@target_key", targetKey)
};
using var multi = await conn.QueryMultipleAsync( DataAccess dataAccess = GetDataAccess();
"mem_get_target_by_id", var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_target_by_id");
new { target_key = targetKey },
commandType: CommandType.StoredProcedure);
// Read the first result set (Target) // Read the first result set (Target)
var target = await multi.ReadSingleOrDefaultAsync<Target>(); var targets = await _queryMapper.MapAsync<Target>(dataSet);
var target = targets.FirstOrDefault();
if (target == null) return null; if (target == null) return null;
// Read the second result set (TargetColumns) // Read the second result set (TargetColumns)
var columns = await multi.ReadAsync<TargetColumn>(); var columns = await _queryMapper.MapAsync<TargetColumn>(dataSet);
target.Columns = columns.ToList(); target.Columns = columns.ToList();
return target; return target;
} }
public async Task<List<Target>> GetAllAsync(bool activeOnly = true) public async Task<List<Target>> GetAllAsync(bool activeOnly = true)
{ {
ArgumentNullException.ThrowIfNull(_config); var parameters = new List<SqlParameter>
ArgumentNullException.ThrowIfNull(_connectionStringName); {
new SqlParameter("@active_only", activeOnly)
};
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName)); DataAccess dataAccess = GetDataAccess();
await conn.OpenAsync(); var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_target_all");
using var multi = await conn.QueryMultipleAsync(
"mem_get_target_all",
new { active_only = activeOnly },
commandType: CommandType.StoredProcedure);
// Read the first result set (Targets) // Read the first result set (Targets)
var targets = (await multi.ReadAsync<Target>()).ToList(); var targets = await _queryMapper.MapAsync<Target>(dataSet);
if (!targets.Any()) return targets; var targetList = targets.ToList();
if (!targetList.Any()) return targetList;
// Read the second result set (TargetColumns) // Read the second result set (TargetColumns)
var columns = (await multi.ReadAsync<TargetColumn>()).ToList(); var columns = await _queryMapper.MapAsync<TargetColumn>(dataSet);
var columnList = columns.ToList();
// Map columns to their respective targets // Map columns to their respective targets
var targetDictionary = targets.ToDictionary(t => t.Id!.Value); var targetDictionary = targetList.ToDictionary(t => t.Id!.Value);
foreach (var column in columns) foreach (var column in columnList)
{ {
if (targetDictionary.TryGetValue(column.TargetId, out var target)) if (targetDictionary.TryGetValue(column.TargetId, out var target))
{ {
@ -92,7 +90,7 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
} }
} }
return targets; return targetList;
} }
public async Task<int?> CreateAsync(Target target) public async Task<int?> CreateAsync(Target target)
@ -101,61 +99,70 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
if (target.Id != null && target.Id > 0) if (target.Id != null && target.Id > 0)
throw new Exception("ID must be null"); throw new Exception("ID must be null");
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName)); SqlParameter pmTargetKey = new SqlParameter("@target_key", SqlDbType.Int)
{
Direction = ParameterDirection.Output
};
SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
{
Direction = ParameterDirection.Output
};
var parameters = new DynamicParameters(); List<SqlParameter> parameters = new List<SqlParameter>
parameters.Add("@target_key", dbType: DbType.Int32, direction: ParameterDirection.Output); {
parameters.Add("@server_key", target.ServerId, DbType.Int32); pmTargetKey,
parameters.Add("@name", target.Name, DbType.String); new SqlParameter("@server_key", target.ServerId),
parameters.Add("@database_name", target.DatabaseName, DbType.String); new SqlParameter("@name", target.Name),
parameters.Add("@view_name", target.ViewName, DbType.String); new SqlParameter("@database_name", target.DatabaseName),
parameters.Add("@filter_query", target.FilterQuery, DbType.String); new SqlParameter("@view_name", target.ViewName),
parameters.Add("@allow_write_back", target.AllowWriteBack, DbType.Boolean); new SqlParameter("@filter_query", target.FilterQuery),
parameters.Add("@is_active", target.IsActive, DbType.Boolean); new SqlParameter("@allow_write_back", target.AllowWriteBack),
if(target.Columns != null) new SqlParameter("@is_active", target.IsActive),
parameters.Add("@column_json", JsonSerializer.Serialize(target.Columns), DbType.String); new SqlParameter("@column_json", target.Columns != null ? JsonSerializer.Serialize(target.Columns) : (object)DBNull.Value),
pmSuccess
};
// Output parameter DataAccess dataAccess = GetDataAccess();
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output); await dataAccess.CallActionProcedureAsync(parameters, "mem_save_target");
await conn.ExecuteAsync("mem_save_target", parameters, commandType: CommandType.StoredProcedure);
// Retrieve the output parameter value
bool success = parameters.Get<bool>("@success");
bool success = pmSuccess.Value != null && (bool)pmSuccess.Value;
if (success) if (success)
return parameters.Get<int>("@target_key"); return (int?)pmTargetKey.Value;
return null; return null;
} }
public async Task<bool> UpdateAsync(Target target) public async Task<bool> UpdateAsync(Target target)
{ {
ArgumentNullException.ThrowIfNull(target); ArgumentNullException.ThrowIfNull(target);
ArgumentNullException.ThrowIfNull(target.Id); ArgumentNullException.ThrowIfNull(target.Id);
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName));
var parameters = new DynamicParameters(); SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
parameters.Add("@target_key", target.Id, DbType.Int32); {
parameters.Add("@server_key", target.ServerId, DbType.Int32); Direction = ParameterDirection.Output
parameters.Add("@name", target.Name, DbType.String); };
parameters.Add("@database_name", target.DatabaseName, DbType.String);
parameters.Add("@view_name", target.ViewName, DbType.String);
parameters.Add("@filter_query", target.FilterQuery, DbType.String);
parameters.Add("@allow_write_back", target.AllowWriteBack, DbType.Boolean);
parameters.Add("@is_active", target.IsActive, DbType.Boolean);
if (target.Columns != null)
parameters.Add("@column_json", JsonSerializer.Serialize(target.Columns), DbType.String);
// Output parameter List<SqlParameter> parameters = new List<SqlParameter>
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output); {
new SqlParameter("@target_key", target.Id),
new SqlParameter("@server_key", target.ServerId),
new SqlParameter("@name", target.Name),
new SqlParameter("@database_name", target.DatabaseName),
new SqlParameter("@view_name", target.ViewName),
new SqlParameter("@filter_query", target.FilterQuery),
new SqlParameter("@allow_write_back", target.AllowWriteBack),
new SqlParameter("@is_active", target.IsActive),
new SqlParameter("@column_json", target.Columns != null ? JsonSerializer.Serialize(target.Columns) : (object)DBNull.Value),
pmSuccess
};
await conn.ExecuteAsync("mem_save_target", parameters, commandType: CommandType.StoredProcedure); DataAccess dataAccess = GetDataAccess();
await dataAccess.CallActionProcedureAsync(parameters, "mem_save_target");
// Retrieve the output parameter value
bool success = parameters.Get<bool>("@success");
bool success = pmSuccess.Value != null && (bool)pmSuccess.Value;
return success; return success;
} }
public async Task<TargetSample?> TestTargetAsync( public async Task<TargetSample?> TestTargetAsync(
string serverName, string serverName,
short port, short port,
@ -179,6 +186,7 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
// Clean up server name // Clean up server name
serverName = serverName.Replace("110494-db", "www.surge365.com"); serverName = serverName.Replace("110494-db", "www.surge365.com");
// Get configuration // Get configuration
string sql = _config["TestTargetSql"] ?? ""; string sql = _config["TestTargetSql"] ?? "";
string connectionStringTemplate = _config["ConnectionStringTemplate"] ?? ""; string connectionStringTemplate = _config["ConnectionStringTemplate"] ?? "";
@ -201,20 +209,42 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
using var connection = new SqlConnection(connectionString); using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(); await connection.OpenAsync();
// Assuming the SQL returns three result sets: columns, sample data, and row count // Execute the SQL and get multiple result sets
using var multi = await connection.QueryMultipleAsync(sql, commandTimeout: 300); using var command = new SqlCommand(sql, connection);
command.CommandTimeout = 300;
// Read column definitions using var adapter = new SqlDataAdapter(command);
var columnEntities = await multi.ReadAsync<dynamic>(); var dataSet = new DataSet();
if (!columnEntities.Any()) adapter.Fill(dataSet);
if (dataSet.Tables.Count < 3)
return null; return null;
// Read sample data // Read column definitions (first table)
var sampleRows = (await multi.ReadAsync<dynamic>()).ToList(); var columnTable = dataSet.Tables[0];
if (columnTable.Rows.Count == 0)
return null;
// Read row count // Read sample data (second table)
var rowCountResult = await multi.ReadSingleAsync<dynamic>(); var sampleTable = dataSet.Tables[1];
int rowCount = Convert.ToInt32(rowCountResult.row_count); var sampleRows = new List<Dictionary<string, string>>();
foreach (DataRow row in sampleTable.Rows)
{
var dict = new Dictionary<string, string>();
foreach (DataColumn col in sampleTable.Columns)
{
dict[col.ColumnName] = row[col]?.ToString() ?? "";
}
sampleRows.Add(dict);
}
// Read row count (third table)
var rowCountTable = dataSet.Tables[2];
int rowCount = 0;
if (rowCountTable.Rows.Count > 0)
{
rowCount = Convert.ToInt32(rowCountTable.Rows[0]["row_count"]);
}
// Build TargetSample // Build TargetSample
var targetSample = new TargetSample(); var targetSample = new TargetSample();
@ -224,10 +254,10 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
bool bounceFound = false; bool bounceFound = false;
bool unsubFound = false; bool unsubFound = false;
foreach (var col in columnEntities) foreach (DataRow col in columnTable.Rows)
{ {
string name = col.name; string name = col["name"].ToString();
string dataType = col.data_type; string dataType = col["data_type"].ToString();
string typeCode = TargetColumnType.General; string typeCode = TargetColumnType.General;
if (!emailFound && name.ToUpper().Contains("EMAIL")) if (!emailFound && name.ToUpper().Contains("EMAIL"))
@ -259,13 +289,8 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
}; };
} }
// Convert sample rows to dictionary format // Add sample rows
foreach (var row in sampleRows) targetSample.Rows.AddRange(sampleRows);
{
var dict = ((IDictionary<string, object>)row)
.ToDictionary(k => k.Key, v => v.Value?.ToString() ?? "");
targetSample.Rows.Add(dict);
}
return targetSample; return targetSample;
} }

View File

@ -1,7 +1,8 @@
using Dapper; using Microsoft.Data.SqlClient;
using Dapper.FluentMap;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Surge365.Core.Interfaces;
using Surge365.Core.Mapping;
using Surge365.Core.Services;
using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
using System; using System;
@ -15,22 +16,19 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
public class TemplateRepository : ITemplateRepository public class TemplateRepository : ITemplateRepository
{ {
private IConfiguration _config; private IConfiguration _config;
private const string _connectionStringName = "MassEmail.ConnectionString"; private DataAccessFactory _dataAccessFactory;
private string? ConnectionString private IQueryMapper _queryMapper;
{ public DataAccess GetDataAccess(string connectionStringName = "MassEmail") => _dataAccessFactory.Get(connectionStringName) ?? throw new ArgumentNullException(nameof(_dataAccessFactory), $"DataAccess context for '{connectionStringName}' not found.");
get
{
return _config.GetConnectionString(_connectionStringName);
}
}
public TemplateRepository(IConfiguration config) public TemplateRepository(IConfiguration config, DataAccessFactory dataAccessFactory, IQueryMapper queryMapper)
{ {
_config = config; _config = config;
_dataAccessFactory = dataAccessFactory ?? throw new ArgumentNullException(nameof(dataAccessFactory), "DataAccessFactory cannot be null.");
_queryMapper = queryMapper;
#if DEBUG #if DEBUG
if (!FluentMapper.EntityMaps.ContainsKey(typeof(Template))) if (!_queryMapper.EntityMaps.ContainsKey(typeof(Template)))
{ {
throw new InvalidOperationException("Template dapper mapping is missing. Make sure ConfigureMappings() is called inside program.cs (program startup)."); throw new InvalidOperationException("Template query mapping is missing. Make sure ConfigureCustomMaps() is called inside program.cs (program startup).");
} }
#endif #endif
} }
@ -38,21 +36,28 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
public async Task<Template?> GetByIdAsync(int id) public async Task<Template?> GetByIdAsync(int id)
{ {
ArgumentNullException.ThrowIfNull(_config); ArgumentNullException.ThrowIfNull(_config);
ArgumentNullException.ThrowIfNull(_connectionStringName);
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName)); var parameters = new List<SqlParameter>
return (await conn.QueryAsync<Template>("mem_get_template_by_id", new { template_key = id }, {
commandType: CommandType.StoredProcedure)).FirstOrDefault(); new SqlParameter("@template_key", id)
};
DataAccess dataAccess = GetDataAccess();
var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_template_by_id");
var results = await _queryMapper.MapAsync<Template>(dataSet);
return results.FirstOrDefault();
} }
public async Task<List<Template>> GetAllAsync(bool activeOnly = true) public async Task<List<Template>> GetAllAsync(bool activeOnly = true)
{ {
ArgumentNullException.ThrowIfNull(_config); var parameters = new List<SqlParameter>();
ArgumentNullException.ThrowIfNull(_connectionStringName);
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName)); DataAccess dataAccess = GetDataAccess();
return (await conn.QueryAsync<Template>("mem_get_template_all", new { }, var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_template_all");
commandType: CommandType.StoredProcedure)).ToList();
var results = await _queryMapper.MapAsync<Template>(dataSet);
return results.ToList();
} }
public async Task<int?> CreateAsync(Template template) public async Task<int?> CreateAsync(Template template)
@ -61,27 +66,38 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
if (template.Id != null && template.Id > 0) if (template.Id != null && template.Id > 0)
throw new Exception("ID must be null"); throw new Exception("ID must be null");
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName)); SqlParameter pmTemplateKey = new SqlParameter("@template_key", SqlDbType.Int)
var parameters = new DynamicParameters(); {
parameters.Add("@template_key", dbType: DbType.Int32, direction: ParameterDirection.Output); Direction = ParameterDirection.Output
parameters.Add("@name", template.Name, DbType.String); };
parameters.Add("@domain_key", template.DomainId, DbType.Int32); SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
parameters.Add("@description", template.Description, DbType.String); {
parameters.Add("@html_body", template.HtmlBody, DbType.String); Direction = ParameterDirection.Output
parameters.Add("@subject", template.Subject, DbType.String); };
parameters.Add("@to_name", template.ToName, DbType.String);
parameters.Add("@from_name", template.FromName, DbType.String);
parameters.Add("@from_email", template.FromEmail, DbType.String);
parameters.Add("@reply_to_email", template.ReplyToEmail, DbType.String);
parameters.Add("@click_tracking", template.ClickTracking, DbType.Boolean);
parameters.Add("@open_tracking", template.OpenTracking, DbType.Boolean);
parameters.Add("@category_xml", template.CategoryXml, DbType.Xml);
parameters.Add("@is_active", template.IsActive, DbType.Boolean);
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output);
await conn.ExecuteAsync("mem_save_template", parameters, commandType: CommandType.StoredProcedure); List<SqlParameter> parameters = new List<SqlParameter>
bool success = parameters.Get<bool>("@success"); {
return success ? parameters.Get<int>("@template_key") : null; pmTemplateKey,
new SqlParameter("@name", template.Name),
new SqlParameter("@domain_key", template.DomainId),
new SqlParameter("@description", template.Description),
new SqlParameter("@html_body", template.HtmlBody),
new SqlParameter("@subject", template.Subject),
new SqlParameter("@to_name", template.ToName),
new SqlParameter("@from_name", template.FromName),
new SqlParameter("@from_email", template.FromEmail),
new SqlParameter("@reply_to_email", template.ReplyToEmail),
new SqlParameter("@click_tracking", template.ClickTracking),
new SqlParameter("@open_tracking", template.OpenTracking),
new SqlParameter("@category_xml", template.CategoryXml),
new SqlParameter("@is_active", template.IsActive),
pmSuccess
};
DataAccess dataAccess = GetDataAccess();
await dataAccess.CallActionProcedureAsync(parameters, "mem_save_template");
bool success = pmSuccess.Value != null && (bool)pmSuccess.Value;
return success ? (int?)pmTemplateKey.Value : null;
} }
public async Task<bool> UpdateAsync(Template template) public async Task<bool> UpdateAsync(Template template)
@ -89,26 +105,34 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
ArgumentNullException.ThrowIfNull(template); ArgumentNullException.ThrowIfNull(template);
ArgumentNullException.ThrowIfNull(template.Id); ArgumentNullException.ThrowIfNull(template.Id);
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName)); SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
var parameters = new DynamicParameters(); {
parameters.Add("@template_key", template.Id, DbType.Int32); Direction = ParameterDirection.Output
parameters.Add("@name", template.Name, DbType.String); };
parameters.Add("@domain_key", template.DomainId, DbType.Int32);
parameters.Add("@description", template.Description, DbType.String);
parameters.Add("@html_body", template.HtmlBody, DbType.String);
parameters.Add("@subject", template.Subject, DbType.String);
parameters.Add("@to_name", template.ToName, DbType.String);
parameters.Add("@from_name", template.FromName, DbType.String);
parameters.Add("@from_email", template.FromEmail, DbType.String);
parameters.Add("@reply_to_email", template.ReplyToEmail, DbType.String);
parameters.Add("@click_tracking", template.ClickTracking, DbType.Boolean);
parameters.Add("@open_tracking", template.OpenTracking, DbType.Boolean);
parameters.Add("@category_xml", template.CategoryXml, DbType.Xml);
parameters.Add("@is_active", template.IsActive, DbType.Boolean);
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output);
await conn.ExecuteAsync("mem_save_template", parameters, commandType: CommandType.StoredProcedure); List<SqlParameter> parameters = new List<SqlParameter>
return parameters.Get<bool>("@success"); {
new SqlParameter("@template_key", template.Id),
new SqlParameter("@name", template.Name),
new SqlParameter("@domain_key", template.DomainId),
new SqlParameter("@description", template.Description),
new SqlParameter("@html_body", template.HtmlBody),
new SqlParameter("@subject", template.Subject),
new SqlParameter("@to_name", template.ToName),
new SqlParameter("@from_name", template.FromName),
new SqlParameter("@from_email", template.FromEmail),
new SqlParameter("@reply_to_email", template.ReplyToEmail),
new SqlParameter("@click_tracking", template.ClickTracking),
new SqlParameter("@open_tracking", template.OpenTracking),
new SqlParameter("@category_xml", template.CategoryXml),
new SqlParameter("@is_active", template.IsActive),
pmSuccess
};
DataAccess dataAccess = GetDataAccess();
await dataAccess.CallActionProcedureAsync(parameters, "mem_save_template");
bool success = pmSuccess.Value != null && (bool)pmSuccess.Value;
return success;
} }
} }
} }

View File

@ -1,6 +1,8 @@
using Dapper; using Microsoft.Data.SqlClient;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Surge365.Core.Interfaces;
using Surge365.Core.Mapping;
using Surge365.Core.Services;
using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
using System; using System;
@ -14,36 +16,48 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
public class TestEmailListRepository : ITestEmailListRepository public class TestEmailListRepository : ITestEmailListRepository
{ {
private readonly IConfiguration _config; private readonly IConfiguration _config;
private const string _connectionStringName = "MassEmail.ConnectionString"; private DataAccessFactory _dataAccessFactory;
private IQueryMapper _queryMapper;
public DataAccess GetDataAccess(string connectionStringName = "MassEmail") => _dataAccessFactory.Get(connectionStringName) ?? throw new ArgumentNullException(nameof(_dataAccessFactory), $"DataAccess context for '{connectionStringName}' not found.");
public TestEmailListRepository(IConfiguration config) public TestEmailListRepository(IConfiguration config, DataAccessFactory dataAccessFactory, IQueryMapper queryMapper)
{ {
_config = config; _config = config;
_dataAccessFactory = dataAccessFactory ?? throw new ArgumentNullException(nameof(dataAccessFactory), "DataAccessFactory cannot be null.");
_queryMapper = queryMapper;
#if DEBUG
if (!_queryMapper.EntityMaps.ContainsKey(typeof(TestEmailList)))
{
throw new InvalidOperationException("TestEmailList query mapping is missing. Make sure ConfigureCustomMaps() is called inside program.cs (program startup).");
}
#endif
} }
public async Task<TestEmailList?> GetByIdAsync(int id) public async Task<TestEmailList?> GetByIdAsync(int id)
{ {
ArgumentNullException.ThrowIfNull(_config); ArgumentNullException.ThrowIfNull(_config);
ArgumentNullException.ThrowIfNull(_connectionStringName);
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName)); var parameters = new List<SqlParameter>
return (await conn.QueryAsync<TestEmailList>( {
"mem_get_test_email_list_by_id", new SqlParameter("@test_email_list_key", id)
new { test_email_list_key = id }, };
commandType: CommandType.StoredProcedure
)).FirstOrDefault(); DataAccess dataAccess = GetDataAccess();
var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_test_email_list_by_id");
var results = await _queryMapper.MapAsync<TestEmailList>(dataSet);
return results.FirstOrDefault();
} }
public async Task<List<TestEmailList>> GetAllAsync() public async Task<List<TestEmailList>> GetAllAsync()
{ {
ArgumentNullException.ThrowIfNull(_config); var parameters = new List<SqlParameter>();
ArgumentNullException.ThrowIfNull(_connectionStringName);
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName)); DataAccess dataAccess = GetDataAccess();
return (await conn.QueryAsync<TestEmailList>( var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_test_email_list_all");
"mem_get_test_email_list_all",
commandType: CommandType.StoredProcedure var results = await _queryMapper.MapAsync<TestEmailList>(dataSet);
)).ToList(); return results.ToList();
} }
public async Task<int?> CreateAsync(TestEmailList testEmailList) public async Task<int?> CreateAsync(TestEmailList testEmailList)
@ -52,17 +66,28 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
if (testEmailList.Id != null && testEmailList.Id > 0) if (testEmailList.Id != null && testEmailList.Id > 0)
throw new Exception("ID must be null"); throw new Exception("ID must be null");
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName)); SqlParameter pmTestEmailListKey = new SqlParameter("@test_email_list_key", SqlDbType.Int)
var parameters = new DynamicParameters(); {
parameters.Add("@test_email_list_key", dbType: DbType.Int32, direction: ParameterDirection.Output); Direction = ParameterDirection.Output
parameters.Add("@name", testEmailList.Name, DbType.String); };
parameters.Add("@list", testEmailList.GetListXml(), DbType.String); SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output); {
Direction = ParameterDirection.Output
};
await conn.ExecuteAsync("mem_save_test_email_list", parameters, commandType: CommandType.StoredProcedure); List<SqlParameter> parameters = new List<SqlParameter>
bool success = parameters.Get<bool>("@success"); {
pmTestEmailListKey,
new SqlParameter("@name", testEmailList.Name),
new SqlParameter("@list", testEmailList.GetListXml()),
pmSuccess
};
DataAccess dataAccess = GetDataAccess();
await dataAccess.CallActionProcedureAsync(parameters, "mem_save_test_email_list");
bool success = pmSuccess.Value != null && (bool)pmSuccess.Value;
if (success) if (success)
return parameters.Get<int>("@test_email_list_key"); return (int?)pmTestEmailListKey.Value;
return null; return null;
} }
@ -71,15 +96,23 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
ArgumentNullException.ThrowIfNull(testEmailList); ArgumentNullException.ThrowIfNull(testEmailList);
ArgumentNullException.ThrowIfNull(testEmailList.Id); ArgumentNullException.ThrowIfNull(testEmailList.Id);
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName)); SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
var parameters = new DynamicParameters(); {
parameters.Add("@test_email_list_key", testEmailList.Id, DbType.Int32); Direction = ParameterDirection.Output
parameters.Add("@name", testEmailList.Name, DbType.String); };
parameters.Add("@list", testEmailList.GetListXml(), DbType.String);
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output);
await conn.ExecuteAsync("mem_save_test_email_list", parameters, commandType: CommandType.StoredProcedure); List<SqlParameter> parameters = new List<SqlParameter>
return parameters.Get<bool>("@success"); {
new SqlParameter("@test_email_list_key", testEmailList.Id),
new SqlParameter("@name", testEmailList.Name),
new SqlParameter("@list", testEmailList.GetListXml()),
pmSuccess
};
DataAccess dataAccess = GetDataAccess();
await dataAccess.CallActionProcedureAsync(parameters, "mem_save_test_email_list");
bool success = pmSuccess.Value != null && (bool)pmSuccess.Value;
return success;
} }
} }
} }

View File

@ -1,12 +1,14 @@
using Dapper; using Microsoft.Data.SqlClient;
using Dapper.FluentMap;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Surge365.Core.Interfaces;
using Surge365.Core.Mapping;
using Surge365.Core.Services;
using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data; using System.Data;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Infrastructure.Repositories namespace Surge365.MassEmailReact.Infrastructure.Repositories
@ -14,81 +16,104 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
public class UnsubscribeUrlRepository : IUnsubscribeUrlRepository public class UnsubscribeUrlRepository : IUnsubscribeUrlRepository
{ {
private IConfiguration _config; private IConfiguration _config;
private const string _connectionStringName = "MassEmail.ConnectionString"; private DataAccessFactory _dataAccessFactory;
private string? ConnectionString private IQueryMapper _queryMapper;
{ public DataAccess GetDataAccess(string connectionStringName = "MassEmail") => _dataAccessFactory.Get(connectionStringName) ?? throw new ArgumentNullException(nameof(_dataAccessFactory), $"DataAccess context for '{connectionStringName}' not found.");
get
{ public UnsubscribeUrlRepository(IConfiguration config, DataAccessFactory dataAccessFactory, IQueryMapper queryMapper)
return _config.GetConnectionString(_connectionStringName);
}
}
public UnsubscribeUrlRepository(IConfiguration config)
{ {
_config = config; _config = config;
_dataAccessFactory = dataAccessFactory ?? throw new ArgumentNullException(nameof(dataAccessFactory), "DataAccessFactory cannot be null.");
_queryMapper = queryMapper;
#if DEBUG #if DEBUG
if (!FluentMapper.EntityMaps.ContainsKey(typeof(UnsubscribeUrl))) if (!_queryMapper.EntityMaps.ContainsKey(typeof(UnsubscribeUrl)))
{ {
throw new InvalidOperationException("UnsubscribeUrl dapper mapping is missing. Make sure ConfigureMappings() is called inside program.cs (program startup)."); throw new InvalidOperationException("UnsubscribeUrl query mapping is missing. Make sure ConfigureCustomMaps() is called inside program.cs (program startup).");
} }
#endif #endif
} }
public async Task<UnsubscribeUrl?> GetByIdAsync(int unsubscribeUrlKey) public async Task<UnsubscribeUrl?> GetByIdAsync(int unsubscribeUrlKey)
{ {
ArgumentNullException.ThrowIfNull(_config); ArgumentNullException.ThrowIfNull(_config);
ArgumentNullException.ThrowIfNull(_connectionStringName);
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName)); var parameters = new List<SqlParameter>
{
new SqlParameter("@unsubscribe_url_key", unsubscribeUrlKey)
};
return (await conn.QueryAsync<UnsubscribeUrl>("mem_get_unsubscribe_url_by_id", new { unsubscribe_url_key = unsubscribeUrlKey }, commandType: CommandType.StoredProcedure)).FirstOrDefault(); DataAccess dataAccess = GetDataAccess();
var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_unsubscribe_url_by_id");
var results = await _queryMapper.MapAsync<UnsubscribeUrl>(dataSet);
return results.FirstOrDefault();
} }
public async Task<List<UnsubscribeUrl>> GetAllAsync(bool activeOnly = true) public async Task<List<UnsubscribeUrl>> GetAllAsync(bool activeOnly = true)
{ {
ArgumentNullException.ThrowIfNull(_config); var parameters = new List<SqlParameter>();
ArgumentNullException.ThrowIfNull(_connectionStringName);
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName)); DataAccess dataAccess = GetDataAccess();
var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_unsubscribe_url_all");
return (await conn.QueryAsync<UnsubscribeUrl>("mem_get_unsubscribe_url_all", new { }, commandType: CommandType.StoredProcedure)).ToList(); var results = await _queryMapper.MapAsync<UnsubscribeUrl>(dataSet);
return results.ToList();
} }
public async Task<int?> CreateAsync(UnsubscribeUrl unsubscribeUrl) public async Task<int?> CreateAsync(UnsubscribeUrl unsubscribeUrl)
{ {
ArgumentNullException.ThrowIfNull(unsubscribeUrl); ArgumentNullException.ThrowIfNull(unsubscribeUrl);
if (unsubscribeUrl.Id != null && unsubscribeUrl.Id > 0) if (unsubscribeUrl.Id != null && unsubscribeUrl.Id > 0)
throw new Exception("ID must be null"); throw new Exception("ID must be null");
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName)); SqlParameter pmUnsubscribeUrlKey = new SqlParameter("@unsubscribe_url_key", SqlDbType.Int)
{
Direction = ParameterDirection.Output
};
SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
{
Direction = ParameterDirection.Output
};
var parameters = new DynamicParameters(); List<SqlParameter> parameters = new List<SqlParameter>
parameters.Add("@unsubscribe_url_key", dbType: DbType.Int32, direction: ParameterDirection.Output); {
parameters.Add("@name", unsubscribeUrl.Name, DbType.String); pmUnsubscribeUrlKey,
parameters.Add("@url", unsubscribeUrl.Url, DbType.String); new SqlParameter("@name", unsubscribeUrl.Name),
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output); new SqlParameter("@url", unsubscribeUrl.Url),
pmSuccess
};
await conn.ExecuteAsync("mem_save_unsubscribe_url", parameters, commandType: CommandType.StoredProcedure); DataAccess dataAccess = GetDataAccess();
await dataAccess.CallActionProcedureAsync(parameters, "mem_save_unsubscribe_url");
bool success = parameters.Get<bool>("@success"); bool success = pmSuccess.Value != null && (bool)pmSuccess.Value;
if (success) if (success)
return parameters.Get<int>("@unsubscribe_url_key"); return (int?)pmUnsubscribeUrlKey.Value;
return null; return null;
} }
public async Task<bool> UpdateAsync(UnsubscribeUrl unsubscribeUrl) public async Task<bool> UpdateAsync(UnsubscribeUrl unsubscribeUrl)
{ {
ArgumentNullException.ThrowIfNull(unsubscribeUrl); ArgumentNullException.ThrowIfNull(unsubscribeUrl);
ArgumentNullException.ThrowIfNull(unsubscribeUrl.Id); ArgumentNullException.ThrowIfNull(unsubscribeUrl.Id);
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName)); SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
{
Direction = ParameterDirection.Output
};
var parameters = new DynamicParameters(); List<SqlParameter> parameters = new List<SqlParameter>
parameters.Add("@unsubscribe_url_key", unsubscribeUrl.Id, DbType.Int32); {
parameters.Add("@name", unsubscribeUrl.Name, DbType.String); new SqlParameter("@unsubscribe_url_key", unsubscribeUrl.Id),
parameters.Add("@url", unsubscribeUrl.Url, DbType.String); new SqlParameter("@name", unsubscribeUrl.Name),
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output); new SqlParameter("@url", unsubscribeUrl.Url),
pmSuccess
};
await conn.ExecuteAsync("mem_save_unsubscribe_url", parameters, commandType: CommandType.StoredProcedure); DataAccess dataAccess = GetDataAccess();
await dataAccess.CallActionProcedureAsync(parameters, "mem_save_unsubscribe_url");
bool success = parameters.Get<bool>("@success"); bool success = pmSuccess.Value != null && (bool)pmSuccess.Value;
return success; return success;
} }

View File

@ -1,121 +0,0 @@
using Surge365.MassEmailReact.Application.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Extensions.Configuration;
using System.Net.Http.Json;
using Surge365.MassEmailReact.Application.DTOs.AuthApi;
namespace Surge365.MassEmailReact.Infrastructure.Services
{
public class AuthService : IAuthService
{
private readonly IConfiguration _config;
string ApiUrl
{
get
{
var apiUrl = _config["AdminAuth:Url"];
if (string.IsNullOrEmpty(apiUrl))
throw new Exception("Auth URL is not configured");
return apiUrl;
}
}
string ApiKey
{
get
{
var apiKey = _config["AdminAuth:ApiKey"];
if (string.IsNullOrEmpty(apiKey))
throw new Exception("Api Key is not configured");
return apiKey;
}
}
string AuthAppCode
{
get
{
var appCode = _config["AuthAppCode"];
if (string.IsNullOrEmpty(appCode))
throw new Exception("Auth App Code is not configured");
return appCode;
}
}
public AuthService(IConfiguration config)
{
_config = config;
}
public async Task<(bool authenticated, (User user, string accessToken, string refreshToken)? data, string errorMessage)> Authenticate(string refreshToken)
{
var authResponse = await AuthenticateAtApi(refreshToken);
if (!authResponse.success) return (false, null, authResponse.authResponse.message ?? "Authentication failed");
if (string.IsNullOrWhiteSpace(authResponse.authResponse.accessToken) || string.IsNullOrWhiteSpace(authResponse.authResponse.refreshToken) || authResponse.authResponse.user == null)
return (false, null, authResponse.authResponse.message ?? "Authentication failed");
return (true, (authResponse.authResponse.user, authResponse.authResponse.accessToken, authResponse.authResponse.refreshToken), "");
}
public async Task<(bool authenticated, (User user, string accessToken, string refreshToken)? data, string errorMessage)> Authenticate(string username, string password)
{
var authResponse = await AuthenticateAtApi(username, password);
if (!authResponse.success) return (false, null, "Authentication failed");
if (string.IsNullOrWhiteSpace(authResponse.authResponse.accessToken) || string.IsNullOrWhiteSpace(authResponse.authResponse.refreshToken) || authResponse.authResponse.user == null)
return (false, null, authResponse.authResponse.message ?? "Authentication failed");
return (true, (authResponse.authResponse.user, authResponse.authResponse.accessToken, authResponse.authResponse.refreshToken), "");
}
async Task<(bool success, AuthResponse authResponse)> AuthenticateAtApi(string refreshToken)
{
var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri(ApiUrl);
httpClient.DefaultRequestHeaders.Add("X-Api-Key", ApiKey);
RefreshTokenApiRequest request = new RefreshTokenApiRequest()
{
appCode = AuthAppCode,
refreshToken = refreshToken
};
var response = await httpClient.PostAsJsonAsync("authentication/refreshtoken", request);
if (!response.IsSuccessStatusCode) return (false, new AuthResponse() { message = "Authentication failed" });
var authResponse = await response.Content.ReadFromJsonAsync<AuthResponse>();
if (authResponse?.user == null) return (false, new AuthResponse() { message = "Authentication failed" });
return (true, authResponse);
}
async Task<(bool success, AuthResponse authResponse)> AuthenticateAtApi(string username, string password)
{
var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri(ApiUrl);
httpClient.DefaultRequestHeaders.Add("X-Api-Key", ApiKey);
AuthenticateApiRequest request = new AuthenticateApiRequest()
{
appCode = AuthAppCode,
username = username,
password = password
};
var response = await httpClient.PostAsJsonAsync("authentication/authenticate", request);
if (!response.IsSuccessStatusCode) return (false, new AuthResponse() { message = "Authentication failed" });
var authResponse = await response.Content.ReadFromJsonAsync<AuthResponse>();
if (authResponse?.user == null) return (false, new AuthResponse() { message = "Authentication failed" });
return (true, authResponse);
}
}
}

View File

@ -1,66 +0,0 @@
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using Surge365.MassEmailReact.Application.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata.Ecma335;
using System.Text;
using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Infrastructure.Services
{
public class LoggingService(IConfiguration configuration) : ILoggingService
{
private IConfiguration _configuration = configuration;
public async Task<bool> InsertLog(LogLevels level, string logger, int? userId, string task, string message,
string exceptionStack, string exceptionMessage, string exceptionInnerMessage = "",
string customMessage1 = "", string customMessage2 = "", string customMessage3 = "",
string customMessage4 = "", string customMessage5 = "")
{
try
{
string applicationCode = _configuration["AppCode"] ?? "";
List<SqlParameter> pms = new List<SqlParameter> {
new SqlParameter("@ApplicationCode", applicationCode),
new SqlParameter("@level", level),
new SqlParameter("@logger", (string.IsNullOrWhiteSpace(logger) ? this.GetType().Name : logger)),
new SqlParameter("@UserKey", userId),
new SqlParameter("@Task", task),
new SqlParameter("@Message", message),
new SqlParameter("@ExceptionStack", exceptionStack),
new SqlParameter("@ExceptionMessage", exceptionMessage),
new SqlParameter("@ExceptionInnerMessage", exceptionInnerMessage),
new SqlParameter("@CustomMessage1", customMessage1),
new SqlParameter("@CustomMessage2", customMessage2),
new SqlParameter("@CustomMessage3", customMessage3),
new SqlParameter("@CustomMessage4", customMessage4),
new SqlParameter("@CustomMessage5", customMessage5)
};
DataAccess da = new DataAccess(_configuration, "YTBLog.ConnectionString");
await da.CallActionProcedureAsync(pms, "usp_InsertLog");
}
catch
{
return false;
}
return true;
}
public Task<bool> LogError(Exception ex)
{
string exceptionMessage = "";
string exceptionInnerMessage = "";
string exceptionStack = "";
exceptionMessage = ex.Message;
exceptionStack = ex.StackTrace ?? "";
if (ex.InnerException != null)
{
exceptionInnerMessage = ex.InnerException.Message;
}
return InsertLog(LogLevels.Error, "", null, "LogError", ex.Message,
exceptionStack, exceptionMessage, exceptionInnerMessage, "", "", "", "", "");
}
}
}

View File

@ -10,6 +10,7 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using SendGrid; using SendGrid;
using SendGrid.Helpers.Mail; using SendGrid.Helpers.Mail;
using System.Net.Http;
namespace Surge365.MassEmailReact.Infrastructure.Services namespace Surge365.MassEmailReact.Infrastructure.Services
{ {

View File

@ -1,5 +1,4 @@
using Dapper; using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Application.Interfaces;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;

View File

@ -7,23 +7,14 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\Core\Surge365.Core\Surge365.Core.csproj" />
<ProjectReference Include="..\Surge365.MassEmailReact.Application\Surge365.MassEmailReact.Application.csproj" /> <ProjectReference Include="..\Surge365.MassEmailReact.Application\Surge365.MassEmailReact.Application.csproj" />
<ProjectReference Include="..\Surge365.MassEmailReact.Domain\Surge365.MassEmailReact.Domain.csproj" /> <ProjectReference Include="..\Surge365.MassEmailReact.Domain\Surge365.MassEmailReact.Domain.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.4.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.6" />
<PackageReference Include="Dapper.FluentMap" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
<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.8.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,19 +0,0 @@
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Infrastructure
{
public static class Utilities
{
public static string GetAppCode(IConfiguration? configuration)
{
if (configuration == null)
return "";
return configuration["AppCode"] ?? "";
}
}
}

View File

@ -6,7 +6,7 @@ import { useAuth } from '@/components/auth/AuthContext';
const AuthCheck: React.FC = () => { const AuthCheck: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { accessToken, setAuth, isLoading } = useAuth(); const { accessToken, refreshToken, isLoading, refreshAuthToken } = useAuth();
useEffect(() => { useEffect(() => {
if (isLoading) return; // Wait for AuthProvider to finish if (isLoading) return; // Wait for AuthProvider to finish
@ -14,28 +14,16 @@ const AuthCheck: React.FC = () => {
if (currentPath.toLowerCase() === "/login") return; if (currentPath.toLowerCase() === "/login") return;
const tryRefreshToken = async () => { const tryRefreshToken = async () => {
try { const success = await refreshAuthToken();
const response = await fetch('/api/authentication/refreshtoken', { if (!success) {
method: 'POST',
credentials: 'include',
});
if (response.ok) {
const data = await response.json();
setAuth(data.accessToken);
} else {
setAuth(null);
navigate('/login');
}
} catch {
setAuth(null);
navigate('/login'); navigate('/login');
} }
}; };
if (!accessToken || utils.isTokenExpired(accessToken)) { if (refreshToken && (!accessToken || utils.isTokenExpired(accessToken))) {
tryRefreshToken(); tryRefreshToken();
} }
}, [navigate, location.pathname, accessToken, setAuth, isLoading]); }, [navigate, accessToken, refreshToken, location.pathname, isLoading, refreshAuthToken]);
return null; return null;
}; };

View File

@ -4,46 +4,71 @@ import { Box, CircularProgress, Typography } from '@mui/material';
interface AuthContextType { interface AuthContextType {
accessToken: string | null; accessToken: string | null;
refreshToken: string | null;
userRoles: string[]; userRoles: string[];
setAuth: (token: string | null) => void; // eslint-disable-next-line no-unused-vars
setAuth: (accessToken: string | null, refreshToken?: string | null) => void;
isLoading: boolean; // Add loading state isLoading: boolean; // Add loading state
refreshToken: () => Promise<boolean>; refreshAuthToken: () => Promise<boolean>;
syncAccessToken: () => Promise<void>;
} }
const AuthContext = createContext<AuthContextType>({ const AuthContext = createContext<AuthContextType>({
accessToken: null, accessToken: null,
refreshToken: null,
userRoles: [], userRoles: [],
setAuth: () => { }, setAuth: () => { },
isLoading: true, // Default to loading isLoading: true, // Default to loading
refreshToken: async () => false, refreshAuthToken: async () => false,
syncAccessToken: async () => { },
}); });
export const AuthProvider = ({ children }: { children: ReactNode }) => { export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [accessToken, setAccessToken] = useState<string | null>(null); // Start as null const [accessToken, setAccessToken] = useState<string | null>(null); // Start as null
const [refreshToken, setRefreshToken] = useState<string | null>(null); // Start as null
const [userRoles, setUserRoles] = useState<string[]>([]); const [userRoles, setUserRoles] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(true); // Track loading const [isLoading, setIsLoading] = useState(true); // Track loading
const setAuth = (token: string | null) => { const setAuth = (accessToken: string | null, refreshToken?: string | null) => {
if (token) { if (accessToken) {
localStorage.setItem('accessToken', token); localStorage.setItem('accessToken', accessToken);
setAccessToken(token); setAccessToken(accessToken);
setUserRoles(utils.getUserRoles(token)); setUserRoles(utils.getUserRoles(accessToken));
// Handle refreshToken if provided
if (refreshToken !== undefined && refreshToken !== null) {
localStorage.setItem('refreshToken', refreshToken);
setRefreshToken(refreshToken);
}
} else { } else {
localStorage.removeItem('accessToken'); localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
setAccessToken(null); setAccessToken(null);
setRefreshToken(null);
setUserRoles([]); setUserRoles([]);
} }
}; };
const refreshToken = async (): Promise<boolean> => { const refreshAuthToken = async (): Promise<boolean> => {
try { try {
const storedRefreshToken = localStorage.getItem('refreshToken');
if (!storedRefreshToken) {
setAuth(null);
return false;
}
const response = await fetch('/api/authentication/refreshtoken', { const response = await fetch('/api/authentication/refreshtoken', {
method: 'POST', method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', credentials: 'include',
body: JSON.stringify({ refreshToken: storedRefreshToken }),
}); });
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
setAuth(data.accessToken); setAuth(data.accessToken, data.refreshToken);
return true; return true;
} else { } else {
setAuth(null); setAuth(null);
@ -55,23 +80,47 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
} }
}; };
const syncAccessToken = async (): Promise<void> => {
if (!accessToken) {
const storedAccessToken = localStorage.getItem('accessToken');
if (storedAccessToken && !utils.isTokenExpired(storedAccessToken)) {
setAccessToken(storedAccessToken);
setUserRoles(utils.getUserRoles(storedAccessToken));
return;
}
}
else {
localStorage.setItem('accessToken', accessToken);
}
};
// Check auth on mount // Check auth on mount
useEffect(() => { useEffect(() => {
const initializeAuth = async () => { const initializeAuth = async () => {
const storedToken = localStorage.getItem('accessToken'); const storedAccessToken = localStorage.getItem('accessToken');
if (storedToken && !utils.isTokenExpired(storedToken)) { const storedRefreshToken = localStorage.getItem('refreshToken');
setAccessToken(storedToken);
setUserRoles(utils.getUserRoles(storedToken)); if (storedAccessToken && !utils.isTokenExpired(storedAccessToken)) {
setAccessToken(storedAccessToken);
setRefreshToken(storedRefreshToken);
setUserRoles(utils.getUserRoles(storedAccessToken));
setIsLoading(false); setIsLoading(false);
} else { } else {
try { try {
const response = await fetch('/api/authentication/refreshtoken', { if (storedRefreshToken) {
method: 'POST', const response = await fetch('/api/authentication/refreshtoken', {
credentials: 'include', method: 'POST',
}); headers: {
if (response.ok) { 'Content-Type': 'application/json',
const data = await response.json(); },
setAuth(data.accessToken); credentials: 'include',
body: JSON.stringify({ refreshToken: storedRefreshToken }),
});
if (response.ok) {
const data = await response.json();
setAuth(data.accessToken, data.refreshToken);
} else {
setAuth(null);
}
} else { } else {
setAuth(null); setAuth(null);
} }
@ -86,7 +135,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
}, []); }, []);
return ( return (
<AuthContext.Provider value={{ accessToken, userRoles, setAuth, isLoading, refreshToken }}> <AuthContext.Provider value={{ accessToken, refreshToken, userRoles, setAuth, isLoading, refreshAuthToken, syncAccessToken }}>
{isLoading ? ( {isLoading ? (
<Box <Box
sx={{ sx={{
@ -100,13 +149,13 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.1)', // Optional: light overlay backgroundColor: 'rgba(0, 0, 0, 0.1)', // Optional: light overlay
zIndex: 9999, // Ensure its above everything zIndex: 9999, // Ensure it's above everything
}} }}
> >
<CircularProgress <CircularProgress
size={80} // Larger spinner size={80} // Larger spinner
thickness={4} // Slightly thicker for visibility thickness={4} // Slightly thicker for visibility
sx={{ color: 'primary.main' }} // Use themes primary color sx={{ color: 'primary.main' }} // Use theme's primary color
/> />
<Typography <Typography
variant="h6" variant="h6"

View File

@ -60,6 +60,12 @@ const DrawerHeader = styled('div')(({ theme }) => ({
justifyContent: 'flex-end', justifyContent: 'flex-end',
})); }));
const getSystemTheme = (): 'light' | 'dark' => {
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
};
const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })<{ const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })<{
open?: boolean; open?: boolean;
}>(({ theme, open }) => ({ }>(({ theme, open }) => ({
@ -103,7 +109,7 @@ const Layout = ({ children }: LayoutProps) => {
{ text: 'Completed Mailings', icon: <CheckCircleIcon />, path: '/completedMailings' }, { text: 'Completed Mailings', icon: <CheckCircleIcon />, path: '/completedMailings' },
{ text: 'Cancelled Mailings', icon: <CancelIcon />, path: '/cancelledMailings' }, { text: 'Cancelled Mailings', icon: <CancelIcon />, path: '/cancelledMailings' },
]; ];
const { userRoles, setAuth } = useAuth(); // Use context const { userRoles, setAuth, refreshAuthToken } = useAuth(); // Use context
const [profileMenuAnchorEl, setProfileMenuAnchorEl] = React.useState<null | HTMLElement>(null); const [profileMenuAnchorEl, setProfileMenuAnchorEl] = React.useState<null | HTMLElement>(null);
const profileMenuOpen = Boolean(profileMenuAnchorEl); const profileMenuOpen = Boolean(profileMenuAnchorEl);
const handleOpenProfileMenu = (event: React.MouseEvent<HTMLButtonElement>) => { const handleOpenProfileMenu = (event: React.MouseEvent<HTMLButtonElement>) => {
@ -119,21 +125,8 @@ const Layout = ({ children }: LayoutProps) => {
}); });
const handleRefreshUser = async () => { const handleRefreshUser = async () => {
handleCloseProfileMenu(); handleCloseProfileMenu();
try { const success = await refreshAuthToken();
const response = await customFetch('/api/authentication/refreshtoken', { if (!success) {
method: 'POST',
credentials: 'include',
});
if (response.ok) {
const data = await response.json();
setAuth(data.accessToken); // Update context
} else {
setAuth(null); // Clear context on failure
navigate('/login');
}
} catch {
setAuth(null); // Clear context on failure
navigate('/login'); navigate('/login');
} }
} }
@ -157,6 +150,8 @@ const Layout = ({ children }: LayoutProps) => {
} }
}, [isMobile]); }, [isMobile]);
const effectiveMode = (mode === 'system' ? getSystemTheme() : mode) || "light";
const handleThemeChange = (event: SelectChangeEvent) => { const handleThemeChange = (event: SelectChangeEvent) => {
setMode(event.target.value as 'light' | 'dark'); setMode(event.target.value as 'light' | 'dark');
if (iconButtonRef.current) { if (iconButtonRef.current) {
@ -231,7 +226,7 @@ const Layout = ({ children }: LayoutProps) => {
<Select <Select
labelId="theme-select-label" labelId="theme-select-label"
id="theme-select" id="theme-select"
value={mode || 'light'} value={effectiveMode || 'light'}
label="Theme" label="Theme"
onChange={handleThemeChange} onChange={handleThemeChange}
sx={{ sx={{

View File

@ -88,165 +88,165 @@ const App = () => {
<ColorModeContext.Provider value={colorMode}> <ColorModeContext.Provider value={colorMode}>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<CssBaseline /> <CssBaseline />
<SetupDataProvider>
<AuthProvider> <AuthProvider>
<Router basename="/"> <Router basename="/">
<AuthCheck /> <AuthCheck />
<ToastContainer /> <SetupDataProvider>
<Routes> <ToastContainer />
<Route path="/" element={<Navigate to="/home" replace />} /> <Routes>
<Route <Route path="/" element={<Navigate to="/home" replace />} />
path="/home" <Route
element={ path="/home"
<PageWrapper title="Dashboard"> element={
<Layout> <PageWrapper title="Dashboard">
<Home /> <Layout>
</Layout> <Home />
</PageWrapper> </Layout>
} </PageWrapper>
/> }
<Route />
path="/servers" <Route
element={ path="/servers"
<PageWrapper title="Servers"> element={
<Layout> <PageWrapper title="Servers">
<Servers /> <Layout>
</Layout> <Servers />
</PageWrapper> </Layout>
} </PageWrapper>
/> }
<Route />
path="/targets" <Route
element={ path="/targets"
<PageWrapper title="Targets"> element={
<Layout> <PageWrapper title="Targets">
<Targets /> <Layout>
</Layout> <Targets />
</PageWrapper> </Layout>
} </PageWrapper>
/> }
<Route />
path="/testEmailLists" <Route
element={ path="/testEmailLists"
<PageWrapper title="Test Email Lists"> element={
<Layout> <PageWrapper title="Test Email Lists">
<TestEmailLists /> <Layout>
</Layout> <TestEmailLists />
</PageWrapper> </Layout>
} </PageWrapper>
/> }
<Route />
path="/blockedEmails" <Route
element={ path="/blockedEmails"
<PageWrapper title="Blocked Emails"> element={
<Layout> <PageWrapper title="Blocked Emails">
<BouncedEmails /> <Layout>
</Layout> <BouncedEmails />
</PageWrapper> </Layout>
} </PageWrapper>
/> }
<Route />
path="/blockedEmails" <Route
element={ path="/blockedEmails"
<PageWrapper title="Blocked Emails"> element={
<Layout> <PageWrapper title="Blocked Emails">
<BouncedEmails /> <Layout>
</Layout> <BouncedEmails />
</PageWrapper> </Layout>
} </PageWrapper>
/> }
<Route />
path="/emailDomains" <Route
element={ path="/emailDomains"
<PageWrapper title="Email Domains"> element={
<Layout> <PageWrapper title="Email Domains">
<EmailDomains /> <Layout>
</Layout> <EmailDomains />
</PageWrapper> </Layout>
} </PageWrapper>
/> }
<Route />
path="/unsubscribeUrls" <Route
element={ path="/unsubscribeUrls"
<PageWrapper title="Unsubscribe Urls"> element={
<Layout> <PageWrapper title="Unsubscribe Urls">
<UnsubscribeUrls /> <Layout>
</Layout> <UnsubscribeUrls />
</PageWrapper> </Layout>
} </PageWrapper>
/> }
<Route />
path="/templates" <Route
element={ path="/templates"
<PageWrapper title="Templates"> element={
<Layout> <PageWrapper title="Templates">
<Templates /> <Layout>
</Layout> <Templates />
</PageWrapper> </Layout>
} </PageWrapper>
/> }
<Route />
path="/newMailings" <Route
element={ path="/newMailings"
<PageWrapper title="New Mailings"> element={
<Layout> <PageWrapper title="New Mailings">
<NewMailings /> <Layout>
</Layout> <NewMailings />
</PageWrapper> </Layout>
} </PageWrapper>
/> }
<Route />
path="/scheduledMailings" <Route
element={ path="/scheduledMailings"
<PageWrapper title="Scheduled Mailings"> element={
<Layout> <PageWrapper title="Scheduled Mailings">
<ScheduledMailings /> <Layout>
</Layout> <ScheduledMailings />
</PageWrapper> </Layout>
} </PageWrapper>
/> }
<Route />
path="/activeMailings" <Route
element={ path="/activeMailings"
<PageWrapper title="Active Mailings"> element={
<Layout> <PageWrapper title="Active Mailings">
<ActiveMailings /> <Layout>
</Layout> <ActiveMailings />
</PageWrapper> </Layout>
} </PageWrapper>
/> }
<Route />
path="/cancelledMailings" <Route
element={ path="/cancelledMailings"
<PageWrapper title="Cancelled Mailings"> element={
<Layout> <PageWrapper title="Cancelled Mailings">
<CancelledMailings /> <Layout>
</Layout> <CancelledMailings />
</PageWrapper> </Layout>
} </PageWrapper>
/> }
<Route />
path="/completedMailings" <Route
element={ path="/completedMailings"
<PageWrapper title="Completed Mailings"> element={
<Layout> <PageWrapper title="Completed Mailings">
<CompletedMailings /> <Layout>
</Layout> <CompletedMailings />
</PageWrapper> </Layout>
} </PageWrapper>
/> }
<Route />
path="/login" <Route
element={ path="/login"
<LayoutLogin> element={
<Login /> <LayoutLogin>
</LayoutLogin> <Login />
} </LayoutLogin>
/> }
</Routes> />
</Routes>
</SetupDataProvider>
</Router> </Router>
</AuthProvider> </AuthProvider>
</SetupDataProvider>
</ThemeProvider> </ThemeProvider>
</ColorModeContext.Provider> </ColorModeContext.Provider>
); );

View File

@ -9,12 +9,14 @@ import {
Alert, Alert,
} from '@mui/material'; } from '@mui/material';
import { AuthResponse, User } from '@/types/auth'; import { AuthResponse, User } from '@/types/auth';
import { useAuth } from '@/components/auth/AuthContext';
//import ForgotPasswordModal from '@/components/modals/ForgotPasswordModal'; //import ForgotPasswordModal from '@/components/modals/ForgotPasswordModal';
type SpinnerState = Record<string, boolean>; type SpinnerState = Record<string, boolean>;
type FormErrors = Record<string, string>; type FormErrors = Record<string, string>;
function Login() { function Login() {
const { accessToken, setAuth } = useAuth();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [spinners, setSpinnersState] = useState<SpinnerState>({}); const [spinners, setSpinnersState] = useState<SpinnerState>({});
const [formErrors, setFormErrors] = useState<FormErrors>({}); const [formErrors, setFormErrors] = useState<FormErrors>({});
@ -84,7 +86,7 @@ function Login() {
if (response.ok) { if (response.ok) {
const json: AuthResponse = await response.json(); const json: AuthResponse = await response.json();
try { try {
localStorage.setItem('accessToken', json.accessToken); setAuth(json.accessToken, json.refreshToken);
loggedInUser = json.user; loggedInUser = json.user;
if (loggedInUser == null) { if (loggedInUser == null) {
@ -130,18 +132,22 @@ function Login() {
useEffect(() => { //Reset app settings to clear out prev login useEffect(() => { //Reset app settings to clear out prev login
const resetAppSettings = async () => { const resetAppSettings = async () => {
localStorage.removeItem('accessToken'); const originalAccessToken = accessToken;
localStorage.removeItem('session_currentUser'); setAuth(null);
//localStorage.removeItem('session_currentUser');
//localStorage.clear(); //localStorage.clear();
//sessionStorage.clear(); //sessionStorage.clear();
await fetch('/api/authentication/logout', { method: 'POST', credentials: 'include' }); if (originalAccessToken) await fetch('/api/authentication/logout', { method: 'POST', credentials: 'include' });
}; };
resetAppSettings(); resetAppSettings();
}, []); // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); //No dependencies, only run once on page load.
const finishUserLogin = async (user: User) => {
if (!user) console.log("user is undefined");
const finishUserLogin = async (_: User) => {
setIsLoading(false); setIsLoading(false);
setSpinners({ Login: false, LoginWithPasskey: false }); setSpinners({ Login: false, LoginWithPasskey: false });

View File

@ -6,6 +6,8 @@ import BouncedEmail from '@/types/bouncedEmail';
import UnsubscribeUrl from '@/types/unsubscribeUrl'; import UnsubscribeUrl from '@/types/unsubscribeUrl';
import Template from '@/types/template'; import Template from '@/types/template';
import EmailDomain from '@/types/emailDomain'; import EmailDomain from '@/types/emailDomain';
import { useCustomFetchNoNavigate } from "@/utils/customFetch";
import { useAuth } from '@/components/auth/AuthContext';
export type SetupData = { export type SetupData = {
targets: Target[]; targets: Target[];
@ -51,6 +53,9 @@ export type SetupData = {
const SetupDataContext = createContext<SetupData | undefined>(undefined); const SetupDataContext = createContext<SetupData | undefined>(undefined);
export const SetupDataProvider = ({ children }: { children: React.ReactNode }) => { export const SetupDataProvider = ({ children }: { children: React.ReactNode }) => {
const customFetch = useCustomFetchNoNavigate();
const { accessToken } = useAuth();
const [targets, setTargets] = useState<Target[]>([]); const [targets, setTargets] = useState<Target[]>([]);
const [targetsLoading, setTargetsLoading] = useState<boolean>(false); const [targetsLoading, setTargetsLoading] = useState<boolean>(false);
@ -80,6 +85,7 @@ export const SetupDataProvider = ({ children }: { children: React.ReactNode }) =
} }
const fetchSetupData = async () => { const fetchSetupData = async () => {
try { try {
if (!accessToken) return;
setDataLoading(true); setDataLoading(true);
const cachedData = sessionStorage.getItem("setupData"); const cachedData = sessionStorage.getItem("setupData");
@ -150,7 +156,7 @@ export const SetupDataProvider = ({ children }: { children: React.ReactNode }) =
} }
} }
if (loadTargets) { if (loadTargets) {
const targetsResponse = await fetch("/api/targets/GetAll?activeOnly=false"); const targetsResponse = await customFetch("/api/targets/GetAll?activeOnly=false");
targetsData = await targetsResponse.json(); targetsData = await targetsResponse.json();
if (targetsData) { if (targetsData) {
setTargets(targetsData); setTargets(targetsData);
@ -163,7 +169,7 @@ export const SetupDataProvider = ({ children }: { children: React.ReactNode }) =
} }
if (loadServers) { if (loadServers) {
const serversResponse = await fetch("/api/servers/GetAll?activeOnly=false&returnPassword=false"); const serversResponse = await customFetch("/api/servers/GetAll?activeOnly=false&returnPassword=false");
serversData = await serversResponse.json(); serversData = await serversResponse.json();
if (serversData) { if (serversData) {
setServers(serversData); setServers(serversData);
@ -176,7 +182,7 @@ export const SetupDataProvider = ({ children }: { children: React.ReactNode }) =
} }
if (loadTestEmailLists) { if (loadTestEmailLists) {
const testEmailListsResponse = await fetch("/api/testEmailLists/GetAll?activeOnly=false"); const testEmailListsResponse = await customFetch("/api/testEmailLists/GetAll?activeOnly=false");
testEmailListsData = await testEmailListsResponse.json(); testEmailListsData = await testEmailListsResponse.json();
if (testEmailListsData) { if (testEmailListsData) {
setTestEmailLists(testEmailListsData); setTestEmailLists(testEmailListsData);
@ -189,7 +195,7 @@ export const SetupDataProvider = ({ children }: { children: React.ReactNode }) =
} }
if (loadBouncedEmails) { if (loadBouncedEmails) {
const bouncedEmailsResponse = await fetch("/api/bouncedEmails/GetAll?activeOnly=false"); const bouncedEmailsResponse = await customFetch("/api/bouncedEmails/GetAll?activeOnly=false");
bouncedEmailsData = await bouncedEmailsResponse.json(); bouncedEmailsData = await bouncedEmailsResponse.json();
if (bouncedEmailsData) { if (bouncedEmailsData) {
setBouncedEmails(bouncedEmailsData); setBouncedEmails(bouncedEmailsData);
@ -202,7 +208,7 @@ export const SetupDataProvider = ({ children }: { children: React.ReactNode }) =
} }
if (loadUnsubscribeUrls) { if (loadUnsubscribeUrls) {
const unsubscribeUrlsResponse = await fetch("/api/unsubscribeUrls/GetAll?activeOnly=false"); const unsubscribeUrlsResponse = await customFetch("/api/unsubscribeUrls/GetAll?activeOnly=false");
unsubscribeUrlsData = await unsubscribeUrlsResponse.json(); unsubscribeUrlsData = await unsubscribeUrlsResponse.json();
if (unsubscribeUrlsData) { if (unsubscribeUrlsData) {
setUnsubscribeUrls(unsubscribeUrlsData); setUnsubscribeUrls(unsubscribeUrlsData);
@ -215,7 +221,7 @@ export const SetupDataProvider = ({ children }: { children: React.ReactNode }) =
} }
if (loadTemplates) { if (loadTemplates) {
const templatesResponse = await fetch("/api/templates/GetAll?activeOnly=false"); const templatesResponse = await customFetch("/api/templates/GetAll?activeOnly=false");
templatesData = await templatesResponse.json(); templatesData = await templatesResponse.json();
if (templatesData) { if (templatesData) {
setTemplates(templatesData); setTemplates(templatesData);
@ -227,7 +233,7 @@ export const SetupDataProvider = ({ children }: { children: React.ReactNode }) =
} }
if (loadEmailDomains) { if (loadEmailDomains) {
const emailDomainsResponse = await fetch("/api/emailDomains/GetAll?activeOnly=false&returnPassword=false"); const emailDomainsResponse = await customFetch("/api/emailDomains/GetAll?activeOnly=false&returnPassword=false");
emailDomainsData = await emailDomainsResponse.json(); emailDomainsData = await emailDomainsResponse.json();
if (emailDomainsData) { if (emailDomainsData) {
setEmailDomains(emailDomainsData); setEmailDomains(emailDomainsData);

View File

@ -15,6 +15,7 @@ export interface User {
export interface AuthResponse { export interface AuthResponse {
accessToken: string; accessToken: string;
refreshToken: string;
user: User; user: User;
} }

View File

@ -9,22 +9,25 @@ const customFetch = async (
url: string, url: string,
options: FetchOptions = {}, options: FetchOptions = {},
authContext: ReturnType<typeof useAuth>, authContext: ReturnType<typeof useAuth>,
navigate: ReturnType<typeof useNavigate> navigate: ReturnType<typeof useNavigate> | null
): Promise<Response> => { ): Promise<Response> => {
const { accessToken, refreshToken } = authContext; const { accessToken, refreshAuthToken, syncAccessToken } = authContext;
syncAccessToken(); // Ensure accessToken is up-to-date
const headers = { const headers = {
...options.headers, ...options.headers,
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {})
}; };
const response = await fetch(url, { const response = await fetch(url, {
...options, ...options,
headers, headers,
credentials: options.credentials ? options.credentials : 'omit',
}); });
if (response.status === 401) { if (response.status === 401) {
const refreshSuccess = await refreshToken();
const refreshSuccess = await refreshAuthToken();
if (refreshSuccess) { if (refreshSuccess) {
const newAccessToken = authContext.accessToken; const newAccessToken = authContext.accessToken;
const retryResponse = await fetch(url, { const retryResponse = await fetch(url, {
@ -36,7 +39,7 @@ const customFetch = async (
}); });
return retryResponse; return retryResponse;
} else { } else {
navigate('/login'); if (navigate) navigate('/login');
throw new Error('Authentication failed'); throw new Error('Authentication failed');
} }
} }
@ -52,4 +55,10 @@ export const useCustomFetch = () => {
customFetch(url, options, authContext, navigate); customFetch(url, options, authContext, navigate);
}; };
export const useCustomFetchNoNavigate = () => {
const authContext = useAuth();
return (url: string, options: FetchOptions = {}) =>
customFetch(url, options, authContext, null);
};
export default customFetch; export default customFetch;

View File

@ -20,6 +20,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
README.md = README.md README.md = README.md
EndProjectSection EndProjectSection
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Surge365.Core", "..\..\Core\Surge365.Core\Surge365.Core.csproj", "{6CB40316-D334-B074-F904-B0ABC46FADC9}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -52,6 +54,10 @@ Global
{A90972B7-7D32-4C1A-AB68-1043819F2A56}.Debug|Any CPU.Build.0 = Debug|Any CPU {A90972B7-7D32-4C1A-AB68-1043819F2A56}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A90972B7-7D32-4C1A-AB68-1043819F2A56}.Release|Any CPU.ActiveCfg = Release|Any CPU {A90972B7-7D32-4C1A-AB68-1043819F2A56}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A90972B7-7D32-4C1A-AB68-1043819F2A56}.Release|Any CPU.Build.0 = Release|Any CPU {A90972B7-7D32-4C1A-AB68-1043819F2A56}.Release|Any CPU.Build.0 = Release|Any CPU
{6CB40316-D334-B074-F904-B0ABC46FADC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6CB40316-D334-B074-F904-B0ABC46FADC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6CB40316-D334-B074-F904-B0ABC46FADC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6CB40316-D334-B074-F904-B0ABC46FADC9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE