Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,21 @@ jobs:
path: example

- name: Set up Node
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: 20
cache: npm
cache-dependency-path: example/package-lock.json
- 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 }}
Expand Down
56 changes: 56 additions & 0 deletions .github/workflows/test-main.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/io/castle/example/config/CastleConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
}
}
2 changes: 1 addition & 1 deletion src/main/java/io/castle/example/config/Demos.java
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/io/castle/example/web/CastleSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<String, Object> error(String message) {
Map<String, Object> error = new LinkedHashMap<>();
error.put("error", message);
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/io/castle/example/web/DemoController.java
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ public Map<String, Object> 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<String, Object> user = new LinkedHashMap<>();
user.put("id", env.get("valid_user_id"));
Expand Down Expand Up @@ -263,8 +263,8 @@ public Map<String, Object> createList(@RequestBody(required = false) Map<String,
ListResponse created = client.createList(request);
List<ListResponse> all = client.listAllLists();
Map<String, Object> 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());
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/templates/password_reset.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

<th:block layout:fragment="desc">
<p>This demo records the password-reset event with the non-blocking <code>/log</code> endpoint, which stores the event without returning a verdict.</p>
<p>Assume the user already passed your reset challenge (e.g. an emailed OTP). Enter a value <em>different from</em> the valid password to send <code>$password_reset / $succeeded</code>, or the valid password to send <code>$password_reset / $failed</code>. (The password is not actually changed.)</p>
<p>Assume the user already passed your reset challenge (e.g. an emailed OTP). Enter a value <em>different from</em> the valid password to send <code>$profile_reset / $succeeded</code>, or the valid password to send <code>$profile_reset / $failed</code>. (The password is not actually changed.)</p>
</th:block>

<th:block layout:fragment="scripts">
Expand Down
Loading