diff --git a/README.md b/README.md index fd7caee..dbdbee5 100644 --- a/README.md +++ b/README.md @@ -29,3 +29,19 @@ $ mvn jetty:run Navigate to: http://localhost:8080/ + +API demos +========= + +The home page links to a set of demos exercising the Castle SDK directly: + +* **Lists & list items API** (`/lists-demo`) — creates a list, adds an item, + queries the items, lists all lists and deletes the list. +* **Events API** (`/events-demo`) — fetches the event schema and queries events. +* **Privacy data request** (`/privacy-demo?userId=...`) — requests the data + Castle holds for a user. +* **Received webhooks** (`/webhooks`) — `POST` a Castle webhook with a valid + `X-Castle-Signature` header to have it verified against the raw request body + and listed on the page. + +These demos require `castle-java` 2.2.0. diff --git a/pom.xml b/pom.xml index 617bc1a..7f64fc2 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ io.castle.example castle-example war - 1.4.0 + 1.5.0 Castle Java JDK Example https://github.com/castle/castle-java-example @@ -27,7 +27,7 @@ io.castle castle-java - 1.4.0 + 2.2.0 diff --git a/src/main/java/io/castle/example/EventsDemoServlet.java b/src/main/java/io/castle/example/EventsDemoServlet.java new file mode 100644 index 0000000..4755a79 --- /dev/null +++ b/src/main/java/io/castle/example/EventsDemoServlet.java @@ -0,0 +1,45 @@ +package io.castle.example; + +import com.google.common.collect.ImmutableMap; +import io.castle.client.Castle; +import io.castle.client.api.CastleApi; +import io.castle.client.model.CastleResponse; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; + +/** + * Demonstrates the Events API (schema and query). + */ +@WebServlet("/events-demo") +public class EventsDemoServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setContentType("text/html; charset=utf-8"); + PrintWriter out = resp.getWriter(); + out.println("Events API demo"); + out.println("

Events API demo

← Home

"); + + CastleApi client = Castle.instance().client(); + + try { + CastleResponse schema = client.eventsSchema(); + ListsDemoServlet.render(out, "eventsSchema", schema); + + CastleResponse query = client.queryEvents(ImmutableMap.builder() + .put("filters", ImmutableMap.of()) + .build()); + ListsDemoServlet.render(out, "queryEvents", query); + } catch (Exception e) { + out.println("
Error: " + e.getMessage() + "
"); + } + + out.println(""); + } +} diff --git a/src/main/java/io/castle/example/ListsDemoServlet.java b/src/main/java/io/castle/example/ListsDemoServlet.java new file mode 100644 index 0000000..d5b7c73 --- /dev/null +++ b/src/main/java/io/castle/example/ListsDemoServlet.java @@ -0,0 +1,70 @@ +package io.castle.example; + +import com.google.common.collect.ImmutableMap; +import io.castle.client.Castle; +import io.castle.client.api.CastleApi; +import io.castle.client.model.CastleResponse; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; + +/** + * Demonstrates the Lists and List items API end to end: + * create a list, add an item, query the items, list all lists and delete the list. + */ +@WebServlet("/lists-demo") +public class ListsDemoServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setContentType("text/html; charset=utf-8"); + PrintWriter out = resp.getWriter(); + out.println("Lists API demo"); + out.println("

Lists API demo

← Home

"); + + CastleApi client = Castle.instance().client(); + + try { + CastleResponse created = client.createList(ImmutableMap.builder() + .put("name", "Example trusted IPs") + .put("description", "Created by the castle-java example app") + .put("color", "$green") + .put("primary_field", "context.ip") + .build()); + render(out, "createList", created); + + String listId = created.json().getAsJsonObject().get("id").getAsString(); + + CastleResponse item = client.createListItem(listId, ImmutableMap.builder() + .put("primary_value", "1.2.3.4") + .put("comment", "added from the example app") + .build()); + render(out, "createListItem", item); + + CastleResponse items = client.queryListItems(listId, ImmutableMap.builder() + .put("filters", ImmutableMap.of()) + .build()); + render(out, "queryListItems", items); + + CastleResponse all = client.getAllLists(); + render(out, "getAllLists", all); + + CastleResponse deleted = client.deleteList(listId); + render(out, "deleteList", deleted); + } catch (Exception e) { + out.println("
Error: " + e.getMessage() + "
"); + } + + out.println(""); + } + + static void render(PrintWriter out, String label, CastleResponse response) { + out.println("

" + label + " (HTTP " + (response.isSuccessful() ? "2xx" : "error") + ")

"); + out.println("
" + response.json() + "
"); + } +} diff --git a/src/main/java/io/castle/example/PrivacyDemoServlet.java b/src/main/java/io/castle/example/PrivacyDemoServlet.java new file mode 100644 index 0000000..3d9cbcc --- /dev/null +++ b/src/main/java/io/castle/example/PrivacyDemoServlet.java @@ -0,0 +1,47 @@ +package io.castle.example; + +import com.google.common.collect.ImmutableMap; +import io.castle.client.Castle; +import io.castle.client.api.CastleApi; +import io.castle.client.model.CastleResponse; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; + +/** + * Demonstrates the privacy data-request API. + */ +@WebServlet("/privacy-demo") +public class PrivacyDemoServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setContentType("text/html; charset=utf-8"); + PrintWriter out = resp.getWriter(); + out.println("Privacy API demo"); + out.println("

Privacy API demo

← Home

"); + + String userId = req.getParameter("userId"); + if (userId == null || userId.isEmpty()) { + userId = "1"; + } + + CastleApi client = Castle.instance().client(); + + try { + CastleResponse response = client.requestUserData(ImmutableMap.builder() + .put("user_id", userId) + .build()); + ListsDemoServlet.render(out, "requestUserData(" + userId + ")", response); + } catch (Exception e) { + out.println("
Error: " + e.getMessage() + "
"); + } + + out.println(""); + } +} diff --git a/src/main/java/io/castle/example/WebhookServlet.java b/src/main/java/io/castle/example/WebhookServlet.java new file mode 100644 index 0000000..c8de831 --- /dev/null +++ b/src/main/java/io/castle/example/WebhookServlet.java @@ -0,0 +1,70 @@ +package io.castle.example; + +import io.castle.client.Castle; + +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Receives Castle webhooks and verifies the {@code X-Castle-Signature} header + * against the raw request body before trusting the payload. + */ +@WebServlet("/webhooks") +public class WebhookServlet extends HttpServlet { + + private static final List RECEIVED = new CopyOnWriteArrayList(); + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + byte[] body = readBody(req); + + boolean valid = Castle.instance().verifyWebhookSignature(req, body); + + if (!valid) { + resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); + resp.getWriter().println("invalid signature"); + return; + } + + RECEIVED.add(0, new String(body, "UTF-8")); + resp.setStatus(HttpServletResponse.SC_OK); + resp.getWriter().println("ok"); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setContentType("text/html; charset=utf-8"); + PrintWriter out = resp.getWriter(); + out.println("Webhooks"); + out.println("

Received webhooks

← Home

"); + out.println("

POST a Castle webhook to /webhooks with a valid " + + "X-Castle-Signature header to see it verified and listed here.

"); + if (RECEIVED.isEmpty()) { + out.println("

No verified webhooks received yet.

"); + } + for (String payload : RECEIVED) { + out.println("
" + payload + "
"); + } + out.println(""); + } + + private byte[] readBody(HttpServletRequest req) throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + ServletInputStream in = req.getInputStream(); + byte[] chunk = new byte[4096]; + int read; + while ((read = in.read(chunk)) != -1) { + buffer.write(chunk, 0, read); + } + return buffer.toByteArray(); + } +} diff --git a/src/main/webapp/index.jsp b/src/main/webapp/index.jsp index 6e7f89f..fb91522 100644 --- a/src/main/webapp/index.jsp +++ b/src/main/webapp/index.jsp @@ -31,6 +31,15 @@
Change your password
+
+

API demos

+ +
@@ -72,6 +81,15 @@
Forgot your password?
+
+

API demos

+ +

Test data

The example application contains a built-in list of users with the following logins: