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.Mvc;
using Surge365.Core.Controllers;
using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities;
using System.Net.Mail;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,21 +1,24 @@
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.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 Surge365.Core.Interfaces;
using Surge365.MassEmailReact.Infrastructure.EntityMaps;
var builder = WebApplication.CreateBuilder(args);
builder.AddCustomConfigurationSources();
WebApplication? app = null;
try
{
@ -50,21 +53,41 @@ try
IssuerSigningKey = new SymmetricSecurityKey(jwtKey),
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 =>
{
client.BaseAddress = new Uri("https://api.sendgrid.com/"); // Optional, for clarity
@ -74,10 +97,10 @@ try
});
// 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
builder.Services.AddOpenApi();
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<ITargetService, TargetService>();
builder.Services.AddScoped<ITargetRepository, TargetRepository>();
builder.Services.AddScoped<IServerService, ServerService>();
@ -95,10 +118,12 @@ try
builder.Services.AddScoped<IMailingService, MailingService>();
builder.Services.AddScoped<IMailingRepository, MailingRepository>();
EntityMapperConfiguration.ConfigureCustomMaps();
app = builder.Build();
app.UseCustomExceptionHandler();
app.UseDefaultFiles();
app.MapStaticAssets();
@ -109,17 +134,17 @@ try
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
DapperConfiguration.ConfigureMappings();
}
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();
return;
}

View File

@ -18,6 +18,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Core\Surge365.Core\Surge365.Core.csproj" />
<ProjectReference Include="..\Surge365.MassEmailReact.Application\Surge365.MassEmailReact.Application.csproj" />
<ProjectReference Include="..\Surge365.MassEmailReact.Infrastructure\Surge365.MassEmailReact.Infrastructure.csproj" />
</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>
</PropertyGroup>
<ItemGroup>
<Folder Include="Enums\Extensions\" />
</ItemGroup>
</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.Text;
using System.Threading.Tasks;
using Dapper.FluentMap.Mapping;
using Surge365.Core.Mapping;
using Surge365.MassEmailReact.Domain.Entities;
namespace Surge365.MassEmailReact.Infrastructure.DapperMaps
namespace Surge365.MassEmailReact.Infrastructure.EntityMaps
{
public class BouncedEmailMap : EntityMap<BouncedEmail>
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
using Dapper.FluentMap.Mapping;
using Surge365.Core.Mapping;
using Surge365.MassEmailReact.Domain.Entities;
namespace Surge365.MassEmailReact.Infrastructure.DapperMaps
namespace Surge365.MassEmailReact.Infrastructure.EntityMaps
{
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 Dapper.FluentMap;
using Microsoft.Data.SqlClient;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using Surge365.Core.Interfaces;
using Surge365.Core.Mapping;
using Surge365.Core.Services;
using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Infrastructure.Repositories
@ -14,22 +16,19 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
public class BouncedEmailRepository : IBouncedEmailRepository
{
private IConfiguration _config;
private const string _connectionStringName = "MassEmail.ConnectionString";
private string? ConnectionString
{
get
{
return _config.GetConnectionString(_connectionStringName);
}
}
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 BouncedEmailRepository(IConfiguration config)
public BouncedEmailRepository(IConfiguration config, DataAccessFactory dataAccessFactory, IQueryMapper queryMapper)
{
_config = config;
_dataAccessFactory = dataAccessFactory ?? throw new ArgumentNullException(nameof(dataAccessFactory), "DataAccessFactory cannot be null.");
_queryMapper = queryMapper;
#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
}
@ -37,21 +36,28 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
public async Task<BouncedEmail?> GetByEmailAsync(string emailAddress)
{
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()
{
ArgumentNullException.ThrowIfNull(_config);
ArgumentNullException.ThrowIfNull(_connectionStringName);
var parameters = new List<SqlParameter>();
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)
@ -59,22 +65,30 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
ArgumentNullException.ThrowIfNull(bouncedEmail);
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();
parameters.Add("@email_address", bouncedEmail.EmailAddress, DbType.String);
parameters.Add("@spam", bouncedEmail.Spam, DbType.Boolean);
parameters.Add("@unsubscribe", bouncedEmail.Unsubscribe, DbType.Boolean);
parameters.Add("@entered_by_admin", bouncedEmail.EnteredByAdmin, DbType.Boolean);
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output);
parameters.Add("@bounced_email_key", dbType: DbType.Int32, direction: ParameterDirection.Output);
List<SqlParameter> parameters = new List<SqlParameter>
{
pmBouncedEmailKey,
new SqlParameter("@email_address", bouncedEmail.EmailAddress),
new SqlParameter("@spam", bouncedEmail.Spam),
new SqlParameter("@unsubscribe", bouncedEmail.Unsubscribe),
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 parameters.Get<int>("@bounced_email_key");
if (success) return (int?)pmBouncedEmailKey.Value;
return null;
}
@ -85,19 +99,24 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
ArgumentNullException.ThrowIfNullOrEmpty(originalEmailAddress);
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();
parameters.Add("@old_email_address", originalEmailAddress, DbType.String);
parameters.Add("@email_address", bouncedEmail.EmailAddress, DbType.String);
parameters.Add("@spam", bouncedEmail.Spam, DbType.Boolean);
parameters.Add("@unsubscribe", bouncedEmail.Unsubscribe, DbType.Boolean);
parameters.Add("@entered_by_admin", bouncedEmail.EnteredByAdmin, DbType.Boolean);
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output);
List<SqlParameter> parameters = new List<SqlParameter>
{
new SqlParameter("@old_email_address", originalEmailAddress),
new SqlParameter("@email_address", bouncedEmail.EmailAddress),
new SqlParameter("@spam", bouncedEmail.Spam),
new SqlParameter("@unsubscribe", bouncedEmail.Unsubscribe),
new SqlParameter("@entered_by_admin", bouncedEmail.EnteredByAdmin),
pmSuccess
};
await conn.ExecuteAsync("mem_save_bounced_email", parameters, commandType: CommandType.StoredProcedure);
bool success = parameters.Get<bool>("@success");
DataAccess dataAccess = GetDataAccess();
await dataAccess.CallActionProcedureAsync(parameters, "mem_save_bounced_email");
bool success = pmSuccess.Value != null && (bool)pmSuccess.Value;
return success;
}
@ -106,15 +125,20 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
{
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();
parameters.Add("@email_address", emailAddress, DbType.String);
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output);
List<SqlParameter> parameters = new List<SqlParameter>
{
new SqlParameter("@email_address", emailAddress),
pmSuccess
};
await conn.ExecuteAsync("mem_delete_bounced_email", parameters, commandType: CommandType.StoredProcedure);
bool success = parameters.Get<bool>("@success");
DataAccess dataAccess = GetDataAccess();
await dataAccess.CallActionProcedureAsync(parameters, "mem_delete_bounced_email");
bool success = pmSuccess.Value != null && (bool)pmSuccess.Value;
return success;
}

View File

@ -1,14 +1,14 @@
using Dapper;
using Dapper.FluentMap;
using Microsoft.Data.SqlClient;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using Surge365.Core.Interfaces;
using Surge365.Core.Mapping;
using Surge365.Core.Services;
using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Runtime.Intrinsics.Arm;
using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Infrastructure.Repositories
@ -16,22 +16,19 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
public class EmailDomainRepository : IEmailDomainRepository
{
private readonly IConfiguration _config;
private const string _connectionStringName = "MassEmail.ConnectionString";
private string? ConnectionString
{
get
{
return _config.GetConnectionString(_connectionStringName);
}
}
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 EmailDomainRepository(IConfiguration config)
public EmailDomainRepository(IConfiguration config, DataAccessFactory dataAccessFactory, IQueryMapper queryMapper)
{
_config = config;
_dataAccessFactory = dataAccessFactory ?? throw new ArgumentNullException(nameof(dataAccessFactory), "DataAccessFactory cannot be null.");
_queryMapper = queryMapper;
#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
}
@ -39,10 +36,18 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
public async Task<EmailDomain?> GetByIdAsync(int id, bool returnPassword = false)
{
ArgumentNullException.ThrowIfNull(_config);
ArgumentNullException.ThrowIfNull(_connectionStringName);
using SqlConnection conn = new SqlConnection(ConnectionString);
EmailDomain? domain = (await conn.QueryAsync<EmailDomain>("mem_get_domain_by_id", new { domain_key = id }, commandType: CommandType.StoredProcedure)).FirstOrDefault();
var parameters = new List<SqlParameter>
{
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)
{
domain.Password = "";
@ -52,11 +57,17 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
public async Task<List<EmailDomain>> GetAllAsync(bool activeOnly = true, bool returnPassword = false)
{
ArgumentNullException.ThrowIfNull(_config);
ArgumentNullException.ThrowIfNull(_connectionStringName);
var parameters = new List<SqlParameter>
{
new SqlParameter("@active_only", activeOnly)
};
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();
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();
if(!returnPassword)
{
foreach (var domain in domains)
@ -73,21 +84,33 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
if (emailDomain.Id != null && emailDomain.Id > 0)
throw new Exception("ID must be null");
using SqlConnection conn = new SqlConnection(ConnectionString);
var parameters = new DynamicParameters();
parameters.Add("@domain_key", dbType: 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);
SqlParameter pmDomainKey = new SqlParameter("@domain_key", SqlDbType.Int)
{
Direction = ParameterDirection.Output
};
SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
{
Direction = ParameterDirection.Output
};
await conn.ExecuteAsync("mem_save_domain", parameters, commandType: CommandType.StoredProcedure);
bool success = parameters.Get<bool>("@success");
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;
if (success)
return parameters.Get<int>("@domain_key");
return (int?)pmDomainKey.Value;
return null;
}
@ -96,19 +119,26 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
ArgumentNullException.ThrowIfNull(emailDomain);
ArgumentNullException.ThrowIfNull(emailDomain.Id);
using SqlConnection conn = new SqlConnection(ConnectionString);
var parameters = new DynamicParameters();
parameters.Add("@domain_key", emailDomain.Id, DbType.Int32);
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);
SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
{
Direction = ParameterDirection.Output
};
await conn.ExecuteAsync("mem_save_domain", parameters, commandType: CommandType.StoredProcedure);
bool success = parameters.Get<bool>("@success");
List<SqlParameter> parameters = new List<SqlParameter>
{
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;
}
}

View File

@ -1,7 +1,8 @@
using Dapper;
using Dapper.FluentMap;
using Microsoft.Data.SqlClient;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using Surge365.Core.Interfaces;
using Surge365.Core.Mapping;
using Surge365.Core.Services;
using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities;
using System;
@ -16,38 +17,43 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
public class MailingRepository : IMailingRepository
{
private readonly IConfiguration _config;
private const string _connectionStringName = "MassEmail.ConnectionString";
private string? ConnectionString => _config.GetConnectionString(_connectionStringName);
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 MailingRepository(IConfiguration config)
public MailingRepository(IConfiguration config, DataAccessFactory dataAccessFactory, IQueryMapper queryMapper)
{
_config = config;
_dataAccessFactory = dataAccessFactory ?? throw new ArgumentNullException(nameof(dataAccessFactory), "DataAccessFactory cannot be null.");
_queryMapper = queryMapper;
#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
}
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(
"mem_get_blast_by_id",
new { blast_key = id },
commandType: CommandType.StoredProcedure);
var mailing = await multi.ReadSingleOrDefaultAsync<Mailing>();
// Handle multiple result sets
var mailings = await _queryMapper.MapAsync<Mailing>(dataSet);
var mailing = mailings.FirstOrDefault();
if (mailing == null) return null;
var template = await multi.ReadSingleOrDefaultAsync<MailingTemplate>();
if (mailing != null)
var templates = await _queryMapper.MapAsync<MailingTemplate>(dataSet);
var template = templates.FirstOrDefault();
if (mailing != null && template != null)
mailing.Template = template;
return mailing;
@ -55,24 +61,23 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
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(
"mem_get_blast_all",
new { active_only = activeOnly },
commandType: CommandType.StoredProcedure);
var templates = await _queryMapper.MapAsync<MailingTemplate>(dataSet);
var templateList = templates.ToList();
var mailings = (await multi.ReadAsync<Mailing>()).ToList();
if (!mailings.Any()) return mailings;
var templates = (await multi.ReadAsync<MailingTemplate>()).ToList();
var mailingDictionary = mailings.ToDictionary(t => t.Id!.Value);
foreach (var template in templates)
var mailingDictionary = mailingList.ToDictionary(t => t.Id!.Value);
foreach (var template in templateList)
{
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)
{
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);
await conn.OpenAsync();
DataAccess dataAccess = GetDataAccess();
var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_blast_by_status");
using var multi = await conn.QueryMultipleAsync(
"mem_get_blast_by_status",
new { blast_status_codes = codes, start_date = startDate, end_date = endDate },
commandType: CommandType.StoredProcedure);
var mailings = await _queryMapper.MapAsync<Mailing>(dataSet);
var mailingList = mailings.ToList();
if (!mailingList.Any()) return mailingList;
var mailings = (await multi.ReadAsync<Mailing>()).ToList();
if (!mailings.Any()) return mailings;
var templates = await _queryMapper.MapAsync<MailingTemplate>(dataSet);
var templateList = templates.ToList();
var templates = (await multi.ReadAsync<MailingTemplate>()).ToList();
var mailingDictionary = mailings.ToDictionary(t => t.Id!.Value);
foreach (var template in templates)
var mailingDictionary = mailingList.ToDictionary(t => t.Id!.Value);
foreach (var template in templateList)
{
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)
{
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);
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();
DataAccess dataAccess = GetDataAccess();
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)
{
ArgumentNullException.ThrowIfNull(ConnectionString);
var parameters = new List<SqlParameter>
{
new SqlParameter("@blast_key", id)
};
using SqlConnection conn = new SqlConnection(ConnectionString);
return (await conn.QueryAsync<MailingStatistic>("mem_get_blast_statistic_by_blast", new { blast_key = id }, commandType: CommandType.StoredProcedure)).FirstOrDefault();
DataAccess dataAccess = GetDataAccess();
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)
{
ArgumentNullException.ThrowIfNull(ConnectionString);
var parameters = new List<SqlParameter>
{
new SqlParameter("@blast_key", id)
};
using SqlConnection conn = new SqlConnection(ConnectionString);
return (await conn.QueryAsync<MailingEmail>("mem_get_blast_email_by_blast_id", new { blast_key = id }, commandType: CommandType.StoredProcedure)).ToList();
DataAccess dataAccess = GetDataAccess();
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)
{
ArgumentNullException.ThrowIfNull(ConnectionString);
var parameters = new List<SqlParameter>
{
new SqlParameter("@blast_key", id)
};
using SqlConnection conn = new SqlConnection(ConnectionString);
return (await conn.QueryAsync<MailingTemplate>("mem_get_blast_template_by_blast_id", new { blast_key = id }, commandType: CommandType.StoredProcedure)).FirstOrDefault();
DataAccess dataAccess = GetDataAccess();
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)
{
ArgumentNullException.ThrowIfNull(ConnectionString);
var parameters = new List<SqlParameter>
{
new SqlParameter("@blast_key", id)
};
using SqlConnection conn = new SqlConnection(ConnectionString);
return (await conn.QueryAsync<MailingTarget>("mem_get_blast_target_by_blast_id", new { blast_key = id }, commandType: CommandType.StoredProcedure)).FirstOrDefault();
DataAccess dataAccess = GetDataAccess();
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)
{
ArgumentNullException.ThrowIfNull(ConnectionString);
using var conn = new SqlConnection(ConnectionString);
SqlParameter pmAvailable = new SqlParameter("@available", SqlDbType.Bit)
{
Direction = ParameterDirection.Output
};
var parameters = new DynamicParameters();
parameters.Add("@blast_key", id, DbType.Int32);
parameters.Add("@blast_name", name, DbType.String);
parameters.Add("@available", dbType: DbType.Boolean, direction: ParameterDirection.Output);
var parameters = new List<SqlParameter>
{
new SqlParameter("@blast_key", id ?? (object)DBNull.Value),
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)
{
ArgumentNullException.ThrowIfNull(ConnectionString);
using var conn = new SqlConnection(ConnectionString);
SqlParameter pmNextBlastName = new SqlParameter("@next_blast_name", SqlDbType.NVarChar, -1)
{
Direction = ParameterDirection.Output
};
var parameters = new DynamicParameters();
parameters.Add("@blast_key", id, DbType.Int32);
parameters.Add("@blast_name", name, DbType.String);
parameters.Add("@next_blast_name", dbType: DbType.String, size:-1, direction: ParameterDirection.Output);
var parameters = new List<SqlParameter>
{
new SqlParameter("@blast_key", id ?? (object)DBNull.Value),
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)
{
ArgumentNullException.ThrowIfNull(ConnectionString);
ArgumentNullException.ThrowIfNull(mailing);
if (mailing.Id != null && mailing.Id > 0)
throw new Exception("ID must be null");
using SqlConnection conn = new SqlConnection(ConnectionString);
var parameters = new DynamicParameters();
parameters.Add("@blast_key", dbType: 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);
SqlParameter pmBlastKey = new SqlParameter("@blast_key", SqlDbType.Int)
{
Direction = ParameterDirection.Output
};
SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
{
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)
return parameters.Get<int>("@blast_key");
return (int?)pmBlastKey.Value;
return null;
}
public async Task<bool> UpdateAsync(Mailing mailing)
{
ArgumentNullException.ThrowIfNull(ConnectionString);
ArgumentNullException.ThrowIfNull(mailing);
ArgumentNullException.ThrowIfNull(mailing.Id);
using SqlConnection conn = new SqlConnection(ConnectionString);
var parameters = new DynamicParameters();
parameters.Add("@blast_key", mailing.Id, DbType.Int32);
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);
SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
{
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)
{
SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
{
Direction = ParameterDirection.Output
};
using SqlConnection conn = new SqlConnection(ConnectionString);
var parameters = new DynamicParameters();
parameters.Add("@blast_key", id, DbType.Int32);
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output);
var parameters = new List<SqlParameter>
{
new SqlParameter("@blast_key", id),
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 Dapper.FluentMap;
using Microsoft.Data.SqlClient;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using Surge365.Core.Interfaces;
using Surge365.Core.Mapping;
using Surge365.Core.Services;
using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities;
using Surge365.MassEmailReact.Domain.Enums;
using Surge365.MassEmailReact.Domain.Enums.Extensions;
using System;
using System.Collections.Generic;
using System.Data;
@ -18,32 +17,37 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
public class ServerRepository : IServerRepository
{
private IConfiguration _config;
private const string _connectionStringName = "MassEmail.ConnectionString";
private string? ConnectionString
{
get
{
return _config.GetConnectionString(_connectionStringName);
}
}
public ServerRepository(IConfiguration config)
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 ServerRepository(IConfiguration config, DataAccessFactory dataAccessFactory, IQueryMapper queryMapper)
{
_config = config;
_dataAccessFactory = dataAccessFactory ?? throw new ArgumentNullException(nameof(dataAccessFactory), "DataAccessFactory cannot be null.");
_queryMapper = queryMapper;
#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
}
public async Task<Server?> GetByIdAsync(int serverKey, bool returnPassword = false)
{
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)
{
server.Password = "";
@ -52,18 +56,15 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
}
public async Task<List<Server>> GetAllAsync(bool activeOnly = true, bool returnPassword = false)
{
ArgumentNullException.ThrowIfNull(_config);
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)
var parameters = new List<SqlParameter>
{
foreach (Server server in servers)
server.Password = "";
}
return servers;
new SqlParameter("@active_only", activeOnly)
};
DataAccess dataAccess = GetDataAccess();
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)
@ -72,26 +73,30 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
if (server.Id != null && server.Id > 0)
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();
parameters.Add("@server_key", dbType: DbType.Int32, direction: ParameterDirection.Output);
parameters.Add("@name", server.Name, DbType.String);
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);
DataAccess dataAccess = GetDataAccess();
await dataAccess.CallActionProcedureAsync(parameters, "mem_save_server");
bool success = pmSuccess.Value != null && (bool)pmSuccess.Value;
// Output parameter
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");
if (success) return (int?)pmServerKey.Value;
return null;
}
@ -99,31 +104,27 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
{
ArgumentNullException.ThrowIfNull(server);
ArgumentNullException.ThrowIfNull(server.Id);
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName));
var parameters = new DynamicParameters();
parameters.Add("@server_key", server.Id, DbType.Int32);
parameters.Add("@name", server.Name, DbType.String);
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);
SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
{
Direction = ParameterDirection.Output
};
// Output parameter
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");
List<SqlParameter> parameters = new List<SqlParameter>
{
new SqlParameter("@server_key", server.Id),
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
};
DataAccess dataAccess = GetDataAccess();
await dataAccess.CallActionProcedureAsync(parameters, "mem_save_server");
bool success = pmSuccess.Value != null && (bool)pmSuccess.Value;
return success;
}
//public void Add(Server server)
//{
// Servers.Add(server);
//}
}
}

View File

@ -1,12 +1,11 @@
using Dapper;
using Dapper.FluentMap;
using Microsoft.Data.SqlClient;
using Microsoft.Data.SqlClient;
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.Interfaces;
using Surge365.MassEmailReact.Domain.Entities;
using Surge365.MassEmailReact.Domain.Enums;
using Surge365.MassEmailReact.Domain.Enums.Extensions;
using System;
using System.Collections.Generic;
using System.Data;
@ -21,70 +20,69 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
public class TargetRepository : ITargetRepository
{
private IConfiguration _config;
private const string _connectionStringName = "MassEmail.ConnectionString";
private string? ConnectionString
{
get
{
return _config.GetConnectionString(_connectionStringName);
}
}
public TargetRepository(IConfiguration config)
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 TargetRepository(IConfiguration config, DataAccessFactory dataAccessFactory, IQueryMapper queryMapper)
{
_config = config;
_dataAccessFactory = dataAccessFactory ?? throw new ArgumentNullException(nameof(dataAccessFactory), "DataAccessFactory cannot be null.");
_queryMapper = queryMapper;
#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
}
public async Task<Target?> GetByIdAsync(int targetKey)
{
ArgumentNullException.ThrowIfNull(_config);
ArgumentNullException.ThrowIfNull(_connectionStringName);
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName));
await conn.OpenAsync();
var parameters = new List<SqlParameter>
{
new SqlParameter("@target_key", targetKey)
};
using var multi = await conn.QueryMultipleAsync(
"mem_get_target_by_id",
new { target_key = targetKey },
commandType: CommandType.StoredProcedure);
DataAccess dataAccess = GetDataAccess();
var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_target_by_id");
// 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;
// Read the second result set (TargetColumns)
var columns = await multi.ReadAsync<TargetColumn>();
var columns = await _queryMapper.MapAsync<TargetColumn>(dataSet);
target.Columns = columns.ToList();
return target;
}
public async Task<List<Target>> GetAllAsync(bool activeOnly = true)
{
ArgumentNullException.ThrowIfNull(_config);
ArgumentNullException.ThrowIfNull(_connectionStringName);
var parameters = new List<SqlParameter>
{
new SqlParameter("@active_only", activeOnly)
};
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName));
await conn.OpenAsync();
using var multi = await conn.QueryMultipleAsync(
"mem_get_target_all",
new { active_only = activeOnly },
commandType: CommandType.StoredProcedure);
DataAccess dataAccess = GetDataAccess();
var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_target_all");
// Read the first result set (Targets)
var targets = (await multi.ReadAsync<Target>()).ToList();
if (!targets.Any()) return targets;
var targets = await _queryMapper.MapAsync<Target>(dataSet);
var targetList = targets.ToList();
if (!targetList.Any()) return targetList;
// 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
var targetDictionary = targets.ToDictionary(t => t.Id!.Value);
foreach (var column in columns)
var targetDictionary = targetList.ToDictionary(t => t.Id!.Value);
foreach (var column in columnList)
{
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)
@ -101,61 +99,70 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
if (target.Id != null && target.Id > 0)
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();
parameters.Add("@target_key", dbType: DbType.Int32, direction: ParameterDirection.Output);
parameters.Add("@server_key", target.ServerId, DbType.Int32);
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);
List<SqlParameter> parameters = new List<SqlParameter>
{
pmTargetKey,
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
};
// Output parameter
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output);
await conn.ExecuteAsync("mem_save_target", parameters, commandType: CommandType.StoredProcedure);
// Retrieve the output parameter value
bool success = parameters.Get<bool>("@success");
DataAccess dataAccess = GetDataAccess();
await dataAccess.CallActionProcedureAsync(parameters, "mem_save_target");
bool success = pmSuccess.Value != null && (bool)pmSuccess.Value;
if (success)
return parameters.Get<int>("@target_key");
return (int?)pmTargetKey.Value;
return null;
}
public async Task<bool> UpdateAsync(Target target)
{
ArgumentNullException.ThrowIfNull(target);
ArgumentNullException.ThrowIfNull(target.Id);
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName));
var parameters = new DynamicParameters();
parameters.Add("@target_key", target.Id, DbType.Int32);
parameters.Add("@server_key", target.ServerId, DbType.Int32);
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);
SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
{
Direction = ParameterDirection.Output
};
// Output parameter
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output);
List<SqlParameter> parameters = new List<SqlParameter>
{
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);
// Retrieve the output parameter value
bool success = parameters.Get<bool>("@success");
DataAccess dataAccess = GetDataAccess();
await dataAccess.CallActionProcedureAsync(parameters, "mem_save_target");
bool success = pmSuccess.Value != null && (bool)pmSuccess.Value;
return success;
}
public async Task<TargetSample?> TestTargetAsync(
string serverName,
short port,
@ -179,6 +186,7 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
// Clean up server name
serverName = serverName.Replace("110494-db", "www.surge365.com");
// Get configuration
string sql = _config["TestTargetSql"] ?? "";
string connectionStringTemplate = _config["ConnectionStringTemplate"] ?? "";
@ -201,20 +209,42 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
// Assuming the SQL returns three result sets: columns, sample data, and row count
using var multi = await connection.QueryMultipleAsync(sql, commandTimeout: 300);
// Execute the SQL and get multiple result sets
using var command = new SqlCommand(sql, connection);
command.CommandTimeout = 300;
using var adapter = new SqlDataAdapter(command);
var dataSet = new DataSet();
adapter.Fill(dataSet);
// Read column definitions
var columnEntities = await multi.ReadAsync<dynamic>();
if (!columnEntities.Any())
if (dataSet.Tables.Count < 3)
return null;
// Read sample data
var sampleRows = (await multi.ReadAsync<dynamic>()).ToList();
// Read column definitions (first table)
var columnTable = dataSet.Tables[0];
if (columnTable.Rows.Count == 0)
return null;
// Read row count
var rowCountResult = await multi.ReadSingleAsync<dynamic>();
int rowCount = Convert.ToInt32(rowCountResult.row_count);
// Read sample data (second table)
var sampleTable = dataSet.Tables[1];
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
var targetSample = new TargetSample();
@ -224,10 +254,10 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
bool bounceFound = false;
bool unsubFound = false;
foreach (var col in columnEntities)
foreach (DataRow col in columnTable.Rows)
{
string name = col.name;
string dataType = col.data_type;
string name = col["name"].ToString();
string dataType = col["data_type"].ToString();
string typeCode = TargetColumnType.General;
if (!emailFound && name.ToUpper().Contains("EMAIL"))
@ -259,13 +289,8 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
};
}
// Convert sample rows to dictionary format
foreach (var row in sampleRows)
{
var dict = ((IDictionary<string, object>)row)
.ToDictionary(k => k.Key, v => v.Value?.ToString() ?? "");
targetSample.Rows.Add(dict);
}
// Add sample rows
targetSample.Rows.AddRange(sampleRows);
return targetSample;
}

View File

@ -1,7 +1,8 @@
using Dapper;
using Dapper.FluentMap;
using Microsoft.Data.SqlClient;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using Surge365.Core.Interfaces;
using Surge365.Core.Mapping;
using Surge365.Core.Services;
using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities;
using System;
@ -15,22 +16,19 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
public class TemplateRepository : ITemplateRepository
{
private IConfiguration _config;
private const string _connectionStringName = "MassEmail.ConnectionString";
private string? ConnectionString
{
get
{
return _config.GetConnectionString(_connectionStringName);
}
}
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 TemplateRepository(IConfiguration config)
public TemplateRepository(IConfiguration config, DataAccessFactory dataAccessFactory, IQueryMapper queryMapper)
{
_config = config;
_dataAccessFactory = dataAccessFactory ?? throw new ArgumentNullException(nameof(dataAccessFactory), "DataAccessFactory cannot be null.");
_queryMapper = queryMapper;
#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
}
@ -38,21 +36,28 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
public async Task<Template?> GetByIdAsync(int id)
{
ArgumentNullException.ThrowIfNull(_config);
ArgumentNullException.ThrowIfNull(_connectionStringName);
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName));
return (await conn.QueryAsync<Template>("mem_get_template_by_id", new { template_key = id },
commandType: CommandType.StoredProcedure)).FirstOrDefault();
var parameters = new List<SqlParameter>
{
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)
{
ArgumentNullException.ThrowIfNull(_config);
ArgumentNullException.ThrowIfNull(_connectionStringName);
var parameters = new List<SqlParameter>();
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName));
return (await conn.QueryAsync<Template>("mem_get_template_all", new { },
commandType: CommandType.StoredProcedure)).ToList();
DataAccess dataAccess = GetDataAccess();
var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_template_all");
var results = await _queryMapper.MapAsync<Template>(dataSet);
return results.ToList();
}
public async Task<int?> CreateAsync(Template template)
@ -61,27 +66,38 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
if (template.Id != null && template.Id > 0)
throw new Exception("ID must be null");
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName));
var parameters = new DynamicParameters();
parameters.Add("@template_key", dbType: 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);
SqlParameter pmTemplateKey = new SqlParameter("@template_key", SqlDbType.Int)
{
Direction = ParameterDirection.Output
};
SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
{
Direction = ParameterDirection.Output
};
await conn.ExecuteAsync("mem_save_template", parameters, commandType: CommandType.StoredProcedure);
bool success = parameters.Get<bool>("@success");
return success ? parameters.Get<int>("@template_key") : null;
List<SqlParameter> parameters = new List<SqlParameter>
{
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)
@ -89,26 +105,34 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
ArgumentNullException.ThrowIfNull(template);
ArgumentNullException.ThrowIfNull(template.Id);
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName));
var parameters = new DynamicParameters();
parameters.Add("@template_key", template.Id, DbType.Int32);
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);
SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
{
Direction = ParameterDirection.Output
};
await conn.ExecuteAsync("mem_save_template", parameters, commandType: CommandType.StoredProcedure);
return parameters.Get<bool>("@success");
List<SqlParameter> parameters = new List<SqlParameter>
{
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 Surge365.Core.Interfaces;
using Surge365.Core.Mapping;
using Surge365.Core.Services;
using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities;
using System;
@ -14,36 +16,48 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
public class TestEmailListRepository : ITestEmailListRepository
{
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;
_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)
{
ArgumentNullException.ThrowIfNull(_config);
ArgumentNullException.ThrowIfNull(_connectionStringName);
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName));
return (await conn.QueryAsync<TestEmailList>(
"mem_get_test_email_list_by_id",
new { test_email_list_key = id },
commandType: CommandType.StoredProcedure
)).FirstOrDefault();
var parameters = new List<SqlParameter>
{
new SqlParameter("@test_email_list_key", id)
};
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()
{
ArgumentNullException.ThrowIfNull(_config);
ArgumentNullException.ThrowIfNull(_connectionStringName);
var parameters = new List<SqlParameter>();
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName));
return (await conn.QueryAsync<TestEmailList>(
"mem_get_test_email_list_all",
commandType: CommandType.StoredProcedure
)).ToList();
DataAccess dataAccess = GetDataAccess();
var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_test_email_list_all");
var results = await _queryMapper.MapAsync<TestEmailList>(dataSet);
return results.ToList();
}
public async Task<int?> CreateAsync(TestEmailList testEmailList)
@ -52,17 +66,28 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
if (testEmailList.Id != null && testEmailList.Id > 0)
throw new Exception("ID must be null");
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName));
var parameters = new DynamicParameters();
parameters.Add("@test_email_list_key", dbType: 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);
SqlParameter pmTestEmailListKey = new SqlParameter("@test_email_list_key", SqlDbType.Int)
{
Direction = ParameterDirection.Output
};
SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
{
Direction = ParameterDirection.Output
};
await conn.ExecuteAsync("mem_save_test_email_list", parameters, commandType: CommandType.StoredProcedure);
bool success = parameters.Get<bool>("@success");
List<SqlParameter> parameters = new List<SqlParameter>
{
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)
return parameters.Get<int>("@test_email_list_key");
return (int?)pmTestEmailListKey.Value;
return null;
}
@ -71,15 +96,23 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
ArgumentNullException.ThrowIfNull(testEmailList);
ArgumentNullException.ThrowIfNull(testEmailList.Id);
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName));
var parameters = new DynamicParameters();
parameters.Add("@test_email_list_key", testEmailList.Id, DbType.Int32);
parameters.Add("@name", testEmailList.Name, DbType.String);
parameters.Add("@list", testEmailList.GetListXml(), DbType.String);
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output);
SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit)
{
Direction = ParameterDirection.Output
};
await conn.ExecuteAsync("mem_save_test_email_list", parameters, commandType: CommandType.StoredProcedure);
return parameters.Get<bool>("@success");
List<SqlParameter> parameters = new List<SqlParameter>
{
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 Dapper.FluentMap;
using Microsoft.Data.SqlClient;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using Surge365.Core.Interfaces;
using Surge365.Core.Mapping;
using Surge365.Core.Services;
using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Infrastructure.Repositories
@ -14,81 +16,104 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
public class UnsubscribeUrlRepository : IUnsubscribeUrlRepository
{
private IConfiguration _config;
private const string _connectionStringName = "MassEmail.ConnectionString";
private string? ConnectionString
{
get
{
return _config.GetConnectionString(_connectionStringName);
}
}
public UnsubscribeUrlRepository(IConfiguration config)
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 UnsubscribeUrlRepository(IConfiguration config, DataAccessFactory dataAccessFactory, IQueryMapper queryMapper)
{
_config = config;
_dataAccessFactory = dataAccessFactory ?? throw new ArgumentNullException(nameof(dataAccessFactory), "DataAccessFactory cannot be null.");
_queryMapper = queryMapper;
#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
}
public async Task<UnsubscribeUrl?> GetByIdAsync(int unsubscribeUrlKey)
{
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)
{
ArgumentNullException.ThrowIfNull(_config);
ArgumentNullException.ThrowIfNull(_connectionStringName);
var parameters = new List<SqlParameter>();
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)
{
ArgumentNullException.ThrowIfNull(unsubscribeUrl);
if (unsubscribeUrl.Id != null && unsubscribeUrl.Id > 0)
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();
parameters.Add("@unsubscribe_url_key", dbType: DbType.Int32, direction: ParameterDirection.Output);
parameters.Add("@name", unsubscribeUrl.Name, DbType.String);
parameters.Add("@url", unsubscribeUrl.Url, DbType.String);
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output);
List<SqlParameter> parameters = new List<SqlParameter>
{
pmUnsubscribeUrlKey,
new SqlParameter("@name", unsubscribeUrl.Name),
new SqlParameter("@url", unsubscribeUrl.Url),
pmSuccess
};
await conn.ExecuteAsync("mem_save_unsubscribe_url", parameters, commandType: CommandType.StoredProcedure);
bool success = parameters.Get<bool>("@success");
DataAccess dataAccess = GetDataAccess();
await dataAccess.CallActionProcedureAsync(parameters, "mem_save_unsubscribe_url");
bool success = pmSuccess.Value != null && (bool)pmSuccess.Value;
if (success)
return parameters.Get<int>("@unsubscribe_url_key");
return (int?)pmUnsubscribeUrlKey.Value;
return null;
}
public async Task<bool> UpdateAsync(UnsubscribeUrl unsubscribeUrl)
{
ArgumentNullException.ThrowIfNull(unsubscribeUrl);
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();
parameters.Add("@unsubscribe_url_key", unsubscribeUrl.Id, DbType.Int32);
parameters.Add("@name", unsubscribeUrl.Name, DbType.String);
parameters.Add("@url", unsubscribeUrl.Url, DbType.String);
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output);
List<SqlParameter> parameters = new List<SqlParameter>
{
new SqlParameter("@unsubscribe_url_key", unsubscribeUrl.Id),
new SqlParameter("@name", unsubscribeUrl.Name),
new SqlParameter("@url", unsubscribeUrl.Url),
pmSuccess
};
await conn.ExecuteAsync("mem_save_unsubscribe_url", parameters, commandType: CommandType.StoredProcedure);
bool success = parameters.Get<bool>("@success");
DataAccess dataAccess = GetDataAccess();
await dataAccess.CallActionProcedureAsync(parameters, "mem_save_unsubscribe_url");
bool success = pmSuccess.Value != null && (bool)pmSuccess.Value;
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 SendGrid;
using SendGrid.Helpers.Mail;
using System.Net.Http;
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.Collections.Generic;
using System.Linq;

View File

@ -7,23 +7,14 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Core\Surge365.Core\Surge365.Core.csproj" />
<ProjectReference Include="..\Surge365.MassEmailReact.Application\Surge365.MassEmailReact.Application.csproj" />
<ProjectReference Include="..\Surge365.MassEmailReact.Domain\Surge365.MassEmailReact.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.4.0" />
<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="Microsoft.Extensions.Http" Version="9.0.6" />
<PackageReference Include="SendGrid" Version="9.29.3" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.8.0" />
</ItemGroup>
</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 navigate = useNavigate();
const location = useLocation();
const { accessToken, setAuth, isLoading } = useAuth();
const { accessToken, refreshToken, isLoading, refreshAuthToken } = useAuth();
useEffect(() => {
if (isLoading) return; // Wait for AuthProvider to finish
@ -14,30 +14,18 @@ const AuthCheck: React.FC = () => {
if (currentPath.toLowerCase() === "/login") return;
const tryRefreshToken = async () => {
try {
const response = await fetch('/api/authentication/refreshtoken', {
method: 'POST',
credentials: 'include',
});
if (response.ok) {
const data = await response.json();
setAuth(data.accessToken);
} else {
setAuth(null);
navigate('/login');
}
} catch {
setAuth(null);
const success = await refreshAuthToken();
if (!success) {
navigate('/login');
}
};
if (!accessToken || utils.isTokenExpired(accessToken)) {
if (refreshToken && (!accessToken || utils.isTokenExpired(accessToken))) {
tryRefreshToken();
}
}, [navigate, location.pathname, accessToken, setAuth, isLoading]);
}, [navigate, accessToken, refreshToken, location.pathname, isLoading, refreshAuthToken]);
return null;
};
export default AuthCheck;
export default AuthCheck;

View File

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

View File

@ -60,6 +60,12 @@ const DrawerHeader = styled('div')(({ theme }) => ({
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' })<{
open?: boolean;
}>(({ theme, open }) => ({
@ -103,7 +109,7 @@ const Layout = ({ children }: LayoutProps) => {
{ text: 'Completed Mailings', icon: <CheckCircleIcon />, path: '/completedMailings' },
{ 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 profileMenuOpen = Boolean(profileMenuAnchorEl);
const handleOpenProfileMenu = (event: React.MouseEvent<HTMLButtonElement>) => {
@ -119,21 +125,8 @@ const Layout = ({ children }: LayoutProps) => {
});
const handleRefreshUser = async () => {
handleCloseProfileMenu();
try {
const response = await customFetch('/api/authentication/refreshtoken', {
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
const success = await refreshAuthToken();
if (!success) {
navigate('/login');
}
}
@ -156,6 +149,8 @@ const Layout = ({ children }: LayoutProps) => {
setOpen(false);
}
}, [isMobile]);
const effectiveMode = (mode === 'system' ? getSystemTheme() : mode) || "light";
const handleThemeChange = (event: SelectChangeEvent) => {
setMode(event.target.value as 'light' | 'dark');
@ -231,7 +226,7 @@ const Layout = ({ children }: LayoutProps) => {
<Select
labelId="theme-select-label"
id="theme-select"
value={mode || 'light'}
value={effectiveMode || 'light'}
label="Theme"
onChange={handleThemeChange}
sx={{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,6 +20,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
README.md = README.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Surge365.Core", "..\..\Core\Surge365.Core\Surge365.Core.csproj", "{6CB40316-D334-B074-F904-B0ABC46FADC9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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}.Release|Any CPU.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE