feat(notification): add admin rest management and deletion
Add revision-safe service and template APIs, channel mapping, and validated admin controls. Ref: IT-1601
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
using HrynCo.NotificationService.Services.TemplateDefinitions;
|
||||
using MediatR;
|
||||
using HrynCo.NotificationService.DAL.Abstract.Repositories;
|
||||
using HrynCo.NotificationService.DAL.Abstract.Templates;
|
||||
using HrynCo.NotificationService.Web.Controllers.Admin.ViewModels;
|
||||
@@ -6,7 +8,8 @@ using Microsoft.AspNetCore.Mvc;
|
||||
namespace HrynCo.NotificationService.Web.Controllers.Admin;
|
||||
|
||||
[Route("admin/services")]
|
||||
public sealed class AdminServicesController(INotificationServiceRepository services, ILogger<AdminServicesController> logger) : Controller
|
||||
public sealed class AdminServicesController(INotificationServiceRepository services, ILogger<AdminServicesController> logger,
|
||||
IEmailTemplateDefinitionRepository definitions, IMediator mediator) : Controller
|
||||
{
|
||||
[HttpGet("")]
|
||||
public async Task<IActionResult> Index(CancellationToken ct) => View(await services.GetAllAsync(ct));
|
||||
@@ -77,6 +80,55 @@ public sealed class AdminServicesController(INotificationServiceRepository servi
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("{serviceId:guid}/delete")]
|
||||
public async Task<IActionResult> ConfirmServiceDelete([FromRoute] Guid serviceId, CancellationToken ct)
|
||||
{
|
||||
var model = await ServiceDeleteModelAsync(serviceId, ct);
|
||||
return model is null ? NotFound() : View("DeleteService", model);
|
||||
}
|
||||
|
||||
[HttpPost("{serviceId:guid}/delete")]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> DeleteService([FromRoute] Guid serviceId, NotificationServiceDeleteViewModel model, CancellationToken ct)
|
||||
{
|
||||
if (!ModelState.IsValid || !model.ConfirmDelete)
|
||||
return await ServiceDeleteErrorAsync(serviceId, "Confirm deletion of this service to continue.", ct);
|
||||
try
|
||||
{
|
||||
var result = await mediator.Send(new DeleteNotificationServiceCommand(serviceId, model.Revision), ct);
|
||||
if (result.IsSuccess)
|
||||
return RedirectToAction(nameof(Index));
|
||||
return await ServiceDeleteErrorAsync(serviceId, result.Error!.Message, ct);
|
||||
}
|
||||
catch (Exception error) when (error is not OperationCanceledException)
|
||||
{
|
||||
logger.LogError(error, "Failed to delete notification service.");
|
||||
return await ServiceDeleteErrorAsync(serviceId, "Could not complete deletion. Review the current service before trying again.", ct);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<NotificationServiceDeleteViewModel?> ServiceDeleteModelAsync(Guid serviceId, CancellationToken ct)
|
||||
{
|
||||
var service = await services.GetAsync(serviceId, ct);
|
||||
if (service is null)
|
||||
return null;
|
||||
return new NotificationServiceDeleteViewModel
|
||||
{
|
||||
ServiceId = service.Id, Name = service.Name, Revision = service.Revision,
|
||||
DefinitionCount = (await definitions.GetDefinitionsAsync(serviceId, ct)).Count
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<IActionResult> ServiceDeleteErrorAsync(Guid serviceId, string error, CancellationToken ct)
|
||||
{
|
||||
var model = await ServiceDeleteModelAsync(serviceId, ct);
|
||||
if (model is null)
|
||||
return NotFound();
|
||||
ModelState.Clear();
|
||||
ModelState.AddModelError("", error);
|
||||
return View("DeleteService", model);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> ServiceEditorViewAsync(NotificationServiceEditViewModel model, CancellationToken ct)
|
||||
{
|
||||
if (model.Id is not Guid id)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using HrynCo.NotificationService.Services.TemplateDefinitions;
|
||||
using System.Text.Json;
|
||||
using HrynCo.NotificationService.DAL.Abstract.Repositories;
|
||||
using HrynCo.NotificationService.DAL.Abstract.Templates;
|
||||
@@ -95,6 +96,59 @@ public sealed class AdminTemplatesController(IMediator mediator, IEmailTemplateD
|
||||
return RedirectToAction(nameof(Index), new { serviceId, key = keyFilter });
|
||||
}
|
||||
|
||||
[HttpGet("{definitionId:guid}/delete")]
|
||||
public async Task<IActionResult> ConfirmDefinitionDelete([FromRoute] Guid serviceId, [FromRoute] Guid definitionId,
|
||||
[FromQuery] string? keyFilter, CancellationToken ct)
|
||||
{
|
||||
var model = await DefinitionDeleteModelAsync(serviceId, definitionId, keyFilter, ct);
|
||||
return model is null ? NotFound() : View("DeleteDefinition", model);
|
||||
}
|
||||
|
||||
[HttpPost("{definitionId:guid}/delete")]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> DeleteDefinition([FromRoute] Guid serviceId, [FromRoute] Guid definitionId,
|
||||
EmailTemplateDefinitionDeleteViewModel model, CancellationToken ct)
|
||||
{
|
||||
var current = await DefinitionDeleteModelAsync(serviceId, definitionId, model.KeyFilter, ct);
|
||||
if (current is null)
|
||||
return NotFound();
|
||||
if (!ModelState.IsValid || !model.ConfirmDelete)
|
||||
return await DefinitionDeleteErrorAsync(serviceId, definitionId, model.KeyFilter, "Confirm deletion of this template and its translations to continue.", ct);
|
||||
try
|
||||
{
|
||||
var result = await mediator.Send(new DeleteEmailTemplateDefinitionCommand(serviceId, current.Key, model.Revision), ct);
|
||||
if (result.IsSuccess)
|
||||
return RedirectToAction(nameof(Index), new { serviceId, key = model.KeyFilter });
|
||||
return await DefinitionDeleteErrorAsync(serviceId, definitionId, model.KeyFilter, result.Error!.Message, ct);
|
||||
}
|
||||
catch (Exception error) when (error is not OperationCanceledException)
|
||||
{
|
||||
logger.LogError(error, "Failed to delete email template definition.");
|
||||
return await DefinitionDeleteErrorAsync(serviceId, definitionId, model.KeyFilter, "Could not complete deletion. Review the current template before trying again.", ct);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<EmailTemplateDefinitionDeleteViewModel?> DefinitionDeleteModelAsync(Guid serviceId, Guid definitionId, string? keyFilter, CancellationToken ct)
|
||||
{
|
||||
var service = await services.GetAsync(serviceId, ct);
|
||||
var definition = await OwnedDefinitionAsync(service, definitionId, ct);
|
||||
return definition is null ? null : new EmailTemplateDefinitionDeleteViewModel
|
||||
{
|
||||
ServiceId = serviceId, DefinitionId = definition.Id, ServiceName = definition.ServiceName, Key = definition.Key,
|
||||
Revision = definition.Revision, KeyFilter = keyFilter, LanguageCodes = definition.Languages.Select(l => l.LanguageCode).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<IActionResult> DefinitionDeleteErrorAsync(Guid serviceId, Guid definitionId, string? keyFilter, string error, CancellationToken ct)
|
||||
{
|
||||
var model = await DefinitionDeleteModelAsync(serviceId, definitionId, keyFilter, ct);
|
||||
if (model is null)
|
||||
return NotFound();
|
||||
ModelState.Clear();
|
||||
ModelState.AddModelError("", error);
|
||||
return View("DeleteDefinition", model);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> SaveDefinitionAsync(EmailTemplateDefinition definition, EmailTemplateEditViewModel model,
|
||||
NotificationServiceInfo service, CancellationToken ct)
|
||||
{
|
||||
@@ -117,7 +171,7 @@ public sealed class AdminTemplatesController(IMediator mediator, IEmailTemplateD
|
||||
{
|
||||
if (service is null)
|
||||
return null;
|
||||
return (await definitions.GetDefinitionsAsync(service.Name, ct: ct))
|
||||
return (await definitions.GetDefinitionsAsync(service.Id, ct))
|
||||
.SingleOrDefault(d => d.Id == definitionId && d.ServiceId == service.Id);
|
||||
}
|
||||
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
namespace HrynCo.NotificationService.Web.Controllers.Admin.ViewModels;
|
||||
|
||||
public sealed class EmailTemplateDefinitionDeleteViewModel
|
||||
{
|
||||
public Guid ServiceId { get; init; }
|
||||
public Guid DefinitionId { get; init; }
|
||||
public string ServiceName { get; init; } = "";
|
||||
public string Key { get; init; } = "";
|
||||
public Guid Revision { get; set; }
|
||||
public string? KeyFilter { get; set; }
|
||||
public IReadOnlyList<string> LanguageCodes { get; init; } = [];
|
||||
public bool ConfirmDelete { get; set; }
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
namespace HrynCo.NotificationService.Web.Controllers.Admin.ViewModels;
|
||||
|
||||
public sealed class NotificationServiceDeleteViewModel
|
||||
{
|
||||
public Guid ServiceId { get; init; }
|
||||
public string Name { get; init; } = "";
|
||||
public Guid Revision { get; set; }
|
||||
public int DefinitionCount { get; init; }
|
||||
public bool ConfirmDelete { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user