Commit 34d6dc50b5e876c01400cd82bd653fb537e9f537

Authored by Igor Kulikov
1 parent 113a6389

Swagger improvements

@@ -15,25 +15,30 @@ @@ -15,25 +15,30 @@
15 */ 15 */
16 package org.thingsboard.server.config; 16 package org.thingsboard.server.config;
17 17
18 -import com.fasterxml.classmate.ResolvedType;  
19 import com.fasterxml.classmate.TypeResolver; 18 import com.fasterxml.classmate.TypeResolver;
20 -import com.fasterxml.jackson.databind.JsonNode;  
21 -import org.springframework.beans.factory.annotation.Autowired; 19 +import lombok.extern.slf4j.Slf4j;
  20 +import org.apache.commons.lang3.RandomStringUtils;
  21 +import org.jetbrains.annotations.NotNull;
22 import org.springframework.beans.factory.annotation.Value; 22 import org.springframework.beans.factory.annotation.Value;
23 import org.springframework.context.annotation.Bean; 23 import org.springframework.context.annotation.Bean;
24 import org.springframework.context.annotation.Configuration; 24 import org.springframework.context.annotation.Configuration;
25 import org.springframework.core.annotation.Order; 25 import org.springframework.core.annotation.Order;
26 import org.springframework.http.HttpMethod; 26 import org.springframework.http.HttpMethod;
  27 +import org.springframework.http.HttpStatus;
27 import org.springframework.http.MediaType; 28 import org.springframework.http.MediaType;
  29 +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
28 import org.thingsboard.server.common.data.security.Authority; 30 import org.thingsboard.server.common.data.security.Authority;
  31 +import org.thingsboard.server.exception.ThingsboardCredentialsExpiredResponse;
29 import org.thingsboard.server.exception.ThingsboardErrorResponse; 32 import org.thingsboard.server.exception.ThingsboardErrorResponse;
30 import org.thingsboard.server.service.security.auth.rest.LoginRequest; 33 import org.thingsboard.server.service.security.auth.rest.LoginRequest;
31 import org.thingsboard.server.service.security.auth.rest.LoginResponse; 34 import org.thingsboard.server.service.security.auth.rest.LoginResponse;
32 import springfox.documentation.builders.ApiInfoBuilder; 35 import springfox.documentation.builders.ApiInfoBuilder;
  36 +import springfox.documentation.builders.ExampleBuilder;
33 import springfox.documentation.builders.OperationBuilder; 37 import springfox.documentation.builders.OperationBuilder;
34 import springfox.documentation.builders.RepresentationBuilder; 38 import springfox.documentation.builders.RepresentationBuilder;
35 import springfox.documentation.builders.RequestParameterBuilder; 39 import springfox.documentation.builders.RequestParameterBuilder;
36 import springfox.documentation.builders.ResponseBuilder; 40 import springfox.documentation.builders.ResponseBuilder;
  41 +import springfox.documentation.schema.Example;
37 import springfox.documentation.service.ApiDescription; 42 import springfox.documentation.service.ApiDescription;
38 import springfox.documentation.service.ApiInfo; 43 import springfox.documentation.service.ApiInfo;
39 import springfox.documentation.service.ApiListing; 44 import springfox.documentation.service.ApiListing;
@@ -74,6 +79,7 @@ import static java.util.function.Predicate.not; @@ -74,6 +79,7 @@ import static java.util.function.Predicate.not;
74 import static springfox.documentation.builders.PathSelectors.any; 79 import static springfox.documentation.builders.PathSelectors.any;
75 import static springfox.documentation.builders.PathSelectors.regex; 80 import static springfox.documentation.builders.PathSelectors.regex;
76 81
  82 +@Slf4j
77 @Configuration 83 @Configuration
78 public class SwaggerConfiguration { 84 public class SwaggerConfiguration {
79 85
@@ -100,44 +106,30 @@ public class SwaggerConfiguration { @@ -100,44 +106,30 @@ public class SwaggerConfiguration {
100 @Value("${swagger.version}") 106 @Value("${swagger.version}")
101 private String version; 107 private String version;
102 108
103 - @Autowired  
104 - private CachingOperationNameGenerator operationNames;  
105 -  
106 @Bean 109 @Bean
107 public Docket thingsboardApi() { 110 public Docket thingsboardApi() {
108 TypeResolver typeResolver = new TypeResolver(); 111 TypeResolver typeResolver = new TypeResolver();
109 - final ResolvedType jsonNodeType =  
110 - typeResolver.resolve(  
111 - JsonNode.class);  
112 - final ResolvedType stringType =  
113 - typeResolver.resolve(  
114 - String.class);  
115 -  
116 return new Docket(DocumentationType.OAS_30) 112 return new Docket(DocumentationType.OAS_30)
117 .groupName("thingsboard") 113 .groupName("thingsboard")
118 .apiInfo(apiInfo()) 114 .apiInfo(apiInfo())
119 .additionalModels( 115 .additionalModels(
120 typeResolver.resolve(ThingsboardErrorResponse.class), 116 typeResolver.resolve(ThingsboardErrorResponse.class),
  117 + typeResolver.resolve(ThingsboardCredentialsExpiredResponse.class),
121 typeResolver.resolve(LoginRequest.class), 118 typeResolver.resolve(LoginRequest.class),
122 typeResolver.resolve(LoginResponse.class) 119 typeResolver.resolve(LoginResponse.class)
123 ) 120 )
124 - /* .alternateTypeRules(  
125 - new AlternateTypeRule(  
126 - jsonNodeType,  
127 - stringType))*/  
128 .select() 121 .select()
129 .paths(apiPaths()) 122 .paths(apiPaths())
130 .paths(any()) 123 .paths(any())
131 .build() 124 .build()
132 .globalResponses(HttpMethod.GET, 125 .globalResponses(HttpMethod.GET,
133 - List.of(  
134 - new ResponseBuilder()  
135 - .code("401")  
136 - .description("Unauthorized")  
137 - .representation(MediaType.APPLICATION_JSON)  
138 - .apply(classRepresentation(ThingsboardErrorResponse.class, true))  
139 - .build()  
140 - ) 126 + defaultErrorResponses(false)
  127 + )
  128 + .globalResponses(HttpMethod.POST,
  129 + defaultErrorResponses(true)
  130 + )
  131 + .globalResponses(HttpMethod.DELETE,
  132 + defaultErrorResponses(false)
141 ) 133 )
142 .securitySchemes(newArrayList(httpLogin())) 134 .securitySchemes(newArrayList(httpLogin()))
143 .securityContexts(newArrayList(securityContext())) 135 .securityContexts(newArrayList(securityContext()))
@@ -146,15 +138,15 @@ public class SwaggerConfiguration { @@ -146,15 +138,15 @@ public class SwaggerConfiguration {
146 138
147 @Bean 139 @Bean
148 @Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER) 140 @Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER)
149 - ApiListingScannerPlugin loginEndpointListingScanner() { 141 + ApiListingScannerPlugin loginEndpointListingScanner(final CachingOperationNameGenerator operationNames) {
150 return new ApiListingScannerPlugin() { 142 return new ApiListingScannerPlugin() {
151 @Override 143 @Override
152 public List<ApiDescription> apply(DocumentationContext context) { 144 public List<ApiDescription> apply(DocumentationContext context) {
153 - return List.of(loginEndpointApiDescription()); 145 + return List.of(loginEndpointApiDescription(operationNames));
154 } 146 }
155 147
156 @Override 148 @Override
157 - public boolean supports(DocumentationType delimiter) { 149 + public boolean supports(@NotNull DocumentationType delimiter) {
158 return DocumentationType.SWAGGER_2.equals(delimiter) || DocumentationType.OAS_30.equals(delimiter); 150 return DocumentationType.SWAGGER_2.equals(delimiter) || DocumentationType.OAS_30.equals(delimiter);
159 } 151 }
160 }; 152 };
@@ -176,7 +168,7 @@ public class SwaggerConfiguration { @@ -176,7 +168,7 @@ public class SwaggerConfiguration {
176 } 168 }
177 169
178 @Override 170 @Override
179 - public boolean supports(DocumentationType delimiter) { 171 + public boolean supports(@NotNull DocumentationType delimiter) {
180 return DocumentationType.SWAGGER_2.equals(delimiter) || DocumentationType.OAS_30.equals(delimiter); 172 return DocumentationType.SWAGGER_2.equals(delimiter) || DocumentationType.OAS_30.equals(delimiter);
181 } 173 }
182 }; 174 };
@@ -199,6 +191,7 @@ public class SwaggerConfiguration { @@ -199,6 +191,7 @@ public class SwaggerConfiguration {
199 .showCommonExtensions(false) 191 .showCommonExtensions(false)
200 .supportedSubmitMethods(UiConfiguration.Constants.DEFAULT_SUBMIT_METHODS) 192 .supportedSubmitMethods(UiConfiguration.Constants.DEFAULT_SUBMIT_METHODS)
201 .validatorUrl(null) 193 .validatorUrl(null)
  194 + .persistAuthorization(true)
202 .syntaxHighlightActivate(true) 195 .syntaxHighlightActivate(true)
203 .syntaxHighlightTheme("agate") 196 .syntaxHighlightTheme("agate")
204 .build(); 197 .build();
@@ -248,7 +241,7 @@ public class SwaggerConfiguration { @@ -248,7 +241,7 @@ public class SwaggerConfiguration {
248 .build(); 241 .build();
249 } 242 }
250 243
251 - private ApiDescription loginEndpointApiDescription() { 244 + private ApiDescription loginEndpointApiDescription(final CachingOperationNameGenerator operationNames) {
252 return new ApiDescription(null, "/api/auth/login", "Login method to get user JWT token data", "Login endpoint", Collections.singletonList( 245 return new ApiDescription(null, "/api/auth/login", "Login method to get user JWT token data", "Login endpoint", Collections.singletonList(
253 new OperationBuilder(operationNames) 246 new OperationBuilder(operationNames)
254 .summary("Login method to get user JWT token data") 247 .summary("Login method to get user JWT token data")
@@ -257,7 +250,8 @@ public class SwaggerConfiguration { @@ -257,7 +250,8 @@ public class SwaggerConfiguration {
257 .position(0) 250 .position(0)
258 .codegenMethodNameStem("loginPost") 251 .codegenMethodNameStem("loginPost")
259 .method(HttpMethod.POST) 252 .method(HttpMethod.POST)
260 - .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.") 253 + .notes("Login method used to authenticate user and get JWT token data.\n\nValue of the response **token** " +
  254 + "field can be used as **X-Authorization** header value:\n\n`X-Authorization: Bearer $JWT_TOKEN_VALUE`.")
261 .requestParameters( 255 .requestParameters(
262 List.of( 256 List.of(
263 new RequestParameterBuilder() 257 new RequestParameterBuilder()
@@ -278,7 +272,8 @@ public class SwaggerConfiguration { @@ -278,7 +272,8 @@ public class SwaggerConfiguration {
278 } 272 }
279 273
280 private Collection<Response> loginResponses() { 274 private Collection<Response> loginResponses() {
281 - return List.of( 275 + List<Response> responses = new ArrayList<>();
  276 + responses.add(
282 new ResponseBuilder() 277 new ResponseBuilder()
283 .code("200") 278 .code("200")
284 .description("OK") 279 .description("OK")
@@ -286,11 +281,82 @@ public class SwaggerConfiguration { @@ -286,11 +281,82 @@ public class SwaggerConfiguration {
286 .apply(classRepresentation(LoginResponse.class, true)). 281 .apply(classRepresentation(LoginResponse.class, true)).
287 build() 282 build()
288 ); 283 );
  284 + responses.addAll(loginErrorResponses());
  285 + return responses;
289 } 286 }
290 287
291 /** Helper methods **/ 288 /** Helper methods **/
292 289
293 - private Consumer<RepresentationBuilder> classRepresentation(Class clazz, boolean isResponse) { 290 + private List<Response> defaultErrorResponses(boolean isPost) {
  291 + return List.of(
  292 + errorResponse("400", "Bad Request",
  293 + ThingsboardErrorResponse.of(isPost ? "Invalid request body" : "Invalid UUID string: 123", ThingsboardErrorCode.BAD_REQUEST_PARAMS, HttpStatus.BAD_REQUEST)),
  294 + errorResponse("401", "Unauthorized",
  295 + ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)),
  296 + errorResponse("403", "Forbidden",
  297 + ThingsboardErrorResponse.of("You don't have permission to perform this operation!",
  298 + ThingsboardErrorCode.PERMISSION_DENIED, HttpStatus.FORBIDDEN)),
  299 + errorResponse("404", "Not Found",
  300 + ThingsboardErrorResponse.of("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND, HttpStatus.NOT_FOUND)),
  301 + errorResponse("429", "Too Many Requests",
  302 + ThingsboardErrorResponse.of("Too many requests for current tenant!",
  303 + ThingsboardErrorCode.TOO_MANY_REQUESTS, HttpStatus.TOO_MANY_REQUESTS))
  304 + );
  305 + }
  306 +
  307 + private List<Response> loginErrorResponses() {
  308 + return List.of(
  309 + errorResponse("401", "Unauthorized",
  310 + List.of(
  311 + errorExample("bad-credentials", "Bad credentials",
  312 + ThingsboardErrorResponse.of("Invalid username or password", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)),
  313 + errorExample("token-expired", "JWT token expired",
  314 + ThingsboardErrorResponse.of("Token has expired", ThingsboardErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED)),
  315 + errorExample("account-disabled", "Disabled account",
  316 + ThingsboardErrorResponse.of("User account is not active", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)),
  317 + errorExample("account-locked", "Locked account",
  318 + ThingsboardErrorResponse.of("User account is locked due to security policy", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)),
  319 + errorExample("authentication-failed", "General authentication error",
  320 + ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED))
  321 + )
  322 + ),
  323 + errorResponse("401 ", "Unauthorized (**Expired credentials**)",
  324 + List.of(
  325 + errorExample("credentials-expired", "Expired credentials",
  326 + ThingsboardCredentialsExpiredResponse.of("User password expired!", RandomStringUtils.randomAlphanumeric(30)))
  327 + ), ThingsboardCredentialsExpiredResponse.class
  328 + )
  329 + );
  330 + }
  331 +
  332 + private Response errorResponse(String code, String description, ThingsboardErrorResponse example) {
  333 + return errorResponse(code, description, List.of(errorExample("error-code-" + code, description, example)));
  334 + }
  335 +
  336 + private Response errorResponse(String code, String description, List<Example> examples) {
  337 + return errorResponse(code, description, examples, ThingsboardErrorResponse.class);
  338 + }
  339 +
  340 + private Response errorResponse(String code, String description, List<Example> examples,
  341 + Class<? extends ThingsboardErrorResponse> errorResponseClass) {
  342 + return new ResponseBuilder()
  343 + .code(code)
  344 + .description(description)
  345 + .examples(examples)
  346 + .representation(MediaType.APPLICATION_JSON)
  347 + .apply(classRepresentation(errorResponseClass, true))
  348 + .build();
  349 + }
  350 +
  351 + private Example errorExample(String id, String summary, ThingsboardErrorResponse example) {
  352 + return new ExampleBuilder()
  353 + .mediaType(MediaType.APPLICATION_JSON_VALUE)
  354 + .summary(summary)
  355 + .id(id)
  356 + .value(example).build();
  357 + }
  358 +
  359 + private Consumer<RepresentationBuilder> classRepresentation(Class<?> clazz, boolean isResponse) {
294 return r -> r.model( 360 return r -> r.model(
295 m -> 361 m ->
296 m.referenceModel(ref -> 362 m.referenceModel(ref ->
@@ -15,9 +15,12 @@ @@ -15,9 +15,12 @@
15 */ 15 */
16 package org.thingsboard.server.exception; 16 package org.thingsboard.server.exception;
17 17
  18 +import io.swagger.annotations.ApiModel;
  19 +import io.swagger.annotations.ApiModelProperty;
18 import org.springframework.http.HttpStatus; 20 import org.springframework.http.HttpStatus;
19 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; 21 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
20 22
  23 +@ApiModel
21 public class ThingsboardCredentialsExpiredResponse extends ThingsboardErrorResponse { 24 public class ThingsboardCredentialsExpiredResponse extends ThingsboardErrorResponse {
22 25
23 private final String resetToken; 26 private final String resetToken;
@@ -31,6 +34,7 @@ public class ThingsboardCredentialsExpiredResponse extends ThingsboardErrorRespo @@ -31,6 +34,7 @@ public class ThingsboardCredentialsExpiredResponse extends ThingsboardErrorRespo
31 return new ThingsboardCredentialsExpiredResponse(message, resetToken); 34 return new ThingsboardCredentialsExpiredResponse(message, resetToken);
32 } 35 }
33 36
  37 + @ApiModelProperty(position = 5, value = "Password reset token", readOnly = true)
34 public String getResetToken() { 38 public String getResetToken() {
35 return resetToken; 39 return resetToken;
36 } 40 }
@@ -15,11 +15,14 @@ @@ -15,11 +15,14 @@
15 */ 15 */
16 package org.thingsboard.server.exception; 16 package org.thingsboard.server.exception;
17 17
  18 +import io.swagger.annotations.ApiModel;
  19 +import io.swagger.annotations.ApiModelProperty;
18 import org.springframework.http.HttpStatus; 20 import org.springframework.http.HttpStatus;
19 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; 21 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
20 22
21 import java.util.Date; 23 import java.util.Date;
22 24
  25 +@ApiModel
23 public class ThingsboardErrorResponse { 26 public class ThingsboardErrorResponse {
24 // HTTP Response Status Code 27 // HTTP Response Status Code
25 private final HttpStatus status; 28 private final HttpStatus status;
@@ -43,18 +46,35 @@ public class ThingsboardErrorResponse { @@ -43,18 +46,35 @@ public class ThingsboardErrorResponse {
43 return new ThingsboardErrorResponse(message, errorCode, status); 46 return new ThingsboardErrorResponse(message, errorCode, status);
44 } 47 }
45 48
  49 + @ApiModelProperty(position = 1, value = "HTTP Response Status Code", example = "401", readOnly = true)
46 public Integer getStatus() { 50 public Integer getStatus() {
47 return status.value(); 51 return status.value();
48 } 52 }
49 53
  54 + @ApiModelProperty(position = 2, value = "Error message", example = "Authentication failed", readOnly = true)
50 public String getMessage() { 55 public String getMessage() {
51 return message; 56 return message;
52 } 57 }
53 58
  59 + @ApiModelProperty(position = 3, value = "Platform error code:" +
  60 + "\n* `2` - General error (HTTP: 500 - Internal Server Error)" +
  61 + "\n\n* `10` - Authentication failed (HTTP: 401 - Unauthorized)" +
  62 + "\n\n* `11` - JWT token expired (HTTP: 401 - Unauthorized)" +
  63 + "\n\n* `15` - Credentials expired (HTTP: 401 - Unauthorized)" +
  64 + "\n\n* `20` - Permission denied (HTTP: 403 - Forbidden)" +
  65 + "\n\n* `30` - Invalid arguments (HTTP: 400 - Bad Request)" +
  66 + "\n\n* `31` - Bad request params (HTTP: 400 - Bad Request)" +
  67 + "\n\n* `32` - Item not found (HTTP: 404 - Not Found)" +
  68 + "\n\n* `33` - Too many requests (HTTP: 429 - Too Many Requests)" +
  69 + "\n\n* `34` - Too many updates (Too many updates over Websocket session)" +
  70 + "\n\n* `40` - Subscription violation (HTTP: 403 - Forbidden)",
  71 + example = "10", dataType = "integer",
  72 + readOnly = true)
54 public ThingsboardErrorCode getErrorCode() { 73 public ThingsboardErrorCode getErrorCode() {
55 return errorCode; 74 return errorCode;
56 } 75 }
57 76
  77 + @ApiModelProperty(position = 4, value = "Timestamp", readOnly = true)
58 public Date getTimestamp() { 78 public Date getTimestamp() {
59 return timestamp; 79 return timestamp;
60 } 80 }
@@ -83,7 +83,7 @@ @@ -83,7 +83,7 @@
83 <rabbitmq.version>4.8.0</rabbitmq.version> 83 <rabbitmq.version>4.8.0</rabbitmq.version>
84 <surfire.version>2.19.1</surfire.version> 84 <surfire.version>2.19.1</surfire.version>
85 <jar-plugin.version>3.0.2</jar-plugin.version> 85 <jar-plugin.version>3.0.2</jar-plugin.version>
86 - <springfox-swagger.version>3.0.1</springfox-swagger.version> 86 + <springfox-swagger.version>3.0.2</springfox-swagger.version>
87 <swagger-annotations.version>1.6.3</swagger-annotations.version> 87 <swagger-annotations.version>1.6.3</swagger-annotations.version>
88 <spatial4j.version>0.7</spatial4j.version> 88 <spatial4j.version>0.7</spatial4j.version>
89 <jts.version>1.15.0</jts.version> 89 <jts.version>1.15.0</jts.version>