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;
import com.fasterxml.classmate.ResolvedType;
import com.fasterxml.classmate.TypeResolver;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.beans.factory.annotation.Autowired;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
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.exception.ThingsboardCredentialsExpiredResponse;
import org.thingsboard.server.exception.ThingsboardErrorResponse;
import org.thingsboard.server.service.security.auth.rest.LoginRequest;
import org.thingsboard.server.service.security.auth.rest.LoginResponse;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ExampleBuilder;
import springfox.documentation.builders.OperationBuilder;
import springfox.documentation.builders.RepresentationBuilder;
import springfox.documentation.builders.RequestParameterBuilder;
import springfox.documentation.builders.ResponseBuilder;
import springfox.documentation.schema.Example;
import springfox.documentation.service.ApiDescription;
import springfox.documentation.service.ApiInfo;
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.regex;
@Slf4j
@Configuration
public class SwaggerConfiguration {
@ -100,44 +106,30 @@ public class SwaggerConfiguration {
@Value("${swagger.version}")
private String version;
@Autowired
private CachingOperationNameGenerator operationNames;
@Bean
public Docket thingsboardApi() {
TypeResolver typeResolver = new TypeResolver();
final ResolvedType jsonNodeType =
typeResolver.resolve(
JsonNode.class);
final ResolvedType stringType =
typeResolver.resolve(
String.class);
return new Docket(DocumentationType.OAS_30)
.groupName("thingsboard")
.apiInfo(apiInfo())
.additionalModels(
typeResolver.resolve(ThingsboardErrorResponse.class),
typeResolver.resolve(ThingsboardCredentialsExpiredResponse.class),
typeResolver.resolve(LoginRequest.class),
typeResolver.resolve(LoginResponse.class)
)
/* .alternateTypeRules(
new AlternateTypeRule(
jsonNodeType,
stringType))*/
.select()
.paths(apiPaths())
.paths(any())
.build()
.globalResponses(HttpMethod.GET,
List.of(
new ResponseBuilder()
.code("401")
.description("Unauthorized")
.representation(MediaType.APPLICATION_JSON)
.apply(classRepresentation(ThingsboardErrorResponse.class, true))
.build()
defaultErrorResponses(false)
)
.globalResponses(HttpMethod.POST,
defaultErrorResponses(true)
)
.globalResponses(HttpMethod.DELETE,
defaultErrorResponses(false)
)
.securitySchemes(newArrayList(httpLogin()))
.securityContexts(newArrayList(securityContext()))
@ -146,15 +138,15 @@ public class SwaggerConfiguration {
@Bean
@Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER)
ApiListingScannerPlugin loginEndpointListingScanner() {
ApiListingScannerPlugin loginEndpointListingScanner(final CachingOperationNameGenerator operationNames) {
return new ApiListingScannerPlugin() {
@Override
public List<ApiDescription> apply(DocumentationContext context) {
return List.of(loginEndpointApiDescription());
return List.of(loginEndpointApiDescription(operationNames));
}
@Override
public boolean supports(DocumentationType delimiter) {
public boolean supports(@NotNull DocumentationType delimiter) {
return DocumentationType.SWAGGER_2.equals(delimiter) || DocumentationType.OAS_30.equals(delimiter);
}
};
@ -176,7 +168,7 @@ public class SwaggerConfiguration {
}
@Override
public boolean supports(DocumentationType delimiter) {
public boolean supports(@NotNull DocumentationType delimiter) {
return DocumentationType.SWAGGER_2.equals(delimiter) || DocumentationType.OAS_30.equals(delimiter);
}
};
@ -199,6 +191,7 @@ public class SwaggerConfiguration {
.showCommonExtensions(false)
.supportedSubmitMethods(UiConfiguration.Constants.DEFAULT_SUBMIT_METHODS)
.validatorUrl(null)
.persistAuthorization(true)
.syntaxHighlightActivate(true)
.syntaxHighlightTheme("agate")
.build();
@ -248,7 +241,7 @@ public class SwaggerConfiguration {
.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(
new OperationBuilder(operationNames)
.summary("Login method to get user JWT token data")
@ -257,7 +250,8 @@ public class SwaggerConfiguration {
.position(0)
.codegenMethodNameStem("loginPost")
.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(
List.of(
new RequestParameterBuilder()
@ -278,7 +272,8 @@ public class SwaggerConfiguration {
}
private Collection<Response> loginResponses() {
return List.of(
List<Response> responses = new ArrayList<>();
responses.add(
new ResponseBuilder()
.code("200")
.description("OK")
@ -286,11 +281,82 @@ public class SwaggerConfiguration {
.apply(classRepresentation(LoginResponse.class, true)).
build()
);
responses.addAll(loginErrorResponses());
return responses;
}
/** 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(
m ->
m.referenceModel(ref ->

View File

@ -15,9 +15,12 @@
*/
package org.thingsboard.server.exception;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import org.springframework.http.HttpStatus;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
@ApiModel
public class ThingsboardCredentialsExpiredResponse extends ThingsboardErrorResponse {
private final String resetToken;
@ -31,6 +34,7 @@ public class ThingsboardCredentialsExpiredResponse extends ThingsboardErrorRespo
return new ThingsboardCredentialsExpiredResponse(message, resetToken);
}
@ApiModelProperty(position = 5, value = "Password reset token", readOnly = true)
public String getResetToken() {
return resetToken;
}

View File

@ -15,11 +15,14 @@
*/
package org.thingsboard.server.exception;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import org.springframework.http.HttpStatus;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import java.util.Date;
@ApiModel
public class ThingsboardErrorResponse {
// HTTP Response Status Code
private final HttpStatus status;
@ -43,18 +46,35 @@ public class ThingsboardErrorResponse {
return new ThingsboardErrorResponse(message, errorCode, status);
}
@ApiModelProperty(position = 1, value = "HTTP Response Status Code", example = "401", readOnly = true)
public Integer getStatus() {
return status.value();
}
@ApiModelProperty(position = 2, value = "Error message", example = "Authentication failed", readOnly = true)
public String getMessage() {
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() {
return errorCode;
}
@ApiModelProperty(position = 4, value = "Timestamp", readOnly = true)
public Date getTimestamp() {
return timestamp;
}

View File

@ -83,7 +83,7 @@
<rabbitmq.version>4.8.0</rabbitmq.version>
<surfire.version>2.19.1</surfire.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>
<spatial4j.version>0.7</spatial4j.version>
<jts.version>1.15.0</jts.version>