docs: rewrite README into docs/, fix documented run paths, Keycloak stack, and two demo bugs - #85
Merged
Merged
Conversation
Stage 1 builds the boot jar with the Gradle wrapper on eclipse-temurin:21-jdk-jammy, so 'docker compose build' no longer needs a jar in build/libs from a prior host build. Stage 2 is unchanged apart from the JRE 21 base. Add .dockerignore so the context stays small and the gitignored application-local.yml can never reach an image.
runtimeOnly extended developmentOnly, which put spring-boot-docker-compose into the boot jar. The packaged app then ran the Docker Compose lifecycle, and with a compose file configured it failed at startup with "'files' content [compose.dev.yaml] must exist". bootRun still gets the configuration on its classpath explicitly.
- application.yml: default spring.docker.compose.file to compose.dev.yaml, so bootRun starts a matching MariaDB instead of the full compose.yaml app stack. Previously this lived only in the gitignored application-local.yml. - application.yml: add /actuator/health to unprotectedURIs, so the Dockerfile and compose healthchecks can reach it. Other actuator endpoints stay behind login. - application-local.yml-example: replace the leftover mymariadb/mydb datasource values with the compose.dev.yaml ones, and add the sample-event seed block (data-local.sql). - compose.yaml: skip verification email in the demo stack (the mail container cannot reach real inboxes) and align the app healthcheck with the keycloak stack. - docker-compose-keycloak.yml: drop the obsolete version key, and use wget for the app healthcheck since the JRE image has no curl.
Leftover debug code: on the local and dev profiles it logged every DemoUserProfile row at startup. Nothing references it.
DemoSessionProfile was annotated only @component. Spring's @scope is not inherited from BaseSessionProfile, so the bean was a singleton shared by every HTTP session and one user's profile leaked to all other users. Use the framework's @SessionScopedProfile meta-annotation instead, and add a context test that asserts the scopedTarget bean definition has session scope and that the injected reference is a scoped proxy. Also repoints the DomainRegistrationGuard Javadoc link at the framework's docs/REGISTRATION-GUARD.md, which moved out of the repository root.
The admin actions page posted {email} to /admin/lockAccount and
/admin/unlockAccount, but no handler existed in the demo or the framework,
so both actions always failed. Add AdminAPIController with both POST
endpoints, guarded by ADMIN_PRIVILEGE like the page itself. Lock sets
locked and lockedDate; unlock clears both and resets the failed login
counter, matching what LoginAttemptService does when a lockout expires.
Every outcome returns a JSONResponse body so the page's fetch() can read
messages[0].
The stack could not complete an OIDC login. Four independent breakages:
- The realm export declared realm `master`. Keycloak creates the master realm
before `--import-realm` runs and the import strategy is IGNORE_EXISTING, so
the export (and the ds-spring-user-framework-demo client with it) was skipped
with only an INFO line. Retargeted the export to a `demo` realm, which does
not exist yet and therefore imports. Dropped the master-only pieces that
cannot be imported elsewhere (the master-realm client and its roles, the
admin/create-realm realm roles) and the client's authorization-services
settings, whose JS policy import is rejected by default.
- The client secret in the export was the masked `**********` and did not match
keycloak.env. Set it to the env value.
- The realm had no user with an email claim, which the framework's OIDC user
service requires. Added `demo` / `demo` with demo@example.com.
- All provider URIs pointed at keycloak:8080, which a host browser cannot
resolve. Only the authorization endpoint is browser-facing, so it now uses
the published localhost:8180; token, userinfo and JWK stay on the compose
network. KC_HOSTNAME pins the frontend URL (and so the token iss) and
KC_HOSTNAME_BACKCHANNEL_DYNAMIC keeps the backchannel on keycloak:8080.
issuer-uri is now unset: Spring Boot treats it as a discovery location, and no
single URL is reachable from both a host browser and the app container. The
consequence (Spring skips the iss comparison) is documented in the yml.
application-docker-keycloak.yml is now tracked and no longer dockerignored. It
holds only ${ENV_VAR} placeholders, and the image is built from src/, so
without it a fresh clone builds an image with no keycloak registration at all.
Also: the Keycloak healthcheck hit /auth/health/live, a path Keycloak 25 does
not serve, and the management port speaks HTTPS with a self-signed certificate
here, so it probes the demo realm's OIDC metadata instead. The app now waits
for Keycloak to be healthy.
Verified end to end: /oauth2/authorization/keycloak, sign in as demo/demo,
land on /index.html?messageKey=message.login.success signed in as Demo User,
with a user_account row (provider KEYCLOAK, enabled 1).
Review round 1. - keycloak/README.md documented `up -d --build` and claimed it returns when the stack is ready. It does not: nothing depends on myapp-main, so `up -d` returns while the app is still booting, and the "about a minute" figure ignored the in-image Gradle build on a fresh clone. Documents `up -d --build --wait` and says the first build takes several minutes. - The keycloak healthcheck had no start_period, so the depends_on gate added in the previous commit gave Liquibase plus the realm import only the interval*retries budget (~100s) before failing `up` outright. Added start_period: 60s. - Narrowed the demo client's redirectUris from ["*"] to the exact callback http://localhost:8080/login/oauth2/code/keycloak, matching the redirect-uri pinned in application-docker-keycloak.yml and the earlier webOrigins change. - Deleted application-docker-keycloak.yml-example: byte-identical to the tracked application-docker-keycloak.yml, so it could only drift. - Labelled KC_HOSTNAME_STRICT vestigial: it only governs deriving the hostname from request headers when KC_HOSTNAME is unset. Checked with `docker compose -f docker-compose-keycloak.yml config` and `python3 -m json.tool` on the realm export; the stack was not restarted.
…xample The masked client-secret (*******) is not valid YAML, and Spring Boot resolves the provider issuer-uri at startup against a host that does not exist, so copying the example to application-local.yml killed the local quick start before the context started. Both blocks are commented out with a pointer to the Keycloak stack, which is where OIDC actually runs.
…he keycloak stack application-playwright-test.yml replaces user.security.unprotectedURIs wholesale, so it had silently lost /user/registration/passwordless, /webauthn/authenticate/**, /login/webauthn and /actuator/health relative to the base list in application.yml. The override now matches that list entry for entry, with a comment saying it has to. docker-compose-keycloak.yml now sets USER_REGISTRATION_SENDVERIFICATIONEMAIL false the way compose.yaml does, so form registration in that stack no longer creates a disabled account waiting on mail the bundled relay cannot deliver. Also drops a stray trailing # after container_name.
- README: the Playwright block ran ./gradlew from playwright/, and the install line was redundant since playwrightTest depends on playwrightBrowsers and playwrightInstall; passwordless registration lives in register.js, not only webauthn-*.js; the layout claimed one application-<profile>.yml per profile, which is false for local, test and registration-guard, and omitted resources/messages/; playwright-test disables the verification and password-reset emails, not all outbound mail - CLAUDE.md: the profile list in the bootRun comment was four of eight; the profile entity is DemoUserProfile - AUTHENTICATION.md: troubleshooting said Google and Facebook will not call back to localhost, contradicting the OAuth2 section above it; notes that form registration in the Keycloak stack also skips verification, and that the bootRun database container name derives from the checkout directory - TESTING.md: 64 test files, not 62; the demo test layout was missing controller/ and user/profile/session/; the disabled-test categories do not cover AdminUserManagementTest or SecurityConfigurationTest; Gradle tasks run from the repository root - CONFIGURATION.md: playwright-test also overrides spring.datasource.* and restates unprotectedURIs in full - Line citations into application-local.yml-example shifted by three
Contributor
There was a problem hiding this comment.
Pull request overview
This PR restructures the demo’s documentation into a smaller root README plus focused docs/ pages, and aligns build/config/Docker/Keycloak paths so the documented “fresh clone → run” flows work reliably. It also fixes two demo issues found during documentation: session profile scoping and missing admin lock/unlock endpoints.
Changes:
- Replaced the oversized README with a concise overview and added focused docs for configuration, development, testing, extension points, and authentication (including Keycloak).
- Fixed/standardized run paths (Docker image build, Compose healthchecks, Boot “docker compose” integration, local example config) so documented commands work from a clean checkout.
- Added missing admin lock/unlock API + tests, and corrected
DemoSessionProfileto be truly session-scoped (with a regression test).
Reviewed changes
Copilot reviewed 45 out of 47 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/test/java/com/digitalsanctuary/spring/user/security/EmailVerificationEdgeCaseTest.java | Update disabled-test pointer to new testing doc |
| src/test/java/com/digitalsanctuary/spring/user/security/AuditLoggingIntegrationTest.java | Update disabled-test pointer to new testing doc |
| src/test/java/com/digitalsanctuary/spring/user/oauth2/GoogleOAuth2IntegrationTest.java | Update disabled-test pointer to new testing doc |
| src/test/java/com/digitalsanctuary/spring/user/integration/SecurityConfigurationTest.java | Update disabled-test pointer to new testing doc |
| src/test/java/com/digitalsanctuary/spring/user/integration/AuthenticationIntegrationTest.java | Update disabled-test pointer to new testing doc |
| src/test/java/com/digitalsanctuary/spring/user/concurrent/AdminUserManagementTest.java | Update disabled-test pointer to new testing doc |
| src/test/java/com/digitalsanctuary/spring/user/api/UserRegistrationEdgeCaseTest.java | Update disabled-test pointer to new testing doc |
| src/test/java/com/digitalsanctuary/spring/user/api/UserRegistrationCoreTest.java | Update disabled-test pointer to new testing doc |
| src/test/java/com/digitalsanctuary/spring/user/api/UserRegistrationComprehensiveTest.java | Update disabled-test pointer to new testing doc |
| src/test/java/com/digitalsanctuary/spring/user/api/UserApiTest.java | Update disabled-test pointers to new testing doc |
| src/test/java/com/digitalsanctuary/spring/user/api/PasswordResetCompletionTest.java | Update disabled-test pointer to new testing doc |
| src/test/java/com/digitalsanctuary/spring/user/api/PasswordResetApiTestSimplified.java | Update disabled-test pointer to new testing doc |
| src/test/java/com/digitalsanctuary/spring/user/api/PasswordResetApiTest.java | Update disabled-test pointer to new testing doc |
| src/test/java/com/digitalsanctuary/spring/user/api/AuthenticatedUserApiTestSimplified.java | Update disabled-test pointer to new testing doc |
| src/test/java/com/digitalsanctuary/spring/user/api/ApiSecurityTest.java | Update disabled-test pointer to new testing doc |
| src/test/java/com/digitalsanctuary/spring/demo/user/profile/session/DemoSessionProfileScopeTest.java | Regression test to ensure session profile stays session-scoped |
| src/test/java/com/digitalsanctuary/spring/demo/controller/AdminAPIControllerTest.java | MockMvc tests for new admin lock/unlock API |
| src/main/resources/application.yml | Base config: enable bootRun Compose file + unprotect /actuator/health |
| src/main/resources/application-playwright-test.yml | Restore full unprotectedURIs override list for Playwright profile |
| src/main/resources/application-local.yml-example | Fix local example datasource + seed data + comment out broken Keycloak block |
| src/main/resources/application-docker-keycloak.yml-example | Remove redundant example file (replaced by tracked real profile file) |
| src/main/resources/application-docker-keycloak.yml | Add tracked Keycloak profile using env-driven OIDC endpoints |
| src/main/java/com/digitalsanctuary/spring/demo/util/TempTest.java | Remove temporary startup logger |
| src/main/java/com/digitalsanctuary/spring/demo/user/profile/session/DemoSessionProfile.java | Fix session profile bean to be properly session-scoped |
| src/main/java/com/digitalsanctuary/spring/demo/registration/DomainRegistrationGuard.java | Fix framework-doc link in Javadoc |
| src/main/java/com/digitalsanctuary/spring/demo/controller/AdminAPIController.java | Add JSON admin lock/unlock endpoints backing existing admin UI |
| README.md | Rewrite README into concise overview + docs index |
| mise.toml | Pin Java tool version to 21 |
| keycloak/realm/realm-export.json | Fix realm export (rename realm, align client secret, add demo user, tighten redirect URIs) |
| keycloak/README.md | Add Keycloak stack usage, ports, creds, realm export guidance |
| keycloak.env | Align OIDC endpoints with host/container split and updated realm |
| docs/TESTING.md | New testing guide (JUnit + disabled tests + Playwright + CI) |
| docs/TEST-ANALYSIS.md | Remove stale test analysis doc (content absorbed into TESTING.md) |
| docs/HELP.md | Remove boilerplate Initializr help doc |
| docs/EXTENDING.md | New extension-points guide mapping demo code to framework SPIs |
| docs/DEVELOPMENT.md | New development/run guide covering bootRun/Compose/DevTools/LiveReload |
| docs/CONFIGURATION.md | New profile/property guide describing overrides and env vars |
| docs/AUTHENTICATION.md | New authentication guide (verification, passkeys, MFA, OAuth2, Keycloak, admin) |
| Dockerfile | Multi-stage build on Java 21; build jar inside image |
| docker-compose-keycloak.yml | Improve readiness gating + healthchecks + disable verification email for stack |
| CONFIG.md | Remove stale root config doc (replaced by framework docs + new demo docs) |
| compose.yaml | Disable verification email in Docker stack + adjust healthcheck |
| CLAUDE.md | Update repository guidance (profiles, paths, documentation locations) |
| CHANGELOG.md | Add 2026-08-15 entry summarizing doc/run-path/bugfix work |
| build.gradle | Keep development-only deps out of packaged jar; ensure bootRun includes devOnly classpath |
| .gitignore | Stop ignoring tracked Keycloak profile file (add explanatory comment) |
| .dockerignore | Add dockerignore to shrink context and exclude local secrets |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This was referenced Aug 15, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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
Rewrites the README (828 lines, roughly 40% wrong or generic) into a ~220-line overview plus five
docs/files, and fixes the code and config so every documented run path actually works from a fresh clone. Two demo bugs found while documenting are fixed as well.Documentation
README.mdrewritten: what the demo shows (capability → code → doc), quick start (Docker, local Gradle, Keycloak), profiles, docs index, testing, contributing, license.docs/CONFIGURATION.md,docs/DEVELOPMENT.md,docs/TESTING.md,docs/EXTENDING.md,docs/AUTHENTICATION.md,keycloak/README.md. Every path, property key, command, andfile:linecitation was verified against the repo or the framework source at 5.3.0.CONFIG.md(stale copy of the framework's, with wrong keys),docs/HELP.md(Initializr boilerplate),docs/TEST-ANALYSIS.md(stale; absorbed intodocs/TESTING.md; the 15@Disabledmessages now point there).CLAUDE.mdfacts corrected (builders path,CustomUserEmailService, controller names, template dirs, profile list).CHANGELOG.mdgets a 2026-08-15 entry plus a backfill of the version bumps since March.Run-path fixes
Dockerfile: multi-stage on Java 21 (wasCOPY build/libs/*.jaron a Java 17 JRE, sodocker compose up --buildfailed on a fresh clone and the jar could not start). Added.dockerignore.build.gradle:runtimeOnlyno longer extendsdevelopmentOnly, sospring-boot-docker-composestays out of the jar (it made every container fail at startup).spring.docker.compose.file: compose.dev.yamlmoved to baseapplication.yml;bootRunstarts its DB under any profile./actuator/healthadded tounprotectedURIsfor the container healthchecks.compose.yamlanddocker-compose-keycloak.ymlsetUSER_REGISTRATION_SENDVERIFICATIONEMAIL=false(the mail container is a relay that cannot deliver, so registration could never complete).application-local.yml-example: points at the compose.dev DB, seedsdata-local.sql, and no longer contains the Keycloak block whose maskedclient-secretwas invalid YAML and whoseissuer-uritriggered OIDC discovery against a nonexistent host (the local quick start could not boot).application-playwright-test.yml:unprotectedURIsoverride restored to match the base list.mise.tomlpins Java 21.TempTeststartup logger removed.Keycloak stack
Login never worked: the realm export was
master, which Keycloak's--import-realmskips, so the client did not exist. Renamed todemo(logindemo/demo;admin/adminis the admin console), aligned the client secret, split browser-facing (localhost:8180) and container-facing (keycloak:8080) URLs, narrowedredirectUris, fixed the healthcheck, trackedapplication-docker-keycloak.yml(placeholders only) and removed the identical-example.issuer-uriis deliberately unset (Spring skips only theisscomparison; signature, audience, nonce, expiry are still validated); the tradeoff and the alternative are documented in the file.Demo bugs fixed
DemoSessionProfilewas a plain@Component, i.e. a singleton shared across every HTTP session (the framework'sBaseSessionProfileJavadoc warns about exactly this). Now@SessionScopedProfile, with a scope test./admin/lockAccountand/admin/unlockAccount, which existed nowhere. AddedAdminAPIController(ADMIN_PRIVILEGE, JSON in/out) with MockMvc tests. Locks follow the framework's semantics (auto-expire afteraccountLockoutDurationunless -1).Verification
./gradlew test: 309 tests, 0 failures, 182 pre-existing skips.docker compose up --buildfrom a clean context: image builds,/actuator/healthUP anonymously, register + login works.docker compose -f docker-compose-keycloak.yml up -d --build --wait: OIDC login from a host browser asdemo/demolands on the logged-in home page.bootRununderlocal, sample events render, browser registration and login succeed.Rulings worth a look
/actuator/healthis now public (status only, no details).AdminAPIControllerif you would rather not carry them.docker-mailserveras a relay and disabled verification email in the Docker stacks instead of swapping to a capturing mail server (Mailpit would let the demo show the verification flow; happy to do that as a follow-up).