Skip to content

docs: rewrite README into docs/, fix documented run paths, Keycloak stack, and two demo bugs - #85

Merged
devondragon merged 33 commits into
mainfrom
docs/readme-overhaul
Aug 15, 2026
Merged

docs: rewrite README into docs/, fix documented run paths, Keycloak stack, and two demo bugs#85
devondragon merged 33 commits into
mainfrom
docs/readme-overhaul

Conversation

@devondragon

Copy link
Copy Markdown
Owner

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.md rewritten: what the demo shows (capability → code → doc), quick start (Docker, local Gradle, Keycloak), profiles, docs index, testing, contributing, license.
  • New docs/CONFIGURATION.md, docs/DEVELOPMENT.md, docs/TESTING.md, docs/EXTENDING.md, docs/AUTHENTICATION.md, keycloak/README.md. Every path, property key, command, and file:line citation was verified against the repo or the framework source at 5.3.0.
  • Deleted root CONFIG.md (stale copy of the framework's, with wrong keys), docs/HELP.md (Initializr boilerplate), docs/TEST-ANALYSIS.md (stale; absorbed into docs/TESTING.md; the 15 @Disabled messages now point there).
  • CLAUDE.md facts corrected (builders path, CustomUserEmailService, controller names, template dirs, profile list). CHANGELOG.md gets a 2026-08-15 entry plus a backfill of the version bumps since March.

Run-path fixes

  • Dockerfile: multi-stage on Java 21 (was COPY build/libs/*.jar on a Java 17 JRE, so docker compose up --build failed on a fresh clone and the jar could not start). Added .dockerignore.
  • build.gradle: runtimeOnly no longer extends developmentOnly, so spring-boot-docker-compose stays out of the jar (it made every container fail at startup).
  • spring.docker.compose.file: compose.dev.yaml moved to base application.yml; bootRun starts its DB under any profile.
  • /actuator/health added to unprotectedURIs for the container healthchecks.
  • compose.yaml and docker-compose-keycloak.yml set USER_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, seeds data-local.sql, and no longer contains the Keycloak block whose masked client-secret was invalid YAML and whose issuer-uri triggered OIDC discovery against a nonexistent host (the local quick start could not boot).
  • application-playwright-test.yml: unprotectedURIs override restored to match the base list.
  • mise.toml pins Java 21. TempTest startup logger removed.

Keycloak stack

Login never worked: the realm export was master, which Keycloak's --import-realm skips, so the client did not exist. Renamed to demo (login demo/demo; admin/admin is the admin console), aligned the client secret, split browser-facing (localhost:8180) and container-facing (keycloak:8080) URLs, narrowed redirectUris, fixed the healthcheck, tracked application-docker-keycloak.yml (placeholders only) and removed the identical -example. issuer-uri is deliberately unset (Spring skips only the iss comparison; signature, audience, nonce, expiry are still validated); the tradeoff and the alternative are documented in the file.

Demo bugs fixed

  • DemoSessionProfile was a plain @Component, i.e. a singleton shared across every HTTP session (the framework's BaseSessionProfile Javadoc warns about exactly this). Now @SessionScopedProfile, with a scope test.
  • The admin actions page posted to /admin/lockAccount and /admin/unlockAccount, which existed nowhere. Added AdminAPIController (ADMIN_PRIVILEGE, JSON in/out) with MockMvc tests. Locks follow the framework's semantics (auto-expire after accountLockoutDuration unless -1).

Verification

  • ./gradlew test: 309 tests, 0 failures, 182 pre-existing skips.
  • docker compose up --build from a clean context: image builds, /actuator/health UP anonymously, register + login works.
  • docker compose -f docker-compose-keycloak.yml up -d --build --wait: OIDC login from a host browser as demo/demo lands on the logged-in home page.
  • Local quick start: copied the example, bootRun under local, sample events render, browser registration and login succeed.
  • Link/anchor checker over all markdown: 181 links, 0 broken. No em/en dashes, no references to deleted files, no secrets in the diff.

Rulings worth a look

  • /actuator/health is now public (status only, no details).
  • Admin lock/unlock endpoints were implemented rather than left as a dead UI; drop AdminAPIController if you would rather not carry them.
  • Kept docker-mailserver as 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).
  • Badges reduced to License and Java; the version table is the single place versions live.

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
Copilot AI lite review requested due to automatic review settings August 15, 2026 08:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 DemoSessionProfile to 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.

@devondragon
devondragon merged commit 55337f5 into main Aug 15, 2026
11 checks passed
@devondragon
devondragon deleted the docs/readme-overhaul branch August 15, 2026 16:56
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