Commit 34d6dc50b5e876c01400cd82bd653fb537e9f537

Authored by Igor Kulikov
1 parent 113a6389

Swagger improvements

... ... @@ -15,25 +15,30 @@
15 15 */
16 16 package org.thingsboard.server.config;
17 17
18   -import com.fasterxml.classmate.ResolvedType;
19 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 22 import org.springframework.beans.factory.annotation.Value;
23 23 import org.springframework.context.annotation.Bean;
24 24 import org.springframework.context.annotation.Configuration;
25 25 import org.springframework.core.annotation.Order;
26 26 import org.springframework.http.HttpMethod;
  27 +import org.springframework.http.HttpStatus;
27 28 import org.springframework.http.MediaType;
  29 +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
28 30 import org.thingsboard.server.common.data.security.Authority;
  31 +import org.thingsboard.server.exception.ThingsboardCredentialsExpiredResponse;
29 32 import org.thingsboard.server.exception.ThingsboardErrorResponse;
30 33 import org.thingsboard.server.service.security.auth.rest.LoginRequest;
31 34 import org.thingsboard.server.service.security.auth.rest.LoginResponse;
32 35 import springfox.documentation.builders.ApiInfoBuilder;
  36 +import springfox.documentation.builders.ExampleBuilder;
33 37 import springfox.documentation.builders.OperationBuilder;
34 38 import springfox.documentation.builders.RepresentationBuilder;
35 39 import springfox.documentation.builders.RequestParameterBuilder;
36 40 import springfox.documentation.builders.ResponseBuilder;
  41 +import springfox.documentation.schema.Example;
37 42 import springfox.documentation.service.ApiDescription;
38 43 import springfox.documentation.service.ApiInfo;
39 44 import springfox.documentation.service.ApiListing;
... ... @@ -74,6 +79,7 @@ import static java.util.function.Predicate.not;
74 79 import static springfox.documentation.builders.PathSelectors.any;
75 80 import static springfox.documentation.builders.PathSelectors.regex;
76 81
  82 +@Slf4j
77 83 @Configuration
78 84 public class SwaggerConfiguration {
79 85
... ... @@ -100,44 +106,30 @@ public class SwaggerConfiguration {
100 106 @Value("${swagger.version}")
101 107 private String version;
102 108
103   - @Autowired
104   - private CachingOperationNameGenerator operationNames;
105   -
106 109 @Bean
107 110 public Docket thingsboardApi() {
108 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 112 return new Docket(DocumentationType.OAS_30)
117 113 .groupName("thingsboard")
118 114 .apiInfo(apiInfo())
119 115 .additionalModels(
120 116 typeResolver.resolve(ThingsboardErrorResponse.class),
  117 + typeResolver.resolve(ThingsboardCredentialsExpiredResponse.class),
121 118 typeResolver.resolve(LoginRequest.class),
122 119 typeResolver.resolve(LoginResponse.class)
123 120 )
124   - /* .alternateTypeRules(
125   - new AlternateTypeRule(
126   - jsonNodeType,
127   - stringType))*/
128 121 .select()
129 122 .paths(apiPaths())
130 123 .paths(any())
131 124 .build()
132 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 134 .securitySchemes(newArrayList(httpLogin()))
143 135 .securityContexts(newArrayList(securityContext()))
... ... @@ -146,15 +138,15 @@ public class SwaggerConfiguration {
146 138
147 139 @Bean
148 140 @Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER)
149   - ApiListingScannerPlugin loginEndpointListingScanner() {
  141 + ApiListingScannerPlugin loginEndpointListingScanner(final CachingOperationNameGenerator operationNames) {
150 142 return new ApiListingScannerPlugin() {
151 143 @Override
152 144 public List<ApiDescription> apply(DocumentationContext context) {
153   - return List.of(loginEndpointApiDescription());
  145 + return List.of(loginEndpointApiDescription(operationNames));
154 146 }
155 147
156 148 @Override
157   - public boolean supports(DocumentationType delimiter) {
  149 + public boolean supports(@NotNull DocumentationType delimiter) {
158 150 return DocumentationType.SWAGGER_2.equals(delimiter) || DocumentationType.OAS_30.equals(delimiter);
159 151 }
160 152 };
... ... @@ -176,7 +168,7 @@ public class SwaggerConfiguration {
176 168 }
177 169
178 170 @Override
179   - public boolean supports(DocumentationType delimiter) {
  171 + public boolean supports(@NotNull DocumentationType delimiter) {
180 172 return DocumentationType.SWAGGER_2.equals(delimiter) || DocumentationType.OAS_30.equals(delimiter);
181 173 }
182 174 };
... ... @@ -199,6 +191,7 @@ public class SwaggerConfiguration {
199 191 .showCommonExtensions(false)
200 192 .supportedSubmitMethods(UiConfiguration.Constants.DEFAULT_SUBMIT_METHODS)
201 193 .validatorUrl(null)
  194 + .persistAuthorization(true)
202 195 .syntaxHighlightActivate(true)
203 196 .syntaxHighlightTheme("agate")
204 197 .build();
... ... @@ -248,7 +241,7 @@ public class SwaggerConfiguration {
248 241 .build();
249 242 }
250 243
251   - private ApiDescription loginEndpointApiDescription() {
  244 + private ApiDescription loginEndpointApiDescription(final CachingOperationNameGenerator operationNames) {
252 245 return new ApiDescription(null, "/api/auth/login", "Login method to get user JWT token data", "Login endpoint", Collections.singletonList(
253 246 new OperationBuilder(operationNames)
254 247 .summary("Login method to get user JWT token data")
... ... @@ -257,7 +250,8 @@ public class SwaggerConfiguration {
257 250 .position(0)
258 251 .codegenMethodNameStem("loginPost")
259 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 255 .requestParameters(
262 256 List.of(
263 257 new RequestParameterBuilder()
... ... @@ -278,7 +272,8 @@ public class SwaggerConfiguration {
278 272 }
279 273
280 274 private Collection<Response> loginResponses() {
281   - return List.of(
  275 + List<Response> responses = new ArrayList<>();
  276 + responses.add(
282 277 new ResponseBuilder()
283 278 .code("200")
284 279 .description("OK")
... ... @@ -286,11 +281,82 @@ public class SwaggerConfiguration {
286 281 .apply(classRepresentation(LoginResponse.class, true)).
287 282 build()
288 283 );
  284 + responses.addAll(loginErrorResponses());
  285 + return responses;
289 286 }
290 287
291 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 360 return r -> r.model(
295 361 m ->
296 362 m.referenceModel(ref ->
... ...
... ... @@ -15,9 +15,12 @@
15 15 */
16 16 package org.thingsboard.server.exception;
17 17
  18 +import io.swagger.annotations.ApiModel;
  19 +import io.swagger.annotations.ApiModelProperty;
18 20 import org.springframework.http.HttpStatus;
19 21 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
20 22
  23 +@ApiModel
21 24 public class ThingsboardCredentialsExpiredResponse extends ThingsboardErrorResponse {
22 25
23 26 private final String resetToken;
... ... @@ -31,6 +34,7 @@ public class ThingsboardCredentialsExpiredResponse extends ThingsboardErrorRespo
31 34 return new ThingsboardCredentialsExpiredResponse(message, resetToken);
32 35 }
33 36
  37 + @ApiModelProperty(position = 5, value = "Password reset token", readOnly = true)
34 38 public String getResetToken() {
35 39 return resetToken;
36 40 }
... ...
... ... @@ -15,11 +15,14 @@
15 15 */
16 16 package org.thingsboard.server.exception;
17 17
  18 +import io.swagger.annotations.ApiModel;
  19 +import io.swagger.annotations.ApiModelProperty;
18 20 import org.springframework.http.HttpStatus;
19 21 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
20 22
21 23 import java.util.Date;
22 24
  25 +@ApiModel
23 26 public class ThingsboardErrorResponse {
24 27 // HTTP Response Status Code
25 28 private final HttpStatus status;
... ... @@ -43,18 +46,35 @@ public class ThingsboardErrorResponse {
43 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 50 public Integer getStatus() {
47 51 return status.value();
48 52 }
49 53
  54 + @ApiModelProperty(position = 2, value = "Error message", example = "Authentication failed", readOnly = true)
50 55 public String getMessage() {
51 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 73 public ThingsboardErrorCode getErrorCode() {
55 74 return errorCode;
56 75 }
57 76
  77 + @ApiModelProperty(position = 4, value = "Timestamp", readOnly = true)
58 78 public Date getTimestamp() {
59 79 return timestamp;
60 80 }
... ...
... ... @@ -83,7 +83,7 @@
83 83 <rabbitmq.version>4.8.0</rabbitmq.version>
84 84 <surfire.version>2.19.1</surfire.version>
85 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 87 <swagger-annotations.version>1.6.3</swagger-annotations.version>
88 88 <spatial4j.version>0.7</spatial4j.version>
89 89 <jts.version>1.15.0</jts.version>
... ...