diff --git a/pom.xml b/pom.xml index e3ef8a0..776fd98 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,11 @@ spring-boot-starter-json + + org.springframework.security + spring-security-core + + org.projectlombok diff --git a/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/swagger/RegisterCustomTypesWithSwagger.java b/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/swagger/RegisterCustomTypesWithSwagger.java index ed99c70..cb2cc8e 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/swagger/RegisterCustomTypesWithSwagger.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/swagger/RegisterCustomTypesWithSwagger.java @@ -1,7 +1,7 @@ package it.aboutbits.springboot.toolbox.autoconfiguration.swagger; -import it.aboutbits.springboot.toolbox.swagger.CustomTypeModelConverter; -import it.aboutbits.springboot.toolbox.swagger.CustomTypePropertyCustomizer; +import it.aboutbits.springboot.toolbox.swagger.type.CustomTypeModelConverter; +import it.aboutbits.springboot.toolbox.swagger.type.CustomTypePropertyCustomizer; import org.springframework.context.annotation.Import; import java.lang.annotation.ElementType; diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/annotations/SwaggerScopedAuth.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/annotations/SwaggerScopedAuth.java new file mode 100644 index 0000000..cdf039c --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/annotations/SwaggerScopedAuth.java @@ -0,0 +1,12 @@ +package it.aboutbits.springboot.toolbox.swagger.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface SwaggerScopedAuth { + String value(); +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/alphabetical_model_order/OrderModelsCustomizer.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/alphabetical_model_order/OrderModelsCustomizer.java new file mode 100644 index 0000000..79d9006 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/alphabetical_model_order/OrderModelsCustomizer.java @@ -0,0 +1,15 @@ +package it.aboutbits.springboot.toolbox.swagger.customization.alphabetical_model_order; + +import io.swagger.v3.oas.models.OpenAPI; +import org.springdoc.core.customizers.OpenApiCustomizer; + +import java.util.TreeMap; + +public class OrderModelsCustomizer implements OpenApiCustomizer { + @Override + public void customise(OpenAPI openApi) { + var components = openApi.getComponents(); + + components.schemas(new TreeMap<>(components.getSchemas())); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/authorization_docs/AuthorizationDescriptor.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/authorization_docs/AuthorizationDescriptor.java new file mode 100644 index 0000000..48c548e --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/authorization_docs/AuthorizationDescriptor.java @@ -0,0 +1,50 @@ +package it.aboutbits.springboot.toolbox.swagger.customization.authorization_docs; + +import io.swagger.v3.oas.models.Operation; +import it.aboutbits.springboot.toolbox.swagger.annotations.SwaggerScopedAuth; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.customizers.OperationCustomizer; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.method.HandlerMethod; + +import java.util.ArrayList; +import java.util.Optional; + +@Slf4j +public class AuthorizationDescriptor implements OperationCustomizer { + @Override + public Operation customize(Operation operation, HandlerMethod handlerMethod) { + try { + var additionalDescription = new ArrayList(); + + var maybeAnnotation = Optional.ofNullable(handlerMethod.getMethodAnnotation(PreAuthorize.class)); + if (maybeAnnotation.isPresent()) { + var annotation = maybeAnnotation.get(); + additionalDescription.add("Authorization: " + annotation.value()); + } + + var maybeAnnotation2 = Optional.ofNullable(handlerMethod.getMethodAnnotation(SwaggerScopedAuth.class)); + if (maybeAnnotation2.isPresent()) { + var annotation = maybeAnnotation2.get(); + additionalDescription.add("Scoped Authorization: " + annotation.value()); + } + + if (!additionalDescription.isEmpty()) { + + var currentDescription = Optional.ofNullable(operation.getDescription()); + + var description = String.join("
", additionalDescription); + if (currentDescription.isPresent()) { + description = "

" + description + "

" + currentDescription.get(); + } + + operation.description( + description + ); + } + } catch (Exception e) { + log.error("Error when creating swagger documentation for authorities.", e); + } + return operation; + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/default_not_null/NullableCustomizer.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/default_not_null/NullableCustomizer.java new file mode 100644 index 0000000..41413a2 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/default_not_null/NullableCustomizer.java @@ -0,0 +1,24 @@ +package it.aboutbits.springboot.toolbox.swagger.customization.default_not_null; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.Schema; +import org.springdoc.core.customizers.OpenApiCustomizer; + +import java.util.ArrayList; + +public class NullableCustomizer implements OpenApiCustomizer { + @Override + @SuppressWarnings("unchecked") + public void customise(OpenAPI openApi) { + openApi.getComponents().getSchemas().values() + .forEach(schema -> { + var requiredProperties = new ArrayList(); + ((Schema) schema).getProperties().forEach((propertyName, property) -> { + if (property.getNullable() == null || !property.getNullable()) { + requiredProperties.add(propertyName); + } + }); + schema.setRequired(requiredProperties); + }); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/default_not_null/NullablePropertyCustomizer.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/default_not_null/NullablePropertyCustomizer.java new file mode 100644 index 0000000..a8fdf57 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/default_not_null/NullablePropertyCustomizer.java @@ -0,0 +1,39 @@ +package it.aboutbits.springboot.toolbox.swagger.customization.default_not_null; + +import io.swagger.v3.core.converter.AnnotatedType; +import io.swagger.v3.core.jackson.ModelResolver; +import io.swagger.v3.oas.models.media.Schema; +import org.springdoc.core.customizers.PropertyCustomizer; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Arrays; + +@Component +public class NullablePropertyCustomizer implements PropertyCustomizer { + static { + /* + We need this because the ModelResolver will only process these whitelisted annotations. + We will then be able to manipulate each property based on the set annotations. + */ + + var list = new ArrayList<>(ModelResolver.NOT_NULL_ANNOTATIONS); + list.add("Nullable"); + + ModelResolver.NOT_NULL_ANNOTATIONS = list; + } + + @Override + public Schema customize(Schema property, AnnotatedType annotatedType) { + /* + Mark the nullable ones as nullable. + */ + + if (annotatedType.getCtxAnnotations() != null && Arrays.stream(annotatedType.getCtxAnnotations()) + .anyMatch(a -> "Nullable".equals(a.annotationType().getSimpleName()))) { + property.setNullable(true); + } + + return property; + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/error_response/ErrorCustomizer.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/error_response/ErrorCustomizer.java new file mode 100644 index 0000000..798d0e8 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/error_response/ErrorCustomizer.java @@ -0,0 +1,66 @@ +package it.aboutbits.springboot.toolbox.swagger.customization.error_response; + +import io.swagger.v3.core.converter.ModelConverters; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.oas.models.responses.ApiResponses; +import it.aboutbits.springboot.toolbox.mvc.response.ErrorResponse; +import org.springdoc.core.customizers.OpenApiCustomizer; + +import java.util.Map; + +public class ErrorCustomizer implements OpenApiCustomizer { + @Override + public void customise(OpenAPI openApi) { + openApi.getComponents() + .getSchemas() + .putAll( + ModelConverters.getInstance().read(ErrorResponse.class) + ); + + var errorResponseSchema = openApi.getComponents().getSchemas().get("ErrorResponse"); + @SuppressWarnings("unchecked") + Map> props = errorResponseSchema.getProperties(); + for (var prop : props.values()) { + prop.nullable(true); + } + + openApi.getPaths() + .values() + .forEach( + pathItem -> pathItem.readOperations() + .forEach( + operation -> { + ApiResponses apiResponses = operation.getResponses(); + apiResponses.addApiResponse( + "400", + createApiResponse( + "Bad Request", + errorResponseSchema + ) + ); + apiResponses.addApiResponse( + "404", + createApiResponse( + "Not Found", + errorResponseSchema + ) + ); + } + ) + ); + } + + private ApiResponse createApiResponse(String message, Schema schema) { + var mediaType = new MediaType(); + mediaType.schema(schema); + return new ApiResponse().description(message) + .content(new Content().addMediaType( + org.springframework.http.MediaType.APPLICATION_JSON_VALUE, + mediaType + )); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/logout_route/LogoutCustomizer.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/logout_route/LogoutCustomizer.java new file mode 100644 index 0000000..3bf3ee5 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/logout_route/LogoutCustomizer.java @@ -0,0 +1,26 @@ +package it.aboutbits.springboot.toolbox.swagger.customization.logout_route; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.responses.ApiResponses; +import lombok.RequiredArgsConstructor; +import org.springdoc.core.customizers.OpenApiCustomizer; + +@RequiredArgsConstructor +public class LogoutCustomizer implements OpenApiCustomizer { + private final String logoutUrl; + + @Override + public void customise(OpenAPI openApi) { + var operation = new Operation(); + operation.addTagsItem("Authentication API"); + operation.summary("Logout the current user"); + operation.responses(new ApiResponses()); + + var pathItem = new PathItem(); + pathItem.setPost(operation); + + openApi.getPaths().addPathItem(logoutUrl, pathItem); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/CustomTypeModelConverter.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/type/CustomTypeModelConverter.java similarity index 97% rename from src/main/java/it/aboutbits/springboot/toolbox/swagger/CustomTypeModelConverter.java rename to src/main/java/it/aboutbits/springboot/toolbox/swagger/type/CustomTypeModelConverter.java index 803937f..f0aa8cf 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/swagger/CustomTypeModelConverter.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/type/CustomTypeModelConverter.java @@ -1,4 +1,4 @@ -package it.aboutbits.springboot.toolbox.swagger; +package it.aboutbits.springboot.toolbox.swagger.type; import io.swagger.v3.core.converter.AnnotatedType; import io.swagger.v3.core.converter.ModelConverter; diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/CustomTypePropertyCustomizer.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/type/CustomTypePropertyCustomizer.java similarity index 91% rename from src/main/java/it/aboutbits/springboot/toolbox/swagger/CustomTypePropertyCustomizer.java rename to src/main/java/it/aboutbits/springboot/toolbox/swagger/type/CustomTypePropertyCustomizer.java index fd3f81e..810f906 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/swagger/CustomTypePropertyCustomizer.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/type/CustomTypePropertyCustomizer.java @@ -1,4 +1,4 @@ -package it.aboutbits.springboot.toolbox.swagger; +package it.aboutbits.springboot.toolbox.swagger.type; import com.fasterxml.jackson.databind.type.SimpleType; import io.swagger.v3.core.converter.AnnotatedType; @@ -25,13 +25,17 @@ public Schema customize(Schema property, AnnotatedType annotatedType) { var displayName = rawClass.getSimpleName(); + Class wrappedType; if (EntityId.class.isAssignableFrom(rawClass)) { displayName = resolveEntityIdDisplayName(rawClass); } - var constructor = RecordReflectionUtil.getCanonicalConstructor(rawClass); - var wrappedType = constructor.getParameters()[0].getType(); - + if (rawClass.equals(EntityId.class)) { + wrappedType = simpleType.getBindings().getBoundType(0).getRawClass(); + } else { + var constructor = RecordReflectionUtil.getCanonicalConstructor(rawClass); + wrappedType = constructor.getParameters()[0].getType(); + } if (Short.class.isAssignableFrom(wrappedType)) { property.type("integer");