You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The branch that turns the template into a browser-usable reactive service: ticket-auth realtime transports, a decoupled users/notifications/sample structure, embedded permission snapshots, user lifecycle broadcasts, and multi-instance correctness — recorded in ADRs 0001–0009.
What's in
Realtime auth & lifecycle
WebSocket chat auth via one-time tickets + session lifecycle — heartbeat, sweeper, 4401/4403 contract (ADR-0001), reference client in web/chat.html
SSE stream auth: originally ticket-based (ADR-0002), now bearer-only (ADR-0007); closeAll() on context close so streams die with the acceptor
Keycloak LOGOUT + admin events (RevokeUser directives) revoke live sessions on every adapter
com.softeno.ws.enabled toggle removes the whole WS feature when off (WsDisabledIT)
Notification port + ws/sse adapters (hexagon); listeners touch the port only
Permissions embedded in users as snapshots with fan-out on permission update/delete (UserPermissionSync) — reads never join
One UserEvent wire type with UserAction (CREATED/UPDATED/DELETED/SYNCED); events are cache-invalidation hints, not state transfer — no outbox by design
Multi-instance (ADR-0008/0009)
Per-instance Kafka groups for fan-out listeners (com.softeno.kafka.instance-id); consumer offset default latest
MongoTicketStore — atomic findAndRemove redeem + TTL expiry replaces the in-memory store
API surface
/coroutine/* REST (the /reactive/** flavors are gone), GraphQL, getPermissionsSize + GET /coroutine/permissionsCount for pagination
- split the monolithic sample/http/internal/IntegrationTest.kt into per-context
*IT files: ContextLoadsIT, app/permission/{PermissionApiIT, PermissionApiMockedIT,
PermissionGraphqlIT}, sample/http/external/ExternalControllerIT,
sample/proto/SampleGrpcServiceIT; WsTicketAuthIntegrationTest -> WsTicketAuthIT
- shared infra in testsupport/: BaseIntegrationTest (Testcontainers, cleanup),
R static random-data utility, awaitUntil, TestBearerWebFilter (moved, global)
- fixtures/abilities moved into their context packages: app/permission/
PermissionFixture (randomized, todo fixed), app/kafka/KeycloakEventFixture
(dedupes the event builder also used by WsTicketAuthIT),
sample/websocket/WsTicketAuthAbility (browser-like WS client harness,
ex-inner SocketHandle), security/WsTicketFixture, websocket/ChatConfigFixture,
external/ExternalApiAbility; OriginCheckTest split to its own file
- dynamic data via R across tests (subjects, ids, payloads, tickets);
new coverage: SampleControllerIT, SampleRouterIT, real-DB permission GET
- 62 tests green (was 53); AGENTS.md + README updated to the new test layout
- Move ticket infra to app/ticket/, add transport-bound tickets (TicketOrigin
WEBSOCKET/SSE) - a ticket minted for one transport cannot redeem on the
other; always-on idle sweeper in TicketStore.
- SSE: POST /sse-ticket mint, SseTicketRedeemFilter (redeem at the stream GET,
before any body bytes), SseSubscriptionRegistry with sub->streams reverse
index, token-ceiling sweeper (event: token_expired) and Keycloak LOGOUT ->
Kafka revocation (event: logged_out) for both transports. ADR-0002.
- Dev tools: rename chat/ to web/, served at /web/** by app/config/http/
WebToolConfig (unconditional, removal-safe); add web/sse.html - reference
frontend for the SSE client contract (re-mint on error, refresh + re-mint +
reconnect on token_expired, stop on logged_out). Verified end-to-end in
browsers via Playwright incl. cross-tab logout revocation.
- Tests: cache Spring contexts across IT classes (~90s -> ~40s suite),
tighten auth IT timings (1s heartbeat, 2s ceilings), harden SSE IT
teardown (cancelAndJoin + withTimeout - can fail, cannot hang).
…0003/0004)
- Three feature packages by responsibility: users (application core: db
documents, write path ends at UserEvent publish on Kafka tx), notifications
(optional feature: ws + sse adapters, shared ticket auth, kafka listeners),
sample (demos, websocket moved out); common/ + config/ infrastructure.
No app -> sample coupling points remain.
- Domain wrappers removed: User/Permission classes and toDomain/toDocument
gone; services work on db documents (UserWithPermissions is a read tuple).
REST/GraphQL wire contracts unchanged (verified live).
- Notifications hexagon (ADR-0004): one Notification port (broadcast +
revokeUser), WsNotification + SseNotification adapters; the kafka listeners
depend on the port only and fan out through every enabled adapter.
- ADR-0003 (structure + functional sequence diagrams) and ADR-0004 added;
path refs in ADR-0001/0002 corrected; README + AGENTS.md updated.
- Tests mirror the new packages; 86/86 green on three fresh full-suite runs;
live smoke: REST contract, GraphQL mutation/query shapes, SSE ticket
subscribe + USER_CREATED broadcast.
- KeycloakUserEvent.details nullable: real events carry "details":null (e.g.
LOGOUT from the account client); the non-null Kotlin param threw NPE and the
listener retry-burned the record, silently skipping logout revocation
- WebExceptionHandler keeps ResponseStatusException's real status (favicon 404
was rendered as 500 + ERROR storm); 4xx now logged at warn with E4XX
- regression test deserializes the exact real-world LOGOUT payload
per-request uri/query logging in ExampleWebFilter was unconditional at trace;
now property-gated and off by default (headers stay trace-only so credentials
never land in info logs)
admin-initiated session termination (ACTION users/{id}/logout) and user
deletion (DELETE users/{id}) fire no user-facing LOGOUT event and would
otherwise wait for the token-exp ceiling. new keycloak-admin-events
listener interprets every event into an AdminDirective (dumb wire DTO,
matching lives in AdminDirective.of) and executes it through the same
Notification port, so WS+SSE revoke identically (ADR-0004)
unit tests cover directive interpretation + a captured real logout
payload; WS/SSE ITs cover the end-to-end 4403/logged_out paths
…mongo tickets (ADR-0006-0009)
- users core: one UserEvent wire type with UserAction enum (CREATED/UPDATED/
DELETED/SYNCED); update/delete publish after the DB write; PermissionService
emits a single USERS_SYNCED after the snapshot fan-out (ADR-0006)
- notifications: listeners render per-action broadcasts through the
Notification port (SSE + WS adapters untouched); all fan-out groups are
per-instance via com.softeno.kafka.instance-id, consumer offset default
switched to latest (ADR-0008)
- SSE: bearer-only stream auth, ticket path and web/sse.html removed (ADR-0007);
closeAll() on ContextClosedEvent so streams die with the acceptor
- tickets: MongoTicketStore adapter (atomic findAndRemove + TTL expiry),
multi-instance since ADR-0009; ticket infra is WebSocket-only
- ws: com.softeno.ws.enabled on by default, false removes the whole feature
(contract pinned by WsDisabledIT)
- permissions: getPermissionsSize (GraphQL) + GET /coroutine/permissionsCount
(REST) for pagination; GraphQL propagation tests for permission update/delete
- config: env.template.properties gains MONGO_RS=rs0 (application.properties
resolves spring.mongodb.replica-set-name from it)
- docs: ADR-0006-0009, README/AGENTS/api-changes updated
Full suite: ./gradlew build green (24 classes, 119 tests, 0 failures)
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Unresolved critical issues expose credentials in logs, disable WebSocket token-expiry enforcement, and cause ticket-auth integration tests to fail without MongoDB on clean CI runners.
This PR adds browser-compatible realtime authentication and lifecycle notifications, separates users, notifications, and samples, and introduces shared coordination for multi-instance deployments.
…ow-up)
The MongoTicketStore made this Testcontainers-free test depend on a running
Mongo: green on dev machines (compose stack on :27017), 30 s server-selection
timeouts on CI. Serve the TicketStore port with FakeTicketStore via a nested
@primary @TestConfiguration (imported explicitly — nested configs are not
detected when @SpringBootTest declares classes=). Real filter/controller/
registry stay under test; the Mongo adapter's contract is TicketStoreIT.
Verified: suite green with the local Mongo stopped (CI parity) and full
./gradlew build green (24 classes, 119 tests).
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Unresolved critical issues allow expired JWT sessions to persist, expose credentials in logs, and let concurrent user edits abort permission synchronization or deletion cleanup.
The collector writes on Dispatchers.Default, but every caller supplies an ordinary mutableListOf that the test thread reads while polling and asserting. Those unsynchronized ArrayList reads/writes have no visibility guarantee, making the lifecycle tests racy and potentially timing out despite receiving the event. Use a concurrent collection such as CopyOnWriteArrayList for these sinks, or confine collection and assertions to the same dispatcher.
Review effort: Balanced
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The branch that turns the template into a browser-usable reactive service: ticket-auth realtime transports, a decoupled users/notifications/sample structure, embedded permission snapshots, user lifecycle broadcasts, and multi-instance correctness — recorded in ADRs 0001–0009.
What's in
Realtime auth & lifecycle
web/chat.htmlcloseAll()on context close so streams die with the acceptorRevokeUserdirectives) revoke live sessions on every adaptercom.softeno.ws.enabledtoggle removes the whole WS feature when off (WsDisabledIT)Structure & events (ADR-0003–0006)
users/core → Kafka → optionalnotifications/feature;sample/demos; no domain wrapper classesNotificationport + ws/sse adapters (hexagon); listeners touch the port onlyUserPermissionSync) — reads never joinUserEventwire type withUserAction(CREATED/UPDATED/DELETED/SYNCED); events are cache-invalidation hints, not state transfer — no outbox by designMulti-instance (ADR-0008/0009)
com.softeno.kafka.instance-id); consumer offset defaultlatestMongoTicketStore— atomicfindAndRemoveredeem + TTL expiry replaces the in-memory storeAPI surface
/coroutine/*REST (the/reactive/**flavors are gone), GraphQL,getPermissionsSize+GET /coroutine/permissionsCountfor paginationdocs/api-changes-vs-main.mdValidation
Full suite
./gradlew build: green — 24 classes / 119 tests, 0 failures (Testcontainers Kafka+Mongo, WireMock, WS/SSE ITs included).Refs: ADR-0001 … ADR-0009 in
docs/adr/