feat: add per-tenant health, OIDC, team methods to API clients
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -101,6 +101,111 @@ public class LogtoManagementClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** List members of a Logto organization. */
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public List<Map<String, Object>> listOrganizationMembers(String orgId) {
|
||||||
|
if (!isAvailable()) return List.of();
|
||||||
|
try {
|
||||||
|
var resp = restClient.get()
|
||||||
|
.uri(config.getLogtoEndpoint() + "/api/organizations/" + orgId + "/users")
|
||||||
|
.header("Authorization", "Bearer " + getAccessToken())
|
||||||
|
.retrieve()
|
||||||
|
.body(List.class);
|
||||||
|
return resp != null ? resp : List.of();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to list org members for {}: {}", orgId, e.getMessage());
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get roles assigned to a user within an organization. */
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public List<Map<String, Object>> getUserOrganizationRoles(String orgId, String userId) {
|
||||||
|
if (!isAvailable()) return List.of();
|
||||||
|
try {
|
||||||
|
var resp = restClient.get()
|
||||||
|
.uri(config.getLogtoEndpoint() + "/api/organizations/" + orgId + "/users/" + userId + "/roles")
|
||||||
|
.header("Authorization", "Bearer " + getAccessToken())
|
||||||
|
.retrieve()
|
||||||
|
.body(List.class);
|
||||||
|
return resp != null ? resp : List.of();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to get user roles: {}", e.getMessage());
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Assign a role to a user in an organization. */
|
||||||
|
public void assignOrganizationRole(String orgId, String userId, String roleId) {
|
||||||
|
if (!isAvailable()) return;
|
||||||
|
try {
|
||||||
|
restClient.post()
|
||||||
|
.uri(config.getLogtoEndpoint() + "/api/organizations/" + orgId + "/users/" + userId + "/roles")
|
||||||
|
.header("Authorization", "Bearer " + getAccessToken())
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.body(Map.of("organizationRoleIds", List.of(roleId)))
|
||||||
|
.retrieve()
|
||||||
|
.toBodilessEntity();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to assign role: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove a user from an organization. */
|
||||||
|
public void removeUserFromOrganization(String orgId, String userId) {
|
||||||
|
if (!isAvailable()) return;
|
||||||
|
try {
|
||||||
|
restClient.delete()
|
||||||
|
.uri(config.getLogtoEndpoint() + "/api/organizations/" + orgId + "/users/" + userId)
|
||||||
|
.header("Authorization", "Bearer " + getAccessToken())
|
||||||
|
.retrieve()
|
||||||
|
.toBodilessEntity();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to remove user from org: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a user in Logto and add to organization with role. */
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public String createAndInviteUser(String email, String orgId, String roleId) {
|
||||||
|
if (!isAvailable()) return null;
|
||||||
|
try {
|
||||||
|
var userResp = (Map<String, Object>) restClient.post()
|
||||||
|
.uri(config.getLogtoEndpoint() + "/api/users")
|
||||||
|
.header("Authorization", "Bearer " + getAccessToken())
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.body(Map.of("primaryEmail", email, "name", email.split("@")[0]))
|
||||||
|
.retrieve()
|
||||||
|
.body(Map.class);
|
||||||
|
String userId = String.valueOf(userResp.get("id"));
|
||||||
|
addUserToOrganization(orgId, userId);
|
||||||
|
if (roleId != null) {
|
||||||
|
assignOrganizationRole(orgId, userId, roleId);
|
||||||
|
}
|
||||||
|
return userId;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to create and invite user: {}", e.getMessage());
|
||||||
|
throw new RuntimeException("Invite failed: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** List available organization roles. */
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public List<Map<String, Object>> listOrganizationRoles() {
|
||||||
|
if (!isAvailable()) return List.of();
|
||||||
|
try {
|
||||||
|
var resp = restClient.get()
|
||||||
|
.uri(config.getLogtoEndpoint() + "/api/organization-roles")
|
||||||
|
.header("Authorization", "Bearer " + getAccessToken())
|
||||||
|
.retrieve()
|
||||||
|
.body(List.class);
|
||||||
|
return resp != null ? resp : List.of();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to list org roles: {}", e.getMessage());
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final String MGMT_API_RESOURCE = "https://default.logto.app/api";
|
private static final String MGMT_API_RESOURCE = "https://default.logto.app/api";
|
||||||
|
|
||||||
private synchronized String getAccessToken() {
|
private synchronized String getAccessToken() {
|
||||||
|
|||||||
@@ -72,18 +72,60 @@ public class ServerApiClient {
|
|||||||
.toBodilessEntity();
|
.toBodilessEntity();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Health check for a specific tenant's server. */
|
||||||
* Check server health.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public Map<String, Object> getHealth(String serverEndpoint) {
|
public ServerHealthResponse getHealth(String serverEndpoint) {
|
||||||
return RestClient.create(serverEndpoint)
|
try {
|
||||||
.get()
|
String url = serverEndpoint + "/actuator/health";
|
||||||
.uri("/api/v1/health")
|
var resp = RestClient.create().get().uri(url)
|
||||||
|
.header("Authorization", "Bearer " + getAccessToken())
|
||||||
|
.header("X-Cameleer-Protocol-Version", "1")
|
||||||
.retrieve()
|
.retrieve()
|
||||||
.body(Map.class);
|
.body(Map.class);
|
||||||
|
String status = resp != null ? String.valueOf(resp.get("status")) : "UNKNOWN";
|
||||||
|
return new ServerHealthResponse("UP".equals(status), status);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Health check failed for {}: {}", serverEndpoint, e.getMessage());
|
||||||
|
return new ServerHealthResponse(false, "DOWN");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Push OIDC configuration to a tenant's server. */
|
||||||
|
public void pushOidcConfig(String serverEndpoint, Map<String, Object> oidcConfig) {
|
||||||
|
try {
|
||||||
|
RestClient.create().put()
|
||||||
|
.uri(serverEndpoint + "/api/admin/oidc")
|
||||||
|
.header("Authorization", "Bearer " + getAccessToken())
|
||||||
|
.header("X-Cameleer-Protocol-Version", "1")
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.body(oidcConfig)
|
||||||
|
.retrieve()
|
||||||
|
.toBodilessEntity();
|
||||||
|
log.info("Pushed OIDC config to {}", serverEndpoint);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to push OIDC config to {}: {}", serverEndpoint, e.getMessage());
|
||||||
|
throw new RuntimeException("OIDC config push failed: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get OIDC configuration from a tenant's server. */
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public Map<String, Object> getOidcConfig(String serverEndpoint) {
|
||||||
|
try {
|
||||||
|
return RestClient.create().get()
|
||||||
|
.uri(serverEndpoint + "/api/admin/oidc")
|
||||||
|
.header("Authorization", "Bearer " + getAccessToken())
|
||||||
|
.header("X-Cameleer-Protocol-Version", "1")
|
||||||
|
.retrieve()
|
||||||
|
.body(Map.class);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to get OIDC config from {}: {}", serverEndpoint, e.getMessage());
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ServerHealthResponse(boolean healthy, String status) {}
|
||||||
|
|
||||||
private synchronized String getAccessToken() {
|
private synchronized String getAccessToken() {
|
||||||
if (cachedToken != null && Instant.now().isBefore(tokenExpiry.minusSeconds(60))) {
|
if (cachedToken != null && Instant.now().isBefore(tokenExpiry.minusSeconds(60))) {
|
||||||
return cachedToken;
|
return cachedToken;
|
||||||
|
|||||||
Reference in New Issue
Block a user