diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2026-04-02 12:38:10 +0200 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2026-04-02 12:45:33 +0200 |
| commit | 1b4f4a674ac533c0b51260ba35ab91dd2cf9486d (patch) | |
| tree | 9fdaf418bef86c51737bcf203483089c9e2b908b /src/Enmarcha.Backend/Controllers | |
| parent | 749e04d6fc2304bb29920db297d1fa4d73b57648 (diff) | |
Basic push notification system for service alerts
Co-authored-by: Copilot <copilot@github.com>
Diffstat (limited to 'src/Enmarcha.Backend/Controllers')
3 files changed, 110 insertions, 1 deletions
diff --git a/src/Enmarcha.Backend/Controllers/AlertsController.cs b/src/Enmarcha.Backend/Controllers/AlertsController.cs new file mode 100644 index 0000000..4860399 --- /dev/null +++ b/src/Enmarcha.Backend/Controllers/AlertsController.cs @@ -0,0 +1,40 @@ +using Enmarcha.Backend.Data; +using Enmarcha.Backend.Data.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Enmarcha.Backend.Controllers; + +[Route("api/alerts")] +[ApiController] +public class AlertsController(AppDbContext db) : ControllerBase +{ + /// <summary> + /// Returns all service alerts that are currently published and not yet hidden. + /// Includes PreNotice, Active, and Finished phases. + /// </summary> + [HttpGet] + public async Task<IActionResult> GetAlerts() + { + var now = DateTime.UtcNow; + var alerts = await db.ServiceAlerts + .Where(a => a.PublishDate <= now && a.HiddenDate > now) + .OrderByDescending(a => a.EventStartDate) + .ToListAsync(); + + return Ok(alerts.Select(a => new + { + id = a.Id, + version = a.Version, + phase = a.GetPhase(now).ToString(), + cause = a.Cause.ToString(), + effect = a.Effect.ToString(), + header = (Dictionary<string, string>)a.Header, + description = (Dictionary<string, string>)a.Description, + selectors = a.Selectors.Select(s => s.Raw).ToList(), + infoUrls = a.InfoUrls, + eventStartDate = a.EventStartDate, + eventEndDate = a.EventEndDate, + })); + } +} diff --git a/src/Enmarcha.Backend/Controllers/Backoffice/AlertsController.cs b/src/Enmarcha.Backend/Controllers/Backoffice/AlertsController.cs index 4e83abc..3fa499e 100644 --- a/src/Enmarcha.Backend/Controllers/Backoffice/AlertsController.cs +++ b/src/Enmarcha.Backend/Controllers/Backoffice/AlertsController.cs @@ -1,4 +1,5 @@ using Enmarcha.Backend.Data; +using Enmarcha.Backend.Services; using Enmarcha.Backend.ViewModels; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -8,7 +9,11 @@ namespace Enmarcha.Backend.Controllers.Backoffice; [Route("backoffice/alerts")] [Authorize(AuthenticationSchemes = "Backoffice")] -public class AlertsController(AppDbContext db) : Controller +public class AlertsController( + AppDbContext db, + IPushNotificationService pushService, + ILogger<AlertsController> logger +) : Controller { [HttpGet("")] public async Task<IActionResult> Index() @@ -28,6 +33,7 @@ public class AlertsController(AppDbContext db) : Controller { if (!ModelState.IsValid) { + logger.LogWarning("Invalid model state when creating alert: {Errors}", ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage)); return View("Edit", model); } @@ -50,6 +56,7 @@ public class AlertsController(AppDbContext db) : Controller { if (!ModelState.IsValid) { + logger.LogWarning("Invalid model state when editing alert {Id}: {Errors}", id, ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage)); return View("Edit", model); } @@ -80,4 +87,16 @@ public class AlertsController(AppDbContext db) : Controller await db.SaveChangesAsync(); return RedirectToAction(nameof(Index)); } + + [HttpPost("{id}/push")] + [ValidateAntiForgeryToken] + public async Task<IActionResult> SendPush(string id) + { + var alert = await db.ServiceAlerts.FindAsync(id); + if (alert is null) return NotFound(); + + await pushService.SendAlertAsync(alert); + TempData["SuccessMessage"] = $"Notificación enviada (v{alert.Version})"; + return RedirectToAction(nameof(Index)); + } } diff --git a/src/Enmarcha.Backend/Controllers/PushController.cs b/src/Enmarcha.Backend/Controllers/PushController.cs new file mode 100644 index 0000000..9df6b48 --- /dev/null +++ b/src/Enmarcha.Backend/Controllers/PushController.cs @@ -0,0 +1,50 @@ +using Enmarcha.Backend.Configuration; +using Enmarcha.Backend.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; + +namespace Enmarcha.Backend.Controllers; + +[Route("api/push")] +[ApiController] +public class PushController(IPushNotificationService pushService, IOptions<AppConfiguration> config) : ControllerBase +{ + /// <summary>Returns the VAPID public key for the browser to use when subscribing.</summary> + [HttpGet("vapid-public-key")] + public IActionResult GetVapidPublicKey() + { + var vapid = config.Value.Vapid; + if (vapid is null) + return StatusCode(StatusCodes.Status503ServiceUnavailable, "Push notifications are not configured on this server."); + + return Ok(new { publicKey = vapid.PublicKey }); + } + + /// <summary>Registers a new push subscription.</summary> + [HttpPost("subscribe")] + public async Task<IActionResult> Subscribe([FromBody] SubscribeRequest request) + { + if (!Uri.TryCreate(request.Endpoint, UriKind.Absolute, out var uri) || uri.Scheme != Uri.UriSchemeHttps) + return BadRequest("Invalid push endpoint: must be an absolute HTTPS URL."); + + if (string.IsNullOrWhiteSpace(request.P256Dh) || string.IsNullOrWhiteSpace(request.Auth)) + return BadRequest("Missing encryption keys."); + + await pushService.SubscribeAsync(request.Endpoint, request.P256Dh, request.Auth); + return NoContent(); + } + + /// <summary>Removes a push subscription.</summary> + [HttpDelete("unsubscribe")] + public async Task<IActionResult> Unsubscribe([FromBody] UnsubscribeRequest request) + { + if (string.IsNullOrWhiteSpace(request.Endpoint)) + return BadRequest("Endpoint is required."); + + await pushService.UnsubscribeAsync(request.Endpoint); + return NoContent(); + } +} + +public record SubscribeRequest(string Endpoint, string P256Dh, string Auth); +public record UnsubscribeRequest(string Endpoint); |
