Add WebNotifications Module for push notifications in browser.#230
Add WebNotifications Module for push notifications in browser.#230JohannHoepfner wants to merge 9 commits into
Conversation
28f6f16 to
2e186e3
Compare
bb5beb9 to
538ea0e
Compare
538ea0e to
6fc6622
Compare
4db1f7f to
f986ab9
Compare
There was a problem hiding this comment.
Pull request overview
Adds a WebNotifications feature spanning backend + web client to support in-app notifications via SignalR, optional Web Push delivery via a service worker, and user notification settings (including opting out of email notifications).
Changes:
- WebClient: add PWA InjectManifest service worker, push subscription helper, notification center UI, and settings toggles.
- Backend: introduce
Altafraner.Backbone.WebNotificationmodule (SignalR hub, EF entities, VAPID + Web Push sender) and an AfraApp Notifications module with API endpoints + composite notification delivery. - Email scheduling: split email delivery behind
IEmailNotificationServiceand route application notifications through the new composite service.
Reviewed changes
Copilot reviewed 41 out of 41 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| WebClient/vite.config.js | Switch PWA strategy to injectManifest and point to custom src/sw.js. |
| WebClient/src/views/Settings.vue | Add notification settings + push enable/disable + test notification UI. |
| WebClient/src/sw.js | Implement Workbox precache/routing plus push + notificationclick handlers. |
| WebClient/src/stores/notifications.js | Add Pinia store for notifications list, settings, SignalR hub, and push subscription APIs. |
| WebClient/src/composables/pushNotifications.js | Add composable for push subscribe/unsubscribe + SW readiness helper. |
| WebClient/src/components/NotificationCenter.vue | Add bell UI with Popover list and live updates via SignalR. |
| WebClient/src/components/AfraNav.vue | Mount the notification center in the global navigation. |
| WebClient/src/assets/main.css | Minor CSS string quoting change. |
| Backend/Altafraner.Backbone.WebNotification/WebNotificationsModule.cs | Register WebNotifications services and map SignalR hub route. |
| Backend/Altafraner.Backbone.WebNotification/VapidConfiguration.cs | Add VAPID configuration object with DbContext registration helper. |
| Backend/Altafraner.Backbone.WebNotification/Services/WebPushSender.cs | Implement VAPID JWT + aes128gcm payload encryption and send via HttpClient. |
| Backend/Altafraner.Backbone.WebNotification/Services/PushSubscriptionGoneException.cs | Add exception type for stale/expired subscriptions. |
| Backend/Altafraner.Backbone.WebNotification/Services/InAppNotificationService.cs | Persist notifications, push to SignalR clients, and fan out Web Push. |
| Backend/Altafraner.Backbone.WebNotification/Services/IInAppNotificationService.cs | Define interface for in-app notifications + push subscription management. |
| Backend/Altafraner.Backbone.WebNotification/Domain/Models/PushSubscription.cs | EF model for storing Web Push subscriptions. |
| Backend/Altafraner.Backbone.WebNotification/Domain/Models/InAppNotification.cs | EF model for stored in-app notifications. |
| Backend/Altafraner.Backbone.WebNotification/Contracts/IWebNotificationRecipient.cs | Contract for recipients that can receive web notifications. |
| Backend/Altafraner.Backbone.WebNotification/Contracts/IWebNotificationContext.cs | Contract for DbContext integration (notifications + subscriptions). |
| Backend/Altafraner.Backbone.WebNotification/API/Hubs/NotificationHub.cs | Add authorized SignalR hub for notification delivery. |
| Backend/Altafraner.Backbone.WebNotification/API/Hubs/INotificationHubClient.cs | Define SignalR client contract and payload record. |
| Backend/Altafraner.Backbone.WebNotification/Altafraner.Backbone.WebNotification.csproj | New library project file for the WebNotification module. |
| Backend/Altafraner.Backbone.EmailSchedulingModule/Services/EmailNotificationService.cs | Change implementation to IEmailNotificationService. |
| Backend/Altafraner.Backbone.EmailSchedulingModule/EmailSchedulingModule.cs | Register IEmailNotificationService in DI. |
| Backend/Altafraner.Backbone.EmailSchedulingModule/Contracts/IEmailNotificationService.cs | Introduce email-only notification service interface. |
| Backend/Altafraner.AfraApp/User/Domain/Models/Person.cs | Implement web notification recipient and add ReceiveEmailNotifications. |
| Backend/Altafraner.AfraApp/Program.cs | Register Notifications module and WebNotifications Backbone module. |
| Backend/Altafraner.AfraApp/Profundum/Services/ProfundumEnrollmentService.cs | Switch notification dependency to the new app-level service. |
| Backend/Altafraner.AfraApp/Otium/Services/OtiumEndpointService.cs | Switch notification dependency to the new app-level service. |
| Backend/Altafraner.AfraApp/Otium/Services/EnrollmentService.cs | Switch notification dependency to the new app-level service. |
| Backend/Altafraner.AfraApp/Otium/Jobs/StudentMisbehaviourNotificationJob.cs | Switch notification dependency to the new app-level service. |
| Backend/Altafraner.AfraApp/Otium/Jobs/EnrollmentReminderJob.cs | Switch notification dependency to the new app-level service. |
| Backend/Altafraner.AfraApp/Notifications/Services/CompositeNotificationService.cs | Add composite delivery (in-app + conditional email). |
| Backend/Altafraner.AfraApp/Notifications/NotificationsModule.cs | Add module wiring for endpoints + notification service. |
| Backend/Altafraner.AfraApp/Notifications/Domain/DTO/PushSubscriptionDto.cs | DTO for push subscription registration/removal. |
| Backend/Altafraner.AfraApp/Notifications/Domain/DTO/NotificationSettingsDto.cs | DTO for user notification settings. |
| Backend/Altafraner.AfraApp/Notifications/Domain/DTO/NotificationDto.cs | DTO for returning notifications to the web client. |
| Backend/Altafraner.AfraApp/Notifications/Contracts/INotificationService.cs | Define app-level notification abstraction used by jobs/services. |
| Backend/Altafraner.AfraApp/Notifications/API/Endpoints/NotificationEndpoints.cs | Add notification CRUD/settings/push subscription/test endpoints. |
| Backend/Altafraner.AfraApp/Database/AfraAppContext.cs | Add EF sets + model config for notifications/subscriptions + email pref default. |
| Backend/Altafraner.AfraApp/appsettings.Development.json | Add VAPID config section for development. |
| Backend/Altafraner.AfraApp/Altafraner.AfraApp.csproj | Reference the new WebNotification Backbone project. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| async function checkPushSubscription() { | ||
| if (!pushHelper.isSupported()) return; | ||
| const reg = await navigator.serviceWorker.ready; | ||
| const sub = await reg.pushManager.getSubscription(); | ||
| pushSubscribed.value = !!sub; | ||
| } |
There was a problem hiding this comment.
checkPushSubscription() awaits navigator.serviceWorker.ready without any timeout/guard. When service workers are disabled (as noted in the PR description) or the SW never becomes ready, this promise may never resolve and the subscription check will hang indefinitely. Consider reusing getServiceWorkerRegistration() (with timeout) from pushNotifications.js here or adding a bounded wait + error handling.
| onMounted(async () => { | ||
| await store.fetchNotifications(); | ||
| store.connectHub(); | ||
| }); |
There was a problem hiding this comment.
store.fetchNotifications() can throw (the store only uses a try/finally), but the onMounted hook awaits it without a try/catch. A failed request will surface as an unhandled component error and the hub connection will never be started. Consider catching errors here (and/or having the store handle errors) so the UI fails gracefully and you can still attempt connectHub() or show a message.
| if (string.IsNullOrEmpty(cfg.PublicKey) || string.IsNullOrEmpty(cfg.PrivateKey)) | ||
| { | ||
| IsEnabled = false; | ||
| return; | ||
| } | ||
|
|
||
| byte[] pubKeyBytes; |
There was a problem hiding this comment.
WebPushSender enables push when only public/private keys are set, but _subject is allowed to fall back to an empty string (cfg.Subject ?? string.Empty). A missing subject will create invalid VAPID JWTs and cause delivery failures at runtime. Consider requiring Subject as part of the IsEnabled check (and/or logging a clear warning when push is disabled due to missing config).
| async function unsubscribe() { | ||
| if (!isSupported()) return; | ||
|
|
||
| const registration = await getServiceWorkerRegistration(); | ||
| const subscription = await registration.pushManager.getSubscription(); | ||
| if (!subscription) return; | ||
|
|
||
| await store.removePushSubscription(subscription); | ||
| await subscription.unsubscribe(); | ||
| } |
There was a problem hiding this comment.
unsubscribe() calls the backend first and only unsubscribes the browser subscription afterwards. If the API call fails (transient network/server error), the function exits before calling subscription.unsubscribe(), leaving the user still subscribed even though they tried to disable push. Consider unsubscribing in a finally (or doing browser unsubscribe first and then best-effort cleanup server-side with retry).
|
|
||
| var body = EncryptPayload(Encoding.UTF8.GetBytes(jsonPayload), uaPublicKey, authSecret); | ||
|
|
||
| var audience = GetAudience(endpoint); | ||
| var jwt = CreateVapidJwt(audience); | ||
|
|
||
| var client = _httpClientFactory.CreateClient("WebPush"); | ||
| using var request = new HttpRequestMessage(HttpMethod.Post, endpoint); | ||
|
|
||
| request.Headers.TryAddWithoutValidation("Authorization", $"vapid t={jwt},k={_publicKeyBase64Url}"); |
There was a problem hiding this comment.
SendAsync() decodes p256dhBase64Url / authBase64Url and passes them straight into EncryptPayload(), which slices uaPublicKey[1..33]/[33..65]. If the stored subscription keys are malformed (or intentionally crafted), this will throw (IndexOutOfRange) and can break notification delivery. Consider validating decoded key lengths (p256dh: 65 bytes starting with 0x04; auth: 16 bytes) and failing with a clear exception (or dropping the subscription).
43ef843 to
ec2b74b
Compare
bc3d737 to
736c25b
Compare
736c25b to
ea40f36
Compare
Features:
Issues:
not ready for review. sketchy rfc impl needs to be checked.Features 2: