Add push contracts, provider delivery and channel diagnostics. Link email channels to services and document migration and validation. Ref: IT-1074
12 KiB
Push channels and Android FCM — IT-1074
Notification Service owns Firebase configuration. Invemory owns installation/user bindings and notification preferences; it publishes a push request without Firebase credentials or OAuth access tokens. The public Contracts package must be delivered first; updating Invemory is a separate step.
Administration
The Push Channels menu opens /admin/push-channels. Create/Edit use the existing service catalog (ServiceId UUID), Firebase Project ID, service account JSON and Active. Fields use compact desktop widths and wrap on small screens. A service has one push channel, enforced by a unique ServiceId index. There is no priority/fallback or extra routing configuration. Inactive channels are retained but never selected for sending. Reassignment is allowed; service rename automatically updates the displayed name without breaking the FK.
Creating a channel requires a valid service-account JSON whose project_id matches the Firebase Project ID. The server validates its type, email and RSA private key without contacting Google. Editing with an empty credentials field preserves the stored value; changing the project requires replacement credentials. An inactive channel also requires valid credentials, so activation does not introduce a partly configured channel.
Safe list/edit models contain no secret. Edit does not load credentials into the browser, and validation failures clear both the model and attempted ModelState value before rendering. Credential parser/provider errors use fixed messages and never log the JSON/private key. The Worker constructs Google credentials from the validated service-account email/private key; supplied token_uri or other endpoint fields are not used. There is no ADC fallback.
Storage follows the existing SMTP approach: credentials are server-only JSONB in the Notification Service database, without new application-level encryption. Restrict access to this private administration app, its database and backups. Existing private-network access assumptions are unchanged; this feature adds no public administration endpoint.
Persistence and migration
Apply 20261024080457_AddPushChannels before opening Push Channels or enabling delivery. The additive migration creates push_channels with a required services.id FK using RESTRICT and unique service_id index. It does not alter existing email channels, settings or usage. Service deletion is blocked while either email or push channels still reference it; reassign a push channel before deleting its service. Saves reuse the service administration transaction lock to serialize service membership and duplicate-channel checks. Migration downgrade drops push channels and their credentials, so it is destructive after channels have been configured.
The existing DAL.EF/update-development-database.bat applies the schema to the database from the Web Development configuration; it does not provision containers or restart Web/Worker.
Worker and contracts
Set FcmPush__Enabled=true to register the push consumer. Firebase project/credentials and service routing come from the active channel, not environment settings or producer payloads. The previous draft FcmPush__ServiceName host filter is removed: each request's exact current ServiceName resolves to services.id and then that service's active push channel. Missing services, missing/inactive channels and invalid stored configuration produce safe terminal failure results without contacting Firebase. No legacy service-name alias is used at runtime.
SendPushMessageData retains registration/binding/user identity, ServiceName, device Token and TokenHash, title/body, version, /home route and ExpiresAt. FcmProjectId, FcmAccessToken and FcmAccessTokenExpiresAt have been removed. SupportedMessageType remains public in Contracts; the existing Notification.SendPush.v1 header and notification.send-push queue are unchanged. OAuth tokens are obtained by the Firebase SDK inside Notification Service from the channel's service account. They are never included in RabbitMQ request/result contracts.
This replaces the unpublished draft that obtained OAuth authorization in Invemory. Source clients still initializing the removed properties must update after the new Contracts package is explicitly published. Old payloads containing additional JSON fields are not a configuration source; the server ignores those fields and still requires its own active channel. Do not resume producers that send Firebase secrets until that separate Invemory update removes them. No Invemory source or production configuration is changed by this task.
The Worker sends bounded version-1 data-only messages with /home routes and a maximum 15-minute expiry, preserving Android binding checks before display. Delivery outcomes are persisted by service/message ID under an advisory lock and replayed after reply failures. A crash after FCM acceptance but before DB commit may still repeat delivery; this is not exactly-once delivery. Expired messages are not sent.
UNREGISTERED invalidates only the matching registration/binding/token-hash. Sender mismatch, invalid argument and authorization/configuration failures do not invalidate device tokens. Transient delivery uses the existing three attempts, exponential delay, jitter and Retry-After. Outcomes expire after 30 days; existing outcome/retry metrics contain no target/token labels. Firebase acceptance does not guarantee Android display (force-stop, Doze and connectivity apply).
Validation boundaries
Tests cover the administration lifecycle, secret non-disclosure, unchanged-secret edits, service reassignment/rename, inactive selection, duplicate/missing services, project changes, worker name-to-ID resolution, server credential selection and credential-free Contracts. All provider routing tests use a fake Firebase client. Real Firebase delivery, credential permissions and Android notification display require a separate end-to-end verification with an authorized configured project/device; fake local credentials cannot prove those.
On 2026-10-24 the Release solution build and targeted tests passed (21 unit and 24 integration). Rider inspection reported no errors in the changed production classes; the existing Firebase SDK Token deprecation warning remains. Structural review kept changed methods within cognitive complexity 10; the tool did not report numeric scores. The review compared email-channel mapping/editor patterns, service catalog controllers, transaction locking and existing push delivery/retry behavior. No unresolved blocking findings remained for the implemented scope.
Desktop browser validation used a disposable database and Web at 127.0.0.1:52658, with an offline-generated test service account (no Google account or network send): empty list, create, invalid JSON feedback/secret clearing, edit without secret disclosure, and Active off with credentials preserved all passed. Mobile wrapping was inspected in CSS but not exercised on an Android device. The temporary Web and database container were removed after validation. The owner-applied AddPushChannels migration was separately confirmed in the existing development database; it was not reapplied.
Channel tests — 2026-10-24
The editor has separate Check Firebase authorization and Send test push actions, following the current-form SMTP test workflow. Neither action saves a channel. New channels use the entered service-account JSON; blank JSON on an existing channel uses its stored credentials only if the project is unchanged. Inactive channels can be tested explicitly. Both POST endpoints require the form antiforgery token and bounded request bodies. The existing private-network/port-forward access boundary is retained: this Web application has no in-app authentication or administrator-role middleware. Do not expose it publicly.
Authorization requests an OAuth access token with the firebase.messaging scope and a 30-second timeout. It neither requires a recipient nor calls FCM send. A successful OAuth exchange proves service-account authentication only, not FCM API enablement, send permission or device delivery. The access token is never returned, saved or logged.
Send opens a separate Recipient token field and sends a neutral notification message through the same FirebasePushClient transport using current form credentials. RecipientToken is not part of the save model or channel storage. The dialog clears it after the attempt or close. Only test on an explicitly supplied, owned test-device token. Provider acceptance is shown as acceptance, never as verified delivery/display. This direct provider diagnostic does not exercise Invemory's data-only binding, expiry/tap checks, transactional Outbox or result queue; it does not replace that separate end-to-end validation. Foreground display depends on the receiving app. No user inventory or account data is included.
Errors are fixed, safe messages; no provider exception, credentials, access token or recipient is echoed. The UI uses textContent for results. Saved credentials never populate the editor.
Channel-test verification
Release solution build passed. 29 focused push unit tests and 9 PushChannelAdmin integration tests passed, including CSRF rejection, missing credentials, unchanged/changed project reuse, unsaved credentials, separate authorization/send calls, transient errors and no persistence. Rider's final changed-file error inspection was clean. Existing FirebaseAdmin Message.Token obsolescence and Microsoft.OpenApi vulnerability warnings remain outside this task. Cognitive complexity was manually reviewed, not numerically measured.
A temporary local Web instance on 127.0.0.1:52658 verified both buttons, modal recipient input, empty-token validation, missing-credential response and clearing the recipient after a failed attempt. Only a fake non-deliverable token with missing credentials was submitted; the request stopped before Firebase. No channel was created or modified by this UI verification. Code review compared current SMTP test behavior, existing credential parser/provider, Razor editor layout, antiforgery and repository interfaces. Fixed script initialization to wait until footer buttons exist and rejected malformed/oversized test requests safely. The final review found no further actionable issue in these two actions.
The owner subsequently configured the local channel, verified OAuth authorization and sent a real test to the current Samsung token on 2026-10-24. The owner confirmed notification display and opening the installed application on tap. This validates the direct Firebase notification test, not the Invemory data-only/outbox flow. The installed receiver intentionally validates data-only binding fields; a notification-only test received while Invemory is in the foreground does not satisfy that contract. Background notification display is handled by the Firebase SDK. No additional diagnostic messages were sent by the agent.
The authorization button and its result are directly beneath the service-account JSON field. The recipient input and Send test push button remain in the separate test dialog.
Delivery verification: the full Release solution build and all 221 tests passed (67 unit, 154 integration). The pre-existing Microsoft.OpenApi 2.0.0 vulnerability warning remains. Delivery is restricted to Notification Service; Invemory producer/client migration and its runtime validation remain separate pending work. TeamCity main triggers include general checks, image publication, Contracts publication and production deployment. The owner explicitly authorized this automatic production consequence for the Notification Service delivery on 2026-10-24. It does not authorize delivery of the remaining Invemory repositories.