diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b034441..35bc667 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: path: example - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: 20 cache: npm @@ -27,13 +27,13 @@ jobs: - run: npm ci working-directory: example - # castle-java 3.0.0 is not yet on Maven Central; build it from source and - # install it into the local repository so the example can resolve it. + # Build castle-java from main and install it locally so the example can + # resolve 3.0.0 before it is on Maven Central. - name: Check out castle-java SDK uses: actions/checkout@v5 with: repository: castle/castle-java - ref: modernize-3.0.0 + ref: main path: castle-java - name: Set up JDK ${{ matrix.java-version }} diff --git a/.github/workflows/test-main.yml b/.github/workflows/test-main.yml new file mode 100644 index 0000000..aaeacb7 --- /dev/null +++ b/.github/workflows/test-main.yml @@ -0,0 +1,56 @@ +name: Test against SDK main + +# Exercises the example app against the unreleased Castle Java SDK (main +# branch). castle-java 3.0.0 is consumed as a Maven artifact, so we clone, +# build and install the SDK main branch into the local repository and run +# the example against it. +# +# This is the permanent test/sdk-main branch: every push to it runs the suite +# against the latest main. Nightly/manual runs require this file to also live +# on the default branch (GitHub only honours schedule/workflow_dispatch from +# the default branch). +on: + push: + branches: [test/sdk-main] + workflow_dispatch: + schedule: + - cron: "0 6 * * *" + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Check out example + uses: actions/checkout@v5 + with: + path: example + + - name: Set up Node + uses: actions/setup-node@v5 + with: + node-version: 20 + cache: npm + cache-dependency-path: example/package-lock.json + - run: npm ci + working-directory: example + + - name: Check out castle-java SDK + uses: actions/checkout@v5 + with: + repository: castle/castle-java + ref: main + path: castle-java + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + cache: maven + + - name: Install castle-java from main + run: mvn -B -ntp -f castle-java/pom.xml install -DskipTests -Dmaven.javadoc.skip=true + + - name: Build and test example + working-directory: example + run: mvn -B -ntp verify diff --git a/README.md b/README.md index 1e79269..18c0c93 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ the backend, which calls Castle and acts on the verdict. - **sign up** – `$registration` to `filter` (anonymous, so the email goes in `params`): `$attempted` for a new email, `$failed` (resolved via `matching_user_id`) for an email that already exists - **login** – `$login` reusing one request token across two calls: `filter` `$attempted` first, then `risk` `$succeeded` on success or `filter` `$failed` (wrong password / unknown user) - **account** – post-login actions: profile update (`$profile_update` to `risk`), a custom event (`Castle.custom()`), and logout (`$logout` via the non-blocking `log` endpoint) -- **password reset** – `$password_reset` via the non-blocking `log` endpoint +- **password reset** – `$profile_reset` via the non-blocking `log` endpoint - **lists** – the Lists API (`createList`, `getAllLists`) - **privacy** – the Privacy API (`requestUserData` and a delete call to the privacy endpoint) - **webhooks** – incoming Castle webhooks are signature-verified with `verifyWebhookSignature` (against the `X-Castle-Signature` header) and the most recent payloads are listed diff --git a/src/main/java/io/castle/example/config/CastleConfig.java b/src/main/java/io/castle/example/config/CastleConfig.java index ae81de8..f89b3db 100644 --- a/src/main/java/io/castle/example/config/CastleConfig.java +++ b/src/main/java/io/castle/example/config/CastleConfig.java @@ -5,6 +5,7 @@ import org.springframework.context.annotation.Configuration; import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; /** * Initialises the Castle SDK singleton once, on startup. The API secret comes @@ -37,4 +38,9 @@ public void initialize() { throw new IllegalStateException("The Castle SDK configuration is not correct", e); } } + + @PreDestroy + public void shutdown() { + Castle.instance().close(); + } } diff --git a/src/main/java/io/castle/example/config/Demos.java b/src/main/java/io/castle/example/config/Demos.java index 189c492..0b69124 100644 --- a/src/main/java/io/castle/example/config/Demos.java +++ b/src/main/java/io/castle/example/config/Demos.java @@ -18,7 +18,7 @@ public final class Demos { new Demo("account", "account", "Update your profile, send a custom event, and log out.", null), new Demo("password_reset", "password reset", - "Record a password-reset event with the non-blocking log endpoint.", null), + "Record a password-reset event ($profile_reset) with the non-blocking log endpoint.", null), new Demo("lists", "lists", "Create and fetch lists with the Lists API.", null), new Demo("privacy", "privacy", diff --git a/src/main/java/io/castle/example/web/CastleSupport.java b/src/main/java/io/castle/example/web/CastleSupport.java index 5f9d0c3..1ff2804 100644 --- a/src/main/java/io/castle/example/web/CastleSupport.java +++ b/src/main/java/io/castle/example/web/CastleSupport.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; import io.castle.client.Castle; import io.castle.client.model.CastleResponse; @@ -46,6 +47,17 @@ static Object toJava(CastleResponse response, ObjectMapper mapper) { } } + static Object toJava(Object value, ObjectMapper mapper) { + if (value == null) { + return null; + } + try { + return mapper.readValue(new Gson().toJson(value), Object.class); + } catch (Exception e) { + return String.valueOf(value); + } + } + static Map error(String message) { Map error = new LinkedHashMap<>(); error.put("error", message); diff --git a/src/main/java/io/castle/example/web/DemoController.java b/src/main/java/io/castle/example/web/DemoController.java index 9953335..0afa791 100644 --- a/src/main/java/io/castle/example/web/DemoController.java +++ b/src/main/java/io/castle/example/web/DemoController.java @@ -186,7 +186,7 @@ public Map evaluateNewPassword(@RequestBody(required = false) Ma // A new password that differs from the current one is a successful reset. String status = password.equals(env.get("valid_password")) ? "$failed" : "$succeeded"; - String type = "$password_reset"; + String type = "$profile_reset"; Map user = new LinkedHashMap<>(); user.put("id", env.get("valid_user_id")); @@ -263,8 +263,8 @@ public Map createList(@RequestBody(required = false) Map all = client.listAllLists(); Map ok = new LinkedHashMap<>(); - ok.put("created", mapper.convertValue(created, Object.class)); - ok.put("all_lists", mapper.convertValue(all, Object.class)); + ok.put("created", CastleSupport.toJava(created, mapper)); + ok.put("all_lists", CastleSupport.toJava(all, mapper)); result = ok; } catch (Exception e) { result = CastleSupport.error(e.getMessage()); diff --git a/src/main/resources/templates/password_reset.html b/src/main/resources/templates/password_reset.html index 73ba096..65568be 100644 --- a/src/main/resources/templates/password_reset.html +++ b/src/main/resources/templates/password_reset.html @@ -21,7 +21,7 @@

This demo records the password-reset event with the non-blocking /log endpoint, which stores the event without returning a verdict.

-

Assume the user already passed your reset challenge (e.g. an emailed OTP). Enter a value different from the valid password to send $password_reset / $succeeded, or the valid password to send $password_reset / $failed. (The password is not actually changed.)

+

Assume the user already passed your reset challenge (e.g. an emailed OTP). Enter a value different from the valid password to send $profile_reset / $succeeded, or the valid password to send $profile_reset / $failed. (The password is not actually changed.)