aboutsummaryrefslogtreecommitdiff
path: root/src/Enmarcha.Backend/Services/PushNotificationService.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/Enmarcha.Backend/Services/PushNotificationService.cs')
-rw-r--r--src/Enmarcha.Backend/Services/PushNotificationService.cs134
1 files changed, 134 insertions, 0 deletions
diff --git a/src/Enmarcha.Backend/Services/PushNotificationService.cs b/src/Enmarcha.Backend/Services/PushNotificationService.cs
new file mode 100644
index 0000000..c9d79dc
--- /dev/null
+++ b/src/Enmarcha.Backend/Services/PushNotificationService.cs
@@ -0,0 +1,134 @@
+using Enmarcha.Backend.Configuration;
+using Enmarcha.Backend.Data;
+using Enmarcha.Backend.Data.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Options;
+using System.Text.Json;
+using WebPush;
+
+namespace Enmarcha.Backend.Services;
+
+public interface IPushNotificationService
+{
+ Task SubscribeAsync(string endpoint, string p256dh, string auth);
+ Task UnsubscribeAsync(string endpoint);
+ Task SendAlertAsync(ServiceAlert alert);
+}
+
+public class PushNotificationService(
+ AppDbContext db,
+ IOptions<AppConfiguration> options,
+ ILogger<PushNotificationService> logger
+) : IPushNotificationService
+{
+ private readonly WebPushClient? _client = BuildClient(options.Value.Vapid);
+
+ private static WebPushClient? BuildClient(VapidConfiguration? vapid)
+ {
+ if (vapid is null) return null;
+ var client = new WebPushClient();
+ client.SetVapidDetails(vapid.Subject, vapid.PublicKey, vapid.PrivateKey);
+ return client;
+ }
+
+ public async Task SubscribeAsync(string endpoint, string p256dh, string auth)
+ {
+ var existing = await db.PushSubscriptions
+ .FirstOrDefaultAsync(s => s.Endpoint == endpoint);
+
+ if (existing is null)
+ {
+ db.PushSubscriptions.Add(new Data.Models.PushSubscription
+ {
+ Id = Guid.NewGuid(),
+ Endpoint = endpoint,
+ P256DhKey = p256dh,
+ AuthKey = auth,
+ CreatedAt = DateTime.UtcNow,
+ });
+ }
+ else
+ {
+ // Refresh keys in case they changed (e.g. after re-subscription)
+ existing.P256DhKey = p256dh;
+ existing.AuthKey = auth;
+ }
+
+ await db.SaveChangesAsync();
+ }
+
+ public async Task UnsubscribeAsync(string endpoint)
+ {
+ var subscription = await db.PushSubscriptions
+ .FirstOrDefaultAsync(s => s.Endpoint == endpoint);
+
+ if (subscription is not null)
+ {
+ db.PushSubscriptions.Remove(subscription);
+ await db.SaveChangesAsync();
+ }
+ }
+
+ public async Task SendAlertAsync(ServiceAlert alert)
+ {
+ if (_client is null)
+ {
+ logger.LogWarning("VAPID not configured — skipping push notification for alert {AlertId}", alert.Id);
+ return;
+ }
+
+ var now = DateTime.UtcNow;
+ var phase = alert.GetPhase(now);
+
+ alert.Version++;
+
+ if (phase == AlertPhase.PreNotice)
+ alert.PreNoticeNotifiedAt = now;
+ else if (phase == AlertPhase.Active)
+ alert.ActiveNotifiedAt = now;
+
+ var payload = JsonSerializer.Serialize(new
+ {
+ alertId = alert.Id,
+ version = alert.Version,
+ phase = phase.ToString(),
+ cause = alert.Cause.ToString(),
+ effect = alert.Effect.ToString(),
+ header = (Dictionary<string, string>)alert.Header,
+ description = (Dictionary<string, string>)alert.Description,
+ selectors = alert.Selectors.Select(s => s.Raw).ToList(),
+ eventStart = alert.EventStartDate,
+ eventEnd = alert.EventEndDate,
+ });
+
+ var subscriptions = await db.PushSubscriptions.ToListAsync();
+ var expired = new List<Data.Models.PushSubscription>();
+
+ foreach (var sub in subscriptions)
+ {
+ try
+ {
+ var pushSub = new WebPush.PushSubscription(sub.Endpoint, sub.P256DhKey, sub.AuthKey);
+ await _client.SendNotificationAsync(pushSub, payload);
+ }
+ catch (WebPushException ex) when (
+ ex.StatusCode is System.Net.HttpStatusCode.Gone or System.Net.HttpStatusCode.NotFound)
+ {
+ // Subscription expired or was revoked — remove it
+ expired.Add(sub);
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(ex, "Failed to deliver push notification to endpoint {Endpoint}", sub.Endpoint[..Math.Min(40, sub.Endpoint.Length)]);
+ }
+ }
+
+ if (expired.Count > 0)
+ {
+ db.PushSubscriptions.RemoveRange(expired);
+ logger.LogInformation("Removed {Count} expired push subscription(s)", expired.Count);
+ }
+
+ await db.SaveChangesAsync();
+ }
+}