Skip to content

feat: realtime auth + lifecycle events, hexagon structure, multi-instance fanout (ADR-0001-0009) - #3

Merged
xcodeassociated merged 11 commits into
mainfrom
feat/ws-ticket-auth
Nov 7, 2026
Merged

xcodeassociated merged 11 commits into
mainfrom
feat/ws-ticket-auth

Conversation

@xcodeassociated

Copy link
Copy Markdown
Owner

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

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

Structure & events (ADR-0003–0006)

  • users/ core → Kafka → optional notifications/ feature; sample/ demos; no domain wrapper classes
  • 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
  • Frontend integration contract: docs/api-changes-vs-main.md

Validation

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/

- POST /ws-ticket mints 256-bit single-use tickets (SHA-256-keyed store, 10s TTL);
  redeem at /ws upgrade pre-handshake: atomic compare-and-delete, mint-time claims
  into the security context; 401 replay/expired, 403 foreign Origin; /ws/** and
  /ws-ticket now .authenticated() instead of header ROLE_ADMIN
- sessions bind {sub,sid,exp} with sub->sessions reverse index; server stamps
  `from`; sweeper enforces exp ceiling (REAUTH_REQUIRED/4401) + stale pongs;
  Kafka LOGOUT closes sessions (LOGGED_OUT/4403); graceful shutdown closes 1001
- remove reactive WS variant, coroutines end-to-end (suspend mint endpoint);
  reorganize sample/websocket into api/config/security; MDC merged with OTel
  trace context per session
- chat/chat.html dev page at /chat/, http/ws.http, ADR-0001; README/AGENTS updated
- 53 tests incl. Testcontainers-free WS integration suite; live-verified with two
  browser users via Keycloak and ijhttp CLI
- delete /reactive/** user + permission REST controllers
- delete ReactiveUserUpdateEmitter and /update/user/reactive SSE
- convert sample/http internal demo to coroutine service + controller;
  router bridges suspend calls with mono { MDCContext() } to keep MDC
- move PermissionsReactiveMongoTemplate to app/permission/db (infra, kept)
- drop /reactive/** from SecurityConfig; convert reactive tests to coroutine
- README + AGENTS.md updated to coroutine-first wording
- 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.
… (ADR-0005)

- drop User/Permission domain wrappers; UserService works on db documents
- PermissionSnapshot embedded in UserDocument, kept fresh by UserPermissionSync
  fan-out on permission update/delete
- Kotlin-conventional file/class renames (UserController, TestSecurityConfig, …)
- UserCreatedEvent typed payload; listener renders USER_CREATED: <id>
- config/graphql exception handler + sample external graphql client config moves
- Kover coverage, 12 new tests (UserApiIT, converter, propagation, fallback)
- docs: ADR-0005, api-changes-vs-main + frontend-update-prompt handoffs
- 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)
Copilot AI balanced review requested due to automatic review settings November 7, 2026 12:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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.

7 open issues
What changed in this PR

This PR adds browser-compatible realtime authentication and lifecycle notifications, separates users, notifications, and samples, and introduces shared coordination for multi-instance deployments.

Changes:

  • Adds WebSocket tickets, bearer-authenticated SSE, session lifecycle controls, and Keycloak-driven revocation.
  • Introduces embedded permission snapshots, typed user lifecycle events, notification adapters, and per-instance Kafka fan-out.
  • Updates coroutine APIs, permission counts, tests, configuration, examples, architecture records, and coverage reporting.

This pull request requires changes. Critical findings concern WebSocket token-expiry enforcement, credential logging, and Mongo-dependent test wiring. Additional findings cover ineffective Origin configuration, interrupted permission propagation, incorrect HTTP log levels, SSE shutdown assertions and test synchronization, nonconcurrent ticket-redemption coverage, and gRPC channel cleanup.

File Description
src/​test/​resources/​application.properties Configures realtime tests and Kafka deserialization.
src/​test/​kotlin/​com/​softeno/​template/​users/​permission/​PermissionFixture.kt Adds randomized permission fixtures.
src/​test/​kotlin/​com/​softeno/​template/​users/​permission/​PermissionApiMockedIT.kt Tests REST responses with a mocked repository.
src/​test/​kotlin/​com/​softeno/​template/​users/​permission/​PermissionApiIT.kt Tests permission operations and counts.
src/​test/​kotlin/​com/​softeno/​template/​testsupport/​TestBearerWebFilter.kt Supplies simulated JWT authentication for tests.
src/​test/​kotlin/​com/​softeno/​template/​testsupport/​R.kt Centralizes randomized test-data generation.
src/​test/​kotlin/​com/​softeno/​template/​testsupport/​BaseIntegrationTest.kt Shares application and Testcontainers setup.
src/​test/​kotlin/​com/​softeno/​template/​testsupport/​Await.kt Adds bounded asynchronous assertion polling.
src/​test/​kotlin/​com/​softeno/​template/​sample/​proto/​SampleGrpcServiceIT.kt Tests unary and streaming gRPC samples.
src/​test/​kotlin/​com/​softeno/​template/​sample/​http/​internal/​router/​SampleRouterIT.kt Tests functional HTTP routes.
src/​test/​kotlin/​com/​softeno/​template/​sample/​http/​internal/​coroutine/​SampleControllerIT.kt Tests coroutine sample endpoints.
src/​test/​kotlin/​com/​softeno/​template/​sample/​http/​external/​ExternalControllerIT.kt Tests outbound requests and fallback behavior.
src/​test/​kotlin/​com/​softeno/​template/​sample/​http/​external/​ExternalApiAbility.kt Provides external-service WireMock stubs.
src/​test/​kotlin/​com/​softeno/​template/​notifications/​ws/​WsTicketAuthAbility.kt Adds ticket-authenticated WebSocket test helpers.
src/​test/​kotlin/​com/​softeno/​template/​notifications/​ws/​WsDisabledIT.kt Tests WebSocket feature removal when disabled.
src/​test/​kotlin/​com/​softeno/​template/​notifications/​ws/​SessionMdcTest.kt Tests session MDC propagation.
src/​test/​kotlin/​com/​softeno/​template/​notifications/​ws/​security/​WsTicketRedeemFilterTest.kt Tests ticket redemption and Origin checks.
src/​test/​kotlin/​com/​softeno/​template/​notifications/​ws/​ChatConfigFixture.kt Supplies WebSocket configuration fixtures.
src/​test/​kotlin/​com/​softeno/​template/​notifications/​ticket/​TicketStoreIT.kt Tests Mongo ticket redemption and TTL indexing.
src/​test/​kotlin/​com/​softeno/​template/​notifications/​ticket/​TicketFixture.kt Supplies ticket-identity fixtures.
src/​test/​kotlin/​com/​softeno/​template/​notifications/​ticket/​TicketAuthenticationTest.kt Tests authentication reconstruction from tickets.
src/​test/​kotlin/​com/​softeno/​template/​notifications/​ticket/​OriginCheckTest.kt Tests exact-match Origin validation.
src/​test/​kotlin/​com/​softeno/​template/​notifications/​ticket/​FakeTicketStore.kt Adds an in-memory ticket-store test double.
src/​test/​kotlin/​com/​softeno/​template/​notifications/​sse/​SseSubscriptionRegistryTest.kt Tests SSE registration, revocation, expiry, and broadcasts.
src/​test/​kotlin/​com/​softeno/​template/​notifications/​sse/​SseShutdownIT.kt Tests subscription closure on context shutdown.
src/​test/​kotlin/​com/​softeno/​template/​notifications/​kafka/​UserEventsListenerTest.kt Tests lifecycle-event rendering and adapter fan-out.
src/​test/​kotlin/​com/​softeno/​template/​notifications/​kafka/​KeycloakEventFixture.kt Supplies Keycloak event fixtures.
src/​test/​kotlin/​com/​softeno/​template/​notifications/​kafka/​KafkaKeycloakControllerTest.kt Tests logout-driven transport revocation.
src/​test/​kotlin/​com/​softeno/​template/​notifications/​kafka/​KafkaKeycloakAdminControllerTest.kt Tests admin-event interpretation and revocation.
src/​test/​kotlin/​com/​softeno/​template/​fixture/​SampleFixture.kt Removes the fixed permission fixture.
src/​test/​kotlin/​com/​softeno/​template/​ContextLoadsIT.kt Adds startup and listener-identity checks.
src/​test/​kotlin/​com/​softeno/​template/​config/​security/​TestSecurityConfig.kt Relocates and renames test security configuration.
src/​test/​kotlin/​com/​softeno/​template/​config/​security/​SecurityJwtConvertersTest.kt Tests production JWT converters.
src/​test/​kotlin/​com/​softeno/​template/​app/​user/​api/​GraphQl.kt Removes legacy test GraphQL client configuration.
src/​main/​resources/​logback-spring.xml Adds session IDs to console logging.
src/​main/​resources/​graphql/​schema.graphqls Adds the permission-count query.
src/​main/​resources/​config/​env.template.properties Adds replica-set and admin-topic settings.
src/​main/​resources/​application.properties Configures fan-out identity and realtime lifecycles.
src/​main/​kotlin/​com/​softeno/​template/​users/​UserService.kt Implements document-based CRUD and lifecycle publication.
src/​main/​kotlin/​com/​softeno/​template/​users/​UserModifyCommand.kt Separates user write inputs.
src/​main/​kotlin/​com/​softeno/​template/​users/​UserEvents.kt Defines typed lifecycle events and publication.
src/​main/​kotlin/​com/​softeno/​template/​users/​permission/​PermissionService.kt Propagates permission changes before completion broadcasts.
src/​main/​kotlin/​com/​softeno/​template/​users/​permission/​PermissionModifyCommand.kt Separates permission input and document conversion.
src/​main/​kotlin/​com/​softeno/​template/​users/​permission/​db/​PermissionsReactiveMongoTemplate.kt Provides version-checked permission updates.
src/​main/​kotlin/​com/​softeno/​template/​users/​permission/​db/​PermissionRepository.kt Separates the coroutine permission repository.
src/​main/​kotlin/​com/​softeno/​template/​users/​permission/​db/​PermissionDocument.kt Relocates permission persistence and extracts repositories.
src/​main/​kotlin/​com/​softeno/​template/​users/​permission/​api/​PermissionGraphqlController.kt Adds document-based permission GraphQL mappings.
src/​main/​kotlin/​com/​softeno/​template/​users/​permission/​api/​PermissionExceptions.kt Relocates permission exceptions.
src/​main/​kotlin/​com/​softeno/​template/​users/​permission/​api/​PermissionDto.kt Separates permission REST DTO mapping.
src/​main/​kotlin/​com/​softeno/​template/​users/​permission/​api/​PermissionController.kt Updates permission REST wiring and adds counts.
src/​main/​kotlin/​com/​softeno/​template/​users/​db/​UserRepository.kt Adds coroutine access and snapshot-holder queries.
src/​main/​kotlin/​com/​softeno/​template/​users/​db/​UserPermissionSync.kt Propagates embedded permission updates and removals.
src/​main/​kotlin/​com/​softeno/​template/​users/​db/​UserDocument.kt Introduces embedded permission snapshots.
src/​main/​kotlin/​com/​softeno/​template/​users/​api/​UserGraphqlController.kt Maps GraphQL directly to the users core.
src/​main/​kotlin/​com/​softeno/​template/​users/​api/​UserExceptions.kt Relocates user exceptions.
src/​main/​kotlin/​com/​softeno/​template/​users/​api/​UserDto.kt Maps user documents and snapshots to REST DTOs.
src/​main/​kotlin/​com/​softeno/​template/​users/​api/​UserController.kt Provides coroutine user REST endpoints.
src/​main/​kotlin/​com/​softeno/​template/​sample/​http/​internal/​SampleService.kt Consolidates the coroutine HTTP sample service.
src/​main/​kotlin/​com/​softeno/​template/​sample/​http/​internal/​router/​Router.kt Bridges functional routes to suspend services.
src/​main/​kotlin/​com/​softeno/​template/​sample/​http/​internal/​reactive/​SampleController.kt Removes the parallel reactive HTTP implementation.
src/​main/​kotlin/​com/​softeno/​template/​sample/​http/​internal/​ExampleWebFilter.kt Moves optional request logging into the sample feature.
src/​main/​kotlin/​com/​softeno/​template/​sample/​http/​internal/​coroutine/​SampleController.kt Adds coroutine annotated sample endpoints.
src/​main/​kotlin/​com/​softeno/​template/​sample/​http/​external/​coroutine/​ExternalController.kt Updates DTO imports and coroutine DELETE handling.
src/​main/​kotlin/​com/​softeno/​template/​sample/​http/​external/​config/​ExternalGraphqlClientConfig.kt Moves outbound GraphQL configuration into samples.
src/​main/​kotlin/​com/​softeno/​template/​notifications/​ws/​WsNotification.kt Implements the WebSocket notification adapter.
src/​main/​kotlin/​com/​softeno/​template/​notifications/​ws/​WsChatCommon.kt Defines WebSocket lifecycle controls and configuration.
src/​main/​kotlin/​com/​softeno/​template/​notifications/​ws/​security/​WsTicketRedeemFilter.kt Authenticates upgrades through single-use tickets.
src/​main/​kotlin/​com/​softeno/​template/​notifications/​ws/​security/​SessionBinding.kt Extracts handshake-bound identity and token expiry.
src/​main/​kotlin/​com/​softeno/​template/​notifications/​ws/​api/​WsTicketController.kt Mints authenticated WebSocket tickets.
src/​main/​kotlin/​com/​softeno/​template/​notifications/​ticket/​TicketStore.kt Defines the shared ticket-store contract.
src/​main/​kotlin/​com/​softeno/​template/​notifications/​ticket/​Ticket.kt Defines ticket claims, configuration, and authentication helpers.
src/​main/​kotlin/​com/​softeno/​template/​notifications/​ticket/​MongoTicketStore.kt Adds shared atomic ticket redemption.
src/​main/​kotlin/​com/​softeno/​template/​notifications/​ticket/​db/​TicketDocument.kt Defines hashed ticket records with TTL expiry.
src/​main/​kotlin/​com/​softeno/​template/​notifications/​sse/​SseNotification.kt Implements the SSE notification adapter.
src/​main/​kotlin/​com/​softeno/​template/​notifications/​sse/​Service.kt Adds subscription-bound SSE events and heartbeats.
src/​main/​kotlin/​com/​softeno/​template/​notifications/​sse/​Controllers.kt Binds bearer identity to SSE subscriptions.
src/​main/​kotlin/​com/​softeno/​template/​notifications/​Notification.kt Defines transport-neutral broadcast and revocation operations.
src/​main/​kotlin/​com/​softeno/​template/​notifications/​Message.kt Defines the shared notification envelope.
src/​main/​kotlin/​com/​softeno/​template/​notifications/​kafka/​UserEventsListener.kt Renders lifecycle events through notification adapters.
src/​main/​kotlin/​com/​softeno/​template/​notifications/​kafka/​KafkaKeycloakController.kt Fans logout revocation out per instance.
src/​main/​kotlin/​com/​softeno/​template/​notifications/​kafka/​KafkaKeycloakAdminController.kt Interprets admin events into revocation directives.
src/​main/​kotlin/​com/​softeno/​template/​config/​security/​SecurityConfig.kt Updates authorization rules for realtime endpoints.
src/​main/​kotlin/​com/​softeno/​template/​config/​security/​ReactiveMongoAuditorConfig.kt Relocates Mongo auditing configuration.
src/​main/​kotlin/​com/​softeno/​template/​config/​opentelemetry/​OpenTelemetryConfig.kt Relocates telemetry configuration.
src/​main/​kotlin/​com/​softeno/​template/​config/​kafka/​KafkaConfig.kt Rewires Kafka production to typed user events.
src/​main/​kotlin/​com/​softeno/​template/​config/​kafka/​KafkaApplicationProperties.kt Separates Kafka topic configuration.
src/​main/​kotlin/​com/​softeno/​template/​config/​http/​WebToolConfig.kt Serves static browser development tools.
src/​main/​kotlin/​com/​softeno/​template/​config/​http/​WebExceptionHandler.kt Preserves framework statuses and redacts ticket messages.
src/​main/​kotlin/​com/​softeno/​template/​config/​graphql/​GraphqlExceptionHandler.kt Separates global GraphQL exception handling.
src/​main/​kotlin/​com/​softeno/​template/​common/​Utils.kt Relocates shared pagination utilities.
src/​main/​kotlin/​com/​softeno/​template/​common/​ErrorFactory.kt Updates shared exception imports.
src/​main/​kotlin/​com/​softeno/​template/​common/​BaseException.kt Relocates the shared exception base.
src/​main/​kotlin/​com/​softeno/​template/​common/​BaseDocument.kt Relocates the shared document contract.
src/​main/​kotlin/​com/​softeno/​template/​app/​user/​UserService.kt Removes the legacy join-based users implementation.
src/​main/​kotlin/​com/​softeno/​template/​app/​user/​User.kt Removes the user domain wrapper.
src/​main/​kotlin/​com/​softeno/​template/​app/​user/​notification/​Service.kt Removes core-coupled SSE emitter implementations.
src/​main/​kotlin/​com/​softeno/​template/​app/​user/​notification/​Controllers.kt Removes legacy SSE controllers and reactive routes.
src/​main/​kotlin/​com/​softeno/​template/​app/​user/​db/​UserDocument.kt Removes the reference-based user persistence model.
src/​main/​kotlin/​com/​softeno/​template/​app/​user/​api/​reactive/​ReactiveUsers.kt Removes the parallel reactive user API.
src/​main/​kotlin/​com/​softeno/​template/​app/​user/​api/​GraphqlController.kt Removes joined GraphQL mappings and client coupling.
src/​main/​kotlin/​com/​softeno/​template/​app/​user/​api/​Controller.kt Removes legacy user REST and logging classes.
src/​main/​kotlin/​com/​softeno/​template/​app/​permission/​PermissionService.kt Removes the domain-wrapper permission service.
src/​main/​kotlin/​com/​softeno/​template/​app/​permission/​Permission.kt Removes the permission domain wrapper.
src/​main/​kotlin/​com/​softeno/​template/​app/​permission/​api/​reactive/​ReactivePermission.kt Removes the parallel reactive permission API.
src/​main/​kotlin/​com/​softeno/​template/​app/​permission/​api/​GraphqlController.kt Removes legacy permission GraphQL configuration.
src/​main/​kotlin/​com/​softeno/​template/​app/​kafka/​KafkaSampleHandler.kt Removes transport-coupled sample event handling.
src/​main/​kotlin/​com/​softeno/​template/​app/​kafka/​KafkaKeycloakController.kt Removes the legacy informational Keycloak listener.
src/​main/​kotlin/​com/​softeno/​template/​app/​common/​PrincipalHandler.kt Removes principal-debugging helpers.
http/​ws.http Adds ticket-authenticated WebSocket examples.
http/​user-app.http Adds permission counts and GraphQL CRUD examples.
http/​update.http Updates SSE examples to bearer authentication.
docs/​adr/​0009-mongo-backed-tickets.md Records shared Mongo-backed ticket storage.
docs/​adr/​0008-multi-instance-notification-fanout.md Records per-instance notification consumer groups.
docs/​adr/​0007-sse-bearer-fetch-auth.md Records bearer-only SSE authentication.
docs/​adr/​0006-user-lifecycle-events-broadcasts.md Records typed user lifecycle broadcasts.
docs/​adr/​0005-embedded-permission-snapshots-fanout.md Records embedded snapshots and propagation tradeoffs.
docs/​adr/​0004-notifications-hexagon-port-adapters.md Records the notification port-and-adapter boundary.
docs/​adr/​0002-sse-ticket-auth-stream-lifecycle.md Records the superseded SSE ticket design.
build.gradle Adds Kover coverage reporting and exclusions.
.gitignore Updates Kotlin and browser-tool artifact exclusions.

Review effort: Balanced

Comment on lines +42 to +44
mongoTemplate.insert(
TicketDocument(
id = hash(ticket),

@ConfigurationProperties(value = "com.softeno.ws")
data class ChatConfigProperties(
val staleCheck: Boolean = true,
val safeQuery = queryParams.filterKeys { it != "ticket" }
log.info("Incoming request uri: ${uri?.run { "$scheme://$host$port$path" }}")
log.info("Incoming request queryParams: $safeQuery")
log.trace("Incoming request headers: $headers")
// save (not saveAll): coroutine saveAll does not apply version-checked
// updates for existing documents — verified empirically (Spring Data Mongo 5.1)
affected.forEach { user ->
userRepository.save(user.copy(permissions = user.permissions.map { if (it.permissionId == snapshot.permissionId) snapshot else it }.toSet()))
Comment on lines +41 to +44
val terminal = runCatching {
withTimeout(2000) { sub.channel.receiveCatching() }
}.getOrNull()
assertTrue(terminal == null || terminal.isClosed, "channel must be closed on shutdown")

runBlocking {
List(16) {
async { if (ticketStore.redeem(ticket, TicketOrigin.WEBSOCKET).block() != null) winners.incrementAndGet() }
Comment on lines +25 to +35
private lateinit var testStub: SampleGrpcServiceGrpcKt.SampleGrpcServiceCoroutineStub

@BeforeEach
fun setup() {
val channel = ManagedChannelBuilder
.forAddress("localhost", port)
.usePlaintext()
.build()

testStub = SampleGrpcServiceGrpcKt.SampleGrpcServiceCoroutineStub(channel)
}
…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).
Copilot AI balanced review requested due to automatic review settings November 7, 2026 12:35
@xcodeassociated
xcodeassociated merged commit ecf75bd into main Nov 7, 2026
3 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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.

7 open issues
1 previously missed issue

In code that hasn't changed since last review

Medium severity Use a thread-safe collection for asynchronously collected events

src/​test/​kotlin/​com/​softeno/​template/​notifications/​sse/​SseSubscriptionRegistryTest.kt:41

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

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