Merge branch 'development'
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
using HrynCo.NotificationService.Services.Core;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace HrynCo.NotificationService.Services.EmailChannels.TestSmtp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a test email using the provided SMTP settings without persisting anything.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record TestSmtpCommand(
|
||||||
|
string Host,
|
||||||
|
int Port,
|
||||||
|
string Username,
|
||||||
|
string Password,
|
||||||
|
bool UseSsl,
|
||||||
|
string FromEmail,
|
||||||
|
string FromName,
|
||||||
|
string ToEmail
|
||||||
|
) : IRequest<ServiceResult<Core.Unit>>;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Mail;
|
||||||
|
using HrynCo.NotificationService.DAL.Abstract;
|
||||||
|
using HrynCo.NotificationService.Services.Core;
|
||||||
|
using HrynCo.NotificationService.Services.Logging;
|
||||||
|
using static HrynCo.NotificationService.Services.Core.ServiceResultHelper;
|
||||||
|
|
||||||
|
namespace HrynCo.NotificationService.Services.EmailChannels.TestSmtp;
|
||||||
|
|
||||||
|
internal sealed class TestSmtpHandler
|
||||||
|
: RequestHandler<TestSmtpCommand, ServiceResult<Unit>>
|
||||||
|
{
|
||||||
|
public TestSmtpHandler(
|
||||||
|
IContextualSerilogLogger<TestSmtpCommand> logger,
|
||||||
|
IUnitOfWork unitOfWork)
|
||||||
|
: base(logger, unitOfWork)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<ServiceResult<Unit>> DoOnHandle(
|
||||||
|
TestSmtpCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var client = new SmtpClient(request.Host, request.Port)
|
||||||
|
{
|
||||||
|
EnableSsl = request.UseSsl,
|
||||||
|
Credentials = string.IsNullOrWhiteSpace(request.Username)
|
||||||
|
? null
|
||||||
|
: new NetworkCredential(request.Username, request.Password)
|
||||||
|
};
|
||||||
|
|
||||||
|
using var mail = new MailMessage
|
||||||
|
{
|
||||||
|
From = new MailAddress(request.FromEmail, request.FromName),
|
||||||
|
Subject = "✅ Test email from Notification Service",
|
||||||
|
Body = "<p>This is a test email sent from the <b>Notification Service</b> admin panel to verify the channel settings.</p>",
|
||||||
|
IsBodyHtml = true
|
||||||
|
};
|
||||||
|
mail.To.Add(new MailAddress(request.ToEmail));
|
||||||
|
|
||||||
|
await client.SendMailAsync(mail, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Error(ex, "Ad-hoc SMTP test failed for host {Host}", request.Host);
|
||||||
|
return Failure<Unit>(ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Success(Unit.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ using HrynCo.NotificationService.Services.EmailChannels.Delete;
|
|||||||
using HrynCo.NotificationService.Services.EmailChannels.Get;
|
using HrynCo.NotificationService.Services.EmailChannels.Get;
|
||||||
using HrynCo.NotificationService.Services.EmailChannels.GetUsageSummary;
|
using HrynCo.NotificationService.Services.EmailChannels.GetUsageSummary;
|
||||||
using HrynCo.NotificationService.Services.EmailChannels.Send;
|
using HrynCo.NotificationService.Services.EmailChannels.Send;
|
||||||
|
using HrynCo.NotificationService.Services.EmailChannels.TestSmtp;
|
||||||
using HrynCo.NotificationService.Services.EmailChannels.Update;
|
using HrynCo.NotificationService.Services.EmailChannels.Update;
|
||||||
using HrynCo.NotificationService.Web.Controllers.Admin.ViewModels;
|
using HrynCo.NotificationService.Web.Controllers.Admin.ViewModels;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -133,6 +134,20 @@ public class AdminChannelsController(IMediator mediator) : Controller
|
|||||||
return RedirectToAction(nameof(Index));
|
return RedirectToAction(nameof(Index));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST /admin/channels/test-smtp
|
||||||
|
[HttpPost("test-smtp")]
|
||||||
|
public async Task<IActionResult> TestSmtp([FromBody] TestSmtpRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(new TestSmtpCommand(
|
||||||
|
request.Host, request.Port, request.Username, request.Password,
|
||||||
|
request.UseSsl, request.FromEmail, request.FromName, request.ToEmail), ct);
|
||||||
|
|
||||||
|
if (!result.IsSuccess)
|
||||||
|
return Ok(new { success = false, message = result.Error?.Message });
|
||||||
|
|
||||||
|
return Ok(new { success = true, message = $"Test email sent to {request.ToEmail}." });
|
||||||
|
}
|
||||||
|
|
||||||
// POST /admin/channels/{id}/test
|
// POST /admin/channels/{id}/test
|
||||||
[HttpPost("{id:guid}/test")]
|
[HttpPost("{id:guid}/test")]
|
||||||
public async Task<IActionResult> Test(Guid id, [FromBody] TestChannelRequest request, CancellationToken ct)
|
public async Task<IActionResult> Test(Guid id, [FromBody] TestChannelRequest request, CancellationToken ct)
|
||||||
@@ -165,3 +180,6 @@ public class AdminChannelsController(IMediator mediator) : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
public record TestChannelRequest(string ToEmail);
|
public record TestChannelRequest(string ToEmail);
|
||||||
|
public record TestSmtpRequest(
|
||||||
|
string Host, int Port, string Username, string Password,
|
||||||
|
bool UseSsl, string FromEmail, string FromName, string ToEmail);
|
||||||
|
|||||||
@@ -121,21 +121,16 @@
|
|||||||
<button type="submit" form="channelForm" class="btn btn-primary">
|
<button type="submit" form="channelForm" class="btn btn-primary">
|
||||||
<i class="bi bi-floppy me-1"></i> Save
|
<i class="bi bi-floppy me-1"></i> Save
|
||||||
</button>
|
</button>
|
||||||
@if (!Model.IsNew)
|
|
||||||
{
|
|
||||||
<button type="button" class="btn btn-success" id="testModalBtn">
|
<button type="button" class="btn btn-success" id="testModalBtn">
|
||||||
<i class="bi bi-send me-1"></i> Test
|
<i class="bi bi-send me-1"></i> Test
|
||||||
</button>
|
</button>
|
||||||
}
|
|
||||||
<a href="/admin/channels" class="btn btn-secondary">
|
<a href="/admin/channels" class="btn btn-secondary">
|
||||||
<i class="bi bi-x-lg me-1"></i> Cancel
|
<i class="bi bi-x-lg me-1"></i> Cancel
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@if (!Model.IsNew)
|
<div class="modal fade" id="testModal" tabindex="-1">
|
||||||
{
|
|
||||||
<div class="modal fade" id="testModal" tabindex="-1">
|
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@@ -143,28 +138,30 @@
|
|||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
<p class="text-muted small">Tests the current form settings — no need to save first.</p>
|
||||||
<label for="testToEmail" class="form-label fw-semibold">Recipient Email</label>
|
<label for="testToEmail" class="form-label fw-semibold">Recipient Email</label>
|
||||||
<input type="email" id="testToEmail" class="form-control" placeholder="you@example.com" />
|
<input type="email" id="testToEmail" class="form-control" placeholder="you@example.com" />
|
||||||
<div id="testResult" class="mt-3" style="display:none"></div>
|
<div id="testResult" class="mt-3" style="display:none"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
<button type="button" class="btn btn-success" id="testSendBtn" onclick="sendTestEmail('@Model.Id')">
|
<button type="button" class="btn btn-success" id="testSendBtn" onclick="sendTestEmail()">
|
||||||
<i class="bi bi-send me-1"></i> Send
|
<i class="bi bi-send me-1"></i> Send
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('click', function (e) {
|
document.addEventListener('click', function (e) {
|
||||||
if (e.target.closest('#testModalBtn')) {
|
if (e.target.closest('#testModalBtn')) {
|
||||||
|
document.getElementById('testResult').style.display = 'none';
|
||||||
bootstrap.Modal.getOrCreateInstance(document.getElementById('testModal')).show();
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('testModal')).show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function sendTestEmail(channelId) {
|
async function sendTestEmail() {
|
||||||
const toEmail = document.getElementById('testToEmail').value.trim();
|
const toEmail = document.getElementById('testToEmail').value.trim();
|
||||||
const resultDiv = document.getElementById('testResult');
|
const resultDiv = document.getElementById('testResult');
|
||||||
const sendBtn = document.getElementById('testSendBtn');
|
const sendBtn = document.getElementById('testSendBtn');
|
||||||
@@ -175,15 +172,27 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const form = document.getElementById('channelForm');
|
||||||
|
const payload = {
|
||||||
|
host: form.querySelector('[name="Host"]').value,
|
||||||
|
port: parseInt(form.querySelector('[name="Port"]').value, 10),
|
||||||
|
username: form.querySelector('[name="Username"]').value,
|
||||||
|
password: form.querySelector('[name="Password"]').value,
|
||||||
|
useSsl: form.querySelector('[name="UseSsl"]').checked,
|
||||||
|
fromEmail: form.querySelector('[name="FromEmail"]').value,
|
||||||
|
fromName: form.querySelector('[name="FromName"]').value,
|
||||||
|
toEmail
|
||||||
|
};
|
||||||
|
|
||||||
sendBtn.disabled = true;
|
sendBtn.disabled = true;
|
||||||
sendBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Sending…';
|
sendBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Sending…';
|
||||||
resultDiv.style.display = 'none';
|
resultDiv.style.display = 'none';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/admin/channels/${channelId}/test`, {
|
const resp = await fetch('/admin/channels/test-smtp', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ toEmail })
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
resultDiv.style.display = 'block';
|
resultDiv.style.display = 'block';
|
||||||
@@ -198,5 +207,4 @@
|
|||||||
sendBtn.innerHTML = '<i class="bi bi-send me-1"></i> Send';
|
sendBtn.innerHTML = '<i class="bi bi-send me-1"></i> Send';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user