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 logger) : Controller { private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) { WriteIndented = true }; [HttpGet("email")] public async Task 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 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 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 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 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 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 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() }; }