feat(push): add service-owned firebase channels and delivery

Add push contracts, provider delivery and channel diagnostics. Link email channels to services and document migration and validation.

Ref: IT-1074
This commit is contained in:
2026-10-24 12:31:35 +03:00
parent b30dba93e8
commit 1631786ce2
105 changed files with 3838 additions and 147 deletions
+168
View File
@@ -0,0 +1,168 @@
# 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.