aboutsummaryrefslogtreecommitdiff
path: root/src/Enmarcha.Backend/Controllers
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2026-04-02 12:38:10 +0200
committerAriel Costas Guerrero <ariel@costas.dev>2026-04-02 12:45:33 +0200
commit1b4f4a674ac533c0b51260ba35ab91dd2cf9486d (patch)
tree9fdaf418bef86c51737bcf203483089c9e2b908b /src/Enmarcha.Backend/Controllers
parent749e04d6fc2304bb29920db297d1fa4d73b57648 (diff)
Basic push notification system for service alerts
Co-authored-by: Copilot <copilot@github.com>
Diffstat (limited to 'src/Enmarcha.Backend/Controllers')
-rw-r--r--src/Enmarcha.Backend/Controllers/AlertsController.cs40
-rw-r--r--src/Enmarcha.Backend/Controllers/Backoffice/AlertsController.cs21
-rw-r--r--src/Enmarcha.Backend/Controllers/PushController.cs50
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);