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 synchronized String getAccessToken() {
|
||||
|
||||
@@ -72,18 +72,60 @@ public class ServerApiClient {
|
||||
.toBodilessEntity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check server health.
|
||||
*/
|
||||
/** Health check for a specific tenant's server. */
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, Object> getHealth(String serverEndpoint) {
|
||||
return RestClient.create(serverEndpoint)
|
||||
.get()
|
||||
.uri("/api/v1/health")
|
||||
public ServerHealthResponse getHealth(String serverEndpoint) {
|
||||
try {
|
||||
String url = serverEndpoint + "/actuator/health";
|
||||
var resp = RestClient.create().get().uri(url)
|
||||
.header("Authorization", "Bearer " + getAccessToken())
|
||||
.header("X-Cameleer-Protocol-Version", "1")
|
||||
.retrieve()
|
||||
.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() {
|
||||
if (cachedToken != null && Instant.now().isBefore(tokenExpiry.minusSeconds(60))) {
|
||||
return cachedToken;
|
||||
|
||||
Reference in New Issue
Block a user