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