using WebPush; namespace Service; public interface INotificationService { Task GetVapidPublicKeyAsync(); Task SubscribeAsync(PushSubscriptionEntity subscription); Task SendNotificationAsync(string message, string? url = null); } public class NotificationService( IPushSubscriptionRepository subscriptionRepo, IConfiguration configuration, ILogger logger) : INotificationService { private NotificationSettings GetSettings() { var settings = configuration.GetSection("NotificationSettings").Get(); if (settings == null) { // Fallback or throw. For now, let's return empty to avoid crashing if not configured, // but logging error is better. logger.LogWarning("NotificationSettings not configured"); return new NotificationSettings(); } return settings; } public Task GetVapidPublicKeyAsync() { return Task.FromResult(GetSettings().PublicKey); } public async Task SubscribeAsync(PushSubscriptionEntity subscription) { var existing = await subscriptionRepo.GetByEndpointAsync(subscription.Endpoint); if (existing != null) { existing.P256DH = subscription.P256DH; existing.Auth = subscription.Auth; existing.UpdateTime = DateTime.Now; await subscriptionRepo.UpdateAsync(existing); } else { await subscriptionRepo.AddAsync(subscription); } } public async Task SendNotificationAsync(string message, string? url = null) { var settings = GetSettings(); if (string.IsNullOrEmpty(settings.PublicKey) || string.IsNullOrEmpty(settings.PrivateKey)) { logger.LogWarning("VAPID keys not configured, skipping notification"); return; } var vapidDetails = new VapidDetails(settings.Subject, settings.PublicKey, settings.PrivateKey); var webPushClient = new WebPushClient(); var subscriptions = await subscriptionRepo.GetAllAsync(); var payload = System.Text.Json.JsonSerializer.Serialize(new { title = "System Notification", body = message, url = url ?? "/", icon = "/pwa-192x192.png" }); foreach (var sub in subscriptions) { try { var pushSubscription = new PushSubscription(sub.Endpoint, sub.P256DH, sub.Auth); await webPushClient.SendNotificationAsync(pushSubscription, payload, vapidDetails); } catch (WebPushException ex) { if (ex.StatusCode == System.Net.HttpStatusCode.Gone || ex.StatusCode == System.Net.HttpStatusCode.NotFound) { await subscriptionRepo.DeleteAsync(sub.Id); } logger.LogError(ex, "Error sending push notification to {Endpoint}", sub.Endpoint); } catch (Exception ex) { logger.LogError(ex, "Error sending push notification to {Endpoint}", sub.Endpoint); } } } }