feat(push): add service-owned firebase channels and delivery

Add push contracts, provider delivery and channel diagnostics. Link email channels to services and document migration and validation.

Ref: IT-1074
This commit is contained in:
2026-10-24 12:31:35 +03:00
parent b30dba93e8
commit 1631786ce2
105 changed files with 3838 additions and 147 deletions
@@ -14,7 +14,7 @@ using Microsoft.AspNetCore.Mvc;
namespace HrynCo.NotificationService.Web.Controllers.Admin;
[Route("admin/channels")]
public class AdminChannelsController(IMediator mediator) : Controller
public class AdminChannelsController(IMediator mediator, INotificationServiceRepository services) : Controller
{
// GET /admin/channels
@@ -33,10 +33,7 @@ public class AdminChannelsController(IMediator mediator) : Controller
// GET /admin/channels/create
[HttpGet("create")]
public IActionResult Create()
{
return View("Edit", new EmailChannelEditViewModel());
}
public Task<IActionResult> Create(CancellationToken ct) => EditorViewAsync(new EmailChannelEditViewModel(), ct);
// GET /admin/channels/{id}
[HttpGet("{id:guid}")]
@@ -55,7 +52,7 @@ public class AdminChannelsController(IMediator mediator) : Controller
var vm = new EmailChannelEditViewModel
{
Id = channel.Id,
ServiceName = channel.ServiceName,
ServiceId = channel.ServiceId,
Priority = channel.Priority,
EmailChannelType = channel.EmailChannelType,
DailyLimit = channel.DailyLimit,
@@ -70,7 +67,7 @@ public class AdminChannelsController(IMediator mediator) : Controller
FromName = smtp.FromName
};
return View(vm);
return await EditorViewAsync(vm, ct);
}
// POST /admin/channels/save
@@ -79,13 +76,13 @@ public class AdminChannelsController(IMediator mediator) : Controller
public async Task<IActionResult> Save(EmailChannelEditViewModel model, CancellationToken ct)
{
if (!ModelState.IsValid)
return EditWithErrors(model);
return await EditorViewAsync(model, ct);
var (password, passwordError) = await ResolvePasswordForSave(model, ct);
if (passwordError is not null)
{
ModelState.AddModelError(nameof(model.Password), passwordError);
return EditWithErrors(model);
return await EditorViewAsync(model, ct);
}
var smtpSettings = new SmtpChannelSettings
@@ -102,7 +99,7 @@ public class AdminChannelsController(IMediator mediator) : Controller
if (model.IsNew)
{
var command = new CreateEmailChannelCommand(
model.ServiceName,
model.ServiceId!.Value,
model.Priority,
EmailChannelType.Smtp,
smtpSettings,
@@ -115,13 +112,14 @@ public class AdminChannelsController(IMediator mediator) : Controller
if (!result.IsSuccess)
{
ModelState.AddModelError("", result.Error?.Message ?? "Failed to create channel.");
return EditWithErrors(model);
return await EditorViewAsync(model, ct);
}
}
else
{
var command = new UpdateEmailChannelCommand(
model.Id,
model.ServiceId!.Value,
model.Priority,
smtpSettings,
model.DailyLimit,
@@ -133,7 +131,7 @@ public class AdminChannelsController(IMediator mediator) : Controller
if (!result.IsSuccess)
{
ModelState.AddModelError("", result.Error?.Message ?? "Failed to update channel.");
return EditWithErrors(model);
return await EditorViewAsync(model, ct);
}
}
@@ -157,9 +155,12 @@ public class AdminChannelsController(IMediator mediator) : Controller
return (saved.Password, null);
}
private IActionResult EditWithErrors(EmailChannelEditViewModel model)
private async Task<IActionResult> EditorViewAsync(EmailChannelEditViewModel model, CancellationToken ct)
{
model.Password = null;
model.Services = (await services.GetAllAsync(ct))
.Select(service => new Microsoft.AspNetCore.Mvc.Rendering.SelectListItem(service.Name, service.Id.ToString()))
.ToList();
return View("Edit", model);
}
@@ -227,5 +228,3 @@ public class AdminChannelsController(IMediator mediator) : Controller
return RedirectToAction(nameof(Index));
}
}
public record TestChannelRequest(string ToEmail);