|
|
@ -86,6 +86,9 @@ public class SwaggerConfiguration { |
|
|
public static final String LOGIN_ENDPOINT = "/api/auth/login"; |
|
|
public static final String LOGIN_ENDPOINT = "/api/auth/login"; |
|
|
public static final String REFRESH_TOKEN_ENDPOINT = "/api/auth/token"; |
|
|
public static final String REFRESH_TOKEN_ENDPOINT = "/api/auth/token"; |
|
|
|
|
|
|
|
|
|
|
|
private static final String LOGIN_PASSWORD_SCHEME = "HTTP login form"; |
|
|
|
|
|
private static final String API_KEY_SCHEME = "API key form"; |
|
|
|
|
|
|
|
|
private static final ApiResponses loginResponses = loginResponses(); |
|
|
private static final ApiResponses loginResponses = loginResponses(); |
|
|
private static final ApiResponses defaultErrorResponses = defaultErrorResponses(false); |
|
|
private static final ApiResponses defaultErrorResponses = defaultErrorResponses(false); |
|
|
private static final ApiResponses defaultPostErrorResponses = defaultErrorResponses(true); |
|
|
private static final ApiResponses defaultPostErrorResponses = defaultErrorResponses(true); |
|
|
@ -142,14 +145,28 @@ public class SwaggerConfiguration { |
|
|
.license(license) |
|
|
.license(license) |
|
|
.version(apiVersion); |
|
|
.version(apiVersion); |
|
|
|
|
|
|
|
|
SecurityScheme securityScheme = new SecurityScheme() |
|
|
SecurityScheme loginPasswordScheme = new SecurityScheme() |
|
|
.type(SecurityScheme.Type.HTTP) |
|
|
.type(SecurityScheme.Type.HTTP) |
|
|
.description("Enter Username / Password") |
|
|
.description("Enter Username / Password") |
|
|
.scheme("loginPassword") |
|
|
.scheme("loginPassword") |
|
|
.bearerFormat("/api/auth/login|X-Authorization"); |
|
|
.bearerFormat("/api/auth/login|X-Authorization"); |
|
|
|
|
|
|
|
|
|
|
|
SecurityScheme apiKeyScheme = new SecurityScheme() |
|
|
|
|
|
.type(SecurityScheme.Type.APIKEY) |
|
|
|
|
|
.name("X-Authorization") |
|
|
|
|
|
.in(SecurityScheme.In.HEADER) |
|
|
|
|
|
.description(""" |
|
|
|
|
|
Enter the API key value with 'ApiKey' prefix in format: **ApiKey <your_api_key_value>** |
|
|
|
|
|
|
|
|
|
|
|
Example: **ApiKey tb_5te51SkLRYpjGrujUGwqkjFvooWBlQpVe2An2Dr3w13wjfxDW** |
|
|
|
|
|
|
|
|
|
|
|
<br>**NOTE**: Use only ONE authentication method at a time. If both are authorized, JWT auth takes the priority.<br> |
|
|
|
|
|
"""); |
|
|
|
|
|
|
|
|
var openApi = new OpenAPI() |
|
|
var openApi = new OpenAPI() |
|
|
.components(new Components().addSecuritySchemes("HTTP login form", securityScheme)) |
|
|
.components(new Components() |
|
|
|
|
|
.addSecuritySchemes(LOGIN_PASSWORD_SCHEME, loginPasswordScheme) |
|
|
|
|
|
.addSecuritySchemes(API_KEY_SCHEME, apiKeyScheme)) |
|
|
.info(info); |
|
|
.info(info); |
|
|
addDefaultSchemas(openApi); |
|
|
addDefaultSchemas(openApi); |
|
|
addLoginOperation(openApi); |
|
|
addLoginOperation(openApi); |
|
|
@ -198,13 +215,14 @@ public class SwaggerConfiguration { |
|
|
operation.summary("Login method to get user JWT token data"); |
|
|
operation.summary("Login method to get user JWT token data"); |
|
|
operation.description(""" |
|
|
operation.description(""" |
|
|
Login method used to authenticate user and get JWT token data. |
|
|
Login method used to authenticate user and get JWT token data. |
|
|
|
|
|
|
|
|
Value of the response **token** field can be used as **X-Authorization** header value: |
|
|
Value of the response **token** field can be used as **X-Authorization** header value: |
|
|
|
|
|
|
|
|
`X-Authorization: Bearer $JWT_TOKEN_VALUE`."""); |
|
|
`X-Authorization: Bearer $JWT_TOKEN_VALUE`."""); |
|
|
|
|
|
|
|
|
var requestBody = new RequestBody().description("Login request") |
|
|
var requestBody = new RequestBody().description("Login request") |
|
|
.content(new Content().addMediaType(APPLICATION_JSON_VALUE, |
|
|
.content(new Content().addMediaType(APPLICATION_JSON_VALUE, |
|
|
new MediaType().schema(new Schema<LoginRequest>().$ref("#/components/schemas/LoginRequest")))); |
|
|
new MediaType().schema(new Schema<LoginRequest>().$ref("#/components/schemas/LoginRequest")))); |
|
|
operation.requestBody(requestBody); |
|
|
operation.requestBody(requestBody); |
|
|
|
|
|
|
|
|
operation.responses(loginResponses); |
|
|
operation.responses(loginResponses); |
|
|
@ -218,11 +236,11 @@ public class SwaggerConfiguration { |
|
|
var operation = new Operation(); |
|
|
var operation = new Operation(); |
|
|
operation.summary("Refresh user JWT token data"); |
|
|
operation.summary("Refresh user JWT token data"); |
|
|
operation.description(""" |
|
|
operation.description(""" |
|
|
Method to refresh JWT token. Provide a valid refresh token to get a new JWT token. |
|
|
Method to refresh JWT token. Provide a valid refresh token to get a new JWT token. |
|
|
|
|
|
|
|
|
The response contains a new token that can be used for authorization. |
|
|
The response contains a new token that can be used for authorization. |
|
|
|
|
|
|
|
|
`X-Authorization: Bearer $JWT_TOKEN_VALUE`"""); |
|
|
`X-Authorization: Bearer $JWT_TOKEN_VALUE`"""); |
|
|
|
|
|
|
|
|
var requestBody = new RequestBody().description("Refresh token request") |
|
|
var requestBody = new RequestBody().description("Refresh token request") |
|
|
.content(new Content().addMediaType(APPLICATION_JSON_VALUE, |
|
|
.content(new Content().addMediaType(APPLICATION_JSON_VALUE, |
|
|
@ -291,8 +309,9 @@ public class SwaggerConfiguration { |
|
|
return (routerOperation, handlerMethod) -> { |
|
|
return (routerOperation, handlerMethod) -> { |
|
|
String[] pNames = localSpringDocParameterNameDiscoverer.getParameterNames(handlerMethod.getMethod()); |
|
|
String[] pNames = localSpringDocParameterNameDiscoverer.getParameterNames(handlerMethod.getMethod()); |
|
|
String[] reflectionParametersNames = Arrays.stream(handlerMethod.getMethod().getParameters()).map(java.lang.reflect.Parameter::getName).toArray(String[]::new); |
|
|
String[] reflectionParametersNames = Arrays.stream(handlerMethod.getMethod().getParameters()).map(java.lang.reflect.Parameter::getName).toArray(String[]::new); |
|
|
if (pNames == null || Arrays.stream(pNames).anyMatch(Objects::isNull)) |
|
|
if (pNames == null || Arrays.stream(pNames).anyMatch(Objects::isNull)) { |
|
|
pNames = reflectionParametersNames; |
|
|
pNames = reflectionParametersNames; |
|
|
|
|
|
} |
|
|
MethodParameter[] parameters = handlerMethod.getMethodParameters(); |
|
|
MethodParameter[] parameters = handlerMethod.getMethodParameters(); |
|
|
List<String> requestParams = new ArrayList<>(); |
|
|
List<String> requestParams = new ArrayList<>(); |
|
|
for (var i = 0; i < parameters.length; i++) { |
|
|
for (var i = 0; i < parameters.length; i++) { |
|
|
@ -324,26 +343,25 @@ public class SwaggerConfiguration { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private OpenApiCustomizer customOpenApiCustomizer() { |
|
|
private OpenApiCustomizer customOpenApiCustomizer() { |
|
|
var loginForm = new SecurityRequirement().addList("HTTP login form", Arrays.asList( |
|
|
var loginRequirement = createSecurityRequirement(LOGIN_PASSWORD_SCHEME); |
|
|
Authority.SYS_ADMIN.name(), |
|
|
var apiKeyRequirement = createSecurityRequirement(API_KEY_SCHEME); |
|
|
Authority.TENANT_ADMIN.name(), |
|
|
|
|
|
Authority.CUSTOMER_USER.name() |
|
|
|
|
|
)); |
|
|
|
|
|
return openAPI -> { |
|
|
return openAPI -> { |
|
|
var paths = openAPI.getPaths(); |
|
|
var paths = openAPI.getPaths(); |
|
|
paths.entrySet().stream().peek(entry -> { |
|
|
paths.entrySet().stream() |
|
|
securityCustomization(loginForm, entry); |
|
|
.peek(entry -> { |
|
|
if (!entry.getKey().equals(LOGIN_ENDPOINT)) { |
|
|
securityCustomization(entry, loginRequirement, apiKeyRequirement); |
|
|
defaultErrorResponsesCustomization(entry.getValue()); |
|
|
if (!entry.getKey().equals(LOGIN_ENDPOINT)) { |
|
|
} |
|
|
defaultErrorResponsesCustomization(entry.getValue()); |
|
|
}).map(this::tagsCustomization).filter(Objects::nonNull).distinct().sorted(Comparator.comparing(Tag::getName)).forEach(openAPI::addTagsItem); |
|
|
} |
|
|
|
|
|
}) |
|
|
|
|
|
.map(this::extractTagFromPath).filter(Objects::nonNull).distinct().sorted(Comparator.comparing(Tag::getName)).forEach(openAPI::addTagsItem); |
|
|
|
|
|
|
|
|
var pathItemsByTags = new TreeMap<String, Map<String, PathItem>>(); |
|
|
var pathItemsByTags = new TreeMap<String, Map<String, PathItem>>(); |
|
|
paths.forEach((k, v) -> { |
|
|
paths.forEach((k, v) -> { |
|
|
var tagItem = tagItemFromPathItem(v); |
|
|
var tagItem = tagItemFromPathItem(v); |
|
|
if (tagItem != null) { |
|
|
if (tagItem != null) { |
|
|
var pathItemMap = pathItemsByTags.computeIfAbsent(tagItem, k1 -> new TreeMap<>()); |
|
|
pathItemsByTags.computeIfAbsent(tagItem, k1 -> new TreeMap<>()).put(k, v); |
|
|
pathItemMap.put(k, v); |
|
|
|
|
|
} |
|
|
} |
|
|
}); |
|
|
}); |
|
|
var sortedPaths = new Paths(); |
|
|
var sortedPaths = new Paths(); |
|
|
@ -357,13 +375,35 @@ public class SwaggerConfiguration { |
|
|
}; |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private SecurityRequirement createSecurityRequirement(String schemeName) { |
|
|
|
|
|
return new SecurityRequirement().addList(schemeName, Arrays.asList( |
|
|
|
|
|
Authority.SYS_ADMIN.name(), |
|
|
|
|
|
Authority.TENANT_ADMIN.name(), |
|
|
|
|
|
Authority.CUSTOMER_USER.name() |
|
|
|
|
|
)); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private void securityCustomization(Map.Entry<String, PathItem> entry, SecurityRequirement jwtBearerRequirement, SecurityRequirement apiKeyRequirement) { |
|
|
|
|
|
var path = entry.getKey(); |
|
|
|
|
|
|
|
|
|
|
|
if (path.matches(securityPathRegex) |
|
|
|
|
|
&& !path.matches(nonSecurityPathRegex) |
|
|
|
|
|
&& !path.equals(LOGIN_ENDPOINT) |
|
|
|
|
|
&& !path.equals(REFRESH_TOKEN_ENDPOINT)) { |
|
|
|
|
|
|
|
|
private Tag tagsCustomization(Map.Entry<String, PathItem> entry) { |
|
|
entry.getValue() |
|
|
var tagItem = tagItemFromPathItem(entry.getValue()); |
|
|
.readOperationsMap() |
|
|
if (tagItem != null) { |
|
|
.values() |
|
|
return tagFromTagItem(tagItem); |
|
|
.forEach(operation -> { |
|
|
|
|
|
operation.addSecurityItem(jwtBearerRequirement); |
|
|
|
|
|
operation.addSecurityItem(apiKeyRequirement); |
|
|
|
|
|
}); |
|
|
} |
|
|
} |
|
|
return null; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private Tag extractTagFromPath(Map.Entry<String, PathItem> entry) { |
|
|
|
|
|
var tagName = tagItemFromPathItem(entry.getValue()); |
|
|
|
|
|
return tagName != null ? tagFromTagItem(tagName) : null; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private String tagItemFromPathItem(PathItem item) { |
|
|
private String tagItemFromPathItem(PathItem item) { |
|
|
@ -383,40 +423,33 @@ public class SwaggerConfiguration { |
|
|
StringBuilder sb = new StringBuilder(); |
|
|
StringBuilder sb = new StringBuilder(); |
|
|
|
|
|
|
|
|
for (String word : words) { |
|
|
for (String word : words) { |
|
|
sb.append(word.substring(0, 1).toUpperCase()); |
|
|
if (!word.isEmpty()) { |
|
|
sb.append(word.substring(1).toLowerCase()); |
|
|
sb.append(word.substring(0, 1).toUpperCase()); |
|
|
sb.append(" "); |
|
|
sb.append(word.substring(1).toLowerCase()); |
|
|
|
|
|
sb.append(" "); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return new Tag().name(tagItem).description(sb.toString().trim()); |
|
|
return new Tag().name(tagItem).description(sb.toString().trim()); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private void defaultErrorResponsesCustomization(PathItem pathItem) { |
|
|
private void defaultErrorResponsesCustomization(PathItem pathItem) { |
|
|
pathItem.readOperationsMap().forEach(((httpMethod, operation) -> { |
|
|
pathItem.readOperationsMap().forEach((httpMethod, operation) -> { |
|
|
var errorResponses = httpMethod.equals(PathItem.HttpMethod.POST) ? defaultPostErrorResponses : defaultErrorResponses; |
|
|
var errorResponses = httpMethod.equals(PathItem.HttpMethod.POST) ? defaultPostErrorResponses : defaultErrorResponses; |
|
|
|
|
|
|
|
|
var responses = operation.getResponses(); |
|
|
var responses = operation.getResponses(); |
|
|
if (responses == null) { |
|
|
if (responses == null) { |
|
|
responses = errorResponses; |
|
|
responses = new ApiResponses(); |
|
|
} else { |
|
|
operation.setResponses(responses); |
|
|
ApiResponses updated = responses; |
|
|
|
|
|
errorResponses.forEach((key, apiResponse) -> { |
|
|
|
|
|
if (!updated.containsKey(key)) { |
|
|
|
|
|
updated.put(key, apiResponse); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
} |
|
|
operation.setResponses(responses); |
|
|
|
|
|
})); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private void securityCustomization(SecurityRequirement loginForm, Map.Entry<String, PathItem> entry) { |
|
|
ApiResponses finalResponses = responses; |
|
|
var path = entry.getKey(); |
|
|
errorResponses.forEach((code, response) -> { |
|
|
if (path.matches(securityPathRegex) && !path.matches(nonSecurityPathRegex) && !path.equals(LOGIN_ENDPOINT)) { |
|
|
if (!finalResponses.containsKey(code)) { |
|
|
entry.getValue() |
|
|
finalResponses.put(code, response); |
|
|
.readOperationsMap() |
|
|
} |
|
|
.values() |
|
|
}); |
|
|
.forEach(operation -> operation.addSecurityItem(loginForm)); |
|
|
}); |
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private static ApiResponses loginResponses() { |
|
|
private static ApiResponses loginResponses() { |
|
|
@ -430,6 +463,7 @@ public class SwaggerConfiguration { |
|
|
|
|
|
|
|
|
private static ApiResponses defaultErrorResponses(boolean isPost) { |
|
|
private static ApiResponses defaultErrorResponses(boolean isPost) { |
|
|
ApiResponses apiResponses = new ApiResponses(); |
|
|
ApiResponses apiResponses = new ApiResponses(); |
|
|
|
|
|
|
|
|
apiResponses.addApiResponse("400", errorResponse("400", "Bad Request", |
|
|
apiResponses.addApiResponse("400", errorResponse("400", "Bad Request", |
|
|
ThingsboardErrorResponse.of(isPost ? "Invalid request body" : "Invalid UUID string: 123", ThingsboardErrorCode.BAD_REQUEST_PARAMS, HttpStatus.BAD_REQUEST))); |
|
|
ThingsboardErrorResponse.of(isPost ? "Invalid request body" : "Invalid UUID string: 123", ThingsboardErrorCode.BAD_REQUEST_PARAMS, HttpStatus.BAD_REQUEST))); |
|
|
|
|
|
|
|
|
@ -465,8 +499,7 @@ public class SwaggerConfiguration { |
|
|
ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)) |
|
|
ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)) |
|
|
) |
|
|
) |
|
|
)); |
|
|
)); |
|
|
var credentialsExpiredSchema = new Schema<ThingsboardCredentialsExpiredResponse>(); |
|
|
var credentialsExpiredSchema = new Schema<ThingsboardCredentialsExpiredResponse>().$ref("#/components/schemas/ThingsboardCredentialsExpiredResponse"); |
|
|
credentialsExpiredSchema.$ref("#/components/schemas/ThingsboardCredentialsExpiredResponse"); |
|
|
|
|
|
apiResponses.addApiResponse("401 ", errorResponse("Unauthorized (**Expired credentials**)", |
|
|
apiResponses.addApiResponse("401 ", errorResponse("Unauthorized (**Expired credentials**)", |
|
|
Map.of( |
|
|
Map.of( |
|
|
"credentials-expired", errorExample("Expired credentials", |
|
|
"credentials-expired", errorExample("Expired credentials", |
|
|
@ -482,15 +515,13 @@ public class SwaggerConfiguration { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private static ApiResponse errorResponse(String description, Map<String, Example> examples) { |
|
|
private static ApiResponse errorResponse(String description, Map<String, Example> examples) { |
|
|
var schema = new Schema<ThingsboardErrorResponse>(); |
|
|
var schema = new Schema<ThingsboardErrorResponse>().$ref("#/components/schemas/ThingsboardErrorResponse"); |
|
|
schema.$ref("#/components/schemas/ThingsboardErrorResponse"); |
|
|
|
|
|
return errorResponse(description, examples, schema); |
|
|
return errorResponse(description, examples, schema); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private static ApiResponse errorResponse(String description, Map<String, Example> examples, Schema<? extends ThingsboardErrorResponse> errorResponseSchema) { |
|
|
private static ApiResponse errorResponse(String description, Map<String, Example> examples, Schema<? extends ThingsboardErrorResponse> errorResponseSchema) { |
|
|
MediaType mediaType = new MediaType().schema(errorResponseSchema); |
|
|
MediaType mediaType = new MediaType().schema(errorResponseSchema).examples(examples); |
|
|
mediaType.setExamples(examples); |
|
|
Content content = new Content().addMediaType(org.springframework.http.MediaType.APPLICATION_JSON_VALUE, mediaType); |
|
|
Content content = new Content().addMediaType(org.springframework.http.MediaType.APPLICATION_JSON_VALUE, mediaType); |
|
|
|
|
|
return new ApiResponse().description(description).content(content); |
|
|
return new ApiResponse().description(description).content(content); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|