├── .github └── workflows │ └── main.yml ├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── Dockerfile ├── README.md ├── deploy.yaml ├── mvnw ├── mvnw.cmd ├── my_test_plan.jmx ├── pipeline-diagram.png ├── pom.xml ├── service.yaml └── src ├── main ├── java │ └── com │ │ └── medicalhourmanagement │ │ └── medicalhourmanagement │ │ ├── MedicalHourManagementApplication.java │ │ ├── configs │ │ ├── AppConfig.java │ │ ├── DatabaseInitializer.java │ │ ├── SecurityBeansConfig.java │ │ └── SecurityFilterConfig.java │ │ ├── controllers │ │ ├── AppointmentController.java │ │ ├── AuthenticationController.java │ │ ├── DoctorController.java │ │ ├── PatientController.java │ │ └── SpecialtyController.java │ │ ├── dtos │ │ ├── AppointmentDTO.java │ │ ├── DoctorDTO.java │ │ ├── PatientDTO.java │ │ ├── SpecialtyDTO.java │ │ ├── UserDTO.java │ │ ├── request │ │ │ ├── AuthenticationRequestDTO.java │ │ │ ├── ChangePasswordRequestDTO.java │ │ │ ├── RegisterRequestDTO.java │ │ │ └── RequestAppointmentDTO.java │ │ └── response │ │ │ └── AuthenticationResponseDTO.java │ │ ├── entities │ │ ├── Appointment.java │ │ ├── Doctor.java │ │ ├── Patient.java │ │ ├── Specialty.java │ │ ├── Token.java │ │ └── User.java │ │ ├── exceptions │ │ ├── controlleradvice │ │ │ └── GlobalExceptionHandler.java │ │ └── dtos │ │ │ ├── AppException.java │ │ │ ├── DuplicateKeyException.java │ │ │ ├── ExceptionDTO.java │ │ │ ├── ExpiredTokenException.java │ │ │ ├── InternalServerErrorException.java │ │ │ ├── InvalidTokenException.java │ │ │ ├── NotFoundException.java │ │ │ ├── RequestException.java │ │ │ └── UnauthorizedAppointmentException.java │ │ ├── repositories │ │ ├── AppointmentRepository.java │ │ ├── DoctorRepository.java │ │ ├── PatientRepository.java │ │ ├── SpecialtyRepository.java │ │ ├── TokenRepository.java │ │ └── UserRepository.java │ │ ├── security │ │ ├── filters │ │ │ └── JwtAuthFilter.java │ │ └── services │ │ │ ├── JwtService.java │ │ │ └── LogoutService.java │ │ ├── services │ │ ├── AppointmentService.java │ │ ├── AuthenticationService.java │ │ ├── DoctorService.java │ │ ├── PatientService.java │ │ ├── SpecialtyService.java │ │ └── impl │ │ │ ├── AppointmentServiceImpl.java │ │ │ ├── AuthenticationServiceImpl.java │ │ │ ├── DoctorServiceImpl.java │ │ │ ├── PatientServiceImpl.java │ │ │ └── SpecialtyServiceImpl.java │ │ └── utils │ │ ├── constants │ │ ├── AuthConstants.java │ │ ├── EndpointsConstants.java │ │ ├── ExceptionMessageConstants.java │ │ └── RoleConstants.java │ │ ├── constraints │ │ └── PasswordConstraint.java │ │ ├── enums │ │ ├── Permission.java │ │ ├── Role.java │ │ └── TokenType.java │ │ └── validators │ │ └── PasswordConstraintValidator.java └── resources │ ├── application-dev.properties │ ├── application-prod.properties │ └── application.properties └── test └── java └── com └── medicalhourmanagement └── medicalhourmanagement ├── AppointmentControllerTest.java ├── AuthenticationControllerTest.java ├── DoctorControllerTest.java ├── PatientControllerTest.java └── SpecialtyControllerTest.java /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: DevSecOps Pipeline 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | Build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | 13 | - name: Setup Java JDK para Maven 14 | uses: actions/setup-java@main 15 | with: 16 | java-version: '17' 17 | distribution: 'temurin' 18 | 19 | - name: Compilar Dependencias Maven 20 | run: mvn clean package -DskipTests 21 | 22 | - name: Test Unitarios y Reporte Cobertura 23 | run: mvn verify 24 | 25 | - name: Subir Reporte JaCoCo como Artefacto 26 | uses: actions/upload-artifact@v2 27 | with: 28 | name: jacoco-report 29 | path: target/site/jacoco/index.html 30 | 31 | - name: Setup Java JDK para SonarCloud 32 | uses: actions/setup-java@main 33 | with: 34 | java-version: '17' 35 | distribution: 'temurin' 36 | 37 | - name: Analisis Sonar Cloud 38 | run: | 39 | mvn clean verify org.sonarsource.scanner.maven:sonar-maven-plugin:3.11.0.3922:sonar \ 40 | -Dsonar.token=${{ secrets.SONAR_TOKEN }} \ 41 | -Dsonar.host.url=https://sonarcloud.io \ 42 | -Dsonar.organization=sebagq \ 43 | -Dsonar.projectKey=SebaGQ_api-rest-java 44 | 45 | - name: SonarQube Quality Gate check 46 | id: sonarqube-quality-gate-check 47 | uses: sonarsource/sonarqube-quality-gate-action@master 48 | env: 49 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 50 | SONAR_HOST_URL: https://sonarcloud.io 51 | with: 52 | scanMetadataReportFile: target/sonar/report-task.txt 53 | 54 | - name: Set up Docker 55 | uses: docker/setup-buildx-action@v3 56 | 57 | - name: Docker Login 58 | uses: docker/login-action@v3.1.0 59 | with: 60 | username: ${{ secrets.DOCKER_USERNAME }} 61 | password: ${{ secrets.DOCKER_PASSWORD }} 62 | 63 | - name: Docker Build 64 | run: docker build -t sebagq/medical-hour-management:latest . 65 | 66 | - name: Docker Push 67 | run: docker push sebagq/medical-hour-management:latest 68 | 69 | SCA: 70 | needs: Build 71 | runs-on: ubuntu-latest 72 | permissions: 73 | security-events: write 74 | actions: read 75 | contents: read 76 | 77 | steps: 78 | - name: Checkout 79 | uses: actions/checkout@v4 80 | 81 | - name: Setup Java JDK 82 | uses: actions/setup-java@v3 83 | with: 84 | java-version: '17' 85 | distribution: 'temurin' 86 | 87 | - name: Install Dependencies 88 | run: mvn install 89 | 90 | - name: Dependency Check 91 | id: dependency-check 92 | uses: dependency-check/Dependency-Check_Action@main 93 | env: 94 | JAVA_HOME: /opt/jdk 95 | with: 96 | project: '${{ github.event.repository.name }}' 97 | path: '.' 98 | format: 'SARIF' 99 | args: > 100 | --enableRetired 101 | 102 | - name: Upload SARIF report 103 | uses: github/codeql-action/upload-sarif@v3 104 | with: 105 | sarif_file: ${{github.workspace}}/reports/dependency-check-report.sarif 106 | category: dependency-check 107 | 108 | 109 | ImageAnalysis: 110 | needs: SCA 111 | runs-on: ubuntu-latest 112 | permissions: 113 | security-events: write 114 | actions: read 115 | contents: read 116 | steps: 117 | - name: Setup Docker 118 | uses: docker/setup-buildx-action@v3 119 | 120 | - name: Docker Login 121 | uses: docker/login-action@v3.1.0 122 | with: 123 | username: ${{ secrets.DOCKER_USERNAME }} 124 | password: ${{ secrets.DOCKER_PASSWORD }} 125 | 126 | - name: Docker Pull 127 | run: docker pull sebagq/medical-hour-management:latest 128 | 129 | - name: Trivy Scan 130 | uses: aquasecurity/trivy-action@master 131 | with: 132 | image-ref: 'sebagq/medical-hour-management:latest' 133 | format: 'sarif' 134 | output: 'trivy-results-docker.sarif' 135 | severity: 'CRITICAL,HIGH,MEDIUM' 136 | 137 | - name: Upload Trivy scan results to GitHub Security tab 138 | uses: github/codeql-action/upload-sarif@v3 139 | with: 140 | sarif_file: 'trivy-results-docker.sarif' 141 | category: dependency-check 142 | 143 | - name: Check Trivy Vulnerabilities 144 | id: check_trivy 145 | run: | 146 | CRITICAL_COUNT=$(jq '[ .runs[].results[] as $result | .runs[].tool.driver.rules[] as $rule | select($result.ruleId == $rule.id) | select($rule.properties.tags[]? == "CRITICAL") ] | length' trivy-results-docker.sarif) 147 | HIGH_COUNT=$(jq '[ .runs[].results[] as $result | .runs[].tool.driver.rules[] as $rule | select($result.ruleId == $rule.id) | select($rule.properties.tags[]? == "HIGH") ] | length' trivy-results-docker.sarif) 148 | MEDIUM_COUNT=$(jq '[ .runs[].results[] as $result | .runs[].tool.driver.rules[] as $rule | select($result.ruleId == $rule.id) | select($rule.properties.tags[]? == "MEDIUM") ] | length' trivy-results-docker.sarif) 149 | echo "Critical vulnerabilities: $CRITICAL_COUNT" 150 | echo "High vulnerabilities: $HIGH_COUNT" 151 | echo "Medium vulnerabilities: $MEDIUM_COUNT" 152 | if [ "$CRITICAL_COUNT" -gt 0 ] || [ "$HIGH_COUNT" -gt 0 ] || [ "$MEDIUM_COUNT" -gt 0 ]; then 153 | echo "Vulnerabilities found." 154 | else 155 | echo "No vulnerabilities found." 156 | fi 157 | 158 | DAST: 159 | needs: ImageAnalysis 160 | runs-on: self-hosted 161 | permissions: 162 | issues: write 163 | steps: 164 | - name: Checkout 165 | uses: actions/checkout@v4 166 | 167 | - name: Setup Docker 168 | uses: docker/setup-buildx-action@v3 169 | 170 | - name: Docker Login 171 | uses: docker/login-action@v3.1.0 172 | with: 173 | username: ${{ secrets.DOCKER_USERNAME }} 174 | password: ${{ secrets.DOCKER_PASSWORD }} 175 | 176 | - name: Docker Run 177 | run: | 178 | docker pull sebagq/medical-hour-management:latest 179 | docker run -d -p 8080:8080 --name medical-hour-app sebagq/medical-hour-management:latest 180 | 181 | - name: ZAP Scan 182 | uses: zaproxy/action-baseline@v0.12.0 183 | with: 184 | target: 'http://localhost:8080' 185 | fail_action: false 186 | 187 | Deploy: 188 | needs: DAST 189 | runs-on: self-hosted 190 | steps: 191 | - name: Checkout 192 | uses: actions/checkout@v4 193 | 194 | - name: Despliegue aplicación y servicio 195 | run: | 196 | kubectl apply -f deploy.yaml 197 | kubectl apply -f service.yaml 198 | 199 | Test: 200 | needs: Deploy 201 | runs-on: self-hosted 202 | steps: 203 | - name: Checkout 204 | uses: actions/checkout@v4 205 | 206 | - name: Descargar y descomprimir JMeter 207 | run: | 208 | Invoke-WebRequest -Uri https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-5.6.3.tgz -OutFile jmeter.tgz 209 | New-Item -Path "jmeter" -ItemType Directory 210 | tar -xf jmeter.tgz -C jmeter 211 | 212 | - name: Ejecutar pruebas de rendimiento con JMeter 213 | run: | 214 | jmeter/apache-jmeter-5.6.3/bin/jmeter -n -t my_test_plan.jmx -l test_results.jtl -e -o ./report/ 215 | 216 | - name: Subir Reportes de Rendimiento como Artefacto 217 | uses: actions/upload-artifact@v2 218 | with: 219 | name: jmeter-report 220 | path: report/ 221 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ IDEA 2 | .idea/ 3 | *.iws 4 | *.iml 5 | *.ipr 6 | out/ 7 | 8 | # IntelliJ IDEA log 9 | idea.log 10 | 11 | # Visual Studio Code 12 | .vscode/ 13 | 14 | # Visual Studio 15 | .vs/ 16 | *.csproj.patient 17 | *.suo 18 | *.patient 19 | *.userosscache 20 | *.sln.docstates 21 | 22 | # Compiled class files 23 | *.class 24 | 25 | # Log files 26 | *.log 27 | 28 | # Spring Boot specific files 29 | *.jar 30 | *.war 31 | 32 | # Others 33 | .DS_Store 34 | Thumbs.db 35 | 36 | # Generated by Maven 37 | target/ 38 | pom.xml.tag 39 | pom.xml.releaseBackup 40 | pom.xml.versionsBackup 41 | pom.xml.next 42 | release.properties 43 | dependency-reduced-pom.xml 44 | buildNumber.properties 45 | .mvn/timing.properties 46 | 47 | # Generated by Gradle 48 | .gradle 49 | build/ 50 | 51 | # Eclipse specific files 52 | .classpath 53 | .project 54 | .settings/ 55 | 56 | # Windows specific files 57 | Thumbs.db 58 | 59 | # Exclude the .git directory itself 60 | .git/ 61 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebaGQ/api-rest-java-jwt-devsecops/df1d344cdc31c5f3ebd5f193523d532eaef6cdcf/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.7/apache-maven-3.8.7-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Etapa de construcción 2 | FROM openjdk:17-slim as builder 3 | 4 | # Establecer el directorio de trabajo 5 | WORKDIR /workspace/app 6 | 7 | # Copiar el archivo pom.xml y descargar las dependencias 8 | COPY mvnw . 9 | COPY .mvn .mvn 10 | COPY pom.xml . 11 | RUN chmod +x mvnw 12 | RUN ./mvnw dependency:go-offline 13 | 14 | # Copiar el código fuente y compilar el jar 15 | COPY src src 16 | RUN ./mvnw package -DskipTests 17 | 18 | # Etapa de ejecución, crear la imagen final 19 | FROM openjdk:17-slim 20 | 21 | VOLUME /tmp 22 | 23 | # Copiar el jar creado con mvnw 24 | COPY --from=builder /workspace/app/target/medical-hour-management-0.0.1-SNAPSHOT.jar app.jar 25 | 26 | # Exponer puerto para permitir comunicación desde fuera 27 | EXPOSE 8080 28 | 29 | # Definir el punto de entrada. 30 | ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"] 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Proyecto de Gestión de Horas Médicas 2 | 3 | Este proyecto es una aplicación de gestión de horas médicas desarrollada con Java Spring Boot. Incluye un pipeline DevSecOps implementado con GitHub Actions, que automatiza el proceso de integración continua, entrega continua y seguridad continua. 4 | 5 | La lógica de negocio de la aplicación es simple, pero se implementa un conjunto de buenas prácticas de desarrollo de software que mejoran la calidad, mantenibilidad y seguridad del código. 6 | 7 | 8 | Aún se está trabajando sobre este repositorio. 9 | 10 | ## Estructura del Proyecto 11 | 12 | ``` 13 | medical-hour-management/ 14 | │ 15 | ├── .github/ 16 | │ └── workflows/ 17 | │ └── main.yml 18 | │ 19 | ├── src/ 20 | │ └── main/ 21 | │ ├── java/com/medicalhourmanagement/ 22 | │ │ ├── configs/ 23 | │ │ ├── controllers/ 24 | │ │ ├── dtos/ 25 | │ │ │ ├── request/ 26 | │ │ │ └── response/ 27 | │ │ ├── entities/ 28 | │ │ ├── exceptions/ 29 | │ │ ├── repositories/ 30 | │ │ ├── security/ 31 | │ │ │ ├── filter/ 32 | │ │ │ └── service/ 33 | │ │ ├── services/ 34 | │ │ │ └── impl/ 35 | │ │ ├── utils/ 36 | │ │ │ ├── constant/ 37 | │ │ │ ├── enums/ 38 | │ │ │ └── validator/ 39 | │ │ └── MedicalHourManagementApplication.java 40 | │ │ 41 | │ └── resources/ 42 | │ ├── application.properties 43 | │ ├── application-dev.properties 44 | │ ├── application-prod.properties 45 | │ 46 | ├── .gitignore 47 | ├── mvnw 48 | ├── mvnw.cmd 49 | ├── pom.xml 50 | └── README.md 51 | ``` 52 | 53 | ## Principales Componentes 54 | 55 | - `security/`: Contiene las clases relacionadas con la autenticación y seguridad. 56 | - `configs/`: Configuraciones generales de la aplicación. 57 | - `constants/`: Constantes utilizadas en todo el proyecto. 58 | - `controllers/`: Controladores REST que manejan las solicitudes HTTP. 59 | - `dtos/`: Objetos de Transferencia de Datos (DTOs) para la comunicación entre capas. 60 | - `entities/`: Entidades JPA que representan las tablas de la base de datos. 61 | - `enums/`: Enumeraciones utilizadas en el proyecto. 62 | - `exceptions/`: Manejo centralizado de excepciones. 63 | - `repositories/`: Interfaces de repositorio para el acceso a datos. 64 | - `services/`: Servicios que contienen la lógica de negocio. 65 | 66 | ## Pipeline DevSecOps 67 | 68 | El proyecto incluye un pipeline de DevSecOps implementado con GitHub Actions. Este pipeline automatiza varios aspectos del desarrollo, pruebas y despliegue, incluyendo: 69 | 70 | - Compilación y pruebas unitarias 71 | - Análisis de código estático (SAST) con SonarCloud 72 | - Análisis de composición de software (SCA) 73 | - Construcción y escaneo de imágenes Docker 74 | - Pruebas de seguridad de aplicaciones dinámicas (DAST) 75 | - Despliegue automatizado en un cluster de Kubernetes 76 | 77 | El pipeline está configurado en el archivo `.github/workflows/main.yml`. 78 | 79 | ### Diagrama del Pipeline 80 | 81 | ![Diagrama del Pipeline DevSecOps](pipeline-diagram.png) 82 | Este diagrama muestra el flujo completo del pipeline, desde que el desarrollador sube el código hasta el despliegue en producción, pasando por todas las etapas de seguridad y calidad. 83 | 84 | ## Detalles del Pipeline DevSecOps 85 | 86 | En aquellos pasos que consideran detención del pipeline se está bypasseando la detención para facilitar el desarrollo. 87 | 88 | ### SAST (Static Application Security Testing) 89 | - Utiliza SonarCloud para el análisis de código estático. 90 | - Se configuró un Quality Profile personalizado para detectar solo vulnerabilidades con un nivel de severidad específico. 91 | - El Quality Gate se configuró para detener el pipeline basado en estas vulnerabilidades. 92 | 93 | ### SCA (Software Composition Analysis) 94 | - Utiliza Dependency Check (action: dependency-check/Dependency-Check_Action@main). 95 | - Genera reportes en formato SARIF para su visualización en la pestaña de Seguridad de GitHub. 96 | - Configurado para detener el pipeline ante vulnerabilidades de severidad media o superior (CVSS >= 4). 97 | 98 | ### Análisis de Imagen 99 | - Utiliza Trivy para escanear las imágenes Docker. 100 | - Genera reportes en formato SARIF. 101 | - Se implementó un paso adicional para contar las vulnerabilidades y detener el pipeline si se encuentran vulnerabilidades Medias, Altas o Críticas. 102 | 103 | ### DAST (Dynamic Application Security Testing) 104 | - Utiliza OWASP ZAP (action: zaproxy/action-baseline@v0.12.0). 105 | - Genera reportes en formatos HTML, Markdown y JSON. 106 | - Los resultados se suben automáticamente a la pestaña de Issues del repositorio en GitHub. 107 | 108 | 109 | 110 | ## Buenas Prácticas Implementadas 111 | 112 | En este apartado se mencionan y detallan algunas de las buenas prácticas que he aprendido a lo largo de mi experiencia en desarrollo y que están implementadas en este proyecto. 113 | 114 | ### 1. Arquitectura en Capas 115 | 116 | Se separan las responsabilidades del sistema en capas distintas (como controladores, servicios, repositorios, etc). 117 | 118 | **Impacto:** Mejora la modularidad, facilita el mantenimiento y permite cambios en una capa sin afectar a las demás. 119 | 120 | **Ejemplo:** 121 | ```java 122 | @RestController 123 | @RequestMapping(EndpointsConstants.ENDPOINT_APPOINTMENTS) 124 | public class AppointmentController { 125 | private final AppointmentService appointmentService; 126 | // ... 127 | } 128 | 129 | @Service 130 | public class AppointmentServiceImpl implements AppointmentService { 131 | private final AppointmentRepository appointmentRepository; 132 | // ... 133 | } 134 | ``` 135 | 136 | ### 2. Implementación de Spring Security 137 | 138 | Se usó Spring Security para la implementación de autenticación/autorización basada en roles. 139 | 140 | **Impacto:** Protege el acceso a los distintos endpoints de la aplicación. 141 | 142 | **Ejemplo:** 143 | ```java 144 | @Configuration 145 | @EnableWebSecurity 146 | public class SecurityConfig { 147 | // ... 148 | @Bean 149 | public SecurityFilterChain securityFilterChain(HttpSecurity http) { 150 | // ... 151 | } 152 | } 153 | ``` 154 | 155 | ### 3. Implementación de Roles y Permisos 156 | 157 | Se definen roles y permisos para permitir un control de acceso granular a diferentes endpoints de la aplicación. 158 | 159 | **Impacto:** Mejora la seguridad al restringir el acceso basado en roles de usuario. 160 | 161 | **Ejemplo:** 162 | ```java 163 | public enum Role { 164 | USER(Collections.emptySet()), 165 | ADMIN(Set.of(ADMIN_READ, ADMIN_UPDATE, ADMIN_DELETE, ADMIN_CREATE)), 166 | // ... 167 | } 168 | ``` 169 | 170 | ### 4. Control de Acceso Basado en Roles a Nivel de Método 171 | 172 | Se usa para implementar seguridad en múltiples capas, si bien el securityFilterChain restringe a nivel de URL también se usa seguridad a nivel de método. 173 | 174 | **Impacto:** Mejora la seguridad del código. 175 | 176 | **Ejemplo:** 177 | ```java 178 | @PreAuthorize("hasRole('" + RoleConstants.ROLE_ADMIN + "')") 179 | public PatientDTO updatePatient(@NonNull final Long patientId, @NonNull final PatientDTO patientDTO) { 180 | // ... 181 | } 182 | ``` 183 | 184 | ### 5. Uso de DTOs (Data Transfer Objects) 185 | 186 | Para mover la información entre las distintas capas, en lugar de usar la entidad directamente se implementa un DTO, que es un objeto simple que se usa netamente para transferir datos. 187 | 188 | **Impacto:** Mejora la seguridad al no exponer cualquier información innecesaria. 189 | 190 | **Ejemplo:** 191 | ```java 192 | public class AppointmentDTO { 193 | private Long id; 194 | private DoctorDTO doctor; 195 | private PatientDTO patient; 196 | private LocalDateTime date; 197 | // ... 198 | } 199 | ``` 200 | 201 | ### 6. Validaciones de Entrada 202 | 203 | Se usa para asegurar que los datos de entrada cumplan con los requisitos de seguridad y negocio antes de ser procesados. 204 | 205 | **Impacto:** Previene errores y vulnerabilidades de seguridad, como por ejemplo inyecciones SQL. 206 | 207 | **Ejemplo:** 208 | ```java 209 | public class RegisterRequestDTO { 210 | @PasswordConstraint 211 | private String password; 212 | // ... 213 | } 214 | ``` 215 | 216 | ### 7. Manejo de Excepciones Centralizado 217 | 218 | Se usa un @RestControllerAdvice , que proporciona un punto central para manejar las excepciones. 219 | 220 | **Impacto:** Mejora la consistencia en las respuestas de error y facilita el mantenimiento. 221 | 222 | **Ejemplo:** 223 | ```java 224 | @RestControllerAdvice 225 | public class GlobalExceptionHandler { 226 | @ExceptionHandler(NotFoundException.class) 227 | public ResponseEntity handleNotFoundException(NotFoundException ex, WebRequest request) { 228 | // ... 229 | } 230 | // ... 231 | } 232 | ``` 233 | 234 | ### 8. Implementación de Interfaces para Servicios 235 | 236 | Se crean interfaces para los servicios, lo que mejora abstracción y facilita futuras pruebas unitarias. 237 | 238 | **Impacto:** Mejora la mantenibilidad y permite cambiar implementaciones fácilmente. 239 | 240 | **Ejemplo:** 241 | ```java 242 | public interface AppointmentService { 243 | List getAppointments(); 244 | // ... 245 | } 246 | 247 | @Service 248 | public class AppointmentServiceImpl implements AppointmentService { 249 | // ... 250 | } 251 | ``` 252 | 253 | ### 9. Uso de @Transactional 254 | 255 | Se usa para asegurar la integridad de los datos en operaciones que realizan modifican la base de datos. 256 | 257 | **Impacto:** Previene inconsistencias en los datos en caso de fallos durante las operaciones. 258 | 259 | **Ejemplo:** 260 | ```java 261 | @Transactional 262 | public AppointmentDTO createAppointment(@NonNull final RequestAppointmentDTO requestAppointmentDTO) { 263 | // ... 264 | } 265 | ``` 266 | 267 | ### 10. Logging Adecuado 268 | 269 | Se implementa logs en todas las funciones críticas. 270 | 271 | **Impacto:** Facilita la depuración y el monitoreo del sistema. 272 | 273 | **Ejemplo:** 274 | ```java 275 | private static final Logger LOGGER = LoggerFactory.getLogger(AppointmentServiceImpl.class); 276 | 277 | LOGGER.info("Creating appointment for patient {} with doctor {} at {}", patientId, doctorId, date); 278 | ``` 279 | 280 | En un ambiente productivo se debe ser cuidadoso con qué loggear, el logear excepciones sin normalizar o entregar atributos sensibles puede ser un regalo para cualquier atacante. 281 | 282 | ### 11. Configuración Externalizada con Variables de Entorno 283 | 284 | Permite cambiar la configuración sin necesidad de modificar directamente el código. 285 | 286 | **Impacto:** Mejora la seguridad y facilita el despliegue en diferentes entornos. 287 | 288 | **Ejemplo:** 289 | ```properties 290 | spring.datasource.url=${DATABASE_URL} 291 | spring.datasource.username=${DATABASE_USERNAME} 292 | spring.datasource.password=${DATABASE_PASSWORD} 293 | ``` 294 | 295 | ### 12. Uso de Lombok 296 | 297 | Es una libreria usada para reducir código repetitivo, propociona notaciones para getters, setters, constructores, etc. 298 | 299 | **Impacto:** Mejora la legibilidad del código, reduce la probabilidad de errores y superficie de ataque. 300 | 301 | **Ejemplo:** 302 | ```java 303 | @Data 304 | @Builder 305 | @AllArgsConstructor 306 | @NoArgsConstructor 307 | public class AuthenticationResponseDTO { 308 | private String accessToken; 309 | private String refreshToken; 310 | } 311 | ``` 312 | 313 | ### 13. Uso de ModelMapper 314 | 315 | Simplifica el mapeo entre objetos de diferentes capas. 316 | 317 | **Impacto:** Reduce el código repetitivo y mejora la mantenibilidad. 318 | 319 | **Ejemplo:** 320 | ```java 321 | private DoctorDTO convertToRest(Doctor doctor) { 322 | return mapper.map(doctor, DoctorDTO.class); 323 | } 324 | ``` 325 | 326 | ### 14. Uso de Constantes 327 | 328 | **Justificación:** Centraliza valores comunes y reduce errores por strings mal escritos. 329 | 330 | **Impacto:** Mejora la mantenibilidad y reduce errores. 331 | 332 | **Ejemplo:** 333 | ```java 334 | public class ExceptionMessageConstants { 335 | public static final String USER_NOT_FOUND_MSG = "User not found"; 336 | // ... 337 | } 338 | ``` 339 | 340 | ### 15. Uso de Enumeraciones 341 | 342 | Se usan enums para todos los valores que representan Tipos. 343 | 344 | **Impacto:** Reduce errores y mejora la legibilidad del código. 345 | 346 | **Ejemplo:** 347 | ```java 348 | public enum TokenType { 349 | BEARER 350 | } 351 | ``` 352 | 353 | ### 16. Uso de Swagger 354 | 355 | Se usa swagger para documentar de manera automatizada los endpoints del proyecto. 356 | 357 | **Impacto:** Facilita la documentación del proyecto. 358 | 359 | **Ejemplo:** 360 | ```xml 361 | 362 | org.springdoc 363 | springdoc-openapi-starter-webmvc-ui 364 | 2.1.0 365 | 366 | ``` 367 | 368 | ### 17. Aplicación de Principios de Clean Code 369 | 370 | Se implementan principios de Clean Code a lo largo del proyecto, especialmente en la capa de servicio. Esto incluye métodos con responsabilidad única, nombres descriptivos, y una estructura clara y lógica del código. 371 | 372 | **Impacto:** Mejora significativamente la legibilidad, mantenibilidad y escalabilidad del código. 373 | 374 | **Ejemplo:** 375 | 376 | ```java 377 | @Service 378 | @RequiredArgsConstructor 379 | public class AppointmentServiceImpl implements AppointmentService { 380 | 381 | private final DoctorService doctorService; 382 | private final PatientService patientService; 383 | private final AppointmentRepository appointmentRepository; 384 | 385 | @Override 386 | @Transactional 387 | public AppointmentDTO createAppointment(@NonNull final RequestAppointmentDTO request) { 388 | Patient patient = getPatient(request.getPatient()); 389 | Doctor doctor = getDoctor(request.getDoctor()); 390 | 391 | validateUserAuthorization(patient); 392 | validateAppointmentTime(request.getDate(), doctor.getId(), patient.getId()); 393 | 394 | Appointment appointment = buildAppointment(request.getDate(), doctor, patient); 395 | return saveAppointment(appointment); 396 | } 397 | 398 | private Patient getPatient(Long patientId) { 399 | return mapper.map(patientService.getPatientById(patientId), Patient.class); 400 | } 401 | 402 | private Doctor getDoctor(Long doctorId) { 403 | return mapper.map(doctorService.getDoctorById(doctorId), Doctor.class); 404 | } 405 | 406 | private void validateUserAuthorization(Patient patient) { 407 | // Lógica de validación 408 | } 409 | 410 | private void validateAppointmentTime(LocalDateTime date, Long doctorId, Long patientId) { 411 | // Lógica de validación de tiempo 412 | } 413 | 414 | private Appointment buildAppointment(LocalDateTime date, Doctor doctor, Patient patient) { 415 | // Construcción de la cita 416 | } 417 | 418 | private AppointmentDTO saveAppointment(Appointment appointment) { 419 | // Guardado y conversión a DTO 420 | } 421 | } 422 | ``` 423 | 424 | 425 | ## Pendiente por hacer: 426 | - Se están implementando pruebas de rendimiento con JMeter, eso está casi completo, aún falta implementar el manejo del token en las solicitudes. 427 | 428 | - Solucionar algunos problemas de seguridad identificados por las herramientas de seguridad. 429 | -------------------------------------------------------------------------------- /deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: medical-hour-management 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: medical-hour-management 10 | template: 11 | metadata: 12 | labels: 13 | app: medical-hour-management 14 | spec: 15 | containers: 16 | - name: medical-hour-management 17 | image: sebagq/medical-hour-management:latest 18 | ports: 19 | - containerPort: 8080 20 | resources: 21 | requests: 22 | cpu: 100m 23 | memory: 128Mi 24 | limits: 25 | cpu: 500m 26 | memory: 512Mi 27 | readinessProbe: 28 | httpGet: 29 | path: /actuator/health 30 | port: 8080 31 | initialDelaySeconds: 30 32 | periodSeconds: 10 33 | livenessProbe: 34 | httpGet: 35 | path: /actuator/health 36 | port: 8080 37 | initialDelaySeconds: 60 38 | periodSeconds: 15 -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /usr/local/etc/mavenrc ] ; then 40 | . /usr/local/etc/mavenrc 41 | fi 42 | 43 | if [ -f /etc/mavenrc ] ; then 44 | . /etc/mavenrc 45 | fi 46 | 47 | if [ -f "$HOME/.mavenrc" ] ; then 48 | . "$HOME/.mavenrc" 49 | fi 50 | 51 | fi 52 | 53 | # OS specific support. $var _must_ be set to either true or false. 54 | cygwin=false; 55 | darwin=false; 56 | mingw=false 57 | case "`uname`" in 58 | CYGWIN*) cygwin=true ;; 59 | MINGW*) mingw=true;; 60 | Darwin*) darwin=true 61 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 62 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 63 | if [ -z "$JAVA_HOME" ]; then 64 | if [ -x "/usr/libexec/java_home" ]; then 65 | export JAVA_HOME="`/usr/libexec/java_home`" 66 | else 67 | export JAVA_HOME="/Library/Java/Home" 68 | fi 69 | fi 70 | ;; 71 | esac 72 | 73 | if [ -z "$JAVA_HOME" ] ; then 74 | if [ -r /etc/gentoo-release ] ; then 75 | JAVA_HOME=`java-config --jre-home` 76 | fi 77 | fi 78 | 79 | if [ -z "$M2_HOME" ] ; then 80 | ## resolve links - $0 may be a link to maven's home 81 | PRG="$0" 82 | 83 | # need this for relative symlinks 84 | while [ -h "$PRG" ] ; do 85 | ls=`ls -ld "$PRG"` 86 | link=`expr "$ls" : '.*-> \(.*\)$'` 87 | if expr "$link" : '/.*' > /dev/null; then 88 | PRG="$link" 89 | else 90 | PRG="`dirname "$PRG"`/$link" 91 | fi 92 | done 93 | 94 | saveddir=`pwd` 95 | 96 | M2_HOME=`dirname "$PRG"`/.. 97 | 98 | # make it fully qualified 99 | M2_HOME=`cd "$M2_HOME" && pwd` 100 | 101 | cd "$saveddir" 102 | # echo Using m2 at $M2_HOME 103 | fi 104 | 105 | # For Cygwin, ensure paths are in UNIX format before anything is touched 106 | if $cygwin ; then 107 | [ -n "$M2_HOME" ] && 108 | M2_HOME=`cygpath --unix "$M2_HOME"` 109 | [ -n "$JAVA_HOME" ] && 110 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 111 | [ -n "$CLASSPATH" ] && 112 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 113 | fi 114 | 115 | # For Mingw, ensure paths are in UNIX format before anything is touched 116 | if $mingw ; then 117 | [ -n "$M2_HOME" ] && 118 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 119 | [ -n "$JAVA_HOME" ] && 120 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 121 | fi 122 | 123 | if [ -z "$JAVA_HOME" ]; then 124 | javaExecutable="`which javac`" 125 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 126 | # readlink(1) is not available as standard on Solaris 10. 127 | readLink=`which readlink` 128 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 129 | if $darwin ; then 130 | javaHome="`dirname \"$javaExecutable\"`" 131 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 132 | else 133 | javaExecutable="`readlink -f \"$javaExecutable\"`" 134 | fi 135 | javaHome="`dirname \"$javaExecutable\"`" 136 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 137 | JAVA_HOME="$javaHome" 138 | export JAVA_HOME 139 | fi 140 | fi 141 | fi 142 | 143 | if [ -z "$JAVACMD" ] ; then 144 | if [ -n "$JAVA_HOME" ] ; then 145 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 146 | # IBM's JDK on AIX uses strange locations for the executables 147 | JAVACMD="$JAVA_HOME/jre/sh/java" 148 | else 149 | JAVACMD="$JAVA_HOME/bin/java" 150 | fi 151 | else 152 | JAVACMD="`\\unset -f command; \\command -v java`" 153 | fi 154 | fi 155 | 156 | if [ ! -x "$JAVACMD" ] ; then 157 | echo "Error: JAVA_HOME is not defined correctly." >&2 158 | echo " We cannot execute $JAVACMD" >&2 159 | exit 1 160 | fi 161 | 162 | if [ -z "$JAVA_HOME" ] ; then 163 | echo "Warning: JAVA_HOME environment variable is not set." 164 | fi 165 | 166 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 167 | 168 | # traverses directory structure from process work directory to filesystem root 169 | # first directory with .mvn subdirectory is considered project base directory 170 | find_maven_basedir() { 171 | 172 | if [ -z "$1" ] 173 | then 174 | echo "Path not specified to find_maven_basedir" 175 | return 1 176 | fi 177 | 178 | basedir="$1" 179 | wdir="$1" 180 | while [ "$wdir" != '/' ] ; do 181 | if [ -d "$wdir"/.mvn ] ; then 182 | basedir=$wdir 183 | break 184 | fi 185 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 186 | if [ -d "${wdir}" ]; then 187 | wdir=`cd "$wdir/.."; pwd` 188 | fi 189 | # end of workaround 190 | done 191 | echo "${basedir}" 192 | } 193 | 194 | # concatenates all lines of a file 195 | concat_lines() { 196 | if [ -f "$1" ]; then 197 | echo "$(tr -s '\n' ' ' < "$1")" 198 | fi 199 | } 200 | 201 | BASE_DIR=`find_maven_basedir "$(pwd)"` 202 | if [ -z "$BASE_DIR" ]; then 203 | exit 1; 204 | fi 205 | 206 | ########################################################################################## 207 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 208 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 209 | ########################################################################################## 210 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Found .mvn/wrapper/maven-wrapper.jar" 213 | fi 214 | else 215 | if [ "$MVNW_VERBOSE" = true ]; then 216 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 217 | fi 218 | if [ -n "$MVNW_REPOURL" ]; then 219 | jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 220 | else 221 | jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 222 | fi 223 | while IFS="=" read key value; do 224 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 225 | esac 226 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 227 | if [ "$MVNW_VERBOSE" = true ]; then 228 | echo "Downloading from: $jarUrl" 229 | fi 230 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 231 | if $cygwin; then 232 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 233 | fi 234 | 235 | if command -v wget > /dev/null; then 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Found wget ... using wget" 238 | fi 239 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 240 | wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 241 | else 242 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 243 | fi 244 | elif command -v curl > /dev/null; then 245 | if [ "$MVNW_VERBOSE" = true ]; then 246 | echo "Found curl ... using curl" 247 | fi 248 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 249 | curl -o "$wrapperJarPath" "$jarUrl" -f 250 | else 251 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 252 | fi 253 | 254 | else 255 | if [ "$MVNW_VERBOSE" = true ]; then 256 | echo "Falling back to using Java to download" 257 | fi 258 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 259 | # For Cygwin, switch paths to Windows format before running javac 260 | if $cygwin; then 261 | javaClass=`cygpath --path --windows "$javaClass"` 262 | fi 263 | if [ -e "$javaClass" ]; then 264 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 265 | if [ "$MVNW_VERBOSE" = true ]; then 266 | echo " - Compiling MavenWrapperDownloader.java ..." 267 | fi 268 | # Compiling the Java class 269 | ("$JAVA_HOME/bin/javac" "$javaClass") 270 | fi 271 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 272 | # Running the downloader 273 | if [ "$MVNW_VERBOSE" = true ]; then 274 | echo " - Running MavenWrapperDownloader.java ..." 275 | fi 276 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 277 | fi 278 | fi 279 | fi 280 | fi 281 | ########################################################################################## 282 | # End of extension 283 | ########################################################################################## 284 | 285 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 286 | if [ "$MVNW_VERBOSE" = true ]; then 287 | echo $MAVEN_PROJECTBASEDIR 288 | fi 289 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 290 | 291 | # For Cygwin, switch paths to Windows format before running java 292 | if $cygwin; then 293 | [ -n "$M2_HOME" ] && 294 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 295 | [ -n "$JAVA_HOME" ] && 296 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 297 | [ -n "$CLASSPATH" ] && 298 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 299 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 300 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 301 | fi 302 | 303 | # Provide a "standardized" way to retrieve the CLI args that will 304 | # work with both Windows and non-Windows executions. 305 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 306 | export MAVEN_CMD_LINE_ARGS 307 | 308 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 309 | 310 | exec "$JAVACMD" \ 311 | $MAVEN_OPTS \ 312 | $MAVEN_DEBUG_OPTS \ 313 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 314 | "-Dmaven.home=${M2_HOME}" \ 315 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 316 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 317 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a patient defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 50 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 124 | 125 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% ^ 162 | %JVM_CONFIG_MAVEN_PROPS% ^ 163 | %MAVEN_OPTS% ^ 164 | %MAVEN_DEBUG_OPTS% ^ 165 | -classpath %WRAPPER_JAR% ^ 166 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 167 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 168 | if ERRORLEVEL 1 goto error 169 | goto end 170 | 171 | :error 172 | set ERROR_CODE=1 173 | 174 | :end 175 | @endlocal & set ERROR_CODE=%ERROR_CODE% 176 | 177 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 178 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 179 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 180 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 181 | :skipRcPost 182 | 183 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 184 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 185 | 186 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 187 | 188 | cmd /C exit /B %ERROR_CODE% 189 | -------------------------------------------------------------------------------- /my_test_plan.jmx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | email 9 | admin3@email.com 10 | = 11 | 12 | 13 | password 14 | admin12345 15 | = 16 | 17 | 18 | 19 | 20 | 21 | 22 | 1 23 | 1 24 | true 25 | continue 26 | 27 | 1 28 | false 29 | 30 | 31 | 32 | 33 | 34 | 35 | Content-Type 36 | application/json 37 | 38 | 39 | 40 | 41 | 42 | localhost 43 | 8080 44 | http 45 | /api/v1/auth/register 46 | true 47 | POST 48 | true 49 | false 50 | 51 | 52 | 53 | false 54 | 55 | {"firstname":"Admin","lastname":"User","email":"${email}","password":"${password}"} 56 | = 57 | true 58 | application/json 59 | 60 | 61 | 62 | 63 | 64 | 65 | localhost 66 | 8080 67 | http 68 | /api/v1/auth/authenticate 69 | true 70 | POST 71 | true 72 | false 73 | 74 | 75 | 76 | false 77 | 78 | {"email":"${email}","password":"${password}"} 79 | = 80 | true 81 | application/json 82 | 83 | 84 | 85 | 86 | 87 | 88 | token 89 | $.token 90 | 1 91 | NO_TOKEN 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | false 100 | 101 | 102 | 103 | 104 | 105 | 10 106 | 10 107 | true 108 | continue 109 | 110 | 1 111 | false 112 | 113 | 114 | 115 | 116 | 117 | 118 | Content-Type 119 | application/json 120 | 121 | 122 | Authorization 123 | Bearer ${__property(BEARER)} 124 | 125 | 126 | 127 | 128 | 129 | localhost 130 | 8080 131 | http 132 | /api/v1/appointments 133 | true 134 | GET 135 | true 136 | false 137 | 138 | 139 | 140 | 141 | 142 | 143 | localhost 144 | 8080 145 | http 146 | /api/v1/appointments 147 | true 148 | POST 149 | true 150 | false 151 | 152 | 153 | 154 | false 155 | 156 | { "patient": "John Doe", "time": "10:00", "date": "2023-11-05" } 157 | = 158 | true 159 | application/json 160 | 161 | 162 | 163 | 164 | 165 | 166 | localhost 167 | 8080 168 | http 169 | /api/v1/appointments/1 170 | true 171 | GET 172 | true 173 | false 174 | 175 | 176 | 177 | 178 | 179 | 180 | localhost 181 | 8080 182 | http 183 | /api/v1/appointments/1 184 | true 185 | DELETE 186 | true 187 | false 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | false 196 | 197 | saveConfig 198 | 199 | 200 | true 201 | true 202 | true 203 | 204 | true 205 | true 206 | true 207 | true 208 | false 209 | true 210 | true 211 | false 212 | false 213 | false 214 | true 215 | false 216 | false 217 | false 218 | true 219 | 0 220 | true 221 | true 222 | true 223 | true 224 | true 225 | true 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | -------------------------------------------------------------------------------- /pipeline-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebaGQ/api-rest-java-jwt-devsecops/df1d344cdc31c5f3ebd5f193523d532eaef6cdcf/pipeline-diagram.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.0.5 9 | 10 | 11 | com.medicalhourmanagement 12 | medical-hour-management 13 | 0.0.1-SNAPSHOT 14 | Medical Hour Management 15 | Aplicacion simple de manejo de horas medicas. 16 | 17 | 17 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-data-jpa 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-web 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-devtools 32 | runtime 33 | true 34 | 35 | 36 | 37 | org.projectlombok 38 | lombok 39 | true 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-starter-test 44 | test 45 | 46 | 47 | org.modelmapper 48 | modelmapper 49 | 3.1.1 50 | 51 | 52 | com.h2database 53 | h2 54 | runtime 55 | 56 | 57 | org.jetbrains 58 | annotations 59 | 24.0.1 60 | compile 61 | 62 | 63 | jakarta.validation 64 | jakarta.validation-api 65 | 66 | 67 | org.hibernate.validator 68 | hibernate-validator 69 | 70 | 71 | org.springdoc 72 | springdoc-openapi-starter-webmvc-ui 73 | 2.1.0 74 | 75 | 76 | 77 | 78 | 79 | org.springframework.boot 80 | spring-boot-starter-security 81 | 82 | 83 | 84 | 85 | io.jsonwebtoken 86 | jjwt-api 87 | 0.11.5 88 | 89 | 90 | io.jsonwebtoken 91 | jjwt-impl 92 | 0.11.5 93 | 94 | 95 | io.jsonwebtoken 96 | jjwt-jackson 97 | 0.11.5 98 | 99 | 100 | org.springframework.security 101 | spring-security-test 102 | test 103 | 104 | 105 | 106 | jakarta.servlet 107 | jakarta.servlet-api 108 | 6.0.0 109 | provided 110 | 111 | 112 | 113 | 114 | 115 | 116 | org.springframework.boot 117 | spring-boot-maven-plugin 118 | 119 | 120 | 121 | org.projectlombok 122 | lombok 123 | 124 | 125 | 126 | 127 | 128 | 129 | org.jacoco 130 | jacoco-maven-plugin 131 | 0.8.7 132 | 133 | 134 | default-prepare-agent 135 | 136 | prepare-agent 137 | 138 | 139 | 140 | default-report 141 | prepare-package 142 | 143 | report 144 | 145 | 146 | 147 | jacoco-check 148 | 149 | check 150 | 151 | 152 | 153 | 154 | BUNDLE 155 | 156 | 157 | LINE 158 | COVEREDRATIO 159 | 0.20 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | -------------------------------------------------------------------------------- /service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: medical-hour-management-service 5 | spec: 6 | type: NodePort 7 | ports: 8 | - port: 8080 9 | targetPort: 8080 10 | nodePort: 30000 11 | selector: 12 | app: medical-hour-management -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/MedicalHourManagementApplication.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class MedicalHourManagementApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(MedicalHourManagementApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/configs/AppConfig.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.configs; 2 | 3 | 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import org.modelmapper.ModelMapper; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | @Configuration 10 | public class AppConfig { 11 | 12 | @Bean 13 | public ModelMapper modelMapper(){ 14 | return new ModelMapper(); 15 | } 16 | 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/configs/DatabaseInitializer.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.configs; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.entities.Doctor; 4 | import com.medicalhourmanagement.medicalhourmanagement.entities.Patient; 5 | import com.medicalhourmanagement.medicalhourmanagement.utils.enums.Role; 6 | import com.medicalhourmanagement.medicalhourmanagement.repositories.DoctorRepository; 7 | import com.medicalhourmanagement.medicalhourmanagement.repositories.PatientRepository; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.boot.CommandLineRunner; 10 | import org.springframework.security.crypto.password.PasswordEncoder; 11 | import org.springframework.stereotype.Component; 12 | 13 | 14 | 15 | 16 | /* 17 | Esta clase tiene propositos de prueba, esto en producción es ilegalXD 18 | */ 19 | @Component 20 | @RequiredArgsConstructor 21 | public class DatabaseInitializer implements CommandLineRunner { 22 | 23 | private final DoctorRepository doctorRepository; 24 | private final PatientRepository patientRepository; 25 | private final PasswordEncoder passwordEncoder; 26 | 27 | @Override 28 | public void run(String... args) { 29 | // Inicializar doctores 30 | Doctor doctor = Doctor.builder() 31 | .firstName("Doctor John") 32 | .lastName("Doctor Doe") 33 | .email("admin@email.com") 34 | .password(passwordEncoder.encode("admin12345")) 35 | .role(Role.ADMIN) 36 | .build(); 37 | doctorRepository.save(doctor); 38 | 39 | 40 | // Inicializar pacientes 41 | 42 | Patient patient = Patient.builder() 43 | .firstName("Jane") 44 | .lastName("Doe") 45 | .email("doctor@email.com") 46 | .password(passwordEncoder.encode("user12345")) 47 | .rut("12345678-9") 48 | .phoneNumber("123456789") 49 | .address("123 Main St") 50 | .build(); 51 | patientRepository.save(patient); 52 | 53 | } 54 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/configs/SecurityBeansConfig.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.configs; 2 | 3 | 4 | import com.medicalhourmanagement.medicalhourmanagement.utils.constants.ExceptionMessageConstants; 5 | import com.medicalhourmanagement.medicalhourmanagement.repositories.PatientRepository; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.security.authentication.AuthenticationManager; 10 | import org.springframework.security.authentication.AuthenticationProvider; 11 | import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 12 | import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; 13 | import org.springframework.security.core.userdetails.UserDetailsService; 14 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 15 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 16 | import org.springframework.security.crypto.password.PasswordEncoder; 17 | import org.springframework.web.cors.CorsConfiguration; 18 | import org.springframework.web.cors.CorsConfigurationSource; 19 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 20 | 21 | import java.util.Arrays; 22 | import java.util.Collections; 23 | 24 | @Configuration 25 | @RequiredArgsConstructor 26 | public class SecurityBeansConfig { 27 | 28 | private final PatientRepository repository; 29 | 30 | @Bean 31 | public UserDetailsService userDetailsService() { 32 | return username -> repository.findByEmail(username) 33 | .orElseThrow(() -> new UsernameNotFoundException(ExceptionMessageConstants.USER_NOT_FOUND_MSG)); 34 | } 35 | @Bean 36 | public PasswordEncoder passwordEncoder() { 37 | return new BCryptPasswordEncoder(); 38 | } 39 | 40 | @Bean 41 | public AuthenticationProvider authenticationProvider() { 42 | DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); 43 | authProvider.setUserDetailsService(userDetailsService()); 44 | authProvider.setPasswordEncoder(passwordEncoder()); 45 | return authProvider; 46 | } 47 | 48 | @Bean 49 | public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { 50 | return config.getAuthenticationManager(); 51 | } 52 | 53 | @Bean 54 | public CorsConfigurationSource corsConfigurationSource() { 55 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); 56 | CorsConfiguration config = new CorsConfiguration(); 57 | config.setAllowedOriginPatterns(Collections.singletonList("*")); 58 | config.setAllowedMethods(Collections.singletonList("*")); 59 | config.setAllowCredentials(true); 60 | config.setAllowedHeaders(Arrays.asList("*")); 61 | config.setExposedHeaders(Arrays.asList("*")); 62 | config.setMaxAge(3600L); // 1 hora 63 | source.registerCorsConfiguration("/**", config); 64 | return source; 65 | } 66 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/configs/SecurityFilterConfig.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.configs; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.security.filters.JwtAuthFilter; 4 | import com.medicalhourmanagement.medicalhourmanagement.utils.constants.EndpointsConstants; 5 | import com.medicalhourmanagement.medicalhourmanagement.utils.constants.RoleConstants; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.security.authentication.AuthenticationProvider; 10 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 11 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 12 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 13 | import org.springframework.security.core.context.SecurityContextHolder; 14 | import org.springframework.security.web.SecurityFilterChain; 15 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 16 | import org.springframework.security.web.authentication.logout.LogoutHandler; 17 | import org.springframework.web.cors.CorsConfigurationSource; 18 | 19 | import static org.springframework.http.HttpMethod.*; 20 | import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS; 21 | 22 | @Configuration 23 | @EnableWebSecurity 24 | @RequiredArgsConstructor 25 | public class SecurityFilterConfig { 26 | 27 | private final JwtAuthFilter jwtAuthFilter; 28 | private final AuthenticationProvider authenticationProvider; 29 | private final LogoutHandler logoutHandler; 30 | private final CorsConfigurationSource corsConfigurationSource; 31 | 32 | private static final String[] WHITE_LIST_URL = { 33 | EndpointsConstants.ENDPOINT_AUTH_PATTERN, 34 | EndpointsConstants.ENDPOINT_ACTUATOR_PATTERN 35 | }; 36 | @Bean 37 | public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { 38 | http 39 | .cors(cors -> cors.configurationSource(corsConfigurationSource)) 40 | .csrf(AbstractHttpConfigurer::disable) 41 | .authorizeHttpRequests(req -> 42 | req 43 | .requestMatchers(WHITE_LIST_URL).permitAll() 44 | .requestMatchers(GET, EndpointsConstants.ENDPOINT_DOCTORS_PATTERN).authenticated() 45 | .requestMatchers(POST, EndpointsConstants.ENDPOINT_DOCTORS_PATTERN).hasAnyRole(RoleConstants.ROLE_ADMIN) 46 | .requestMatchers(PUT, EndpointsConstants.ENDPOINT_DOCTORS_PATTERN).hasAnyRole(RoleConstants.ROLE_ADMIN) 47 | .requestMatchers(DELETE, EndpointsConstants.ENDPOINT_DOCTORS_PATTERN).hasAnyRole(RoleConstants.ROLE_ADMIN) 48 | 49 | .requestMatchers(GET, EndpointsConstants.ENDPOINT_PATIENTS_PATTERN).hasAnyRole(RoleConstants.ROLE_ADMIN, RoleConstants.ROLE_MANAGER) 50 | .requestMatchers(POST, EndpointsConstants.ENDPOINT_PATIENTS_PATTERN).hasAnyRole(RoleConstants.ROLE_ADMIN) 51 | .requestMatchers(PUT, EndpointsConstants.ENDPOINT_PATIENTS_PATTERN).hasAnyRole(RoleConstants.ROLE_ADMIN) 52 | .requestMatchers(DELETE, EndpointsConstants.ENDPOINT_PATIENTS_PATTERN).hasAnyRole(RoleConstants.ROLE_ADMIN) 53 | 54 | .requestMatchers(GET, EndpointsConstants.ENDPOINT_APPOINTMENTS_PATTERN).authenticated() 55 | .requestMatchers(POST, EndpointsConstants.ENDPOINT_APPOINTMENTS_PATTERN).authenticated() 56 | .requestMatchers(PUT, EndpointsConstants.ENDPOINT_APPOINTMENTS_PATTERN).hasAnyRole(RoleConstants.ROLE_MANAGER, RoleConstants.ROLE_ADMIN) 57 | .requestMatchers(DELETE, EndpointsConstants.ENDPOINT_APPOINTMENTS_PATTERN).hasAnyRole(RoleConstants.ROLE_MANAGER, RoleConstants.ROLE_ADMIN) 58 | 59 | .requestMatchers(GET, EndpointsConstants.ENDPOINT_SPECIALTIES_PATTERN).authenticated() 60 | .requestMatchers(POST, EndpointsConstants.ENDPOINT_SPECIALTIES_PATTERN).hasAnyRole(RoleConstants.ROLE_ADMIN) 61 | 62 | .anyRequest().authenticated() 63 | ) 64 | .sessionManagement(session -> session.sessionCreationPolicy(STATELESS)) 65 | .authenticationProvider(authenticationProvider) 66 | .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) 67 | .logout(logout -> 68 | logout.logoutUrl(EndpointsConstants.ENDPOINT_LOGOUT) 69 | .addLogoutHandler(logoutHandler) 70 | .logoutSuccessHandler((request, response, authentication) -> SecurityContextHolder.clearContext()) 71 | ); 72 | return http.build(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/controllers/AppointmentController.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.controllers; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.utils.constants.EndpointsConstants; 4 | import com.medicalhourmanagement.medicalhourmanagement.dtos.AppointmentDTO; 5 | import com.medicalhourmanagement.medicalhourmanagement.dtos.request.RequestAppointmentDTO; 6 | import com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos.NotFoundException; 7 | import com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos.UnauthorizedAppointmentException; 8 | import com.medicalhourmanagement.medicalhourmanagement.services.AppointmentService; 9 | import jakarta.validation.Valid; 10 | import lombok.NonNull; 11 | import lombok.RequiredArgsConstructor; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.springframework.http.HttpStatus; 15 | import org.springframework.http.ResponseEntity; 16 | import org.springframework.web.bind.annotation.*; 17 | 18 | import java.util.List; 19 | 20 | @RestController 21 | @RequestMapping(EndpointsConstants.ENDPOINT_APPOINTMENTS) 22 | @RequiredArgsConstructor 23 | public class AppointmentController { 24 | 25 | private static final Logger LOGGER = LoggerFactory.getLogger(AppointmentController.class); 26 | private final AppointmentService appointmentService; 27 | 28 | @GetMapping 29 | public ResponseEntity> getAppointments() { 30 | LOGGER.info("Received request to get all appointments"); 31 | List appointments = appointmentService.getAppointments(); 32 | LOGGER.info("Returning {} appointments", appointments.size()); 33 | return ResponseEntity.ok(appointments); 34 | } 35 | 36 | @GetMapping("/{id}") 37 | public ResponseEntity getAppointmentById(@NonNull @PathVariable final Long id) { 38 | LOGGER.info("Received request to get appointment with ID: {}", id); 39 | try { 40 | AppointmentDTO appointment = appointmentService.getAppointmentById(id); 41 | LOGGER.info("Returning appointment with ID: {}", id); 42 | return ResponseEntity.ok(appointment); 43 | } catch (NotFoundException e) { 44 | LOGGER.warn("Appointment not found with ID: {}", id); 45 | return ResponseEntity.notFound().build(); 46 | } 47 | } 48 | 49 | @PostMapping 50 | public ResponseEntity createAppointment(@NonNull @Valid @RequestBody final RequestAppointmentDTO requestAppointmentDTO) { 51 | LOGGER.info("Received request to create a new appointment"); 52 | try { 53 | AppointmentDTO createdAppointment = appointmentService.createAppointment(requestAppointmentDTO); 54 | LOGGER.info("Created new appointment with ID: {}", createdAppointment.getId()); 55 | return ResponseEntity.status(HttpStatus.CREATED).body(createdAppointment); 56 | } catch (UnauthorizedAppointmentException e) { 57 | LOGGER.warn("Unauthorized appointment creation: {}", e.getMessage()); 58 | return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); 59 | } catch (IllegalArgumentException e) { 60 | LOGGER.warn("Invalid appointment data: {}", e.getMessage()); 61 | return ResponseEntity.badRequest().build(); 62 | } 63 | } 64 | 65 | @PutMapping(value = "/{id}") 66 | public ResponseEntity updateAppointment(@NonNull @PathVariable final Long id, @NonNull @Valid @RequestBody final AppointmentDTO appointmentRequest) { 67 | LOGGER.info("Received request to update appointment with ID: {}", id); 68 | try { 69 | AppointmentDTO updatedAppointment = appointmentService.updateAppointment(id, appointmentRequest); 70 | LOGGER.info("Updated appointment with ID: {}", id); 71 | return ResponseEntity.ok(updatedAppointment); 72 | } catch (NotFoundException e) { 73 | LOGGER.warn("Appointment not found with ID: {}", id); 74 | return ResponseEntity.notFound().build(); 75 | } catch (UnauthorizedAppointmentException e) { 76 | LOGGER.warn("Unauthorized appointment update: {}", e.getMessage()); 77 | return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); 78 | } 79 | } 80 | 81 | @DeleteMapping(value = "/{id}") 82 | public ResponseEntity deleteAppointment(@NonNull @PathVariable final Long id) { 83 | LOGGER.info("Received request to delete appointment with ID: {}", id); 84 | try { 85 | appointmentService.deleteAppointmentById(id); 86 | LOGGER.info("Deleted appointment with ID: {}", id); 87 | return ResponseEntity.noContent().build(); 88 | } catch (NotFoundException e) { 89 | LOGGER.warn("Appointment not found with ID: {}", id); 90 | return ResponseEntity.notFound().build(); 91 | } catch (UnauthorizedAppointmentException e) { 92 | LOGGER.warn("Unauthorized appointment deletion: {}", e.getMessage()); 93 | return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/controllers/AuthenticationController.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.controllers; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.utils.constants.EndpointsConstants; 4 | import com.medicalhourmanagement.medicalhourmanagement.dtos.request.AuthenticationRequestDTO; 5 | import com.medicalhourmanagement.medicalhourmanagement.dtos.response.AuthenticationResponseDTO; 6 | import com.medicalhourmanagement.medicalhourmanagement.services.impl.AuthenticationServiceImpl; 7 | import com.medicalhourmanagement.medicalhourmanagement.dtos.request.RegisterRequestDTO; 8 | import jakarta.servlet.http.HttpServletRequest; 9 | import jakarta.servlet.http.HttpServletResponse; 10 | import jakarta.validation.Valid; 11 | import lombok.RequiredArgsConstructor; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.springframework.http.ResponseEntity; 15 | import org.springframework.web.bind.annotation.PostMapping; 16 | import org.springframework.web.bind.annotation.RequestBody; 17 | import org.springframework.web.bind.annotation.RequestMapping; 18 | import org.springframework.web.bind.annotation.RestController; 19 | 20 | import java.io.IOException; 21 | 22 | @RestController 23 | @RequestMapping(EndpointsConstants.ENDPOINT_AUTH) 24 | @RequiredArgsConstructor 25 | public class AuthenticationController { 26 | 27 | private static final Logger LOGGER = LoggerFactory.getLogger(AuthenticationController.class); 28 | private final AuthenticationServiceImpl service; 29 | 30 | @PostMapping("/register") 31 | public ResponseEntity register(@RequestBody @Valid RegisterRequestDTO request) { 32 | LOGGER.info("Received request to register a new user with email: {}", request.getEmail()); 33 | AuthenticationResponseDTO response = service.register(request); 34 | LOGGER.info("User registered successfully with email: {}", request.getEmail()); 35 | return ResponseEntity.ok(response); 36 | } 37 | 38 | @PostMapping("/authenticate") 39 | public ResponseEntity authenticate(@RequestBody AuthenticationRequestDTO request) { 40 | LOGGER.info("Received request to authenticate user with email: {}", request.getEmail()); 41 | AuthenticationResponseDTO response = service.authenticate(request); 42 | LOGGER.info("User authenticated successfully with email: {}", request.getEmail()); 43 | return ResponseEntity.ok(response); 44 | } 45 | 46 | @PostMapping("/refresh-token") 47 | public void refreshToken(HttpServletRequest request, HttpServletResponse response) throws IOException { 48 | LOGGER.info("Received request to refresh token"); 49 | service.refreshToken(request, response); 50 | LOGGER.info("Token refreshed successfully"); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/controllers/DoctorController.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.controllers; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.utils.constants.EndpointsConstants; 4 | import com.medicalhourmanagement.medicalhourmanagement.dtos.DoctorDTO; 5 | import com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos.NotFoundException; 6 | import com.medicalhourmanagement.medicalhourmanagement.services.DoctorService; 7 | import jakarta.validation.Valid; 8 | import lombok.NonNull; 9 | import lombok.RequiredArgsConstructor; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.http.HttpStatus; 13 | import org.springframework.http.ResponseEntity; 14 | import org.springframework.web.bind.annotation.*; 15 | 16 | import java.util.List; 17 | 18 | @RestController 19 | @RequestMapping(path = EndpointsConstants.ENDPOINT_DOCTORS) 20 | @RequiredArgsConstructor 21 | public class DoctorController { 22 | 23 | private static final Logger LOGGER = LoggerFactory.getLogger(DoctorController.class); 24 | private final DoctorService doctorService; 25 | 26 | @GetMapping 27 | public ResponseEntity> getDoctors() { 28 | LOGGER.info("Received request to get all doctors"); 29 | List doctors = doctorService.getDoctors(); 30 | LOGGER.info("Returning {} doctors", doctors.size()); 31 | return ResponseEntity.ok(doctors); 32 | } 33 | 34 | @GetMapping(value = "/{id}") 35 | public ResponseEntity getDoctorById(@NonNull @PathVariable final Long id) { 36 | LOGGER.info("Received request to get doctor with ID: {}", id); 37 | try { 38 | DoctorDTO doctor = doctorService.getDoctorById(id); 39 | LOGGER.info("Returning doctor with ID: {}", id); 40 | return ResponseEntity.ok(doctor); 41 | } catch (NotFoundException e) { 42 | LOGGER.warn("Doctor not found with ID: {}", id); 43 | return ResponseEntity.notFound().build(); 44 | } 45 | } 46 | 47 | @PostMapping 48 | public ResponseEntity saveDoctor(@NonNull @Valid @RequestBody final DoctorDTO doctor) { 49 | LOGGER.info("Received request to save a new doctor"); 50 | try { 51 | DoctorDTO savedDoctor = doctorService.saveDoctor(doctor); 52 | LOGGER.info("Saved new doctor with ID: {}", savedDoctor.getId()); 53 | return ResponseEntity.status(HttpStatus.CREATED).body(savedDoctor); 54 | } catch (IllegalArgumentException e) { 55 | LOGGER.warn("Invalid doctor data: {}", e.getMessage()); 56 | return ResponseEntity.badRequest().build(); 57 | } 58 | } 59 | 60 | @PutMapping(value = "/{id}") 61 | public ResponseEntity updateDoctor(@NonNull @PathVariable final Long id, @NonNull @Valid @RequestBody final DoctorDTO doctorDTO) { 62 | LOGGER.info("Received request to update doctor with ID: {}", id); 63 | try { 64 | DoctorDTO updatedDoctor = doctorService.updateDoctor(id, doctorDTO); 65 | LOGGER.info("Updated doctor with ID: {}", id); 66 | return ResponseEntity.ok(updatedDoctor); 67 | } catch (NotFoundException e) { 68 | LOGGER.warn("Doctor not found with ID: {}", id); 69 | return ResponseEntity.notFound().build(); 70 | } 71 | } 72 | 73 | @DeleteMapping(value = "/{id}") 74 | public ResponseEntity deleteDoctorById(@NonNull @PathVariable final Long id) { 75 | LOGGER.info("Received request to delete doctor with ID: {}", id); 76 | try { 77 | doctorService.deleteDoctorById(id); 78 | LOGGER.info("Deleted doctor with ID: {}", id); 79 | return ResponseEntity.noContent().build(); 80 | } catch (NotFoundException e) { 81 | LOGGER.warn("Doctor not found with ID: {}", id); 82 | return ResponseEntity.notFound().build(); 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/controllers/PatientController.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.controllers; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.dtos.PatientDTO; 4 | import com.medicalhourmanagement.medicalhourmanagement.dtos.request.ChangePasswordRequestDTO; 5 | import com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos.NotFoundException; 6 | import com.medicalhourmanagement.medicalhourmanagement.services.PatientService; 7 | import lombok.RequiredArgsConstructor; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.http.ResponseEntity; 12 | import org.springframework.web.bind.annotation.*; 13 | 14 | import jakarta.validation.Valid; 15 | import java.security.Principal; 16 | import java.util.List; 17 | 18 | @RestController 19 | @RequestMapping("/api/v1/patients") 20 | @RequiredArgsConstructor 21 | public class PatientController { 22 | 23 | private static final Logger LOGGER = LoggerFactory.getLogger(PatientController.class); 24 | private final PatientService patientService; 25 | 26 | @GetMapping 27 | public ResponseEntity> getPatients() { 28 | LOGGER.info("Received request to get all patients"); 29 | List patients = patientService.getPatients(); 30 | LOGGER.info("Returning {} patients", patients.size()); 31 | return ResponseEntity.ok(patients); 32 | } 33 | 34 | @GetMapping("/{id}") 35 | public ResponseEntity getPatientById(@PathVariable Long id) { 36 | LOGGER.info("Received request to get patient with ID: {}", id); 37 | try { 38 | PatientDTO patient = patientService.getPatientById(id); 39 | LOGGER.info("Returning patient with ID: {}", id); 40 | return ResponseEntity.ok(patient); 41 | } catch (NotFoundException e) { 42 | LOGGER.warn("Patient not found with ID: {}", id); 43 | return ResponseEntity.notFound().build(); 44 | } 45 | } 46 | 47 | @PostMapping 48 | public ResponseEntity savePatient(@Valid @RequestBody PatientDTO patient) { 49 | LOGGER.info("Received request to save a new patient"); 50 | try { 51 | PatientDTO savedPatient = patientService.savePatient(patient); 52 | LOGGER.info("Saved new patient with ID: {}", savedPatient.getId()); 53 | return ResponseEntity.status(HttpStatus.CREATED).body(savedPatient); 54 | } catch (IllegalArgumentException e) { 55 | LOGGER.warn("Invalid patient data: {}", e.getMessage()); 56 | return ResponseEntity.badRequest().build(); 57 | } 58 | } 59 | 60 | @PutMapping("/{id}") 61 | public ResponseEntity updatePatient(@PathVariable Long id, @Valid @RequestBody PatientDTO patientDTO) { 62 | LOGGER.info("Received request to update patient with ID: {}", id); 63 | try { 64 | PatientDTO updatedPatient = patientService.updatePatient(id, patientDTO); 65 | LOGGER.info("Updated patient with ID: {}", id); 66 | return ResponseEntity.ok(updatedPatient); 67 | } catch (NotFoundException e) { 68 | LOGGER.warn("Patient not found with ID: {}", id); 69 | return ResponseEntity.notFound().build(); 70 | } 71 | } 72 | 73 | @DeleteMapping("/{id}") 74 | public ResponseEntity deletePatientById(@PathVariable Long id) { 75 | LOGGER.info("Received request to delete patient with ID: {}", id); 76 | try { 77 | patientService.deletePatientById(id); 78 | LOGGER.info("Deleted patient with ID: {}", id); 79 | return ResponseEntity.noContent().build(); 80 | } catch (NotFoundException e) { 81 | LOGGER.warn("Patient not found with ID: {}", id); 82 | return ResponseEntity.notFound().build(); 83 | } 84 | } 85 | 86 | @PatchMapping("/change-password") 87 | public ResponseEntity changePassword(@Valid @RequestBody ChangePasswordRequestDTO request, Principal connectedUser) { 88 | LOGGER.info("Received request to change password for user: {}", connectedUser.getName()); 89 | try { 90 | patientService.changePassword(request, connectedUser); 91 | LOGGER.info("Password changed successfully for user: {}", connectedUser.getName()); 92 | return ResponseEntity.ok().build(); 93 | } catch (IllegalStateException e) { 94 | LOGGER.warn("Failed to change password: {}", e.getMessage()); 95 | return ResponseEntity.badRequest().build(); 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/controllers/SpecialtyController.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.controllers; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.dtos.SpecialtyDTO; 4 | import com.medicalhourmanagement.medicalhourmanagement.services.SpecialtyService; 5 | import com.medicalhourmanagement.medicalhourmanagement.utils.constants.EndpointsConstants; 6 | import lombok.RequiredArgsConstructor; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.http.ResponseEntity; 11 | import org.springframework.web.bind.annotation.*; 12 | 13 | import java.util.List; 14 | 15 | @RestController 16 | @RequestMapping(EndpointsConstants.ENDPOINT_SPECIALTIES_PATTERN) 17 | @RequiredArgsConstructor 18 | public class SpecialtyController { 19 | 20 | private static final Logger LOGGER = LoggerFactory.getLogger(SpecialtyController.class); 21 | private final SpecialtyService specialtyService; 22 | 23 | @GetMapping 24 | public ResponseEntity> getAllSpecialties() { 25 | LOGGER.info("Received request to get all specialties"); 26 | List specialties = specialtyService.getAllSpecialties(); 27 | LOGGER.info("Returning {} specialties", specialties.size()); 28 | return ResponseEntity.ok(specialties); 29 | } 30 | 31 | @PostMapping 32 | public ResponseEntity createSpecialty(@RequestBody SpecialtyDTO specialtyDTO) { 33 | LOGGER.info("Received request to create a new specialty"); 34 | try { 35 | SpecialtyDTO savedSpecialty = specialtyService.createSpecialty(specialtyDTO); 36 | LOGGER.info("Saved new specialty with ID: {}", savedSpecialty.getId()); 37 | return ResponseEntity.status(HttpStatus.CREATED).body(savedSpecialty); 38 | } catch (IllegalArgumentException e) { 39 | LOGGER.warn("Invalid specialty data: {}", e.getMessage()); 40 | return ResponseEntity.badRequest().build(); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/dtos/AppointmentDTO.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.dtos; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import lombok.Data; 7 | 8 | import java.time.LocalDateTime; 9 | 10 | 11 | /** 12 | Esto es un DTO, se utilizan para mover los datos de una capa a otra. 13 | */ 14 | @JsonInclude(JsonInclude.Include.NON_NULL) 15 | @Data 16 | public class AppointmentDTO { 17 | 18 | @JsonProperty("id") 19 | private Long id; 20 | 21 | @JsonProperty("doctor") 22 | private DoctorDTO doctor; 23 | 24 | @JsonProperty("patient") 25 | private PatientDTO patient; 26 | 27 | @JsonProperty("date") 28 | private LocalDateTime date; 29 | 30 | /** 31 | * Con @JsonIgnoreProperties manejamos las referencias circulares 32 | * La anotación @Data de lombok genera solo los métodos getter y setter faltantes, es decir, no sobrescribe los ya definidos. 33 | */ 34 | @JsonIgnoreProperties({"appointments"}) 35 | public DoctorDTO getDoctor() { 36 | return doctor; 37 | } 38 | 39 | @JsonIgnoreProperties({"appointments"}) 40 | public PatientDTO getPatient() { 41 | return patient; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/dtos/DoctorDTO.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.dtos; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | 7 | import java.util.Set; 8 | 9 | 10 | @JsonInclude(JsonInclude.Include.NON_NULL) 11 | @Data 12 | @EqualsAndHashCode(callSuper = true) 13 | public class DoctorDTO extends UserDTO { 14 | private String firstName; 15 | private String lastName; 16 | private Set specialties; 17 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/dtos/PatientDTO.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.dtos; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | 7 | @Data 8 | @EqualsAndHashCode(callSuper = true) 9 | @JsonInclude(JsonInclude.Include.NON_NULL) 10 | public class PatientDTO extends UserDTO { 11 | private String firstName; 12 | private String lastName; 13 | private String rut; 14 | private String phoneNumber; 15 | private String address; 16 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/dtos/SpecialtyDTO.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.dtos; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class SpecialtyDTO { 7 | private Long id; 8 | private String name; 9 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/dtos/UserDTO.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.dtos; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.medicalhourmanagement.medicalhourmanagement.utils.enums.Role; 5 | import lombok.Data; 6 | 7 | @Data 8 | @JsonInclude(JsonInclude.Include.NON_NULL) 9 | public class UserDTO { 10 | private Long id; 11 | private String email; 12 | private Role role; 13 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/dtos/request/AuthenticationRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.dtos.request; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Data 9 | @Builder 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | public class AuthenticationRequestDTO { 13 | 14 | private String email; 15 | String password; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/dtos/request/ChangePasswordRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.dtos.request; 2 | 3 | import lombok.*; 4 | 5 | @Getter 6 | @Setter 7 | @NoArgsConstructor 8 | public class ChangePasswordRequestDTO { 9 | 10 | private String currentPassword; 11 | private String newPassword; 12 | private String confirmationPassword; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/dtos/request/RegisterRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.dtos.request; 2 | 3 | 4 | import com.medicalhourmanagement.medicalhourmanagement.utils.constraints.PasswordConstraint; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | @Data 11 | @Builder 12 | @AllArgsConstructor 13 | @NoArgsConstructor 14 | public class RegisterRequestDTO { 15 | 16 | private String firstname; 17 | private String lastname; 18 | private String email; 19 | @PasswordConstraint 20 | private String password; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/dtos/request/RequestAppointmentDTO.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.dtos.request; 2 | 3 | 4 | import com.fasterxml.jackson.annotation.JsonFormat; 5 | import com.fasterxml.jackson.annotation.JsonInclude; 6 | import com.fasterxml.jackson.annotation.JsonProperty; 7 | import lombok.Data; 8 | 9 | import java.time.LocalDateTime; 10 | 11 | @JsonInclude(JsonInclude.Include.NON_NULL) 12 | @Data 13 | public class RequestAppointmentDTO { 14 | @JsonProperty("doctor") 15 | private Long doctor; 16 | 17 | @JsonProperty("patient") 18 | private Long patient; 19 | 20 | @JsonProperty("date") 21 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") 22 | private LocalDateTime date; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/dtos/response/AuthenticationResponseDTO.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.dtos.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Data 10 | @Builder 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | public class AuthenticationResponseDTO { 14 | 15 | @JsonProperty("Token") 16 | private String accessToken; 17 | @JsonProperty("RefreshToken") 18 | private String refreshToken; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/entities/Appointment.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.entities; 2 | 3 | import com.fasterxml.jackson.annotation.JsonBackReference; 4 | import jakarta.persistence.*; 5 | import jakarta.validation.constraints.Future; 6 | import jakarta.validation.constraints.NotNull; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | 11 | import java.time.LocalDateTime; 12 | 13 | /** 14 | * Esta entidad representa la cita de un paciente con un médico 15 | * La cantidad de atributos es la menor posible para no sobreextender el código, ya que el principal 16 | * objetivo de este proyecto es mostrar un buen manejo en la estructura de la aplicación y las buenas prácticas. 17 | */ 18 | @Entity 19 | @Table(name="APPOINTMENTS") 20 | @Data 21 | @AllArgsConstructor 22 | @NoArgsConstructor 23 | public class Appointment { 24 | 25 | @Id 26 | @GeneratedValue(strategy = GenerationType.IDENTITY) 27 | private Long id; 28 | 29 | /** 30 | * Se hacen validaciones a los atributos en su declaración para mejorar la robustez del código 31 | * La validación depende del tipo de atributo. 32 | * Future maneja que la fecha debe ser futura. 33 | */ 34 | @Future(message = "DATE MUST BE FUTURE") 35 | @NotNull(message = "DATE CANNOT BE NULL") 36 | @Column(name = "date") 37 | private LocalDateTime date; 38 | 39 | /** 40 | * La notación @JsonBackReference se usa en conjunto con @JsonManagedReference(En la otra entidad de la relacion) para evitar referencias circulares 41 | */ 42 | @ManyToOne(fetch = FetchType.LAZY) 43 | @JoinColumn(name = "doctor_id") 44 | @JsonBackReference("doctor-appointments") 45 | @NotNull(message = "DOCTOR CANNOT BE NULL") 46 | private Doctor doctor; 47 | 48 | 49 | @ManyToOne(fetch = FetchType.LAZY) 50 | @JoinColumn(name = "patient_id") 51 | @JsonBackReference("patient-appointments") 52 | @NotNull(message = "PATIENT CANNOT BE NULL") 53 | private Patient patient; 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/entities/Doctor.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.entities; 2 | 3 | import com.fasterxml.jackson.annotation.JsonManagedReference; 4 | import jakarta.persistence.*; 5 | import jakarta.validation.constraints.NotBlank; 6 | import jakarta.validation.constraints.Size; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.NoArgsConstructor; 11 | import lombok.experimental.SuperBuilder; 12 | 13 | import java.util.List; 14 | import java.util.Set; 15 | 16 | @EqualsAndHashCode(callSuper = true) 17 | @Entity 18 | @Table(name = "DOCTORS") 19 | @Data 20 | @SuperBuilder 21 | @AllArgsConstructor 22 | @NoArgsConstructor 23 | public class Doctor extends User { 24 | 25 | @Size(min = 1, max = 50, message = "FIRST NAME MUST BE BETWEEN {min} AND {max} CHARACTERS LONG") 26 | @NotBlank(message = "FIRST NAME CANNOT BE NULL") 27 | @Column(name = "FIRST_NAME") 28 | private String firstName; 29 | 30 | @Size(min = 1, max = 50, message = "LAST NAME MUST BE BETWEEN {min} AND {max} CHARACTERS LONG") 31 | @NotBlank(message = "LAST NAME CANNOT BE NULL") 32 | @Column(name = "LAST_NAME") 33 | private String lastName; 34 | 35 | @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "doctor") 36 | @JsonManagedReference("doctor-appointments") 37 | private List appointments; 38 | @ManyToMany 39 | @JoinTable( 40 | name = "doctor_specialties", 41 | joinColumns = @JoinColumn(name = "doctor_id"), 42 | inverseJoinColumns = @JoinColumn(name = "specialty_id") 43 | ) 44 | private Set specialties; 45 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/entities/Patient.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.entities; 2 | 3 | import com.fasterxml.jackson.annotation.JsonManagedReference; 4 | import jakarta.persistence.*; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.NoArgsConstructor; 9 | import lombok.experimental.SuperBuilder; 10 | 11 | import java.util.List; 12 | 13 | @EqualsAndHashCode(callSuper = true) 14 | @Data 15 | @SuperBuilder 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | @Entity 19 | @Table(name = "_patient") 20 | public class Patient extends User { 21 | 22 | private String firstName; 23 | private String lastName; 24 | 25 | @Column(unique = true) 26 | private String rut; 27 | 28 | @Column(unique = true) 29 | private String phoneNumber; 30 | 31 | private String address; 32 | 33 | @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "patient") 34 | @JsonManagedReference("patient-appointments") 35 | private List appointments; 36 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/entities/Specialty.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.entities; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Entity 9 | @Table(name = "SPECIALTIES") 10 | @Data 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | public class Specialty { 14 | 15 | @Id 16 | @GeneratedValue(strategy = GenerationType.IDENTITY) 17 | private Long id; 18 | 19 | @Column(nullable = false, unique = true) 20 | private String name; 21 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/entities/Token.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.entities; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.utils.enums.TokenType; 4 | import jakarta.persistence.*; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | @Data 11 | @Builder 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | @Entity 15 | public class Token { 16 | 17 | @Id 18 | @GeneratedValue 19 | public Long id; 20 | 21 | @Column(unique = true, name = "access_token") 22 | public String accessToken; 23 | 24 | @Enumerated(EnumType.STRING) 25 | public TokenType tokenType = TokenType.BEARER; 26 | 27 | private boolean revoked; 28 | 29 | private boolean expired; 30 | 31 | @ManyToOne(fetch = FetchType.LAZY) 32 | @JoinColumn(name = "user_id") 33 | public User user; 34 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/entities/User.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.entities; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.utils.enums.Role; 4 | import jakarta.persistence.*; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | import lombok.experimental.SuperBuilder; 9 | import org.springframework.security.core.GrantedAuthority; 10 | import org.springframework.security.core.userdetails.UserDetails; 11 | 12 | import java.util.Collection; 13 | import java.util.List; 14 | 15 | @Data 16 | @SuperBuilder 17 | @NoArgsConstructor 18 | @AllArgsConstructor 19 | @Entity 20 | @Table(name = "_user") 21 | @Inheritance(strategy = InheritanceType.JOINED) 22 | public class User implements UserDetails { 23 | 24 | @Id 25 | @GeneratedValue(strategy = GenerationType.IDENTITY) 26 | private Long id; 27 | 28 | @Column(unique = true) 29 | private String email; 30 | 31 | private String password; 32 | 33 | @Enumerated(EnumType.STRING) 34 | private Role role; 35 | 36 | @OneToMany(mappedBy = "user", fetch = FetchType.EAGER) 37 | private List tokens; 38 | 39 | @Override 40 | public Collection getAuthorities() { 41 | return role.getAuthorities(); 42 | } 43 | 44 | @Override 45 | public String getUsername() { 46 | return email; 47 | } 48 | 49 | @Override 50 | public boolean isAccountNonExpired() { 51 | return true; 52 | } 53 | 54 | @Override 55 | public boolean isAccountNonLocked() { 56 | return true; 57 | } 58 | 59 | @Override 60 | public boolean isCredentialsNonExpired() { 61 | return true; 62 | } 63 | 64 | @Override 65 | public boolean isEnabled() { 66 | return true; 67 | } 68 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/exceptions/controlleradvice/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.exceptions.controlleradvice; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.utils.constants.ExceptionMessageConstants; 4 | import com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos.*; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.security.core.AuthenticationException; 10 | import org.springframework.web.HttpMediaTypeNotSupportedException; 11 | import org.springframework.web.bind.MethodArgumentNotValidException; 12 | import org.springframework.web.bind.annotation.ExceptionHandler; 13 | import org.springframework.web.bind.annotation.RestControllerAdvice; 14 | import org.springframework.web.context.request.WebRequest; 15 | 16 | /** 17 | * Manejador Global de Excepciones 18 | */ 19 | @RestControllerAdvice 20 | public class GlobalExceptionHandler { 21 | 22 | private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class); 23 | 24 | private ResponseEntity buildResponseEntity(HttpStatus status, String message, WebRequest request) { 25 | ExceptionDTO error = new ExceptionDTO(); 26 | error.setStatus(status.value()); 27 | error.setError(status.getReasonPhrase()); 28 | error.setMessage(message); 29 | error.setPath(request.getDescription(false).substring(4)); // Remover "uri=" 30 | return new ResponseEntity<>(error, status); 31 | } 32 | 33 | 34 | @ExceptionHandler(AuthenticationException.class) 35 | public ResponseEntity handleAuthenticationException(AuthenticationException ex, WebRequest request) { 36 | LOGGER.error("AuthenticationException: {}", ex.getMessage(), ex); 37 | HttpStatus status = HttpStatus.UNAUTHORIZED; 38 | return buildResponseEntity(status, ex.getMessage(), request); 39 | } 40 | 41 | @ExceptionHandler(AppException.class) 42 | public ResponseEntity handleAppException(AppException ex, WebRequest request) { 43 | LOGGER.error("AppException: {}", ex.getMessage(), ex); 44 | HttpStatus status = HttpStatus.valueOf(ex.getResponseCode()); 45 | return buildResponseEntity(status, ex.getMessage(), request); 46 | } 47 | 48 | @ExceptionHandler(NotFoundException.class) 49 | public ResponseEntity handleNotFoundException(NotFoundException ex, WebRequest request) { 50 | LOGGER.warn("NotFoundException: {}", ex.getMessage(), ex); 51 | return buildResponseEntity(HttpStatus.NOT_FOUND, ex.getMessage(), request); 52 | } 53 | 54 | @ExceptionHandler(RequestException.class) 55 | public ResponseEntity handleRequestException(RequestException ex, WebRequest request) { 56 | LOGGER.warn("RequestException: {}", ex.getMessage(), ex); 57 | return buildResponseEntity(HttpStatus.BAD_REQUEST, ex.getMessage(), request); 58 | } 59 | 60 | @ExceptionHandler(MethodArgumentNotValidException.class) 61 | public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException ex, WebRequest request) { 62 | LOGGER.warn("MethodArgumentNotValidException: {}", ex.getMessage(), ex); 63 | return buildResponseEntity(HttpStatus.BAD_REQUEST, ex.getMessage(), request); 64 | } 65 | 66 | @ExceptionHandler(HttpMediaTypeNotSupportedException.class) 67 | public ResponseEntity handleHttpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException ex, WebRequest request) { 68 | LOGGER.warn("HttpMediaTypeNotSupportedException: {}", ex.getMessage(), ex); 69 | return buildResponseEntity(HttpStatus.BAD_REQUEST, ex.getMessage(), request); 70 | } 71 | 72 | @ExceptionHandler(IllegalStateException.class) 73 | public ResponseEntity handleIllegalStateExceptionn(IllegalStateException ex, WebRequest request) { 74 | LOGGER.warn("IllegalStateException: {}", ex.getMessage(), ex); 75 | return buildResponseEntity(HttpStatus.BAD_REQUEST, ex.getMessage(), request); 76 | } 77 | 78 | 79 | @ExceptionHandler(DuplicateKeyException.class) 80 | public ResponseEntity handleDuplicateKeyException(DuplicateKeyException ex, WebRequest request) { 81 | LOGGER.warn("DuplicateKeyException: {}", ex.getMessage(), ex); 82 | return buildResponseEntity(HttpStatus.CONFLICT, ex.getMessage(), request); 83 | } 84 | 85 | @ExceptionHandler(UnauthorizedAppointmentException.class) 86 | public ResponseEntity handleUnauthorizedAppointmentException(UnauthorizedAppointmentException ex, WebRequest request) { 87 | LOGGER.warn("UnauthorizedAppointmentException: {}", ex.getMessage(), ex); 88 | return buildResponseEntity(HttpStatus.FORBIDDEN, ex.getMessage(), request); 89 | } 90 | 91 | @ExceptionHandler(InternalServerErrorException.class) 92 | public ResponseEntity handleInternalServerErrorException(InternalServerErrorException ex, WebRequest request) { 93 | LOGGER.error("InternalServerErrorException: {}", ex.getMessage(), ex); 94 | return buildResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR, ExceptionMessageConstants.INTERNAL_SERVER_ERROR_MSG, request); 95 | } 96 | 97 | @ExceptionHandler(NullPointerException.class) 98 | public ResponseEntity handleNullPointerException(NullPointerException ex, WebRequest request) { 99 | LOGGER.error("NullPointerException: {}", ex.getMessage(), ex); 100 | return buildResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR, ExceptionMessageConstants.INTERNAL_SERVER_ERROR_MSG, request); 101 | } 102 | 103 | @ExceptionHandler(RuntimeException.class) 104 | public ResponseEntity handleRuntimeException(RuntimeException ex, WebRequest request) { 105 | LOGGER.error("RuntimeException: {}", ex.getMessage(), ex); 106 | return buildResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR, ExceptionMessageConstants.INTERNAL_SERVER_ERROR_MSG, request); 107 | } 108 | 109 | @ExceptionHandler(Exception.class) 110 | public ResponseEntity handleGenericException(Exception ex, WebRequest request) { 111 | LOGGER.error("Exception: {}", ex.getMessage(), ex); 112 | return buildResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR, ExceptionMessageConstants.INTERNAL_SERVER_ERROR_MSG, request); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/exceptions/dtos/AppException.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos; 2 | 3 | import lombok.Data; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | @Data 8 | public class AppException extends RuntimeException { 9 | 10 | private final String code; 11 | private final int responseCode; 12 | private final List errorList = new ArrayList<>(); 13 | 14 | public AppException(String code, int responseCode, String message) { 15 | super(message); 16 | this.code = code; 17 | this.responseCode = responseCode; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/exceptions/dtos/DuplicateKeyException.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos; 2 | 3 | 4 | import lombok.Data; 5 | 6 | @Data 7 | public class DuplicateKeyException extends RuntimeException { 8 | 9 | private final String message; 10 | 11 | public DuplicateKeyException(String message) { 12 | super(message); 13 | this.message = message; 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/exceptions/dtos/ExceptionDTO.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos; 2 | 3 | 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | import java.time.LocalDateTime; 8 | 9 | @Getter 10 | @Setter 11 | public class ExceptionDTO { 12 | private LocalDateTime timestamp; 13 | private int status; 14 | private String error; 15 | private String message; 16 | private String path; 17 | 18 | public ExceptionDTO() { 19 | this.timestamp = LocalDateTime.now(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/exceptions/dtos/ExpiredTokenException.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos; 2 | 3 | public class ExpiredTokenException extends RuntimeException { 4 | public ExpiredTokenException(String message) { 5 | super(message); 6 | } 7 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/exceptions/dtos/InternalServerErrorException.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class InternalServerErrorException extends RuntimeException { 7 | 8 | private String code; 9 | private String message; 10 | 11 | public InternalServerErrorException(String message) { 12 | this.message = message; 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/exceptions/dtos/InvalidTokenException.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos; 2 | 3 | public class InvalidTokenException extends RuntimeException { 4 | public InvalidTokenException(String message) { 5 | super(message); 6 | } 7 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/exceptions/dtos/NotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class NotFoundException extends RuntimeException { 7 | 8 | private final String message; 9 | 10 | public NotFoundException(String message) { 11 | super(message); 12 | this.message = message; 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/exceptions/dtos/RequestException.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class RequestException extends RuntimeException{ 7 | 8 | private final String message; 9 | public RequestException( String message) { 10 | this.message = message; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/exceptions/dtos/UnauthorizedAppointmentException.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos; 2 | 3 | public class UnauthorizedAppointmentException extends RuntimeException { 4 | public UnauthorizedAppointmentException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/repositories/AppointmentRepository.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.repositories; 2 | 3 | 4 | import com.medicalhourmanagement.medicalhourmanagement.entities.Appointment; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import java.util.List; 9 | 10 | 11 | @Repository 12 | public interface AppointmentRepository extends JpaRepository { 13 | List findByDoctorIdOrPatientId(Long doctorId,Long patientId); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/repositories/DoctorRepository.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.repositories; 2 | 3 | 4 | import com.medicalhourmanagement.medicalhourmanagement.entities.Doctor; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.stereotype.Repository; 7 | 8 | @Repository 9 | public interface DoctorRepository extends JpaRepository { 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/repositories/PatientRepository.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.repositories; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.entities.Patient; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.Optional; 7 | 8 | public interface PatientRepository extends JpaRepository { 9 | 10 | Optional findByEmail(String email); 11 | 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/repositories/SpecialtyRepository.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.repositories; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.entities.Specialty; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | import java.util.Optional; 8 | 9 | @Repository 10 | public interface SpecialtyRepository extends JpaRepository { 11 | Optional findByName(String name); 12 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/repositories/TokenRepository.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.repositories; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.entities.Token; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.jpa.repository.Query; 6 | 7 | import java.util.List; 8 | import java.util.Optional; 9 | 10 | public interface TokenRepository extends JpaRepository { 11 | @Query(""" 12 | select t from Token t inner join User u 13 | on t.user.id = u.id 14 | where u.id = :userId and (t.expired = false or t.revoked = false) 15 | """) 16 | List findAllValidTokenByUser(Long userId); 17 | 18 | Optional findByAccessToken(String token); 19 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/repositories/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.repositories; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.entities.User; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | import java.util.Optional; 8 | 9 | @Repository 10 | public interface UserRepository extends JpaRepository { 11 | Optional findByEmail(String email); 12 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/security/filters/JwtAuthFilter.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.security.filters; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.security.services.JwtService; 4 | import com.medicalhourmanagement.medicalhourmanagement.utils.constants.AuthConstants; 5 | import com.medicalhourmanagement.medicalhourmanagement.utils.constants.EndpointsConstants; 6 | import com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos.ExpiredTokenException; 7 | import com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos.InvalidTokenException; 8 | import com.medicalhourmanagement.medicalhourmanagement.repositories.TokenRepository; 9 | import jakarta.servlet.FilterChain; 10 | import jakarta.servlet.ServletException; 11 | import jakarta.servlet.http.HttpServletRequest; 12 | import jakarta.servlet.http.HttpServletResponse; 13 | import lombok.RequiredArgsConstructor; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.lang.NonNull; 17 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 18 | import org.springframework.security.core.context.SecurityContextHolder; 19 | import org.springframework.security.core.userdetails.UserDetails; 20 | import org.springframework.security.core.userdetails.UserDetailsService; 21 | import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; 22 | import org.springframework.stereotype.Component; 23 | import org.springframework.web.filter.OncePerRequestFilter; 24 | 25 | import java.io.IOException; 26 | 27 | @Component 28 | @RequiredArgsConstructor 29 | public class JwtAuthFilter extends OncePerRequestFilter { 30 | 31 | private static final Logger logger = LoggerFactory.getLogger(JwtAuthFilter.class); 32 | 33 | private final JwtService jwtService; 34 | private final UserDetailsService userDetailsService; 35 | private final TokenRepository tokenRepository; 36 | 37 | @Override 38 | protected void doFilterInternal( 39 | @NonNull HttpServletRequest request, 40 | @NonNull HttpServletResponse response, 41 | @NonNull FilterChain filterChain 42 | ) throws ServletException, IOException { 43 | logger.debug("Processing request to: " + request.getServletPath()); 44 | 45 | if (isAuthPath(request)) { 46 | logger.debug("Skipping JWT authentication for auth path"); 47 | filterChain.doFilter(request, response); 48 | return; 49 | } 50 | 51 | final String authHeader = request.getHeader(AuthConstants.AUTHORIZATION_HEADER); 52 | if (isInvalidAuthHeader(authHeader)) { 53 | logger.debug("Invalid auth header, proceeding with filter chain"); 54 | filterChain.doFilter(request, response); 55 | return; 56 | } 57 | 58 | final String jwt = extractJwtFromHeader(authHeader); 59 | final String userEmail; 60 | 61 | try { 62 | userEmail = jwtService.extractUsername(jwt); 63 | } catch (ExpiredTokenException e) { 64 | logger.warn("Expired token: {}", e.getMessage()); 65 | handleException(response, e.getMessage(), HttpServletResponse.SC_UNAUTHORIZED); 66 | return; 67 | } catch (InvalidTokenException e) { 68 | logger.warn("Invalid token: {}", e.getMessage()); 69 | handleException(response, e.getMessage(), HttpServletResponse.SC_BAD_REQUEST); 70 | return; 71 | } 72 | 73 | if (userEmail != null && isNotAuthenticated()) { 74 | processTokenAuthentication(request, jwt, userEmail); 75 | } 76 | 77 | filterChain.doFilter(request, response); 78 | } 79 | 80 | private boolean isAuthPath(HttpServletRequest request) { 81 | return request.getServletPath().equals(EndpointsConstants.ENDPOINT_AUTH + "/login") || 82 | request.getServletPath().equals(EndpointsConstants.ENDPOINT_AUTH + "/register"); 83 | } 84 | 85 | private boolean isInvalidAuthHeader(String authHeader) { 86 | return authHeader == null || !authHeader.startsWith(AuthConstants.BEARER_PREFIX); 87 | } 88 | 89 | private String extractJwtFromHeader(String authHeader) { 90 | return authHeader.substring(AuthConstants.BEARER_PREFIX.length()); 91 | } 92 | 93 | private boolean isNotAuthenticated() { 94 | return SecurityContextHolder.getContext().getAuthentication() == null; 95 | } 96 | 97 | private void processTokenAuthentication(HttpServletRequest request, String jwt, String userEmail) { 98 | UserDetails userDetails = userDetailsService.loadUserByUsername(userEmail); 99 | boolean isTokenValid = tokenRepository.findByAccessToken(jwt) 100 | .map(t -> !t.isExpired() && !t.isRevoked()) 101 | .orElse(false); 102 | 103 | logger.debug("Processing token authentication for user: " + userEmail); 104 | logger.debug("Is token valid: " + isTokenValid); 105 | 106 | if (jwtService.isTokenValid(jwt, userDetails) && isTokenValid) { 107 | UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( 108 | userDetails, null, userDetails.getAuthorities() 109 | ); 110 | authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); 111 | SecurityContextHolder.getContext().setAuthentication(authToken); 112 | logger.debug("Authentication set in SecurityContext for user: " + userEmail); 113 | } else { 114 | logger.debug("Token validation failed for user: " + userEmail); 115 | } 116 | } 117 | 118 | private void handleException(HttpServletResponse response, String message, int status) throws IOException { 119 | response.setStatus(status); 120 | response.setContentType(AuthConstants.CONTENT_TYPE_JSON); 121 | response.getWriter().write("{\"error\": \"" + message + "\"}"); 122 | response.getWriter().flush(); 123 | } 124 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/security/services/JwtService.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.security.services; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.utils.constants.AuthConstants; 4 | import com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos.ExpiredTokenException; 5 | import com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos.InvalidTokenException; 6 | import io.jsonwebtoken.Claims; 7 | import io.jsonwebtoken.ExpiredJwtException; 8 | import io.jsonwebtoken.Jwts; 9 | import io.jsonwebtoken.SignatureAlgorithm; 10 | import io.jsonwebtoken.io.Decoders; 11 | import io.jsonwebtoken.security.Keys; 12 | import com.medicalhourmanagement.medicalhourmanagement.utils.constants.ExceptionMessageConstants; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | import org.springframework.beans.factory.annotation.Value; 16 | import org.springframework.security.core.GrantedAuthority; 17 | import org.springframework.security.core.userdetails.UserDetails; 18 | import org.springframework.stereotype.Service; 19 | 20 | import java.security.Key; 21 | import java.util.Date; 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | import java.util.function.Function; 25 | 26 | @Service 27 | public class JwtService { 28 | 29 | private static final Logger LOGGER = LoggerFactory.getLogger(JwtService.class); 30 | 31 | @Value("${application.security.jwt.secret-key}") 32 | private String secretKey; 33 | @Value("${application.security.jwt.expiration}") 34 | private long jwtExpiration; 35 | @Value("${application.security.jwt.refresh-token.expiration}") 36 | private long refreshExpiration; 37 | 38 | public String extractUsername(String token) { 39 | return extractClaim(token, Claims::getSubject); 40 | } 41 | 42 | public T extractClaim(String token, Function claimsResolver) { 43 | final Claims claims = extractAllClaims(token); 44 | return claimsResolver.apply(claims); 45 | } 46 | 47 | public String generateToken(UserDetails userDetails) { 48 | Map claims = new HashMap<>(); 49 | claims.put(AuthConstants.ROLE_CLAIM, userDetails.getAuthorities().stream() 50 | .map(GrantedAuthority::getAuthority) 51 | .findFirst().orElseThrow(() -> { 52 | LOGGER.error(ExceptionMessageConstants.ROLE_NOT_FOUND_MSG); 53 | return new RuntimeException(ExceptionMessageConstants.ROLE_NOT_FOUND_MSG); 54 | })); 55 | 56 | return generateToken(claims, userDetails); 57 | } 58 | 59 | public String generateToken( 60 | Map extraClaims, 61 | UserDetails userDetails 62 | ) { 63 | return buildToken(extraClaims, userDetails, jwtExpiration); 64 | } 65 | 66 | public String generateRefreshToken( 67 | UserDetails userDetails 68 | ) { 69 | return buildToken(new HashMap<>(), userDetails, refreshExpiration); 70 | } 71 | 72 | private String buildToken( 73 | Map extraClaims, 74 | UserDetails userDetails, 75 | long expiration 76 | ) { 77 | LOGGER.info("Building token for user {}", userDetails.getUsername()); 78 | return Jwts 79 | .builder() 80 | .setClaims(extraClaims) 81 | .setSubject(userDetails.getUsername()) 82 | .setIssuedAt(new Date(System.currentTimeMillis())) 83 | .setExpiration(new Date(System.currentTimeMillis() + expiration)) 84 | .signWith(getSignInKey(), SignatureAlgorithm.HS512) 85 | .compact(); 86 | } 87 | 88 | public boolean isTokenValid(String token, UserDetails userDetails) { 89 | final String username = extractUsername(token); 90 | boolean isValid = (username.equals(userDetails.getUsername())) && !isTokenExpired(token); 91 | LOGGER.info("Token validation for user {}: {}", username, isValid); 92 | return isValid; 93 | } 94 | 95 | private boolean isTokenExpired(String token) { 96 | return extractExpiration(token).before(new Date()); 97 | } 98 | 99 | private Date extractExpiration(String token) { 100 | return extractClaim(token, Claims::getExpiration); 101 | } 102 | 103 | private Claims extractAllClaims(String token) { 104 | try { 105 | return Jwts 106 | .parserBuilder() 107 | .setSigningKey(getSignInKey()) 108 | .build() 109 | .parseClaimsJws(token) 110 | .getBody(); 111 | } catch (ExpiredJwtException e) { 112 | LOGGER.warn("Expired token: {}", e.getMessage()); 113 | throw new ExpiredTokenException(ExceptionMessageConstants.EXPIRED_TOKEN_MSG); 114 | } catch (IllegalArgumentException e) { 115 | LOGGER.warn("Invalid token: {}", e.getMessage()); 116 | throw new InvalidTokenException(ExceptionMessageConstants.INVALID_TOKEN_MSG); 117 | } 118 | } 119 | 120 | private Key getSignInKey() { 121 | byte[] keyBytes = Decoders.BASE64.decode(secretKey); 122 | return Keys.hmacShaKeyFor(keyBytes); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/security/services/LogoutService.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.security.services; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.utils.constants.AuthConstants; 4 | import com.medicalhourmanagement.medicalhourmanagement.repositories.TokenRepository; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.security.core.Authentication; 9 | import org.springframework.security.core.context.SecurityContextHolder; 10 | import org.springframework.security.web.authentication.logout.LogoutHandler; 11 | import org.springframework.stereotype.Service; 12 | 13 | @Service 14 | @RequiredArgsConstructor 15 | public class LogoutService implements LogoutHandler { 16 | 17 | private final TokenRepository tokenRepository; 18 | 19 | @Override 20 | public void logout( 21 | HttpServletRequest request, 22 | HttpServletResponse response, 23 | Authentication authentication 24 | ) { 25 | final String authHeader = request.getHeader(AuthConstants.AUTHORIZATION_HEADER); 26 | final String jwt; 27 | if (authHeader == null || !authHeader.startsWith(AuthConstants.BEARER_PREFIX)) { 28 | return; 29 | } 30 | jwt = authHeader.substring(AuthConstants.BEARER_PREFIX.length()); 31 | var storedToken = tokenRepository.findByAccessToken(jwt) 32 | .orElse(null); 33 | if (storedToken != null) { 34 | storedToken.setExpired(true); 35 | storedToken.setRevoked(true); 36 | tokenRepository.save(storedToken); 37 | SecurityContextHolder.clearContext(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/services/AppointmentService.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.services; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.dtos.request.RequestAppointmentDTO; 4 | import com.medicalhourmanagement.medicalhourmanagement.dtos.AppointmentDTO; 5 | import lombok.NonNull; 6 | import org.springframework.transaction.annotation.Transactional; 7 | 8 | import java.util.List; 9 | 10 | public interface AppointmentService { 11 | 12 | List getAppointments(); 13 | 14 | AppointmentDTO getAppointmentById(@NonNull Long id) ; 15 | 16 | /** 17 | * Los métodos que realizan modificaciones en la base de datos deben anotarse con @Transactional para asegurar consistencia en la BD. 18 | * Si un método falla durante su ejecución se realiza un rollback. 19 | */ 20 | @Transactional 21 | AppointmentDTO updateAppointment(@NonNull Long appointmentId, @NonNull AppointmentDTO appointmentDTO); 22 | 23 | @Transactional 24 | void deleteAppointmentById(@NonNull Long id); 25 | 26 | @Transactional //Recibe un DTO de request 27 | AppointmentDTO createAppointment(@NonNull RequestAppointmentDTO createAppointmentRest); 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/services/AuthenticationService.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.services; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.dtos.request.RegisterRequestDTO; 4 | import com.medicalhourmanagement.medicalhourmanagement.dtos.request.AuthenticationRequestDTO; 5 | import com.medicalhourmanagement.medicalhourmanagement.dtos.response.AuthenticationResponseDTO; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import java.io.IOException; 9 | 10 | public interface AuthenticationService { 11 | AuthenticationResponseDTO register(RegisterRequestDTO request); 12 | AuthenticationResponseDTO authenticate(AuthenticationRequestDTO request); 13 | void refreshToken(HttpServletRequest request, HttpServletResponse response) throws IOException; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/services/DoctorService.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.services; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.dtos.DoctorDTO; 4 | import lombok.NonNull; 5 | import org.springframework.transaction.annotation.Transactional; 6 | 7 | import java.util.List; 8 | 9 | public interface DoctorService { 10 | List getDoctors(); 11 | 12 | DoctorDTO getDoctorById(@NonNull Long doctorId); 13 | 14 | @Transactional 15 | DoctorDTO saveDoctor(@NonNull DoctorDTO doctorDTO); 16 | 17 | @Transactional 18 | DoctorDTO updateDoctor(@NonNull Long doctorId, @NonNull DoctorDTO doctorDTO); 19 | 20 | @Transactional 21 | void deleteDoctorById(@NonNull Long doctorId); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/services/PatientService.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.services; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.dtos.request.ChangePasswordRequestDTO; 4 | import com.medicalhourmanagement.medicalhourmanagement.dtos.PatientDTO; 5 | import lombok.NonNull; 6 | import org.springframework.transaction.annotation.Transactional; 7 | 8 | import java.security.Principal; 9 | import java.util.List; 10 | 11 | public interface PatientService { 12 | List getPatients(); 13 | 14 | PatientDTO getPatientById(Long patientId); 15 | 16 | @Transactional 17 | PatientDTO updatePatient(@NonNull Long patientId, @NonNull PatientDTO patientDTO); 18 | 19 | @Transactional 20 | void deletePatientById(Long patientId); 21 | 22 | @Transactional 23 | PatientDTO savePatient(PatientDTO paciente); 24 | 25 | void changePassword(ChangePasswordRequestDTO request, Principal connectedUser); 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/services/SpecialtyService.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.services; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.dtos.SpecialtyDTO; 4 | import org.springframework.transaction.annotation.Transactional; 5 | 6 | import java.util.List; 7 | 8 | public interface SpecialtyService { 9 | List getAllSpecialties(); 10 | 11 | @Transactional 12 | SpecialtyDTO createSpecialty(SpecialtyDTO specialtyDTO); 13 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/services/impl/AppointmentServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.services.impl; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.utils.constants.RoleConstants; 4 | import com.medicalhourmanagement.medicalhourmanagement.utils.constants.ExceptionMessageConstants; 5 | import com.medicalhourmanagement.medicalhourmanagement.dtos.request.RequestAppointmentDTO; 6 | import com.medicalhourmanagement.medicalhourmanagement.dtos.AppointmentDTO; 7 | import com.medicalhourmanagement.medicalhourmanagement.entities.Appointment; 8 | import com.medicalhourmanagement.medicalhourmanagement.entities.Doctor; 9 | import com.medicalhourmanagement.medicalhourmanagement.entities.Patient; 10 | import com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos.InternalServerErrorException; 11 | import com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos.NotFoundException; 12 | import com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos.RequestException; 13 | import com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos.UnauthorizedAppointmentException; 14 | import com.medicalhourmanagement.medicalhourmanagement.repositories.AppointmentRepository; 15 | import com.medicalhourmanagement.medicalhourmanagement.services.AppointmentService; 16 | import com.medicalhourmanagement.medicalhourmanagement.services.DoctorService; 17 | import com.medicalhourmanagement.medicalhourmanagement.services.PatientService; 18 | import lombok.NonNull; 19 | import lombok.RequiredArgsConstructor; 20 | import org.modelmapper.ModelMapper; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | import org.springframework.security.core.Authentication; 24 | import org.springframework.security.core.GrantedAuthority; 25 | import org.springframework.security.core.context.SecurityContextHolder; 26 | import org.springframework.stereotype.Service; 27 | import org.springframework.transaction.annotation.Transactional; 28 | 29 | import java.time.Duration; 30 | import java.time.LocalDateTime; 31 | import java.util.Collection; 32 | import java.util.List; 33 | 34 | @Service 35 | @RequiredArgsConstructor 36 | public class AppointmentServiceImpl implements AppointmentService { 37 | 38 | private static final Logger LOGGER = LoggerFactory.getLogger(AppointmentServiceImpl.class); 39 | 40 | private final DoctorService doctorService; 41 | private final PatientService patientService; 42 | private final AppointmentRepository appointmentRepository; 43 | private final ModelMapper mapper; 44 | 45 | @Override 46 | public List getAppointments() { 47 | LOGGER.info("Fetching all appointments"); 48 | return appointmentRepository.findAll().stream() 49 | .map(this::convertToDTO) 50 | .toList(); 51 | } 52 | 53 | @Override 54 | public AppointmentDTO getAppointmentById(@NonNull final Long id) { 55 | LOGGER.info("Fetching appointment with ID: {}", id); 56 | return convertToDTO(getAppointmentByIdHelper(id)); 57 | } 58 | 59 | @Override 60 | @Transactional 61 | public AppointmentDTO createAppointment(@NonNull final RequestAppointmentDTO createAppointmentRest) { 62 | Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 63 | String email = authentication.getName(); 64 | boolean isAdminOrModerator = isAdminOrModerator(authentication.getAuthorities()); 65 | 66 | Patient patient = getPatient(createAppointmentRest.getPatient()); 67 | Doctor doctor = getDoctor(createAppointmentRest.getDoctor()); 68 | 69 | validateUserAuthorization(email, isAdminOrModerator, patient); 70 | validateAppointmentTime(createAppointmentRest.getDate(), doctor.getId(), patient.getId()); 71 | 72 | Appointment appointment = buildAppointment(createAppointmentRest.getDate(), doctor, patient); 73 | LOGGER.info("Creating appointment for patient {} with doctor {} at {}", patient.getId(), doctor.getId(), createAppointmentRest.getDate()); 74 | return saveAppointment(appointment); 75 | } 76 | 77 | @Override 78 | @Transactional 79 | public AppointmentDTO updateAppointment(@NonNull final Long id, @NonNull final AppointmentDTO appointmentRequest) { 80 | Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 81 | String email = authentication.getName(); 82 | boolean isAdminOrModerator = isAdminOrModerator(authentication.getAuthorities()); 83 | 84 | Appointment existingAppointment = getAppointmentByIdHelper(id); 85 | Patient patient = existingAppointment.getPatient(); 86 | Doctor doctor = existingAppointment.getDoctor(); 87 | 88 | validateUserAuthorization(email, isAdminOrModerator, patient); 89 | updateExistingAppointment(existingAppointment, appointmentRequest); 90 | validateAppointmentTime(appointmentRequest.getDate(), doctor.getId(), patient.getId()); 91 | 92 | LOGGER.info("Updating appointment with ID: {}", id); 93 | return saveAppointment(existingAppointment); 94 | } 95 | 96 | @Override 97 | @Transactional 98 | public void deleteAppointmentById(@NonNull final Long id) { 99 | LOGGER.info("Deleting appointment with ID: {}", id); 100 | getAppointmentByIdHelper(id); 101 | appointmentRepository.deleteById(id); 102 | } 103 | 104 | private boolean isAdminOrModerator(Collection authorities) { 105 | return authorities.stream() 106 | .anyMatch(grantedAuthority -> grantedAuthority.getAuthority().equals(RoleConstants.ROLE_ADMIN) || 107 | grantedAuthority.getAuthority().equals(RoleConstants.ROLE_MANAGER)); 108 | } 109 | 110 | private Patient getPatient(Long patientId) { 111 | return mapper.map(patientService.getPatientById(patientId), Patient.class); 112 | } 113 | 114 | private Doctor getDoctor(Long doctorId) { 115 | return mapper.map(doctorService.getDoctorById(doctorId), Doctor.class); 116 | } 117 | 118 | private void validateUserAuthorization(String email, boolean isAdminOrModerator, Patient patient) { 119 | if (!isAdminOrModerator && !patient.getEmail().equals(email)) { 120 | LOGGER.warn("Unauthorized access attempt by user {}", email); 121 | throw new UnauthorizedAppointmentException(ExceptionMessageConstants.UNAUTHORIZED_MSG); 122 | } 123 | } 124 | 125 | private void validateAppointmentTime(@NonNull final LocalDateTime appointmentTime, @NonNull final Long doctorId, @NonNull final Long patientId) { 126 | List appointments = appointmentRepository.findByDoctorIdOrPatientId(doctorId, patientId); 127 | 128 | if (appointmentTime.getHour() < 8 || appointmentTime.getHour() >= 18) { 129 | LOGGER.warn("Invalid appointment time: {}", appointmentTime); 130 | throw new RequestException(ExceptionMessageConstants.TIME_INVALID_MSG); 131 | } 132 | 133 | appointments.forEach(existingAppointment -> { 134 | long timeDifference = Math.abs(Duration.between(existingAppointment.getDate(), appointmentTime).toMinutes()); 135 | if (timeDifference < 60) { 136 | LOGGER.warn("Appointment time conflict for doctor {} or patient {} at {}", doctorId, patientId, appointmentTime); 137 | throw new RequestException(ExceptionMessageConstants.TIME_CONFLICT_MSG); 138 | } 139 | }); 140 | } 141 | 142 | private Appointment buildAppointment(LocalDateTime date, Doctor doctor, Patient patient) { 143 | Appointment appointment = new Appointment(); 144 | appointment.setDate(date); 145 | appointment.setDoctor(doctor); 146 | appointment.setPatient(patient); 147 | return appointment; 148 | } 149 | 150 | private AppointmentDTO saveAppointment(Appointment appointment) { 151 | try { 152 | Appointment savedAppointment = appointmentRepository.save(appointment); 153 | LOGGER.info("Appointment saved successfully with ID: {}", savedAppointment.getId()); 154 | return convertToDTO(savedAppointment); 155 | } catch (Exception e) { 156 | LOGGER.error("Error occurred during appointment save", e); 157 | throw new InternalServerErrorException(ExceptionMessageConstants.INTERNAL_SERVER_ERROR_MSG); 158 | } 159 | } 160 | 161 | private void updateExistingAppointment(Appointment existingAppointment, AppointmentDTO appointmentRequest) { 162 | existingAppointment.setDate(appointmentRequest.getDate()); 163 | existingAppointment.setDoctor(mapper.map(appointmentRequest.getDoctor(), Doctor.class)); 164 | existingAppointment.setPatient(mapper.map(appointmentRequest.getPatient(), Patient.class)); 165 | } 166 | 167 | private Appointment getAppointmentByIdHelper(@NonNull final Long id) { 168 | return appointmentRepository.findById(id) 169 | .orElseThrow(() -> { 170 | LOGGER.warn("Appointment not found with ID: {}", id); 171 | return new NotFoundException(ExceptionMessageConstants.APPOINTMENT_NOT_FOUND_MSG); 172 | }); 173 | } 174 | 175 | private AppointmentDTO convertToDTO(Appointment appointment) { 176 | return mapper.map(appointment, AppointmentDTO.class); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/services/impl/AuthenticationServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.services.impl; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.medicalhourmanagement.medicalhourmanagement.utils.constants.AuthConstants; 5 | import com.medicalhourmanagement.medicalhourmanagement.entities.Patient; 6 | import com.medicalhourmanagement.medicalhourmanagement.entities.User; 7 | import com.medicalhourmanagement.medicalhourmanagement.utils.enums.Role; 8 | import com.medicalhourmanagement.medicalhourmanagement.utils.enums.TokenType; 9 | import com.medicalhourmanagement.medicalhourmanagement.entities.Token; 10 | import com.medicalhourmanagement.medicalhourmanagement.security.services.JwtService; 11 | import com.medicalhourmanagement.medicalhourmanagement.dtos.request.RegisterRequestDTO; 12 | import com.medicalhourmanagement.medicalhourmanagement.dtos.request.AuthenticationRequestDTO; 13 | import com.medicalhourmanagement.medicalhourmanagement.dtos.response.AuthenticationResponseDTO; 14 | import com.medicalhourmanagement.medicalhourmanagement.repositories.TokenRepository; 15 | import com.medicalhourmanagement.medicalhourmanagement.repositories.PatientRepository; 16 | import com.medicalhourmanagement.medicalhourmanagement.services.AuthenticationService; 17 | import jakarta.servlet.http.HttpServletRequest; 18 | import jakarta.servlet.http.HttpServletResponse; 19 | import lombok.RequiredArgsConstructor; 20 | import org.springframework.dao.DataIntegrityViolationException; 21 | import org.springframework.http.HttpHeaders; 22 | import org.springframework.security.authentication.AuthenticationManager; 23 | import org.springframework.security.authentication.BadCredentialsException; 24 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 25 | import org.springframework.security.crypto.password.PasswordEncoder; 26 | import org.springframework.stereotype.Service; 27 | import org.slf4j.Logger; 28 | import org.slf4j.LoggerFactory; 29 | 30 | import java.io.IOException; 31 | 32 | @Service 33 | @RequiredArgsConstructor 34 | public class AuthenticationServiceImpl implements AuthenticationService { 35 | 36 | private static final Logger LOGGER = LoggerFactory.getLogger(AuthenticationServiceImpl.class); 37 | 38 | private final PatientRepository repository; 39 | private final TokenRepository tokenRepository; 40 | private final PasswordEncoder passwordEncoder; 41 | private final JwtService jwtService; 42 | private final AuthenticationManager authenticationManager; 43 | 44 | @Override 45 | public AuthenticationResponseDTO register(RegisterRequestDTO request) { 46 | LOGGER.info("Registering new user with email: {}", request.getEmail()); 47 | Patient user = buildPatientFromRequest(request); 48 | Patient savedUser = repository.save(user); 49 | String jwtToken = jwtService.generateToken(user); 50 | String refreshToken = jwtService.generateRefreshToken(user); 51 | saveUserToken(savedUser, jwtToken); 52 | return buildAuthResponse(jwtToken, refreshToken); 53 | } 54 | 55 | @Override 56 | public AuthenticationResponseDTO authenticate(AuthenticationRequestDTO request) { 57 | LOGGER.info("Authenticating user with email: {}", request.getEmail()); 58 | Patient user = repository.findByEmail(request.getEmail()) 59 | .orElseThrow(() -> new BadCredentialsException("Bad Credentials")); 60 | 61 | authenticateUser(request.getEmail(), request.getPassword()); 62 | 63 | String jwtToken = jwtService.generateToken(user); 64 | String refreshToken = jwtService.generateRefreshToken(user); 65 | revokeAllUserTokens(user); 66 | saveUserToken(user, jwtToken); 67 | return buildAuthResponse(jwtToken, refreshToken); 68 | } 69 | 70 | @Override 71 | public void refreshToken(HttpServletRequest request, HttpServletResponse response) throws IOException { 72 | String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); 73 | if (isInvalidAuthHeader(authHeader)) { 74 | LOGGER.warn("Invalid authorization header during token refresh"); 75 | return; 76 | } 77 | 78 | String refreshToken = extractToken(authHeader); 79 | String userEmail = jwtService.extractUsername(refreshToken); 80 | 81 | if (userEmail != null) { 82 | processRefreshToken(response, refreshToken, userEmail); 83 | } 84 | } 85 | 86 | private boolean isInvalidAuthHeader(String authHeader) { 87 | return authHeader == null || !authHeader.startsWith(AuthConstants.BEARER_PREFIX); 88 | } 89 | 90 | private String extractToken(String authHeader) { 91 | return authHeader.substring(AuthConstants.BEARER_PREFIX.length()); 92 | } 93 | 94 | private void processRefreshToken(HttpServletResponse response, String refreshToken, String userEmail) throws IOException { 95 | Patient user = repository.findByEmail(userEmail).orElseThrow(); 96 | if (jwtService.isTokenValid(refreshToken, user)) { 97 | String accessToken = jwtService.generateToken(user); 98 | revokeAllUserTokens(user); 99 | saveUserToken(user, accessToken); 100 | writeAuthResponse(response, buildAuthResponse(accessToken, refreshToken)); 101 | } 102 | } 103 | 104 | private void writeAuthResponse(HttpServletResponse response, AuthenticationResponseDTO authResponse) throws IOException { 105 | response.setContentType(AuthConstants.CONTENT_TYPE_JSON); 106 | new ObjectMapper().writeValue(response.getOutputStream(), authResponse); 107 | } 108 | 109 | private Patient buildPatientFromRequest(RegisterRequestDTO request) { 110 | return Patient.builder() 111 | .firstName(request.getFirstname()) 112 | .lastName(request.getLastname()) 113 | .email(request.getEmail()) 114 | .password(passwordEncoder.encode(request.getPassword())) 115 | .role(Role.USER) 116 | .build(); 117 | } 118 | 119 | private void authenticateUser(String email, String password) { 120 | authenticationManager.authenticate( 121 | new UsernamePasswordAuthenticationToken(email, password) 122 | ); 123 | } 124 | 125 | private AuthenticationResponseDTO buildAuthResponse(String jwtToken, String refreshToken) { 126 | return AuthenticationResponseDTO.builder() 127 | .accessToken(jwtToken) 128 | .refreshToken(refreshToken) 129 | .build(); 130 | } 131 | 132 | private void saveUserToken(User user, String jwtToken) { 133 | Token token = Token.builder() 134 | .user(user) 135 | .accessToken(jwtToken) 136 | .tokenType(TokenType.BEARER) 137 | .expired(false) 138 | .revoked(false) 139 | .build(); 140 | try { 141 | tokenRepository.save(token); 142 | } catch (DataIntegrityViolationException e) { 143 | // Si el token ya existe, se actualiza 144 | Token existingToken = tokenRepository.findByAccessToken(jwtToken) 145 | .orElseThrow(() -> new RuntimeException("Deberia haber token pero no se encontro")); 146 | existingToken.setExpired(false); 147 | existingToken.setRevoked(false); 148 | tokenRepository.save(existingToken); 149 | } 150 | } 151 | 152 | private void revokeAllUserTokens(Patient patient) { 153 | var validUserTokens = tokenRepository.findAllValidTokenByUser(patient.getId()); 154 | if (validUserTokens.isEmpty()) { 155 | return; 156 | } 157 | validUserTokens.forEach(token -> { 158 | token.setExpired(true); 159 | token.setRevoked(true); 160 | }); 161 | tokenRepository.saveAll(validUserTokens); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/services/impl/DoctorServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.services.impl; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.dtos.DoctorDTO; 4 | import com.medicalhourmanagement.medicalhourmanagement.entities.Doctor; 5 | import com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos.InternalServerErrorException; 6 | import com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos.NotFoundException; 7 | import com.medicalhourmanagement.medicalhourmanagement.repositories.DoctorRepository; 8 | import com.medicalhourmanagement.medicalhourmanagement.services.DoctorService; 9 | import com.medicalhourmanagement.medicalhourmanagement.utils.constants.ExceptionMessageConstants; 10 | import com.medicalhourmanagement.medicalhourmanagement.utils.constants.RoleConstants; 11 | import lombok.NonNull; 12 | import lombok.RequiredArgsConstructor; 13 | import org.modelmapper.ModelMapper; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.security.access.prepost.PreAuthorize; 17 | import org.springframework.stereotype.Service; 18 | import org.springframework.transaction.annotation.Transactional; 19 | 20 | import java.util.List; 21 | 22 | @Service 23 | @RequiredArgsConstructor 24 | public class DoctorServiceImpl implements DoctorService { 25 | 26 | private static final Logger LOGGER = LoggerFactory.getLogger(DoctorServiceImpl.class); 27 | 28 | private final DoctorRepository doctorRepository; 29 | private final ModelMapper mapper; 30 | 31 | @Override 32 | public List getDoctors() { 33 | LOGGER.info("Fetching all doctors"); 34 | List doctors = doctorRepository.findAll(); 35 | return doctors.stream().map(this::convertToRest).toList(); 36 | } 37 | 38 | @Override 39 | public DoctorDTO getDoctorById(@NonNull final Long doctorId) { 40 | LOGGER.info("Fetching doctor with ID: {}", doctorId); 41 | Doctor doctor = getDoctorByIdHelper(doctorId); 42 | return convertToRest(doctor); 43 | } 44 | 45 | @Override 46 | @Transactional 47 | @PreAuthorize("hasRole('" + RoleConstants.ROLE_ADMIN + "')") 48 | public DoctorDTO saveDoctor(@NonNull final DoctorDTO doctorDTO) { 49 | LOGGER.info("Saving new doctor"); 50 | doctorDTO.setId(null); 51 | try { 52 | Doctor doctorEntity = convertToEntity(doctorDTO); 53 | Doctor savedDoctor = doctorRepository.save(doctorEntity); 54 | LOGGER.info("Doctor saved successfully with ID: {}", savedDoctor.getId()); 55 | return convertToRest(savedDoctor); 56 | } catch (Exception e) { 57 | LOGGER.error("Error occurred during doctor save", e); 58 | throw new InternalServerErrorException(ExceptionMessageConstants.INTERNAL_SERVER_ERROR_MSG); 59 | } 60 | } 61 | 62 | @Override 63 | @Transactional 64 | @PreAuthorize("hasRole('" + RoleConstants.ROLE_ADMIN + "')") 65 | public DoctorDTO updateDoctor(@NonNull final Long doctorId, @NonNull final DoctorDTO doctorDTO) { 66 | LOGGER.info("Updating doctor with ID: {}", doctorId); 67 | getDoctorByIdHelper(doctorId); 68 | Doctor doctorEntity = convertToEntity(doctorDTO); 69 | doctorEntity.setId(doctorId); 70 | Doctor updatedDoctor = doctorRepository.save(doctorEntity); 71 | LOGGER.info("Doctor updated successfully with ID: {}", updatedDoctor.getId()); 72 | return convertToRest(updatedDoctor); 73 | } 74 | 75 | @Override 76 | @Transactional 77 | @PreAuthorize("hasRole('" + RoleConstants.ROLE_ADMIN + "')") 78 | public void deleteDoctorById(@NonNull final Long doctorId) { 79 | LOGGER.info("Deleting doctor with ID: {}", doctorId); 80 | getDoctorByIdHelper(doctorId); 81 | doctorRepository.deleteById(doctorId); 82 | LOGGER.info("Doctor deleted successfully with ID: {}", doctorId); 83 | } 84 | 85 | private Doctor getDoctorByIdHelper(@NonNull Long doctorId) { 86 | return doctorRepository.findById(doctorId) 87 | .orElseThrow(() -> { 88 | LOGGER.warn("Doctor not found with ID: {}", doctorId); 89 | return new NotFoundException(ExceptionMessageConstants.DOCTOR_NOT_FOUND_MSG); 90 | }); 91 | } 92 | 93 | private DoctorDTO convertToRest(Doctor doctor) { 94 | return mapper.map(doctor, DoctorDTO.class); 95 | } 96 | 97 | private Doctor convertToEntity(DoctorDTO doctorDTO) { 98 | return mapper.map(doctorDTO, Doctor.class); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/services/impl/PatientServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.services.impl; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.dtos.request.ChangePasswordRequestDTO; 4 | import com.medicalhourmanagement.medicalhourmanagement.dtos.PatientDTO; 5 | import com.medicalhourmanagement.medicalhourmanagement.entities.Patient; 6 | import com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos.NotFoundException; 7 | import com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos.UnauthorizedAppointmentException; 8 | import com.medicalhourmanagement.medicalhourmanagement.repositories.PatientRepository; 9 | import com.medicalhourmanagement.medicalhourmanagement.services.PatientService; 10 | import com.medicalhourmanagement.medicalhourmanagement.utils.constants.ExceptionMessageConstants; 11 | import com.medicalhourmanagement.medicalhourmanagement.utils.constants.RoleConstants; 12 | import lombok.NonNull; 13 | import lombok.RequiredArgsConstructor; 14 | import org.modelmapper.ModelMapper; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | import org.springframework.security.access.prepost.PreAuthorize; 18 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 19 | import org.springframework.security.core.Authentication; 20 | import org.springframework.security.core.GrantedAuthority; 21 | import org.springframework.security.core.context.SecurityContextHolder; 22 | import org.springframework.security.crypto.password.PasswordEncoder; 23 | import org.springframework.stereotype.Service; 24 | import org.springframework.transaction.annotation.Transactional; 25 | 26 | import java.security.Principal; 27 | import java.util.Collection; 28 | import java.util.List; 29 | 30 | @Service 31 | @RequiredArgsConstructor 32 | public class PatientServiceImpl implements PatientService { 33 | 34 | private static final Logger LOGGER = LoggerFactory.getLogger(PatientServiceImpl.class); 35 | 36 | private final PatientRepository patientRepository; 37 | private final PasswordEncoder passwordEncoder; 38 | private final ModelMapper mapper; 39 | 40 | @Override 41 | @PreAuthorize("hasRole('" + RoleConstants.ROLE_ADMIN + "') or hasRole('" + RoleConstants.ROLE_MANAGER + "')") 42 | public List getPatients() { 43 | LOGGER.info("Fetching all patients"); 44 | List patients = patientRepository.findAll(); 45 | return patients.stream().map(this::convertToRest).toList(); 46 | } 47 | 48 | @Override 49 | @PreAuthorize("hasRole('" + RoleConstants.ROLE_ADMIN + "') or hasRole('" + RoleConstants.ROLE_MANAGER + "')") 50 | public PatientDTO getPatientById(@NonNull final Long patientId) { 51 | LOGGER.info("Fetching patient with ID: {}", patientId); 52 | Patient patient = getPatientByIdHelper(patientId); 53 | validateUserAuthorization(patient.getEmail()); 54 | return convertToRest(patient); 55 | } 56 | 57 | @Override 58 | @Transactional 59 | @PreAuthorize("hasRole('" + RoleConstants.ROLE_ADMIN + "')") 60 | public PatientDTO savePatient(@NonNull final PatientDTO patientDTO) { 61 | LOGGER.info("Saving new patient"); 62 | patientDTO.setId(null); 63 | Patient patientEntity = convertToEntity(patientDTO); 64 | Patient savedPatient = patientRepository.save(patientEntity); 65 | if (savedPatient == null) { 66 | LOGGER.error("Failed to save patient"); 67 | throw new IllegalStateException("Failed to save patient"); 68 | } 69 | LOGGER.info("Patient saved successfully with ID: {}", savedPatient.getId()); 70 | return convertToRest(savedPatient); 71 | } 72 | 73 | @Override 74 | @Transactional 75 | @PreAuthorize("hasRole('" + RoleConstants.ROLE_ADMIN + "')") 76 | public PatientDTO updatePatient(@NonNull final Long patientId, @NonNull final PatientDTO patientDTO) { 77 | LOGGER.info("Updating patient with ID: {}", patientId); 78 | Patient existingPatient = getPatientByIdHelper(patientId); 79 | existingPatient.setFirstName(patientDTO.getFirstName()); 80 | existingPatient.setLastName(patientDTO.getLastName()); 81 | Patient updatedPatient = patientRepository.save(existingPatient); 82 | if (updatedPatient == null) { 83 | LOGGER.error("Failed to update patient"); 84 | throw new IllegalStateException("Failed to update patient"); 85 | } 86 | LOGGER.info("Patient updated successfully with ID: {}", updatedPatient.getId()); 87 | return convertToRest(updatedPatient); 88 | } 89 | 90 | @Override 91 | @Transactional 92 | @PreAuthorize("hasRole('" + RoleConstants.ROLE_ADMIN + "')") 93 | public void deletePatientById(@NonNull final Long patientId) { 94 | LOGGER.info("Deleting patient with ID: {}", patientId); 95 | Patient patient = getPatientByIdHelper(patientId); 96 | if (patient == null) { 97 | LOGGER.error("Patient with ID: {} not found", patientId); 98 | throw new NotFoundException("Patient not found"); 99 | } 100 | patientRepository.deleteById(patientId); 101 | LOGGER.info("Patient deleted successfully with ID: {}", patientId); 102 | } 103 | 104 | private Patient getPatientByIdHelper(@NonNull final Long patientId) { 105 | return patientRepository.findById(patientId) 106 | .orElseThrow(() -> { 107 | LOGGER.warn("Patient not found with ID: {}", patientId); 108 | return new NotFoundException(ExceptionMessageConstants.PATIENT_NOT_FOUND_MSG); 109 | }); 110 | } 111 | 112 | private void validateUserAuthorization(String patientEmail) { 113 | Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 114 | String email = authentication.getName(); 115 | Collection authorities = authentication.getAuthorities(); 116 | 117 | boolean isAdminOrManager = authorities.stream() 118 | .anyMatch(grantedAuthority -> grantedAuthority.getAuthority().equals(RoleConstants.ROLE_ADMIN) || 119 | grantedAuthority.getAuthority().equals(RoleConstants.ROLE_MANAGER)); 120 | 121 | if (!isAdminOrManager && !patientEmail.equals(email)) { 122 | LOGGER.warn("Unauthorized access attempt by user {}", email); 123 | throw new UnauthorizedAppointmentException(ExceptionMessageConstants.UNAUTHORIZED_MSG); 124 | } 125 | } 126 | 127 | private PatientDTO convertToRest(Patient patient) { 128 | return mapper.map(patient, PatientDTO.class); 129 | } 130 | 131 | private Patient convertToEntity(PatientDTO patientDTO) { 132 | return mapper.map(patientDTO, Patient.class); 133 | } 134 | 135 | @Override 136 | public void changePassword(ChangePasswordRequestDTO request, Principal connectedUser) { 137 | var user = (Patient) ((UsernamePasswordAuthenticationToken) connectedUser).getPrincipal(); 138 | 139 | if (!passwordEncoder.matches(request.getCurrentPassword(), user.getPassword())) { 140 | LOGGER.warn("Wrong current password for user {}", user.getEmail()); 141 | throw new IllegalStateException(ExceptionMessageConstants.WRONG_PASSWORD_MSG); 142 | } 143 | 144 | if (!request.getNewPassword().equals(request.getConfirmationPassword())) { 145 | LOGGER.warn("New password and confirmation do not match for user {}", user.getEmail()); 146 | throw new IllegalStateException(ExceptionMessageConstants.WRONG_CONF_PASSWORD_MSG); 147 | } 148 | 149 | user.setPassword(passwordEncoder.encode(request.getNewPassword())); 150 | patientRepository.save(user); 151 | LOGGER.info("Password changed successfully for user {}", user.getEmail()); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/services/impl/SpecialtyServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.services.impl; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.dtos.SpecialtyDTO; 4 | import com.medicalhourmanagement.medicalhourmanagement.entities.Specialty; 5 | import com.medicalhourmanagement.medicalhourmanagement.repositories.SpecialtyRepository; 6 | import com.medicalhourmanagement.medicalhourmanagement.services.SpecialtyService; 7 | import lombok.RequiredArgsConstructor; 8 | import org.modelmapper.ModelMapper; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.security.access.prepost.PreAuthorize; 12 | import org.springframework.stereotype.Service; 13 | import org.springframework.transaction.annotation.Transactional; 14 | 15 | import java.util.List; 16 | import java.util.stream.Collectors; 17 | 18 | @Service 19 | @RequiredArgsConstructor 20 | public class SpecialtyServiceImpl implements SpecialtyService { 21 | 22 | private static final Logger LOGGER = LoggerFactory.getLogger(SpecialtyServiceImpl.class); 23 | 24 | private final SpecialtyRepository specialtyRepository; 25 | private final ModelMapper mapper; 26 | 27 | @Override 28 | public List getAllSpecialties() { 29 | LOGGER.info("Fetching all specialties"); 30 | List specialties = specialtyRepository.findAll(); 31 | return specialties.stream().map(this::convertToDTO).collect(Collectors.toList()); 32 | } 33 | 34 | @Override 35 | @Transactional 36 | @PreAuthorize("hasRole('ROLE_ADMIN')") 37 | public SpecialtyDTO createSpecialty(SpecialtyDTO specialtyDTO) { 38 | LOGGER.info("Creating new specialty"); 39 | Specialty specialty = convertToEntity(specialtyDTO); 40 | Specialty savedSpecialty = specialtyRepository.save(specialty); 41 | return convertToDTO(savedSpecialty); 42 | } 43 | 44 | private Specialty convertToEntity(SpecialtyDTO specialtyDTO) { 45 | return mapper.map(specialtyDTO, Specialty.class); 46 | } 47 | 48 | private SpecialtyDTO convertToDTO(Specialty specialty) { 49 | return mapper.map(specialty, SpecialtyDTO.class); 50 | } 51 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/utils/constants/AuthConstants.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.utils.constants; 2 | 3 | public class AuthConstants { 4 | // Constructor privado para evitar la instanciación 5 | private AuthConstants() { 6 | throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); 7 | } 8 | public static final String AUTHORIZATION_HEADER = "Authorization"; 9 | public static final String BEARER_PREFIX = "Bearer "; 10 | public static final String ROLE_CLAIM = "role"; 11 | 12 | public static final String CONTENT_TYPE_JSON = "application/json"; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/utils/constants/EndpointsConstants.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.utils.constants; 2 | 3 | public class EndpointsConstants { 4 | 5 | // Constructor privado para evitar la instanciación 6 | private EndpointsConstants() { 7 | throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); 8 | } 9 | 10 | //Los pattern son usados para la configuracion de spring security 11 | public static final String ENDPOINT_BASE_API = "/api/v1"; 12 | public static final String ENDPOINT_AUTH = ENDPOINT_BASE_API + "/auth"; 13 | public static final String ENDPOINT_AUTH_PATTERN = ENDPOINT_AUTH + "/**"; 14 | public static final String ENDPOINT_LOGOUT = ENDPOINT_AUTH + "/logout"; 15 | public static final String ENDPOINT_ACTUATOR = "/actuator"; 16 | public static final String ENDPOINT_ACTUATOR_PATTERN = ENDPOINT_ACTUATOR + "/**"; 17 | public static final String ENDPOINT_DOCTORS = ENDPOINT_BASE_API + "/doctors"; 18 | public static final String ENDPOINT_DOCTORS_PATTERN = ENDPOINT_DOCTORS + "/**"; 19 | public static final String ENDPOINT_PATIENTS = ENDPOINT_BASE_API + "/patients"; 20 | public static final String ENDPOINT_PATIENTS_PATTERN = ENDPOINT_PATIENTS + "/**"; 21 | public static final String ENDPOINT_APPOINTMENTS = ENDPOINT_BASE_API + "/appointments"; 22 | public static final String ENDPOINT_APPOINTMENTS_PATTERN = ENDPOINT_APPOINTMENTS + "/**"; 23 | 24 | public static final String ENDPOINT_SPECIALTIES = ENDPOINT_BASE_API + "/specialties"; 25 | public static final String ENDPOINT_SPECIALTIES_PATTERN = ENDPOINT_APPOINTMENTS + "/**"; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/utils/constants/ExceptionMessageConstants.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.utils.constants; 2 | 3 | public class ExceptionMessageConstants { 4 | 5 | // Constructor privado para evitar la instanciación 6 | private ExceptionMessageConstants() { 7 | throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); 8 | } 9 | 10 | /** 11 | * AUTH 12 | */ 13 | public static final String UNAUTHORIZED_MSG = "You are not authorized"; 14 | public static final String WRONG_PASSWORD_MSG = "Wrong password"; 15 | public static final String WRONG_CONF_PASSWORD_MSG = "Password are not the same"; 16 | public static final String EXPIRED_TOKEN_MSG = "Token has expired"; 17 | public static final String INVALID_TOKEN_MSG = "Token is invalid"; 18 | public static final String USER_NOT_FOUND_MSG = "User not found"; 19 | public static final String ROLE_NOT_FOUND_MSG = "Role not found"; 20 | 21 | 22 | /** 23 | * APPOINTMENTS 24 | */ 25 | public static final String TIME_CONFLICT_MSG = "Appointments must be at least 60 minutes apart"; 26 | public static final String TIME_INVALID_MSG = "Appointments must be between 8 AM and 6 PM"; 27 | public static final String APPOINTMENT_NOT_FOUND_MSG = "Appointment not found"; 28 | 29 | 30 | /** 31 | * PATIENTS 32 | */ 33 | public static final String PATIENT_NOT_FOUND_MSG = "Patient not found"; 34 | 35 | /** 36 | * DOCTORS 37 | */ 38 | public static final String DOCTOR_NOT_FOUND_MSG = "Doctor not found"; 39 | 40 | 41 | /** 42 | * API RESPONSES 43 | */ 44 | public static final String INTERNAL_SERVER_ERROR_MSG = "An unexpected error occurred"; 45 | 46 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/utils/constants/RoleConstants.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.utils.constants; 2 | 3 | public class RoleConstants { 4 | 5 | // Constructor privado para evitar la instanciación 6 | private RoleConstants() { 7 | throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); 8 | } 9 | 10 | public static final String ROLE_MANAGER = "ROLE_MODERATOR"; 11 | public static final String ROLE_ADMIN = "ROLE_ADMIN"; 12 | public static final String ROLE_USER = "ROLE_USER"; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/utils/constraints/PasswordConstraint.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.utils.constraints; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.utils.validators.PasswordConstraintValidator; 4 | import jakarta.validation.Constraint; 5 | import jakarta.validation.Payload; 6 | 7 | import java.lang.annotation.ElementType; 8 | import java.lang.annotation.Retention; 9 | import java.lang.annotation.RetentionPolicy; 10 | import java.lang.annotation.Target; 11 | 12 | @Constraint(validatedBy = PasswordConstraintValidator.class) 13 | @Target({ ElementType.FIELD, ElementType.PARAMETER }) 14 | @Retention(RetentionPolicy.RUNTIME) 15 | public @interface PasswordConstraint { 16 | String message() default "Invalid password"; 17 | Class[] groups() default {}; 18 | Class[] payload() default {}; 19 | } -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/utils/enums/Permission.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.utils.enums; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | @RequiredArgsConstructor 7 | public enum Permission { 8 | 9 | ADMIN_READ("admin:read"), 10 | ADMIN_UPDATE("admin:update"), 11 | ADMIN_CREATE("admin:create"), 12 | ADMIN_DELETE("admin:delete"), 13 | MANAGER_READ("management:read"), 14 | MANAGER_UPDATE("management:update"), 15 | MANAGER_CREATE("management:create"), 16 | MANAGER_DELETE("management:delete") 17 | 18 | ; 19 | 20 | @Getter 21 | private final String permission; 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/utils/enums/Role.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.utils.enums; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 6 | 7 | import java.util.Collections; 8 | import java.util.List; 9 | import java.util.Set; 10 | import java.util.stream.Collectors; 11 | 12 | import static com.medicalhourmanagement.medicalhourmanagement.utils.enums.Permission.*; 13 | 14 | //Cualquier cambio hecho aca debe comprobarse en RoleConstants 15 | @RequiredArgsConstructor 16 | public enum Role { 17 | 18 | USER(Collections.emptySet()), 19 | ADMIN( 20 | Set.of( 21 | ADMIN_READ, 22 | ADMIN_UPDATE, 23 | ADMIN_DELETE, 24 | ADMIN_CREATE, 25 | MANAGER_READ, 26 | MANAGER_UPDATE, 27 | MANAGER_DELETE, 28 | MANAGER_CREATE 29 | ) 30 | ), 31 | MANAGER( 32 | Set.of( 33 | MANAGER_READ, 34 | MANAGER_UPDATE, 35 | MANAGER_DELETE, 36 | MANAGER_CREATE 37 | ) 38 | ) 39 | 40 | ; 41 | 42 | @Getter 43 | private final Set permissions; 44 | 45 | public List getAuthorities() { 46 | var authorities = getPermissions() 47 | .stream() 48 | .map(permission -> new SimpleGrantedAuthority(permission.getPermission())) 49 | .collect(Collectors.toList()); 50 | authorities.add(new SimpleGrantedAuthority("ROLE_" + this.name())); 51 | return authorities; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/utils/enums/TokenType.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.utils.enums; 2 | 3 | public enum TokenType { 4 | BEARER 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/medicalhourmanagement/medicalhourmanagement/utils/validators/PasswordConstraintValidator.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement.utils.validators; 2 | 3 | 4 | import com.medicalhourmanagement.medicalhourmanagement.utils.constraints.PasswordConstraint; 5 | import jakarta.validation.ConstraintValidator; 6 | import jakarta.validation.ConstraintValidatorContext; 7 | 8 | public class PasswordConstraintValidator implements ConstraintValidator { 9 | 10 | @Override 11 | public boolean isValid(String password, ConstraintValidatorContext context) { 12 | if (password == null) { 13 | return false; 14 | } 15 | // La contraseña debe tener al menos 6 caracteres y contener números y letras 16 | return password.length() >= 6 && password.matches("^(?=.*[a-zA-Z])(?=.*\\d).+$"); 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/resources/application-dev.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:h2:mem:medical_2;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE 2 | spring.datasource.username=sa 3 | spring.datasource.password=password 4 | spring.datasource.driverClassName=org.h2.Driver 5 | 6 | application.security.jwt.secret-key=${JWT_SECRET_KEY:adef760a77fd57aebbc322ad744ee640e35f32e5d304eccbac7810f7f6013721bac35e0261cd854860e5b432b0cf59e3c34260efafb16f18a802d9da4370ab3b} 7 | application.security.jwt.expiration=${JWT_ACCESS_EXPIRATION:400000000} 8 | application.security.jwt.refresh-token.expiration=${JWT_REFRESH_EXPIRATION:40000000000} 9 | 10 | 11 | logging.level.org.hibernate.SQL=DEBUG 12 | 13 | -------------------------------------------------------------------------------- /src/main/resources/application-prod.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=${DATABASE_URL} 2 | spring.datasource.username=${DATABASE_USERNAME} 3 | spring.datasource.password=${DATABASE_PASSWORD} 4 | 5 | application.security.jwt.secret-key=${JWT_SECRET_KEY} 6 | application.security.jwt.expiration=${JWT_ACCESS_EXPIRATION} 7 | application.security.jwt.refresh-token.expiration=${JWT_REFRESH_EXPIRATION} 8 | 9 | logging.level.root=WARN 10 | logging.level.com.medicalhourmanagement=INFO -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # Configuraciones comunes 2 | spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect 3 | spring.jpa.properties.hibernate.format_sql=true 4 | spring.jpa.hibernate.ddl-auto=update 5 | spring.sql.init.mode=never 6 | spring.profiles.active=${SPRING_ACTIVE_PROFILE:dev} 7 | 8 | -------------------------------------------------------------------------------- /src/test/java/com/medicalhourmanagement/medicalhourmanagement/AppointmentControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.controllers.AppointmentController; 4 | import com.medicalhourmanagement.medicalhourmanagement.dtos.AppointmentDTO; 5 | import com.medicalhourmanagement.medicalhourmanagement.dtos.request.RequestAppointmentDTO; 6 | import com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos.NotFoundException; 7 | import com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos.UnauthorizedAppointmentException; 8 | import com.medicalhourmanagement.medicalhourmanagement.services.AppointmentService; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.mockito.InjectMocks; 12 | import org.mockito.Mock; 13 | import org.mockito.MockitoAnnotations; 14 | import org.springframework.http.HttpStatus; 15 | import org.springframework.http.ResponseEntity; 16 | 17 | import java.util.List; 18 | 19 | import static org.junit.jupiter.api.Assertions.assertEquals; 20 | import static org.junit.jupiter.api.Assertions.assertNull; 21 | import static org.mockito.ArgumentMatchers.any; 22 | import static org.mockito.Mockito.*; 23 | 24 | class AppointmentControllerTest { 25 | 26 | @Mock 27 | private AppointmentService appointmentService; 28 | 29 | @InjectMocks 30 | private AppointmentController appointmentController; 31 | 32 | @BeforeEach 33 | void setUp() { 34 | MockitoAnnotations.openMocks(this); 35 | } 36 | 37 | @Test 38 | void testGetAppointments() { 39 | List appointments = List.of(new AppointmentDTO(), new AppointmentDTO()); 40 | when(appointmentService.getAppointments()).thenReturn(appointments); 41 | 42 | ResponseEntity> response = appointmentController.getAppointments(); 43 | 44 | assertEquals(HttpStatus.OK, response.getStatusCode()); 45 | assertEquals(2, response.getBody().size()); 46 | } 47 | 48 | @Test 49 | void testGetAppointmentById() { 50 | AppointmentDTO appointment = new AppointmentDTO(); 51 | when(appointmentService.getAppointmentById(any(Long.class))).thenReturn(appointment); 52 | 53 | ResponseEntity response = appointmentController.getAppointmentById(1L); 54 | 55 | assertEquals(HttpStatus.OK, response.getStatusCode()); 56 | assertEquals(appointment, response.getBody()); 57 | } 58 | 59 | @Test 60 | void testGetAppointmentByIdNotFound() { 61 | when(appointmentService.getAppointmentById(any(Long.class))).thenThrow(new NotFoundException("Appointment not found")); 62 | 63 | ResponseEntity response = appointmentController.getAppointmentById(1L); 64 | 65 | assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); 66 | assertNull(response.getBody()); 67 | } 68 | 69 | @Test 70 | void testCreateAppointment() { 71 | RequestAppointmentDTO requestAppointmentDTO = new RequestAppointmentDTO(); 72 | AppointmentDTO createdAppointment = new AppointmentDTO(); 73 | when(appointmentService.createAppointment(any(RequestAppointmentDTO.class))).thenReturn(createdAppointment); 74 | 75 | ResponseEntity response = appointmentController.createAppointment(requestAppointmentDTO); 76 | 77 | assertEquals(HttpStatus.CREATED, response.getStatusCode()); 78 | assertEquals(createdAppointment, response.getBody()); 79 | } 80 | 81 | @Test 82 | void testCreateAppointmentUnauthorized() { 83 | RequestAppointmentDTO requestAppointmentDTO = new RequestAppointmentDTO(); 84 | when(appointmentService.createAppointment(any(RequestAppointmentDTO.class))).thenThrow(new UnauthorizedAppointmentException("Unauthorized")); 85 | 86 | ResponseEntity response = appointmentController.createAppointment(requestAppointmentDTO); 87 | 88 | assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode()); 89 | assertNull(response.getBody()); 90 | } 91 | 92 | @Test 93 | void testCreateAppointmentInvalid() { 94 | RequestAppointmentDTO requestAppointmentDTO = new RequestAppointmentDTO(); 95 | when(appointmentService.createAppointment(any(RequestAppointmentDTO.class))).thenThrow(new IllegalArgumentException("Invalid data")); 96 | 97 | ResponseEntity response = appointmentController.createAppointment(requestAppointmentDTO); 98 | 99 | assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); 100 | assertNull(response.getBody()); 101 | } 102 | 103 | @Test 104 | void testUpdateAppointment() { 105 | AppointmentDTO appointmentDTO = new AppointmentDTO(); 106 | when(appointmentService.updateAppointment(any(Long.class), any(AppointmentDTO.class))).thenReturn(appointmentDTO); 107 | 108 | ResponseEntity response = appointmentController.updateAppointment(1L, appointmentDTO); 109 | 110 | assertEquals(HttpStatus.OK, response.getStatusCode()); 111 | assertEquals(appointmentDTO, response.getBody()); 112 | } 113 | 114 | @Test 115 | void testUpdateAppointmentNotFound() { 116 | AppointmentDTO appointmentDTO = new AppointmentDTO(); 117 | when(appointmentService.updateAppointment(any(Long.class), any(AppointmentDTO.class))).thenThrow(new NotFoundException("Appointment not found")); 118 | 119 | ResponseEntity response = appointmentController.updateAppointment(1L, appointmentDTO); 120 | 121 | assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); 122 | assertNull(response.getBody()); 123 | } 124 | 125 | @Test 126 | void testUpdateAppointmentUnauthorized() { 127 | AppointmentDTO appointmentDTO = new AppointmentDTO(); 128 | when(appointmentService.updateAppointment(any(Long.class), any(AppointmentDTO.class))).thenThrow(new UnauthorizedAppointmentException("Unauthorized")); 129 | 130 | ResponseEntity response = appointmentController.updateAppointment(1L, appointmentDTO); 131 | 132 | assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode()); 133 | assertNull(response.getBody()); 134 | } 135 | 136 | @Test 137 | void testDeleteAppointment() { 138 | doNothing().when(appointmentService).deleteAppointmentById(any(Long.class)); 139 | 140 | ResponseEntity response = appointmentController.deleteAppointment(1L); 141 | 142 | assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode()); 143 | } 144 | 145 | @Test 146 | void testDeleteAppointmentNotFound() { 147 | doThrow(new NotFoundException("Appointment not found")).when(appointmentService).deleteAppointmentById(any(Long.class)); 148 | 149 | ResponseEntity response = appointmentController.deleteAppointment(1L); 150 | 151 | assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); 152 | } 153 | 154 | @Test 155 | void testDeleteAppointmentUnauthorized() { 156 | doThrow(new UnauthorizedAppointmentException("Unauthorized")).when(appointmentService).deleteAppointmentById(any(Long.class)); 157 | 158 | ResponseEntity response = appointmentController.deleteAppointment(1L); 159 | 160 | assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode()); 161 | } 162 | } -------------------------------------------------------------------------------- /src/test/java/com/medicalhourmanagement/medicalhourmanagement/AuthenticationControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.medicalhourmanagement.medicalhourmanagement.dtos.request.AuthenticationRequestDTO; 5 | import com.medicalhourmanagement.medicalhourmanagement.dtos.request.RegisterRequestDTO; 6 | import com.medicalhourmanagement.medicalhourmanagement.dtos.response.AuthenticationResponseDTO; 7 | import com.medicalhourmanagement.medicalhourmanagement.security.services.JwtService; 8 | import com.medicalhourmanagement.medicalhourmanagement.services.impl.AuthenticationServiceImpl; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.mockito.Mockito; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.test.context.SpringBootTest; 14 | import org.springframework.boot.test.mock.mockito.MockBean; 15 | import org.springframework.http.HttpHeaders; 16 | import org.springframework.http.MediaType; 17 | import org.springframework.security.core.userdetails.UserDetails; 18 | import org.springframework.security.test.context.support.WithMockUser; 19 | import org.springframework.test.web.servlet.MockMvc; 20 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 21 | 22 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 23 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 24 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 25 | 26 | @SpringBootTest 27 | @AutoConfigureMockMvc 28 | public class AuthenticationControllerTest { 29 | 30 | @Autowired 31 | private MockMvc mockMvc; 32 | 33 | @MockBean 34 | private AuthenticationServiceImpl authenticationService; 35 | 36 | @Autowired 37 | private ObjectMapper objectMapper; 38 | 39 | private RegisterRequestDTO registerRequest; 40 | private AuthenticationRequestDTO authRequest; 41 | private AuthenticationResponseDTO authResponse; 42 | 43 | @Autowired 44 | private JwtService jwtService; 45 | 46 | @BeforeEach 47 | void setUp() { 48 | registerRequest = RegisterRequestDTO.builder() 49 | .firstname("John") 50 | .lastname("Doe") 51 | .email("john.doe@example.com") 52 | .password("Password123") 53 | .build(); 54 | 55 | authRequest = AuthenticationRequestDTO.builder() 56 | .email("john.doe@example.com") 57 | .password("Password123") 58 | .build(); 59 | 60 | authResponse = AuthenticationResponseDTO.builder() 61 | .accessToken("access-token") 62 | .refreshToken("refresh-token") 63 | .build(); 64 | } 65 | 66 | @Test 67 | public void testRegister() throws Exception { 68 | Mockito.when(authenticationService.register(Mockito.any(RegisterRequestDTO.class))) 69 | .thenReturn(authResponse); 70 | 71 | mockMvc.perform(post("/api/v1/auth/register") 72 | .contentType(MediaType.APPLICATION_JSON) 73 | .content(objectMapper.writeValueAsString(registerRequest))) 74 | .andExpect(status().isOk()) 75 | .andExpect(content().json(objectMapper.writeValueAsString(authResponse))); 76 | } 77 | 78 | @Test 79 | public void testAuthenticate() throws Exception { 80 | Mockito.when(authenticationService.authenticate(Mockito.any(AuthenticationRequestDTO.class))) 81 | .thenReturn(authResponse); 82 | 83 | mockMvc.perform(post("/api/v1/auth/authenticate") 84 | .contentType(MediaType.APPLICATION_JSON) 85 | .content(objectMapper.writeValueAsString(authRequest))) 86 | .andExpect(status().isOk()) 87 | .andExpect(content().json(objectMapper.writeValueAsString(authResponse))); 88 | } 89 | 90 | @Test 91 | @WithMockUser 92 | public void testRefreshToken() throws Exception { 93 | // Suponiendo que JwtService tiene un método generateRefreshToken que acepta un UserDetails 94 | UserDetails userDetails = Mockito.mock(UserDetails.class); 95 | Mockito.when(userDetails.getUsername()).thenReturn("john.doe@example.com"); 96 | 97 | String refreshToken = jwtService.generateRefreshToken(userDetails); 98 | 99 | Mockito.doNothing().when(authenticationService).refreshToken(Mockito.any(), Mockito.any()); 100 | 101 | mockMvc.perform(post("/api/v1/auth/refresh-token") 102 | .header(HttpHeaders.AUTHORIZATION, "Bearer " + refreshToken) 103 | .contentType(MediaType.APPLICATION_JSON)) 104 | .andExpect(status().isOk()); 105 | } 106 | } -------------------------------------------------------------------------------- /src/test/java/com/medicalhourmanagement/medicalhourmanagement/DoctorControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.controllers.DoctorController; 4 | import com.medicalhourmanagement.medicalhourmanagement.dtos.DoctorDTO; 5 | import com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos.NotFoundException; 6 | import com.medicalhourmanagement.medicalhourmanagement.services.DoctorService; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.mockito.InjectMocks; 10 | import org.mockito.Mock; 11 | import org.mockito.MockitoAnnotations; 12 | import org.springframework.http.HttpStatus; 13 | import org.springframework.http.ResponseEntity; 14 | 15 | import java.util.List; 16 | 17 | import static org.junit.jupiter.api.Assertions.assertEquals; 18 | import static org.junit.jupiter.api.Assertions.assertNull; 19 | import static org.mockito.ArgumentMatchers.any; 20 | import static org.mockito.Mockito.*; 21 | 22 | class DoctorControllerTest { 23 | 24 | @Mock 25 | private DoctorService doctorService; 26 | 27 | @InjectMocks 28 | private DoctorController doctorController; 29 | 30 | @BeforeEach 31 | void setUp() { 32 | MockitoAnnotations.openMocks(this); 33 | } 34 | 35 | @Test 36 | void testGetDoctors() { 37 | List doctors = List.of(new DoctorDTO(), new DoctorDTO()); 38 | when(doctorService.getDoctors()).thenReturn(doctors); 39 | 40 | ResponseEntity> response = doctorController.getDoctors(); 41 | 42 | assertEquals(HttpStatus.OK, response.getStatusCode()); 43 | assertEquals(2, response.getBody().size()); 44 | } 45 | 46 | @Test 47 | void testGetDoctorById() { 48 | DoctorDTO doctor = new DoctorDTO(); 49 | when(doctorService.getDoctorById(any(Long.class))).thenReturn(doctor); 50 | 51 | ResponseEntity response = doctorController.getDoctorById(1L); 52 | 53 | assertEquals(HttpStatus.OK, response.getStatusCode()); 54 | assertEquals(doctor, response.getBody()); 55 | } 56 | 57 | @Test 58 | void testGetDoctorByIdNotFound() { 59 | when(doctorService.getDoctorById(any(Long.class))).thenThrow(new NotFoundException("Doctor not found")); 60 | 61 | ResponseEntity response = doctorController.getDoctorById(1L); 62 | 63 | assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); 64 | assertNull(response.getBody()); 65 | } 66 | 67 | @Test 68 | void testSaveDoctor() { 69 | DoctorDTO doctor = new DoctorDTO(); 70 | when(doctorService.saveDoctor(any(DoctorDTO.class))).thenReturn(doctor); 71 | 72 | ResponseEntity response = doctorController.saveDoctor(doctor); 73 | 74 | assertEquals(HttpStatus.CREATED, response.getStatusCode()); 75 | assertEquals(doctor, response.getBody()); 76 | } 77 | 78 | @Test 79 | void testSaveDoctorInvalid() { 80 | DoctorDTO doctor = new DoctorDTO(); 81 | when(doctorService.saveDoctor(any(DoctorDTO.class))).thenThrow(new IllegalArgumentException("Invalid doctor data")); 82 | 83 | ResponseEntity response = doctorController.saveDoctor(doctor); 84 | 85 | assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); 86 | assertNull(response.getBody()); 87 | } 88 | 89 | @Test 90 | void testUpdateDoctor() { 91 | DoctorDTO doctor = new DoctorDTO(); 92 | when(doctorService.updateDoctor(any(Long.class), any(DoctorDTO.class))).thenReturn(doctor); 93 | 94 | ResponseEntity response = doctorController.updateDoctor(1L, doctor); 95 | 96 | assertEquals(HttpStatus.OK, response.getStatusCode()); 97 | assertEquals(doctor, response.getBody()); 98 | } 99 | 100 | @Test 101 | void testUpdateDoctorNotFound() { 102 | DoctorDTO doctor = new DoctorDTO(); 103 | when(doctorService.updateDoctor(any(Long.class), any(DoctorDTO.class))).thenThrow(new NotFoundException("Doctor not found")); 104 | 105 | ResponseEntity response = doctorController.updateDoctor(1L, doctor); 106 | 107 | assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); 108 | assertNull(response.getBody()); 109 | } 110 | 111 | @Test 112 | void testDeleteDoctorById() { 113 | doNothing().when(doctorService).deleteDoctorById(any(Long.class)); 114 | 115 | ResponseEntity response = doctorController.deleteDoctorById(1L); 116 | 117 | assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode()); 118 | } 119 | 120 | @Test 121 | void testDeleteDoctorByIdNotFound() { 122 | doThrow(new NotFoundException("Doctor not found")).when(doctorService).deleteDoctorById(any(Long.class)); 123 | 124 | ResponseEntity response = doctorController.deleteDoctorById(1L); 125 | 126 | assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); 127 | } 128 | } -------------------------------------------------------------------------------- /src/test/java/com/medicalhourmanagement/medicalhourmanagement/PatientControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.controllers.PatientController; 4 | import com.medicalhourmanagement.medicalhourmanagement.dtos.PatientDTO; 5 | import com.medicalhourmanagement.medicalhourmanagement.dtos.request.ChangePasswordRequestDTO; 6 | import com.medicalhourmanagement.medicalhourmanagement.services.PatientService; 7 | import com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos.NotFoundException; 8 | import com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos.UnauthorizedAppointmentException; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.mockito.InjectMocks; 12 | import org.mockito.Mock; 13 | import org.mockito.MockitoAnnotations; 14 | import org.springframework.http.HttpHeaders; 15 | import org.springframework.http.HttpStatus; 16 | import org.springframework.http.ResponseEntity; 17 | import org.springframework.mock.web.MockHttpServletRequest; 18 | import org.springframework.mock.web.MockHttpServletResponse; 19 | 20 | import jakarta.servlet.ServletException; 21 | 22 | import java.io.IOException; 23 | import java.security.Principal; 24 | import java.util.List; 25 | 26 | import static org.junit.jupiter.api.Assertions.*; 27 | import static org.mockito.ArgumentMatchers.any; 28 | import static org.mockito.Mockito.*; 29 | 30 | class PatientControllerTest { 31 | 32 | @Mock 33 | private PatientService patientService; 34 | 35 | @InjectMocks 36 | private PatientController patientController; 37 | 38 | @BeforeEach 39 | void setUp() { 40 | MockitoAnnotations.openMocks(this); 41 | } 42 | 43 | @Test 44 | void testGetPatients() { 45 | List patients = List.of(new PatientDTO(), new PatientDTO()); 46 | when(patientService.getPatients()).thenReturn(patients); 47 | 48 | ResponseEntity> response = patientController.getPatients(); 49 | 50 | assertEquals(HttpStatus.OK, response.getStatusCode()); 51 | assertEquals(2, response.getBody().size()); 52 | } 53 | 54 | @Test 55 | void testGetPatientById() { 56 | PatientDTO patient = new PatientDTO(); 57 | when(patientService.getPatientById(any(Long.class))).thenReturn(patient); 58 | 59 | ResponseEntity response = patientController.getPatientById(1L); 60 | 61 | assertEquals(HttpStatus.OK, response.getStatusCode()); 62 | assertEquals(patient, response.getBody()); 63 | } 64 | 65 | @Test 66 | void testGetPatientByIdNotFound() { 67 | when(patientService.getPatientById(any(Long.class))).thenThrow(new NotFoundException("Patient not found")); 68 | 69 | ResponseEntity response = patientController.getPatientById(1L); 70 | 71 | assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); 72 | } 73 | 74 | @Test 75 | void testSavePatient() { 76 | PatientDTO patient = new PatientDTO(); 77 | when(patientService.savePatient(any(PatientDTO.class))).thenReturn(patient); 78 | 79 | ResponseEntity response = patientController.savePatient(patient); 80 | 81 | assertEquals(HttpStatus.CREATED, response.getStatusCode()); 82 | assertEquals(patient, response.getBody()); 83 | } 84 | 85 | @Test 86 | void testSavePatientInvalid() { 87 | PatientDTO patient = new PatientDTO(); 88 | when(patientService.savePatient(any(PatientDTO.class))).thenThrow(new IllegalStateException("Failed to save patient")); 89 | 90 | IllegalStateException exception = assertThrows(IllegalStateException.class, () -> { 91 | patientController.savePatient(patient); 92 | }); 93 | 94 | assertEquals("Failed to save patient", exception.getMessage()); 95 | } 96 | 97 | @Test 98 | void testUpdatePatient() { 99 | PatientDTO patient = new PatientDTO(); 100 | when(patientService.updatePatient(any(Long.class), any(PatientDTO.class))).thenReturn(patient); 101 | 102 | ResponseEntity response = patientController.updatePatient(1L, patient); 103 | 104 | assertEquals(HttpStatus.OK, response.getStatusCode()); 105 | assertEquals(patient, response.getBody()); 106 | } 107 | 108 | @Test 109 | void testUpdatePatientNotFound() { 110 | when(patientService.updatePatient(any(Long.class), any(PatientDTO.class))).thenThrow(new NotFoundException("Patient not found")); 111 | 112 | ResponseEntity response = patientController.updatePatient(1L, new PatientDTO()); 113 | 114 | assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); 115 | } 116 | 117 | @Test 118 | void testDeletePatientById() { 119 | doNothing().when(patientService).deletePatientById(any(Long.class)); 120 | 121 | ResponseEntity response = patientController.deletePatientById(1L); 122 | 123 | assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode()); 124 | } 125 | 126 | @Test 127 | void testDeletePatientByIdNotFound() { 128 | doThrow(new NotFoundException("Patient not found")).when(patientService).deletePatientById(any(Long.class)); 129 | 130 | ResponseEntity response = patientController.deletePatientById(1L); 131 | 132 | assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); 133 | } 134 | 135 | @Test 136 | void testChangePassword() throws IOException, ServletException { 137 | MockHttpServletRequest request = new MockHttpServletRequest(); 138 | request.addHeader(HttpHeaders.AUTHORIZATION, "Bearer refresh-token"); 139 | MockHttpServletResponse response = new MockHttpServletResponse(); 140 | ChangePasswordRequestDTO changePasswordRequestDTO = new ChangePasswordRequestDTO(); 141 | Principal principal = mock(Principal.class); 142 | when(principal.getName()).thenReturn("test@example.com"); 143 | 144 | doNothing().when(patientService).changePassword(any(ChangePasswordRequestDTO.class), any(Principal.class)); 145 | 146 | ResponseEntity responseEntity = patientController.changePassword(changePasswordRequestDTO, principal); 147 | 148 | assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); 149 | verify(patientService, times(1)).changePassword(any(ChangePasswordRequestDTO.class), any(Principal.class)); 150 | } 151 | 152 | @Test 153 | void testChangePasswordInvalid() throws IOException, ServletException { 154 | ChangePasswordRequestDTO changePasswordRequestDTO = new ChangePasswordRequestDTO(); 155 | Principal principal = mock(Principal.class); 156 | when(principal.getName()).thenReturn("test@example.com"); 157 | 158 | doThrow(new IllegalStateException("Invalid password")).when(patientService).changePassword(any(ChangePasswordRequestDTO.class), any(Principal.class)); 159 | 160 | ResponseEntity responseEntity = patientController.changePassword(changePasswordRequestDTO, principal); 161 | 162 | assertEquals(HttpStatus.BAD_REQUEST, responseEntity.getStatusCode()); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/test/java/com/medicalhourmanagement/medicalhourmanagement/SpecialtyControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.medicalhourmanagement.medicalhourmanagement; 2 | 3 | import com.medicalhourmanagement.medicalhourmanagement.controllers.SpecialtyController; 4 | import com.medicalhourmanagement.medicalhourmanagement.dtos.SpecialtyDTO; 5 | import com.medicalhourmanagement.medicalhourmanagement.services.SpecialtyService; 6 | import com.medicalhourmanagement.medicalhourmanagement.exceptions.dtos.NotFoundException; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.mockito.InjectMocks; 10 | import org.mockito.Mock; 11 | import org.mockito.MockitoAnnotations; 12 | import org.springframework.http.HttpStatus; 13 | import org.springframework.http.ResponseEntity; 14 | 15 | import java.util.List; 16 | 17 | import static org.junit.jupiter.api.Assertions.assertEquals; 18 | import static org.junit.jupiter.api.Assertions.assertNull; 19 | import static org.mockito.ArgumentMatchers.any; 20 | import static org.mockito.Mockito.*; 21 | 22 | class SpecialtyControllerTest { 23 | 24 | @Mock 25 | private SpecialtyService specialtyService; 26 | 27 | @InjectMocks 28 | private SpecialtyController specialtyController; 29 | 30 | @BeforeEach 31 | void setUp() { 32 | MockitoAnnotations.openMocks(this); 33 | } 34 | 35 | @Test 36 | void testGetAllSpecialties() { 37 | List specialties = List.of(new SpecialtyDTO(), new SpecialtyDTO()); 38 | when(specialtyService.getAllSpecialties()).thenReturn(specialties); 39 | 40 | ResponseEntity> response = specialtyController.getAllSpecialties(); 41 | 42 | assertEquals(HttpStatus.OK, response.getStatusCode()); 43 | assertEquals(2, response.getBody().size()); 44 | } 45 | 46 | @Test 47 | void testCreateSpecialty() { 48 | SpecialtyDTO specialty = new SpecialtyDTO(); 49 | when(specialtyService.createSpecialty(any(SpecialtyDTO.class))).thenReturn(specialty); 50 | 51 | ResponseEntity response = specialtyController.createSpecialty(specialty); 52 | 53 | assertEquals(HttpStatus.CREATED, response.getStatusCode()); 54 | assertEquals(specialty, response.getBody()); 55 | } 56 | 57 | @Test 58 | void testCreateSpecialtyInvalid() { 59 | SpecialtyDTO specialty = new SpecialtyDTO(); 60 | when(specialtyService.createSpecialty(any(SpecialtyDTO.class))).thenThrow(new IllegalArgumentException("Invalid specialty data")); 61 | 62 | ResponseEntity response = specialtyController.createSpecialty(specialty); 63 | 64 | assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); 65 | assertNull(response.getBody()); 66 | } 67 | } --------------------------------------------------------------------------------