Swagger improvements

This commit is contained in:
Igor Kulikov 2021-11-03 15:03:56 +02:00
parent 113a6389fa
commit 34d6dc50b5
4 changed files with 124 additions and 34 deletions

View File

@ -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 ->

View File

@ -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;
} }

View File

@ -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;
} }

View File

@ -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>