diff --git a/README.md b/README.md
index 1e321f1..4352257 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ Check out our [official Java SDK documentation](https://docs.authsignal.com/sdks
Add this dependency to your project's build file:
```groovy
-implementation 'com.authsignal:authsignal-java:2.7.0'
+implementation 'com.authsignal:authsignal-java:2.9.0'
```
### Maven users
@@ -26,7 +26,7 @@ Add this dependency to your project's POM:
com.authsignal
authsignal-java
- 2.7.0
+ 2.9.0
```
diff --git a/gradle.properties b/gradle.properties
index a3315df..28b6517 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1 +1 @@
-version=2.8.0
\ No newline at end of file
+version=2.9.0
\ No newline at end of file
diff --git a/src/main/java/com/authsignal/AuthsignalClient.java b/src/main/java/com/authsignal/AuthsignalClient.java
index 684bd07..72aab6d 100644
--- a/src/main/java/com/authsignal/AuthsignalClient.java
+++ b/src/main/java/com/authsignal/AuthsignalClient.java
@@ -27,7 +27,7 @@ public class AuthsignalClient {
private static final String DEFAULT_API_URL = "https://api.authsignal.com/v1";
private static final int DEFAULT_RETRIES = 2;
- private static final String VERSION = "2.6.0";
+ private static final String VERSION = "2.9.0";
public Webhook webhook;
@@ -58,6 +58,41 @@ public CompletableFuture getUser(GetUserRequest request) {
response -> new Gson().fromJson(response.body(), GetUserResponse.class));
}
+ public CompletableFuture queryUsers(QueryUsersRequest request) {
+ Map params = new HashMap<>();
+
+ if (request.username != null) {
+ params.put("username", request.username);
+ }
+
+ if (request.email != null) {
+ params.put("email", request.email);
+ }
+
+ if (request.phoneNumber != null) {
+ params.put("phoneNumber", request.phoneNumber);
+ }
+
+ if (request.token != null) {
+ params.put("token", request.token);
+ }
+
+ if (request.limit != null) {
+ params.put("limit", request.limit.toString());
+ }
+
+ if (request.lastEvaluatedUserId != null) {
+ params.put("lastEvaluatedUserId", request.lastEvaluatedUserId);
+ }
+
+ String query = buildQueryString(params);
+
+ String path = "/users" + (query.isEmpty() ? "" : "?" + query);
+
+ return getRequest(path)
+ .thenApply(response -> new Gson().fromJson(response.body(), QueryUsersResponse.class));
+ }
+
public CompletableFuture updateUser(UpdateUserRequest request) {
String path = String.format("/users/%s", request.userId);
@@ -112,6 +147,29 @@ public CompletableFuture getAction(GetActionRequest request)
return getRequest(path).thenApply(response -> new Gson().fromJson(response.body(), GetActionResponse.class));
}
+ public CompletableFuture queryUserActions(QueryUserActionsRequest request) {
+ Map params = new HashMap<>();
+
+ if (request.fromDate != null) {
+ params.put("fromDate", request.fromDate);
+ }
+
+ if (request.actionCodes != null && request.actionCodes.length > 0) {
+ params.put("codes", String.join(",", request.actionCodes));
+ }
+
+ if (request.state != null) {
+ params.put("state", request.state.toString());
+ }
+
+ String query = buildQueryString(params);
+
+ String path = String.format("/users/%s/actions", request.userId) + (query.isEmpty() ? "" : "?" + query);
+
+ return getRequest(path)
+ .thenApply(response -> new Gson().fromJson(response.body(), QueryUserActionsResponseItem[].class));
+ }
+
public CompletableFuture updateAction(UpdateActionRequest request) {
String path = String.format("/users/%s/actions/%s/%s", request.userId, request.action, request.idempotencyKey);
diff --git a/src/main/java/com/authsignal/model/QueryUserActionsRequest.java b/src/main/java/com/authsignal/model/QueryUserActionsRequest.java
new file mode 100644
index 0000000..e0f5db1
--- /dev/null
+++ b/src/main/java/com/authsignal/model/QueryUserActionsRequest.java
@@ -0,0 +1,9 @@
+package com.authsignal.model;
+
+public class QueryUserActionsRequest extends ApiModel {
+ public String userId;
+ public String fromDate;
+ public String[] actionCodes;
+ public UserActionState state;
+}
+
diff --git a/src/main/java/com/authsignal/model/QueryUserActionsResponseItem.java b/src/main/java/com/authsignal/model/QueryUserActionsResponseItem.java
new file mode 100644
index 0000000..b667b38
--- /dev/null
+++ b/src/main/java/com/authsignal/model/QueryUserActionsResponseItem.java
@@ -0,0 +1,13 @@
+package com.authsignal.model;
+
+public class QueryUserActionsResponseItem extends ApiModel {
+ public String actionCode;
+ public String idempotencyKey;
+ public String createdAt;
+ public String updatedAt;
+ public String state;
+ public String stateUpdatedAt;
+ public VerificationMethodType verificationMethod;
+ public String verifiedByAuthenticatorId;
+}
+
diff --git a/src/main/java/com/authsignal/model/QueryUsersRequest.java b/src/main/java/com/authsignal/model/QueryUsersRequest.java
new file mode 100644
index 0000000..df105a4
--- /dev/null
+++ b/src/main/java/com/authsignal/model/QueryUsersRequest.java
@@ -0,0 +1,11 @@
+package com.authsignal.model;
+
+public class QueryUsersRequest extends ApiModel {
+ public String username;
+ public String email;
+ public String phoneNumber;
+ public String token;
+ public Integer limit;
+ public String lastEvaluatedUserId;
+}
+
diff --git a/src/main/java/com/authsignal/model/QueryUsersResponse.java b/src/main/java/com/authsignal/model/QueryUsersResponse.java
new file mode 100644
index 0000000..0e98d41
--- /dev/null
+++ b/src/main/java/com/authsignal/model/QueryUsersResponse.java
@@ -0,0 +1,8 @@
+package com.authsignal.model;
+
+public class QueryUsersResponse extends ApiModel {
+ public QueryUsersResponseUser[] users;
+ public String lastEvaluatedUserId;
+ public Object tokenPayload;
+}
+
diff --git a/src/main/java/com/authsignal/model/QueryUsersResponseUser.java b/src/main/java/com/authsignal/model/QueryUsersResponseUser.java
new file mode 100644
index 0000000..9c8c6f0
--- /dev/null
+++ b/src/main/java/com/authsignal/model/QueryUsersResponseUser.java
@@ -0,0 +1,11 @@
+package com.authsignal.model;
+
+public class QueryUsersResponseUser extends ApiModel {
+ public String userId;
+ public String email;
+ public Boolean emailVerified;
+ public String phoneNumber;
+ public Boolean phoneNumberVerified;
+ public String username;
+}
+
diff --git a/src/test/java/com/authsignal/AuthsignalClientTests.java b/src/test/java/com/authsignal/AuthsignalClientTests.java
index 6cb2dfb..bfd04b3 100644
--- a/src/test/java/com/authsignal/AuthsignalClientTests.java
+++ b/src/test/java/com/authsignal/AuthsignalClientTests.java
@@ -272,4 +272,131 @@ public void testPasskeyAuthenticator() {
fail("should not throw any exception");
}
}
+
+ @Test
+ public void testQueryUsers() {
+ String userId = UUID.randomUUID().toString();
+ String testEmail = "test-" + UUID.randomUUID().toString() + "@example.com";
+
+ try {
+ // First create a user with a unique email
+ UpdateUserRequest updateUserRequest = new UpdateUserRequest();
+ updateUserRequest.userId = userId;
+ updateUserRequest.attributes = new UserAttributes();
+ updateUserRequest.attributes.email = testEmail;
+
+ client.updateUser(updateUserRequest).get();
+
+ // Query by email
+ QueryUsersRequest queryRequest = new QueryUsersRequest();
+ queryRequest.email = testEmail;
+
+ QueryUsersResponse queryResponse = client.queryUsers(queryRequest).get();
+
+ assertNotNull("queryResponse should exist", queryResponse);
+ assertNotNull("users array should exist", queryResponse.users);
+ assertTrue("users array should not be empty", queryResponse.users.length > 0);
+
+ QueryUsersResponseUser user = queryResponse.users[0];
+ assertEquals("userId should match", userId, user.userId);
+ assertEquals("email should match", testEmail, user.email);
+
+ // Test pagination with limit
+ QueryUsersRequest limitRequest = new QueryUsersRequest();
+ limitRequest.email = testEmail;
+ limitRequest.limit = 1;
+
+ QueryUsersResponse limitResponse = client.queryUsers(limitRequest).get();
+
+ assertNotNull("limitResponse should exist", limitResponse);
+ assertTrue("users array should respect limit", limitResponse.users.length <= 1);
+
+ // Clean up
+ DeleteUserRequest deleteRequest = new DeleteUserRequest();
+ deleteRequest.userId = userId;
+ client.deleteUser(deleteRequest).get();
+ } catch (Exception e) {
+ System.out.println(e.getMessage());
+
+ fail("should not throw any exception");
+ }
+ }
+
+ @Test
+ public void testQueryUserActions() {
+ String userId = UUID.randomUUID().toString();
+
+ EnrollVerifiedAuthenticatorRequest enrollRequest = new EnrollVerifiedAuthenticatorRequest();
+ enrollRequest.userId = userId;
+ enrollRequest.attributes = new EnrollVerifiedAuthenticatorAttributes();
+ enrollRequest.attributes.verificationMethod = VerificationMethodType.SMS;
+ enrollRequest.attributes.phoneNumber = "+6427000000";
+
+ try {
+ EnrollVerifiedAuthenticatorResponse enrollResponse = client.enrollVerifiedAuthenticator(enrollRequest)
+ .get();
+
+ assertNotNull("enrollResponse should exist", enrollResponse);
+
+ // Track an action
+ TrackRequest trackRequest = new TrackRequest();
+ trackRequest.userId = userId;
+ trackRequest.action = "signIn";
+
+ TrackResponse trackResponse = client.track(trackRequest).get();
+
+ assertNotNull("trackResponse should exist", trackResponse);
+
+ // Query user actions
+ QueryUserActionsRequest queryRequest = new QueryUserActionsRequest();
+ queryRequest.userId = userId;
+
+ QueryUserActionsResponseItem[] actions = client.queryUserActions(queryRequest).get();
+
+ assertNotNull("actions should exist", actions);
+ assertTrue("actions should not be empty", actions.length > 0);
+
+ QueryUserActionsResponseItem action = actions[0];
+ assertEquals("action code should match", "signIn", action.actionCode);
+ assertNotNull("idempotencyKey should exist", action.idempotencyKey);
+ assertNotNull("createdAt should exist", action.createdAt);
+ assertNotNull("state should exist", action.state);
+
+ // Query with filter by action code
+ QueryUserActionsRequest filteredRequest = new QueryUserActionsRequest();
+ filteredRequest.userId = userId;
+ filteredRequest.actionCodes = new String[] { "signIn" };
+
+ QueryUserActionsResponseItem[] filteredActions = client.queryUserActions(filteredRequest).get();
+
+ assertNotNull("filtered actions should exist", filteredActions);
+ assertTrue("filtered actions should not be empty", filteredActions.length > 0);
+
+ for (QueryUserActionsResponseItem filteredAction : filteredActions) {
+ assertEquals("all actions should have signIn code", "signIn", filteredAction.actionCode);
+ }
+
+ // Query with state filter
+ QueryUserActionsRequest stateRequest = new QueryUserActionsRequest();
+ stateRequest.userId = userId;
+ stateRequest.state = UserActionState.CHALLENGE_REQUIRED;
+
+ QueryUserActionsResponseItem[] stateActions = client.queryUserActions(stateRequest).get();
+
+ assertNotNull("state filtered actions should exist", stateActions);
+ // Actions exist if any match the state filter
+ if (stateActions.length > 0) {
+ assertEquals("action state should match filter", "CHALLENGE_REQUIRED", stateActions[0].state);
+ }
+
+ // Clean up
+ DeleteUserRequest deleteRequest = new DeleteUserRequest();
+ deleteRequest.userId = userId;
+ client.deleteUser(deleteRequest).get();
+ } catch (Exception e) {
+ System.out.println(e.getMessage());
+
+ fail("should not throw any exception");
+ }
+ }
}