Swagger improvements
This commit is contained in:
parent
113a6389fa
commit
34d6dc50b5
@ -15,25 +15,30 @@
|
|||||||
*/
|
*/
|
||||||
package org.thingsboard.server.config;
|
package org.thingsboard.server.config;
|
||||||
|
|
||||||
import com.fasterxml.classmate.ResolvedType;
|
|
||||||
import com.fasterxml.classmate.TypeResolver;
|
import com.fasterxml.classmate.TypeResolver;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.core.annotation.Order;
|
import org.springframework.core.annotation.Order;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
|
||||||
import org.thingsboard.server.common.data.security.Authority;
|
import org.thingsboard.server.common.data.security.Authority;
|
||||||
|
import org.thingsboard.server.exception.ThingsboardCredentialsExpiredResponse;
|
||||||
import org.thingsboard.server.exception.ThingsboardErrorResponse;
|
import org.thingsboard.server.exception.ThingsboardErrorResponse;
|
||||||
import org.thingsboard.server.service.security.auth.rest.LoginRequest;
|
import org.thingsboard.server.service.security.auth.rest.LoginRequest;
|
||||||
import org.thingsboard.server.service.security.auth.rest.LoginResponse;
|
import org.thingsboard.server.service.security.auth.rest.LoginResponse;
|
||||||
import springfox.documentation.builders.ApiInfoBuilder;
|
import springfox.documentation.builders.ApiInfoBuilder;
|
||||||
|
import springfox.documentation.builders.ExampleBuilder;
|
||||||
import springfox.documentation.builders.OperationBuilder;
|
import springfox.documentation.builders.OperationBuilder;
|
||||||
import springfox.documentation.builders.RepresentationBuilder;
|
import springfox.documentation.builders.RepresentationBuilder;
|
||||||
import springfox.documentation.builders.RequestParameterBuilder;
|
import springfox.documentation.builders.RequestParameterBuilder;
|
||||||
import springfox.documentation.builders.ResponseBuilder;
|
import springfox.documentation.builders.ResponseBuilder;
|
||||||
|
import springfox.documentation.schema.Example;
|
||||||
import springfox.documentation.service.ApiDescription;
|
import springfox.documentation.service.ApiDescription;
|
||||||
import springfox.documentation.service.ApiInfo;
|
import springfox.documentation.service.ApiInfo;
|
||||||
import springfox.documentation.service.ApiListing;
|
import springfox.documentation.service.ApiListing;
|
||||||
@ -74,6 +79,7 @@ import static java.util.function.Predicate.not;
|
|||||||
import static springfox.documentation.builders.PathSelectors.any;
|
import static springfox.documentation.builders.PathSelectors.any;
|
||||||
import static springfox.documentation.builders.PathSelectors.regex;
|
import static springfox.documentation.builders.PathSelectors.regex;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Configuration
|
@Configuration
|
||||||
public class SwaggerConfiguration {
|
public class SwaggerConfiguration {
|
||||||
|
|
||||||
@ -100,44 +106,30 @@ public class SwaggerConfiguration {
|
|||||||
@Value("${swagger.version}")
|
@Value("${swagger.version}")
|
||||||
private String version;
|
private String version;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private CachingOperationNameGenerator operationNames;
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public Docket thingsboardApi() {
|
public Docket thingsboardApi() {
|
||||||
TypeResolver typeResolver = new TypeResolver();
|
TypeResolver typeResolver = new TypeResolver();
|
||||||
final ResolvedType jsonNodeType =
|
|
||||||
typeResolver.resolve(
|
|
||||||
JsonNode.class);
|
|
||||||
final ResolvedType stringType =
|
|
||||||
typeResolver.resolve(
|
|
||||||
String.class);
|
|
||||||
|
|
||||||
return new Docket(DocumentationType.OAS_30)
|
return new Docket(DocumentationType.OAS_30)
|
||||||
.groupName("thingsboard")
|
.groupName("thingsboard")
|
||||||
.apiInfo(apiInfo())
|
.apiInfo(apiInfo())
|
||||||
.additionalModels(
|
.additionalModels(
|
||||||
typeResolver.resolve(ThingsboardErrorResponse.class),
|
typeResolver.resolve(ThingsboardErrorResponse.class),
|
||||||
|
typeResolver.resolve(ThingsboardCredentialsExpiredResponse.class),
|
||||||
typeResolver.resolve(LoginRequest.class),
|
typeResolver.resolve(LoginRequest.class),
|
||||||
typeResolver.resolve(LoginResponse.class)
|
typeResolver.resolve(LoginResponse.class)
|
||||||
)
|
)
|
||||||
/* .alternateTypeRules(
|
|
||||||
new AlternateTypeRule(
|
|
||||||
jsonNodeType,
|
|
||||||
stringType))*/
|
|
||||||
.select()
|
.select()
|
||||||
.paths(apiPaths())
|
.paths(apiPaths())
|
||||||
.paths(any())
|
.paths(any())
|
||||||
.build()
|
.build()
|
||||||
.globalResponses(HttpMethod.GET,
|
.globalResponses(HttpMethod.GET,
|
||||||
List.of(
|
defaultErrorResponses(false)
|
||||||
new ResponseBuilder()
|
)
|
||||||
.code("401")
|
.globalResponses(HttpMethod.POST,
|
||||||
.description("Unauthorized")
|
defaultErrorResponses(true)
|
||||||
.representation(MediaType.APPLICATION_JSON)
|
)
|
||||||
.apply(classRepresentation(ThingsboardErrorResponse.class, true))
|
.globalResponses(HttpMethod.DELETE,
|
||||||
.build()
|
defaultErrorResponses(false)
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.securitySchemes(newArrayList(httpLogin()))
|
.securitySchemes(newArrayList(httpLogin()))
|
||||||
.securityContexts(newArrayList(securityContext()))
|
.securityContexts(newArrayList(securityContext()))
|
||||||
@ -146,15 +138,15 @@ public class SwaggerConfiguration {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER)
|
@Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER)
|
||||||
ApiListingScannerPlugin loginEndpointListingScanner() {
|
ApiListingScannerPlugin loginEndpointListingScanner(final CachingOperationNameGenerator operationNames) {
|
||||||
return new ApiListingScannerPlugin() {
|
return new ApiListingScannerPlugin() {
|
||||||
@Override
|
@Override
|
||||||
public List<ApiDescription> apply(DocumentationContext context) {
|
public List<ApiDescription> apply(DocumentationContext context) {
|
||||||
return List.of(loginEndpointApiDescription());
|
return List.of(loginEndpointApiDescription(operationNames));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supports(DocumentationType delimiter) {
|
public boolean supports(@NotNull DocumentationType delimiter) {
|
||||||
return DocumentationType.SWAGGER_2.equals(delimiter) || DocumentationType.OAS_30.equals(delimiter);
|
return DocumentationType.SWAGGER_2.equals(delimiter) || DocumentationType.OAS_30.equals(delimiter);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -176,7 +168,7 @@ public class SwaggerConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supports(DocumentationType delimiter) {
|
public boolean supports(@NotNull DocumentationType delimiter) {
|
||||||
return DocumentationType.SWAGGER_2.equals(delimiter) || DocumentationType.OAS_30.equals(delimiter);
|
return DocumentationType.SWAGGER_2.equals(delimiter) || DocumentationType.OAS_30.equals(delimiter);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -199,6 +191,7 @@ public class SwaggerConfiguration {
|
|||||||
.showCommonExtensions(false)
|
.showCommonExtensions(false)
|
||||||
.supportedSubmitMethods(UiConfiguration.Constants.DEFAULT_SUBMIT_METHODS)
|
.supportedSubmitMethods(UiConfiguration.Constants.DEFAULT_SUBMIT_METHODS)
|
||||||
.validatorUrl(null)
|
.validatorUrl(null)
|
||||||
|
.persistAuthorization(true)
|
||||||
.syntaxHighlightActivate(true)
|
.syntaxHighlightActivate(true)
|
||||||
.syntaxHighlightTheme("agate")
|
.syntaxHighlightTheme("agate")
|
||||||
.build();
|
.build();
|
||||||
@ -248,7 +241,7 @@ public class SwaggerConfiguration {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private ApiDescription loginEndpointApiDescription() {
|
private ApiDescription loginEndpointApiDescription(final CachingOperationNameGenerator operationNames) {
|
||||||
return new ApiDescription(null, "/api/auth/login", "Login method to get user JWT token data", "Login endpoint", Collections.singletonList(
|
return new ApiDescription(null, "/api/auth/login", "Login method to get user JWT token data", "Login endpoint", Collections.singletonList(
|
||||||
new OperationBuilder(operationNames)
|
new OperationBuilder(operationNames)
|
||||||
.summary("Login method to get user JWT token data")
|
.summary("Login method to get user JWT token data")
|
||||||
@ -257,7 +250,8 @@ public class SwaggerConfiguration {
|
|||||||
.position(0)
|
.position(0)
|
||||||
.codegenMethodNameStem("loginPost")
|
.codegenMethodNameStem("loginPost")
|
||||||
.method(HttpMethod.POST)
|
.method(HttpMethod.POST)
|
||||||
.notes("Login method to get user JWT token data.\n\nValue of the response **token** field can be used as JWT token value for authorization.")
|
.notes("Login method used to authenticate user and get JWT token data.\n\nValue of the response **token** " +
|
||||||
|
"field can be used as **X-Authorization** header value:\n\n`X-Authorization: Bearer $JWT_TOKEN_VALUE`.")
|
||||||
.requestParameters(
|
.requestParameters(
|
||||||
List.of(
|
List.of(
|
||||||
new RequestParameterBuilder()
|
new RequestParameterBuilder()
|
||||||
@ -278,7 +272,8 @@ public class SwaggerConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Collection<Response> loginResponses() {
|
private Collection<Response> loginResponses() {
|
||||||
return List.of(
|
List<Response> responses = new ArrayList<>();
|
||||||
|
responses.add(
|
||||||
new ResponseBuilder()
|
new ResponseBuilder()
|
||||||
.code("200")
|
.code("200")
|
||||||
.description("OK")
|
.description("OK")
|
||||||
@ -286,11 +281,82 @@ public class SwaggerConfiguration {
|
|||||||
.apply(classRepresentation(LoginResponse.class, true)).
|
.apply(classRepresentation(LoginResponse.class, true)).
|
||||||
build()
|
build()
|
||||||
);
|
);
|
||||||
|
responses.addAll(loginErrorResponses());
|
||||||
|
return responses;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Helper methods **/
|
/** Helper methods **/
|
||||||
|
|
||||||
private Consumer<RepresentationBuilder> classRepresentation(Class clazz, boolean isResponse) {
|
private List<Response> defaultErrorResponses(boolean isPost) {
|
||||||
|
return List.of(
|
||||||
|
errorResponse("400", "Bad Request",
|
||||||
|
ThingsboardErrorResponse.of(isPost ? "Invalid request body" : "Invalid UUID string: 123", ThingsboardErrorCode.BAD_REQUEST_PARAMS, HttpStatus.BAD_REQUEST)),
|
||||||
|
errorResponse("401", "Unauthorized",
|
||||||
|
ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)),
|
||||||
|
errorResponse("403", "Forbidden",
|
||||||
|
ThingsboardErrorResponse.of("You don't have permission to perform this operation!",
|
||||||
|
ThingsboardErrorCode.PERMISSION_DENIED, HttpStatus.FORBIDDEN)),
|
||||||
|
errorResponse("404", "Not Found",
|
||||||
|
ThingsboardErrorResponse.of("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND, HttpStatus.NOT_FOUND)),
|
||||||
|
errorResponse("429", "Too Many Requests",
|
||||||
|
ThingsboardErrorResponse.of("Too many requests for current tenant!",
|
||||||
|
ThingsboardErrorCode.TOO_MANY_REQUESTS, HttpStatus.TOO_MANY_REQUESTS))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Response> loginErrorResponses() {
|
||||||
|
return List.of(
|
||||||
|
errorResponse("401", "Unauthorized",
|
||||||
|
List.of(
|
||||||
|
errorExample("bad-credentials", "Bad credentials",
|
||||||
|
ThingsboardErrorResponse.of("Invalid username or password", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)),
|
||||||
|
errorExample("token-expired", "JWT token expired",
|
||||||
|
ThingsboardErrorResponse.of("Token has expired", ThingsboardErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED)),
|
||||||
|
errorExample("account-disabled", "Disabled account",
|
||||||
|
ThingsboardErrorResponse.of("User account is not active", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)),
|
||||||
|
errorExample("account-locked", "Locked account",
|
||||||
|
ThingsboardErrorResponse.of("User account is locked due to security policy", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)),
|
||||||
|
errorExample("authentication-failed", "General authentication error",
|
||||||
|
ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
errorResponse("401 ", "Unauthorized (**Expired credentials**)",
|
||||||
|
List.of(
|
||||||
|
errorExample("credentials-expired", "Expired credentials",
|
||||||
|
ThingsboardCredentialsExpiredResponse.of("User password expired!", RandomStringUtils.randomAlphanumeric(30)))
|
||||||
|
), ThingsboardCredentialsExpiredResponse.class
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Response errorResponse(String code, String description, ThingsboardErrorResponse example) {
|
||||||
|
return errorResponse(code, description, List.of(errorExample("error-code-" + code, description, example)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Response errorResponse(String code, String description, List<Example> examples) {
|
||||||
|
return errorResponse(code, description, examples, ThingsboardErrorResponse.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Response errorResponse(String code, String description, List<Example> examples,
|
||||||
|
Class<? extends ThingsboardErrorResponse> errorResponseClass) {
|
||||||
|
return new ResponseBuilder()
|
||||||
|
.code(code)
|
||||||
|
.description(description)
|
||||||
|
.examples(examples)
|
||||||
|
.representation(MediaType.APPLICATION_JSON)
|
||||||
|
.apply(classRepresentation(errorResponseClass, true))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Example errorExample(String id, String summary, ThingsboardErrorResponse example) {
|
||||||
|
return new ExampleBuilder()
|
||||||
|
.mediaType(MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
.summary(summary)
|
||||||
|
.id(id)
|
||||||
|
.value(example).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Consumer<RepresentationBuilder> classRepresentation(Class<?> clazz, boolean isResponse) {
|
||||||
return r -> r.model(
|
return r -> r.model(
|
||||||
m ->
|
m ->
|
||||||
m.referenceModel(ref ->
|
m.referenceModel(ref ->
|
||||||
|
|||||||
@ -15,9 +15,12 @@
|
|||||||
*/
|
*/
|
||||||
package org.thingsboard.server.exception;
|
package org.thingsboard.server.exception;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
|
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
|
||||||
|
|
||||||
|
@ApiModel
|
||||||
public class ThingsboardCredentialsExpiredResponse extends ThingsboardErrorResponse {
|
public class ThingsboardCredentialsExpiredResponse extends ThingsboardErrorResponse {
|
||||||
|
|
||||||
private final String resetToken;
|
private final String resetToken;
|
||||||
@ -31,6 +34,7 @@ public class ThingsboardCredentialsExpiredResponse extends ThingsboardErrorRespo
|
|||||||
return new ThingsboardCredentialsExpiredResponse(message, resetToken);
|
return new ThingsboardCredentialsExpiredResponse(message, resetToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiModelProperty(position = 5, value = "Password reset token", readOnly = true)
|
||||||
public String getResetToken() {
|
public String getResetToken() {
|
||||||
return resetToken;
|
return resetToken;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,11 +15,14 @@
|
|||||||
*/
|
*/
|
||||||
package org.thingsboard.server.exception;
|
package org.thingsboard.server.exception;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
|
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
|
@ApiModel
|
||||||
public class ThingsboardErrorResponse {
|
public class ThingsboardErrorResponse {
|
||||||
// HTTP Response Status Code
|
// HTTP Response Status Code
|
||||||
private final HttpStatus status;
|
private final HttpStatus status;
|
||||||
@ -43,18 +46,35 @@ public class ThingsboardErrorResponse {
|
|||||||
return new ThingsboardErrorResponse(message, errorCode, status);
|
return new ThingsboardErrorResponse(message, errorCode, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiModelProperty(position = 1, value = "HTTP Response Status Code", example = "401", readOnly = true)
|
||||||
public Integer getStatus() {
|
public Integer getStatus() {
|
||||||
return status.value();
|
return status.value();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiModelProperty(position = 2, value = "Error message", example = "Authentication failed", readOnly = true)
|
||||||
public String getMessage() {
|
public String getMessage() {
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiModelProperty(position = 3, value = "Platform error code:" +
|
||||||
|
"\n* `2` - General error (HTTP: 500 - Internal Server Error)" +
|
||||||
|
"\n\n* `10` - Authentication failed (HTTP: 401 - Unauthorized)" +
|
||||||
|
"\n\n* `11` - JWT token expired (HTTP: 401 - Unauthorized)" +
|
||||||
|
"\n\n* `15` - Credentials expired (HTTP: 401 - Unauthorized)" +
|
||||||
|
"\n\n* `20` - Permission denied (HTTP: 403 - Forbidden)" +
|
||||||
|
"\n\n* `30` - Invalid arguments (HTTP: 400 - Bad Request)" +
|
||||||
|
"\n\n* `31` - Bad request params (HTTP: 400 - Bad Request)" +
|
||||||
|
"\n\n* `32` - Item not found (HTTP: 404 - Not Found)" +
|
||||||
|
"\n\n* `33` - Too many requests (HTTP: 429 - Too Many Requests)" +
|
||||||
|
"\n\n* `34` - Too many updates (Too many updates over Websocket session)" +
|
||||||
|
"\n\n* `40` - Subscription violation (HTTP: 403 - Forbidden)",
|
||||||
|
example = "10", dataType = "integer",
|
||||||
|
readOnly = true)
|
||||||
public ThingsboardErrorCode getErrorCode() {
|
public ThingsboardErrorCode getErrorCode() {
|
||||||
return errorCode;
|
return errorCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiModelProperty(position = 4, value = "Timestamp", readOnly = true)
|
||||||
public Date getTimestamp() {
|
public Date getTimestamp() {
|
||||||
return timestamp;
|
return timestamp;
|
||||||
}
|
}
|
||||||
|
|||||||
2
pom.xml
2
pom.xml
@ -83,7 +83,7 @@
|
|||||||
<rabbitmq.version>4.8.0</rabbitmq.version>
|
<rabbitmq.version>4.8.0</rabbitmq.version>
|
||||||
<surfire.version>2.19.1</surfire.version>
|
<surfire.version>2.19.1</surfire.version>
|
||||||
<jar-plugin.version>3.0.2</jar-plugin.version>
|
<jar-plugin.version>3.0.2</jar-plugin.version>
|
||||||
<springfox-swagger.version>3.0.1</springfox-swagger.version>
|
<springfox-swagger.version>3.0.2</springfox-swagger.version>
|
||||||
<swagger-annotations.version>1.6.3</swagger-annotations.version>
|
<swagger-annotations.version>1.6.3</swagger-annotations.version>
|
||||||
<spatial4j.version>0.7</spatial4j.version>
|
<spatial4j.version>0.7</spatial4j.version>
|
||||||
<jts.version>1.15.0</jts.version>
|
<jts.version>1.15.0</jts.version>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user