aba4173827
Share template keys across email and push with independent editors and revisions. Render localized actions and preserve version-one delivery. Ref: IT-1627
163 lines
8.6 KiB
C#
163 lines
8.6 KiB
C#
using System.Text.Json;
|
|
using HrynCo.NotificationService.DAL.Abstract.Repositories;
|
|
using HrynCo.NotificationService.DAL.Abstract.Templates;
|
|
using HrynCo.NotificationService.Web.Controllers.Admin.ViewModels;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
namespace HrynCo.NotificationService.Web.Controllers.Admin;
|
|
|
|
[Route("admin/services/{serviceId:guid}/push-templates")]
|
|
public sealed class AdminPushTemplatesController(IPushTemplateDefinitionRepository pushes,
|
|
IEmailTemplateDefinitionRepository emails, INotificationServiceRepository services,
|
|
ILogger<AdminPushTemplatesController> logger) : Controller
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) { WriteIndented = true };
|
|
|
|
[HttpGet("email")]
|
|
public async Task<IActionResult> Email([FromRoute] Guid serviceId, string? key, string? keyFilter, CancellationToken ct)
|
|
{
|
|
var service = await services.GetAsync(serviceId, ct);
|
|
if (service is null) return NotFound();
|
|
if (string.IsNullOrEmpty(key))
|
|
return RedirectToAction("Create", "AdminTemplates", new { serviceId, keyFilter });
|
|
var email = await emails.GetDefinitionAsync(serviceId, key, ct);
|
|
if (email is not null)
|
|
return RedirectToAction("Edit", "AdminTemplates", new { serviceId, definitionId = email.Id, keyFilter });
|
|
return Missing(service, key, keyFilter, false);
|
|
}
|
|
|
|
[HttpGet("edit")]
|
|
public async Task<IActionResult> Edit([FromRoute] Guid serviceId, string? key, string? keyFilter, CancellationToken ct)
|
|
{
|
|
var service = await services.GetAsync(serviceId, ct);
|
|
if (service is null) return NotFound();
|
|
if (string.IsNullOrEmpty(key))
|
|
return RedirectToAction(nameof(Create), new { serviceId, keyFilter });
|
|
var definition = await pushes.GetAsync(serviceId, key, ct);
|
|
return definition is null ? Missing(service, key, keyFilter, true)
|
|
: Editor(ToModel(definition, service, keyFilter), service);
|
|
}
|
|
|
|
[HttpGet("create")]
|
|
public async Task<IActionResult> Create([FromRoute] Guid serviceId, string? key, string? keyFilter, CancellationToken ct)
|
|
{
|
|
var service = await services.GetAsync(serviceId, ct);
|
|
if (service is null) return NotFound();
|
|
if (key is not null && await pushes.GetAsync(serviceId, key, ct) is not null)
|
|
return RedirectToAction(nameof(Edit), new { serviceId, key, keyFilter });
|
|
return Editor(new PushTemplateEditViewModel
|
|
{
|
|
ServiceId = serviceId, ServiceName = service.Name, Key = key ?? "", KeyFilter = keyFilter,
|
|
LockKey = key is not null && await emails.GetDefinitionAsync(serviceId, key, ct) is not null
|
|
}, service);
|
|
}
|
|
|
|
[HttpPost("save"), ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> Save([FromRoute] Guid serviceId, PushTemplateEditViewModel model, CancellationToken ct)
|
|
{
|
|
var service = await services.GetAsync(serviceId, ct);
|
|
if (service is null || serviceId != model.ServiceId) return NotFound();
|
|
model.ServiceName = service.Name;
|
|
var persisted = await pushes.GetAsync(serviceId, model.Key, ct);
|
|
if (model.Id is not null && persisted?.Id != model.Id) return NotFound();
|
|
model.LockKey = model.Id is not null || await emails.GetDefinitionAsync(serviceId, model.Key, ct) is not null;
|
|
SetSavedStatuses(model, persisted);
|
|
ValidateCatalog(model, service);
|
|
if (!ModelState.IsValid) return Editor(model, service);
|
|
return await SaveModelAsync(model, service, ct);
|
|
}
|
|
|
|
private async Task<IActionResult> SaveModelAsync(PushTemplateEditViewModel model, NotificationServiceInfo service, CancellationToken ct)
|
|
{
|
|
try
|
|
{
|
|
await pushes.SaveAsync(new PushTemplateDefinition
|
|
{
|
|
Id = model.Id ?? Guid.Empty, ServiceId = service.Id, Key = model.Key, Revision = model.Revision,
|
|
Variables = EmailTemplateValidation.ParseVariables(model.VariablesJson),
|
|
Languages = model.Languages.Where(l => l.HasContent).Select(l => new PushTemplateTranslation
|
|
{
|
|
LanguageCode = l.LanguageCode, Title = l.Title, Body = l.Body, ActionsJson = l.ActionsJson
|
|
}).ToList()
|
|
}, model.IsNew, ct);
|
|
return RedirectToAction("Index", "AdminTemplates", new { serviceId = service.Id, key = model.KeyFilter });
|
|
}
|
|
catch (JsonException) { ModelState.AddModelError("VariablesJson", "Invalid variables JSON. Use name and required properties."); }
|
|
catch (EmailTemplateWriteException error) { ModelState.AddModelError("", error.Message); }
|
|
catch (Exception error) when (error is not OperationCanceledException)
|
|
{
|
|
logger.LogError(error, "Failed to save push template definition");
|
|
ModelState.AddModelError("", "Could not save the push template. Your entered values are retained; try again.");
|
|
}
|
|
return Editor(model, service);
|
|
}
|
|
|
|
[HttpGet("delete")]
|
|
public async Task<IActionResult> ConfirmDelete([FromRoute] Guid serviceId, string key, string? keyFilter, CancellationToken ct)
|
|
{
|
|
var service = await services.GetAsync(serviceId, ct);
|
|
var definition = await pushes.GetAsync(serviceId, key, ct);
|
|
return service is null || definition is null ? NotFound() : View("Delete", ToModel(definition, service, keyFilter));
|
|
}
|
|
|
|
[HttpPost("delete"), ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> Delete([FromRoute] Guid serviceId, PushTemplateEditViewModel model, CancellationToken ct)
|
|
{
|
|
var service = await services.GetAsync(serviceId, ct);
|
|
var definition = await pushes.GetAsync(serviceId, model.Key, ct);
|
|
if (service is null || definition is null) return NotFound();
|
|
if (!model.ConfirmDelete || !ModelState.IsValid)
|
|
ModelState.AddModelError("", "Confirm deletion of this push template to continue.");
|
|
else
|
|
{
|
|
try
|
|
{
|
|
await pushes.DeleteAsync(serviceId, model.Key, model.Revision, ct);
|
|
return RedirectToAction("Index", "AdminTemplates", new { serviceId, key = model.KeyFilter });
|
|
}
|
|
catch (EmailTemplateWriteException error) { ModelState.AddModelError("", error.Message); }
|
|
}
|
|
var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList();
|
|
ModelState.Clear();
|
|
foreach (var error in errors) ModelState.AddModelError("", error);
|
|
return View("Delete", ToModel(definition, service, model.KeyFilter));
|
|
}
|
|
|
|
private IActionResult Missing(NotificationServiceInfo service, string key, string? keyFilter, bool isPush) => View("Missing", new TemplateChannelNavigationViewModel
|
|
{
|
|
ServiceId = service.Id, ServiceName = service.Name, Key = key, KeyFilter = keyFilter, IsPush = isPush
|
|
});
|
|
|
|
private IActionResult Editor(PushTemplateEditViewModel model, NotificationServiceInfo service)
|
|
{
|
|
foreach (var language in service.Languages)
|
|
if (!model.Languages.Any(l => l.LanguageCode == language.Code))
|
|
model.Languages.Add(new PushTemplateLanguageEditViewModel { LanguageCode = language.Code });
|
|
return View("Edit", model);
|
|
}
|
|
|
|
private void ValidateCatalog(PushTemplateEditViewModel model, NotificationServiceInfo service)
|
|
{
|
|
var codes = model.Languages.Select(l => l.LanguageCode).ToHashSet(StringComparer.Ordinal);
|
|
if (codes.Count != model.Languages.Count || !codes.SetEquals(service.Languages.Select(l => l.Code)))
|
|
ModelState.AddModelError("", "The service language catalog changed or the submitted tabs are invalid. Reload before saving.");
|
|
}
|
|
|
|
private static void SetSavedStatuses(PushTemplateEditViewModel model, PushTemplateDefinition? persisted)
|
|
{
|
|
var codes = persisted?.Languages.Select(l => l.LanguageCode).ToHashSet(StringComparer.Ordinal) ?? [];
|
|
foreach (var language in model.Languages) language.IsPresent = codes.Contains(language.LanguageCode);
|
|
}
|
|
|
|
private static PushTemplateEditViewModel ToModel(PushTemplateDefinition definition, NotificationServiceInfo service, string? keyFilter) => new()
|
|
{
|
|
Id = definition.Id, ServiceId = service.Id, ServiceName = service.Name, Key = definition.Key,
|
|
Revision = definition.Revision, KeyFilter = keyFilter, LockKey = true,
|
|
VariablesJson = JsonSerializer.Serialize(definition.Variables, JsonOptions),
|
|
Languages = definition.Languages.Select(l => new PushTemplateLanguageEditViewModel
|
|
{
|
|
LanguageCode = l.LanguageCode, Title = l.Title, Body = l.Body, ActionsJson = l.ActionsJson, IsPresent = true
|
|
}).ToList()
|
|
};
|
|
}
|