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:
@@ -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.
|
||||
Reference in New Issue
Block a user