feat: implemented RabbitMQ RPC endpoint for query user info.
CI for publishing docker image / build-and-publish (push) Successful in 3m34s
Unit test coverage / gradle-test (push) Failing after 6m31s

ISSUES RELATED: clevermicro/user-management#34
This commit is contained in:
2025-07-21 18:36:39 +08:00
parent 16c040b51b
commit ec7052ddee
7 changed files with 143 additions and 16 deletions
@@ -26,7 +26,7 @@ public abstract class AbstractEndpoint implements AutoCloseable {
private final String handlerTag; private final String handlerTag;
private final CleverMicroAmqpClient client; private final CleverMicroAmqpClient client;
private final ObjectMapper objectMapper; protected final ObjectMapper objectMapper;
private final String queueName; private final String queueName;
@@ -2,20 +2,38 @@ package com.cleverthis.authservice.controller.rabbitmq.model;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/** /**
* The model for replied response. * The model for replied response.
*/ */
@Data @EqualsAndHashCode(callSuper = true)
@Getter
@Setter
@ToString
@Builder @Builder
public class EndpointErrorResponse { public final class EndpointErrorResponse extends EndpointResponse {
@JsonProperty("type")
private final String type = "ERROR";
@JsonProperty("exception") @JsonProperty("exception")
private final String exception; private final String exception;
@JsonProperty("message") @JsonProperty("message")
private final String message; private final String message;
@JsonProperty("additional_info") @JsonProperty("additional_info")
private final Object additionalInfo; private final Object additionalInfo;
/**
* Create an error response.
*/
public EndpointErrorResponse(
final String exception,
final String message,
final Object additionalInfo
) {
super("ERROR");
this.exception = exception;
this.message = message;
this.additionalInfo = additionalInfo;
}
} }
@@ -3,17 +3,25 @@ package com.cleverthis.authservice.controller.rabbitmq.model;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List; import java.util.List;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/** /**
* The model for replied response. * The model for replied response.
*/ */
@Data @EqualsAndHashCode(callSuper = true)
@Getter
@Setter
@ToString
@Builder @Builder
public class EndpointOkResponse { public class EndpointOkResponse extends EndpointResponse {
@JsonProperty("type")
private final String type = "OK";
@JsonProperty("result") @JsonProperty("result")
private final List<?> result; private final List<?> result;
public EndpointOkResponse(final List<?> result) {
super("OK");
this.result = result;
}
} }
@@ -0,0 +1,19 @@
package com.cleverthis.authservice.controller.rabbitmq.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.Setter;
/**
* The model for replied response.
*/
@Getter
@Setter
public abstract class EndpointResponse {
@JsonProperty("type")
protected final String type;
protected EndpointResponse(final String type) {
this.type = type;
}
}
@@ -0,0 +1,71 @@
package com.cleverthis.authservice.controller.rabbitmq.v1.user;
import com.cleverthis.authservice.controller.rabbitmq.AbstractEndpoint;
import com.cleverthis.authservice.controller.rabbitmq.exception.EndpointBadRequestException;
import com.cleverthis.authservice.controller.rabbitmq.exception.EndpointException;
import com.cleverthis.authservice.controller.rabbitmq.model.EndpointRequest;
import com.cleverthis.authservice.controller.rabbitmq.model.EndpointResponses;
import com.cleverthis.authservice.service.impl.KeycloakAdminReactiveClient;
import com.cleverthis.clevermicro.client.amqp.CleverMicroAmqpClient;
import com.cleverthis.clevermicro.client.amqp.handler.HandlerContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.NoSuchElementException;
import org.keycloak.representations.idm.UserRepresentation;
import org.springframework.stereotype.Component;
/**
* Serve endpoints on `user-service-v1`.
*/
@Component
public class UserEndpointV1 extends AbstractEndpoint {
private static final String ROUTING_KEY = "user-service-v1";
private final KeycloakAdminReactiveClient keycloakAdminClient;
public UserEndpointV1(
final CleverMicroAmqpClient client,
final ObjectMapper objectMapper,
final KeycloakAdminReactiveClient keycloakAdminClient
) throws IOException {
super(client, objectMapper, ROUTING_KEY);
this.keycloakAdminClient = keycloakAdminClient;
}
@Override
protected void handleRequest(
final HandlerContext ctx, final EndpointRequest request
) throws EndpointException {
//noinspection SwitchStatementWithTooFewBranches
final Object result = switch (request.getMethod()) {
case "queryUserById" -> queryUserById(request);
default -> throw new EndpointBadRequestException("Method not found");
};
reply(ctx, EndpointResponses.ok(result));
}
private UserRepresentation queryUserById(
final EndpointRequest request
) throws EndpointException {
final var argId = request.getArgs().get("id");
if (argId == null) {
throw new EndpointBadRequestException("Missing parameter 'id'");
}
if (!argId.isTextual()) {
throw new EndpointBadRequestException(
"Invalid type for parameter 'id', required non-null string");
}
// this should be non-null
final var id = argId.textValue();
assert id != null;
try {
return this.keycloakAdminClient.getUserDetails(id).block();
} catch (final NoSuchElementException ex) {
throw new EndpointException(
"cleverthis.clevermicro.auth.user_not_found",
"User id " + id + "not found",
ex
);
}
}
}
@@ -6,9 +6,11 @@ import com.cleverthis.authservice.dto.keycloakadmin.KeycloakInvitationRequest;
import com.cleverthis.authservice.exception.RegistrationException; import com.cleverthis.authservice.exception.RegistrationException;
import com.cleverthis.authservice.exception.ServiceUnavailableException; import com.cleverthis.authservice.exception.ServiceUnavailableException;
import com.cleverthis.authservice.exception.UserAlreadyExistsException; import com.cleverthis.authservice.exception.UserAlreadyExistsException;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.NoSuchElementException;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.Keycloak;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
@@ -460,9 +462,18 @@ public class KeycloakAdminReactiveClient {
} }
/**
* Get user details with id.
*
* @throws NoSuchElementException in mono if keycloak returns 404.
*/
public Mono<UserRepresentation> getUserDetails(String userId) { public Mono<UserRepresentation> getUserDetails(String userId) {
return Mono.just(this.keycloak.realm(this.realm) try {
.users().get(userId).toRepresentation()); return Mono.just(this.keycloak.realm(this.realm)
.users().get(userId).toRepresentation());
} catch (final NotFoundException ex) {
return Mono.error(new NoSuchElementException("User with id " + userId + " not exists"));
}
} }
} }
+3 -3
View File
@@ -7,7 +7,7 @@ spring:
name: auth-service name: auth-service
cloud: cloud:
consul: consul:
host: ${CONSUL_HOST:localhost} host: ${CONSUL_HOST:127.0.0.1}
port: ${CONSUL_PORT:8500} port: ${CONSUL_PORT:8500}
config: config:
import-check: import-check:
@@ -15,8 +15,8 @@ spring:
# Keycloak Configuration (adjust values as needed) # Keycloak Configuration (adjust values as needed)
keycloak: keycloak:
keycloak-host: ${KEYCLOAK_AUTH_SERVER_URL:http://localhost:9100} # Base URL of your Keycloak instance (NO trailing slash) keycloak-host: ${KEYCLOAK_AUTH_SERVER_URL:http://keycloak.dev.localhost} # Base URL of your Keycloak instance (NO trailing slash)
keycloak-admin-host: ${KEYCLOAK_AUTH_ADMIN_SERVER_URL:http://localhost:9100} # Base URL of your Keycloak instance (NO trailing slash) keycloak-admin-host: ${KEYCLOAK_AUTH_ADMIN_SERVER_URL:http://keycloak.dev.localhost} # Base URL of your Keycloak instance (NO trailing slash)
realm: ${KEYCLOAK_AUTH_REALM:clevermicro-dev} # The realm name you are using realm: ${KEYCLOAK_AUTH_REALM:clevermicro-dev} # The realm name you are using
client-id: ${KEYCLOAK_AUTH_SERVICE_CLIENT:auth-service} # Client ID created in Keycloak for this service client-id: ${KEYCLOAK_AUTH_SERVICE_CLIENT:auth-service} # Client ID created in Keycloak for this service
client-secret: ${KEYCLOAK_AUTH_SERVICE_SECRET:UJ1sMcWciIRREfjjAGoLHUDynLT4aYdD} # Client Secret from Keycloak (use secrets management!) client-secret: ${KEYCLOAK_AUTH_SERVICE_SECRET:UJ1sMcWciIRREfjjAGoLHUDynLT4aYdD} # Client Secret from Keycloak (use secrets management!)