Skip to content

Add WebNotifications Module for push notifications in browser.#230

Open
JohannHoepfner wants to merge 9 commits into
mainfrom
web-notifications
Open

Add WebNotifications Module for push notifications in browser.#230
JohannHoepfner wants to merge 9 commits into
mainfrom
web-notifications

Conversation

@JohannHoepfner
Copy link
Copy Markdown
Member

@JohannHoepfner JohannHoepfner commented Mar 9, 2026

  • needs to be tested with service worker enabled (off by default in vite dev)

Features:

  • Allows enrolling browser in push notifications to browser.
  • Adds ugly notification center into web ui
  • Allows disabling email notifications targeted at push notif users.

Issues:

  • not ready for review. sketchy rfc impl needs to be checked.

Features 2:

  • yes I am rolling my own crypto. no there is not a library for that afaIk.

@JohannHoepfner JohannHoepfner self-assigned this Mar 9, 2026
@JohannHoepfner JohannHoepfner changed the title [WIP] Add WebNotifications Module for push and fetch [WIP] Add WebNotifications Module for push and fetch based notifications in browser. Mar 9, 2026
@JohannHoepfner JohannHoepfner changed the title [WIP] Add WebNotifications Module for push and fetch based notifications in browser. [WIP] Add WebNotifications Module for push and poll based notifications in browser. Mar 9, 2026
@JohannHoepfner JohannHoepfner changed the title [WIP] Add WebNotifications Module for push and poll based notifications in browser. [WIP] Add WebNotifications Module for push and polling based notifications in browser. Mar 9, 2026
@JohannHoepfner JohannHoepfner changed the title [WIP] Add WebNotifications Module for push and polling based notifications in browser. [WIP] Add WebNotifications Module for push notifications in browser. Apr 2, 2026
@JohannHoepfner JohannHoepfner force-pushed the web-notifications branch 2 times, most recently from 4db1f7f to f986ab9 Compare April 2, 2026 20:57
@JohannHoepfner JohannHoepfner changed the title [WIP] Add WebNotifications Module for push notifications in browser. Add WebNotifications Module for push notifications in browser. Apr 2, 2026
@JohannHoepfner JohannHoepfner marked this pull request as ready for review April 2, 2026 20:57
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.WebNotification module (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 IEmailNotificationService and 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.

Comment thread WebClient/src/views/Settings.vue
Comment on lines +131 to +136
async function checkPushSubscription() {
if (!pushHelper.isSupported()) return;
const reg = await navigator.serviceWorker.ready;
const sub = await reg.pushManager.getSubscription();
pushSubscribed.value = !!sub;
}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +41
onMounted(async () => {
await store.fetchNotifications();
store.connectHub();
});
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread Backend/Altafraner.AfraApp/Notifications/Services/CompositeNotificationService.cs Outdated
Comment on lines +45 to +51
if (string.IsNullOrEmpty(cfg.PublicKey) || string.IsNullOrEmpty(cfg.PrivateKey))
{
IsEnabled = false;
return;
}

byte[] pubKeyBytes;
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +48
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();
}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +104 to +113

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}");
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
@JohannHoepfner JohannHoepfner force-pushed the web-notifications branch 2 times, most recently from 43ef843 to ec2b74b Compare April 2, 2026 21:18
Comment thread Backend/Altafraner.Backbone.WebNotification/Services/InAppNotificationService.cs Dismissed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants