├── Apps ├── WebGenerateApplication │ ├── .openapi-generator-java-sources.ignore │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ ├── application.properties │ │ │ ├── application.properties.txt │ │ │ └── petclinic-spec.yml │ │ │ └── java │ │ │ └── ru │ │ │ └── app │ │ │ └── gen │ │ │ ├── config │ │ │ ├── WebConfig.java │ │ │ └── SwaggerConfig.java │ │ │ └── WebGenerateApplication.java │ └── build.gradle ├── WebApplication │ ├── src │ │ └── main │ │ │ ├── java │ │ │ └── ru │ │ │ │ └── app │ │ │ │ ├── api │ │ │ │ ├── auth │ │ │ │ │ ├── TokenService.java │ │ │ │ │ ├── DefaultTokenService.java │ │ │ │ │ ├── AuthorizationService.java │ │ │ │ │ └── AuthorizationFilter.java │ │ │ │ ├── dto │ │ │ │ │ └── AnswerDto.java │ │ │ │ ├── DbController.java │ │ │ │ └── WebController.java │ │ │ │ ├── db │ │ │ │ ├── entity │ │ │ │ │ ├── enums │ │ │ │ │ │ └── ContactTypeEnum.java │ │ │ │ │ ├── ContactEntity.java │ │ │ │ │ ├── WorkerEntity.java │ │ │ │ │ └── AbstractEntity.java │ │ │ │ ├── repository │ │ │ │ │ └── WorkerlRepository.java │ │ │ │ └── service │ │ │ │ │ ├── GenerateDataInDbService.java │ │ │ │ │ └── EntityService.java │ │ │ │ ├── utils │ │ │ │ ├── Utils.java │ │ │ │ ├── ApplicationContextProvider.java │ │ │ │ └── ObjectMapperWrapper.java │ │ │ │ ├── config │ │ │ │ ├── AppProperties.java │ │ │ │ ├── SwaggerConfig.java │ │ │ │ └── WebMvcConfig.java │ │ │ │ ├── WebApplication.java │ │ │ │ ├── advice │ │ │ │ └── DefaultAdvice.java │ │ │ │ └── listener │ │ │ │ └── MyRefreshListener.java │ │ │ └── resources │ │ │ ├── application.properties │ │ │ └── application.properties.txt │ └── build.gradle └── WebSecurityApplication │ ├── src │ └── main │ │ ├── java │ │ └── ru │ │ │ └── app │ │ │ ├── service │ │ │ └── MyService.java │ │ │ ├── api │ │ │ ├── auth │ │ │ │ ├── TokenService.java │ │ │ │ ├── DefaultTokenService.java │ │ │ │ └── AuthorizationFilter.java │ │ │ └── WebController.java │ │ │ ├── WebSecurityApplication.java │ │ │ └── config │ │ │ ├── SwaggerConfig.java │ │ │ └── SecurityConfiguration.java │ │ └── resources │ │ ├── application.properties │ │ └── application.properties.txt │ └── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── SpringCloudConfig ├── src │ └── main │ │ ├── resources │ │ ├── img.png │ │ ├── img_1.png │ │ └── application.properties │ │ └── java │ │ └── ru │ │ └── configs │ │ └── SpringCloudConfig.java └── build.gradle ├── AuthService ├── src │ └── main │ │ ├── java │ │ └── ru │ │ │ └── auth │ │ │ ├── api │ │ │ ├── dto │ │ │ │ ├── ErrorResponseDto.java │ │ │ │ ├── AuthRequestDto.java │ │ │ │ └── AuthResponseDto.java │ │ │ └── AuthController.java │ │ │ ├── db │ │ │ ├── repository │ │ │ │ └── ClientRepository.java │ │ │ └── entity │ │ │ │ └── ClientEntity.java │ │ │ ├── service │ │ │ ├── TokenService.java │ │ │ └── DefaultTokenService.java │ │ │ ├── AuthService.java │ │ │ └── config │ │ │ └── WebConfig.java │ │ └── resources │ │ ├── application.properties │ │ ├── OAuth2.txt │ │ └── application.properties.txt └── build.gradle ├── SpringBootAdminServer ├── src │ └── main │ │ ├── resources │ │ └── application.properties │ │ └── java │ │ └── ru │ │ └── admin │ │ ├── SpringBootAdminServer.java │ │ └── config │ │ └── WebSecurityConfig.java.txt └── build.gradle ├── SpringCloudGateway ├── src │ └── main │ │ ├── java │ │ └── ru │ │ │ └── gateway │ │ │ ├── SpringCloudGateway.java │ │ │ ├── actuator │ │ │ ├── CustomHealthIndicator.java │ │ │ ├── CustomInfoContributor.java │ │ │ ├── MyCustomEndpoint.java │ │ │ └── metrics │ │ │ │ └── MetricScheduler.java │ │ │ └── config │ │ │ └── ProxyConfig.java │ │ └── resources │ │ └── application.yml └── build.gradle ├── settings.gradle ├── .gitignore ├── readme.txt ├── gradlew.bat ├── прочти.txt └── gradlew /Apps/WebGenerateApplication/.openapi-generator-java-sources.ignore: -------------------------------------------------------------------------------- 1 | * 2 | **/* 3 | !**/src/main/java/**/* 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upswet/SimpleSpringExample/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /SpringCloudConfig/src/main/resources/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upswet/SimpleSpringExample/HEAD/SpringCloudConfig/src/main/resources/img.png -------------------------------------------------------------------------------- /SpringCloudConfig/src/main/resources/img_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upswet/SimpleSpringExample/HEAD/SpringCloudConfig/src/main/resources/img_1.png -------------------------------------------------------------------------------- /AuthService/src/main/java/ru/auth/api/dto/ErrorResponseDto.java: -------------------------------------------------------------------------------- 1 | package ru.auth.api.dto; 2 | 3 | import lombok.Value; 4 | 5 | /**Дто для ошибочного ответа*/ 6 | @Value 7 | public class ErrorResponseDto { 8 | String message; 9 | } 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Dec 29 22:11:56 NOVT 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /AuthService/src/main/java/ru/auth/api/dto/AuthRequestDto.java: -------------------------------------------------------------------------------- 1 | package ru.auth.api.dto; 2 | 3 | import lombok.Value; 4 | 5 | /**Дто для запросоу аутентификации*/ 6 | @Value 7 | public class AuthRequestDto { 8 | String clientId; 9 | String audience; 10 | String clientSecret; 11 | } 12 | -------------------------------------------------------------------------------- /SpringBootAdminServer/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=SpringBootAdminServer 2 | server.port=8099 3 | #http://localhost:8099/applications 4 | #http://localhost:8099/spring-security-mvc-login/login.html 5 | 6 | #spring.boot.admin.routes.endpoints=env, metrics, trace, jolokia, info, configprops -------------------------------------------------------------------------------- /AuthService/src/main/java/ru/auth/api/dto/AuthResponseDto.java: -------------------------------------------------------------------------------- 1 | package ru.auth.api.dto; 2 | 3 | import lombok.*; 4 | 5 | /**Дто для ответа на запрос аутентифиакции*/ 6 | @Value 7 | public class AuthResponseDto { 8 | private final String type = "Bearer"; 9 | String accessToken; 10 | String refreshToken; 11 | } 12 | -------------------------------------------------------------------------------- /Apps/WebApplication/src/main/java/ru/app/api/auth/TokenService.java: -------------------------------------------------------------------------------- 1 | package ru.app.api.auth; 2 | 3 | import com.auth0.jwt.interfaces.DecodedJWT; 4 | 5 | /**Проверка токена полученного клиентом у AuthService-а*/ 6 | public interface TokenService { 7 | /** Вернёт раскодирвоанный jwt-токен если он валиден или null если не валиден*/ 8 | DecodedJWT checkToken(String token); 9 | } 10 | -------------------------------------------------------------------------------- /Apps/WebSecurityApplication/src/main/java/ru/app/service/MyService.java: -------------------------------------------------------------------------------- 1 | package ru.app.service; 2 | 3 | import org.springframework.security.access.annotation.Secured; 4 | import org.springframework.stereotype.Service; 5 | 6 | @Service 7 | public class MyService { 8 | 9 | @Secured({"ROLE_HELLO"}) 10 | public String helloService(){ 11 | return "my hello"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Apps/WebGenerateApplication/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=open-api-demo 2 | server.port=8082 3 | 4 | #config 5 | spring.profiles.active=swagger, actuator, adminClient 6 | spring.cloud.config.fail-fast=true 7 | spring.cloud.config.uri=http://localhost:8888 8 | spring.config.import=optional:configserver:http://localhost:8888 9 | spring.cloud.config.import-check.enabled=true -------------------------------------------------------------------------------- /Apps/WebSecurityApplication/src/main/java/ru/app/api/auth/TokenService.java: -------------------------------------------------------------------------------- 1 | package ru.app.api.auth; 2 | 3 | import com.auth0.jwt.interfaces.DecodedJWT; 4 | 5 | /**Проверка токена полученного клиентом у AuthService-а*/ 6 | public interface TokenService { 7 | /** Вернёт раскодирвоанный jwt-токен если он валиден или null если не валиден*/ 8 | DecodedJWT checkToken(String token); 9 | } 10 | -------------------------------------------------------------------------------- /Apps/WebSecurityApplication/src/main/java/ru/app/WebSecurityApplication.java: -------------------------------------------------------------------------------- 1 | package ru.app; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class WebSecurityApplication { 8 | public static void main(String[] args) { 9 | SpringApplication.run(WebSecurityApplication.class, args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /AuthService/src/main/java/ru/auth/db/repository/ClientRepository.java: -------------------------------------------------------------------------------- 1 | package ru.auth.db.repository; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | import ru.auth.db.entity.ClientEntity; 5 | 6 | import java.util.Optional; 7 | 8 | public interface ClientRepository extends CrudRepository { 9 | Optional findByClientIdAndAudience(String clientId, String audience); 10 | } 11 | -------------------------------------------------------------------------------- /AuthService/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=AuthService 2 | server.port=8079 3 | 4 | #config 5 | spring.profiles.active=authSecret, swagger, AuthService, hibernate, adminClient, actuator 6 | spring.cloud.config.fail-fast=true 7 | spring.cloud.config.uri=http://localhost:8888 8 | spring.config.import=optional:configserver:http://localhost:8888 9 | spring.cloud.config.import-check.enabled=true 10 | 11 | -------------------------------------------------------------------------------- /SpringCloudConfig/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | group = 'ru.configs' 3 | version = '1.0-SNAPSHOT' 4 | 5 | 6 | dependencies { 7 | //spring 8 | implementation 'org.springframework.cloud:spring-cloud-config-server' 9 | } 10 | 11 | /* 12 | ext { 13 | set('springCloudVersion', "2023.0.1") 14 | } 15 | dependencyManagement { 16 | imports { 17 | mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" 18 | } 19 | }*/ -------------------------------------------------------------------------------- /Apps/WebSecurityApplication/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=WebSecurityApplication1 2 | server.port=8081 3 | 4 | #config 5 | spring.profiles.active=authSecret, swagger, WebSecurityApplication, actuator, adminClient 6 | spring.cloud.config.fail-fast=true 7 | spring.cloud.config.uri=http://localhost:8888 8 | spring.config.import=optional:configserver:http://localhost:8888 9 | spring.cloud.config.import-check.enabled=true 10 | -------------------------------------------------------------------------------- /Apps/WebApplication/src/main/java/ru/app/db/entity/enums/ContactTypeEnum.java: -------------------------------------------------------------------------------- 1 | package ru.app.db.entity.enums; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | /**Тип контакта*/ 7 | @Getter 8 | @AllArgsConstructor 9 | public enum ContactTypeEnum { 10 | MOBILE_PHONE("Мобильный телефон"), 11 | EMAIL("Электронная почта"), 12 | SOCIAL_NET("Социальная сеть"), 13 | MESSAGER("Мессенджер"); 14 | 15 | final String local; 16 | } 17 | -------------------------------------------------------------------------------- /Apps/WebApplication/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=WebApplication1 2 | server.port=8080 3 | 4 | #config 5 | spring.profiles.active=authSecret, swagger, hibernate, WebApplication1, actuator, ext, adminClient 6 | spring.cloud.config.fail-fast=true 7 | spring.cloud.config.uri=http://localhost:8888 8 | spring.config.import=optional:configserver:http://localhost:8888 9 | spring.cloud.config.import-check.enabled=true 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /SpringCloudConfig/src/main/java/ru/configs/SpringCloudConfig.java: -------------------------------------------------------------------------------- 1 | package ru.configs; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.config.server.EnableConfigServer; 6 | 7 | @SpringBootApplication 8 | @EnableConfigServer 9 | public class SpringCloudConfig { 10 | public static void main(String[] args) { 11 | SpringApplication.run(SpringCloudConfig.class, args); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /SpringCloudGateway/src/main/java/ru/gateway/SpringCloudGateway.java: -------------------------------------------------------------------------------- 1 | package ru.gateway; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.scheduling.annotation.EnableScheduling; 6 | 7 | @SpringBootApplication 8 | @EnableScheduling 9 | public class SpringCloudGateway { 10 | public static void main(String[] args) { 11 | SpringApplication.run(SpringCloudGateway.class, args); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Apps/WebApplication/src/main/java/ru/app/db/repository/WorkerlRepository.java: -------------------------------------------------------------------------------- 1 | package ru.app.db.repository; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.stereotype.Repository; 5 | import ru.app.db.entity.WorkerEntity; 6 | 7 | import java.util.List; 8 | import java.util.Optional; 9 | 10 | @Repository 11 | public interface WorkerlRepository extends JpaRepository { 12 | Optional findById(Long id); 13 | List findAll(); 14 | } 15 | -------------------------------------------------------------------------------- /AuthService/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | group = 'ru.auth' 3 | version = '1.0-SNAPSHOT' 4 | 5 | dependencies { 6 | //spring 7 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 8 | implementation 'org.springframework.boot:spring-boot-starter-web' 9 | 10 | //db 11 | runtimeOnly 'com.h2database:h2' 12 | testImplementation 'com.h2database:h2:2.2.224' 13 | 14 | //Bcrypt 15 | implementation group: 'org.mindrot', name: 'jbcrypt', version: '0.4' 16 | } 17 | 18 | test { 19 | useJUnitPlatform() 20 | } -------------------------------------------------------------------------------- /SpringCloudGateway/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | group = 'ru.gateway' 3 | version = '1.0-SNAPSHOT' 4 | 5 | dependencies { 6 | //spring 7 | implementation 'org.springframework.cloud:spring-cloud-starter-gateway' 8 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 9 | 10 | //swagger(webflux) for gateway 11 | implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.6.0' //https://www.baeldung.com/spring-cloud-gateway-integrate-openapi 12 | } 13 | 14 | 15 | test { 16 | useJUnitPlatform() 17 | } -------------------------------------------------------------------------------- /SpringCloudGateway/src/main/java/ru/gateway/actuator/CustomHealthIndicator.java: -------------------------------------------------------------------------------- 1 | package ru.gateway.actuator; 2 | 3 | import org.springframework.boot.actuate.health.Health; 4 | import org.springframework.boot.actuate.health.HealthIndicator; 5 | import org.springframework.stereotype.Component; 6 | 7 | @Component 8 | public class CustomHealthIndicator implements HealthIndicator { 9 | 10 | @Override 11 | public Health health() { 12 | // Custom health check logic 13 | return Health.up().withDetail("custom", "Custom health indicator").build(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'SpringSimple' 2 | 3 | include 'SpringCloudGateway' //gateway 4 | include 'AuthService' //сервис аутентификации. Выдаёт jwt-токен для последующей brear-авторизации 5 | include 'SpringCloudConfig' //конфиг сервер 6 | include 'SpringBootAdminServer' //сервер спринг-бут-админа 7 | 8 | //applications 9 | include 'Apps:WebApplication' //пример приложения 10 | include 'Apps:WebSecurityApplication' //пример приложения с идентиф на основе spring security 11 | include 'Apps:WebGenerateApplication' //пример приложения с автогенерацией веб-сервисов из ямл-файла (нет авторизации) -------------------------------------------------------------------------------- /AuthService/src/main/java/ru/auth/service/TokenService.java: -------------------------------------------------------------------------------- 1 | package ru.auth.service; 2 | 3 | //https://jwt.io/ 4 | 5 | import com.auth0.jwt.interfaces.DecodedJWT; 6 | 7 | import java.util.List; 8 | 9 | /**Генерация jwt-токена для клиента*/ 10 | public interface TokenService { 11 | String generateAccessToken(String clientId, String audience, String roles); 12 | String generateRefreshToken(String clientId, String audience); 13 | 14 | Boolean checkAccessToken(String accessToken); 15 | Boolean checkRefreshToken(String refreshToken); 16 | DecodedJWT decodeRefreshToken(String refreshToken); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /SpringBootAdminServer/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | group = 'ru.configs' 3 | version = '1.0-SNAPSHOT' 4 | 5 | 6 | dependencies { 7 | //spring 8 | implementation 'org.springframework.boot:spring-boot-starter-web' 9 | //spring boot admin 10 | implementation 'de.codecentric:spring-boot-admin-starter-server:3.1.5' 11 | 12 | //add security 13 | //implementation 'org.springframework.boot:spring-boot-starter-security:3.1.5' 14 | } 15 | 16 | /* 17 | ext { 18 | set('springCloudVersion', "2023.0.1") 19 | } 20 | dependencyManagement { 21 | imports { 22 | mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" 23 | } 24 | }*/ -------------------------------------------------------------------------------- /Apps/WebApplication/src/main/java/ru/app/utils/Utils.java: -------------------------------------------------------------------------------- 1 | package ru.app.utils; 2 | 3 | import jakarta.persistence.EntityManager; 4 | import jakarta.persistence.PersistenceContext; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.stereotype.Service; 8 | import ru.app.db.entity.AbstractEntity; 9 | 10 | /**Разные утилиты для работы с бд*/ 11 | @Service 12 | @Slf4j 13 | @RequiredArgsConstructor 14 | public class Utils { 15 | /**Вернёт первый не нулевой объект из переданных*/ 16 | public static T coalesce(T... items) { 17 | for (T i : items) if (i != null) return i; 18 | return null; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SpringCloudGateway/src/main/java/ru/gateway/actuator/CustomInfoContributor.java: -------------------------------------------------------------------------------- 1 | package ru.gateway.actuator; 2 | 3 | import org.springframework.boot.actuate.info.Info.Builder; 4 | import org.springframework.boot.actuate.info.InfoContributor; 5 | import org.springframework.stereotype.Component; 6 | 7 | /**Заполняем информацию в инфо*/ 8 | @Component 9 | public class CustomInfoContributor implements InfoContributor { 10 | 11 | @Override 12 | public void contribute(Builder builder) { 13 | builder 14 | .withDetail("version", "1.0.0") 15 | .withDetail("description", "My Spring Boot Application") 16 | .withDetail("environment", "production"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/modules.xml 9 | .idea/jarRepositories.xml 10 | .idea/compiler.xml 11 | .idea/libraries/ 12 | *.iws 13 | *.iml 14 | *.ipr 15 | out/ 16 | !**/src/main/**/out/ 17 | !**/src/test/**/out/ 18 | 19 | ### Eclipse ### 20 | .apt_generated 21 | .classpath 22 | .factorypath 23 | .project 24 | .settings 25 | .springBeans 26 | .sts4-cache 27 | bin/ 28 | !**/src/main/**/bin/ 29 | !**/src/test/**/bin/ 30 | 31 | ### NetBeans ### 32 | /nbproject/private/ 33 | /nbbuild/ 34 | /dist/ 35 | /nbdist/ 36 | /.nb-gradle/ 37 | 38 | ### VS Code ### 39 | .vscode/ 40 | 41 | ### Mac OS ### 42 | .DS_Store -------------------------------------------------------------------------------- /SpringBootAdminServer/src/main/java/ru/admin/SpringBootAdminServer.java: -------------------------------------------------------------------------------- 1 | package ru.admin; 2 | 3 | import de.codecentric.boot.admin.server.config.AdminServerHazelcastAutoConfiguration; 4 | import de.codecentric.boot.admin.server.config.EnableAdminServer; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | 8 | //https://www.baeldung.com/spring-boot-admin 9 | //https://habr.com/ru/articles/479954/ 10 | @SpringBootApplication(exclude = AdminServerHazelcastAutoConfiguration.class) 11 | @EnableAdminServer 12 | public class SpringBootAdminServer { 13 | public static void main(String[] args) { 14 | SpringApplication.run(SpringBootAdminServer.class, args); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Apps/WebGenerateApplication/src/main/resources/application.properties.txt: -------------------------------------------------------------------------------- 1 | spring.application.name=open-api-demo 2 | 3 | #web 4 | server.port=8082 5 | # inout answer on ResponseStatusException 6 | server.error.include-message=always 7 | 8 | #swagger 9 | #http://localhost:8080/api-docs 10 | #http://localhost:8080/api-docs.yaml 11 | #http://localhost:8080/swagger-ui/index.html 12 | springdoc.api-docs.path=/api-docs 13 | 14 | #actuator 15 | #https://www.baeldung.com/spring-boot-actuators 16 | management.endpoint.shutdown.enabled=true 17 | management.endpoints.web.exposure.include=* 18 | management.endpoint.health.group.custom.include=diskSpace,ping 19 | management.endpoint.health.group.custom.show-components=always 20 | management.endpoint.health.group.custom.show-details=always -------------------------------------------------------------------------------- /Apps/WebApplication/src/main/java/ru/app/api/dto/AnswerDto.java: -------------------------------------------------------------------------------- 1 | package ru.app.api.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import io.swagger.v3.oas.annotations.media.Schema; 5 | import lombok.Data; 6 | import lombok.experimental.Accessors; 7 | 8 | @Data 9 | @Accessors(chain = true) 10 | @JsonInclude(JsonInclude.Include.NON_NULL) 11 | @Schema(description ="Стандартизированное дто-ответа") 12 | public class AnswerDto { 13 | @Schema(description = "Текст ошибки, если она произошла", example = "Ошибка такая-то") 14 | String errorText; 15 | @Schema(description = "Тип ошибки, если она произошла", example = "ArithmeticException") 16 | String errorType; 17 | @Schema(description = "Данные ответа в случае успешной обработки запроса") 18 | Object data; 19 | } 20 | -------------------------------------------------------------------------------- /Apps/WebSecurityApplication/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | group = 'ru.app' 3 | version = '1.0-SNAPSHOT' 4 | 5 | dependencies { 6 | //spring 7 | implementation 'org.springframework.boot:spring-boot-starter-web' 8 | //spring security 9 | implementation 'org.springframework.boot:spring-boot-starter-security:3.4.1' 10 | 11 | //implementation 'org.springframework.security:spring-security-core:6.4.2' 12 | //implementation 'org.springframework.security:spring-security-config:6.4.2' 13 | //implementation 'org.springframework.security:spring-security-web:6.4.2' 14 | 15 | //swagger 16 | implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' //https://www.baeldung.com/spring-rest-openapi-documentation 17 | 18 | } 19 | 20 | 21 | test { 22 | useJUnitPlatform() 23 | } -------------------------------------------------------------------------------- /AuthService/src/main/java/ru/auth/AuthService.java: -------------------------------------------------------------------------------- 1 | package ru.auth; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.web.cors.CorsConfiguration; 7 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 8 | import org.springframework.web.filter.CorsFilter; 9 | 10 | //https://struchkov.dev/blog/ru/jwt-implementation-in-spring/ 11 | //https://tproger.ru/articles/pishem-java-veb-prilozhenie-na-sovremennom-steke-s-nulja-do-mikroservisnoj-arhitektury-chast-2 12 | @SpringBootApplication 13 | public class AuthService { 14 | public static void main(String[] args) { 15 | SpringApplication.run(AuthService.class, args); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /AuthService/src/main/java/ru/auth/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package ru.auth.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 5 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 6 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 7 | 8 | //https://www.baeldung.com/spring-cors 9 | /**Разрешим CORS-запросы на этот веб-сервер чтобы сваггер с шлюза мог успешно перенаправлять запросы сюда*/ 10 | @Configuration 11 | @EnableWebMvc 12 | public class WebConfig implements WebMvcConfigurer { 13 | 14 | @Override 15 | /**Разрешаем CORS для сваггера*/ 16 | public void addCorsMappings(CorsRegistry registry) { 17 | registry.addMapping("/**"); 18 | //registry.addMapping("/api-docs"); //только для сваггера 19 | } 20 | } -------------------------------------------------------------------------------- /Apps/WebApplication/src/main/java/ru/app/config/AppProperties.java: -------------------------------------------------------------------------------- 1 | package ru.app.config; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.Getter; 5 | import lombok.experimental.FieldDefaults; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.stereotype.Component; 8 | 9 | import javax.annotation.PostConstruct; 10 | 11 | /**Классс свойств*/ 12 | @Component 13 | @Getter 14 | @FieldDefaults(level = AccessLevel.PRIVATE) 15 | public class AppProperties { 16 | /**УИД-приложения*/ 17 | @Value("${spring.application.name:none}") 18 | String appName; 19 | 20 | /**Включена ли аутентификация*/ 21 | @Value("${auth.enabled:true}") 22 | private Boolean enabled; 23 | 24 | public static AppProperties self; 25 | 26 | @PostConstruct 27 | public void postConstruct(){ 28 | self=this; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Apps/WebGenerateApplication/src/main/java/ru/app/gen/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package ru.app.gen.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 5 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 6 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 7 | 8 | //https://www.baeldung.com/spring-cors 9 | 10 | /**Разрешим CORS-запросы на этот веб-сервер чтобы сваггер с шлюза мог успешно перенаправлять запросы сюда*/ 11 | @Configuration 12 | @EnableWebMvc 13 | public class WebConfig implements WebMvcConfigurer { 14 | 15 | @Override 16 | /**Разрешаем CORS для сваггера*/ 17 | public void addCorsMappings(CorsRegistry registry) { 18 | registry.addMapping("/**"); 19 | //registry.addMapping("/api-docs"); //только для сваггера 20 | } 21 | } -------------------------------------------------------------------------------- /Apps/WebSecurityApplication/src/main/resources/application.properties.txt: -------------------------------------------------------------------------------- 1 | spring.application.name=WebSecurityApplication1 2 | 3 | spring.security.user.name=u2 4 | spring.security.user.password=p2 5 | 6 | auth.jwt.secret.access=super-secret-key- 7 | 8 | #web 9 | server.port=8081 10 | # inout answer on ResponseStatusException 11 | server.error.include-message=always 12 | 13 | #swagger 14 | #http://localhost:8080/api-docs 15 | #http://localhost:8080/api-docs.yaml 16 | #http://localhost:8080/swagger-ui/index.html 17 | springdoc.api-docs.path=/api-docs 18 | 19 | #actuator 20 | #https://www.baeldung.com/spring-boot-actuators 21 | management.endpoint.shutdown.enabled=true 22 | management.endpoints.web.exposure.include=* 23 | management.endpoint.health.group.custom.include=diskSpace,ping 24 | management.endpoint.health.group.custom.show-components=always 25 | management.endpoint.health.group.custom.show-details=always -------------------------------------------------------------------------------- /Apps/WebApplication/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | group = 'ru.app' 3 | version = '1.0-SNAPSHOT' 4 | 5 | repositories { 6 | flatDir { 7 | dirs 'libs' 8 | } 9 | } 10 | 11 | dependencies { 12 | //spring 13 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 14 | implementation 'org.springframework.boot:spring-boot-starter-web' 15 | 16 | 17 | //db 18 | runtimeOnly 'com.h2database:h2' 19 | //runtimeOnly 'org.postgresql:postgresql' 20 | testImplementation 'com.h2database:h2:2.2.224' 21 | 22 | 23 | /*gson, json and json simple 24 | implementation 'com.googlecode.json-simple:json-simple:1.1' 25 | implementation 'org.openapitools:jackson-databind-nullable:0.2.6' 26 | implementation 'com.google.code.gson:gson:2.10.1' 27 | implementation 'org.apache.commons:commons-lang3:3.10' 28 | implementation 'org.json:json:20240303'*/ 29 | } 30 | 31 | test { 32 | useJUnitPlatform() 33 | } -------------------------------------------------------------------------------- /AuthService/src/main/java/ru/auth/db/entity/ClientEntity.java: -------------------------------------------------------------------------------- 1 | package ru.auth.db.entity; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Data 9 | @NoArgsConstructor 10 | @AllArgsConstructor 11 | @Entity 12 | @Table(name = "clients", indexes = { 13 | @Index(name = "clientId_audience", columnList = "clientId, audience", unique = true) 14 | }) 15 | /**Сущность для хранения польователей*/ 16 | public class ClientEntity { 17 | @Id 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | private Long id; 20 | 21 | /**УИД-клиента или его псевдоним*/ 22 | private String clientId; 23 | 24 | /**Его роли в рамках данной audience через запятую*/ 25 | private String roles; 26 | 27 | /**Система для которой клиенту предоставлен доступ под нужной ролью*/ 28 | private String audience; 29 | 30 | /**Хэш его пароля*/ 31 | private String hash; 32 | } 33 | -------------------------------------------------------------------------------- /Apps/WebGenerateApplication/src/main/java/ru/app/gen/config/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package ru.app.gen.config; 2 | 3 | import io.swagger.v3.oas.annotations.OpenAPIDefinition; 4 | import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; 5 | import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; 6 | import io.swagger.v3.oas.annotations.info.Contact; 7 | import io.swagger.v3.oas.annotations.info.Info; 8 | import io.swagger.v3.oas.annotations.security.SecurityScheme; 9 | 10 | //https://struchkov.dev/blog/ru/api-swagger/ 11 | 12 | //описание свагерра 13 | @OpenAPIDefinition( 14 | info = @Info( 15 | title = "Тестовое Api", 16 | description = "Пример описания АПИ", 17 | version = "1.0.0", 18 | contact = @Contact( 19 | name = "Иван Иванов", 20 | email = "ivan@ivanov.ruv", 21 | url = "https://ivan.ivanov.ru" 22 | ) 23 | ) 24 | ) 25 | public class SwaggerConfig { 26 | } 27 | -------------------------------------------------------------------------------- /Apps/WebApplication/src/main/java/ru/app/WebApplication.java: -------------------------------------------------------------------------------- 1 | package ru.app; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; 7 | import ru.app.db.service.GenerateDataInDbService; 8 | import ru.app.utils.ApplicationContextProvider; 9 | 10 | //@SpringBootApplication(exclude = { SecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class }) 11 | //выкл автоконфигурацию для спринг-секьюрити см https://www.baeldung.com/spring-boot-security-autoconfiguration 12 | @SpringBootApplication 13 | public class WebApplication { 14 | 15 | public static void main(String[] args) { 16 | SpringApplication.run(WebApplication.class, args); 17 | 18 | ApplicationContextProvider.getBean(GenerateDataInDbService.class).generate(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Apps/WebApplication/src/main/java/ru/app/utils/ApplicationContextProvider.java: -------------------------------------------------------------------------------- 1 | package ru.app.utils; 2 | 3 | import org.springframework.beans.BeansException; 4 | import org.springframework.context.ApplicationContext; 5 | import org.springframework.context.ApplicationContextAware; 6 | import org.springframework.stereotype.Component; 7 | 8 | 9 | /**Даёт возможность работать со спринговским контекстом в статических методах и/или методах класса не являющихся компонентами спринга*/ 10 | @Component 11 | public class ApplicationContextProvider implements ApplicationContextAware { 12 | 13 | private static ApplicationContext CONTEXT; 14 | 15 | @Override 16 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 17 | CONTEXT = applicationContext; 18 | } 19 | 20 | public static T getBean(Class beanClass) { 21 | return CONTEXT.getBean(beanClass); 22 | } 23 | 24 | public static Object getBean(String beanName) { 25 | return CONTEXT.getBean(beanName); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Apps/WebApplication/src/main/java/ru/app/advice/DefaultAdvice.java: -------------------------------------------------------------------------------- 1 | package ru.app.advice; 2 | 3 | import lombok.NoArgsConstructor; 4 | import lombok.Setter; 5 | import lombok.ToString; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.ControllerAdvice; 10 | import org.springframework.web.bind.annotation.ExceptionHandler; 11 | import ru.app.api.dto.AnswerDto; 12 | 13 | /**Перехват и пользовательская обработка исключений*/ 14 | @ControllerAdvice 15 | @Slf4j 16 | public class DefaultAdvice { 17 | @ExceptionHandler(Exception.class) 18 | public ResponseEntity handleException(Exception e) { 19 | log.error("Перехвачена ошибка: ",e); 20 | 21 | AnswerDto response = new AnswerDto(); 22 | response.setErrorText(e.getClass().getName()+": "+e.getLocalizedMessage()+(e.getCause()!=null ? " ("+e.getCause().toString()+")" : "")); 23 | response.setErrorType(e.getClass().getSimpleName()); 24 | 25 | return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Apps/WebApplication/src/main/java/ru/app/listener/MyRefreshListener.java: -------------------------------------------------------------------------------- 1 | package ru.app.listener; 2 | 3 | //https://habr.com/ru/companies/otus/articles/590761/ 4 | 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.cloud.context.config.annotation.RefreshScope; 9 | import org.springframework.cloud.context.environment.EnvironmentChangeEvent; 10 | import org.springframework.context.ApplicationListener; 11 | import org.springframework.stereotype.Service; 12 | 13 | //https://www.baeldung.com/spring-reloading-properties 14 | //https://habr.com/ru/companies/otus/articles/590761/ 15 | /**Пример как отловить выполнение /actuator/refresh*/ 16 | @Service 17 | @RefreshScope 18 | @Slf4j 19 | public class MyRefreshListener implements ApplicationListener { 20 | @Value("${myValue:none}") 21 | private String myValue; 22 | 23 | @Override 24 | public void onApplicationEvent(EnvironmentChangeEvent event) { 25 | if(event.getKeys().contains("myValue")) { 26 | log.info("REFRESH! myValue={}",myValue); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /SpringCloudGateway/src/main/java/ru/gateway/config/ProxyConfig.java: -------------------------------------------------------------------------------- 1 | package ru.gateway.config; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.cloud.gateway.route.RouteLocator; 5 | import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.http.HttpMethod; 9 | 10 | @Configuration 11 | class ProxyConfig { 12 | @Bean 13 | RouteLocator customRouteLocator(RouteLocatorBuilder builder) { 14 | return builder.routes() 15 | /* Как пример настройки 16 | .route("random_animal_route", 17 | route -> route.path("/myauth") 18 | .and() 19 | .method(HttpMethod.POST) 20 | //.filters(filter -> filter.stripPrefix(1)) 21 | .uri("http://localhost:8079/auth")) 22 | .route("zoo_route", 23 | route -> route.path("/zoo/**") 24 | .filters(filter -> filter.stripPrefix(1)) 25 | .uri("lb://zoo"))*/ 26 | .build(); 27 | } 28 | } -------------------------------------------------------------------------------- /Apps/WebApplication/src/main/java/ru/app/db/entity/ContactEntity.java: -------------------------------------------------------------------------------- 1 | package ru.app.db.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonBackReference; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonInclude; 6 | import jakarta.persistence.*; 7 | import jakarta.persistence.CascadeType; 8 | import lombok.AccessLevel; 9 | import lombok.Getter; 10 | import lombok.Setter; 11 | import lombok.experimental.FieldDefaults; 12 | import org.hibernate.annotations.*; 13 | import ru.app.db.entity.enums.ContactTypeEnum; 14 | 15 | /**Контакт*/ 16 | @Entity 17 | @Getter 18 | @Setter 19 | @FieldDefaults(level = AccessLevel.PUBLIC) 20 | @SQLRestriction("deleted = false") 21 | @SQLDelete(sql = "update Contact_Entity set deleted=true where id=?") //имя таблицы приходится задавать как константу 22 | @JsonInclude(JsonInclude.Include.NON_NULL) 23 | public class ContactEntity extends AbstractEntity { 24 | /**тип контакта*/ 25 | @Enumerated(EnumType.STRING) 26 | ContactTypeEnum type; 27 | 28 | /**сам контакт*/ 29 | String contact; 30 | 31 | /**поле обратной связи*/ 32 | @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) 33 | //@JsonBackReference https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion 34 | @JsonIgnore 35 | WorkerEntity worker; 36 | } 37 | -------------------------------------------------------------------------------- /Apps/WebSecurityApplication/src/main/java/ru/app/config/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package ru.app.config; 2 | 3 | import io.swagger.v3.oas.annotations.OpenAPIDefinition; 4 | import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; 5 | import io.swagger.v3.oas.annotations.info.Contact; 6 | import io.swagger.v3.oas.annotations.info.Info; 7 | import io.swagger.v3.oas.annotations.security.SecurityScheme; 8 | 9 | //https://struchkov.dev/blog/ru/api-swagger/ 10 | 11 | //описание свагерра 12 | @OpenAPIDefinition( 13 | info = @Info( 14 | title = "Тестовое Api", 15 | description = "Пример описания АПИ", 16 | version = "1.0.0", 17 | contact = @Contact( 18 | name = "Иван Иванов", 19 | email = "ivan@ivanov.ruv", 20 | url = "https://ivan.ivanov.ru" 21 | ) 22 | ) 23 | ) 24 | //авторизация которую должен исп сваггер 25 | @SecurityScheme( 26 | name = "JWT", 27 | type = SecuritySchemeType.HTTP, 28 | bearerFormat = "JWT", 29 | scheme = "bearer" 30 | ) 31 | /*@SecurityScheme( //for spring security 32 | name = "jsessionid", 33 | type = SecuritySchemeType.APIKEY, 34 | in = SecuritySchemeIn.COOKIE, 35 | paramName = "JSESSIONID" 36 | )*/ 37 | public class SwaggerConfig { 38 | } 39 | -------------------------------------------------------------------------------- /SpringCloudGateway/src/main/java/ru/gateway/actuator/MyCustomEndpoint.java: -------------------------------------------------------------------------------- 1 | package ru.gateway.actuator; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.ToString; 7 | import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation; 8 | import org.springframework.boot.actuate.endpoint.annotation.Endpoint; 9 | import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; 10 | import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; 11 | import org.springframework.stereotype.Component; 12 | 13 | /**Пример самописного эндпойнта для актуатора*/ 14 | @Component 15 | @Endpoint(id = "myCustomEndpoint") 16 | public class MyCustomEndpoint { 17 | MyCustomData data=new MyCustomData(101, "Hello"); 18 | 19 | @ReadOperation //GET http://localhost/actuator/myCustomEndpoint 20 | public MyCustomData getData() { 21 | return data; 22 | } 23 | @WriteOperation //POST 24 | public void writeData(Integer id, String msg) { 25 | data.i=id; 26 | data.s=msg; 27 | } 28 | @DeleteOperation //DELETE 29 | public Integer deleteData() { 30 | data.i=-1; 31 | data.s=""; 32 | return 0; 33 | } 34 | } 35 | 36 | @AllArgsConstructor 37 | @NoArgsConstructor 38 | @Data 39 | class MyCustomData{ 40 | Integer i; 41 | String s; 42 | } -------------------------------------------------------------------------------- /Apps/WebApplication/src/main/java/ru/app/config/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package ru.app.config; 2 | 3 | import io.swagger.v3.oas.annotations.OpenAPIDefinition; 4 | import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; 5 | import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; 6 | import io.swagger.v3.oas.annotations.info.Contact; 7 | import io.swagger.v3.oas.annotations.info.Info; 8 | import io.swagger.v3.oas.annotations.security.SecurityScheme; 9 | 10 | //https://struchkov.dev/blog/ru/api-swagger/ 11 | 12 | //описание свагерра 13 | @OpenAPIDefinition( 14 | info = @Info( 15 | title = "Тестовое Api", 16 | description = "Пример описания АПИ", 17 | version = "1.0.0", 18 | contact = @Contact( 19 | name = "Иван Иванов", 20 | email = "ivan@ivanov.ruv", 21 | url = "https://ivan.ivanov.ru" 22 | ) 23 | ) 24 | ) 25 | //авторизация которую должен исп сваггер 26 | @SecurityScheme( 27 | name = "JWT", 28 | type = SecuritySchemeType.HTTP, 29 | bearerFormat = "JWT", 30 | scheme = "bearer" 31 | ) 32 | /*@SecurityScheme( //for spring security 33 | name = "jsessionid", 34 | type = SecuritySchemeType.APIKEY, 35 | in = SecuritySchemeIn.COOKIE, 36 | paramName = "JSESSIONID" 37 | )*/ 38 | public class SwaggerConfig { 39 | } 40 | -------------------------------------------------------------------------------- /Apps/WebApplication/src/main/java/ru/app/utils/ObjectMapperWrapper.java: -------------------------------------------------------------------------------- 1 | package ru.app.utils; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAutoDetect; 4 | import com.fasterxml.jackson.annotation.PropertyAccessor; 5 | import com.fasterxml.jackson.databind.DeserializationFeature; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import com.fasterxml.jackson.databind.SerializationFeature; 8 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 9 | import lombok.SneakyThrows; 10 | 11 | /**Враппер для мапера из джсона в объект и обратно*/ 12 | public class ObjectMapperWrapper { 13 | 14 | private static final ObjectMapper mapper; 15 | 16 | static{ 17 | mapper =new ObjectMapper(); 18 | mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 19 | mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); 20 | mapper.registerModule(new JavaTimeModule()); 21 | mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); 22 | } 23 | 24 | /**Преобразует json-строку в объект*/ 25 | @SneakyThrows 26 | public static T readValue(String content, Class valueType){ 27 | return mapper.readValue(content, valueType); 28 | } 29 | 30 | /**Преобразует объект в json-строку*/ 31 | @SneakyThrows 32 | public static String writeValueAsString(Object value) { 33 | return mapper.writeValueAsString(value); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Apps/WebApplication/src/main/java/ru/app/api/auth/DefaultTokenService.java: -------------------------------------------------------------------------------- 1 | package ru.app.api.auth; 2 | 3 | import com.auth0.jwt.JWT; 4 | import com.auth0.jwt.JWTVerifier; 5 | import com.auth0.jwt.algorithms.Algorithm; 6 | import com.auth0.jwt.exceptions.JWTVerificationException; 7 | import com.auth0.jwt.interfaces.DecodedJWT; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.stereotype.Service; 12 | import org.springframework.web.context.request.RequestContextHolder; 13 | 14 | import java.util.Arrays; 15 | 16 | @Service 17 | @Slf4j 18 | @RequiredArgsConstructor 19 | public class DefaultTokenService implements TokenService { 20 | @Value("${auth.jwt.secret.access}") 21 | private String secretKey; 22 | @Value("${spring.application.name}") 23 | private String appName; 24 | 25 | @Override 26 | public DecodedJWT checkToken(String token) { 27 | Algorithm algorithm = Algorithm.HMAC256(secretKey); 28 | JWTVerifier verifier = JWT.require(algorithm).build(); 29 | 30 | try { 31 | DecodedJWT decodedJWT = verifier.verify(token); 32 | if (!decodedJWT.getIssuer().equals("auth-service")) { 33 | log.error("Issuer is incorrect"); 34 | return null; 35 | } 36 | 37 | if (!decodedJWT.getAudience().contains(appName)) { 38 | log.error("Audience is incorrect"); 39 | return null; 40 | } 41 | 42 | return decodedJWT; 43 | } catch (JWTVerificationException e) { 44 | log.error("Token is invalid: " + e.getMessage()); 45 | return null; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Apps/WebApplication/src/main/java/ru/app/db/entity/WorkerEntity.java: -------------------------------------------------------------------------------- 1 | package ru.app.db.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import com.fasterxml.jackson.annotation.JsonManagedReference; 6 | import jakarta.persistence.*; 7 | import lombok.AccessLevel; 8 | import lombok.Getter; 9 | import lombok.Setter; 10 | import lombok.experimental.FieldDefaults; 11 | import org.hibernate.annotations.SQLDelete; 12 | import org.hibernate.annotations.SQLRestriction; 13 | import org.hibernate.annotations.SoftDelete; 14 | import org.springframework.context.annotation.Lazy; 15 | 16 | import java.util.ArrayList; 17 | import java.util.HashSet; 18 | import java.util.List; 19 | import java.util.Set; 20 | 21 | /**Сотрудник*/ 22 | @Entity 23 | @Getter 24 | @Setter 25 | @FieldDefaults(level = AccessLevel.PUBLIC) 26 | @Lazy 27 | @SQLRestriction("deleted = false") 28 | @SQLDelete(sql = "update Worker_Entity set deleted=true where id=?") //имя таблицы приходится задавать как константу 29 | //@SoftDelete(columnName = "deleted") нельзя использовать lazy-стратегии загрузки связанных сущностей 30 | @JsonInclude(JsonInclude.Include.NON_NULL) 31 | public class WorkerEntity extends AbstractEntity { 32 | /**Имя сотрудника*/ 33 | @JsonIgnore 34 | String lastName=""; 35 | @JsonIgnore 36 | String firstName=""; 37 | 38 | public String getFullName(){ 39 | return lastName +" " +firstName; 40 | } 41 | 42 | /**Контакты сотрудника*/ 43 | @OneToMany(mappedBy = "worker", fetch = FetchType.LAZY, cascade = CascadeType.ALL) 44 | //@JsonManagedReference 45 | Set contacts=new HashSet<>(); 46 | 47 | /**Заметки о сотруднике*/ 48 | @ElementCollection 49 | List notes=new ArrayList<>(); 50 | } 51 | -------------------------------------------------------------------------------- /AuthService/src/main/resources/OAuth2.txt: -------------------------------------------------------------------------------- 1 | https://struchkov.dev/blog/ru/jwt-implementation-in-spring/ 2 | 3 | Одним из преимуществ аутентификации с использованием JWT является возможность выделить процессы генерации токенов и обработки данных пользователей в отдельное приложение, переложив проверку валидности токенов на клиентские приложения. Такой подход идеально подходит для микросервисной архитектуры. 4 | 5 | Для начала мы создадим приложение, которое будет совмещать бизнес-логику и функцию выдачи токенов. Позже мы разделим эти функции на два отдельных приложения: одно будет отвечать за генерацию токенов, а другое — за бизнес-логику. Приложение с бизнес-логикой не сможет выдавать токены, но будет иметь возможность их валидировать. В будущем подобных приложений может быть несколько. 6 | 7 | Рассмотрим процесс аутентификации пошагово. В большинстве обучающих материалов часто не упоминается наличие refresh-токена, хотя он является важной частью системы аутентификации на основе JWT. Поэтому мы также включим его в рассмотрение. Вот как будет выглядеть процесс аутентификации: 8 | 9 | Запрос с логином и паролем. Клиент (чаще всего это фронтенд) отправляет запрос с объектом, содержащим логин и пароль. 10 | Генерация токенов. Если введённый пароль корректен, сервер генерирует access- и refresh-токены и возвращает их клиенту. 11 | Использование access-токена. Клиент использует access-токен для взаимодействия с API. 12 | Обновление access-токена. Через пять минут, когда срок действия access-токена истекает, клиент отправляет refresh-токен и получает новый access-токен. Этот процесс повторяется до тех пор, пока не истечёт срок действия refresh-токена. 13 | Продление refresh-токена. Refresh-токен выдаётся на 30 дней. Примерно на 25-29 день клиент отправляет запрос с действительными access- и refresh-токенами и получает новую пару токенов. -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | Исходники для статьи https://habr.com/ru/articles/872776/ 2 | 3 | 4 | У нас был spring и hibernate 5 | Мы твёрдо знали, что OAuth2.0 это хорошо, а принцип "Api first" ещё лучше. 6 | И нам дали ровно 48-ь часов чтобы создать работающую инфраструктуру с возможностями авторизации, отслеживания метрик, динамической маршрутизацией запросов и раздачей конфигураций сервисам "на лету". Кроме того от нас хотели получить "best practics" по работе с БД в коде и лёгкой поддержке ролевой модели. 7 | Мы справились. 8 | Это оказалось даже легче чем можно было подумать сначала. 9 | 10 | 11 | 12 | В рамках данного репозитория созданы и увязаны в единую инрфаструктуру следующие проекты: 13 | 14 | 1. Сервис авторизации (AuthService) который будет по протоколу oauth 2.0 выдавать доступы к нашим приложениям. 15 | 16 | 2. Сервис конфигурации (SpringCloudConfig) который будет раздавать настройки всем нашим приложениям которым требуются настройки. Также он даст нам возможность обновлять настройки приложения без необходимости перезапускать само приложение и/или данный сервер конфигурации. 17 | 18 | 3. Гейтвей (SpringCloudGateway) который отвечает за пробрасывание запросов из условно не безопасной зоны в условно безопасную зону где расположены все остальные сервисы. 19 | 20 | 4. Сервис мониторинга (SpringBootAdminServer) предоставляющий простую систему мониторинга множества работающих сервисов. 21 | 22 | 5. Сервис-приложение 1 (WebApplication) представляющий собой обычный веб-сервис с доступом к БД и ручной авторизаций через сервер-авторизации. 23 | 24 | 6. Сервис-приложение 2 (WebSecurityApplication) представляющий собой обычный веб-сервис с авторизацией частично использующей механизмы spring security 25 | 26 | 7. Сервис-приложение 3 (WebGenerateApplication) не требующий авторизации, но зато демонстрирующий использование популярного при разработке принципа "api first" и автогенерации кода. -------------------------------------------------------------------------------- /Apps/WebSecurityApplication/src/main/java/ru/app/api/WebController.java: -------------------------------------------------------------------------------- 1 | package ru.app.api; 2 | 3 | import io.swagger.v3.oas.annotations.ExternalDocumentation; 4 | import io.swagger.v3.oas.annotations.Operation; 5 | import io.swagger.v3.oas.annotations.responses.ApiResponse; 6 | import io.swagger.v3.oas.annotations.responses.ApiResponses; 7 | import io.swagger.v3.oas.annotations.security.SecurityRequirement; 8 | import io.swagger.v3.oas.annotations.tags.Tag; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.security.access.prepost.PreAuthorize; 12 | import org.springframework.web.bind.annotation.*; 13 | import ru.app.service.MyService; 14 | 15 | @RestController 16 | @Tag(name = "Название контроллера", description = "Описание контроллера", 17 | externalDocs = @ExternalDocumentation( 18 | description = "Ссылка на общую документацию", 19 | url = "https://example.com/docs/user-controller" 20 | ) 21 | ) 22 | @RequestMapping("/app/v1") 23 | @Slf4j 24 | @RequiredArgsConstructor 25 | @SecurityRequirement(name = "JWT") 26 | public class WebController { 27 | private final MyService service; 28 | 29 | @Operation(summary = "Хэллоу1-апи", description = "пример простого тестового апи") 30 | @GetMapping("/hello1") 31 | public String hello1() { 32 | return "Hello_1"; 33 | } 34 | 35 | @PreAuthorize("hasRole('ADMIN')") 36 | @Operation(summary = "Хэллоу2-апи", description = "пример простого тестового апи") 37 | @GetMapping("/hello2") 38 | public String hello2() { 39 | return "Hello_2"; 40 | } 41 | 42 | @PreAuthorize("hasRole('ADMIN')") 43 | @Operation(summary = "Хэллоу3-апи", description = "пример простого тестового апи") 44 | @GetMapping("/hello3") 45 | public String hello3() { 46 | return service.helloService(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /SpringCloudGateway/src/main/java/ru/gateway/actuator/metrics/MetricScheduler.java: -------------------------------------------------------------------------------- 1 | package ru.gateway.actuator.metrics; 2 | 3 | import io.micrometer.core.instrument.Counter; 4 | import io.micrometer.core.instrument.MeterRegistry; 5 | import org.springframework.scheduling.annotation.Scheduled; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.util.Random; 9 | import java.util.concurrent.atomic.AtomicInteger; 10 | 11 | /**Шедуллер для обработки кастомных меток 12 | * https://habr.com/ru/companies/otus/articles/650871/ 13 | * Из MeterRegistry можно инстанцировать следующие типы счетчиков: 14 | * Counter: сообщает только о результатах подсчета указанного свойства приложения. 15 | * Gauge: показывает текущее значение измерительного прибора 16 | * Timers: измеряет задержки или частоту событий 17 | * DistributionSummary: обеспечивает дистрибуцию событий и простую итоговую сводку.*/ 18 | @Component 19 | public class MetricScheduler { 20 | private final AtomicInteger testGauge; 21 | private final Counter testCounter; 22 | 23 | public MetricScheduler(MeterRegistry meterRegistry) { 24 | // Counter vs. gauge, summary vs. histogram 25 | // https://prometheus.io/docs/practices/instrumentation/#counter-vs-gauge-summary-vs-histogram 26 | testGauge = meterRegistry.gauge("custom_gauge", new AtomicInteger(0)); 27 | testCounter = meterRegistry.counter("custom_counter"); 28 | } 29 | 30 | @Scheduled(fixedRateString = "1000", initialDelayString = "0") 31 | public void schedulingTask() { 32 | testGauge.set(getRandomNumberInRange(0, 100)); 33 | testCounter.increment(); 34 | } 35 | 36 | private static int getRandomNumberInRange(int min, int max) { 37 | if (min >= max) { 38 | throw new IllegalArgumentException("max must be greater than min"); 39 | } 40 | 41 | Random r = new Random(); 42 | return r.nextInt((max - min) + 1) + min; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Apps/WebGenerateApplication/build.gradle: -------------------------------------------------------------------------------- 1 | group = 'ru.app.gen' 2 | version = '0.0.1-SNAPSHOT' 3 | 4 | apply plugin: 'org.openapi.generator' 5 | 6 | java { 7 | toolchain { 8 | languageVersion = JavaLanguageVersion.of(21) 9 | } 10 | } 11 | 12 | dependencies { 13 | implementation 'org.springframework.boot:spring-boot-starter-web' 14 | 15 | //swagger 16 | implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' 17 | 18 | implementation 'jakarta.validation:jakarta.validation-api:3.0.2' 19 | implementation 'javax.servlet:javax.servlet-api:3.0.1' 20 | implementation 'org.openapitools:jackson-databind-nullable:0.2.6' 21 | } 22 | 23 | 24 | tasks.named('compileJava') { 25 | dependsOn(tasks.openApiGenerate) 26 | } 27 | 28 | 29 | //restclient 30 | /* 31 | openApiGenerate { 32 | generatorName.set('java') 33 | configOptions.set([ 34 | library: 'restclient', 35 | openApiNullable: 'false' 36 | ]) 37 | inputSpec = "${projectDir}/src/main/resources/petclinic-spec.yml" 38 | outputDir = "${projectDir}/build/generated/java-rest-client" 39 | ignoreFileOverride.set(".openapi-generator-java-sources.ignore") 40 | invokerPackage.set('com.myapp') 41 | modelPackage.set('com.myapp.model') 42 | apiPackage.set('com.myapp.api') 43 | } 44 | */ 45 | 46 | //web-server 47 | openApiGenerate { 48 | generatorName = 'spring' 49 | configOptions.set([ 50 | library: 'spring-boot', 51 | openApiNullable: 'false', 52 | generateSupportingFiles: 'false', 53 | interfaceOnly: 'true', 54 | useSpringBoot3: 'true' 55 | ]) 56 | inputSpec = "${projectDir}/src/main/resources/petclinic-spec.yml" 57 | outputDir = "${projectDir}/build/generated/java-server" 58 | apiPackage = "com.example.api" 59 | modelPackage = "com.example.model" 60 | //configOptions = [dateLibrary: "java8"] 61 | } 62 | 63 | sourceSets { 64 | main { 65 | java { 66 | srcDirs("$buildDir/generated/java-server/src/main/java") //server 67 | srcDirs("$buildDir/generated/java-rest-client/src/main/java") //client 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Apps/WebGenerateApplication/src/main/java/ru/app/gen/WebGenerateApplication.java: -------------------------------------------------------------------------------- 1 | package ru.app.gen; 2 | /* 3 | import com.myapp.ApiClient; 4 | import com.myapp.api.PetApi; 5 | */ 6 | import com.example.api.PetsApi; 7 | import com.example.model.Pet; 8 | import org.springframework.boot.ApplicationRunner; 9 | import org.springframework.boot.SpringApplication; 10 | import org.springframework.boot.autoconfigure.SpringBootApplication; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.http.ResponseEntity; 13 | import org.springframework.web.bind.annotation.GetMapping; 14 | import org.springframework.web.bind.annotation.RestController; 15 | import org.springframework.web.client.RestClient; 16 | 17 | import java.util.List; 18 | 19 | //https://habr.com/ru/companies/spring_aio/articles/833096/ 20 | //https://www.restack.io/p/openapi-generator-gradle-plugin-answer-spring-boot-example 21 | //https://openvalue.blog/posts/2023/11/26/communicating_our_apis_part2/ 22 | @SpringBootApplication 23 | @RestController 24 | public class WebGenerateApplication implements PetsApi { 25 | 26 | public static void main(String[] args) { 27 | SpringApplication.run(WebGenerateApplication.class, args); 28 | } 29 | 30 | 31 | @Override 32 | public ResponseEntity getPet(Integer petId) { 33 | Pet pet = new Pet(); 34 | pet.setName("Всем привет, это тестовый пример!"); 35 | return ResponseEntity.ok(pet); 36 | } 37 | 38 | /* 39 | @Bean 40 | ApiClient apiClient(RestClient.Builder builder) { 41 | var apiClient = new ApiClient(builder.build()); 42 | apiClient.setUsername("admin"); 43 | apiClient.setPassword("admin"); 44 | apiClient.setBasePath("http://localhost:9966/petclinic/api"); 45 | return apiClient; 46 | } 47 | 48 | @Bean 49 | PetApi petApi(ApiClient apiClient) { 50 | return new PetApi(apiClient); 51 | } 52 | */ 53 | 54 | /* @Bean 55 | ApplicationRunner applicationRunner(PetApi petApi) { 56 | return args -> { 57 | System.out.println(petApi.listPets()); 58 | }; 59 | }*/ 60 | } 61 | -------------------------------------------------------------------------------- /Apps/WebApplication/src/main/java/ru/app/db/service/GenerateDataInDbService.java: -------------------------------------------------------------------------------- 1 | package ru.app.db.service; 2 | 3 | import org.springframework.stereotype.Service; 4 | import org.springframework.transaction.annotation.Transactional; 5 | import ru.app.db.entity.AbstractEntity; 6 | import ru.app.db.entity.ContactEntity; 7 | import ru.app.db.entity.WorkerEntity; 8 | import ru.app.db.entity.enums.ContactTypeEnum; 9 | 10 | import java.util.*; 11 | 12 | /**Сервис генерации тестовых данных в бд*/ 13 | @Service 14 | public class GenerateDataInDbService { 15 | 16 | /**Сгенерируем тестовые данные для бд: сотрудников и их контакты*/ 17 | @Transactional 18 | public void generate(){ 19 | Random randomizer = new Random(); 20 | for(int i=0; i < 10; i++){ 21 | WorkerEntity worker = new WorkerEntity(); 22 | worker.setLastName(List.of("Иванов","Сидоров","Козлов","Петров","Багров").get(randomizer.nextInt(5))); 23 | worker.setFirstName(List.of("Иван","Сидор","Илья","Александр","Данила").get(randomizer.nextInt(5))); 24 | 25 | for(int j=0; j < randomizer.nextInt(4); j++){ 26 | ContactEntity contact = new ContactEntity(); 27 | contact.setType(getRandomValueFromEnum(ContactTypeEnum.values())); 28 | contact.setContact("123456"); 29 | contact.setWorker(worker); 30 | contact=AbstractEntity.save(contact); 31 | worker.getContacts().add(contact); 32 | } 33 | 34 | for(int j=0; j < randomizer.nextInt(4); j++) 35 | worker.getNotes().add(List.of("Хороший сотрудник","Плохой сотрудник","Вечно опаздывает","Постоянно перерабатывает","Коллег называет братьями и спрашивает 'в чём сила?'").get(randomizer.nextInt(4))); 36 | 37 | worker = AbstractEntity.save(worker); 38 | } 39 | } 40 | 41 | 42 | /** 43 | * Выбрать случайный элемент из перечислеия 44 | */ 45 | private static > V getRandomValueFromEnum(V[] enumArray) { 46 | return Arrays.stream(enumArray).toList().get((new Random()).nextInt(enumArray.length)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Apps/WebSecurityApplication/src/main/java/ru/app/api/auth/DefaultTokenService.java: -------------------------------------------------------------------------------- 1 | package ru.app.api.auth; 2 | 3 | import com.auth0.jwt.JWT; 4 | import com.auth0.jwt.JWTVerifier; 5 | import com.auth0.jwt.algorithms.Algorithm; 6 | import com.auth0.jwt.exceptions.JWTVerificationException; 7 | import com.auth0.jwt.interfaces.DecodedJWT; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 12 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 13 | import org.springframework.security.core.context.SecurityContext; 14 | import org.springframework.security.core.context.SecurityContextHolder; 15 | import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; 16 | import org.springframework.stereotype.Service; 17 | import org.springframework.web.context.request.RequestContextHolder; 18 | 19 | import java.util.Arrays; 20 | import java.util.List; 21 | 22 | @Service 23 | @Slf4j 24 | @RequiredArgsConstructor 25 | public class DefaultTokenService implements TokenService { 26 | @Value("${auth.jwt.secret.access}") 27 | private String secretKey; 28 | @Value("${spring.application.name}") 29 | private String appName; 30 | 31 | @Override 32 | public DecodedJWT checkToken(String token) { 33 | Algorithm algorithm = Algorithm.HMAC256(secretKey); 34 | JWTVerifier verifier = JWT.require(algorithm).build(); 35 | 36 | try { 37 | DecodedJWT decodedJWT = verifier.verify(token); 38 | if (!decodedJWT.getIssuer().equals("auth-service")) { 39 | log.error("Issuer is incorrect"); 40 | return null; 41 | } 42 | 43 | if (!decodedJWT.getAudience().contains(appName)) { 44 | log.error("Audience is incorrect"); 45 | return null; 46 | } 47 | 48 | return decodedJWT; 49 | } catch (JWTVerificationException e) { 50 | log.error("Token is invalid: " + e.getMessage()); 51 | return null; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /AuthService/src/main/resources/application.properties.txt: -------------------------------------------------------------------------------- 1 | spring.application.name=AuthService 2 | auth.jwt.secret.access=super-secret-key- 3 | auth.jwt.secret.refresh=super-secret-key- 4 | 5 | #web 6 | server.port=8079 7 | # inout answer on ResponseStatusException 8 | server.error.include-message=always 9 | 10 | #swagger 11 | #http://localhost:8080/api-docs 12 | #http://localhost:8080/api-docs.yaml 13 | #http://localhost:8080/swagger-ui/index.html 14 | springdoc.api-docs.enabled=true 15 | springdoc.swagger-ui.enabled=true 16 | springdoc.swagger-ui.path=swagger-ui.html 17 | springdoc.api-docs.path=/api-docs 18 | 19 | 20 | #h2 db 21 | spring.datasource.url: jdbc:h2:~/MYDB1;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=TRUE;AUTO_SERVER=TRUE 22 | #spring.datasource.url: jdbc:h2:mem:MYDB;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=TRUE 23 | spring.datasource.driverClassName: org.h2.Driver 24 | spring.datasource.username: sa 25 | spring.datasource.password: 26 | spring.jpa.database-platform: org.hibernate.dialect.H2Dialect 27 | #http://localhost:8080/h2-console 28 | spring.h2.console.enabled=true 29 | 30 | # https://www.baeldung.com/spring-boot-data-sql-and-schema-sql (schema.sql ? data.sql) ??(never/always) 31 | spring.datasource.initialization-mode=never 32 | spring.jpa.defer-datasource-initialization = false 33 | spring.sql.init.mode = never 34 | #encoding data.sql 35 | spring.datasource.sql-script-encoding= UTF-8 36 | spring.sql.init.encoding = UTF-8 37 | 38 | # Without below HikariCP uses deprecated com.zaxxer.hikari.hibernate.HikariConnectionProvider 39 | # Surprisingly enough below ConnectionProvider is in hibernate-hikaricp dependency and not hibernate-core 40 | # So you need to pull that dependency but, make sure to exclude it transitive dependencies or you will end up 41 | # with different versions of hibernate-core 42 | spring.jpa.hibernate.connection.provider_class=org.hibernate.hikaricp.internal.HikariCPConnectionProvider 43 | ## debug sql 44 | #spring.jpa.show-sql=true 45 | #spring.jpa.properties.hibernate.format_sql=true 46 | #logging.level.org.hibernate.SQL=DEBUG 47 | #logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE 48 | 49 | #turned on to enable lazy loading 50 | spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true 51 | 52 | ## Hibernate settings (update | none) 53 | spring.jpa.hibernate.ddl-auto=update 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Apps/WebApplication/src/main/java/ru/app/api/auth/AuthorizationService.java: -------------------------------------------------------------------------------- 1 | package ru.app.api.auth; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.web.context.request.RequestContextHolder; 5 | import ru.app.config.AppProperties; 6 | 7 | import java.util.List; 8 | 9 | @Slf4j 10 | public class AuthorizationService { 11 | /** 12 | * Проверит, что текущий пользователь имеет указанную роль или поднимет исключение 13 | * Если код выполнялся от лица системы, то исключения не будет 14 | */ 15 | public static void checkRole(String roleName) { 16 | if (!AppProperties.self.getEnabled()){ 17 | log.info("authorization disabled"); 18 | return; 19 | } 20 | 21 | try { 22 | String clientId = (String) RequestContextHolder.getRequestAttributes().getAttribute("clientId", 0); 23 | List roles = (List) RequestContextHolder.getRequestAttributes().getAttribute("roles", 0); 24 | Boolean result = roles.contains(roleName); 25 | log.info("clientId={} with roles={} check roleName={} result is {}", clientId, roles, roleName, result); 26 | if (!result) 27 | throw new RuntimeException("clientId=" + clientId + " no have role=" + roleName); 28 | 29 | } catch (NullPointerException e) { 30 | log.info("clientId={} with roles={} check roleName={} result is {}", "system", "[*]", roleName, true); 31 | } 32 | } 33 | 34 | 35 | /** 36 | * Проверит, что текущий пользователь имеет все указанные роли или поднимет исключение 37 | * Если код выполнялся от лица системы, то исключения не будет 38 | */ 39 | public static void checkRolesAnd(List roles) { 40 | roles.forEach(roleName -> checkRole(roleName)); 41 | } 42 | 43 | /** 44 | * Проверит, что текущий пользователь имеет хотя бы одну указанныю роль или поднимет исключение 45 | * Если код выполнялся от лица системы, то исключения не будет 46 | */ 47 | public static void checkRolesOr(List roles) { 48 | boolean result=false; 49 | for (String roleName : roles) 50 | try { 51 | checkRole(roleName); 52 | result=true; 53 | break; 54 | } catch (RuntimeException re) {} 55 | if (!result) 56 | throw new RuntimeException("No find role in client for roles=" + roles); 57 | 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /SpringCloudConfig/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=SpringCloudConfig 2 | 3 | # \u041F\u043E\u0440\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 4 | server.port = 8888 5 | 6 | spring.boot.admin.client.url=http://localhost:8099 7 | 8 | # \u0420\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u0438\u0435 Git-\u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F 9 | #\u041F\u043E\u0441\u043B\u0435 \u0437\u0430\u043F\u0443\u0441\u043A\u0430 Config Server \u043C\u043E\u0436\u043D\u043E \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C \u0434\u043E\u0441\u0442\u0443\u043F \u043A \u043A\u043E\u043D\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044F\u043C \u043F\u043E \u0430\u0434\u0440\u0435\u0441\u0443 http://localhost:8888/{applicationName}/{profile} 10 | #http://localhost:8888/application/default \u0437\u0434\u0435\u0441\u044C application \u2014 \u0438\u043C\u044F \u0444\u0430\u0439\u043B\u0430 (\u0431\u0435\u0437 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F), default \u2014 \u0438\u043C\u044F \u043F\u0440\u043E\u0444\u0438\u043B\u044F (\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E default). 11 | #remoute 12 | # spring.cloud.config.server.git.uri=https://github.com/myluckagain/config.git 13 | # \u041F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C, \u0447\u0442\u043E \u043F\u0440\u0438 \u0437\u0430\u043F\u0443\u0441\u043A\u0435 \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u043D\u0435 \u0431\u0443\u0434\u0435\u0442 \u043F\u0440\u043E\u0431\u043B\u0435\u043C \u0441 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0435\u043C 14 | #spring.cloud.config.server.git.cloneOnStart=true 15 | #local 16 | #spring.cloud.config.server.git.uri=d:\\Work\\Project\\SpringSimple\\SpringSimple\\SpringCloudConfig\\git-local-repo 17 | 18 | #\u0447\u0442\u043E\u0431\u044B \u0445\u0440\u0430\u043D\u0438\u0442\u044C \u0443 \u0441\u0435\u0431\u044F \u0432 \u0440\u0435\u0441\u0443\u0440\u0441\u0430\u0445 (resources/config/*) 19 | #spring.cloud.config.server.native.search-locations=classpath:/config 20 | #spring.cloud.config.server.git.search-paths=config-server/config 21 | 22 | #\u0427\u0442\u043E\u0431\u044B \u0445\u0440\u0430\u043D\u0438\u0442\u044C \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E \u0431\u0435\u0437 \u0433\u0438\u0442-\u0430 23 | spring.profiles.active=native 24 | spring.cloud.config.server.native.search-locations=file:///d:\\Work\\Project\\SpringSimple\\SpringSimple\\SpringCloudConfig\\git-local-repo 25 | -------------------------------------------------------------------------------- /Apps/WebApplication/src/main/java/ru/app/api/DbController.java: -------------------------------------------------------------------------------- 1 | package ru.app.api; 2 | 3 | import io.swagger.v3.oas.annotations.ExternalDocumentation; 4 | import io.swagger.v3.oas.annotations.Operation; 5 | import io.swagger.v3.oas.annotations.responses.ApiResponse; 6 | import io.swagger.v3.oas.annotations.responses.ApiResponses; 7 | import io.swagger.v3.oas.annotations.security.SecurityRequirement; 8 | import io.swagger.v3.oas.annotations.tags.Tag; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.transaction.annotation.Transactional; 12 | import org.springframework.web.bind.annotation.*; 13 | import ru.app.api.dto.AnswerDto; 14 | import ru.app.db.entity.AbstractEntity; 15 | import ru.app.db.entity.WorkerEntity; 16 | 17 | @RestController 18 | @Tag(name = "Примеры работы с БД", description = "Объединяет апи написанные в качестве примеров для работы с бд через хибернейт", 19 | externalDocs = @ExternalDocumentation( 20 | description = "Ссылка на общую документацию", 21 | url = "https://example.com/docs/user-controller" 22 | ) 23 | ) 24 | @RequestMapping("/app/db/worker") 25 | @Slf4j 26 | @RequiredArgsConstructor 27 | @SecurityRequirement(name = "JWT") 28 | public class DbController { 29 | @Operation(summary = "Получить сотрудника") 30 | @ApiResponses(value = { 31 | @ApiResponse(responseCode = "200", description = "успешно"), 32 | @ApiResponse(responseCode = "500", description = "внутренняя ошибка") 33 | }) 34 | @GetMapping("{id}") 35 | public AnswerDto findById(@PathVariable("id") Long workerId) { 36 | return new AnswerDto().setData(AbstractEntity.findById(WorkerEntity.class, workerId)); 37 | } 38 | 39 | @Operation(summary = "Удалить сотрудника") 40 | @ApiResponses(value = { 41 | @ApiResponse(responseCode = "200", description = "успешно"), 42 | @ApiResponse(responseCode = "500", description = "внутренняя ошибка") 43 | }) 44 | @DeleteMapping("{id}") 45 | @Transactional 46 | public AnswerDto delete(@PathVariable("id") Long workerId) { 47 | AbstractEntity.delete(AbstractEntity.findById(WorkerEntity.class, workerId)); 48 | return new AnswerDto().setData("Успешно удалено"); 49 | } 50 | 51 | @Operation(summary = "Получить всех сотрудников") 52 | @ApiResponses(value = { 53 | @ApiResponse(responseCode = "200", description = "успешно"), 54 | @ApiResponse(responseCode = "500", description = "внутренняя ошибка") 55 | }) 56 | @GetMapping("") 57 | public AnswerDto findByAll() { 58 | return new AnswerDto().setData(AbstractEntity.findForQuery("select we from WorkerEntity we",false, WorkerEntity.class)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /SpringBootAdminServer/src/main/java/ru/admin/config/WebSecurityConfig.java.txt: -------------------------------------------------------------------------------- 1 | package ru.admin.config; 2 | 3 | import de.codecentric.boot.admin.server.config.AdminServerProperties; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.http.HttpMethod; 8 | import org.springframework.security.config.Customizer; 9 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 10 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 11 | import org.springframework.security.web.SecurityFilterChain; 12 | import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; 13 | import org.springframework.security.web.csrf.CookieCsrfTokenRepository; 14 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 15 | 16 | import java.util.UUID; 17 | 18 | @Configuration 19 | @EnableWebSecurity 20 | @RequiredArgsConstructor 21 | public class WebSecurityConfig{ 22 | 23 | private final AdminServerProperties adminServer; 24 | 25 | @Bean 26 | public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { 27 | SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); 28 | successHandler.setTargetUrlParameter("redirectTo"); 29 | successHandler.setDefaultTargetUrl(this.adminServer.getContextPath() + "/"); 30 | 31 | http.authorizeHttpRequests(req -> req.requestMatchers(this.adminServer.getContextPath() + "/assets/**") 32 | .permitAll() 33 | .requestMatchers(this.adminServer.getContextPath() + "/login") 34 | .permitAll() 35 | .anyRequest() 36 | .authenticated()) 37 | .formLogin(formLogin -> formLogin.loginPage(this.adminServer.getContextPath() + "/login").successHandler(successHandler)) 38 | .logout((logout) -> logout.logoutUrl(this.adminServer.getContextPath() + "/logout")) 39 | .httpBasic(Customizer.withDefaults()) 40 | .csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) 41 | .ignoringRequestMatchers( 42 | new AntPathRequestMatcher(this.adminServer.getContextPath() + "/instances", HttpMethod.POST.toString()), 43 | new AntPathRequestMatcher(this.adminServer.getContextPath() + "/instances/*", HttpMethod.DELETE.toString()), 44 | new AntPathRequestMatcher(this.adminServer.getContextPath() + "/actuator/**"))) 45 | .rememberMe(rememberMe -> rememberMe.key(UUID.randomUUID().toString()).tokenValiditySeconds(1209600)); 46 | return http.build(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Apps/WebApplication/src/main/resources/application.properties.txt: -------------------------------------------------------------------------------- 1 | spring.application.name=WebApplication1 2 | 3 | #on/of auth and auth secret key 4 | auth.enabled=true 5 | auth.jwt.secret.access=super-secret-key- 6 | 7 | #web 8 | server.port=8080 9 | # inout answer on ResponseStatusException 10 | server.error.include-message=always 11 | 12 | #swagger 13 | #http://localhost:8080/api-docs 14 | #http://localhost:8080/api-docs.yaml 15 | #http://localhost:8080/swagger-ui/index.html 16 | springdoc.api-docs.path=/api-docs 17 | 18 | #actuator 19 | #https://www.baeldung.com/spring-boot-actuators 20 | management.endpoint.shutdown.enabled=true 21 | management.endpoints.web.exposure.include=* 22 | management.endpoint.health.group.custom.include=diskSpace,ping 23 | management.endpoint.health.group.custom.show-components=always 24 | management.endpoint.health.group.custom.show-details=always 25 | 26 | #h2 db 27 | #spring.datasource.url: jdbc:h2:~/MYDB;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=TRUE;AUTO_SERVER=TRUE 28 | spring.datasource.url: jdbc:h2:mem:MYDB;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=TRUE 29 | spring.datasource.driverClassName: org.h2.Driver 30 | spring.datasource.username: sa 31 | spring.datasource.password: 32 | spring.jpa.database-platform: org.hibernate.dialect.H2Dialect 33 | #http://localhost:8080/h2-console 34 | spring.h2.console.enabled=true 35 | 36 | # https://www.baeldung.com/spring-boot-data-sql-and-schema-sql (schema.sql ? data.sql) ??(never/always) 37 | spring.datasource.initialization-mode=never 38 | spring.jpa.defer-datasource-initialization = false 39 | spring.sql.init.mode = never 40 | #encoding data.sql 41 | spring.datasource.sql-script-encoding= UTF-8 42 | spring.sql.init.encoding = UTF-8 43 | 44 | # Without below HikariCP uses deprecated com.zaxxer.hikari.hibernate.HikariConnectionProvider 45 | # Surprisingly enough below ConnectionProvider is in hibernate-hikaricp dependency and not hibernate-core 46 | # So you need to pull that dependency but, make sure to exclude it transitive dependencies or you will end up 47 | # with different versions of hibernate-core 48 | spring.jpa.hibernate.connection.provider_class=org.hibernate.hikaricp.internal.HikariCPConnectionProvider 49 | ## debug sql 50 | #spring.jpa.show-sql=true 51 | #spring.jpa.properties.hibernate.format_sql=true 52 | #logging.level.org.hibernate.SQL=DEBUG 53 | #logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE 54 | 55 | #turned on to enable lazy loading 56 | spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true 57 | 58 | ## Hibernate settings (update | none) 59 | spring.jpa.hibernate.ddl-auto=update 60 | 61 | 62 | ## MULTIPART (MultipartProperties) 63 | # Enable multipart uploads 64 | spring.servlet.multipart.enabled=true 65 | # Threshold after which files are written to disk. 66 | spring.servlet.multipart.file-size-threshold=2KB 67 | # Max file size. 68 | spring.servlet.multipart.max-file-size=200MB 69 | # Max Request Size 70 | spring.servlet.multipart.max-request-size=215MB 71 | 72 | 73 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /Apps/WebApplication/src/main/java/ru/app/api/auth/AuthorizationFilter.java: -------------------------------------------------------------------------------- 1 | package ru.app.api.auth; 2 | 3 | import com.auth0.jwt.interfaces.DecodedJWT; 4 | import jakarta.servlet.FilterChain; 5 | import jakarta.servlet.ServletException; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.http.HttpHeaders; 11 | import org.springframework.http.HttpMethod; 12 | import org.springframework.stereotype.Component; 13 | import org.springframework.web.context.request.RequestContextHolder; 14 | import org.springframework.web.filter.OncePerRequestFilter; 15 | 16 | import java.io.IOException; 17 | import java.util.Arrays; 18 | import java.util.List; 19 | 20 | /**Фильтр проверяющий ацесс-токен для авторизации который надо получать у auth-service если в настройках включена авторизация*/ 21 | @Component 22 | @RequiredArgsConstructor 23 | public class AuthorizationFilter extends OncePerRequestFilter { 24 | 25 | private final TokenService tokenService; 26 | 27 | @Value("${auth.enabled}") 28 | private boolean enabled; 29 | 30 | @Override 31 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { 32 | if (!enabled 33 | || request.getMethod().equals(HttpMethod.OPTIONS.name()) 34 | || request.getRequestURI().contains("/public/") 35 | || request.getRequestURI().contains("/h2-console") 36 | || request.getRequestURI().contains("/actuator") 37 | || request.getRequestURI().contains("/swagger-ui") 38 | || request.getRequestURI().contains("/api-docs") 39 | ) { 40 | filterChain.doFilter(request, response); 41 | return; 42 | } 43 | 44 | String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); 45 | 46 | if (authHeader == null || authHeader.isBlank()) 47 | response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 48 | else { 49 | DecodedJWT decodedJWT = checkAuthorization(authHeader); 50 | if (decodedJWT==null) 51 | response.setStatus(HttpServletResponse.SC_FORBIDDEN); 52 | else { 53 | //Авторизация прошла успешно 54 | //заполним контекст запроса 55 | //получать как RequestContextHolder.getRequestAttributes().getAttribute("roles", 0); 56 | RequestContextHolder.getRequestAttributes().setAttribute("clientId", decodedJWT.getSubject(), 0); 57 | RequestContextHolder.getRequestAttributes().setAttribute("roles", Arrays.stream(decodedJWT.getClaim("roles").asString().split(",")).toList().stream().map(String::trim).toList(), 0); 58 | 59 | filterChain.doFilter(request, response); 60 | } 61 | } 62 | } 63 | 64 | private DecodedJWT checkAuthorization(String auth) { 65 | if (!auth.startsWith("Bearer ")) 66 | return null; 67 | 68 | String token = auth.substring(7); 69 | return tokenService.checkToken(token); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /прочти.txt: -------------------------------------------------------------------------------- 1 | Общий градле-проект 2 | https://www.book2s.com/tutorials/gradle-dependency-management.html 3 | 4 | SpringCloudGateway 5 | Гейтей связывающий AuthService и экземпляры WebApplication 6 | https://tproger.ru/articles/pishem-java-veb-prilozhenie-na-sovremennom-steke-s-nulja-do-mikroservisnoj-arhitektury-chast-3 7 | https://sysout.ru/spring-cloud-api-gateway/?ysclid=m5ergtfz7887528745 8 | https://www.concretepage.com/spring-boot/spring-cloud-gateway 9 | https://cloud.spring.io/spring-cloud-gateway/reference/html/ 10 | + actuator 11 | https://www.concretepage.com/spring-boot/spring-boot-actuator-endpoints 12 | https://www.book2s.com/tutorials/spring-boot-actuator.html 13 | https://habr.com/ru/companies/otus/articles/650871/ 14 | 15 | AuthService (OAuth) 16 | Сервер авторизации. 17 | Client Credentials Flow - Этот поток полезен для систем, которые должны выполнять операции API, когда пользователя нет. Например, ночные операции или другие, которые предполагают обращение к защищенным API OAuth 18 | https://tproger.ru/articles/pishem-java-veb-prilozhenie-na-sovremennom-steke-s-nulja-do-mikroservisnoj-arhitektury-chast-2 19 | https://jwt.io/ 20 | OAuth2 21 | https://struchkov.dev/blog/ru/jwt-implementation-in-spring/ 22 | https://habr.com/ru/articles/784508/ 23 | 24 | WebApplication 25 | Пример приложения работающего с БД и предоставляющего REST API + swagger. 26 | Добавлены разные мелкие красивости вроде примера перехватчика запросов/ответов или общего обработчика исключений, а также пример настраиваемого мягкого/жёсткого удаления 27 | Добавлена возможность работы через авторизацию посредством AuthService 28 | https://struchkov.dev/blog/ru/api-swagger/ 29 | https://tproger.ru/articles/pishem-java-veb-prilozhenie-na-sovremennom-steke-s-nulja-do-mikroservisnoj-arhitektury-chast-1 30 | https://tproger.ru/articles/pishem-java-veb-prilozhenie-na-sovremennom-steke-s-nulja-do-mikroservisnoj-arhitektury-chast-2 31 | 32 | 33 | WebSecurityApplication 34 | //https://habr.com/ru/articles/482552/ 35 | //https://habr.com/ru/articles/784508/ 36 | Немного информации о Spring Security 37 | Самым фундаментальным объектом является SecurityContextHolder. В нем хранится информация о текущем контексте безопасности приложения, который включает в себя подробную информацию о пользователе (принципале), работающим с приложением. 38 | Spring Security использует объект Authentication, пользователя авторизованной сессии. 39 | «Пользователь» – это просто Object. В большинстве случаев он может быть 40 | приведен к классу UserDetails. UserDetails можно представить, как адаптер между БД пользователей и тем что требуется Spring Security внутри SecurityContextHolder. 41 | Для создания UserDetails используется интерфейс UserDetailsService, с единственным методом: 42 | UserDetails loadUserByUsername(String username) throws UsernameNotFoundException 43 | 44 | WebGenerateApplication 45 | Пример автогенерации веб-сервисов из ямл-файлов 46 | https://habr.com/ru/companies/spring_aio/articles/833096/ 47 | https://openvalue.blog/posts/2023/11/26/communicating_our_apis_part2/ 48 | https://openapi-generator.tech/docs/generators/#server-generators 49 | 50 | SpringCloudConfig 51 | Конфиг сервер - раздаёт конфиги другим приложениям по запросу храня их либо локально, либо в гит-е 52 | https://sysout.ru/spring-cloud-configuration-server/ 53 | https://www.czetsuyatech.com/2019/10/spring-config-using-git.html 54 | https://docs.spring.io/spring-cloud-config/docs/current/reference/html/ 55 | https://www.baeldung.com/spring-cloud-config-without-git 56 | 57 | SpringBootAdminServer 58 | Сприг-бут-админ-сервер 59 | https://habr.com/ru/articles/479954/ -------------------------------------------------------------------------------- /Apps/WebApplication/src/main/java/ru/app/db/entity/AbstractEntity.java: -------------------------------------------------------------------------------- 1 | package ru.app.db.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import jakarta.persistence.*; 5 | import lombok.AccessLevel; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import lombok.Setter; 9 | import lombok.experimental.FieldDefaults; 10 | import org.hibernate.annotations.*; 11 | import org.springframework.core.annotation.AnnotatedElementUtils; 12 | import ru.app.db.service.EntityService; 13 | 14 | import java.io.Serializable; 15 | import java.lang.reflect.Field; 16 | import java.time.LocalDateTime; 17 | import java.util.List; 18 | import java.util.stream.Collectors; 19 | 20 | /**Общий абстрактный предок для всех сущностей*/ 21 | @MappedSuperclass 22 | @Getter 23 | @Setter 24 | @NoArgsConstructor 25 | @FieldDefaults(level = AccessLevel.PUBLIC) 26 | @SQLRestriction("deleted = false") 27 | public abstract class AbstractEntity implements Cloneable, Serializable { 28 | @Id 29 | @GeneratedValue(strategy = GenerationType.IDENTITY) 30 | Long id; 31 | 32 | @Column(columnDefinition = "boolean DEFAULT false") 33 | @JsonIgnore 34 | Boolean deleted = false; 35 | 36 | @CreationTimestamp //https://www.baeldung.com/hibernate-creationtimestamp-updatetimestamp 37 | LocalDateTime createdDt; 38 | @UpdateTimestamp 39 | LocalDateTime modifiedDt; 40 | 41 | /**Вызывается перед сохранением. Здесь можно проверять сложные условия перед сохранением или как-то изменять поля*/ 42 | public void beforeSave(){} 43 | 44 | /**Вызывается перед удалением. Здесь можно проверять сложные условия перед удалением и не дать удалить*/ 45 | public void beforeDelete(){} 46 | 47 | /**Сохранить сущность 48 | * @param entity - сохраняемая сущности 49 | * @return - сама сущность*/ 50 | static public A save(A entity){ 51 | return EntityService.self.save(entity); 52 | } 53 | 54 | /**Получить сущность по её уид-у 55 | * @param id - уид сущности 56 | * @param entityClass - класс искомой сущности 57 | * @return - сама сущность или исключение если её не было найдено*/ 58 | static public A findById(Class entityClass, Long id){ 59 | return EntityService.self.findById(entityClass, id); 60 | } 61 | 62 | /** Удалить сущность 63 | * @param entity - удаляемая сущность 64 | **/ 65 | static public void delete(A entity){ 66 | EntityService.self.delete(entity); 67 | } 68 | 69 | /**Получить набор сущностей по запросу 70 | * @param query- запрос 71 | * @param isNativeQuery - истина если нативный скл запрос и ложь если hql-запрос 72 | * @param entityClass - класс искомой сущности 73 | * @return - сама сущность или исключение если её не было найдено 74 | * 75 | * Примеры запросов 76 | * "select distinct e from WorkerEntity e where element(e.contacts).contact like '"+"%@%"+"'" 77 | * */ 78 | static public List findForQuery(String query, Boolean isNativeQuery, Class entityClass){ 79 | List list = isNativeQuery ? EntityService.self.getEm().createNativeQuery(query, entityClass).getResultList() : EntityService.self.getEm().createQuery(query, entityClass).getResultList(); 80 | return list.stream().map(e -> { 81 | e = EntityService.lazyInitializer(e); 82 | return e; 83 | }).collect(Collectors.toList()); 84 | } 85 | 86 | @Override 87 | public boolean equals(Object obj) { 88 | if (this.getId()==null || ((AbstractEntity) obj).getId()==null) return false; 89 | if (!this.getClass().equals(obj.getClass())) return false; 90 | return ((AbstractEntity) obj).getId().equals(this.getId()); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /AuthService/src/main/java/ru/auth/service/DefaultTokenService.java: -------------------------------------------------------------------------------- 1 | package ru.auth.service; 2 | 3 | import com.auth0.jwt.JWT; 4 | import com.auth0.jwt.JWTVerifier; 5 | import com.auth0.jwt.algorithms.Algorithm; 6 | import com.auth0.jwt.exceptions.JWTVerificationException; 7 | import com.auth0.jwt.interfaces.DecodedJWT; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.time.Instant; 13 | import java.time.temporal.ChronoUnit; 14 | import java.util.Date; 15 | import java.util.List; 16 | 17 | @Service 18 | @Slf4j 19 | public class DefaultTokenService implements TokenService { 20 | @Value("${auth.jwt.secret.access}") 21 | private String secretAccessKey; 22 | @Value("${auth.jwt.secret.refresh}") 23 | private String secretRefreshKey; 24 | 25 | 26 | /**Сгенерировать токен для клиента 27 | * @param clientId - клиент для которого генерируем токен 28 | * @param secretKey - секретный ключ на основе которого будем его генерировать 29 | * @param minuts - через сколько минут истечёт срок действия токена 30 | * @param audience - для какой системы генерируем токен 31 | * @param roles - список ролей доступных текущему пользователю для данной системы чере запятую 32 | * @return - сгенерированный токен 33 | * */ 34 | private String generateToken(String clientId, String secretKey, Long minuts, String audience, String roles) { 35 | Algorithm algorithm = Algorithm.HMAC256(secretKey); 36 | 37 | Instant now = Instant.now(); 38 | Instant exp = now.plus(minuts, ChronoUnit.MINUTES); 39 | 40 | return JWT.create() 41 | .withIssuer("auth-service") 42 | .withClaim("roles",roles) 43 | .withAudience(audience) 44 | .withSubject(clientId) 45 | .withIssuedAt(Date.from(now)) 46 | .withExpiresAt(Date.from(exp)) 47 | .sign(algorithm); 48 | } 49 | 50 | @Override 51 | public String generateAccessToken(String clientId, String audience, String roles) { 52 | return generateToken(clientId, secretAccessKey, 5L, audience, roles); 53 | } 54 | 55 | @Override 56 | public String generateRefreshToken(String clientId, String audience) { 57 | return generateToken(clientId, secretRefreshKey, 666L, audience, null); 58 | } 59 | 60 | 61 | /**Сгенерировать токен для клиента 62 | * @param token - проверяемый токен 63 | * @param secretKey - секретный ключ которым он был закодирован 64 | * @return - истина, если токен валиден и ложь если не валиден*/ 65 | private boolean checkToken(String token, String secretKey) { 66 | Algorithm algorithm = Algorithm.HMAC256(secretKey); 67 | JWTVerifier verifier = JWT.require(algorithm).build(); 68 | 69 | try { 70 | DecodedJWT decodedJWT = verifier.verify(token); 71 | return true; 72 | } catch (JWTVerificationException e) { 73 | log.error("Token is invalid: " + e.getMessage()); 74 | return false; 75 | } 76 | } 77 | 78 | /**Декодировать токен 79 | * @param refreshToken - рефреш-токен 80 | * @return - декодированный токен или исключение*/ 81 | @Override 82 | public DecodedJWT decodeRefreshToken(String refreshToken) { 83 | Algorithm algorithm = Algorithm.HMAC256(secretAccessKey); 84 | JWTVerifier verifier = JWT.require(algorithm).build(); 85 | return verifier.verify(refreshToken); 86 | } 87 | 88 | @Override 89 | public Boolean checkAccessToken(String accessToken) { 90 | return checkToken(accessToken, secretAccessKey); 91 | } 92 | 93 | @Override 94 | public Boolean checkRefreshToken(String refreshToken) { 95 | return checkToken(refreshToken, secretRefreshKey); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Apps/WebApplication/src/main/java/ru/app/api/WebController.java: -------------------------------------------------------------------------------- 1 | package ru.app.api; 2 | 3 | import io.swagger.v3.oas.annotations.ExternalDocumentation; 4 | import io.swagger.v3.oas.annotations.Operation; 5 | import io.swagger.v3.oas.annotations.responses.ApiResponse; 6 | import io.swagger.v3.oas.annotations.responses.ApiResponses; 7 | import io.swagger.v3.oas.annotations.security.SecurityRequirement; 8 | import io.swagger.v3.oas.annotations.tags.Tag; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.beans.factory.annotation.Value; 12 | import org.springframework.cloud.context.config.annotation.RefreshScope; 13 | import org.springframework.core.io.InputStreamResource; 14 | import org.springframework.core.io.Resource; 15 | import org.springframework.http.HttpHeaders; 16 | import org.springframework.http.MediaType; 17 | import org.springframework.http.ResponseEntity; 18 | import org.springframework.web.bind.annotation.*; 19 | import org.springframework.web.multipart.MultipartFile; 20 | import ru.app.api.auth.AuthorizationService; 21 | import ru.app.api.dto.AnswerDto; 22 | 23 | import java.io.ByteArrayInputStream; 24 | import java.nio.charset.StandardCharsets; 25 | import java.util.Map; 26 | 27 | @RestController 28 | @Tag(name = "Название контроллера", description = "Описание контроллера", 29 | externalDocs = @ExternalDocumentation( 30 | description = "Ссылка на общую документацию", 31 | url = "https://example.com/docs/user-controller" 32 | ) 33 | ) 34 | @RequestMapping("/app/v1") 35 | @Slf4j 36 | @RequiredArgsConstructor 37 | @SecurityRequirement(name = "JWT") 38 | @RefreshScope 39 | public class WebController { 40 | @Value("${myValue:none}") 41 | private String myValue; 42 | 43 | @Operation(summary = "Хэллоу-апи", description = "пример простого тестового апи") 44 | @ApiResponses(value = { 45 | @ApiResponse(responseCode = "200", description = "успешно") 46 | }) 47 | @GetMapping("/hello") 48 | public AnswerDto hello(@RequestParam(required = false) String name) { 49 | 50 | /*Thread thread = new Thread(() -> { 51 | AuthorizationService.checkRole("ROLE_WEB"); 52 | }); 53 | thread.start();*/ 54 | 55 | AuthorizationService.checkRole("ROLE_WEB"); 56 | return new AnswerDto().setData("Hello, " + name+" "+myValue); 57 | } 58 | 59 | @Operation(summary = "Пример апи получающего и отправляющего файлы") 60 | @ApiResponses(value = { 61 | @ApiResponse(responseCode = "200", description = "успешно") 62 | }) 63 | @PostMapping(value = "/file", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}) 64 | public ResponseEntity workWithFile(@RequestPart("file") MultipartFile file, @RequestParam Map allParams) { 65 | MediaType contentType = MediaType.valueOf(file.getContentType()); 66 | 67 | StringBuilder sb = new StringBuilder(); 68 | sb.append("Параметры которые вы прислали: "+allParams.toString()); 69 | sb.append("\nИнформация по присланному файлу"); 70 | sb.append("\nИмя файла: "+file.getOriginalFilename()); 71 | sb.append("\nМедиа-тип файла: "+contentType.toString()); 72 | //file.getInputStream().readAllBytes().toString() 73 | 74 | Resource fileResource = new InputStreamResource(new ByteArrayInputStream(sb.toString().getBytes(StandardCharsets.UTF_8))); 75 | return ResponseEntity.ok() 76 | .contentType(MediaType.TEXT_MARKDOWN) 77 | .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + "answer.txt" + "\"") 78 | .body(fileResource); 79 | 80 | } 81 | 82 | @Operation(summary = "Ошибка-апи", description = "Демонстрирует обработку ошибок") 83 | @ApiResponses(value = { 84 | @ApiResponse(responseCode = "200", description = "успешно"), 85 | @ApiResponse(responseCode = "500", description = "внутренняя ошибка") 86 | }) 87 | @GetMapping(value = "/error") 88 | public AnswerDto error() { 89 | return new AnswerDto().setData(10/0); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Apps/WebApplication/src/main/java/ru/app/config/WebMvcConfig.java: -------------------------------------------------------------------------------- 1 | package ru.app.config; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.web.servlet.HandlerInterceptor; 8 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 9 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 10 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 11 | import org.springframework.web.util.ContentCachingResponseWrapper; 12 | 13 | import java.util.Enumeration; 14 | 15 | @Configuration 16 | public class WebMvcConfig implements WebMvcConfigurer { 17 | 18 | /**Разрешаем CORS для сваггера*/ 19 | @Override 20 | public void addCorsMappings(CorsRegistry registry) { 21 | registry.addMapping("/**"); 22 | //registry.addMapping("/api-docs"); //только для сваггера 23 | } 24 | 25 | /**Добавим свой перехватчик*/ 26 | @Override 27 | public void addInterceptors(InterceptorRegistry registry) { 28 | registry.addInterceptor(new LoggerInterceptor()); 29 | } 30 | } 31 | 32 | 33 | //https://www.baeldung.com/spring-mvc-handlerinterceptor 34 | /**Создадим собственный перехватчик для возможности логирования входных и выходных запросов и пропишем его*/ 35 | @Slf4j 36 | class LoggerInterceptor implements HandlerInterceptor { 37 | @Override 38 | /**Как следует из названия, перехватчик вызывает preHandle() перед обработкой запроса. 39 | По умолчанию этот метод возвращает true, чтобы отправить запрос дальше, к методу обработчика. Однако мы можем указать Spring остановить выполнение, вернув false. 40 | Мы можем использовать хук для записи информации о параметрах запроса, например о том, откуда пришёл запрос.*/ 41 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 42 | log.info("[preHandle][" + request + "]" + "[" + request.getMethod() + "]" + request.getRequestURI() + getParameters(request)); 43 | return true; 44 | } 45 | 46 | @Override 47 | /**Мы можем использовать этот метод для получения данных запроса и ответа после отображения представления*/ 48 | public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { 49 | if (ex != null){ 50 | ex.printStackTrace(); 51 | } 52 | 53 | log.info("[afterCompletion][" + request + "][exception: " + ex + "] => "+response); 54 | } 55 | 56 | 57 | private String getParameters(HttpServletRequest request) { 58 | StringBuffer posted = new StringBuffer(); 59 | Enumeration e = request.getParameterNames(); 60 | if (e != null) { 61 | posted.append("?"); 62 | } 63 | while (e.hasMoreElements()) { 64 | if (posted.length() > 1) { 65 | posted.append("&"); 66 | } 67 | String curr = (String) e.nextElement(); 68 | posted.append(curr + "="); 69 | if (curr.contains("password") 70 | || curr.contains("pass") 71 | || curr.contains("pwd")) { 72 | posted.append("*****"); 73 | } else { 74 | posted.append(request.getParameter(curr)); 75 | } 76 | } 77 | String ip = request.getHeader("X-FORWARDED-FOR"); 78 | String ipAddr = (ip == null) ? getRemoteAddr(request) : ip; 79 | if (ipAddr!=null && !ipAddr.equals("")) { 80 | posted.append("&_psip=" + ipAddr); 81 | } 82 | return posted.toString(); 83 | } 84 | 85 | private String getRemoteAddr(HttpServletRequest request) { 86 | String ipFromHeader = request.getHeader("X-FORWARDED-FOR"); 87 | if (ipFromHeader != null && ipFromHeader.length() > 0) { 88 | log.debug("ip from proxy - X-FORWARDED-FOR : " + ipFromHeader); 89 | return ipFromHeader; 90 | } 91 | return request.getRemoteAddr(); 92 | } 93 | } -------------------------------------------------------------------------------- /Apps/WebSecurityApplication/src/main/java/ru/app/config/SecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package ru.app.config; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.security.authentication.AuthenticationManager; 7 | import org.springframework.security.authentication.AuthenticationProvider; 8 | import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 9 | import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; 10 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 11 | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 12 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 13 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 14 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 15 | import org.springframework.security.core.userdetails.User; 16 | import org.springframework.security.core.userdetails.UserDetailsService; 17 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 18 | import org.springframework.security.crypto.password.PasswordEncoder; 19 | import org.springframework.security.web.SecurityFilterChain; 20 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 21 | import org.springframework.stereotype.Service; 22 | import org.springframework.web.cors.CorsConfiguration; 23 | import ru.app.api.auth.AuthorizationFilter; 24 | 25 | import java.util.List; 26 | 27 | import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS; 28 | 29 | @Configuration 30 | @EnableWebSecurity 31 | @EnableMethodSecurity(securedEnabled = true) //https://www.baeldung.com/spring-security-method-security 32 | @RequiredArgsConstructor 33 | public class SecurityConfiguration { 34 | private final AuthorizationFilter authorizationFilter; 35 | 36 | @Bean 37 | public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { 38 | http.csrf(AbstractHttpConfigurer::disable) 39 | // Своего рода отключение CORS (разрешение запросов со всех доменов) 40 | .cors(cors -> cors.configurationSource(request -> { 41 | var corsConfiguration = new CorsConfiguration(); 42 | corsConfiguration.setAllowedOriginPatterns(List.of("*")); 43 | corsConfiguration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); 44 | corsConfiguration.setAllowedHeaders(List.of("*")); 45 | corsConfiguration.setAllowCredentials(true); 46 | return corsConfiguration; 47 | })) 48 | // Настройка доступа к конечным точкам 49 | .authorizeHttpRequests(request -> request 50 | // Можно указать конкретный путь, * - 1 уровень вложенности, ** - любое количество уровней вложенности 51 | /*permitAll - Эндпоинт доступен всем пользователям, и авторизованным и нет 52 | authenticated - Только авторизованные пользователи 53 | hasRole - Пользователь должен иметь конкретную роль, и, соответственно быть авторизованным 54 | hasAnyRole - Должен иметь одну из перечисленных ролей (не представлено в коде)*/ 55 | .requestMatchers("/actuator/**").permitAll() 56 | .requestMatchers("/swagger-ui/**", "/swagger-resources/*", "/api-docs/**").permitAll() 57 | .requestMatchers("/app/v1/**").hasRole("WEB") 58 | .anyRequest().authenticated()) 59 | .sessionManagement(manager -> manager.sessionCreationPolicy(STATELESS)) 60 | //.authenticationProvider(authenticationProvider()) 61 | .addFilterBefore(authorizationFilter, UsernamePasswordAuthenticationFilter.class); 62 | return http.build(); 63 | } 64 | 65 | 66 | } 67 | 68 | -------------------------------------------------------------------------------- /Apps/WebSecurityApplication/src/main/java/ru/app/api/auth/AuthorizationFilter.java: -------------------------------------------------------------------------------- 1 | package ru.app.api.auth; 2 | 3 | import com.auth0.jwt.interfaces.DecodedJWT; 4 | import jakarta.servlet.FilterChain; 5 | import jakarta.servlet.ServletException; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.http.HttpHeaders; 11 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 12 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 13 | import org.springframework.security.core.context.SecurityContext; 14 | import org.springframework.security.core.context.SecurityContextHolder; 15 | import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; 16 | import org.springframework.stereotype.Component; 17 | import org.springframework.web.context.request.RequestContextHolder; 18 | import org.springframework.web.filter.OncePerRequestFilter; 19 | 20 | import java.io.IOException; 21 | import java.util.Arrays; 22 | import java.util.List; 23 | 24 | /**Фильтр проверяющий ацесс-токен для авторизации который надо получать у auth-service если в настройках включена авторизация*/ 25 | @Component 26 | @RequiredArgsConstructor 27 | public class AuthorizationFilter extends OncePerRequestFilter { 28 | 29 | private final TokenService tokenService; 30 | 31 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { 32 | /* не нужно проверять так как доступ к разрешённым ресурсам разерешён в конфигурации. 33 | В данном фильтре проверяем строго токен для доступа к ресурсам требующим авторизации 34 | if ( 35 | request.getRequestURI().contains("/public/") 36 | || request.getRequestURI().contains("/actuator") 37 | || request.getRequestURI().contains("/swagger-ui") 38 | || request.getRequestURI().contains("/api-docs") 39 | ) { 40 | filterChain.doFilter(request, response); 41 | return; 42 | }*/ 43 | 44 | String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); 45 | if (authHeader==null || authHeader.isEmpty() || !authHeader.startsWith("Bearer ")) { 46 | filterChain.doFilter(request, response); 47 | return; 48 | } 49 | 50 | if (authHeader == null || authHeader.isBlank()) 51 | response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 52 | else { 53 | DecodedJWT decodedJWT = checkAuthorization(authHeader); 54 | if (decodedJWT==null) 55 | response.setStatus(HttpServletResponse.SC_FORBIDDEN); 56 | else { 57 | //Авторизация прошла успешно 58 | //Создадим сприговский контекст безопасности 59 | //получать как SecurityContextHolder.getContext().getAuthentication() 60 | //Имя авторизованного пользователя как SecurityContextHolder.getContext().getAuthentication().getName() 61 | List roles = Arrays.stream(decodedJWT.getClaim("roles").asString().split(",")).toList().stream().map(String::trim).map(SimpleGrantedAuthority::new).toList(); 62 | 63 | SecurityContext context = SecurityContextHolder.createEmptyContext(); 64 | UsernamePasswordAuthenticationToken authToken =new UsernamePasswordAuthenticationToken(decodedJWT.getSubject(), null, roles); 65 | authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); 66 | context.setAuthentication(authToken); 67 | SecurityContextHolder.setContext(context); 68 | 69 | //продолжим обработку цепочки фильтров 70 | filterChain.doFilter(request, response); 71 | } 72 | } 73 | } 74 | 75 | private DecodedJWT checkAuthorization(String auth) { 76 | if (!auth.startsWith("Bearer ")) 77 | return null; 78 | 79 | String token = auth.substring(7); 80 | return tokenService.checkToken(token); 81 | } 82 | } 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /SpringCloudGateway/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | #web 2 | server: 3 | port: 80 4 | 5 | management: 6 | endpoint: 7 | health: 8 | show-details: always 9 | info: 10 | enabled: true 11 | shutdown: 12 | enabled: true #запрос на выклю приложения curl -i -X POST http://localhost:80/actuator/shutdown 13 | gateway: 14 | enabled: true 15 | endpoints: 16 | web: 17 | exposure: 18 | include: gateway, health, metrics, shutdown, info, myCustomEndpoint #http://localhost/actuator/gateway/routes 19 | 20 | 21 | #http://localhost/webjars/swagger-ui/index.html?urls.primaryName=gateway-service 22 | springdoc: 23 | enable-native-support: true 24 | api-docs: 25 | enabled: true 26 | path: /api-docs 27 | swagger-ui: 28 | enabled: true 29 | path: /swagger-ui 30 | urls: 31 | #Gateway(this) 32 | - name: gateway-service 33 | primaryName: API Gateway Service 34 | url: /api-docs 35 | #AuthService http://localhost/webjars/swagger-ui/index.html?urls.primaryName=auth-service 36 | - name: auth-service 37 | primaryName: API Auth Service 38 | url: http://localhost:8079/api-docs 39 | #WebApplication1 http://localhost/webjars/swagger-ui/index.html?urls.primaryName=web-app1-service 40 | - name: web-app1-service 41 | primaryName: API Application 'WebApplication1' 42 | url: http://localhost:8080/api-docs 43 | #WebSecurityApplication http://localhost/webjars/swagger-ui/index.html?urls.primaryName=web-sec-app-service 44 | - name: web-sec-app-service 45 | primaryName: API Application 'WebSecurityApplication' 46 | url: http://localhost:8081/api-docs 47 | #WebGenerateApplication http://localhost/webjars/swagger-ui/index.html?urls.primaryName=web-gen-app-service 48 | - name: web-gen-app-service 49 | primaryName: API Application 'WebGenerateApplication' 50 | url: http://localhost:8082/api-docs 51 | 52 | spring: 53 | boot: 54 | admin: 55 | client: 56 | url: http://localhost:8099 57 | application: 58 | name: Gateway 59 | cloud: 60 | gateway: 61 | httpclient: 62 | ssl: #доверять всем сертификатам 63 | useInsecureTrustManager: true 64 | routes: 65 | #AuthService - http://localhost:80/AuthService/login 66 | - id: AuthService-registration 67 | uri: http://localhost:8079 68 | predicates: 69 | - Path=/AuthService/** 70 | filters: 71 | - RewritePath=/AuthService, /auth 72 | 73 | #WebApplication1 74 | #запрашивать как http://localhost:80/WebApplication1/web/hello?name=rrrrr 75 | - id: WebApplication1-web 76 | uri: http://localhost:8080 #урл сервиса куда перенаправляем запрос 77 | predicates: #условие по которому запрос перенаправляется 78 | - Path=/WebApplication1/web/** 79 | filters: #как модифицируется запрос на пути туда и обратно 80 | - RewritePath=/WebApplication1/web, /app/v1 81 | 82 | #запрашивать как http://localhost:80/WebApplication1/db/1 83 | - id: WebApplication1-db 84 | uri: http://localhost:8080 85 | predicates: 86 | - Path=/WebApplication1/db/** 87 | filters: 88 | - RewritePath=/WebApplication1/db, /app/db/worker 89 | 90 | #WebSecurityApplication как http://localhost:80/WebSecurityApplication/web/hello1 91 | - id: WebSecurityApplication-web 92 | uri: http://localhost:8081 93 | predicates: 94 | - Path=/WebSecurityApplication/web/** 95 | filters: 96 | - RewritePath=/WebSecurityApplication/web, /app/v1 97 | 98 | #WebGenerateApplication 99 | - id: WebGenerateApplication-pets 100 | uri: http://localhost:8082 101 | predicates: 102 | - Path=/WebGenerateApplication/** 103 | filters: 104 | - RewritePath=/WebGenerateApplication, / 105 | 106 | #other 107 | - id: help 108 | uri: https://spring.io/guides 109 | predicates: 110 | - Path=/help 111 | filters: 112 | - RedirectTo=302, https://spring.io/guides 113 | -------------------------------------------------------------------------------- /AuthService/src/main/java/ru/auth/api/AuthController.java: -------------------------------------------------------------------------------- 1 | package ru.auth.api; 2 | 3 | import com.auth0.jwt.interfaces.DecodedJWT; 4 | import lombok.RequiredArgsConstructor; 5 | import org.mindrot.jbcrypt.BCrypt; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.*; 8 | import ru.auth.api.dto.AuthRequestDto; 9 | import ru.auth.api.dto.AuthResponseDto; 10 | import ru.auth.api.dto.ErrorResponseDto; 11 | import ru.auth.db.entity.ClientEntity; 12 | import ru.auth.db.repository.ClientRepository; 13 | import ru.auth.service.TokenService; 14 | 15 | import javax.security.auth.login.LoginException; 16 | import java.util.Optional; 17 | import java.util.HashMap; 18 | import java.util.Map; 19 | 20 | @RestController 21 | @RequestMapping("/auth") 22 | @RequiredArgsConstructor 23 | public class AuthController { 24 | 25 | private final ClientRepository userRepository; 26 | private final TokenService tokenService; 27 | private final Map refreshStorage = new HashMap<>(); //хранилище выданных рефреш-токенов. Для дополнительной проверки что мы действительно выдавали данный токен 28 | 29 | /**Сервис только для админов. Зарегистрировать нового клиента 30 | * @param clientId - клиент 31 | * @param clientSecret - пароль, 32 | * @param audience - система на которую даём права клиенту 33 | * @param roles - права которые даём*/ 34 | @PostMapping("/reg") 35 | public void register(String clientId, String clientSecret, String audience, String roles) throws LoginException { 36 | if(userRepository.findByClientIdAndAudience(clientId, audience).isPresent()) 37 | throw new LoginException("Client with id=" + clientId+" audience="+audience + " already registered"); 38 | 39 | String hash = BCrypt.hashpw(clientSecret, BCrypt.gensalt()); 40 | userRepository.save(new ClientEntity(null,clientId, roles, audience, hash)); 41 | } 42 | 43 | /**Логин 44 | * @param user - пользователь для которого хотим получить токены на доступ к заданной системе 45 | * @return - ацесс и рефреш токены*/ 46 | @PostMapping("/login") 47 | public AuthResponseDto login(@RequestBody AuthRequestDto user) throws LoginException{ 48 | checkCredentials(user.getClientId(), user.getClientSecret(), user.getAudience()); 49 | return generateNewAccessAndRefreshTokens(user.getClientId(), user.getAudience()); 50 | } 51 | 52 | /**Обновить асцесс токен по рефреш токену 53 | * @param refreshToken - рефреш токен 54 | * @return - новый ацесс токен*/ 55 | @PostMapping("/access") 56 | public AuthResponseDto getUpdateAccessToken(String refreshToken) throws LoginException{ 57 | if (tokenService.checkRefreshToken(refreshToken)){ 58 | DecodedJWT decodedJWT = tokenService.decodeRefreshToken(refreshToken); 59 | String clientId=decodedJWT.getSubject(); 60 | String audience=decodedJWT.getAudience().get(0); 61 | String savingRefreshToken = this.refreshStorage.get(clientId+"-"+audience); 62 | if (!refreshToken.equals(savingRefreshToken)) 63 | throw new LoginException("Refresh token not found"); 64 | 65 | ClientEntity user=userRepository.findByClientIdAndAudience(clientId, audience).get(); 66 | return new AuthResponseDto(tokenService.generateAccessToken(user.getClientId(), user.getAudience(), user.getRoles()), null); 67 | } 68 | return new AuthResponseDto(null, null); 69 | } 70 | /**Обновить ацесс и рефреш токен по рефреш токену 71 | * @param refreshToken - рефреш токен 72 | * @return - ацесс и рефреш токены*/ 73 | @PostMapping("/refresh") 74 | public AuthResponseDto getUpdateAccessAndRefreshToken(String refreshToken) throws LoginException{ 75 | if (tokenService.checkRefreshToken(refreshToken)){ 76 | DecodedJWT decodedJWT = tokenService.decodeRefreshToken(refreshToken); 77 | String clientId=decodedJWT.getSubject(); 78 | String audience=decodedJWT.getAudience().get(0); 79 | String savingRefreshToken = this.refreshStorage.get(clientId+"-"+audience); 80 | if (!refreshToken.equals(savingRefreshToken)) 81 | throw new LoginException("Refresh token not found"); 82 | 83 | return generateNewAccessAndRefreshTokens(clientId, audience); 84 | } 85 | return new AuthResponseDto(null, null); 86 | } 87 | 88 | @ExceptionHandler({LoginException.class}) 89 | public ResponseEntity handleUserRegistrationException(Exception ex) { 90 | return ResponseEntity 91 | .badRequest() 92 | .body(new ErrorResponseDto(ex.getMessage())); 93 | } 94 | 95 | /**Проверить пароль для данного пользователя и данной системы 96 | * @param clientId - клиент 97 | * @param clientSecret - пароль 98 | * @param audience - система на которую даём права клиенту*/ 99 | private void checkCredentials(String clientId, String clientSecret, String audience) throws LoginException { 100 | Optional optionalUserEntity = userRepository.findByClientIdAndAudience(clientId, audience); 101 | if (optionalUserEntity.isEmpty()) 102 | throw new LoginException("Client with id=" + clientId+" and audience="+audience + " not found"); 103 | 104 | ClientEntity clientEntity = optionalUserEntity.get(); 105 | if (!BCrypt.checkpw(clientSecret, clientEntity.getHash())) 106 | throw new LoginException("Secret is incorrect"); 107 | } 108 | 109 | /**Сгенерировать новые ацесс и рефреш токены*/ 110 | private AuthResponseDto generateNewAccessAndRefreshTokens(String clientId, String audience) { 111 | String roles=userRepository.findByClientIdAndAudience(clientId, audience).get().getRoles(); 112 | String access=tokenService.generateAccessToken(clientId, audience, roles); 113 | String refresh=tokenService.generateRefreshToken(clientId, audience); 114 | this.refreshStorage.put(clientId+"-"+ audience, refresh); 115 | return new AuthResponseDto(access, refresh); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Apps/WebApplication/src/main/java/ru/app/db/service/EntityService.java: -------------------------------------------------------------------------------- 1 | package ru.app.db.service; 2 | 3 | import jakarta.annotation.PostConstruct; 4 | import jakarta.persistence.EntityManager; 5 | import jakarta.persistence.PersistenceContext; 6 | import lombok.Getter; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.SneakyThrows; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.hibernate.Session; 11 | import org.hibernate.proxy.HibernateProxy; 12 | import org.springframework.beans.factory.annotation.Value; 13 | import org.springframework.stereotype.Service; 14 | import org.springframework.transaction.annotation.Transactional; 15 | import ru.app.db.entity.AbstractEntity; 16 | 17 | import java.util.List; 18 | 19 | /** 20 | * Сервис работы с сущностями в ДБ 21 | * 22 | * https://hibernate-refdoc.3141.ru/ch16.HQL?ysclid=m2lijbbewo31087775 23 | * https://docs.jboss.org/hibernate/core/3.3/reference/en/html/queryhql.html 24 | */ 25 | @Service 26 | @Slf4j 27 | @RequiredArgsConstructor 28 | @Getter 29 | public class EntityService { 30 | @PersistenceContext 31 | private EntityManager em; 32 | 33 | public static EntityService self; 34 | 35 | /**Истина - жёсткое удаления, ложь - мягкое удаление*/ 36 | @Value("${app.delete-is-true:false}") 37 | private boolean deleteIsTrue; 38 | 39 | @PostConstruct 40 | public void postConstructor(){ 41 | self=this; 42 | } 43 | 44 | /** 45 | * Получить ссылку на сущность по её уид-у (можно исп для проверки существования сущности) 46 | * 47 | * @param id - уид сущности 48 | * @param entityClass - класс искомой сущности 49 | * @return - сущность или null, если сущности не существует 50 | */ 51 | public A getReferenceForId(Class entityClass, Long id){ 52 | /*Hibernate не выполняет SQL-запрос, когда вы вызываете метод getReference. 53 | Если сущность не является управляемой, Hibernate инстанцирует прокси-объект и инициализирует атрибут первичного ключа 54 | 55 | Это очень похоже на неинициализированную ассоциацию с ленивой загрузкой, которая тоже дает вам прокси-объект. 56 | В обоих случаях устанавливаются только атрибуты первичного ключа. Как только вы попытаетесь получить доступ к первому же атрибуту, не являющемуся первичным ключем, Hibernate выполнит запрос к базе данных, чтобы получить все атрибуты. 57 | Это также первый раз, когда Hibernate проверит, существует ли указанный объект сущности. Если выполненный запрос не возвращает ожидаемого результата, Hibernate генерирует исключение 58 | * */ 59 | return em.getReference(entityClass, id); 60 | } 61 | 62 | /** 63 | * Получить сущность по её уид-у 64 | * 65 | * @param id - уид сущности 66 | * @param entityClass - класс искомой сущности 67 | * @return - сущность 68 | */ 69 | @SuppressWarnings("unchecked") 70 | @SneakyThrows 71 | public A findById(Class entityClass, Long id) { 72 | try { 73 | A e = (A) em.createQuery("from " + entityClass.getSimpleName() + " where id=" + id.toString()).getSingleResult(); 74 | return e; 75 | } catch (Exception ex) { 76 | log.error("No find entity for id"+id, ex); 77 | return null; 78 | } 79 | } 80 | 81 | /** 82 | * Получить набор сущностей по условию 83 | * 84 | * @param hql - запрос со всеми условиями 85 | * @param startPosition - с какой записи получать 86 | * @param maxResult - по какую запись получить 87 | * @param entityClass - класс искомой сущности 88 | * @return - сущность 89 | */ 90 | @SuppressWarnings("unchecked") 91 | @SneakyThrows 92 | public List findByAllFromConditionals(Class entityClass, String hql, Integer startPosition, Integer maxResult) { 93 | if (hql.toLowerCase().contains("insert") || hql.toLowerCase().contains("update")) 94 | throw new RuntimeException("bad sql request: "+hql); 95 | 96 | if (startPosition == null) startPosition = 0; 97 | if (maxResult == null) maxResult = Integer.MAX_VALUE; 98 | try { 99 | log.info(hql); 100 | List list = em.createQuery("select distinct e "+hql, entityClass).setFirstResult(startPosition).setMaxResults(maxResult).getResultList(); 101 | return list; 102 | } catch (Exception ex) { 103 | log.error("Error select for: " + hql, ex); 104 | throw ex; 105 | } 106 | } 107 | 108 | /** 109 | * Получить количество сущностей в выбранном наборе согласно условиям 110 | * 111 | * @param hql - запрос со всеми условиями 112 | * @return - общее количество выбранных сущностей 113 | */ 114 | @SuppressWarnings("unchecked") 115 | @SneakyThrows 116 | public Integer findByAllFromConditionalsCount(String hql) { 117 | if (hql.toLowerCase().contains("insert") || hql.toLowerCase().contains("update")) 118 | throw new RuntimeException("bad sql request: "+hql); 119 | 120 | int i=hql.indexOf("order"); 121 | if (i>0) 122 | hql=hql.substring(0, i); 123 | try { 124 | return Integer.parseInt(em.createQuery("select count(distinct e) " + hql).getSingleResult().toString()); 125 | } catch (Exception ex) { 126 | log.error("Error count select for: " + hql, ex); 127 | throw ex; 128 | } 129 | } 130 | 131 | 132 | /** 133 | * Сохранить сущность в бд 134 | * 135 | * @param entity - сохраняемая сущность 136 | * @return - сохранённую сущность (мб добавить ай-ди или другие поля) 137 | */ 138 | @SneakyThrows 139 | @Transactional 140 | public S save(S entity) { 141 | entity.beforeSave(); 142 | if (entity.getId() == null) em.persist(entity); 143 | else entity=em.merge(entity); 144 | 145 | return entity; 146 | } 147 | 148 | /**Удаляем запись. Удаление или настоящее, или псевдо-удаление в зависимости от настроек*/ 149 | @Transactional 150 | public void delete(AbstractEntity entity) { 151 | entity.beforeDelete(); 152 | 153 | //em.remove(em.contains(entity) ? entity : em.merge(entity));//delete entity 154 | 155 | if (deleteIsTrue) 156 | em.remove(em.contains(entity) ? entity : em.merge(entity));//hard delete entity 157 | else{//soft delete 158 | entity.setDeleted(true); 159 | //todo: надо добавить также удаление всех связанных сущностей согласно CascadeType 160 | /*также для реализации "мягкого" удаления можно использовать аннотации вида 161 | @SQLDelete(sql = "update Worker_Entity set deleted=true where id=?") но тогда имя таблицы приходится задавать как константу 162 | @SoftDelete(columnName = "deleted") но тогда нельзя использовать lazy-стратегии загрузки связанных сущностей*/ 163 | save(entity); 164 | } 165 | } 166 | 167 | /**Получить класс сущности по её текствому имени посредством поиска во всех сущностях 168 | * @param entityClassName - имя класса сущности 169 | * @return - класс сущности*/ 170 | public Class getEntityClassForName(String entityClassName){ 171 | var entityImpl =em.getMetamodel().getEntities().stream().filter(elem -> elem.getName().equals(entityClassName)).findFirst().orElseThrow(()-> new RuntimeException("no find entity class with class name is "+entityClassName)); 172 | return (Class) entityImpl.getJavaType(); 173 | } 174 | 175 | /** 176 | * Когда мы получаем вложенную сущность через ленивую загрузку, иногда приходится принудительно инициализировать её поля. Новый запрос в БД не идёт 177 | * 178 | * @param ae - потомок AbstractEntity, вложенная сущность полученная через ленивую загрузку 179 | * @return - та же сущность, но с инициализированными полями 180 | */ 181 | @SuppressWarnings("unchecked") 182 | public static R lazyInitializer(R ae) { 183 | if (ae != null) 184 | if (ae.id == null) 185 | if (ae.getId() != null) 186 | return (R) ((HibernateProxy) ae).getHibernateLazyInitializer().getImplementation(); //если по каким-то причинам поле не было извлечено, то извлечём его принудительно 187 | else return null; 188 | else return ae; 189 | return null; 190 | } 191 | 192 | } 193 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /Apps/WebGenerateApplication/src/main/resources/petclinic-spec.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.1 2 | info: 3 | title: Spring PetClinic 4 | description: Spring PetClinic Sample Application. 5 | license: 6 | name: Apache 2.0 7 | url: http://www.apache.org/licenses/LICENSE-2.0 8 | version: '1.0' 9 | servers: 10 | - url: http://localhost:9966/petclinic/api 11 | tags: 12 | - name: failing 13 | description: Endpoint which always returns an error. 14 | - name: owner 15 | description: Endpoints related to pet owners. 16 | - name: user 17 | description: Endpoints related to users. 18 | - name: pet 19 | description: Endpoints related to pets. 20 | - name: vet 21 | description: Endpoints related to vets. 22 | - name: visit 23 | description: Endpoints related to vet visits. 24 | - name: pettypes 25 | description: Endpoints related to pet types. 26 | - name: specialty 27 | description: Endpoints related to vet specialties. 28 | security: 29 | - BasicAuth: [] 30 | paths: 31 | /oops: 32 | get: 33 | tags: 34 | - failing 35 | operationId: failingRequest 36 | summary: Always fails 37 | description: Produces sample error response. 38 | responses: 39 | 200: 40 | description: Never returned. 41 | headers: 42 | ETag: 43 | description: An ID for this version of the response. 44 | schema: 45 | type: string 46 | content: 47 | text/plain: 48 | schema: 49 | type: string 50 | 304: 51 | description: Not modified. 52 | headers: 53 | ETag: 54 | description: An ID for this version of the response. 55 | schema: 56 | type: string 57 | 400: 58 | description: Bad request. 59 | content: 60 | application/json: 61 | schema: 62 | $ref: '#/components/schemas/RestError' 63 | /owners: 64 | post: 65 | tags: 66 | - owner 67 | operationId: addOwner 68 | summary: Adds a pet owner 69 | description: Records the details of a new pet owner. 70 | requestBody: 71 | description: The pet owner 72 | content: 73 | application/json: 74 | schema: 75 | $ref: '#/components/schemas/OwnerFields' 76 | required: true 77 | responses: 78 | 201: 79 | description: The pet owner was sucessfully added. 80 | content: 81 | application/json: 82 | schema: 83 | $ref: '#/components/schemas/Owner' 84 | 400: 85 | description: Bad request. 86 | content: 87 | application/json: 88 | schema: 89 | $ref: '#/components/schemas/RestError' 90 | 500: 91 | description: Server error. 92 | content: 93 | application/json: 94 | schema: 95 | $ref: '#/components/schemas/RestError' 96 | get: 97 | tags: 98 | - owner 99 | operationId: listOwners 100 | summary: Lists pet owners 101 | description: Returns an array of pet owners. 102 | parameters: 103 | - name: lastName 104 | in: query 105 | description: Last name. 106 | required: false 107 | schema: 108 | type: string 109 | example: Davis 110 | responses: 111 | 200: 112 | description: Owner details found and returned. 113 | headers: 114 | ETag: 115 | description: An ID for this version of the response. 116 | schema: 117 | type: string 118 | content: 119 | application/json: 120 | schema: 121 | type: array 122 | items: 123 | $ref: '#/components/schemas/Owner' 124 | 304: 125 | description: Not modified. 126 | headers: 127 | ETag: 128 | description: An ID for this version of the response. 129 | schema: 130 | type: string 131 | 500: 132 | description: Server error. 133 | content: 134 | application/json: 135 | schema: 136 | $ref: '#/components/schemas/RestError' 137 | /owners/{ownerId}: 138 | get: 139 | tags: 140 | - owner 141 | operationId: getOwner 142 | summary: Get a pet owner by ID 143 | description: Returns the pet owner or a 404 error. 144 | parameters: 145 | - name: ownerId 146 | in: path 147 | description: The ID of the pet owner. 148 | required: true 149 | schema: 150 | type: integer 151 | format: int32 152 | minimum: 0 153 | example: 1 154 | responses: 155 | 200: 156 | description: Owner details found and returned. 157 | headers: 158 | ETag: 159 | description: An ID for this version of the response. 160 | schema: 161 | type: string 162 | content: 163 | application/json: 164 | schema: 165 | $ref: '#/components/schemas/Owner' 166 | 304: 167 | description: Not modified. 168 | headers: 169 | ETag: 170 | description: An ID for this version of the response. 171 | schema: 172 | type: string 173 | 400: 174 | description: Bad request. 175 | content: 176 | application/json: 177 | schema: 178 | $ref: '#/components/schemas/RestError' 179 | 404: 180 | description: Owner not found. 181 | content: 182 | application/json: 183 | schema: 184 | $ref: '#/components/schemas/RestError' 185 | 500: 186 | description: Server error. 187 | content: 188 | application/json: 189 | schema: 190 | $ref: '#/components/schemas/RestError' 191 | put: 192 | tags: 193 | - owner 194 | operationId: updateOwner 195 | summary: Update a pet owner's details 196 | description: Updates the pet owner record with the specified details. 197 | parameters: 198 | - name: ownerId 199 | in: path 200 | description: The ID of the pet owner. 201 | required: true 202 | schema: 203 | type: integer 204 | format: int32 205 | minimum: 0 206 | example: 1 207 | requestBody: 208 | description: The pet owner details to use for the update. 209 | content: 210 | application/json: 211 | schema: 212 | $ref: '#/components/schemas/OwnerFields' 213 | required: true 214 | responses: 215 | 200: 216 | description: Update successful. 217 | content: 218 | application/json: 219 | schema: 220 | $ref: '#/components/schemas/Owner' 221 | 400: 222 | description: Bad request. 223 | content: 224 | application/json: 225 | schema: 226 | $ref: '#/components/schemas/RestError' 227 | 404: 228 | description: Owner not found. 229 | content: 230 | application/json: 231 | schema: 232 | $ref: '#/components/schemas/RestError' 233 | 500: 234 | description: Server error. 235 | content: 236 | application/json: 237 | schema: 238 | $ref: '#/components/schemas/RestError' 239 | 240 | delete: 241 | tags: 242 | - owner 243 | operationId: deleteOwner 244 | summary: Delete an owner by ID 245 | description: Returns the owner or a 404 error. 246 | parameters: 247 | - name: ownerId 248 | in: path 249 | description: The ID of the owner. 250 | required: true 251 | schema: 252 | type: integer 253 | format: int32 254 | minimum: 0 255 | example: 1 256 | responses: 257 | 200: 258 | description: Owner details found and returned. 259 | headers: 260 | ETag: 261 | description: An ID for this version of the response. 262 | schema: 263 | type: string 264 | content: 265 | application/json: 266 | schema: 267 | $ref: '#/components/schemas/Owner' 268 | 304: 269 | description: Not modified. 270 | headers: 271 | ETag: 272 | description: An ID for this version of the response. 273 | schema: 274 | type: string 275 | 400: 276 | description: Bad request. 277 | content: 278 | application/json: 279 | schema: 280 | $ref: '#/components/schemas/RestError' 281 | 404: 282 | description: Owner not found. 283 | content: 284 | application/json: 285 | schema: 286 | $ref: '#/components/schemas/RestError' 287 | 500: 288 | description: Server error. 289 | content: 290 | application/json: 291 | schema: 292 | $ref: '#/components/schemas/RestError' 293 | /owners/{ownerId}/pets: 294 | post: 295 | tags: 296 | - pet 297 | operationId: addPetToOwner 298 | summary: Adds a pet to an owner 299 | description: Records the details of a new pet. 300 | parameters: 301 | - name: ownerId 302 | in: path 303 | description: The ID of the pet owner. 304 | required: true 305 | schema: 306 | type: integer 307 | format: int32 308 | minimum: 0 309 | example: 1 310 | requestBody: 311 | description: The details of the new pet. 312 | content: 313 | application/json: 314 | schema: 315 | $ref: '#/components/schemas/PetFields' 316 | required: true 317 | responses: 318 | 201: 319 | description: The pet was sucessfully added. 320 | content: 321 | application/json: 322 | schema: 323 | $ref: '#/components/schemas/Pet' 324 | 400: 325 | description: Bad request. 326 | content: 327 | application/json: 328 | schema: 329 | $ref: '#/components/schemas/RestError' 330 | 404: 331 | description: Pet not found. 332 | content: 333 | application/json: 334 | schema: 335 | $ref: '#/components/schemas/RestError' 336 | 500: 337 | description: Server error. 338 | content: 339 | application/json: 340 | schema: 341 | $ref: '#/components/schemas/RestError' 342 | /owners/{ownerId}/pets/{petId}: 343 | get: 344 | tags: 345 | - pet 346 | operationId: getOwnersPet 347 | summary: Get a pet by ID 348 | description: Returns the pet or a 404 error. 349 | parameters: 350 | - name: ownerId 351 | in: path 352 | description: The ID of the pet owner. 353 | required: true 354 | schema: 355 | type: integer 356 | format: int32 357 | minimum: 0 358 | example: 1 359 | - name: petId 360 | in: path 361 | description: The ID of the pet. 362 | required: true 363 | schema: 364 | type: integer 365 | format: int32 366 | minimum: 0 367 | example: 1 368 | responses: 369 | 200: 370 | description: Pet details found and returned. 371 | headers: 372 | ETag: 373 | description: An ID for this version of the response. 374 | schema: 375 | type: string 376 | content: 377 | application/json: 378 | schema: 379 | $ref: '#/components/schemas/Pet' 380 | 304: 381 | description: Not modified. 382 | headers: 383 | ETag: 384 | description: An ID for this version of the response. 385 | schema: 386 | type: string 387 | 400: 388 | description: Bad request. 389 | content: 390 | application/json: 391 | schema: 392 | $ref: '#/components/schemas/RestError' 393 | 404: 394 | description: Pet not found. 395 | content: 396 | application/json: 397 | schema: 398 | $ref: '#/components/schemas/RestError' 399 | 500: 400 | description: Server error. 401 | content: 402 | application/json: 403 | schema: 404 | $ref: '#/components/schemas/RestError' 405 | put: 406 | tags: 407 | - pet 408 | operationId: updateOwnersPet 409 | 410 | summary: Update a pet's details 411 | description: Updates the pet record with the specified details. 412 | parameters: 413 | - name: ownerId 414 | in: path 415 | description: The ID of the pet owner. 416 | required: true 417 | schema: 418 | type: integer 419 | format: int32 420 | minimum: 0 421 | example: 1 422 | - name: petId 423 | in: path 424 | description: The ID of the pet. 425 | required: true 426 | schema: 427 | type: integer 428 | format: int32 429 | minimum: 0 430 | example: 1 431 | requestBody: 432 | description: The pet details to use for the update. 433 | content: 434 | application/json: 435 | schema: 436 | $ref: '#/components/schemas/PetFields' 437 | required: true 438 | responses: 439 | 204: 440 | description: Update successful. 441 | 400: 442 | description: Bad request. 443 | content: 444 | application/json: 445 | schema: 446 | $ref: '#/components/schemas/RestError' 447 | 404: 448 | description: Pet not found for this owner. 449 | content: 450 | application/json: 451 | schema: 452 | $ref: '#/components/schemas/RestError' 453 | 500: 454 | description: Server error. 455 | content: 456 | application/json: 457 | schema: 458 | $ref: '#/components/schemas/RestError' 459 | /owners/{ownerId}/pets/{petId}/visits: 460 | post: 461 | tags: 462 | - visit 463 | operationId: addVisitToOwner 464 | summary: Adds a vet visit 465 | description: Records the details of a new vet visit. 466 | parameters: 467 | - name: ownerId 468 | in: path 469 | description: The ID of the pet owner. 470 | required: true 471 | schema: 472 | type: integer 473 | format: int32 474 | minimum: 0 475 | example: 1 476 | - name: petId 477 | in: path 478 | description: The ID of the pet. 479 | required: true 480 | schema: 481 | type: integer 482 | format: int32 483 | minimum: 0 484 | example: 1 485 | requestBody: 486 | description: The details of the new vet visit. 487 | content: 488 | application/json: 489 | schema: 490 | $ref: '#/components/schemas/VisitFields' 491 | required: true 492 | responses: 493 | 201: 494 | description: The vet visit was sucessfully added. 495 | content: 496 | application/json: 497 | schema: 498 | $ref: '#/components/schemas/Visit' 499 | 400: 500 | description: Bad request. 501 | content: 502 | application/json: 503 | schema: 504 | $ref: '#/components/schemas/RestError' 505 | 404: 506 | description: Pet not found for this owner. 507 | content: 508 | application/json: 509 | schema: 510 | $ref: '#/components/schemas/RestError' 511 | 500: 512 | description: Server error. 513 | content: 514 | application/json: 515 | schema: 516 | $ref: '#/components/schemas/RestError' 517 | /pettypes: 518 | get: 519 | tags: 520 | - pettypes 521 | operationId: listPetTypes 522 | summary: Lists pet types 523 | description: Returns an array of pet types. 524 | responses: 525 | 200: 526 | description: Pet types found and returned. 527 | headers: 528 | ETag: 529 | description: An ID for this version of the response. 530 | schema: 531 | type: string 532 | content: 533 | application/json: 534 | schema: 535 | type: array 536 | items: 537 | $ref: '#/components/schemas/PetType' 538 | 304: 539 | description: Not modified. 540 | headers: 541 | ETag: 542 | description: An ID for this version of the response. 543 | schema: 544 | type: string 545 | 500: 546 | description: Server error. 547 | content: 548 | application/json: 549 | schema: 550 | $ref: '#/components/schemas/RestError' 551 | post: 552 | tags: 553 | - pettypes 554 | operationId: addPetType 555 | summary: Create a pet type 556 | description: Creates a pet type . 557 | requestBody: 558 | description: The pet type 559 | content: 560 | application/json: 561 | schema: 562 | $ref: '#/components/schemas/PetType' 563 | required: true 564 | responses: 565 | 200: 566 | description: Pet type created successfully. 567 | headers: 568 | ETag: 569 | description: An ID for this version of the response. 570 | schema: 571 | type: string 572 | content: 573 | application/json: 574 | schema: 575 | $ref: '#/components/schemas/PetType' 576 | 304: 577 | description: Not modified. 578 | headers: 579 | ETag: 580 | description: An ID for this version of the response. 581 | schema: 582 | type: string 583 | 400: 584 | description: Bad request. 585 | content: 586 | application/json: 587 | schema: 588 | $ref: '#/components/schemas/RestError' 589 | 404: 590 | description: Pet Type not found. 591 | content: 592 | application/json: 593 | schema: 594 | $ref: '#/components/schemas/RestError' 595 | 500: 596 | description: Server error. 597 | content: 598 | application/json: 599 | schema: 600 | $ref: '#/components/schemas/RestError' 601 | /pettypes/{petTypeId}: 602 | get: 603 | tags: 604 | - pettypes 605 | operationId: getPetType 606 | summary: Get a pet type by ID 607 | description: Returns the pet type or a 404 error. 608 | parameters: 609 | - name: petTypeId 610 | in: path 611 | description: The ID of the pet type. 612 | required: true 613 | schema: 614 | type: integer 615 | format: int32 616 | minimum: 0 617 | example: 1 618 | responses: 619 | 200: 620 | description: Pet type details found and returned. 621 | headers: 622 | ETag: 623 | description: An ID for this version of the response. 624 | schema: 625 | type: string 626 | content: 627 | application/json: 628 | schema: 629 | $ref: '#/components/schemas/PetType' 630 | 304: 631 | description: Not modified. 632 | headers: 633 | ETag: 634 | description: An ID for this version of the response. 635 | schema: 636 | type: string 637 | 400: 638 | description: Bad request. 639 | content: 640 | application/json: 641 | schema: 642 | $ref: '#/components/schemas/RestError' 643 | 404: 644 | description: Pet Type not found. 645 | content: 646 | application/json: 647 | schema: 648 | $ref: '#/components/schemas/RestError' 649 | 500: 650 | description: Server error. 651 | content: 652 | application/json: 653 | schema: 654 | $ref: '#/components/schemas/RestError' 655 | put: 656 | tags: 657 | - pettypes 658 | operationId: updatePetType 659 | summary: Update a pet type by ID 660 | description: Returns the pet type or a 404 error. 661 | parameters: 662 | - name: petTypeId 663 | in: path 664 | description: The ID of the pet type. 665 | required: true 666 | schema: 667 | type: integer 668 | format: int32 669 | minimum: 0 670 | example: 1 671 | requestBody: 672 | description: The pet type 673 | content: 674 | application/json: 675 | schema: 676 | $ref: '#/components/schemas/PetType' 677 | required: true 678 | responses: 679 | 200: 680 | description: Pet type details found and returned. 681 | headers: 682 | ETag: 683 | description: An ID for this version of the response. 684 | schema: 685 | type: string 686 | content: 687 | application/json: 688 | schema: 689 | $ref: '#/components/schemas/PetType' 690 | 304: 691 | description: Not modified. 692 | headers: 693 | ETag: 694 | description: An ID for this version of the response. 695 | schema: 696 | type: string 697 | 400: 698 | description: Bad request. 699 | content: 700 | application/json: 701 | schema: 702 | $ref: '#/components/schemas/RestError' 703 | 404: 704 | description: Pet Type not found. 705 | content: 706 | application/json: 707 | schema: 708 | $ref: '#/components/schemas/RestError' 709 | 500: 710 | description: Server error. 711 | content: 712 | application/json: 713 | schema: 714 | $ref: '#/components/schemas/RestError' 715 | delete: 716 | tags: 717 | - pettypes 718 | operationId: deletePetType 719 | summary: Delete a pet type by ID 720 | description: Returns the pet type or a 404 error. 721 | parameters: 722 | - name: petTypeId 723 | in: path 724 | description: The ID of the pet type. 725 | required: true 726 | schema: 727 | type: integer 728 | format: int32 729 | minimum: 0 730 | example: 1 731 | responses: 732 | 200: 733 | description: Pet type details found and returned. 734 | headers: 735 | ETag: 736 | description: An ID for this version of the response. 737 | schema: 738 | type: string 739 | content: 740 | application/json: 741 | schema: 742 | $ref: '#/components/schemas/PetType' 743 | 304: 744 | description: Not modified. 745 | headers: 746 | ETag: 747 | description: An ID for this version of the response. 748 | schema: 749 | type: string 750 | 400: 751 | description: Bad request. 752 | content: 753 | application/json: 754 | schema: 755 | $ref: '#/components/schemas/RestError' 756 | 404: 757 | description: Pet type not found. 758 | content: 759 | application/json: 760 | schema: 761 | $ref: '#/components/schemas/RestError' 762 | 500: 763 | description: Server error. 764 | content: 765 | application/json: 766 | schema: 767 | $ref: '#/components/schemas/RestError' 768 | 769 | /pets: 770 | get: 771 | tags: 772 | - pet 773 | operationId: listPets 774 | summary: Lists pet 775 | description: Returns an array of pet . 776 | responses: 777 | 200: 778 | description: Pet types found and returned. 779 | headers: 780 | ETag: 781 | description: An ID for this version of the response. 782 | schema: 783 | type: string 784 | content: 785 | application/json: 786 | schema: 787 | type: array 788 | items: 789 | $ref: '#/components/schemas/Pet' 790 | 304: 791 | description: Not modified. 792 | headers: 793 | ETag: 794 | description: An ID for this version of the response. 795 | schema: 796 | type: string 797 | 500: 798 | description: Server error. 799 | content: 800 | application/json: 801 | schema: 802 | $ref: '#/components/schemas/RestError' 803 | post: 804 | tags: 805 | - pet 806 | operationId: addPet 807 | summary: Create a pet 808 | description: Creates a pet . 809 | requestBody: 810 | description: The pet 811 | content: 812 | application/json: 813 | schema: 814 | $ref: '#/components/schemas/Pet' 815 | required: true 816 | responses: 817 | 200: 818 | description: Pet type created successfully. 819 | headers: 820 | ETag: 821 | description: An ID for this version of the response. 822 | schema: 823 | type: string 824 | content: 825 | application/json: 826 | schema: 827 | $ref: '#/components/schemas/Pet' 828 | 304: 829 | description: Not modified. 830 | headers: 831 | ETag: 832 | description: An ID for this version of the response. 833 | schema: 834 | type: string 835 | 400: 836 | description: Bad request. 837 | content: 838 | application/json: 839 | schema: 840 | $ref: '#/components/schemas/RestError' 841 | 404: 842 | description: Pet not found. 843 | content: 844 | application/json: 845 | schema: 846 | $ref: '#/components/schemas/RestError' 847 | 500: 848 | description: Server error. 849 | content: 850 | application/json: 851 | schema: 852 | $ref: '#/components/schemas/RestError' 853 | /pets/{petId}: 854 | get: 855 | tags: 856 | - pet 857 | operationId: getPet 858 | summary: Get a pet by ID 859 | description: Returns the pet or a 404 error. 860 | parameters: 861 | - name: petId 862 | in: path 863 | description: The ID of the pet. 864 | required: true 865 | schema: 866 | type: integer 867 | format: int32 868 | minimum: 0 869 | example: 1 870 | responses: 871 | 200: 872 | description: Pet details found and returned. 873 | headers: 874 | ETag: 875 | description: An ID for this version of the response. 876 | schema: 877 | type: string 878 | content: 879 | application/json: 880 | schema: 881 | $ref: '#/components/schemas/Pet' 882 | 304: 883 | description: Not modified. 884 | headers: 885 | ETag: 886 | description: An ID for this version of the response. 887 | schema: 888 | type: string 889 | 400: 890 | description: Bad request. 891 | content: 892 | application/json: 893 | schema: 894 | $ref: '#/components/schemas/RestError' 895 | 404: 896 | description: Pet not found. 897 | content: 898 | application/json: 899 | schema: 900 | $ref: '#/components/schemas/RestError' 901 | 500: 902 | description: Server error. 903 | content: 904 | application/json: 905 | schema: 906 | $ref: '#/components/schemas/RestError' 907 | put: 908 | tags: 909 | - pet 910 | operationId: updatePet 911 | summary: Update a pet by ID 912 | description: Returns the pet or a 404 error. 913 | parameters: 914 | - name: petId 915 | in: path 916 | description: The ID of the pet. 917 | required: true 918 | schema: 919 | type: integer 920 | format: int32 921 | minimum: 0 922 | example: 1 923 | requestBody: 924 | description: The pet 925 | content: 926 | application/json: 927 | schema: 928 | $ref: '#/components/schemas/Pet' 929 | required: true 930 | responses: 931 | 200: 932 | description: Pet details found and returned. 933 | headers: 934 | ETag: 935 | description: An ID for this version of the response. 936 | schema: 937 | type: string 938 | content: 939 | application/json: 940 | schema: 941 | $ref: '#/components/schemas/Pet' 942 | 304: 943 | description: Not modified. 944 | headers: 945 | ETag: 946 | description: An ID for this version of the response. 947 | schema: 948 | type: string 949 | 400: 950 | description: Bad request. 951 | content: 952 | application/json: 953 | schema: 954 | $ref: '#/components/schemas/RestError' 955 | 404: 956 | description: Pet not found. 957 | content: 958 | application/json: 959 | schema: 960 | $ref: '#/components/schemas/RestError' 961 | 500: 962 | description: Server error. 963 | content: 964 | application/json: 965 | schema: 966 | $ref: '#/components/schemas/RestError' 967 | delete: 968 | tags: 969 | - pet 970 | operationId: deletePet 971 | summary: Delete a pet by ID 972 | description: Returns the pet or a 404 error. 973 | parameters: 974 | - name: petId 975 | in: path 976 | description: The ID of the pet. 977 | required: true 978 | schema: 979 | type: integer 980 | format: int32 981 | minimum: 0 982 | example: 1 983 | responses: 984 | 200: 985 | description: Pet details found and returned. 986 | headers: 987 | ETag: 988 | description: An ID for this version of the response. 989 | schema: 990 | type: string 991 | content: 992 | application/json: 993 | schema: 994 | $ref: '#/components/schemas/Pet' 995 | 304: 996 | description: Not modified. 997 | headers: 998 | ETag: 999 | description: An ID for this version of the response. 1000 | schema: 1001 | type: string 1002 | 400: 1003 | description: Bad request. 1004 | content: 1005 | application/json: 1006 | schema: 1007 | $ref: '#/components/schemas/RestError' 1008 | 404: 1009 | description: Pet not found. 1010 | content: 1011 | application/json: 1012 | schema: 1013 | $ref: '#/components/schemas/RestError' 1014 | 500: 1015 | description: Server error. 1016 | content: 1017 | application/json: 1018 | schema: 1019 | $ref: '#/components/schemas/RestError' 1020 | /visits: 1021 | get: 1022 | tags: 1023 | - visit 1024 | operationId: listVisits 1025 | summary: Lists visits 1026 | description: Returns an array of visit . 1027 | responses: 1028 | 200: 1029 | description: visits found and returned. 1030 | headers: 1031 | ETag: 1032 | description: An ID for this version of the response. 1033 | schema: 1034 | type: string 1035 | content: 1036 | application/json: 1037 | schema: 1038 | type: array 1039 | items: 1040 | $ref: '#/components/schemas/Visit' 1041 | 304: 1042 | description: Not modified. 1043 | headers: 1044 | ETag: 1045 | description: An ID for this version of the response. 1046 | schema: 1047 | type: string 1048 | 500: 1049 | description: Server error. 1050 | content: 1051 | application/json: 1052 | schema: 1053 | $ref: '#/components/schemas/RestError' 1054 | post: 1055 | tags: 1056 | - visit 1057 | operationId: addVisit 1058 | summary: Create a visit 1059 | description: Creates a visit. 1060 | requestBody: 1061 | description: The visit 1062 | content: 1063 | application/json: 1064 | schema: 1065 | $ref: '#/components/schemas/Visit' 1066 | required: true 1067 | responses: 1068 | 200: 1069 | description: visit created successfully. 1070 | headers: 1071 | ETag: 1072 | description: An ID for this version of the response. 1073 | schema: 1074 | type: string 1075 | content: 1076 | application/json: 1077 | schema: 1078 | $ref: '#/components/schemas/Visit' 1079 | 304: 1080 | description: Not modified. 1081 | headers: 1082 | ETag: 1083 | description: An ID for this version of the response. 1084 | schema: 1085 | type: string 1086 | 400: 1087 | description: Bad request. 1088 | content: 1089 | application/json: 1090 | schema: 1091 | $ref: '#/components/schemas/RestError' 1092 | 404: 1093 | description: Visit not found. 1094 | content: 1095 | application/json: 1096 | schema: 1097 | $ref: '#/components/schemas/RestError' 1098 | 500: 1099 | description: Server error. 1100 | content: 1101 | application/json: 1102 | schema: 1103 | $ref: '#/components/schemas/RestError' 1104 | /visits/{visitId}: 1105 | get: 1106 | tags: 1107 | - visit 1108 | operationId: getVisit 1109 | summary: Get a visit by ID 1110 | description: Returns the visit or a 404 error. 1111 | parameters: 1112 | - name: visitId 1113 | in: path 1114 | description: The ID of the visit. 1115 | required: true 1116 | schema: 1117 | type: integer 1118 | format: int32 1119 | minimum: 0 1120 | example: 1 1121 | responses: 1122 | 200: 1123 | description: Visit details found and returned. 1124 | headers: 1125 | ETag: 1126 | description: An ID for this version of the response. 1127 | schema: 1128 | type: string 1129 | content: 1130 | application/json: 1131 | schema: 1132 | $ref: '#/components/schemas/Visit' 1133 | 304: 1134 | description: Not modified. 1135 | headers: 1136 | ETag: 1137 | description: An ID for this version of the response. 1138 | schema: 1139 | type: string 1140 | 400: 1141 | description: Bad request. 1142 | content: 1143 | application/json: 1144 | schema: 1145 | $ref: '#/components/schemas/RestError' 1146 | 404: 1147 | description: Visit not found. 1148 | content: 1149 | application/json: 1150 | schema: 1151 | $ref: '#/components/schemas/RestError' 1152 | 500: 1153 | description: Server error. 1154 | content: 1155 | application/json: 1156 | schema: 1157 | $ref: '#/components/schemas/RestError' 1158 | put: 1159 | tags: 1160 | - visit 1161 | operationId: updateVisit 1162 | summary: Update a visit by ID 1163 | description: Returns the visit or a 404 error. 1164 | parameters: 1165 | - name: visitId 1166 | in: path 1167 | description: The ID of the visit. 1168 | required: true 1169 | schema: 1170 | type: integer 1171 | format: int32 1172 | minimum: 0 1173 | example: 1 1174 | requestBody: 1175 | description: The visit 1176 | content: 1177 | application/json: 1178 | schema: 1179 | $ref: '#/components/schemas/Visit' 1180 | required: true 1181 | responses: 1182 | 200: 1183 | description: Visit details found and returned. 1184 | headers: 1185 | ETag: 1186 | description: An ID for this version of the response. 1187 | schema: 1188 | type: string 1189 | content: 1190 | application/json: 1191 | schema: 1192 | $ref: '#/components/schemas/Visit' 1193 | 304: 1194 | description: Not modified. 1195 | headers: 1196 | ETag: 1197 | description: An ID for this version of the response. 1198 | schema: 1199 | type: string 1200 | 400: 1201 | description: Bad request. 1202 | content: 1203 | application/json: 1204 | schema: 1205 | $ref: '#/components/schemas/RestError' 1206 | 404: 1207 | description: Visit not found. 1208 | content: 1209 | application/json: 1210 | schema: 1211 | $ref: '#/components/schemas/RestError' 1212 | 500: 1213 | description: Server error. 1214 | content: 1215 | application/json: 1216 | schema: 1217 | $ref: '#/components/schemas/RestError' 1218 | delete: 1219 | tags: 1220 | - visit 1221 | operationId: deleteVisit 1222 | summary: Delete a visit by ID 1223 | description: Returns the visit or a 404 error. 1224 | parameters: 1225 | - name: visitId 1226 | in: path 1227 | description: The ID of the visit. 1228 | required: true 1229 | schema: 1230 | type: integer 1231 | format: int32 1232 | minimum: 0 1233 | example: 1 1234 | responses: 1235 | 200: 1236 | description: Visit details found and returned. 1237 | headers: 1238 | ETag: 1239 | description: An ID for this version of the response. 1240 | schema: 1241 | type: string 1242 | content: 1243 | application/json: 1244 | schema: 1245 | $ref: '#/components/schemas/Visit' 1246 | 304: 1247 | description: Not modified. 1248 | headers: 1249 | ETag: 1250 | description: An ID for this version of the response. 1251 | schema: 1252 | type: string 1253 | 400: 1254 | description: Bad request. 1255 | content: 1256 | application/json: 1257 | schema: 1258 | $ref: '#/components/schemas/RestError' 1259 | 404: 1260 | description: Visit not found. 1261 | content: 1262 | application/json: 1263 | schema: 1264 | $ref: '#/components/schemas/RestError' 1265 | 500: 1266 | description: Server error. 1267 | content: 1268 | application/json: 1269 | schema: 1270 | $ref: '#/components/schemas/RestError' 1271 | /specialties: 1272 | get: 1273 | tags: 1274 | - specialty 1275 | operationId: listSpecialties 1276 | summary: Lists specialties 1277 | description: Returns an array of specialty . 1278 | responses: 1279 | 200: 1280 | description: Specialties found and returned. 1281 | headers: 1282 | ETag: 1283 | description: An ID for this version of the response. 1284 | schema: 1285 | type: string 1286 | content: 1287 | application/json: 1288 | schema: 1289 | type: array 1290 | items: 1291 | $ref: '#/components/schemas/Specialty' 1292 | 304: 1293 | description: Not modified. 1294 | headers: 1295 | ETag: 1296 | description: An ID for this version of the response. 1297 | schema: 1298 | type: string 1299 | 500: 1300 | description: Server error. 1301 | content: 1302 | application/json: 1303 | schema: 1304 | $ref: '#/components/schemas/RestError' 1305 | post: 1306 | tags: 1307 | - specialty 1308 | operationId: addSpecialty 1309 | summary: Create a specialty 1310 | description: Creates a specialty . 1311 | requestBody: 1312 | description: The specialty 1313 | content: 1314 | application/json: 1315 | schema: 1316 | $ref: '#/components/schemas/Specialty' 1317 | required: true 1318 | responses: 1319 | 200: 1320 | description: Specialty created successfully. 1321 | headers: 1322 | ETag: 1323 | description: An ID for this version of the response. 1324 | schema: 1325 | type: string 1326 | content: 1327 | application/json: 1328 | schema: 1329 | $ref: '#/components/schemas/Specialty' 1330 | 304: 1331 | description: Not modified. 1332 | headers: 1333 | ETag: 1334 | description: An ID for this version of the response. 1335 | schema: 1336 | type: string 1337 | 400: 1338 | description: Bad request. 1339 | content: 1340 | application/json: 1341 | schema: 1342 | $ref: '#/components/schemas/RestError' 1343 | 404: 1344 | description: Specialty not found. 1345 | content: 1346 | application/json: 1347 | schema: 1348 | $ref: '#/components/schemas/RestError' 1349 | 500: 1350 | description: Server error. 1351 | content: 1352 | application/json: 1353 | schema: 1354 | $ref: '#/components/schemas/RestError' 1355 | /specialties/{specialtyId}: 1356 | get: 1357 | tags: 1358 | - specialty 1359 | operationId: getSpecialty 1360 | summary: Get a specialty by ID 1361 | description: Returns the specialty or a 404 error. 1362 | parameters: 1363 | - name: specialtyId 1364 | in: path 1365 | description: The ID of the speciality. 1366 | required: true 1367 | schema: 1368 | type: integer 1369 | format: int32 1370 | minimum: 0 1371 | example: 1 1372 | responses: 1373 | 200: 1374 | description: Specialty details found and returned. 1375 | headers: 1376 | ETag: 1377 | description: An ID for this version of the response. 1378 | schema: 1379 | type: string 1380 | content: 1381 | application/json: 1382 | schema: 1383 | $ref: '#/components/schemas/Specialty' 1384 | 304: 1385 | description: Not modified. 1386 | headers: 1387 | ETag: 1388 | description: An ID for this version of the response. 1389 | schema: 1390 | type: string 1391 | 400: 1392 | description: Bad request. 1393 | content: 1394 | application/json: 1395 | schema: 1396 | $ref: '#/components/schemas/RestError' 1397 | 404: 1398 | description: Specialty not found. 1399 | content: 1400 | application/json: 1401 | schema: 1402 | $ref: '#/components/schemas/RestError' 1403 | 500: 1404 | description: Server error. 1405 | content: 1406 | application/json: 1407 | schema: 1408 | $ref: '#/components/schemas/RestError' 1409 | put: 1410 | tags: 1411 | - specialty 1412 | operationId: updateSpecialty 1413 | summary: Update a specialty by ID 1414 | description: Returns the specialty or a 404 error. 1415 | parameters: 1416 | - name: specialtyId 1417 | in: path 1418 | description: The ID of the specialty. 1419 | required: true 1420 | schema: 1421 | type: integer 1422 | format: int32 1423 | minimum: 0 1424 | example: 1 1425 | requestBody: 1426 | description: The pet 1427 | content: 1428 | application/json: 1429 | schema: 1430 | $ref: '#/components/schemas/Specialty' 1431 | required: true 1432 | responses: 1433 | 200: 1434 | description: Specialty details found and returned. 1435 | headers: 1436 | ETag: 1437 | description: An ID for this version of the response. 1438 | schema: 1439 | type: string 1440 | content: 1441 | application/json: 1442 | schema: 1443 | $ref: '#/components/schemas/Specialty' 1444 | 304: 1445 | description: Not modified. 1446 | headers: 1447 | ETag: 1448 | description: An ID for this version of the response. 1449 | schema: 1450 | type: string 1451 | 400: 1452 | description: Bad request. 1453 | content: 1454 | application/json: 1455 | schema: 1456 | $ref: '#/components/schemas/RestError' 1457 | 404: 1458 | description: Specialty not found. 1459 | content: 1460 | application/json: 1461 | schema: 1462 | $ref: '#/components/schemas/RestError' 1463 | 500: 1464 | description: Server error. 1465 | content: 1466 | application/json: 1467 | schema: 1468 | $ref: '#/components/schemas/RestError' 1469 | delete: 1470 | tags: 1471 | - specialty 1472 | operationId: deleteSpecialty 1473 | summary: Delete a specialty by ID 1474 | description: Returns the specialty or a 404 error. 1475 | parameters: 1476 | - name: specialtyId 1477 | in: path 1478 | description: The ID of the specialty. 1479 | required: true 1480 | schema: 1481 | type: integer 1482 | format: int32 1483 | minimum: 0 1484 | example: 1 1485 | responses: 1486 | 200: 1487 | description: Specialty details found and returned. 1488 | headers: 1489 | ETag: 1490 | description: An ID for this version of the response. 1491 | schema: 1492 | type: string 1493 | content: 1494 | application/json: 1495 | schema: 1496 | $ref: '#/components/schemas/Specialty' 1497 | 304: 1498 | description: Not modified. 1499 | headers: 1500 | ETag: 1501 | description: An ID for this version of the response. 1502 | schema: 1503 | type: string 1504 | 400: 1505 | description: Bad request. 1506 | content: 1507 | application/json: 1508 | schema: 1509 | $ref: '#/components/schemas/RestError' 1510 | 404: 1511 | description: Specialty not found. 1512 | content: 1513 | application/json: 1514 | schema: 1515 | $ref: '#/components/schemas/RestError' 1516 | 500: 1517 | description: Server error. 1518 | content: 1519 | application/json: 1520 | schema: 1521 | $ref: '#/components/schemas/RestError' 1522 | /vets: 1523 | get: 1524 | tags: 1525 | - vet 1526 | operationId: listVets 1527 | summary: Lists vets 1528 | description: Returns an array of vets. 1529 | responses: 1530 | 200: 1531 | description: Vets found and returned. 1532 | headers: 1533 | ETag: 1534 | description: An ID for this version of the response. 1535 | schema: 1536 | type: string 1537 | content: 1538 | application/json: 1539 | schema: 1540 | type: array 1541 | items: 1542 | $ref: '#/components/schemas/Vet' 1543 | 304: 1544 | description: Not modified. 1545 | headers: 1546 | ETag: 1547 | description: An ID for this version of the response. 1548 | schema: 1549 | type: string 1550 | 500: 1551 | description: Server error. 1552 | content: 1553 | application/json: 1554 | schema: 1555 | $ref: '#/components/schemas/RestError' 1556 | 1557 | post: 1558 | tags: 1559 | - vet 1560 | operationId: addVet 1561 | summary: Create a Vet 1562 | description: Creates a vet . 1563 | requestBody: 1564 | description: The vet 1565 | content: 1566 | application/json: 1567 | schema: 1568 | $ref: '#/components/schemas/Vet' 1569 | required: true 1570 | responses: 1571 | 200: 1572 | description: Vet created successfully. 1573 | headers: 1574 | ETag: 1575 | description: An ID for this version of the response. 1576 | schema: 1577 | type: string 1578 | content: 1579 | application/json: 1580 | schema: 1581 | $ref: '#/components/schemas/Vet' 1582 | 304: 1583 | description: Not modified. 1584 | headers: 1585 | ETag: 1586 | description: An ID for this version of the response. 1587 | schema: 1588 | type: string 1589 | 400: 1590 | description: Bad request. 1591 | content: 1592 | application/json: 1593 | schema: 1594 | $ref: '#/components/schemas/RestError' 1595 | 404: 1596 | description: Vet not found. 1597 | content: 1598 | application/json: 1599 | schema: 1600 | $ref: '#/components/schemas/RestError' 1601 | 500: 1602 | description: Server error. 1603 | content: 1604 | application/json: 1605 | schema: 1606 | $ref: '#/components/schemas/RestError' 1607 | /vets/{vetId}: 1608 | get: 1609 | tags: 1610 | - vet 1611 | operationId: getVet 1612 | summary: Get a vet by ID 1613 | description: Returns the vet or a 404 error. 1614 | parameters: 1615 | - name: vetId 1616 | in: path 1617 | description: The ID of the vet. 1618 | required: true 1619 | schema: 1620 | type: integer 1621 | format: int32 1622 | minimum: 0 1623 | example: 1 1624 | responses: 1625 | 200: 1626 | description: Vet details found and returned. 1627 | headers: 1628 | ETag: 1629 | description: An ID for this version of the response. 1630 | schema: 1631 | type: string 1632 | content: 1633 | application/json: 1634 | schema: 1635 | $ref: '#/components/schemas/Vet' 1636 | 304: 1637 | description: Not modified. 1638 | headers: 1639 | ETag: 1640 | description: An ID for this version of the response. 1641 | schema: 1642 | type: string 1643 | 400: 1644 | description: Bad request. 1645 | content: 1646 | application/json: 1647 | schema: 1648 | $ref: '#/components/schemas/RestError' 1649 | 404: 1650 | description: Vet not found. 1651 | content: 1652 | application/json: 1653 | schema: 1654 | $ref: '#/components/schemas/RestError' 1655 | 500: 1656 | description: Server error. 1657 | content: 1658 | application/json: 1659 | schema: 1660 | $ref: '#/components/schemas/RestError' 1661 | put: 1662 | tags: 1663 | - vet 1664 | operationId: updateVet 1665 | summary: Update a vet by ID 1666 | description: Returns the vet or a 404 error. 1667 | parameters: 1668 | - name: vetId 1669 | in: path 1670 | description: The ID of the vet. 1671 | required: true 1672 | schema: 1673 | type: integer 1674 | format: int32 1675 | minimum: 0 1676 | example: 1 1677 | requestBody: 1678 | description: The vet 1679 | content: 1680 | application/json: 1681 | schema: 1682 | $ref: '#/components/schemas/Vet' 1683 | required: true 1684 | responses: 1685 | 200: 1686 | description: Pet type details found and returned. 1687 | headers: 1688 | ETag: 1689 | description: An ID for this version of the response. 1690 | schema: 1691 | type: string 1692 | content: 1693 | application/json: 1694 | schema: 1695 | $ref: '#/components/schemas/Vet' 1696 | 304: 1697 | description: Not modified. 1698 | headers: 1699 | ETag: 1700 | description: An ID for this version of the response. 1701 | schema: 1702 | type: string 1703 | 400: 1704 | description: Bad request. 1705 | content: 1706 | application/json: 1707 | schema: 1708 | $ref: '#/components/schemas/RestError' 1709 | 404: 1710 | description: Vet not found. 1711 | content: 1712 | application/json: 1713 | schema: 1714 | $ref: '#/components/schemas/RestError' 1715 | 500: 1716 | description: Server error. 1717 | content: 1718 | application/json: 1719 | schema: 1720 | $ref: '#/components/schemas/RestError' 1721 | delete: 1722 | tags: 1723 | - vet 1724 | operationId: deleteVet 1725 | summary: Delete a vet by ID 1726 | description: Returns the vet or a 404 error. 1727 | parameters: 1728 | - name: vetId 1729 | in: path 1730 | description: The ID of the vet. 1731 | required: true 1732 | schema: 1733 | type: integer 1734 | format: int32 1735 | minimum: 0 1736 | example: 1 1737 | responses: 1738 | 200: 1739 | description: Vet details found and returned. 1740 | headers: 1741 | ETag: 1742 | description: An ID for this version of the response. 1743 | schema: 1744 | type: string 1745 | content: 1746 | application/json: 1747 | schema: 1748 | $ref: '#/components/schemas/Vet' 1749 | 304: 1750 | description: Not modified. 1751 | headers: 1752 | ETag: 1753 | description: An ID for this version of the response. 1754 | schema: 1755 | type: string 1756 | 400: 1757 | description: Bad request. 1758 | content: 1759 | application/json: 1760 | schema: 1761 | $ref: '#/components/schemas/RestError' 1762 | 404: 1763 | description: Vet not found. 1764 | content: 1765 | application/json: 1766 | schema: 1767 | $ref: '#/components/schemas/RestError' 1768 | 500: 1769 | description: Server error. 1770 | content: 1771 | application/json: 1772 | schema: 1773 | $ref: '#/components/schemas/RestError' 1774 | /users: 1775 | post: 1776 | tags: 1777 | - user 1778 | operationId: addUser 1779 | summary: Create a user 1780 | description: Creates a user. 1781 | requestBody: 1782 | description: The user 1783 | content: 1784 | application/json: 1785 | schema: 1786 | $ref: '#/components/schemas/User' 1787 | required: true 1788 | responses: 1789 | 200: 1790 | description: User created successfully. 1791 | headers: 1792 | ETag: 1793 | description: An ID for this version of the response. 1794 | schema: 1795 | type: string 1796 | content: 1797 | application/json: 1798 | schema: 1799 | $ref: '#/components/schemas/User' 1800 | 304: 1801 | description: Not modified. 1802 | headers: 1803 | ETag: 1804 | description: An ID for this version of the response. 1805 | schema: 1806 | type: string 1807 | 400: 1808 | description: Bad request. 1809 | content: 1810 | application/json: 1811 | schema: 1812 | $ref: '#/components/schemas/RestError' 1813 | 404: 1814 | description: User not found. 1815 | content: 1816 | application/json: 1817 | schema: 1818 | $ref: '#/components/schemas/RestError' 1819 | 500: 1820 | description: Server error. 1821 | content: 1822 | application/json: 1823 | schema: 1824 | $ref: '#/components/schemas/RestError' 1825 | components: 1826 | securitySchemes: 1827 | BasicAuth: 1828 | type: http 1829 | scheme: basic 1830 | schemas: 1831 | RestError: 1832 | title: REST Error 1833 | description: The schema for all error responses. 1834 | type: object 1835 | properties: 1836 | status: 1837 | title: Status 1838 | description: The HTTP status code. 1839 | type: integer 1840 | format: int32 1841 | example: 400 1842 | readOnly: true 1843 | error: 1844 | title: Error 1845 | description: The short error message. 1846 | type: string 1847 | example: Bad Request 1848 | readOnly: true 1849 | path: 1850 | title: Path 1851 | description: The path of the URL for this request. 1852 | type: string 1853 | format: uri 1854 | example: '/api/owners' 1855 | readOnly: true 1856 | timestamp: 1857 | title: Timestamp 1858 | description: The time the error occured. 1859 | type: string 1860 | format: date-time 1861 | example: '2019-08-21T21:41:46.158+0000' 1862 | readOnly: true 1863 | message: 1864 | title: Message 1865 | description: The long error message. 1866 | type: string 1867 | example: 'Request failed schema validation' 1868 | readOnly: true 1869 | schemaValidationErrors: 1870 | title: Schema validation errors 1871 | description: Validation errors against the OpenAPI schema. 1872 | type: array 1873 | items: 1874 | $ref: '#/components/schemas/ValidationMessage' 1875 | trace: 1876 | title: Trace 1877 | description: The stacktrace for this error. 1878 | type: string 1879 | example: 'com.atlassian.oai.validator.springmvc.InvalidRequestException: ...' 1880 | readOnly: true 1881 | required: 1882 | - status 1883 | - error 1884 | - path 1885 | - timestamp 1886 | - message 1887 | - schemaValidationErrors 1888 | ValidationMessage: 1889 | title: Validation message 1890 | description: Messages describing a validation error. 1891 | type: object 1892 | properties: 1893 | message: 1894 | title: Message 1895 | description: The valiation message. 1896 | type: string 1897 | example: "[Path '/lastName'] Instance type (null) does not match any allowed primitive type" 1898 | readOnly: true 1899 | required: 1900 | - message 1901 | additionalProperties: true 1902 | Specialty: 1903 | title: Specialty 1904 | description: Fields of specialty of vets. 1905 | type: object 1906 | properties: 1907 | id: 1908 | title: ID 1909 | description: The ID of the specialty. 1910 | type: integer 1911 | format: int32 1912 | minimum: 0 1913 | example: 1 1914 | readOnly: true 1915 | name: 1916 | title: Name 1917 | description: The name of the specialty. 1918 | type: string 1919 | maxLength: 80 1920 | minLength: 1 1921 | example: radiology 1922 | required: 1923 | - id 1924 | - name 1925 | OwnerFields: 1926 | title: Owner fields 1927 | description: Editable fields of a pet owner. 1928 | type: object 1929 | properties: 1930 | firstName: 1931 | title: First name 1932 | description: The first name of the pet owner. 1933 | type: string 1934 | minLength: 1 1935 | maxLength: 30 1936 | pattern: '^[a-zA-Z]*$' 1937 | example: George 1938 | lastName: 1939 | title: Last name 1940 | description: The last name of the pet owner. 1941 | type: string 1942 | minLength: 1 1943 | maxLength: 30 1944 | pattern: '^[a-zA-Z]*$' 1945 | example: Franklin 1946 | address: 1947 | title: Address 1948 | description: The postal address of the pet owner. 1949 | type: string 1950 | minLength: 1 1951 | maxLength: 255 1952 | example: '110 W. Liberty St.' 1953 | city: 1954 | title: City 1955 | description: The city of the pet owner. 1956 | type: string 1957 | minLength: 1 1958 | maxLength: 80 1959 | example: Madison 1960 | telephone: 1961 | title: Telephone number 1962 | description: The telephone number of the pet owner. 1963 | type: string 1964 | minLength: 1 1965 | maxLength: 20 1966 | pattern: '^[0-9]*$' 1967 | example: '6085551023' 1968 | required: 1969 | - firstName 1970 | - lastName 1971 | - address 1972 | - city 1973 | - telephone 1974 | Owner: 1975 | title: Owner 1976 | description: A pet owner. 1977 | allOf: 1978 | - $ref: '#/components/schemas/OwnerFields' 1979 | - type: object 1980 | properties: 1981 | id: 1982 | title: ID 1983 | description: The ID of the pet owner. 1984 | type: integer 1985 | format: int32 1986 | minimum: 0 1987 | example: 1 1988 | readOnly: true 1989 | pets: 1990 | title: Pets 1991 | description: The pets owned by this individual including any booked vet visits. 1992 | type: array 1993 | items: 1994 | $ref: '#/components/schemas/Pet' 1995 | readOnly: true 1996 | required: 1997 | - pets 1998 | PetFields: 1999 | title: Pet fields 2000 | description: Editable fields of a pet. 2001 | type: object 2002 | properties: 2003 | name: 2004 | title: Name 2005 | description: The name of the pet. 2006 | type: string 2007 | maxLength: 30 2008 | example: Leo 2009 | birthDate: 2010 | title: Birth date 2011 | description: The date of birth of the pet. 2012 | type: string 2013 | format: date 2014 | example: '2010-09-07' 2015 | type: 2016 | $ref: '#/components/schemas/PetType' 2017 | required: 2018 | - name 2019 | - birthDate 2020 | - type 2021 | Pet: 2022 | title: Pet 2023 | description: A pet. 2024 | allOf: 2025 | - $ref: '#/components/schemas/PetFields' 2026 | - type: object 2027 | properties: 2028 | id: 2029 | title: ID 2030 | description: The ID of the pet. 2031 | type: integer 2032 | format: int32 2033 | minimum: 0 2034 | example: 1 2035 | readOnly: true 2036 | ownerId: 2037 | title: Owner ID 2038 | description: The ID of the pet's owner. 2039 | type: integer 2040 | format: int32 2041 | minimum: 0 2042 | example: 1 2043 | readOnly: true 2044 | visits: 2045 | title: Visits 2046 | description: Vet visit bookings for this pet. 2047 | type: array 2048 | items: 2049 | $ref: '#/components/schemas/Visit' 2050 | readOnly: true 2051 | required: 2052 | - id 2053 | - type 2054 | - visits 2055 | VetFields: 2056 | title: VetFields 2057 | description: Editable fields of a veterinarian. 2058 | type: object 2059 | properties: 2060 | firstName: 2061 | title: First name 2062 | description: The first name of the vet. 2063 | type: string 2064 | minLength: 1 2065 | maxLength: 30 2066 | pattern: '^[a-zA-Z]*$' 2067 | example: 'James' 2068 | lastName: 2069 | title: Last name 2070 | description: The last name of the vet. 2071 | type: string 2072 | minLength: 1 2073 | maxLength: 30 2074 | pattern: '^[a-zA-Z]*$' 2075 | example: 'Carter' 2076 | specialties: 2077 | title: Specialties 2078 | description: The specialties of the vet. 2079 | type: array 2080 | items: 2081 | $ref: '#/components/schemas/Specialty' 2082 | required: 2083 | - firstName 2084 | - lastName 2085 | - specialties 2086 | Vet: 2087 | title: Vet 2088 | description: A veterinarian. 2089 | allOf: 2090 | - $ref: '#/components/schemas/VetFields' 2091 | - type: object 2092 | properties: 2093 | id: 2094 | title: ID 2095 | description: The ID of the vet. 2096 | type: integer 2097 | format: int32 2098 | minimum: 0 2099 | example: 1 2100 | readOnly: true 2101 | required: 2102 | - id 2103 | - firstName 2104 | - lastName 2105 | - specialties 2106 | VisitFields: 2107 | title: Visit fields 2108 | description: Editable fields of a vet visit. 2109 | type: object 2110 | properties: 2111 | date: 2112 | title: Date 2113 | description: The date of the visit. 2114 | type: string 2115 | format: date 2116 | example: '2013-01-01' 2117 | description: 2118 | title: Description 2119 | description: The description for the visit. 2120 | type: string 2121 | minLength: 1 2122 | maxLength: 255 2123 | example: 'rabies shot' 2124 | required: 2125 | - description 2126 | Visit: 2127 | title: Visit 2128 | description: A booking for a vet visit. 2129 | allOf: 2130 | - $ref: '#/components/schemas/VisitFields' 2131 | - type: object 2132 | properties: 2133 | id: 2134 | title: ID 2135 | description: The ID of the visit. 2136 | type: integer 2137 | format: int32 2138 | minimum: 0 2139 | example: 1 2140 | readOnly: true 2141 | petId: 2142 | title: Pet ID 2143 | description: The ID of the pet. 2144 | type: integer 2145 | format: int32 2146 | minimum: 0 2147 | example: 1 2148 | readOnly: true 2149 | required: 2150 | - id 2151 | PetTypeFields: 2152 | title: PetType fields 2153 | description: Editable fields of a pet type. 2154 | type: object 2155 | properties: 2156 | name: 2157 | title: Name 2158 | description: The name of the pet type. 2159 | type: string 2160 | maxLength: 80 2161 | minLength: 1 2162 | example: cat 2163 | required: 2164 | - name 2165 | PetType: 2166 | title: Pet type 2167 | description: A pet type. 2168 | allOf: 2169 | - $ref: '#/components/schemas/PetTypeFields' 2170 | - type: object 2171 | properties: 2172 | id: 2173 | title: ID 2174 | description: The ID of the pet type. 2175 | type: integer 2176 | format: int32 2177 | minimum: 0 2178 | example: 1 2179 | readOnly: true 2180 | required: 2181 | - id 2182 | User: 2183 | title: User 2184 | description: An user. 2185 | type: object 2186 | properties: 2187 | username: 2188 | title: username 2189 | description: The username 2190 | type: string 2191 | maxLength: 80 2192 | minLength: 1 2193 | example: john.doe 2194 | password: 2195 | title: Password 2196 | description: The password 2197 | type: string 2198 | maxLength: 80 2199 | minLength: 1 2200 | example: 1234abc 2201 | enabled: 2202 | title: enabled 2203 | description: Indicates if the user is enabled 2204 | type: boolean 2205 | example: true 2206 | roles: 2207 | title: Roles 2208 | description: The roles of an user 2209 | type: array 2210 | items: 2211 | $ref: '#/components/schemas/Role' 2212 | required: 2213 | - username 2214 | Role: 2215 | title: Role 2216 | description: A role. 2217 | type: object 2218 | properties: 2219 | name: 2220 | title: name 2221 | description: The role's name 2222 | type: string 2223 | maxLength: 80 2224 | minLength: 1 2225 | example: admin 2226 | required: 2227 | - name 2228 | --------------------------------------------------------------------------------