diff --git a/README.md b/README.md index 4405632..880a6b5 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# Flight Advisor Service ![GitHub release (latest by date)](https://img.shields.io/github/v/release/mohamed-taman/Flight-Advisor) [![Release Codename](https://img.shields.io/badge/codename-Advisor_4.0-yellow.svg)](https://github.com/mohamed-taman/Flight-Advisor/releases) [![Twitter Follow](https://img.shields.io/twitter/follow/_tamanm?label=follow%20me&style=social)](https://twitter.com/_tamanm) Flight advisor Service is a set of APIs for primarily finding the cheapest flight from city A to city B based on price, and as a result it's returning all the trip information alongside the distance(s). ## System Functionality - This project is developing a layered monolith **Spring Boot** based project (with the latest version **3.1.0-M2**), on Java 20, and with an embedded database mode. - This project is an OAuth2 based project, using JWT token to secure endpoints. So you need to register first to continue using the system. - The functionality is reached based on user role, and there are three roles in the system. - **Admin**: user is a predefined user (*admin@traveladvisor.com/Admin1234*). - Admin needs to log-in first through `/login` API to get their token to contact the system. - This user can upload airports and flight routes. - Admin manages cities by adding, updating, or deleting them. - Actually, the admin can do anything in the system. - **Client**: Clients should register first before using the system through `/register` public API. - After successful registration, they can then use the public `/login` API to get a token to contact the system successfully. - Client can use all read API calls. - Clients can add, manage their comments for a city, add, update, delete their comments, and see other comments. - Client can get the cheapest flight by calling `/cities/travel` API and provide airport codes for [from the city] and [to the city]. - **Public**: it is not a role, but anonymous users use APIs under public. - Use `/login` API call to log-in to the system, supplying the username and password. Then you will get a valid JWT token. - Use `/register` API call to register as a client to use the system functionalities related to the client, otherwise for he will receive *Not Authorized*. ## Getting started ### Project Management 1. I have used GitHub projects to manage my tasks in the **Flight-Advisor** project. [Project Link](https://github.com/mohamed-taman/Flight-Advisor/projects/1). 2. All MVP tasks are assigned to the **Flight Advisor API MVP** Milestone. [Milestone Link](https://github.com/mohamed-taman/Flight-Advisor/milestone/1?closed=1). 3. I used pull requests to manage and close assigned tasks. [Tasks Link](https://github.com/mohamed-taman/Flight-Advisor/issues?q=is%3Aclosed). 4. Finally, I have added releases to manage small features sprints until the final release v1.0. [Releases Link](https://github.com/mohamed-taman/Flight-Advisor/releases). 5. Have a look at opened issues for future enhancements. [Opened Issues](https://github.com/mohamed-taman/Flight-Advisor/issues?q=is%3Aopen). ### System components Structure Let's explain first the system layers structure to understand its components: ``` Flight-Advisor --> Parent folder. |- docs --> Contains system images. |- data --> Contains Airports and routes files. |- frontend --> Contains the frontend UI project. |- src/main/java - org.siriusxi.htec.fa (package) |- FlightAdvisorApplication.java --> The main starting point of the application. |- api --> Contains All REST API controllers that receive requests from the client, to process that request, and finally, return appropriate responses. It contains all request and response DTOs. |- repository --> All the database entities CRUD management services. |- domain --> Domain contains all the database modeled entities. |- infra --> Contains all the configurations, exceptions, security management, support utilities, and dto<-->entities mappers, for the system support. |- service --> Contains all the system business login, receives calls from Controllers, call repository to retrieve and manage data, then process them to return back to the controllers. ``` Now, as we have learned about different system layers and components, so it is the time then to play, let's play. ## Playing With Flight Advisor Service First things first, you need to download the following pieces of software to have fun with the project: ### Required software The following are the initially required software pieces: 1. **Maven**: it can be downloaded from https://maven.apache.org/download.cgi#. 2. **Git**: it can be downloaded from https://git-scm.com/downloads. 3. **Java 18.0.0**: it can be downloaded from https://www.oracle.com/java/technologies/downloads/#java18. 4. **Node.js 17.8+**: Latest features, and it can be downloaded from https://nodejs. org/en/download/current/. 5. **Angular CLI 12.2+**: Install it with the following command: `npm install -g @angular/cli@latest` Follow the installation guide for each software, on provided website link and check your software versions from the command line to verify that they are all installed correctly. ### Cloning It Now it is the time to open **terminal** or **git bash** command line, and then clone the project under any of your favorite places with the following command: ```bash > git clone https://github.com/mohamed-taman/Flight-Advisor.git ``` ### Using an IDE I recommend that you work with your Java code using an IDE that supports the development of Spring Boot applications such as **Spring Tool Suite** or **IntelliJ IDEA Community | Ultimate Edition**. All you have to do is fire up your favorite IDE **->** open or import the parent folder `Flight-Advisor,` and everything will be ready for you. ### Building & Running The System To build and run Flight Advisor (FA) system components, run the following command: #### Building FA Components ##### FA backend ```bash 👻 [mtaman]:Flight-Advisor ~~ ./mvnw clean package ``` Now you should expect output like this: ```JavaScript [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] [INFO] --- maven-jar-plugin:3.2.0:jar (default-jar) @ flight-advisor --- [INFO] Building jar: /Flight-Advisor/target/flight-advisor-3.0.jar [INFO] [INFO] --- spring-boot-maven-plugin:2.4.0:repackage (repackage) @ flight-advisor --- [INFO] Replacing main artifact with repackaged archive [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 14.620 s [INFO] Finished at: 2022-04-13T13:40:50+01:00 [INFO] ----------------------------------------------------------------------- ``` ##### FA frontend ```bash 👻 [mtaman]:Flight-Advisor ~~ cd frontend 👻 [mtaman]:frontend ~~ npm install && ng build ``` Now you should expect output like this: ```JavaScript removed 1 package and audited 1521 packages in 6.658s 85 packages are looking for funding run `npm fund` for details found 6 vulnerabilities (3 low, 1 moderate, 1 high, 1 critical) run `npm audit fix` to fix them, or `npm audit` for details ✔ Browser application bundle generation complete. ✔ Copying assets complete. ✔ Index html generation complete. Initial Chunk Files | Names | Size vendor.js | vendor | 4.37 MB scripts.js | scripts | 153.22 kB polyfills.js | polyfills | 149.77 kB styles.css | styles | 142.28 kB main.js | main | 13.32 kB runtime.js | runtime | 6.15 kB | Initial Total | 4.83 MB Build at: 2022-04-13T18:30:00.555Z - Hash: ccf039ad034c30696fdb - Time: 8608ms ``` #### Running the System Now it's the time to run the system, and it's straightforward, just hit the following commands: ##### FA backend ```bash 👻 [mtaman]:Flight-Advisor ~~ java --enable-preview -jar ./target/*.jar \ 👻 [mtaman]:Flight-Advisor ~+ --spring.profiles.active=prod ``` Or ```bash 👻 [mtaman]:Flight-Advisor ~~ ./mvnw spring-boot:run \ 👻 [mtaman]:Flight-Advisor ~+ -Dspring-boot.run.jvmArguments="--enable-preview" \ 👻 [mtaman]:Flight-Advisor ~+ -Dspring-boot.run.arguments="--spring.profiles.active=prod" ``` **Flight Advisor backend service** will run, with embedded H2 **database** that will be created under `db` folder and then the `flightDB.mv.db` file, and you should expect an output like this: ```javascript 2022-04-13 13:56:16.587 INFO 2981 --- [ restartedMain] o.s.b.a.h2.H2ConsoleAutoConfiguration: H2 console available at '/db-console'. Database available at 'jdbc:h2:./db/flightDB' 2022-04-13 13:56:18.081 INFO 2981 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer: Tomcat started on port(s): 8090 (http) with context path 'api/v1/flight/service' 2022-04-13 13:56:18.581 INFO 2981 --- [ restartedMain] o.s.h.f.F.AppStartupRunner: Congratulations, Flight Advisor Application, is Up & Running :) ``` ##### FA frontend ```bash 👻 [mtaman]:frontend ~~ npm start ``` **Flight Advisor UI** will run, and you should expect an output like this: ```javascript ** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ ** ✔ Compiled successfully. ``` ### Access Flight Advisor System APIs You can play and test `Flight Advisor` APIs throughout its **OpenAPI** interface. 1. Go to the landing page at the following URL [http://localhost:8090/api/v1/flight/service/] (http://localhost:8090/api/v1/flight/service/). 2. Follow the link on the page, and you should see the following: ![System APIs](docs/images/SystemAPI.png) 3. More beautifully with its UI at [http://localhost:4200/](http://localhost:4200/), and you expect this view for login: ![ClientLogin page](docs/images/FA-Login.png) And this view after login, to travel: ![System APIs](docs/images/FA-Travel.png) #### System Behaviour 1. First, if you want to upload airports or routes (in the data folder) using the **Files upload Management** section: 1. You need to log-in with the provided admin username/password to the `public/login` endpoint. 2. On a successful login, the response will contain an authorization token; copy it. 3. Click on the Authorize button and paste it in the only field out there, `value,` then click the `Authorize` button. 4. Now, all locks are closed, and you can use the secured APIs. 2. If you are a new client and want to access the system, you need first to register through the `/public/register` endpoint. Then follow previous point **1.1**. 3. When uploading the Airports file, countries and cities will be created automatically. 4. All search parameters are case-insensitive, and the system use like search by default. 5. To add a city, you need a country, so from the **country management** section, you can search for the country you want. 6. To manage comments, you need a city, so from the **city management** section, you can get all cities or search for a specific city. 7. You can search for all airports for a specific city to know their codes, so you can use travel service. 8. Use travel service to search for the cheapest flight from city to city, for example traveling from **CAI** (*Cairo International Airport, Egypt*) to **LAX** (*Los Angeles, USA*) the following results will be returned: ```JSON [ { "start": { "airport": "Cairo International Airport", "city": "Cairo", "country": "Egypt", "iata": "CAI" }, "through": [ { "airport": "Lester B. Pearson International Airport", "city": "Toronto", "country": "Canada", "iata": "YYZ" } ], "end": { "airport": "Los Angeles International Airport", "city": "Los Angeles", "country": "United States", "iata": "LAX" }, "price": { "total": 62.17, "currency": "US" }, "distance": { "total": 12722.2, "in": "KM" } } ] ``` ### Access Flight Advisor System Database You can access database through it online console from the following URL [http://localhost:8090/api/v1/flight/service/db-console/] (http://localhost:8090/api/v1/flight/service/db-console/) with the following properties: - Driver class: `org.h2.Driver` - JDBC URL: `jdbc:h2:./db/flightDB` - user: `sa` - password: `Admin1234` ![System DB](docs/images/SystemDB.png) Hit test, and it should show a green bar for successful settings. So hit the **Connect** button and explore all data. ### Stopping The System Just press the `CTRL+C` keys on the terminal. ### Closing The Story Finally, I hope you enjoyed the application and find it useful. If you would like to enhance, please open **PR**, and yet give it a 🌟. ## The End Happy Coding 😊 ## License Copyright (C) 2023 Mohamed Taman, Licensed under the **MIT License**. \ No newline at end of file +# Flight Advisor Service ![GitHub release (latest by date)](https://img.shields.io/github/v/release/mohamed-taman/Flight-Advisor) [![Release Codename](https://img.shields.io/badge/codename-Advisor_Fresh_5.0-yellow.svg)](https://github.com/mohamed-taman/Flight-Advisor/releases) [![Twitter Follow](https://img.shields.io/twitter/follow/_tamanm?label=follow%20me&style=social)](https://twitter.com/_tamanm) Flight Advisor Service is a set of APIs for primarily finding the cheapest flight from city A to City B is based on price, and as a result, it returns all the trip information alongside the distance(s). > This service is a great example of migrating spring boot from the older version **2.7.x** to **3.1.x**. You can check the changes from commit changes of each version release (as code) or release notes as information. ## System Functionality - This project is developing a layered monolith **Spring Boot** based project (with the latest version **3.1.0-RC1**), on **Java 20**, and with an embedded database mode. - This OAuth2-based project uses a JWT token to secure endpoints and a refresh token to get a new JWT token without login. - Therefore, you need to register first to continue using the system. - The functionality is reached based on user roles, and the system has three roles. - **Admin**: user is a predefined user (*admin@traveladvisor.com/Admin1234*). - Admin needs to log-in first through `/signin` API to get their token to contact the system. - This user can upload airports and flight routes. - Admin manages cities by adding, updating, or deleting them. - Actually, the admin can do anything in the system. - **Client**: Clients should register before using the system through `/signup` public API. - After successful registration, they can use the public `/signin` API to get a token to contact the system successfully. - Client can use all read API calls. - Clients can add, manage their comments for a city, add, update, delete their comments, and see other comments. - Client can get the cheapest flight by calling `/cities/travel` API and providing airport codes for [**from the city**] and [**to the city**]. - **Public**: it is not a role, but anonymous users use APIs under public. - Use `/signin` API call to log-in to the system, supplying the username and password. Then you will get a valid JWT token with a longer living refresh token. - Use `/signout` to logout from the system. - Use `/change_password` to change your current password so you will be logged-out, and you need to log-in again to access the system. - Use `/refresh_token` when your access token is expired to get new JWT access and refresh the token with the previous not expired refresh token without login into the system again. - Use `/signup` API call to register as a client to use the system functionalities related to the client; otherwise, the client will receive *Not Authorized*. ## Getting started ### Project Management 1. I have used GitHub projects to manage my tasks in the **Flight-Advisor** project. [Project Link](https://github.com/mohamed-taman/Flight-Advisor/projects/1). 2. All MVP tasks are assigned to the **Flight Advisor API MVP** Milestone. [Milestone Link](https://github.com/mohamed-taman/Flight-Advisor/milestone/1?closed=1). 3. I used pull requests to manage and close assigned tasks. [Tasks Link](https://github.com/mohamed-taman/Flight-Advisor/issues?q=is%3Aclosed). 4. Finally, I have added releases to manage small feature sprints until the final release, v1.0. [Releases Link](https://github.com/mohamed-taman/Flight-Advisor/releases). 5. Have a look at open issues for future enhancements. [Opened Issues](https://github.com/mohamed-taman/Flight-Advisor/issues?q=is%3Aopen). ### System components Structure Let's first explain the system layers structure to understand its components: ``` Flight-Advisor --> Parent folder. |- docs --> Contains system images. |- data --> Contains Airports and routes files. |- frontend --> Contains the frontend UI project. |- src/main/java - org.siriusxi.fa (package) |- FlightAdvisorApplication.java --> The main starting point of the application. |- api --> Contains All REST API controllers that receive requests from the client to process that request and, finally, return appropriate responses. It contains all request and response DTOs. |- repository --> All the database entities CRUD management services. |- domain --> Domain contains all the database-modeled entities. |- infra --> Contains all the configurations, exceptions, security management, support utilities, and dto <--> entities mappers for the system support. |- service --> Contains all the system business login, receives calls from Controllers, calls the repository to retrieve and manage data, then processes them to return to the controllers. ``` Now, as we have learned about different system layers and components, so it is time then to play, let's play. ## Playing With Flight Advisor Service First things first, you need to download the following pieces of software to have fun with the project: ### Required software The following are the initially required software pieces: 1. **Maven**: it can be downloaded from https://maven.apache.org/download.cgi#. 2. **Git**: it can be downloaded from https://git-scm.com/downloads. 3. **Java 20.0.1**: it can be downloaded from https://www.oracle.com/java/technologies/downloads/#java20. 4. **Node.js 17.8+**: Latest features, can be downloaded from https://nodejs.org/en/download/current/. 5. **Angular CLI 12.2+**: Install it with the following command: `npm install -g @angular/cli@latest` Follow the installation guide for each software on provided website link and check your software versions from the command line to verify that they are all installed correctly. ### Cloning It Now it is the time to open **terminal** or **git bash** command line, and then clone the project under any of your favorite places with the following command: ```bash > git clone https://github.com/mohamed-taman/Flight-Advisor.git ``` ### Using an IDE I recommend that you work with your Java code using an IDE that supports the development of Spring Boot applications such as **Spring Tool Suite** or **IntelliJ IDEA Community | Ultimate Edition**. All you have to do is fire up your favorite IDE **->** open or import the parent folder `Flight-Advisor,` everything will be ready for you. ### Building & Running The System To build and run Flight Advisor (FA) system components, run the following command: #### Building FA Components ##### FA backend ```bash 👻 [mtaman]:Flight-Advisor ~~ ./mvnw clean package ``` Now you should expect output like this: ```JavaScript [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] [INFO] --- maven-jar-plugin:3.2.0:jar (default-jar) @ flight-advisor --- [INFO] Building jar: /Flight-Advisor/target/flight-advisor-3.0.jar [INFO] [INFO] --- spring-boot-maven-plugin:2.4.0:repackage (repackage) @ flight-advisor --- [INFO] Replacing main artifact with repackaged archive [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 14.620 s [INFO] Finished at: 2022-04-13T13:40:50+01:00 [INFO] ----------------------------------------------------------------------- ``` ##### FA frontend ```bash 👻 [mtaman]:Flight-Advisor ~~ cd frontend 👻 [mtaman]:frontend ~~ npm install && ng build ``` Now you should expect output like this: ```JavaScript removed 1 package and audited 1521 packages in 6.658s 85 packages are looking for funding run `npm fund` for details found 6 vulnerabilities (3 low, 1 moderate, 1 high, 1 critical) run `npm audit fix` to fix them, or `npm audit` for details ✔ Browser application bundle generation complete. ✔ Copying assets complete. ✔ Index html generation complete. Initial Chunk Files | Names | Size vendor.js | vendor | 4.37 MB scripts.js | scripts | 153.22 kB polyfills.js | polyfills | 149.77 kB styles.css | styles | 142.28 kB main.js | main | 13.32 kB runtime.js | runtime | 6.15 kB | Initial Total | 4.83 MB Build at: 2022-04-13T18:30:00.555Z - Hash: ccf039ad034c30696fdb - Time: 8608ms ``` #### Running the System Now it's the time to run the system, and it's straightforward, just hit the following commands: ##### FA backend ```bash 👻 [mtaman]:Flight-Advisor ~~ java --enable-preview -jar ./target/*.jar \ 👻 [mtaman]:Flight-Advisor ~+ --spring.profiles.active=prod ``` Or ```bash 👻 [mtaman]:Flight-Advisor ~~ ./mvnw spring-boot:run \ 👻 [mtaman]:Flight-Advisor ~+ -Dspring-boot.run.jvmArguments="--enable-preview" \ 👻 [mtaman]:Flight-Advisor ~+ -Dspring-boot.run.arguments="--spring.profiles.active=prod" ``` **Flight Advisor backend service** will run, with embedded H2 **database** that will be created under `db` folder and then the `flightDB.mv.db` file, and you should expect an output like this: ```javascript 2022-04-13 13:56:16.587 INFO 2981 --- [ restartedMain] o.s.b.a.h2.H2ConsoleAutoConfiguration: H2 console available at '/db-console'. Database available at 'jdbc:h2:./db/flightDB' 2022-04-13 13:56:18.081 INFO 2981 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer: Tomcat started on port(s): 8090 (http) with context path 'api/v1/flight/service' 2022-04-13 13:56:18.581 INFO 2981 --- [ restartedMain] o.s.h.f.F.AppStartupRunner: Congratulations, Flight Advisor Application, is Up & Running :) ``` ##### FA frontend ```bash 👻 [mtaman]:frontend ~~ npm start ``` **Flight Advisor UI** will run, and you should expect an output like this: ```javascript ** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ ** ✔ Compiled successfully. ``` ### Access Flight Advisor System APIs You can play and test `Flight Advisor` APIs throughout its **OpenAPI** interface. 1. Go to the landing page at the following URL [http://localhost:8090/api/v1/flight/service/] (http://localhost:8090/api/v1/flight/service/). 2. Follow the link on the page, and you should see the following: ![System APIs](docs/images/SystemAPI.png) 3. More beautifully with its UI at [http://localhost:4200/](http://localhost:4200/), and you expect this view for login: ![ClientLogin page](docs/images/FA-Login.png) And this view after login, to travel: ![System APIs](docs/images/FA-Travel.png) #### System Behaviour 1. First, if you want to upload airports or routes (in the data folder) using the **Files upload Management** section: 1. Log-in with the provided admin username/password to the `auth/signin` endpoint. 2. On successful login, the response will contain an authorization token; copy it. 3. Click on the Authorize button and paste it into the only field out there, `value,` then click the `Authorize` button. 4. All locks are closed, and you can use the secured APIs. 2. If you are a new client and want to access the system, you must first register through the `/auth/signup` endpoint. Then follow the previous point **1.1**. 3. When uploading the Airports file, countries and cities will be created automatically. 4. All search parameters are case-insensitive, and the system uses a `like` search by default. 5. To add a city, you need a country, so from the **country management** section, you can search for the country you want. 6. To manage comments, you need a city, so from the **city management** section, you can get all cities or search for a specific city. 7. You can search for all airports in a specific city to know their codes so that you can use travel services. 8. Use a travel service to search for the cheapest flight from city to city, for example traveling from **CAI** (*Cairo International Airport, Egypt*) to **LAX** (*Los Angeles, USA*); the following results will be returned: ```JSON [ { "start": { "airport": "Cairo International Airport", "city": "Cairo", "country": "Egypt", "iata": "CAI" }, "through": [ { "airport": "Lester B. Pearson International Airport", "city": "Toronto", "country": "Canada", "iata": "YYZ" } ], "end": { "airport": "Los Angeles International Airport", "city": "Los Angeles", "country": "United States", "iata": "LAX" }, "price": { "total": 62.17, "currency": "US" }, "distance": { "total": 12722.2, "in": "KM" } } ] ``` ### Access Flight Advisor System Database You can access the database through its online console from the following URL [http://localhost:8090/api/v1/flight/service/db-console/] (http://localhost:8090/api/v1/flight/service/db-console/) with the following properties: - Driver class: `org.h2.Driver` - JDBC URL: `jdbc:h2:./db/flightDB` - user: `sa` - password: `Admin1234` ![System DB](docs/images/SystemDB.png) Hit test, and it should show a green bar for successful settings. So hit the **Connect** button and explore all data. ### Stopping The System Just press the `CTRL+C` keys on the terminal. ### Closing The Story Finally, I hope you enjoyed the application and found it useful. If you would like to enhance it, please open **PR**, and yet give it a 🌟. ## The End Happy Coding 😊 ## License Copyright (C) 2023 Mohamed Taman, Licensed under the **MIT License**. \ No newline at end of file diff --git a/docs/images/SystemAPI.png b/docs/images/SystemAPI.png index d35d19c..b44c359 100644 Binary files a/docs/images/SystemAPI.png and b/docs/images/SystemAPI.png differ diff --git a/pom.xml b/pom.xml index ce2f02e..1ce0b33 100644 --- a/pom.xml +++ b/pom.xml @@ -6,13 +6,13 @@ org.springframework.boot spring-boot-starter-parent - 3.1.0-M2 + 3.1.0-RC2 - org.siriusxi.htec + org.siriusxi.fa flight-advisor - 3.0 + 5.0 Flight Advisor API Flight Advisor API Spring Boot based Application. jar @@ -58,10 +58,10 @@ - 20 - 20 - 20 20 + ${java.version} + ${java.version} + ${java.version} UTF-8 UTF-8 @@ -69,7 +69,7 @@ 3.0.0 3.0.0 0.11.5 - 1.5.3.Final + 1.5.4.Final 2.1.0 5.7.1 1.0.1 @@ -114,6 +114,12 @@ org.springdoc springdoc-openapi-starter-webmvc-ui ${org.springdoc.version} + + + org.yaml + snakeyaml + + @@ -180,8 +186,7 @@ ${org.mapstruct.version} - + com.opencsv opencsv diff --git a/src/main/java/org/siriusxi/htec/fa/FlightAdvisorApplication.java b/src/main/java/org/siriusxi/fa/FlightAdvisorApplication.java similarity index 95% rename from src/main/java/org/siriusxi/htec/fa/FlightAdvisorApplication.java rename to src/main/java/org/siriusxi/fa/FlightAdvisorApplication.java index 5d1cc96..1352eaa 100644 --- a/src/main/java/org/siriusxi/htec/fa/FlightAdvisorApplication.java +++ b/src/main/java/org/siriusxi/fa/FlightAdvisorApplication.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa; +package org.siriusxi.fa; import lombok.extern.log4j.Log4j2; import org.springframework.boot.ApplicationRunner; diff --git a/src/main/java/org/siriusxi/fa/api/controller/AuthController.java b/src/main/java/org/siriusxi/fa/api/controller/AuthController.java new file mode 100644 index 0000000..c7dba6a --- /dev/null +++ b/src/main/java/org/siriusxi/fa/api/controller/AuthController.java @@ -0,0 +1,212 @@ +package org.siriusxi.fa.api.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.siriusxi.fa.api.model.request.AuthRequest; +import org.siriusxi.fa.api.model.request.ChangePasswordRequest; +import org.siriusxi.fa.api.model.request.CreateUserRequest; +import org.siriusxi.fa.api.model.request.TokenRefreshRequest; +import org.siriusxi.fa.api.model.response.TokenRefreshResponse; +import org.siriusxi.fa.api.model.response.UserResponse; +import org.siriusxi.fa.domain.User; +import org.siriusxi.fa.infra.exception.NotAllowedException; +import org.siriusxi.fa.infra.exception.RefreshTokenException; +import org.siriusxi.fa.service.UserService; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.HttpClientErrorException; + +import java.util.Optional; + +import static org.siriusxi.fa.infra.security.jwt.JwtTokenHelper.generateJwtToken; +import static org.springframework.http.HttpStatus.UNAUTHORIZED; + +/** + * Authentication controller used to handle users authentication. + * + * @author Mohamed Taman + * @version 1.0 + */ + +@Log4j2 +@Tag(name = "Authentication", + description = "A set of public APIs, for managing user authentication, and the registration.") +@RestController +@RequestMapping("auth") +@RequiredArgsConstructor +public class AuthController { + + private final AuthenticationManager authenticationManager; + private final UserService userService; + + @Operation(description = """ + An API call to authenticate user before using the system, + and if successful a valid token is returned. + """) + @PostMapping(value = "signin") + public ResponseEntity authenticate(@RequestBody @Valid AuthRequest request) { + try { + var authenticate = this.authenticationManager + .authenticate(new UsernamePasswordAuthenticationToken( + request.username(), + request.password())); + + User user = (User) authenticate.getPrincipal(); + + // Generate refresh token + user = this.userService.generateRefreshToken(user); + + return ResponseEntity.ok() + .header(HttpHeaders.AUTHORIZATION, + generateJwtToken( + user.getId(), + user.getUsername())) + .body(this.userService.mapper().toView(user)); + } catch (BadCredentialsException ex) { + throw new HttpClientErrorException(UNAUTHORIZED, UNAUTHORIZED.getReasonPhrase()); + } + } + + @Operation(description = """ + An API call, to register the user, + to be able to authenticate and use the system. + """) + @PostMapping(value = "signup") + public UserResponse register(@RequestBody @Valid CreateUserRequest userRequest) { + + log.debug("User to be created: {}", userRequest); + return this.userService.create(userRequest); + } + + + /** + *
+     *   +--------+                                           +---------------+
+     *   |        |--(A)------- Authorization Grant --------->|               |
+     *   |        |                                           |               |
+     *   |        |<-(B)----------- Access Token -------------|               |
+     *   |        |               & Refresh Token             |               |
+     *   |        |                                           |               |
+     *   |        |                            +----------+   |               |
+     *   |        |--(C)---- Access Token ---->|          |   |               |
+     *   |        |                            |          |   |               |
+     *   |        |<-(D)- Protected Resource --| Resource |   | Authorization |
+     *   | Client |                            |  Server  |   |     Server    |
+     *   |        |--(E)---- Access Token ---->|          |   |               |
+     *   |        |                            |          |   |               |
+     *   |        |<-(F)- Invalid Token Error -|          |   |               |
+     *   |        |                            +----------+   |               |
+     *   |        |                                           |               |
+     *   |        |--(G)----------- Refresh Token ----------->|               |
+     *   |        |                                           |               |
+     *   |        |<-(H)----------- Access Token -------------|               |
+     *   +--------+           & Optional Refresh Token        +---------------+
+     * 
+ *
    + *
  • (A) The client requests an access token by authenticating with the + * authorization server and presenting an authorization grant.
  • + * + *
  • (B) The authorization server authenticates the client and validates + * the authorization grant, and if valid, issues an access token + * and a refresh token.
  • + * + *
  • (C) The client makes a protected resource request to the resource + * server by presenting the access token.
  • + * + *
  • (D) The resource server validates the access token, and if valid, + * serves the request.
  • + * + *
  • (E) Steps (C) and (D) repeat until the access token expires. If the + * client knows the access token expired, it skips to step (G); + * otherwise, it makes another protected resource request.
  • + * + *
  • (F) Since the access token is invalid, the resource server returns + * an invalid token error.
  • + * + *
  • (G) The client requests a new access token by authenticating with + * the authorization server and presenting the refresh token. The + * client authentication requirements are based on the client type + * and on the authorization server policies.
  • + * + *
  • (H) The authorization server authenticates the client and validates + * the refresh token, and if valid, issues a new access token (and, + * optionally, a new refresh token).
  • + *
+ * + * @param tokenRefreshRequest the request for generating a new token, and refresh token. + * @return the new access token, and a new refresh token if not expired. + */ + @Operation(description = """ + An API call, to refresh user JWT token without signing again, + to be able to continue be authenticated and use the system. + """) + @PostMapping(value = "refresh_token") + public TokenRefreshResponse refreshToken(@RequestBody @Valid TokenRefreshRequest tokenRefreshRequest) { + log.debug("Token to be Refreshed: {}", tokenRefreshRequest); + + return this.userService + .findByRefreshToken(tokenRefreshRequest.refreshToken()) + .map(this.userService::verifyRefreshTokenExpiration) + .map(this.userService::generateRefreshToken) + .map(user -> + new TokenRefreshResponse(generateJwtToken( + user.getId(), + user.getUsername()), user.getRefreshToken().toString()) + ) + .orElseThrow(() -> new RefreshTokenException(tokenRefreshRequest.refreshToken(), "Invalid refresh token, can't generate a new one!")); + } + + @Operation(description = """ + An API call, to logout the user, + But has to re-authenticate again to access the system. + """, + security = {@SecurityRequirement(name = "bearer-key")}) + @PostMapping("/signout") + public ResponseEntity logoutUser() { + getUserFromSecurityContext() + .ifPresentOrElse(user -> { + this.userService.invalidateRefreshTokenById(user.getId()); + SecurityContextHolder.getContext().setAuthentication(null); + }, () -> { + throw new NotAllowedException("Can't logout, please signin!"); + }); + + return ResponseEntity.ok("Logged out successful!"); + } + + @Operation(description = """ + An API call, to change user password, + But user has to re-authenticate again to access the system with new password. + """, + security = {@SecurityRequirement(name = "bearer-key")}) + @PostMapping("/change_password") + public ResponseEntity changePassword(@RequestBody @Valid ChangePasswordRequest changePassRequest) { + getUserFromSecurityContext() + .ifPresentOrElse(user -> { + this.userService.updatePassword(user.getId(), changePassRequest); + SecurityContextHolder.getContext().setAuthentication(null); + }, () -> { + throw new NotAllowedException("Can't change password, please signin!"); + }); + + return ResponseEntity.ok("Password changed successful!"); + } + + private Optional getUserFromSecurityContext() { + var auth = SecurityContextHolder.getContext().getAuthentication(); + return auth != null && auth.getPrincipal() instanceof User user ? Optional.of(user) : Optional.empty(); + } + +} diff --git a/src/main/java/org/siriusxi/htec/fa/api/controller/CityController.java b/src/main/java/org/siriusxi/fa/api/controller/CityController.java similarity index 56% rename from src/main/java/org/siriusxi/htec/fa/api/controller/CityController.java rename to src/main/java/org/siriusxi/fa/api/controller/CityController.java index 0aca0fa..dd991f1 100644 --- a/src/main/java/org/siriusxi/htec/fa/api/controller/CityController.java +++ b/src/main/java/org/siriusxi/fa/api/controller/CityController.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.api.controller; +package org.siriusxi.fa.api.controller; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -10,17 +10,17 @@ import jakarta.validation.constraints.Size; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; -import org.siriusxi.htec.fa.api.model.request.CommentUpSrtRequest; -import org.siriusxi.htec.fa.api.model.request.CreateCityRequest; -import org.siriusxi.htec.fa.api.model.request.SearchAirportRequest; -import org.siriusxi.htec.fa.api.model.request.SearchCityRequest; -import org.siriusxi.htec.fa.api.model.response.AirportView; -import org.siriusxi.htec.fa.api.model.response.CityView; -import org.siriusxi.htec.fa.api.model.response.CommentView; -import org.siriusxi.htec.fa.api.model.response.TripView; -import org.siriusxi.htec.fa.domain.User; -import org.siriusxi.htec.fa.service.CityMgmtService; -import org.siriusxi.htec.fa.service.TravelService; +import org.siriusxi.fa.api.model.request.CommentUpSrtRequest; +import org.siriusxi.fa.api.model.request.CreateCityRequest; +import org.siriusxi.fa.api.model.request.SearchAirportRequest; +import org.siriusxi.fa.api.model.request.SearchCityRequest; +import org.siriusxi.fa.api.model.response.AirportResponse; +import org.siriusxi.fa.api.model.response.CityResponse; +import org.siriusxi.fa.api.model.response.CommentResponse; +import org.siriusxi.fa.api.model.response.TripResponse; +import org.siriusxi.fa.domain.User; +import org.siriusxi.fa.service.CityMgmtService; +import org.siriusxi.fa.service.TravelService; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; @@ -35,6 +35,7 @@ @Log4j2 @Tag(name = "City Management", description = "A set of authorized APIs, for getting and managing system cities.") +@SecurityRequirement(name = "bearer-key") @RestController @RequestMapping("cities") @RequiredArgsConstructor @@ -44,22 +45,20 @@ public class CityController { private final TravelService travelService; @Operation(summary = "Get all cities.", - description = "Get city or all cities. You can limit the # of returned comments.", - security = {@SecurityRequirement(name = "bearer-key")}) + description = "Get city or all cities. You can limit the # of returned comments.") @GetMapping - public List getAllCities(@RequestParam(defaultValue = "0") - @Min(0) @Max(Integer.MAX_VALUE) int cLimit) { - return cityMgmtService.searchCities(new SearchCityRequest(""), cLimit); + public List getAllCities(@RequestParam(defaultValue = "0") + @Min(0) @Max(Integer.MAX_VALUE) int cLimit) { + return this.cityMgmtService.searchCities(new SearchCityRequest(""), cLimit); } @Operation(summary = "Search all cities by name.", - description = "Find city or all cities by name. You can limit the # of returned comments.", - security = {@SecurityRequirement(name = "bearer-key")}) + description = "Find city or all cities by name. You can limit the # of returned comments.") @PostMapping("search") - public List searchCities(@RequestParam(defaultValue = "0") - @Min(0) @Max(Integer.MAX_VALUE) int cLimit, - @RequestBody @Valid SearchCityRequest request) { - return cityMgmtService.searchCities(request, cLimit); + public List searchCities(@RequestParam(defaultValue = "0") + @Min(0) @Max(Integer.MAX_VALUE) int cLimit, + @RequestBody @Valid SearchCityRequest request) { + return this.cityMgmtService.searchCities(request, cLimit); } @@ -70,20 +69,18 @@ public List searchCities(@RequestParam(defaultValue = "0") 1. by its id if it is already exist in the system, or 2. by country name if not exist, then the system will \ creat the country and attach it to the city. - """, - security = {@SecurityRequirement(name = "bearer-key")}) + """) @PostMapping - public CityView createCity(@RequestBody @Valid CreateCityRequest request) { - return cityMgmtService.addCity(request); + public CityResponse createCity(@RequestBody @Valid CreateCityRequest request) { + return this.cityMgmtService.addCity(request); } @Operation(summary = "Exited! and wanna travel.", - description = "Find the cheapest trip from source country to a destination country.", - security = {@SecurityRequirement(name = "bearer-key")}) + description = "Find the cheapest trip from source country to a destination country.") @GetMapping("travel") - public List travel(@RequestParam @Size(min = 3) String from, - @RequestParam @Size(min = 3) String to) { + public List travel(@RequestParam @Size(min = 3) String from, + @RequestParam @Size(min = 3) String to) { if (from.isBlank() || to.isBlank()) throw new IllegalArgumentException(""" @@ -95,7 +92,7 @@ public List travel(@RequestParam @Size(min = 3) String from, throw new IllegalArgumentException(String.format( "You are traveling from and to the same destination [%s]", to)); - return travelService + return this.travelService .travel(from.trim().toUpperCase(), to.trim().toUpperCase()); } @@ -103,24 +100,22 @@ public List travel(@RequestParam @Size(min = 3) String from, Airport Management */ @Operation(summary = "Find city airports.", - description = "Find all airports for a specific city.", - security = {@SecurityRequirement(name = "bearer-key")}) + description = "Find all airports for a specific city.") @PostMapping("{id}/airports") - public List searchAirports(@Parameter(description = "City Id") - @PathVariable(name = "id") - @Min(1) @Max(Integer.MAX_VALUE) int cityId, - @RequestBody @Valid SearchAirportRequest request) { - return cityMgmtService.searchAirports(request, cityId); + public List searchAirports(@Parameter(description = "City Id") + @PathVariable(name = "id") + @Min(1) @Max(Integer.MAX_VALUE) int cityId, + @RequestBody @Valid SearchAirportRequest request) { + return this.cityMgmtService.searchAirports(request, cityId); } @Operation(summary = "Get all airports by a any name.", - description = "Find all airports by airport, city or country name.", - security = {@SecurityRequirement(name = "bearer-key")}) + description = "Find all airports by airport, city or country name.") @GetMapping("/airports") - public List searchForCityOrCountryAirports( + public List searchForCityOrCountryAirports( @Parameter(description = "Airport, city or country name") @RequestParam @Size(min = 1) String name) { - return travelService.findAirportsForCityOrCountry(name); + return this.travelService.findAirportsForCityOrCountry(name); } /* @@ -129,20 +124,18 @@ public List searchForCityOrCountryAirports( //Add comment @Operation(summary = "Add a city comment.", - description = "Wanna add a comment to a city you have visited.", - security = {@SecurityRequirement(name = "bearer-key")}) + description = "Wanna add a comment to a city you have visited.") @PostMapping("{id}/comments") - public CommentView addComment(@Parameter(description = "City Id") - @PathVariable(name = "id") - @Min(1) @Max(Integer.MAX_VALUE) int cityId, - @RequestBody @Valid CommentUpSrtRequest request) { - return cityMgmtService.addComment(getCurrentLoginUser(), cityId, request); + public CommentResponse addComment(@Parameter(description = "City Id") + @PathVariable(name = "id") + @Min(1) @Max(Integer.MAX_VALUE) int cityId, + @RequestBody @Valid CommentUpSrtRequest request) { + return this.cityMgmtService.addComment(getCurrentLoginUser(), cityId, request); } //update comment @Operation(summary = "Update my city comment.", - description = "Wanna change your comment to a city you have visited.", - security = {@SecurityRequirement(name = "bearer-key")}) + description = "Wanna change your comment to a city you have visited.") @PutMapping("{id}/comments/{cid}") public void updateComment(@Parameter(description = "City Id") @PathVariable(name = "id") @@ -152,20 +145,19 @@ public void updateComment(@Parameter(description = "City Id") @Min(1) @Max(Integer.MAX_VALUE) int commentId, @RequestBody @Valid CommentUpSrtRequest request) { - cityMgmtService.updateComment(getCurrentLoginUser(), cityId, commentId, request); + this.cityMgmtService.updateComment(getCurrentLoginUser(), cityId, commentId, request); } //Delete comment @Operation(summary = "Delete my city comment.", - description = "Changed your mind, don't like your comment then delete it.", - security = {@SecurityRequirement(name = "bearer-key")}) + description = "Changed your mind, don't like your comment then delete it.") @DeleteMapping("{id}/comments/{cid}") public void deleteComment(@Parameter(description = "City Id") @PathVariable(name = "id") @Min(1) @Max(Integer.MAX_VALUE) int cityId, @Parameter(description = "Comment Id") @PathVariable("cid") @Min(1) @Max(Integer.MAX_VALUE) int commentId) { - cityMgmtService.deleteComment(getCurrentLoginUser(), cityId, commentId); + this.cityMgmtService.deleteComment(getCurrentLoginUser(), cityId, commentId); } private User getCurrentLoginUser() { diff --git a/src/main/java/org/siriusxi/htec/fa/api/controller/CountryController.java b/src/main/java/org/siriusxi/fa/api/controller/CountryController.java similarity index 57% rename from src/main/java/org/siriusxi/htec/fa/api/controller/CountryController.java rename to src/main/java/org/siriusxi/fa/api/controller/CountryController.java index 683f83b..d839816 100644 --- a/src/main/java/org/siriusxi/htec/fa/api/controller/CountryController.java +++ b/src/main/java/org/siriusxi/fa/api/controller/CountryController.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.api.controller; +package org.siriusxi.fa.api.controller; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; @@ -6,9 +6,9 @@ import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; -import org.siriusxi.htec.fa.api.model.response.CountryView; -import org.siriusxi.htec.fa.infra.mapper.CountryMapper; -import org.siriusxi.htec.fa.repository.CountryRepository; +import org.siriusxi.fa.api.model.response.CountryResponse; +import org.siriusxi.fa.infra.mapper.CountryMapper; +import org.siriusxi.fa.repository.CountryRepository; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -17,16 +17,16 @@ import java.util.Set; /** - * City controller used to handle cities API functions. + * Country controller used to handle countries API functions. * * @author Mohamed Taman * @version 1.0 */ -// TODO: Add Swagger documentation @Log4j2 @Tag(name = "Country Management", description = "A set of authorized APIs, for getting and managing system countries.") +@SecurityRequirement(name = "bearer-key") @RestController @RequestMapping("countries") @RequiredArgsConstructor @@ -35,19 +35,21 @@ public class CountryController { private final CountryRepository countryRepository; private final CountryMapper mapper; - @Operation(security = {@SecurityRequirement(name = "bearer-key")}) + @Operation(summary = "Get all countries.", + description = "Get Country or all countries.") @GetMapping - public Set getAllCountries() { - return mapper - .toViews(countryRepository + public Set getAllCountries() { + return this.mapper + .toViews(this.countryRepository .findAllByNameIgnoreCaseIsLike("%%")); } - @Operation(security = {@SecurityRequirement(name = "bearer-key")}) + @Operation(summary = "Search all countries by name.", + description = "Find country or all countries by name.") @GetMapping("search") - public Set searchCountries(@RequestParam @NotBlank String name) { - return mapper - .toViews(countryRepository + public Set searchCountries(@RequestParam @NotBlank String name) { + return this.mapper + .toViews(this.countryRepository .findAllByNameIgnoreCaseIsLike("%" + name + "%")); } diff --git a/src/main/java/org/siriusxi/htec/fa/api/controller/FileUploadController.java b/src/main/java/org/siriusxi/fa/api/controller/FileUploadController.java similarity index 86% rename from src/main/java/org/siriusxi/htec/fa/api/controller/FileUploadController.java rename to src/main/java/org/siriusxi/fa/api/controller/FileUploadController.java index 0e8669c..5ff2790 100644 --- a/src/main/java/org/siriusxi/htec/fa/api/controller/FileUploadController.java +++ b/src/main/java/org/siriusxi/fa/api/controller/FileUploadController.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.api.controller; +package org.siriusxi.fa.api.controller; import com.opencsv.bean.BeanVerifier; import com.opencsv.bean.CsvToBean; @@ -12,17 +12,17 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; -import org.siriusxi.htec.fa.api.model.upload.airport.AirportDto; -import org.siriusxi.htec.fa.api.model.upload.airport.verifer.AirportBeanVerifier; -import org.siriusxi.htec.fa.api.model.upload.route.RouteDto; -import org.siriusxi.htec.fa.api.model.upload.route.verifer.RouteBeanVerifier; -import org.siriusxi.htec.fa.domain.Country; -import org.siriusxi.htec.fa.infra.mapper.AirportMapper; -import org.siriusxi.htec.fa.infra.mapper.RouteMapper; -import org.siriusxi.htec.fa.repository.AirportRepository; -import org.siriusxi.htec.fa.repository.CityRepository; -import org.siriusxi.htec.fa.repository.CountryRepository; -import org.siriusxi.htec.fa.repository.RouteRepository; +import org.siriusxi.fa.api.model.upload.airport.AirportDto; +import org.siriusxi.fa.api.model.upload.airport.verifer.AirportBeanVerifier; +import org.siriusxi.fa.api.model.upload.route.RouteDto; +import org.siriusxi.fa.api.model.upload.route.verifer.RouteBeanVerifier; +import org.siriusxi.fa.domain.Country; +import org.siriusxi.fa.infra.mapper.AirportMapper; +import org.siriusxi.fa.infra.mapper.RouteMapper; +import org.siriusxi.fa.repository.AirportRepository; +import org.siriusxi.fa.repository.CityRepository; +import org.siriusxi.fa.repository.CountryRepository; +import org.siriusxi.fa.repository.RouteRepository; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -49,6 +49,7 @@ description = """ A set of authorized file management APIs, used to feed the system with data files like airports and routes.""") +@SecurityRequirement(name = "bearer-key") @RestController @RequestMapping(value = "upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @RequiredArgsConstructor @@ -62,7 +63,6 @@ public class FileUploadController { private final AirportMapper airportMapper; @Operation(summary = "Upload file that contains flights airports.", - security = {@SecurityRequirement(name = "bearer-key")}, responses = { @ApiResponse(responseCode = "200", description = "OK; File is uploaded and parsed successfully.", @@ -92,22 +92,22 @@ public ResponseEntity uploadAirports(@Parameter(name = "file", required 3. Map Airport Dto to Airport model. 4. Return all airports as list to be saved to DB. */ - airportRepository.saveAll( + this.airportRepository.saveAll( airports .stream()// Save countries and cities to database .map(airportDto -> { - Country country = countryRepository + Country country = this.countryRepository .findOrSaveBy(airportDto.getCountry()); airportDto.setCountryId(country.getId()); - airportDto.setCityId(cityRepository + airportDto.setCityId(this.cityRepository .findOrSaveBy(country, airportDto.getCity()) .getId()); // then converting to - return airportMapper.toModel(airportDto); + return this.airportMapper.toModel(airportDto); }) // Then collect them to list .toList()); @@ -123,7 +123,6 @@ public ResponseEntity uploadAirports(@Parameter(name = "file", required } @Operation(summary = "Upload file that contains flights routes.", - security = {@SecurityRequirement(name = "bearer-key")}, responses = { @ApiResponse(responseCode = "200", description = "OK; File is uploaded and parsed successfully.", @@ -144,7 +143,7 @@ public ResponseEntity uploadRoutes(@Parameter(name = "file", required = if (file.isEmpty()) return new ResponseEntity<>("Please upload a valid file.", BAD_REQUEST); - else if (airportRepository.count() == 0) { + else if (this.airportRepository.count() == 0) { return new ResponseEntity<>(""" "Can't upload flight routes, system doesn't has Airports defined. Please upload airport file, to feed the system with airports." @@ -162,16 +161,16 @@ else if (airportRepository.count() == 0) { 1. Convert to Route model. 2. Save routes in DB. */ - routeRepository + this.routeRepository .saveAll(routes .stream() // filter routes doesn't exists .filter(dto -> - airportRepository + this.airportRepository .findById(dto.getSrcAirportId()).isPresent() && - airportRepository.findById(dto.getDestAirportId()).isPresent()) + this.airportRepository.findById(dto.getDestAirportId()).isPresent()) // converting to - .map(routeMapper::toModel) + .map(this.routeMapper::toModel) // Then collect them to list .toList()); diff --git a/src/main/java/org/siriusxi/htec/fa/api/model/request/AuthRequest.java b/src/main/java/org/siriusxi/fa/api/model/request/AuthRequest.java similarity index 89% rename from src/main/java/org/siriusxi/htec/fa/api/model/request/AuthRequest.java rename to src/main/java/org/siriusxi/fa/api/model/request/AuthRequest.java index 6000b4c..b16a7e7 100644 --- a/src/main/java/org/siriusxi/htec/fa/api/model/request/AuthRequest.java +++ b/src/main/java/org/siriusxi/fa/api/model/request/AuthRequest.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.api.model.request; +package org.siriusxi.fa.api.model.request; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/org/siriusxi/fa/api/model/request/ChangePasswordRequest.java b/src/main/java/org/siriusxi/fa/api/model/request/ChangePasswordRequest.java new file mode 100644 index 0000000..f1fd278 --- /dev/null +++ b/src/main/java/org/siriusxi/fa/api/model/request/ChangePasswordRequest.java @@ -0,0 +1,7 @@ +package org.siriusxi.fa.api.model.request; + +import jakarta.validation.constraints.NotBlank; + +public record ChangePasswordRequest(@NotBlank String newPassword, + @NotBlank String newPasswordAgain) { +} diff --git a/src/main/java/org/siriusxi/htec/fa/api/model/request/CommentUpSrtRequest.java b/src/main/java/org/siriusxi/fa/api/model/request/CommentUpSrtRequest.java similarity index 88% rename from src/main/java/org/siriusxi/htec/fa/api/model/request/CommentUpSrtRequest.java rename to src/main/java/org/siriusxi/fa/api/model/request/CommentUpSrtRequest.java index 66b2ec5..2cc767e 100644 --- a/src/main/java/org/siriusxi/htec/fa/api/model/request/CommentUpSrtRequest.java +++ b/src/main/java/org/siriusxi/fa/api/model/request/CommentUpSrtRequest.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.api.model.request; +package org.siriusxi.fa.api.model.request; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/org/siriusxi/htec/fa/api/model/request/CreateCityRequest.java b/src/main/java/org/siriusxi/fa/api/model/request/CreateCityRequest.java similarity index 93% rename from src/main/java/org/siriusxi/htec/fa/api/model/request/CreateCityRequest.java rename to src/main/java/org/siriusxi/fa/api/model/request/CreateCityRequest.java index 1e5ea75..44463ff 100644 --- a/src/main/java/org/siriusxi/htec/fa/api/model/request/CreateCityRequest.java +++ b/src/main/java/org/siriusxi/fa/api/model/request/CreateCityRequest.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.api.model.request; +package org.siriusxi.fa.api.model.request; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/org/siriusxi/htec/fa/api/model/request/CreateUserRequest.java b/src/main/java/org/siriusxi/fa/api/model/request/CreateUserRequest.java similarity index 94% rename from src/main/java/org/siriusxi/htec/fa/api/model/request/CreateUserRequest.java rename to src/main/java/org/siriusxi/fa/api/model/request/CreateUserRequest.java index e5d5b74..6a4e040 100644 --- a/src/main/java/org/siriusxi/htec/fa/api/model/request/CreateUserRequest.java +++ b/src/main/java/org/siriusxi/fa/api/model/request/CreateUserRequest.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.api.model.request; +package org.siriusxi.fa.api.model.request; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/org/siriusxi/htec/fa/api/model/request/SearchAirportRequest.java b/src/main/java/org/siriusxi/fa/api/model/request/SearchAirportRequest.java similarity index 86% rename from src/main/java/org/siriusxi/htec/fa/api/model/request/SearchAirportRequest.java rename to src/main/java/org/siriusxi/fa/api/model/request/SearchAirportRequest.java index da658e3..09a41df 100644 --- a/src/main/java/org/siriusxi/htec/fa/api/model/request/SearchAirportRequest.java +++ b/src/main/java/org/siriusxi/fa/api/model/request/SearchAirportRequest.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.api.model.request; +package org.siriusxi.fa.api.model.request; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/org/siriusxi/htec/fa/api/model/request/SearchCityRequest.java b/src/main/java/org/siriusxi/fa/api/model/request/SearchCityRequest.java similarity index 83% rename from src/main/java/org/siriusxi/htec/fa/api/model/request/SearchCityRequest.java rename to src/main/java/org/siriusxi/fa/api/model/request/SearchCityRequest.java index d81be8e..f7e5b83 100644 --- a/src/main/java/org/siriusxi/htec/fa/api/model/request/SearchCityRequest.java +++ b/src/main/java/org/siriusxi/fa/api/model/request/SearchCityRequest.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.api.model.request; +package org.siriusxi.fa.api.model.request; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/org/siriusxi/fa/api/model/request/TokenRefreshRequest.java b/src/main/java/org/siriusxi/fa/api/model/request/TokenRefreshRequest.java new file mode 100644 index 0000000..c65c715 --- /dev/null +++ b/src/main/java/org/siriusxi/fa/api/model/request/TokenRefreshRequest.java @@ -0,0 +1,6 @@ +package org.siriusxi.fa.api.model.request; + +import jakarta.validation.constraints.NotBlank; + +public record TokenRefreshRequest(@NotBlank String refreshToken) { +} diff --git a/src/main/java/org/siriusxi/fa/api/model/response/AirportResponse.java b/src/main/java/org/siriusxi/fa/api/model/response/AirportResponse.java new file mode 100644 index 0000000..fd61144 --- /dev/null +++ b/src/main/java/org/siriusxi/fa/api/model/response/AirportResponse.java @@ -0,0 +1,20 @@ +package org.siriusxi.fa.api.model.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; + +public record AirportResponse(@JsonInclude(NON_NULL) + @JsonProperty Integer id, + @JsonProperty String airport, + @JsonProperty String city, + @JsonProperty String country, + @JsonInclude(NON_NULL) + @JsonProperty String iata, + @JsonInclude(NON_NULL) + @JsonProperty String icao) { +} + + + diff --git a/src/main/java/org/siriusxi/fa/api/model/response/CityResponse.java b/src/main/java/org/siriusxi/fa/api/model/response/CityResponse.java new file mode 100644 index 0000000..4054b30 --- /dev/null +++ b/src/main/java/org/siriusxi/fa/api/model/response/CityResponse.java @@ -0,0 +1,17 @@ +package org.siriusxi.fa.api.model.response; + + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; + +public record CityResponse(@JsonProperty int id, + @JsonProperty String name, + @JsonProperty String country, + @JsonProperty String description, + @JsonInclude(NON_NULL) + @JsonProperty List comments) { +} diff --git a/src/main/java/org/siriusxi/htec/fa/api/model/response/CommentView.java b/src/main/java/org/siriusxi/fa/api/model/response/CommentResponse.java similarity index 55% rename from src/main/java/org/siriusxi/htec/fa/api/model/response/CommentView.java rename to src/main/java/org/siriusxi/fa/api/model/response/CommentResponse.java index c13843e..9c7ab9f 100644 --- a/src/main/java/org/siriusxi/htec/fa/api/model/response/CommentView.java +++ b/src/main/java/org/siriusxi/fa/api/model/response/CommentResponse.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.api.model.response; +package org.siriusxi.fa.api.model.response; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -9,10 +9,10 @@ import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; @JsonPropertyOrder({"id", "comment", "by", "createdAt", "updatedAt"}) -public record CommentView(@JsonProperty int id, - @JsonProperty String comment, - @JsonProperty String by, - @JsonProperty("createdAt") LocalDateTime createdAt, - @JsonInclude(NON_NULL) +public record CommentResponse(@JsonProperty int id, + @JsonProperty String comment, + @JsonProperty String by, + @JsonProperty("createdAt") LocalDateTime createdAt, + @JsonInclude(NON_NULL) @JsonProperty("updatedAt") LocalDateTime updatedAt) { } diff --git a/src/main/java/org/siriusxi/fa/api/model/response/CountryResponse.java b/src/main/java/org/siriusxi/fa/api/model/response/CountryResponse.java new file mode 100644 index 0000000..4a1355e --- /dev/null +++ b/src/main/java/org/siriusxi/fa/api/model/response/CountryResponse.java @@ -0,0 +1,8 @@ +package org.siriusxi.fa.api.model.response; + + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record CountryResponse(@JsonProperty int id, + @JsonProperty String name) { +} diff --git a/src/main/java/org/siriusxi/fa/api/model/response/TokenRefreshResponse.java b/src/main/java/org/siriusxi/fa/api/model/response/TokenRefreshResponse.java new file mode 100644 index 0000000..ba0606a --- /dev/null +++ b/src/main/java/org/siriusxi/fa/api/model/response/TokenRefreshResponse.java @@ -0,0 +1,6 @@ +package org.siriusxi.fa.api.model.response; + +public record TokenRefreshResponse( + String accessToken, + String refreshToken) { +} \ No newline at end of file diff --git a/src/main/java/org/siriusxi/htec/fa/api/model/response/TripView.java b/src/main/java/org/siriusxi/fa/api/model/response/TripResponse.java similarity index 72% rename from src/main/java/org/siriusxi/htec/fa/api/model/response/TripView.java rename to src/main/java/org/siriusxi/fa/api/model/response/TripResponse.java index 25ea819..fd955c4 100644 --- a/src/main/java/org/siriusxi/htec/fa/api/model/response/TripView.java +++ b/src/main/java/org/siriusxi/fa/api/model/response/TripResponse.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.api.model.response; +package org.siriusxi.fa.api.model.response; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -7,11 +7,11 @@ import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; -public record TripView( - @JsonProperty AirportView start, +public record TripResponse( + @JsonProperty AirportResponse start, @JsonInclude(NON_NULL) - @JsonProperty List through, - @JsonProperty AirportView end, + @JsonProperty List through, + @JsonProperty AirportResponse end, @JsonProperty Price price, @JsonProperty Distance distance) { diff --git a/src/main/java/org/siriusxi/fa/api/model/response/UserResponse.java b/src/main/java/org/siriusxi/fa/api/model/response/UserResponse.java new file mode 100644 index 0000000..ea78541 --- /dev/null +++ b/src/main/java/org/siriusxi/fa/api/model/response/UserResponse.java @@ -0,0 +1,43 @@ +package org.siriusxi.fa.api.model.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Arrays; +import java.util.Objects; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; + +@JsonInclude(NON_NULL) +public record UserResponse(@JsonProperty String id, + @JsonProperty String username, + @JsonProperty String firstName, + @JsonProperty String lastName, + @JsonProperty String[] authorities, + @JsonProperty String refreshToken) { + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UserResponse userResponse = (UserResponse) o; + return Objects.equals(this.id, userResponse.id) && + Objects.equals(this.username, userResponse.username) && + Objects.equals(this.firstName, userResponse.firstName) && + Objects.equals(this.lastName, userResponse.lastName) && + Objects.equals(this.refreshToken, userResponse.refreshToken) && + Arrays.equals(this.authorities, userResponse.authorities); + } + + @Override + public int hashCode() { + int result = Objects.hash(this.id, this.username, this.firstName, this.lastName, this.refreshToken); + result = 31 * result + Arrays.hashCode(this.authorities); + return result; + } + + @Override + public String toString() { + return this.getClass().getSimpleName().concat(" { id= %s, username= %s, firstName= %s, lastName= %s , authorities= %s, refreshToken= %s }") + .formatted(this.id, this.username, this.firstName, this.lastName, Arrays.toString(this.authorities), this.refreshToken); + } +} diff --git a/src/main/java/org/siriusxi/htec/fa/api/model/upload/airport/AirportDto.java b/src/main/java/org/siriusxi/fa/api/model/upload/airport/AirportDto.java similarity index 87% rename from src/main/java/org/siriusxi/htec/fa/api/model/upload/airport/AirportDto.java rename to src/main/java/org/siriusxi/fa/api/model/upload/airport/AirportDto.java index 9c4d257..8dcf398 100644 --- a/src/main/java/org/siriusxi/htec/fa/api/model/upload/airport/AirportDto.java +++ b/src/main/java/org/siriusxi/fa/api/model/upload/airport/AirportDto.java @@ -1,9 +1,9 @@ -package org.siriusxi.htec.fa.api.model.upload.airport; +package org.siriusxi.fa.api.model.upload.airport; import com.opencsv.bean.CsvBindByPosition; import com.opencsv.bean.processor.PreAssignmentProcessor; import lombok.Data; -import org.siriusxi.htec.fa.api.model.upload.converter.ConvertUnwantedStringsToDefault; +import org.siriusxi.fa.api.model.upload.converter.ConvertUnwantedStringsToDefault; import java.math.BigDecimal; @@ -13,56 +13,56 @@ **/ @Data public class AirportDto { - + @PreAssignmentProcessor(processor = ConvertUnwantedStringsToDefault.class, paramString = "0") @CsvBindByPosition(position = 0) private int airportId; - + @CsvBindByPosition(position = 1) private String name; - + @CsvBindByPosition(position = 2) private String city; - + @CsvBindByPosition(position = 3) private String country; - + @PreAssignmentProcessor(processor = ConvertUnwantedStringsToDefault.class) @CsvBindByPosition(position = 4) private String iata; - + @PreAssignmentProcessor(processor = ConvertUnwantedStringsToDefault.class) @CsvBindByPosition(position = 5) private String icao; - + @CsvBindByPosition(position = 6) private BigDecimal latitude; - + @CsvBindByPosition(position = 7) private BigDecimal longitude; - + @CsvBindByPosition(position = 8) private short altitude; - + @PreAssignmentProcessor(processor = ConvertUnwantedStringsToDefault.class, - paramString = "-99.9") + paramString = "-99.9") @CsvBindByPosition(position = 9) private float timezone; - + @PreAssignmentProcessor(processor = ConvertUnwantedStringsToDefault.class, paramString = "U") @CsvBindByPosition(position = 10) private String dst; - + @PreAssignmentProcessor(processor = ConvertUnwantedStringsToDefault.class, paramString = "U") @CsvBindByPosition(position = 11) private String tz; - + @CsvBindByPosition(position = 12) private String type; - + @CsvBindByPosition(position = 13) private String dataSource; - + // Used for Airport Model mapping of country and city private int cityId; private int countryId; diff --git a/src/main/java/org/siriusxi/htec/fa/api/model/upload/airport/verifer/AirportBeanVerifier.java b/src/main/java/org/siriusxi/fa/api/model/upload/airport/verifer/AirportBeanVerifier.java similarity index 68% rename from src/main/java/org/siriusxi/htec/fa/api/model/upload/airport/verifer/AirportBeanVerifier.java rename to src/main/java/org/siriusxi/fa/api/model/upload/airport/verifer/AirportBeanVerifier.java index 0b66a71..7666d8b 100644 --- a/src/main/java/org/siriusxi/htec/fa/api/model/upload/airport/verifer/AirportBeanVerifier.java +++ b/src/main/java/org/siriusxi/fa/api/model/upload/airport/verifer/AirportBeanVerifier.java @@ -1,7 +1,7 @@ -package org.siriusxi.htec.fa.api.model.upload.airport.verifer; +package org.siriusxi.fa.api.model.upload.airport.verifer; import com.opencsv.bean.BeanVerifier; -import org.siriusxi.htec.fa.api.model.upload.airport.AirportDto; +import org.siriusxi.fa.api.model.upload.airport.AirportDto; public class AirportBeanVerifier implements BeanVerifier { @Override diff --git a/src/main/java/org/siriusxi/htec/fa/api/model/upload/converter/ConvertUnwantedStringsToDefault.java b/src/main/java/org/siriusxi/fa/api/model/upload/converter/ConvertUnwantedStringsToDefault.java similarity index 79% rename from src/main/java/org/siriusxi/htec/fa/api/model/upload/converter/ConvertUnwantedStringsToDefault.java rename to src/main/java/org/siriusxi/fa/api/model/upload/converter/ConvertUnwantedStringsToDefault.java index b5263ff..86b905a 100644 --- a/src/main/java/org/siriusxi/htec/fa/api/model/upload/converter/ConvertUnwantedStringsToDefault.java +++ b/src/main/java/org/siriusxi/fa/api/model/upload/converter/ConvertUnwantedStringsToDefault.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.api.model.upload.converter; +package org.siriusxi.fa.api.model.upload.converter; import com.opencsv.bean.processor.StringProcessor; @@ -11,13 +11,13 @@ public String processString(String value) { if (value == null || value.isBlank() || value.equalsIgnoreCase("\\N") || value.equalsIgnoreCase("N")) { - return defaultValue; + return this.defaultValue; } return value; } @Override public void setParameterString(String value) { - defaultValue = value; + this.defaultValue = value; } } diff --git a/src/main/java/org/siriusxi/htec/fa/api/model/upload/route/RouteDto.java b/src/main/java/org/siriusxi/fa/api/model/upload/route/RouteDto.java similarity index 90% rename from src/main/java/org/siriusxi/htec/fa/api/model/upload/route/RouteDto.java rename to src/main/java/org/siriusxi/fa/api/model/upload/route/RouteDto.java index 260b0c4..87493ab 100644 --- a/src/main/java/org/siriusxi/htec/fa/api/model/upload/route/RouteDto.java +++ b/src/main/java/org/siriusxi/fa/api/model/upload/route/RouteDto.java @@ -1,9 +1,9 @@ -package org.siriusxi.htec.fa.api.model.upload.route; +package org.siriusxi.fa.api.model.upload.route; import com.opencsv.bean.CsvBindByPosition; import com.opencsv.bean.processor.PreAssignmentProcessor; import lombok.Data; -import org.siriusxi.htec.fa.api.model.upload.converter.ConvertUnwantedStringsToDefault; +import org.siriusxi.fa.api.model.upload.converter.ConvertUnwantedStringsToDefault; /** * @author Mohamed Taman diff --git a/src/main/java/org/siriusxi/htec/fa/api/model/upload/route/verifer/RouteBeanVerifier.java b/src/main/java/org/siriusxi/fa/api/model/upload/route/verifer/RouteBeanVerifier.java similarity index 72% rename from src/main/java/org/siriusxi/htec/fa/api/model/upload/route/verifer/RouteBeanVerifier.java rename to src/main/java/org/siriusxi/fa/api/model/upload/route/verifer/RouteBeanVerifier.java index 652263f..16d5081 100644 --- a/src/main/java/org/siriusxi/htec/fa/api/model/upload/route/verifer/RouteBeanVerifier.java +++ b/src/main/java/org/siriusxi/fa/api/model/upload/route/verifer/RouteBeanVerifier.java @@ -1,7 +1,7 @@ -package org.siriusxi.htec.fa.api.model.upload.route.verifer; +package org.siriusxi.fa.api.model.upload.route.verifer; import com.opencsv.bean.BeanVerifier; -import org.siriusxi.htec.fa.api.model.upload.route.RouteDto; +import org.siriusxi.fa.api.model.upload.route.RouteDto; public class RouteBeanVerifier implements BeanVerifier { @Override diff --git a/src/main/java/org/siriusxi/htec/fa/domain/Airport.java b/src/main/java/org/siriusxi/fa/domain/Airport.java similarity index 93% rename from src/main/java/org/siriusxi/htec/fa/domain/Airport.java rename to src/main/java/org/siriusxi/fa/domain/Airport.java index 5f3e9fb..df26352 100644 --- a/src/main/java/org/siriusxi/htec/fa/domain/Airport.java +++ b/src/main/java/org/siriusxi/fa/domain/Airport.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.domain; +package org.siriusxi.fa.domain; import jakarta.persistence.Basic; import jakarta.persistence.Column; @@ -34,70 +34,88 @@ @Table(catalog = "FLIGHTDB", schema = "PUBLIC") @Getter @Setter -@NoArgsConstructor +@NoArgsConstructor(force = true) @RequiredArgsConstructor public class Airport implements Serializable { @Serial private static final long serialVersionUID = 6913599167936010779L; + @Id @Basic(optional = false) @Column(name = "AIRPORT_ID", nullable = false) private Integer airportId; + @NonNull @Basic(optional = false) @Column(nullable = false) private String name; + @NonNull @Basic(optional = false) @Column(name = "CITY", nullable = false, length = 100) private String cityName; + @NonNull @Basic(optional = false) @Column(name = "COUNTRY", nullable = false, length = 100) private String countryName; + @Column(length = 3) private String iata; + @Column(length = 4) private String icao; + @NonNull @Basic(optional = false) @Column(nullable = false, precision = 12, scale = 6) private BigDecimal latitude; + @NonNull @Basic(optional = false) @Column(nullable = false, precision = 12, scale = 6) private BigDecimal longitude; + @NonNull private Integer altitude; + @NonNull private Float timezone; + @NonNull @Enumerated(STRING) @Basic(optional = false) @Column(nullable = false) private Dst dst; + @NonNull @Column(length = 50) private String tz; + @NonNull @Basic(optional = false) @Column(nullable = false, length = 50) private String type; + @NonNull @Basic(optional = false) @Column(name = "DATA_SOURCE", nullable = false) private String dataSource; + @OneToMany(mappedBy = "destinationAirport", fetch = LAZY) @ToString.Exclude private List destinationRoutes; + @OneToMany(mappedBy = "sourceAirport", fetch = LAZY) @ToString.Exclude private List sourceRoutes; + @JoinColumn(name = "CITY_ID", referencedColumnName = "ID", nullable = false) @ManyToOne(optional = false, fetch = LAZY) @ToString.Exclude private City city; + @JoinColumn(name = "COUNTRY_ID", referencedColumnName = "ID", nullable = false) @ManyToOne(optional = false, fetch = LAZY) @ToString.Exclude @@ -113,12 +131,12 @@ public boolean equals(Object o) { if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; - return Objects.equals(airportId, ((Airport) o).airportId); + return Objects.equals(this.airportId, ((Airport) o).airportId); } @Override public int hashCode() { - return Objects.hash(latitude, longitude, altitude); + return Objects.hash(this.latitude, this.longitude, this.altitude); } public enum Dst { diff --git a/src/main/java/org/siriusxi/htec/fa/domain/City.java b/src/main/java/org/siriusxi/fa/domain/City.java similarity index 84% rename from src/main/java/org/siriusxi/htec/fa/domain/City.java rename to src/main/java/org/siriusxi/fa/domain/City.java index 9ada762..a503adc 100644 --- a/src/main/java/org/siriusxi/htec/fa/domain/City.java +++ b/src/main/java/org/siriusxi/fa/domain/City.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.domain; +package org.siriusxi.fa.domain; import jakarta.persistence.Basic; import jakarta.persistence.Column; @@ -34,12 +34,12 @@ @Table(catalog = "FLIGHTDB", schema = "PUBLIC") @Getter @Setter -@NoArgsConstructor +@NoArgsConstructor(force = true) @RequiredArgsConstructor public class City implements Serializable { @Serial - private static final long serialVersionUID = 1322727266984495327L; + private static final long serialVersionUID = 7744039479941701650L; @Id @GeneratedValue(strategy = IDENTITY) @@ -58,11 +58,11 @@ public class City implements Serializable { @OneToMany(cascade = ALL, mappedBy = "city", fetch = LAZY) @ToString.Exclude - private List comments; + private transient List comments; @OneToMany(cascade = ALL, mappedBy = "city", fetch = LAZY) @ToString.Exclude - private List airports; + private transient List airports; @NonNull @JoinColumn(name = "COUNTRY_ID", referencedColumnName = "ID", nullable = false) @@ -85,11 +85,11 @@ public boolean equals(Object o) { if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; - return Objects.equals(id, ((City) o).id); + return Objects.equals(this.id, ((City) o).id); } @Override public int hashCode() { - return Objects.hash(id, name, description, comments, airports, country); + return Objects.hash(this.id, this.name, this.description, this.comments, this.airports, this.country); } } diff --git a/src/main/java/org/siriusxi/htec/fa/domain/Comment.java b/src/main/java/org/siriusxi/fa/domain/Comment.java similarity index 84% rename from src/main/java/org/siriusxi/htec/fa/domain/Comment.java rename to src/main/java/org/siriusxi/fa/domain/Comment.java index 58a9eb7..cb9ec01 100644 --- a/src/main/java/org/siriusxi/htec/fa/domain/Comment.java +++ b/src/main/java/org/siriusxi/fa/domain/Comment.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.domain; +package org.siriusxi.fa.domain; import jakarta.persistence.Basic; import jakarta.persistence.Column; @@ -33,12 +33,12 @@ @Table(name = "CITY_COMMENT", catalog = "FLIGHTDB", schema = "PUBLIC") @Getter @Setter -@NoArgsConstructor +@NoArgsConstructor(force = true) @RequiredArgsConstructor public class Comment implements Serializable { @Serial - private static final long serialVersionUID = -4628882357786781599L; + private static final long serialVersionUID = 3792430609269167831L; @Id @NonNull @@ -74,7 +74,7 @@ public String toString() { return """ Comment{id= %d, comment= %s, createdAt= %s, \ updatedOn= %s, city= "%s", user= %s }""" - .formatted(id, comment, createdAt, updatedAt, city.getName(), user.getUserUuid()); + .formatted(this.id, this.comment, this.createdAt, this.updatedAt, this.city.getName(), this.user.getUserUuid()); } @Override @@ -83,11 +83,11 @@ public boolean equals(Object o) { if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; - return Objects.equals(id, ((Comment) o).id); + return Objects.equals(this.id, ((Comment) o).id); } @Override public int hashCode() { - return Objects.hash(id, comment, createdAt, updatedAt, city, user); + return Objects.hash(this.id, this.comment, this.createdAt, this.updatedAt, this.city, this.user); } } diff --git a/src/main/java/org/siriusxi/htec/fa/domain/Country.java b/src/main/java/org/siriusxi/fa/domain/Country.java similarity index 82% rename from src/main/java/org/siriusxi/htec/fa/domain/Country.java rename to src/main/java/org/siriusxi/fa/domain/Country.java index 5788565..4d7f7fa 100644 --- a/src/main/java/org/siriusxi/htec/fa/domain/Country.java +++ b/src/main/java/org/siriusxi/fa/domain/Country.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.domain; +package org.siriusxi.fa.domain; import jakarta.persistence.Basic; import jakarta.persistence.Column; @@ -33,27 +33,31 @@ @Getter @Setter @ToString -@NoArgsConstructor +@NoArgsConstructor(force = true) @RequiredArgsConstructor public class Country implements Serializable { @Serial - private static final long serialVersionUID = -9057344199173138205L; + private static final long serialVersionUID = 7665467979286278572L; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Basic(optional = false) @Column(nullable = false) private Integer id; + @NonNull @Basic(optional = false) @Column(nullable = false, length = 100) private String name; + @OneToMany(cascade = ALL, mappedBy = "country", fetch = LAZY) @ToString.Exclude - private List airports; + private transient List airports; + @OneToMany(cascade = ALL, mappedBy = "country", fetch = LAZY) @ToString.Exclude - private List cities; + private transient List cities; public Country(Integer id) { this.id = id; @@ -65,11 +69,11 @@ public boolean equals(Object o) { if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; Country country = (Country) o; - return Objects.equals(id, country.id); + return Objects.equals(this.id, country.id); } @Override public int hashCode() { - return Objects.hash(id, name, airports, cities); + return Objects.hash(this.id, this.name, this.airports, this.cities); } } diff --git a/src/main/java/org/siriusxi/htec/fa/domain/Role.java b/src/main/java/org/siriusxi/fa/domain/Role.java similarity index 87% rename from src/main/java/org/siriusxi/htec/fa/domain/Role.java rename to src/main/java/org/siriusxi/fa/domain/Role.java index 1019a09..86a93e2 100644 --- a/src/main/java/org/siriusxi/htec/fa/domain/Role.java +++ b/src/main/java/org/siriusxi/fa/domain/Role.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.domain; +package org.siriusxi.fa.domain; import jakarta.persistence.Basic; import jakarta.persistence.Column; @@ -30,7 +30,7 @@ public class Role implements GrantedAuthority { public static final String ADMIN = "ADMIN"; @Serial - private static final long serialVersionUID = 3353780708738749971L; + private static final long serialVersionUID = 5369984016984936492L; @EmbeddedId protected RolePK rolePK; @@ -51,11 +51,11 @@ public boolean equals(Object o) { if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; Role role = (Role) o; - return Objects.equals(rolePK, role.rolePK); + return Objects.equals(this.rolePK, role.rolePK); } @Override public int hashCode() { - return Objects.hash(rolePK); + return Objects.hash(this.rolePK); } } diff --git a/src/main/java/org/siriusxi/htec/fa/domain/RolePK.java b/src/main/java/org/siriusxi/fa/domain/RolePK.java similarity index 73% rename from src/main/java/org/siriusxi/htec/fa/domain/RolePK.java rename to src/main/java/org/siriusxi/fa/domain/RolePK.java index 6d732ce..529513a 100644 --- a/src/main/java/org/siriusxi/htec/fa/domain/RolePK.java +++ b/src/main/java/org/siriusxi/fa/domain/RolePK.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.domain; +package org.siriusxi.fa.domain; import jakarta.persistence.Basic; import jakarta.persistence.Column; @@ -11,6 +11,7 @@ import lombok.ToString; import org.hibernate.Hibernate; +import java.io.Serial; import java.io.Serializable; import java.util.Objects; @@ -18,10 +19,14 @@ @Getter @Setter @ToString -@NoArgsConstructor +@NoArgsConstructor(force = true) @RequiredArgsConstructor public class RolePK implements Serializable { - + + + @Serial + private static final long serialVersionUID = 7751515991605761066L; + @NonNull @Basic(optional = false) @Column(name = "USER_ID", nullable = false) @@ -38,12 +43,12 @@ public boolean equals(Object o) { if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; RolePK rolePK = (RolePK) o; - return Objects.equals(userId, rolePK.userId) - && Objects.equals(authority, rolePK.authority); + return Objects.equals(this.userId, rolePK.userId) + && Objects.equals(this.authority, rolePK.authority); } @Override public int hashCode() { - return Objects.hash(userId, authority); + return Objects.hash(this.userId, this.authority); } } diff --git a/src/main/java/org/siriusxi/htec/fa/domain/Route.java b/src/main/java/org/siriusxi/fa/domain/Route.java similarity index 94% rename from src/main/java/org/siriusxi/htec/fa/domain/Route.java rename to src/main/java/org/siriusxi/fa/domain/Route.java index 760e595..4e871ed 100644 --- a/src/main/java/org/siriusxi/htec/fa/domain/Route.java +++ b/src/main/java/org/siriusxi/fa/domain/Route.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.domain; +package org.siriusxi.fa.domain; import jakarta.persistence.Column; import jakarta.persistence.EmbeddedId; @@ -33,7 +33,7 @@ public class Route implements Serializable { @Serial - private static final long serialVersionUID = 8818845493466966108L; + private static final long serialVersionUID = -4190697799834880387L; @EmbeddedId protected RoutePK routePK; diff --git a/src/main/java/org/siriusxi/htec/fa/domain/RoutePK.java b/src/main/java/org/siriusxi/fa/domain/RoutePK.java similarity index 73% rename from src/main/java/org/siriusxi/htec/fa/domain/RoutePK.java rename to src/main/java/org/siriusxi/fa/domain/RoutePK.java index 05fdf7d..040c9fe 100644 --- a/src/main/java/org/siriusxi/htec/fa/domain/RoutePK.java +++ b/src/main/java/org/siriusxi/fa/domain/RoutePK.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.domain; +package org.siriusxi.fa.domain; import jakarta.persistence.Basic; import jakarta.persistence.Column; @@ -11,6 +11,7 @@ import lombok.ToString; import org.hibernate.Hibernate; +import java.io.Serial; import java.io.Serializable; import java.util.Objects; @@ -18,9 +19,12 @@ @Getter @Setter @ToString -@NoArgsConstructor +@NoArgsConstructor(force = true) @RequiredArgsConstructor public class RoutePK implements Serializable { + + @Serial + private static final long serialVersionUID = -3409091883135162258L; @NonNull @Basic(optional = false) @@ -38,12 +42,12 @@ public boolean equals(Object o) { if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; RoutePK routePK = (RoutePK) o; - return Objects.equals(source, routePK.source) - && Objects.equals(destination, routePK.destination); + return Objects.equals(this.source, routePK.source) + && Objects.equals(this.destination, routePK.destination); } @Override public int hashCode() { - return Objects.hash(source, destination); + return Objects.hash(this.source, this.destination); } } diff --git a/src/main/java/org/siriusxi/htec/fa/domain/User.java b/src/main/java/org/siriusxi/fa/domain/User.java similarity index 69% rename from src/main/java/org/siriusxi/htec/fa/domain/User.java rename to src/main/java/org/siriusxi/fa/domain/User.java index 1e0bc52..3c7ea14 100644 --- a/src/main/java/org/siriusxi/htec/fa/domain/User.java +++ b/src/main/java/org/siriusxi/fa/domain/User.java @@ -1,30 +1,14 @@ -package org.siriusxi.htec.fa.domain; - -import jakarta.persistence.Basic; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.OneToMany; -import jakarta.persistence.Table; -import jakarta.persistence.UniqueConstraint; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; +package org.siriusxi.fa.domain; + +import jakarta.persistence.*; +import lombok.*; import org.hibernate.Hibernate; import org.springframework.security.core.userdetails.UserDetails; import java.io.Serial; import java.io.Serializable; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; +import java.time.Instant; +import java.util.*; import static jakarta.persistence.CascadeType.ALL; import static jakarta.persistence.FetchType.EAGER; @@ -38,18 +22,20 @@ */ @Entity @Table(name = "users", - catalog = "FLIGHTDB", - schema = "PUBLIC", - uniqueConstraints = { - @UniqueConstraint(columnNames = {"USER_UUID"}), - @UniqueConstraint(columnNames = {"USERNAME"}) - }) + catalog = "FLIGHTDB", + schema = "PUBLIC", + uniqueConstraints = { + @UniqueConstraint(columnNames = {"USER_UUID"}), + @UniqueConstraint(columnNames = {"USERNAME"}), + @UniqueConstraint(columnNames = {"REFRESH_TOKEN"}) + }) @Getter @Setter @NoArgsConstructor(force = true) @RequiredArgsConstructor +@ToString public class User implements UserDetails, Serializable { - + @Serial private static final long serialVersionUID = 5666668516577592568L; @@ -60,9 +46,9 @@ public class User implements UserDetails, Serializable { private Integer id; @Basic(optional = false) - @Column(name = "USER_UUID", nullable = false, updatable = false) + @Column(name = "USER_UUID", columnDefinition = "UUID", nullable = false, updatable = false) @ToString.Exclude - private String userUuid; + private UUID userUuid; @NonNull @Basic(optional = false) @@ -86,6 +72,14 @@ public class User implements UserDetails, Serializable { @ToString.Exclude private String password; + @Basic(optional = false) + @Column(name = "REFRESH_TOKEN", columnDefinition = "UUID") + private UUID refreshToken; + + @Basic(optional = false) + @Column(name = "TOKEN_EXPIRY_DATE") + private Instant tokenExpiryDate; + @OneToMany(cascade = ALL, mappedBy = "user", fetch = LAZY) @ToString.Exclude private List comments; @@ -98,57 +92,57 @@ public class User implements UserDetails, Serializable { @ToString.Exclude private boolean enabled = true; - + public User(Integer id) { this.id = id; } - + public void setAuthorities(Set roles) { for (Role role : roles) { role.setRolePK(new RolePK(this.getId(), role.getAuthority())); this.authorities.add(role); } } - + public void setAuthorities(String... authorities) { for (String authority : authorities) this.authorities.add(new Role(new RolePK(this.getId(), authority))); } - + public String getFullName() { return getFirstName() - .concat(" ") - .concat(getLastName()); + .concat(" ") + .concat(getLastName()); } - + @Override public boolean isAccountNonExpired() { return isEnabled(); } - + @Override public boolean isAccountNonLocked() { return isEnabled(); } - + @Override public boolean isCredentialsNonExpired() { return isEnabled(); } - + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; User user = (User) o; - return Objects.equals(id, user.id); + return Objects.equals(this.id, user.id); } - + @Override public int hashCode() { return Objects.hash( - id, userUuid, firstName, lastName, username, - password, comments, authorities, enabled); + this.id, this.userUuid, this.firstName, this.lastName, this.username, + this.password, this.comments, this.authorities, this.enabled, this.refreshToken); } } diff --git a/src/main/java/org/siriusxi/htec/fa/domain/vo/RouteView.java b/src/main/java/org/siriusxi/fa/domain/vo/RouteView.java similarity index 66% rename from src/main/java/org/siriusxi/htec/fa/domain/vo/RouteView.java rename to src/main/java/org/siriusxi/fa/domain/vo/RouteView.java index 3d79c16..f0f04d7 100644 --- a/src/main/java/org/siriusxi/htec/fa/domain/vo/RouteView.java +++ b/src/main/java/org/siriusxi/fa/domain/vo/RouteView.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.domain.vo; +package org.siriusxi.fa.domain.vo; public record RouteView(String source, String destination, double price) { } diff --git a/src/main/java/org/siriusxi/fa/infra/UuidUtil.java b/src/main/java/org/siriusxi/fa/infra/UuidUtil.java new file mode 100644 index 0000000..94b9003 --- /dev/null +++ b/src/main/java/org/siriusxi/fa/infra/UuidUtil.java @@ -0,0 +1,17 @@ +package org.siriusxi.fa.infra; + +import lombok.experimental.UtilityClass; + +import java.util.UUID; + +@UtilityClass +public class UuidUtil { + + public UUID newUuid() { + return UUID.randomUUID(); + } + + public UUID uuidFrom(String uuid) { + return UUID.fromString(uuid); + } +} diff --git a/src/main/java/org/siriusxi/htec/fa/infra/algorithm/distance/DistanceAlgorithm.java b/src/main/java/org/siriusxi/fa/infra/algorithm/distance/DistanceAlgorithm.java similarity index 96% rename from src/main/java/org/siriusxi/htec/fa/infra/algorithm/distance/DistanceAlgorithm.java rename to src/main/java/org/siriusxi/fa/infra/algorithm/distance/DistanceAlgorithm.java index da49baf..a13e105 100644 --- a/src/main/java/org/siriusxi/htec/fa/infra/algorithm/distance/DistanceAlgorithm.java +++ b/src/main/java/org/siriusxi/fa/infra/algorithm/distance/DistanceAlgorithm.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.infra.algorithm.distance; +package org.siriusxi.fa.infra.algorithm.distance; /** * A distance Algorithm interface to represent different calculations algorithms type @@ -76,7 +76,7 @@ enum MeasureType { } public double getValue() { - return value; + return this.value; } } diff --git a/src/main/java/org/siriusxi/htec/fa/infra/algorithm/distance/HaversineAlgorithm.java b/src/main/java/org/siriusxi/fa/infra/algorithm/distance/HaversineAlgorithm.java similarity index 92% rename from src/main/java/org/siriusxi/htec/fa/infra/algorithm/distance/HaversineAlgorithm.java rename to src/main/java/org/siriusxi/fa/infra/algorithm/distance/HaversineAlgorithm.java index d54c27f..5c90c27 100644 --- a/src/main/java/org/siriusxi/htec/fa/infra/algorithm/distance/HaversineAlgorithm.java +++ b/src/main/java/org/siriusxi/fa/infra/algorithm/distance/HaversineAlgorithm.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.infra.algorithm.distance; +package org.siriusxi.fa.infra.algorithm.distance; import static java.lang.Math.asin; import static java.lang.Math.cos; @@ -14,7 +14,7 @@ * The Haversine formula calculates the shortest distance between two points on a sphere * using their latitudes and longitudes measured along the surface. *

- * Source: Wikipedia + * Source: Wikipedia * * @author Mohamed Taman * @apiNote It is important for use in navigation. @@ -55,6 +55,7 @@ public double calculate(Point first, Point second, MeasureType measure) { /** * {@inheritDoc} */ + @Override public double calculate(Point first, Point second) { return calculate(first, second, null); } diff --git a/src/main/java/org/siriusxi/htec/fa/infra/algorithm/distance/OrthodromicAlgorithm.java b/src/main/java/org/siriusxi/fa/infra/algorithm/distance/OrthodromicAlgorithm.java similarity index 97% rename from src/main/java/org/siriusxi/htec/fa/infra/algorithm/distance/OrthodromicAlgorithm.java rename to src/main/java/org/siriusxi/fa/infra/algorithm/distance/OrthodromicAlgorithm.java index e0f01cd..5ac3051 100644 --- a/src/main/java/org/siriusxi/htec/fa/infra/algorithm/distance/OrthodromicAlgorithm.java +++ b/src/main/java/org/siriusxi/fa/infra/algorithm/distance/OrthodromicAlgorithm.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.infra.algorithm.distance; +package org.siriusxi.fa.infra.algorithm.distance; import static java.lang.Math.asin; import static java.lang.Math.cos; @@ -64,6 +64,7 @@ public double calculate(Point first, Point second, MeasureType measure) { /** * {@inheritDoc} */ + @Override public double calculate(Point first, Point second) { return calculate(first, second, null); } diff --git a/src/main/java/org/siriusxi/htec/fa/infra/algorithm/distance/Point.java b/src/main/java/org/siriusxi/fa/infra/algorithm/distance/Point.java similarity index 76% rename from src/main/java/org/siriusxi/htec/fa/infra/algorithm/distance/Point.java rename to src/main/java/org/siriusxi/fa/infra/algorithm/distance/Point.java index df3c987..a399a39 100644 --- a/src/main/java/org/siriusxi/htec/fa/infra/algorithm/distance/Point.java +++ b/src/main/java/org/siriusxi/fa/infra/algorithm/distance/Point.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.infra.algorithm.distance; +package org.siriusxi.fa.infra.algorithm.distance; /** * Point value class to represent a point as latitude and longitude. diff --git a/src/main/java/org/siriusxi/htec/fa/infra/config/GlobalExceptionHandler.java b/src/main/java/org/siriusxi/fa/infra/config/GlobalExceptionHandler.java similarity index 83% rename from src/main/java/org/siriusxi/htec/fa/infra/config/GlobalExceptionHandler.java rename to src/main/java/org/siriusxi/fa/infra/config/GlobalExceptionHandler.java index 00aab1d..8e8ded4 100644 --- a/src/main/java/org/siriusxi/htec/fa/infra/config/GlobalExceptionHandler.java +++ b/src/main/java/org/siriusxi/fa/infra/config/GlobalExceptionHandler.java @@ -1,12 +1,13 @@ -package org.siriusxi.htec.fa.infra.config; +package org.siriusxi.fa.infra.config; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonRootName; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.ValidationException; import lombok.extern.log4j.Log4j2; -import org.siriusxi.htec.fa.infra.exception.NotAllowedException; -import org.siriusxi.htec.fa.infra.exception.NotFoundException; +import org.siriusxi.fa.infra.exception.NotAllowedException; +import org.siriusxi.fa.infra.exception.NotFoundException; +import org.siriusxi.fa.infra.exception.RefreshTokenException; import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -40,7 +41,7 @@ public ResponseEntity> handleNotFoundException( HttpServletRequest request, NotFoundException ex) { - log.error("NotFoundException {}\n", request.getRequestURI(), ex); + log.error("NotFoundException {} \n", request.getRequestURI(), ex); return ResponseEntity .status(NOT_FOUND) @@ -53,7 +54,7 @@ public ResponseEntity> handleNotFoundException( handleNotAllowedException(HttpServletRequest request, NotAllowedException ex) { - log.error("NotAllowedException {}\n", request.getRequestURI(), ex); + log.error("NotAllowedException {} \n", request.getRequestURI(), ex); return ResponseEntity .status(NOT_ACCEPTABLE) @@ -66,7 +67,7 @@ public ResponseEntity> handleNotFoundException( handleIllegalArgumentException(HttpServletRequest request, IllegalArgumentException ex) { - log.error("IllegalArgumentException {} \n\n {}", + log.error("IllegalArgumentException {} \n {}", request.getRequestURI(), ex.getMessage()); return ResponseEntity @@ -83,7 +84,7 @@ public ResponseEntity> handleNotFoundException( handleValidationException(HttpServletRequest request, ValidationException ex) { - log.error("ValidationException {}\n", request.getRequestURI(), ex); + log.error("ValidationException {} \n", request.getRequestURI(), ex); return ResponseEntity .badRequest() @@ -98,7 +99,7 @@ public ResponseEntity> handleNotFoundException( handleMissingServletRequestParameterException(HttpServletRequest request, MissingServletRequestParameterException ex) { - log.error("handleMissingServletRequestParameterException {}\n", request.getRequestURI(), ex); + log.error("handleMissingServletRequestParameterException {} \n", request.getRequestURI(), ex); return ResponseEntity .badRequest() @@ -132,7 +133,7 @@ public ResponseEntity> handleNotFoundException( handleMethodArgumentNotValidException(HttpServletRequest request, MethodArgumentNotValidException ex) { - log.error("handleMethodArgumentNotValidException {}\n", request.getRequestURI(), ex); + log.error("handleMethodArgumentNotValidException {} \n", request.getRequestURI(), ex); List> details = new ArrayList<>(); ex.getBindingResult() @@ -141,7 +142,7 @@ public ResponseEntity> handleNotFoundException( Map detail = new HashMap<>(); detail.put("objectName", fieldError.getObjectName()); detail.put("field", fieldError.getField()); - detail.put("rejectedValue", "" + fieldError.getRejectedValue()); + detail.put("rejectedValue", String.valueOf(fieldError.getRejectedValue())); detail.put("errorMessage", fieldError.getDefaultMessage()); details.add(detail); }); @@ -158,7 +159,7 @@ public ResponseEntity> handleNotFoundException( public ResponseEntity> handleAccessDeniedException(HttpServletRequest request, AccessDeniedException ex) { - log.error("handleAccessDeniedException {}\n", request.getRequestURI(), ex); + log.error("handleAccessDeniedException {} \n", request.getRequestURI(), ex); return ResponseEntity .status(FORBIDDEN) @@ -170,20 +171,32 @@ public ResponseEntity> handleNotFoundException( public ResponseEntity> handleUnauthorizedException(HttpServletRequest request, HttpClientErrorException ex) { - log.error("handleUnauthorizedException {}\n", request.getRequestURI(), ex); + log.error("handleUnauthorizedException {} \n", request.getRequestURI(), ex); return ResponseEntity .status(UNAUTHORIZED) .body(new ApiCallError<>("Unauthorized Access, check your credentials!", List.of(ex.getMessage() != null ? ex.getMessage() : ""))); } + + @ExceptionHandler(RefreshTokenException.class) + @ResponseStatus(BAD_REQUEST) + public ResponseEntity> + handleRefreshTokenException(HttpServletRequest request, RefreshTokenException ex) { + + log.error("handleRefreshTokenException {} \n", request.getRequestURI(), ex); + + return ResponseEntity + .status(BAD_REQUEST) + .body(new ApiCallError<>("Invalid refresh token operation!", List.of(ex.getMessage()))); + } @ExceptionHandler(Exception.class) @ResponseStatus(INTERNAL_SERVER_ERROR) public ResponseEntity> handleInternalServerError(HttpServletRequest request, Exception ex) { - log.error("handleInternalServerError {}\n", request.getRequestURI(), ex); + log.error("handleInternalServerError {} \n", request.getRequestURI(), ex); return ResponseEntity .status(INTERNAL_SERVER_ERROR) diff --git a/src/main/java/org/siriusxi/htec/fa/infra/config/MvcConfig.java b/src/main/java/org/siriusxi/fa/infra/config/MvcConfig.java similarity index 95% rename from src/main/java/org/siriusxi/htec/fa/infra/config/MvcConfig.java rename to src/main/java/org/siriusxi/fa/infra/config/MvcConfig.java index 3d24993..1cd8027 100644 --- a/src/main/java/org/siriusxi/htec/fa/infra/config/MvcConfig.java +++ b/src/main/java/org/siriusxi/fa/infra/config/MvcConfig.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.infra.config; +package org.siriusxi.fa.infra.config; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; diff --git a/src/main/java/org/siriusxi/htec/fa/infra/config/SwaggerConfig.java b/src/main/java/org/siriusxi/fa/infra/config/SwaggerConfig.java similarity index 84% rename from src/main/java/org/siriusxi/htec/fa/infra/config/SwaggerConfig.java rename to src/main/java/org/siriusxi/fa/infra/config/SwaggerConfig.java index 5280003..eef6e11 100644 --- a/src/main/java/org/siriusxi/htec/fa/infra/config/SwaggerConfig.java +++ b/src/main/java/org/siriusxi/fa/infra/config/SwaggerConfig.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.infra.config; +package org.siriusxi.fa.infra.config; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; @@ -9,6 +9,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import static io.swagger.v3.oas.models.security.SecurityScheme.Type.HTTP; + @Configuration public class SwaggerConfig { @@ -21,12 +23,12 @@ public OpenAPI customOpenAPI() { .components(new Components() .addSecuritySchemes("bearer-key", new SecurityScheme() - .type(SecurityScheme.Type.HTTP) + .type(HTTP) .scheme("bearer") .bearerFormat("JWT"))) .info(new Info() .title("REST API for Flight Advisor Service.") - .version(appVersion) + .version(this.appVersion) .license(new License() .name("MIT License") .url("https://springdoc.org"))); diff --git a/src/main/java/org/siriusxi/htec/fa/infra/config/json/ObjectMapperConfig.java b/src/main/java/org/siriusxi/fa/infra/config/json/ObjectMapperConfig.java similarity index 72% rename from src/main/java/org/siriusxi/htec/fa/infra/config/json/ObjectMapperConfig.java rename to src/main/java/org/siriusxi/fa/infra/config/json/ObjectMapperConfig.java index 038e733..97034a1 100644 --- a/src/main/java/org/siriusxi/htec/fa/infra/config/json/ObjectMapperConfig.java +++ b/src/main/java/org/siriusxi/fa/infra/config/json/ObjectMapperConfig.java @@ -1,11 +1,11 @@ -package org.siriusxi.htec.fa.infra.config.json; +package org.siriusxi.fa.infra.config.json; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import org.siriusxi.htec.fa.infra.config.json.serializer.LocalDateDeserializer; -import org.siriusxi.htec.fa.infra.config.json.serializer.LocalDateSerializer; -import org.siriusxi.htec.fa.infra.config.json.serializer.LocalDateTimeDeserializer; -import org.siriusxi.htec.fa.infra.config.json.serializer.LocalDateTimeSerializer; +import org.siriusxi.fa.infra.config.json.serializer.LocalDateDeserializer; +import org.siriusxi.fa.infra.config.json.serializer.LocalDateSerializer; +import org.siriusxi.fa.infra.config.json.serializer.LocalDateTimeDeserializer; +import org.siriusxi.fa.infra.config.json.serializer.LocalDateTimeSerializer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/org/siriusxi/htec/fa/infra/config/json/serializer/LocalDateDeserializer.java b/src/main/java/org/siriusxi/fa/infra/config/json/serializer/LocalDateDeserializer.java similarity index 91% rename from src/main/java/org/siriusxi/htec/fa/infra/config/json/serializer/LocalDateDeserializer.java rename to src/main/java/org/siriusxi/fa/infra/config/json/serializer/LocalDateDeserializer.java index 81093c5..34661a5 100644 --- a/src/main/java/org/siriusxi/htec/fa/infra/config/json/serializer/LocalDateDeserializer.java +++ b/src/main/java/org/siriusxi/fa/infra/config/json/serializer/LocalDateDeserializer.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.infra.config.json.serializer; +package org.siriusxi.fa.infra.config.json.serializer; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; diff --git a/src/main/java/org/siriusxi/htec/fa/infra/config/json/serializer/LocalDateSerializer.java b/src/main/java/org/siriusxi/fa/infra/config/json/serializer/LocalDateSerializer.java similarity index 91% rename from src/main/java/org/siriusxi/htec/fa/infra/config/json/serializer/LocalDateSerializer.java rename to src/main/java/org/siriusxi/fa/infra/config/json/serializer/LocalDateSerializer.java index 6525433..e58788f 100644 --- a/src/main/java/org/siriusxi/htec/fa/infra/config/json/serializer/LocalDateSerializer.java +++ b/src/main/java/org/siriusxi/fa/infra/config/json/serializer/LocalDateSerializer.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.infra.config.json.serializer; +package org.siriusxi.fa.infra.config.json.serializer; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; diff --git a/src/main/java/org/siriusxi/htec/fa/infra/config/json/serializer/LocalDateTimeDeserializer.java b/src/main/java/org/siriusxi/fa/infra/config/json/serializer/LocalDateTimeDeserializer.java similarity index 91% rename from src/main/java/org/siriusxi/htec/fa/infra/config/json/serializer/LocalDateTimeDeserializer.java rename to src/main/java/org/siriusxi/fa/infra/config/json/serializer/LocalDateTimeDeserializer.java index a26be82..2632da5 100644 --- a/src/main/java/org/siriusxi/htec/fa/infra/config/json/serializer/LocalDateTimeDeserializer.java +++ b/src/main/java/org/siriusxi/fa/infra/config/json/serializer/LocalDateTimeDeserializer.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.infra.config.json.serializer; +package org.siriusxi.fa.infra.config.json.serializer; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; diff --git a/src/main/java/org/siriusxi/htec/fa/infra/config/json/serializer/LocalDateTimeSerializer.java b/src/main/java/org/siriusxi/fa/infra/config/json/serializer/LocalDateTimeSerializer.java similarity index 91% rename from src/main/java/org/siriusxi/htec/fa/infra/config/json/serializer/LocalDateTimeSerializer.java rename to src/main/java/org/siriusxi/fa/infra/config/json/serializer/LocalDateTimeSerializer.java index 6b2d341..d27deda 100644 --- a/src/main/java/org/siriusxi/htec/fa/infra/config/json/serializer/LocalDateTimeSerializer.java +++ b/src/main/java/org/siriusxi/fa/infra/config/json/serializer/LocalDateTimeSerializer.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.infra.config.json.serializer; +package org.siriusxi.fa.infra.config.json.serializer; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; @@ -18,4 +18,3 @@ public void serialize(LocalDateTime value, JsonGenerator gen, gen.writeString(value.format(fmt)); } } - diff --git a/src/main/java/org/siriusxi/htec/fa/infra/exception/NotAllowedException.java b/src/main/java/org/siriusxi/fa/infra/exception/NotAllowedException.java similarity index 83% rename from src/main/java/org/siriusxi/htec/fa/infra/exception/NotAllowedException.java rename to src/main/java/org/siriusxi/fa/infra/exception/NotAllowedException.java index 52e2eea..343d651 100644 --- a/src/main/java/org/siriusxi/htec/fa/infra/exception/NotAllowedException.java +++ b/src/main/java/org/siriusxi/fa/infra/exception/NotAllowedException.java @@ -1,9 +1,14 @@ -package org.siriusxi.htec.fa.infra.exception; +package org.siriusxi.fa.infra.exception; + +import java.io.Serial; import static java.lang.String.format; import static java.lang.String.valueOf; public class NotAllowedException extends RuntimeException { + + @Serial + private static final long serialVersionUID = 6495843604522723819L; public NotAllowedException(String message) { super(message); diff --git a/src/main/java/org/siriusxi/htec/fa/infra/exception/NotFoundException.java b/src/main/java/org/siriusxi/fa/infra/exception/NotFoundException.java similarity index 80% rename from src/main/java/org/siriusxi/htec/fa/infra/exception/NotFoundException.java rename to src/main/java/org/siriusxi/fa/infra/exception/NotFoundException.java index ccc6724..8744468 100644 --- a/src/main/java/org/siriusxi/htec/fa/infra/exception/NotFoundException.java +++ b/src/main/java/org/siriusxi/fa/infra/exception/NotFoundException.java @@ -1,10 +1,16 @@ -package org.siriusxi.htec.fa.infra.exception; +package org.siriusxi.fa.infra.exception; + +import java.io.Serial; import static java.lang.String.format; import static java.lang.String.valueOf; public class NotFoundException extends RuntimeException { - + + + @Serial + private static final long serialVersionUID = -2031412312708381239L; + public NotFoundException(String message) { super(message); } diff --git a/src/main/java/org/siriusxi/fa/infra/exception/RefreshTokenException.java b/src/main/java/org/siriusxi/fa/infra/exception/RefreshTokenException.java new file mode 100644 index 0000000..d3d95c2 --- /dev/null +++ b/src/main/java/org/siriusxi/fa/infra/exception/RefreshTokenException.java @@ -0,0 +1,13 @@ +package org.siriusxi.fa.infra.exception; + +import java.io.Serial; + +public class RefreshTokenException extends RuntimeException { + + @Serial + private static final long serialVersionUID = 8896944115746520398L; + + public RefreshTokenException(String token, String message) { + super(String.format("Failed for [%s]: %s", token, message)); + } +} diff --git a/src/main/java/org/siriusxi/htec/fa/infra/mapper/AirportMapper.java b/src/main/java/org/siriusxi/fa/infra/mapper/AirportMapper.java similarity index 72% rename from src/main/java/org/siriusxi/htec/fa/infra/mapper/AirportMapper.java rename to src/main/java/org/siriusxi/fa/infra/mapper/AirportMapper.java index 9e9d3ea..2343f87 100644 --- a/src/main/java/org/siriusxi/htec/fa/infra/mapper/AirportMapper.java +++ b/src/main/java/org/siriusxi/fa/infra/mapper/AirportMapper.java @@ -1,16 +1,16 @@ -package org.siriusxi.htec.fa.infra.mapper; +package org.siriusxi.fa.infra.mapper; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.siriusxi.htec.fa.api.model.response.AirportView; -import org.siriusxi.htec.fa.api.model.upload.airport.AirportDto; -import org.siriusxi.htec.fa.domain.Airport; +import org.siriusxi.fa.api.model.response.AirportResponse; +import org.siriusxi.fa.api.model.upload.airport.AirportDto; +import org.siriusxi.fa.domain.Airport; import java.util.List; @Mapper(componentModel = "spring") public interface AirportMapper { - + @Mapping(target = "destinationRoutes", ignore = true) @Mapping(target = "sourceRoutes", ignore = true) @Mapping(target = "cityName", source = "city") @@ -18,19 +18,19 @@ public interface AirportMapper { @Mapping(target = "city.id", source = "cityId") @Mapping(target = "country.id", source = "countryId") Airport toModel(AirportDto airportDto); - + @Mapping(target = "id", expression = "java(null)") @Mapping(target = "icao", expression = "java(null)") @Mapping(target = "city", source = "airport.cityName") @Mapping(target = "country", source = "airport.countryName") @Mapping(target = "airport", source = "airport.name") - AirportView toTripView(Airport airport, int value); - + AirportResponse toTripView(Airport airport, int value); + @Mapping(target = "id", source = "airportId") @Mapping(target = "city", source = "cityName") @Mapping(target = "country", source = "countryName") @Mapping(target = "airport", source = "name") - AirportView toView(Airport airport); - - List toView(List airport); + AirportResponse toView(Airport airport); + + List toView(List airport); } \ No newline at end of file diff --git a/src/main/java/org/siriusxi/htec/fa/infra/mapper/CityMapper.java b/src/main/java/org/siriusxi/fa/infra/mapper/CityMapper.java similarity index 72% rename from src/main/java/org/siriusxi/htec/fa/infra/mapper/CityMapper.java rename to src/main/java/org/siriusxi/fa/infra/mapper/CityMapper.java index d539082..794e2de 100644 --- a/src/main/java/org/siriusxi/htec/fa/infra/mapper/CityMapper.java +++ b/src/main/java/org/siriusxi/fa/infra/mapper/CityMapper.java @@ -1,11 +1,11 @@ -package org.siriusxi.htec.fa.infra.mapper; +package org.siriusxi.fa.infra.mapper; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.siriusxi.htec.fa.api.model.request.CreateCityRequest; -import org.siriusxi.htec.fa.api.model.response.CityView; -import org.siriusxi.htec.fa.domain.City; -import org.siriusxi.htec.fa.domain.Country; +import org.siriusxi.fa.api.model.request.CreateCityRequest; +import org.siriusxi.fa.api.model.response.CityResponse; +import org.siriusxi.fa.domain.City; +import org.siriusxi.fa.domain.Country; import org.springframework.beans.factory.annotation.Autowired; import java.util.Collections; @@ -27,7 +27,7 @@ public abstract class CityMapper { @Mapping(target = "comments", expression = "java( city.getComments() != null ? commentMapper.toViews(city.getComments()) : null )") @Mapping(target = "country", source = "country.name") - public abstract CityView toView(City city); + public abstract CityResponse toView(City city); - public abstract List toViews(List city); + public abstract List toViews(List city); } \ No newline at end of file diff --git a/src/main/java/org/siriusxi/htec/fa/infra/mapper/CommentMapper.java b/src/main/java/org/siriusxi/fa/infra/mapper/CommentMapper.java similarity index 72% rename from src/main/java/org/siriusxi/htec/fa/infra/mapper/CommentMapper.java rename to src/main/java/org/siriusxi/fa/infra/mapper/CommentMapper.java index 0cf29fa..b82aa5e 100644 --- a/src/main/java/org/siriusxi/htec/fa/infra/mapper/CommentMapper.java +++ b/src/main/java/org/siriusxi/fa/infra/mapper/CommentMapper.java @@ -1,12 +1,12 @@ -package org.siriusxi.htec.fa.infra.mapper; +package org.siriusxi.fa.infra.mapper; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.siriusxi.htec.fa.api.model.request.CommentUpSrtRequest; -import org.siriusxi.htec.fa.api.model.response.CommentView; -import org.siriusxi.htec.fa.domain.City; -import org.siriusxi.htec.fa.domain.Comment; -import org.siriusxi.htec.fa.domain.User; +import org.siriusxi.fa.api.model.response.CommentResponse; +import org.siriusxi.fa.domain.City; +import org.siriusxi.fa.domain.Comment; +import org.siriusxi.fa.api.model.request.CommentUpSrtRequest; +import org.siriusxi.fa.domain.User; import java.time.LocalDateTime; import java.util.List; @@ -30,7 +30,7 @@ public interface CommentMapper { @Mapping(target = "id", source = "id") @Mapping(target = "by", source = "user.fullName") - CommentView toView(Comment comment); + CommentResponse toView(Comment comment); - List toViews(List comment); + List toViews(List comment); } diff --git a/src/main/java/org/siriusxi/fa/infra/mapper/CountryMapper.java b/src/main/java/org/siriusxi/fa/infra/mapper/CountryMapper.java new file mode 100644 index 0000000..e853ac6 --- /dev/null +++ b/src/main/java/org/siriusxi/fa/infra/mapper/CountryMapper.java @@ -0,0 +1,15 @@ +package org.siriusxi.fa.infra.mapper; + +import org.mapstruct.Mapper; +import org.siriusxi.fa.api.model.response.CountryResponse; +import org.siriusxi.fa.domain.Country; + +import java.util.Set; + +@Mapper(componentModel = "spring") +public interface CountryMapper { + + CountryResponse toView(Country country); + + Set toViews(Set countries); +} \ No newline at end of file diff --git a/src/main/java/org/siriusxi/htec/fa/infra/mapper/RouteMapper.java b/src/main/java/org/siriusxi/fa/infra/mapper/RouteMapper.java similarity index 76% rename from src/main/java/org/siriusxi/htec/fa/infra/mapper/RouteMapper.java rename to src/main/java/org/siriusxi/fa/infra/mapper/RouteMapper.java index 7e722bf..dc8541b 100644 --- a/src/main/java/org/siriusxi/htec/fa/infra/mapper/RouteMapper.java +++ b/src/main/java/org/siriusxi/fa/infra/mapper/RouteMapper.java @@ -1,9 +1,9 @@ -package org.siriusxi.htec.fa.infra.mapper; +package org.siriusxi.fa.infra.mapper; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.siriusxi.htec.fa.api.model.upload.route.RouteDto; -import org.siriusxi.htec.fa.domain.Route; +import org.siriusxi.fa.domain.Route; +import org.siriusxi.fa.api.model.upload.route.RouteDto; @Mapper(componentModel = "spring") public interface RouteMapper { diff --git a/src/main/java/org/siriusxi/fa/infra/mapper/UserMapper.java b/src/main/java/org/siriusxi/fa/infra/mapper/UserMapper.java new file mode 100644 index 0000000..f84ceea --- /dev/null +++ b/src/main/java/org/siriusxi/fa/infra/mapper/UserMapper.java @@ -0,0 +1,56 @@ +package org.siriusxi.fa.infra.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.siriusxi.fa.api.model.request.ChangePasswordRequest; +import org.siriusxi.fa.api.model.request.CreateUserRequest; +import org.siriusxi.fa.api.model.response.UserResponse; +import org.siriusxi.fa.domain.Role; +import org.siriusxi.fa.domain.User; +import org.siriusxi.fa.infra.UuidUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.Set; + +@Mapper(componentModel = "spring", + imports = UuidUtil.class) +public abstract class UserMapper { + + @Autowired + protected PasswordEncoder passwordEncoder; + + @Mapping(target = "id", ignore = true) + @Mapping(target = "comments", ignore = true) + @Mapping(target = "authorities", ignore = true) + @Mapping(target = "enabled", ignore = true) + @Mapping(target = "refreshToken", ignore = true) + @Mapping(target = "tokenExpiryDate", ignore = true) + @Mapping(target = "password", + expression = "java( passwordEncoder.encode(request.password()) )") + @Mapping(target = "userUuid", expression = "java( UuidUtil.newUuid() )") + public abstract User toUser(CreateUserRequest request); + + public abstract UserResponse toView(User user); + + protected String[] map(Set authorities) { + return authorities + .stream() + .map(Role::getAuthority) + .toArray(String[]::new); + } + + @Mapping(target = "id", ignore = true) + @Mapping(target = "comments", ignore = true) + @Mapping(target = "authorities", ignore = true) + @Mapping(target = "enabled", ignore = true) + @Mapping(target = "firstName", ignore = true) + @Mapping(target = "lastName", ignore = true) + @Mapping(target = "username", ignore = true) + @Mapping(target = "userUuid", ignore = true) + @Mapping(target = "refreshToken", expression = "java( null )") + @Mapping(target = "tokenExpiryDate", expression = "java( null )") + @Mapping(target = "password", expression = "java( passwordEncoder.encode(request.newPassword()) )") + public abstract void updatePassword(ChangePasswordRequest request, @MappingTarget User user); +} diff --git a/src/main/java/org/siriusxi/fa/infra/security/AuthJwtEntryPoint.java b/src/main/java/org/siriusxi/fa/infra/security/AuthJwtEntryPoint.java new file mode 100644 index 0000000..e7281eb --- /dev/null +++ b/src/main/java/org/siriusxi/fa/infra/security/AuthJwtEntryPoint.java @@ -0,0 +1,37 @@ +package org.siriusxi.fa.infra.security; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.log4j.Log4j2; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@Component +@Log4j2 +public class AuthJwtEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) + throws IOException { + log.error("Unauthorized error: {}", authException.getMessage()); + + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + + final Map body = new HashMap<>(); + body.put("status", HttpServletResponse.SC_UNAUTHORIZED); + body.put("error", "Unauthorized"); + body.put("message", authException.getMessage()); + body.put("path", request.getServletPath()); + + final ObjectMapper mapper = new ObjectMapper(); + mapper.writeValue(response.getOutputStream(), body); + } +} diff --git a/src/main/java/org/siriusxi/htec/fa/infra/security/JwtTokenFilter.java b/src/main/java/org/siriusxi/fa/infra/security/AuthJwtTokenFilter.java similarity index 58% rename from src/main/java/org/siriusxi/htec/fa/infra/security/JwtTokenFilter.java rename to src/main/java/org/siriusxi/fa/infra/security/AuthJwtTokenFilter.java index a906a9e..63b56ab 100644 --- a/src/main/java/org/siriusxi/htec/fa/infra/security/JwtTokenFilter.java +++ b/src/main/java/org/siriusxi/fa/infra/security/AuthJwtTokenFilter.java @@ -1,11 +1,14 @@ -package org.siriusxi.htec.fa.infra.security; +package org.siriusxi.fa.infra.security; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; -import org.siriusxi.htec.fa.repository.UserRepository; +import org.siriusxi.fa.infra.security.jwt.JwtTokenHelper; +import org.siriusxi.fa.repository.UserRepository; import org.springframework.http.HttpHeaders; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; @@ -17,52 +20,47 @@ import java.util.List; import java.util.Optional; -import static org.siriusxi.htec.fa.infra.security.jwt.JwtTokenHelper.*; import static org.springframework.util.StringUtils.hasText; @Component @Log4j2 -public class JwtTokenFilter extends OncePerRequestFilter { - +@RequiredArgsConstructor +public class AuthJwtTokenFilter extends OncePerRequestFilter { + private final UserRepository userRepository; - - public JwtTokenFilter(UserRepository userRepository) { - this.userRepository = userRepository; - } - + @Override protected void doFilterInternal(HttpServletRequest request, - HttpServletResponse response, - FilterChain chain) throws ServletException, IOException { - + @NonNull HttpServletResponse response, + @NonNull FilterChain chain) throws ServletException, IOException { + // Get authorization header and validate - var authToken = getJwtTokenIfValid(request - .getHeader(HttpHeaders.AUTHORIZATION)); - - String token; - + var authToken = getJwtAccessToken(request.getHeader(HttpHeaders.AUTHORIZATION)); + + String jwtToken; + if (authToken.isEmpty()) { chain.doFilter(request, response); return; - } else token = authToken.get(); - + } else jwtToken = authToken.get(); + // Get user identity and set it on the spring security context - var userDetails = userRepository - .findByUsernameIgnoreCase(getUsername(token)) - .orElse(null); + var userDetails = this.userRepository + .findByUsernameIgnoreCase(JwtTokenHelper.getUsernameFrom(jwtToken)) + .orElse(null); var authentication = new UsernamePasswordAuthenticationToken( - userDetails, null, - userDetails == null ? List.of() : userDetails.getAuthorities() + userDetails, null, + userDetails == null ? List.of() : userDetails.getAuthorities() ); - + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - + SecurityContextHolder.getContext().setAuthentication(authentication); - + chain.doFilter(request, response); } - + /** * This method take a header, and check if it contains authorization value, * if it is exists then validate this token. @@ -70,16 +68,14 @@ protected void doFilterInternal(HttpServletRequest request, * @param header that could contain authorization header * @return JWT token if exists and valid. */ - private Optional getJwtTokenIfValid(String header) { - String token; + private Optional getJwtAccessToken(String header) { //check header value is exists - if (hasText(header) && header.startsWith("Bearer ")) { + if (hasText(header) && header.startsWith(JwtTokenHelper.tokenPrefix())) { // Get jwt token and validate - token = header.split(" ")[1].trim(); - if (validate(token)) + var token = header.split(" ")[1].trim(); + if (JwtTokenHelper.validate(token)) return Optional.of(token); } - return Optional.empty(); } } diff --git a/src/main/java/org/siriusxi/htec/fa/infra/security/SecurityConfig.java b/src/main/java/org/siriusxi/fa/infra/security/SecurityConfig.java similarity index 56% rename from src/main/java/org/siriusxi/htec/fa/infra/security/SecurityConfig.java rename to src/main/java/org/siriusxi/fa/infra/security/SecurityConfig.java index 608e424..bd740a7 100644 --- a/src/main/java/org/siriusxi/htec/fa/infra/security/SecurityConfig.java +++ b/src/main/java/org/siriusxi/fa/infra/security/SecurityConfig.java @@ -1,19 +1,19 @@ -package org.siriusxi.htec.fa.infra.security; +package org.siriusxi.fa.infra.security; import lombok.extern.log4j.Log4j2; -import org.siriusxi.htec.fa.domain.Role; -import org.siriusxi.htec.fa.service.UserService; +import org.siriusxi.fa.domain.Role; +import org.siriusxi.fa.service.UserService; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @@ -21,11 +21,11 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; -import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; +import static org.springframework.http.HttpMethod.POST; +import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS; import static org.springframework.security.core.context.SecurityContextHolder.MODE_INHERITABLETHREADLOCAL; import static org.springframework.security.core.context.SecurityContextHolder.setStrategyName; -import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; @Log4j2 @Configuration @@ -35,30 +35,19 @@ ) public class SecurityConfig { - private final JwtTokenFilter jwtTokenFilter; - private final String allowedOrigins; - private final UserService userService; - private final PasswordEncoder passwordEncoder; - - public SecurityConfig(JwtTokenFilter jwtTokenFilter, - @Value("${app.allowedOrigins:*}") String allowedOrigins, - UserService userService, PasswordEncoder passwordEncoder) { - this.jwtTokenFilter = jwtTokenFilter; - this.allowedOrigins = allowedOrigins; - this.userService = userService; - this.passwordEncoder = passwordEncoder; - + public SecurityConfig() { // Inherit security context in async function calls setStrategyName(MODE_INHERITABLETHREADLOCAL); } // Configure DaoAuthenticationProvider for username and password @Bean - public DaoAuthenticationProvider authenticationProvider() { + public DaoAuthenticationProvider authenticationProvider(final PasswordEncoder passwordEncoder, + final UserService userService) { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); - authProvider.setUserDetailsService(userDetailsService()); - authProvider.setPasswordEncoder(this.passwordEncoder); + authProvider.setUserDetailsService(userService); + authProvider.setPasswordEncoder(passwordEncoder); return authProvider; } @@ -70,88 +59,62 @@ public AuthenticationManager authenticationManager( return authConfig.getAuthenticationManager(); } + // Core Security configurations @Bean - public UserDetailsService userDetailsService() { - return this.userService; - } - - // Security configurations - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain filterChain(final HttpSecurity http, + final AuthJwtTokenFilter authJwtTokenFilter, + final AuthJwtEntryPoint unauthorizedHandler) + throws Exception { // List of Swagger URLs, root page, Our public endpoints, images - final var AUTH_WHITELIST = new String[]{ - "/api-docs/**", "/webjars/**", "/public/**", - "/swagger-ui/**", "/doc/**", "/", "/index.html", - "/assets/**"}; + final var authWhitelist = new String[]{ + "/api-docs/**", "/webjars/**", "/auth/refresh_token", + "/auth/signin", "/auth/signup","/swagger-ui/**", + "/doc/**", "/", "/index.html", "/assets/**"}; http // Enable CORS - .cors() - .and() - + .cors(withDefaults()) //Disable CSRF - .csrf() - .disable() - + .csrf(AbstractHttpConfigurer::disable) // Set session management to stateless - .sessionManagement() - .sessionCreationPolicy(STATELESS) - .and() - + .sessionManagement(customizer -> customizer.sessionCreationPolicy(STATELESS)) // Set unauthorized requests exception handler - .exceptionHandling() - .authenticationEntryPoint( - (request, response, ex) -> { - log.error("Unauthorized request - {}", ex.getMessage()); - response.sendError(SC_UNAUTHORIZED, ex.getMessage()); - }) - .and() + .exceptionHandling(exceptionHandling -> exceptionHandling.authenticationEntryPoint(unauthorizedHandler)) // Set permission to allow open db-console .authorizeHttpRequests(auth -> - { - try { - auth.requestMatchers(antMatcher("/db-console/**")) - .permitAll() - .and() - // This will allow frames with same origin which is much more safe - .headers(headers -> - headers.frameOptions() - .sameOrigin() - .disable()); - } catch (Exception e) { - log.error("Exception in headers - {}", e.getMessage()); - } - }) + auth.requestMatchers("/db-console/**").permitAll()) + // This will allow frames with same origin which is much more safe + .headers(headers -> + headers.frameOptions(FrameOptionsConfig::sameOrigin).disable()) // Enable all whitelisted pages .authorizeHttpRequests(auth -> - auth.requestMatchers(AUTH_WHITELIST) - .permitAll()) + auth.requestMatchers(authWhitelist).permitAll()) // Only Admin Allowed to do the following .authorizeHttpRequests(auth -> // Upload files and manage countries auth.requestMatchers("/upload/**", "/countries/**") - .hasAuthority(Role.ADMIN). + .hasAuthority(Role.ADMIN) // Create cities - requestMatchers(HttpMethod.POST, "/cities") - .hasAuthority(Role.ADMIN)) + .requestMatchers(POST, "/cities") + .hasAuthority(Role.ADMIN) + .requestMatchers(POST,"/auth/change_password", "/auth/signout") + .hasAnyAuthority(Role.ADMIN, Role.CLIENT)) //Our private endpoints - .authorizeHttpRequests(auth -> - auth.anyRequest() - .authenticated()) + .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) // Add JWT token filter - .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class); + .addFilterBefore(authJwtTokenFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } // Used by spring security if CORS is enabled. @Bean - public CorsFilter corsFilter() { + public CorsFilter corsFilter(@Value("${app.allowedOrigins:*}") String allowedOrigins) { var source = new UrlBasedCorsConfigurationSource(); var config = new CorsConfiguration(); config.setAllowCredentials(true); diff --git a/src/main/java/org/siriusxi/htec/fa/infra/security/jwt/JwtTokenHelper.java b/src/main/java/org/siriusxi/fa/infra/security/jwt/JwtTokenHelper.java similarity index 82% rename from src/main/java/org/siriusxi/htec/fa/infra/security/jwt/JwtTokenHelper.java rename to src/main/java/org/siriusxi/fa/infra/security/jwt/JwtTokenHelper.java index f2c2541..54d194f 100644 --- a/src/main/java/org/siriusxi/htec/fa/infra/security/jwt/JwtTokenHelper.java +++ b/src/main/java/org/siriusxi/fa/infra/security/jwt/JwtTokenHelper.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.infra.security.jwt; +package org.siriusxi.fa.infra.security.jwt; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; @@ -17,7 +17,7 @@ import static java.lang.String.format; /** - * Helper class to build, and verify a JWT token and extract the claim from a token. + * A JWT Helper class to build, and verify a JWT token and extract user information and claims from a token. * * @author Mohamed Taman */ @@ -35,14 +35,13 @@ private JwtTokenHelper() { * @param username of the login user. * @return a valid JWT token. */ - public static String generateAccessToken(int id, String username) { - var now = System.currentTimeMillis(); + public static String generateJwtToken(int id, String username) { return Jwts .builder() .setId(String.valueOf(id)) .setSubject(format("%d,%s", id, username)) .setIssuer(JwtConfig.ISSUER) - .setIssuedAt(new Date(now)) + .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(Date.from(ZonedDateTime.now() .plusDays(JwtConfig.TOKEN_EXPIRY_DURATION) .toInstant())) @@ -53,11 +52,11 @@ public static String generateAccessToken(int id, String username) { /** * Extracts the User id claim from the JWT token * - * @param token - token to analyze + * @param jwtToken - token to analyze * @return the User id claim contained in the token */ - public static int getUserId(String token) { - return Integer.parseInt(getClaims(token) + public static int getUserIdFrom(String jwtToken) { + return Integer.parseInt(getClaims(jwtToken) .getSubject() .split(",")[0]); } @@ -65,11 +64,11 @@ public static int getUserId(String token) { /** * Extracts the username claim from the JWT token * - * @param token - token to analyze + * @param jwtToken - token to analyze * @return the Username claim contained in the token */ - public static String getUsername(String token) { - return getClaims(token) + public static String getUsernameFrom(String jwtToken) { + return getClaims(jwtToken) .getSubject() .split(",")[1]; } @@ -80,7 +79,7 @@ public static String getUsername(String token) { * @param token - token to analyze * @return the expiration date claim contained in the token */ - public static Date tokenExpiredAt(String token) { + public static Date getTokenExpiration(String token) { return getClaims(token) .getExpiration(); } @@ -137,9 +136,18 @@ public static String getRealBase64EncodedSecret() { public static byte[] getRealSecret() { return Keys.secretKeyFor(JwtConfig.SIGNATURE_ALGORITHM).getEncoded(); } + + public static String tokenPrefix(){ + return JwtConfig.TOKEN_PREFIX; + } + + public static int refreshTokenExpiration(){ + return JwtConfig.REFRESH_TOKEN_EXPIRATION; + } /** - * jwt configuration interface. + * jwt configurations. + * TODO add all config in application.yaml */ private static class JwtConfig { static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS512; @@ -151,6 +159,7 @@ private static class JwtConfig { static final String TOKEN_PREFIX = "Bearer "; static final String ISSUER = "siriusx.io"; static final int TOKEN_EXPIRY_DURATION = 7; // In days + static final int REFRESH_TOKEN_EXPIRATION = 14; // In days private JwtConfig() { } diff --git a/src/main/java/org/siriusxi/htec/fa/infra/security/password/PasswordManager.java b/src/main/java/org/siriusxi/fa/infra/security/password/PasswordManager.java similarity index 89% rename from src/main/java/org/siriusxi/htec/fa/infra/security/password/PasswordManager.java rename to src/main/java/org/siriusxi/fa/infra/security/password/PasswordManager.java index cf5cd12..027d723 100644 --- a/src/main/java/org/siriusxi/htec/fa/infra/security/password/PasswordManager.java +++ b/src/main/java/org/siriusxi/fa/infra/security/password/PasswordManager.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.infra.security.password; +package org.siriusxi.fa.infra.security.password; import lombok.experimental.UtilityClass; import org.springframework.context.annotation.Bean; diff --git a/src/main/java/org/siriusxi/htec/fa/repository/AirportRepository.java b/src/main/java/org/siriusxi/fa/repository/AirportRepository.java similarity index 83% rename from src/main/java/org/siriusxi/htec/fa/repository/AirportRepository.java rename to src/main/java/org/siriusxi/fa/repository/AirportRepository.java index df0912c..9c62807 100644 --- a/src/main/java/org/siriusxi/htec/fa/repository/AirportRepository.java +++ b/src/main/java/org/siriusxi/fa/repository/AirportRepository.java @@ -1,7 +1,8 @@ -package org.siriusxi.htec.fa.repository; +package org.siriusxi.fa.repository; -import org.siriusxi.htec.fa.domain.Airport; -import org.siriusxi.htec.fa.domain.City; +import lombok.NonNull; +import org.siriusxi.fa.domain.Airport; +import org.siriusxi.fa.domain.City; import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.Cacheable; import org.springframework.data.jpa.repository.Query; @@ -23,7 +24,8 @@ public interface AirportRepository extends CrudRepository { @Cacheable @Override - Optional findById(Integer id); + @NonNull + Optional findById(@NonNull Integer id); @Query(""" SELECT a FROM Airport a diff --git a/src/main/java/org/siriusxi/htec/fa/repository/CityRepository.java b/src/main/java/org/siriusxi/fa/repository/CityRepository.java similarity index 91% rename from src/main/java/org/siriusxi/htec/fa/repository/CityRepository.java rename to src/main/java/org/siriusxi/fa/repository/CityRepository.java index 6a9691b..373fcd1 100644 --- a/src/main/java/org/siriusxi/htec/fa/repository/CityRepository.java +++ b/src/main/java/org/siriusxi/fa/repository/CityRepository.java @@ -1,7 +1,7 @@ -package org.siriusxi.htec.fa.repository; +package org.siriusxi.fa.repository; -import org.siriusxi.htec.fa.domain.City; -import org.siriusxi.htec.fa.domain.Country; +import org.siriusxi.fa.domain.City; +import org.siriusxi.fa.domain.Country; import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.Cacheable; import org.springframework.data.repository.CrudRepository; diff --git a/src/main/java/org/siriusxi/htec/fa/repository/CommentRepository.java b/src/main/java/org/siriusxi/fa/repository/CommentRepository.java similarity index 84% rename from src/main/java/org/siriusxi/htec/fa/repository/CommentRepository.java rename to src/main/java/org/siriusxi/fa/repository/CommentRepository.java index 1dc0975..e37da60 100644 --- a/src/main/java/org/siriusxi/htec/fa/repository/CommentRepository.java +++ b/src/main/java/org/siriusxi/fa/repository/CommentRepository.java @@ -1,8 +1,8 @@ -package org.siriusxi.htec.fa.repository; +package org.siriusxi.fa.repository; -import org.siriusxi.htec.fa.domain.City; -import org.siriusxi.htec.fa.domain.Comment; -import org.siriusxi.htec.fa.domain.User; +import org.siriusxi.fa.domain.City; +import org.siriusxi.fa.domain.Comment; +import org.siriusxi.fa.domain.User; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; diff --git a/src/main/java/org/siriusxi/htec/fa/repository/CountryRepository.java b/src/main/java/org/siriusxi/fa/repository/CountryRepository.java similarity index 85% rename from src/main/java/org/siriusxi/htec/fa/repository/CountryRepository.java rename to src/main/java/org/siriusxi/fa/repository/CountryRepository.java index 198c309..191c068 100644 --- a/src/main/java/org/siriusxi/htec/fa/repository/CountryRepository.java +++ b/src/main/java/org/siriusxi/fa/repository/CountryRepository.java @@ -1,6 +1,6 @@ -package org.siriusxi.htec.fa.repository; +package org.siriusxi.fa.repository; -import org.siriusxi.htec.fa.domain.Country; +import org.siriusxi.fa.domain.Country; import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.Cacheable; import org.springframework.data.repository.CrudRepository; @@ -19,7 +19,7 @@ public interface CountryRepository extends CrudRepository { Set findAllByNameIgnoreCaseIsLike(String name); /** - * Return the country if exist, else save and return it. + * Return the country if existed, else save and return it. * * @param name of the country. * @return the found or saved country. diff --git a/src/main/java/org/siriusxi/htec/fa/repository/RouteRepository.java b/src/main/java/org/siriusxi/fa/repository/RouteRepository.java similarity index 73% rename from src/main/java/org/siriusxi/htec/fa/repository/RouteRepository.java rename to src/main/java/org/siriusxi/fa/repository/RouteRepository.java index 0090825..c5eecc8 100644 --- a/src/main/java/org/siriusxi/htec/fa/repository/RouteRepository.java +++ b/src/main/java/org/siriusxi/fa/repository/RouteRepository.java @@ -1,8 +1,9 @@ -package org.siriusxi.htec.fa.repository; +package org.siriusxi.fa.repository; -import org.siriusxi.htec.fa.domain.Route; -import org.siriusxi.htec.fa.domain.RoutePK; -import org.siriusxi.htec.fa.domain.vo.RouteView; +import lombok.NonNull; +import org.siriusxi.fa.domain.Route; +import org.siriusxi.fa.domain.RoutePK; +import org.siriusxi.fa.domain.vo.RouteView; import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.CacheEvict; import org.springframework.data.jpa.repository.Query; @@ -18,7 +19,7 @@ public interface RouteRepository extends CrudRepository { @Query(value = """ - SELECT new org.siriusxi.htec.fa.domain.vo.RouteView + SELECT new org.siriusxi.fa.domain.vo.RouteView (routePK.source, routePK.destination, price) FROM Route ORDER BY routePK.source ASC @@ -36,9 +37,9 @@ WHERE routePK IN (:routePKs) @CacheEvict(key = "#p0.routePK.source + #p0.routePK.destination") @Override - S save(S s); + @NonNull S save(@NonNull S s); @CacheEvict(allEntries = true) @Override - Iterable saveAll(Iterable iterable); + @NonNull Iterable saveAll(@NonNull Iterable iterable); } diff --git a/src/main/java/org/siriusxi/fa/repository/UserRepository.java b/src/main/java/org/siriusxi/fa/repository/UserRepository.java new file mode 100644 index 0000000..f79849e --- /dev/null +++ b/src/main/java/org/siriusxi/fa/repository/UserRepository.java @@ -0,0 +1,62 @@ +package org.siriusxi.fa.repository; + +import lombok.NonNull; +import org.siriusxi.fa.domain.User; +import org.siriusxi.fa.infra.exception.NotFoundException; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.Caching; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Repository +@CacheConfig(cacheNames = "users") +public interface UserRepository extends CrudRepository { + + @Modifying + @Query(""" + UPDATE User user + SET user.refreshToken = null, + user.tokenExpiryDate = null + WHERE user.id = :userId + """) + void invalidateRefreshTokenById(Integer userId); + + @Override + @CacheEvict(allEntries = true) + @NonNull List saveAll(@NonNull Iterable entities); + + @Override + @Caching(evict = { + @CacheEvict(key = "#p0.id"), + @CacheEvict(key = "#p0.username") + }) + @NonNull S save(@NonNull S entity); + + @Override + @Cacheable + @NonNull + Optional findById(@NonNull Integer id); + + @Cacheable + default User getById(Integer id) { + return findById(id) + .filter(User::isEnabled) + .orElseThrow(() -> new NotFoundException(User.class, id)); + + } + + @Cacheable + Optional findByUsernameIgnoreCase(String username); + + Optional findByRefreshToken(UUID token); + + +} diff --git a/src/main/java/org/siriusxi/fa/service/CityMgmtService.java b/src/main/java/org/siriusxi/fa/service/CityMgmtService.java new file mode 100644 index 0000000..fd0c94c --- /dev/null +++ b/src/main/java/org/siriusxi/fa/service/CityMgmtService.java @@ -0,0 +1,169 @@ +package org.siriusxi.fa.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.siriusxi.fa.api.model.request.CommentUpSrtRequest; +import org.siriusxi.fa.api.model.request.CreateCityRequest; +import org.siriusxi.fa.api.model.request.SearchAirportRequest; +import org.siriusxi.fa.api.model.request.SearchCityRequest; +import org.siriusxi.fa.api.model.response.AirportResponse; +import org.siriusxi.fa.api.model.response.CityResponse; +import org.siriusxi.fa.api.model.response.CommentResponse; +import org.siriusxi.fa.domain.City; +import org.siriusxi.fa.domain.Comment; +import org.siriusxi.fa.domain.Country; +import org.siriusxi.fa.domain.User; +import org.siriusxi.fa.infra.exception.NotAllowedException; +import org.siriusxi.fa.infra.exception.NotFoundException; +import org.siriusxi.fa.infra.mapper.AirportMapper; +import org.siriusxi.fa.infra.mapper.CityMapper; +import org.siriusxi.fa.infra.mapper.CommentMapper; +import org.siriusxi.fa.repository.AirportRepository; +import org.siriusxi.fa.repository.CityRepository; +import org.siriusxi.fa.repository.CommentRepository; +import org.siriusxi.fa.repository.CountryRepository; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static java.util.Objects.isNull; + +@RequiredArgsConstructor +@Log4j2 +@Service +public class CityMgmtService { + + private static final String LIKE = "%"; + private final CityRepository cityRepository; + private final CountryRepository countryRepository; + private final CommentRepository commentRepository; + private final CommentMapper commentMapper; + private final CityMapper cityMapper; + private final AirportRepository airportRepository; + private final AirportMapper airportMapper; + + + @Transactional + public CityResponse addCity(CreateCityRequest cityRequest) { + + Country country; + + if (cityRequest.countryId() != 0) + country = + this.countryRepository + .findById(cityRequest.countryId()) + .orElseThrow(() -> new NotFoundException(Country.class, + cityRequest.countryId())); + else + country = this.countryRepository.findOrSaveBy(cityRequest.country()); + + + City city = this.cityRepository.findOrSaveBy(country, cityRequest.name(), + cityRequest.description()); + + /* + When we create a new city it doesn't have any comments, + so don't show empty comments. + */ + city.setComments(null); + + return this.cityMapper.toView(city); + } + + @Transactional(readOnly = true) + public List searchCities(SearchCityRequest request, int cLimit) { + + var searchWord = LIKE + .concat(isNull(request.name()) ? + "" : + request.name().strip()) + .concat(LIKE); + + if (cLimit > 0) + return this.cityMapper.toViews(this.cityRepository + .findByNameIgnoreCaseIsLike(searchWord) + .stream() + .peek(city -> city + .setComments(this.commentRepository + .findByCity(city, PageRequest.of(0, cLimit)))) + .toList()); + else + return this.cityMapper.toViews(this.cityRepository.findByNameIgnoreCaseIsLike(searchWord)); + + } + + // Airport management + + public List searchAirports(SearchAirportRequest request, int cityId) { + + String searchWord = isNull(request.name()) ? "" : request.name(); + searchWord = LIKE.concat(searchWord).concat(LIKE); + + return this.airportMapper + .toView(this.airportRepository + .findAirportsByCityAndNameIgnoreCaseIsLike( + new City(cityId), searchWord)); + } + + // City Comments management + + @Transactional + public CommentResponse addComment(User user, int cityId, + CommentUpSrtRequest request) { + // Chick if the city is already exists + City city = getCityIfExists(cityId); + + return this.commentMapper + .toView(this.commentRepository + .save(this.commentMapper + .toModel(request, user, city))); + } + + @Transactional + public void updateComment(User user, int cityId, int commentId, + CommentUpSrtRequest request) { + // Chick if the city is already exists + City city = getCityIfExists(cityId); + + // If comment is exist then proceed + this.commentRepository.findById(commentId) + // Check if the comment is associated with given city + .flatMap(comment -> + this.commentRepository.findByIdAndCity(comment.getId(), city)) + /* If exist check if user is allowed to update it */ + .map(found -> + this.commentRepository + .findByIdAndCityAndUser(found.getId(), city, user) + // If user is not allowed throw exception + .orElseThrow(() -> + new NotAllowedException(Comment.class, found.getId(), "Update"))) + .ifPresent(comment -> this.commentMapper + .toView(this.commentRepository + .save(this.commentMapper + .toUpdatedModel(request, comment.getId(), user, city)))); + } + + public void deleteComment(User user, int cityId, int commentId) { + // Chick if the city is already exists + City city = getCityIfExists(cityId); + + // In all cases found or not it will return 200 because delete is Idempotent + // If comment is exist then proceed + this.commentRepository.findById(commentId) + // Check if the comment is associated with given city + .flatMap(comment -> this.commentRepository.findByIdAndCity(comment.getId(), city)) + // If exist check if user is allowed to delete it + .ifPresent(found -> this.commentRepository.delete(this.commentRepository + .findByIdAndCityAndUser(found.getId(), city, user) + // If user is not allowed throw exception + .orElseThrow(() -> new NotAllowedException(Comment.class, found.getId(), "Delete")))); + } + + private City getCityIfExists(int cityId) { + return this.cityRepository + .findById(cityId) + .orElseThrow(() -> new NotFoundException(City.class, cityId)); + } +} diff --git a/src/main/java/org/siriusxi/htec/fa/service/TravelService.java b/src/main/java/org/siriusxi/fa/service/TravelService.java similarity index 50% rename from src/main/java/org/siriusxi/htec/fa/service/TravelService.java rename to src/main/java/org/siriusxi/fa/service/TravelService.java index e2d9ebe..af60012 100644 --- a/src/main/java/org/siriusxi/htec/fa/service/TravelService.java +++ b/src/main/java/org/siriusxi/fa/service/TravelService.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa.service; +package org.siriusxi.fa.service; import es.usc.citius.hipster.algorithm.Algorithm; import es.usc.citius.hipster.algorithm.Hipster; @@ -7,16 +7,16 @@ import es.usc.citius.hipster.model.impl.WeightedNode; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; -import org.siriusxi.htec.fa.api.model.response.AirportView; -import org.siriusxi.htec.fa.api.model.response.TripView; -import org.siriusxi.htec.fa.domain.Airport; -import org.siriusxi.htec.fa.domain.Route; -import org.siriusxi.htec.fa.domain.RoutePK; -import org.siriusxi.htec.fa.infra.algorithm.distance.DistanceAlgorithm; -import org.siriusxi.htec.fa.infra.algorithm.distance.Point; -import org.siriusxi.htec.fa.infra.mapper.AirportMapper; -import org.siriusxi.htec.fa.repository.AirportRepository; -import org.siriusxi.htec.fa.repository.RouteRepository; +import org.siriusxi.fa.api.model.response.AirportResponse; +import org.siriusxi.fa.api.model.response.TripResponse; +import org.siriusxi.fa.domain.Airport; +import org.siriusxi.fa.domain.Route; +import org.siriusxi.fa.domain.RoutePK; +import org.siriusxi.fa.infra.algorithm.distance.DistanceAlgorithm; +import org.siriusxi.fa.infra.algorithm.distance.Point; +import org.siriusxi.fa.infra.mapper.AirportMapper; +import org.siriusxi.fa.repository.AirportRepository; +import org.siriusxi.fa.repository.RouteRepository; import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; @@ -30,7 +30,6 @@ import static java.lang.Double.parseDouble; import static java.util.Objects.requireNonNull; -import static org.siriusxi.htec.fa.infra.algorithm.distance.DistanceAlgorithm.getAlgorithm; /** * Travel Service class is the core of travel calculations from city A to City B. @@ -64,115 +63,115 @@ @Service @CacheConfig(cacheNames = "travels") public class TravelService { - + private final RouteRepository routeRepository; private final AirportRepository airportRepository; private final AirportMapper airportMapper; - private final DistanceAlgorithm orthodromicAlgorithm = getAlgorithm(DistanceAlgorithm.Type.ORTHODROMIC); + private final DistanceAlgorithm orthodromicAlgorithm = DistanceAlgorithm.getAlgorithm(DistanceAlgorithm.Type.ORTHODROMIC); private final DecimalFormat formatter = new DecimalFormat("#.00"); private static FinalTrip getTrip(Algorithm> - .SearchResult result) { - + WeightedNode> + .SearchResult result) { + var paths = result.getOptimalPaths().get(0); int lastIndex = result.getOptimalPaths().get(0).size() - 1; - + // calculate final cost return new FinalTrip( - paths.get(0), - lastIndex != 0 ? paths.subList(1, lastIndex) : Collections.emptyList(), - paths.get(lastIndex), - result.getGoalNode().getCost()); + paths.get(0), + lastIndex != 0 ? paths.subList(1, lastIndex) : Collections.emptyList(), + paths.get(lastIndex), + result.getGoalNode().getCost()); } - + @Transactional(readOnly = true) @Cacheable(key = "#p0 + #p1") - public List travel(String from, String to) { + public List travel(String from, String to) { var trip = findShortestPath(from, to); return buildFinalTripViews(trip); } - - private List buildFinalTripViews(FinalTrip trip) { - var trips = new ArrayList(); + + private List buildFinalTripViews(FinalTrip trip) { + var trips = new ArrayList(); if (trip.through.isEmpty()) { - routeRepository - .findById(new RoutePK(trip.start(), trip.end())) - .ifPresent(route -> - trips.add(newTripView( - route.getSourceAirport(), - route.getDestinationAirport(), null, route.getPrice(), - calculateDistance(route)))); + this.routeRepository + .findById(new RoutePK(trip.start(), trip.end())) + .ifPresent(route -> + trips.add(newTripView( + route.getSourceAirport(), + route.getDestinationAirport(), null, route.getPrice(), + calculateDistance(route)))); } else { var airports = new LinkedList(); //Get source - airportRepository.findByCode(trip.start()).ifPresent(airports::add); + this.airportRepository.findByCode(trip.start()).ifPresent(airports::add); // Get in between destinations trip.through() - .forEach(code -> - airportRepository - .findByCode(code) - .ifPresent(airports::add)); + .forEach(code -> + this.airportRepository + .findByCode(code) + .ifPresent(airports::add)); // Get destination - airportRepository.findByCode(trip.end()).ifPresent(airports::add); - + this.airportRepository.findByCode(trip.end()).ifPresent(airports::add); + var allDest = new ArrayList<>(trip.through()); allDest.add(trip.end()); - + String previous = trip.start(); List routePKs = new ArrayList<>(); - + for (String airportCode : allDest) { routePKs.add(new RoutePK(previous, airportCode)); previous = airportCode; } // Create final trip trips.add(newTripView( - airports.getFirst(), - airports.getLast(), - airports - .subList(1, airports.size() - 1) - .stream() - .map(airport -> airportMapper.toTripView(airport, 0)) - .toList(), - // Calculate final price - routeRepository.getTripCost(routePKs), - // Calculate total distance - routeRepository - .findAllByRoutePKIn(routePKs) - .stream() - .mapToDouble(this::calculateDistance) - .sum())); + airports.getFirst(), + airports.getLast(), + airports + .subList(1, airports.size() - 1) + .stream() + .map(airport -> this.airportMapper.toTripView(airport, 0)) + .toList(), + // Calculate final price + this.routeRepository.getTripCost(routePKs), + // Calculate total distance + this.routeRepository + .findAllByRoutePKIn(routePKs) + .stream() + .mapToDouble(this::calculateDistance) + .sum())); } - + return trips; } - + private double calculateDistance(Route route) { - return orthodromicAlgorithm - .calculate( - new Point( - route.getSourceAirport().getLatitude().doubleValue(), - route.getSourceAirport().getLongitude().doubleValue()), - new Point( - route.getDestinationAirport().getLatitude().doubleValue(), - route.getDestinationAirport().getLongitude().doubleValue())); + return this.orthodromicAlgorithm + .calculate( + new Point( + route.getSourceAirport().getLatitude().doubleValue(), + route.getSourceAirport().getLongitude().doubleValue()), + new Point( + route.getDestinationAirport().getLatitude().doubleValue(), + route.getDestinationAirport().getLongitude().doubleValue())); } - - private TripView newTripView(Airport src, Airport dest, - List through, - double cost, double distance) { - return new TripView( - airportMapper.toTripView(src, 0), through, - airportMapper.toTripView(dest, 0), - new TripView.Price(cost, "US"), - new TripView.Distance(parseDouble(formatter.format(distance)), "KM")); + + private TripResponse newTripView(Airport src, Airport dest, + List through, + double cost, double distance) { + return new TripResponse( + this.airportMapper.toTripView(src, 0), through, + this.airportMapper.toTripView(dest, 0), + new TripResponse.Price(cost, "US"), + new TripResponse.Distance(parseDouble(this.formatter.format(distance)), "KM")); } - + private FinalTrip findShortestPath(String from, String to) { requireNonNull(from, "From cant be null."); requireNonNull(to, "To cant be null."); - + // Create graph builder with vertices and cost var builder = buildGraph(GraphBuilder.create()); @@ -181,43 +180,43 @@ private FinalTrip findShortestPath(String from, String to) { where vertices are Strings and cost values are just doubles. */ var graph = builder.createDirectedGraph(); - + // Create the search problem. For graph problems, just use // the GraphSearchProblem util class to generate the problem with ease. var problem = GraphSearchProblem - .startingFrom(from).in(graph) - .takeCostsFromEdges() - .build(); - + .startingFrom(from).in(graph) + .takeCostsFromEdges() + .build(); + // Search the shortest path from source to destination final var result = Hipster.createDijkstra(problem).search(to); - + return getTrip(result); } - + private GraphBuilder buildGraph(GraphBuilder graph) { // Get all routes - routeRepository - .getAll() - //Build vertices and their price as wight - .forEach(route -> - graph - .connect(route.source()) - .to(route.destination()) - .withEdge(route.price())); + this.routeRepository + .getAll() + //Build vertices and their price as wight + .forEach(route -> + graph + .connect(route.source()) + .to(route.destination()) + .withEdge(route.price())); return graph; } - - public List findAirportsForCityOrCountry(String name) { - return airportMapper - .toView(airportRepository - .findAirportsByCityOrCountryName(name.toLowerCase())); + + public List findAirportsForCityOrCountry(String name) { + return this.airportMapper + .toView(this.airportRepository + .findAirportsByCityOrCountryName(name.toLowerCase())); } - + private record FinalTrip(String start, List through, String end, double totalCost) { } - + } diff --git a/src/main/java/org/siriusxi/fa/service/UserService.java b/src/main/java/org/siriusxi/fa/service/UserService.java new file mode 100644 index 0000000..d37f386 --- /dev/null +++ b/src/main/java/org/siriusxi/fa/service/UserService.java @@ -0,0 +1,143 @@ +package org.siriusxi.fa.service; + +import jakarta.validation.ValidationException; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.siriusxi.fa.api.model.request.ChangePasswordRequest; +import org.siriusxi.fa.api.model.request.CreateUserRequest; +import org.siriusxi.fa.api.model.response.UserResponse; +import org.siriusxi.fa.domain.Role; +import org.siriusxi.fa.domain.User; +import org.siriusxi.fa.infra.UuidUtil; +import org.siriusxi.fa.infra.exception.RefreshTokenException; +import org.siriusxi.fa.infra.mapper.UserMapper; +import org.siriusxi.fa.infra.security.jwt.JwtTokenHelper; +import org.siriusxi.fa.repository.UserRepository; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static java.lang.String.format; +import static java.time.Instant.now; +import static java.time.temporal.ChronoUnit.DAYS; + +@RequiredArgsConstructor +@Log4j2 +@Service +public class UserService implements UserDetailsService { + + private final UserRepository repository; + private final UserMapper userMapper; + + /** + * This method is responsible to create a new user. + * + * @param request data to create a new user + * @return UserView of created user. + */ + @Transactional + public UserResponse create(CreateUserRequest request) { + if (this.repository.findByUsernameIgnoreCase(request.username()).isPresent()) + throw new ValidationException("Username exists!"); + + // Add user + User user = this.repository.save(this.userMapper.toUser(request)); + + // Add user authorities + user.setAuthorities(Role.CLIENT); + + // Update user to add authorities + this.repository.save(user); + + System.out.println("Saved User: "+ user.toString()); + + // Return user view + return this.userMapper.toView(user); + } + + @Override + public UserDetails loadUserByUsername(String username) { + return this.repository + .findByUsernameIgnoreCase(username) + .orElseThrow( + () -> new UsernameNotFoundException( + format("User with username - %s, not found", username))); + } + + + /** + * Method to generate the refresh token + * + * @param user required to hve a new refresh token + * @return the updated user with new refresh token + */ + @Transactional + public User generateRefreshToken(User user) { + user.setTokenExpiryDate(now().plus(JwtTokenHelper.refreshTokenExpiration(), DAYS)); + user.setRefreshToken(UuidUtil.newUuid()); + // Update user to add authorities + return this.repository.save(user); + + } + + /** + * This method return the user based on a valid refresh token. + * + * @param token refresh token to find user. + * @return an optional user represented by the token. + */ + public Optional findByRefreshToken(String token) { + return this.repository.findByRefreshToken(UuidUtil.uuidFrom(token)); + } + + /** + * Method to verify the user refresh token validly, either it is expired or not. + * If valid return the same user object, else throw an exception indicating that user token is expired. + * + * @param user to validate its token expiration status. + * @return user that has valid refresh token. + */ + @Transactional + public User verifyRefreshTokenExpiration(User user) { + if (user.getTokenExpiryDate().compareTo(now()) < 0) { + this.invalidateRefreshTokenById(user.getId()); + throw new RefreshTokenException(user.getRefreshToken().toString(), + "Refresh token expired. Please signin!"); + } + + return user; + } + + /** + * Method to reset user refresh token and its expiration date. + * + * @param userId to reset token values for. + */ + public void invalidateRefreshTokenById(Integer userId) { + this.repository.invalidateRefreshTokenById(userId); + } + + /** + * Method update user password. + * + * @param userId of user need to be updated. + */ + public void updatePassword(Integer userId, ChangePasswordRequest changePassRequest) { + if (!changePassRequest.newPassword().equals(changePassRequest.newPasswordAgain())) + throw new IllegalArgumentException("Passwords doesn't match!"); + + var user = this.repository.findById(userId).orElseThrow( + () -> new UsernameNotFoundException( + format("User with id - %d, not found", userId))); + this.userMapper.updatePassword(changePassRequest, user); + this.repository.save(user); + } + + public UserMapper mapper() { + return this.userMapper; + } +} diff --git a/src/main/java/org/siriusxi/htec/fa/api/controller/AuthController.java b/src/main/java/org/siriusxi/htec/fa/api/controller/AuthController.java deleted file mode 100644 index 163c35a..0000000 --- a/src/main/java/org/siriusxi/htec/fa/api/controller/AuthController.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.siriusxi.htec.fa.api.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; -import org.siriusxi.htec.fa.api.model.request.AuthRequest; -import org.siriusxi.htec.fa.api.model.request.CreateUserRequest; -import org.siriusxi.htec.fa.api.model.response.UserView; -import org.siriusxi.htec.fa.domain.User; -import org.siriusxi.htec.fa.infra.security.jwt.JwtTokenHelper; -import org.siriusxi.htec.fa.service.UserService; -import org.springframework.http.HttpHeaders; -import org.springframework.http.ResponseEntity; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.client.HttpClientErrorException; - -import static org.springframework.http.HttpStatus.UNAUTHORIZED; - -/** - * Authentication controller used to handle users authentication. - * - * @author Mohamed Taman - * @version 1.0 - */ - -// TODO: add refresh token method, change password. -@Log4j2 -@Tag(name = "Authentication", - description = "A set of public APIs, for managing user authentication, and the registration.") -@RestController -@RequestMapping("public") -@RequiredArgsConstructor -public class AuthController { - - private final AuthenticationManager authenticationManager; - private final UserService userService; - - @Operation(description = """ - An API call to authenticate user before using the system, - and if successful a valid token is returned. - """) - @PostMapping(value = "login") - public ResponseEntity authenticate(@RequestBody @Valid AuthRequest request) { - try { - var authenticate = authenticationManager - .authenticate(new UsernamePasswordAuthenticationToken( - request.username(), - request.password())); - - User user = (User) authenticate.getPrincipal(); - - return ResponseEntity.ok() - .header(HttpHeaders.AUTHORIZATION, - JwtTokenHelper.generateAccessToken( - user.getId(), - user.getUsername())) - .body(userService.mapper().toView(user)); - } catch (BadCredentialsException ex) { - throw new HttpClientErrorException(UNAUTHORIZED, UNAUTHORIZED.getReasonPhrase()); - } - } - - @Operation(description = """ - An API call, to register the user, - to be able to authenticate and use the system. - """) - @PostMapping(value = "register") - public UserView register(@RequestBody @Valid CreateUserRequest userRequest) { - - log.debug("User to be created: {}", userRequest); - return userService.create(userRequest); - } - -} diff --git a/src/main/java/org/siriusxi/htec/fa/api/model/response/AirportView.java b/src/main/java/org/siriusxi/htec/fa/api/model/response/AirportView.java deleted file mode 100644 index 3f00486..0000000 --- a/src/main/java/org/siriusxi/htec/fa/api/model/response/AirportView.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.siriusxi.htec.fa.api.model.response; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; - -public record AirportView(@JsonInclude(NON_NULL) - @JsonProperty Integer id, - @JsonProperty String airport, - @JsonProperty String city, - @JsonProperty String country, - @JsonInclude(NON_NULL) - @JsonProperty String iata, - @JsonInclude(NON_NULL) - @JsonProperty String icao) { -} - - - diff --git a/src/main/java/org/siriusxi/htec/fa/api/model/response/CityView.java b/src/main/java/org/siriusxi/htec/fa/api/model/response/CityView.java deleted file mode 100644 index ed183c6..0000000 --- a/src/main/java/org/siriusxi/htec/fa/api/model/response/CityView.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.siriusxi.htec.fa.api.model.response; - - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.List; - -import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; - -public record CityView(@JsonProperty int id, - @JsonProperty String name, - @JsonProperty String country, - @JsonProperty String description, - @JsonInclude(NON_NULL) - @JsonProperty List comments) { -} diff --git a/src/main/java/org/siriusxi/htec/fa/api/model/response/CountryView.java b/src/main/java/org/siriusxi/htec/fa/api/model/response/CountryView.java deleted file mode 100644 index ec2b014..0000000 --- a/src/main/java/org/siriusxi/htec/fa/api/model/response/CountryView.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.siriusxi.htec.fa.api.model.response; - - -import com.fasterxml.jackson.annotation.JsonProperty; - -public record CountryView(@JsonProperty int id, - @JsonProperty String name) { -} diff --git a/src/main/java/org/siriusxi/htec/fa/api/model/response/UserView.java b/src/main/java/org/siriusxi/htec/fa/api/model/response/UserView.java deleted file mode 100644 index d9100ad..0000000 --- a/src/main/java/org/siriusxi/htec/fa/api/model/response/UserView.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.siriusxi.htec.fa.api.model.response; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.Arrays; -import java.util.Objects; - -public record UserView(@JsonProperty String id, - @JsonProperty String username, - @JsonProperty String firstName, - @JsonProperty String lastName, - @JsonProperty String[] authorities) { - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - UserView userView = (UserView) o; - return Objects.equals(id, userView.id) && - Objects.equals(username, userView.username) && - Objects.equals(firstName, userView.firstName) && - Objects.equals(lastName, userView.lastName) && - Arrays.equals(authorities, userView.authorities); - } - - @Override - public int hashCode() { - int result = Objects.hash(id, username, firstName, lastName); - result = 31 * result + Arrays.hashCode(authorities); - return result; - } - - @Override - public String toString() { - return "UserView{ id= %s, username= %s, firstName= %s, lastName= %s , authorities= %s}" - .formatted(id, username, firstName, lastName, Arrays.toString(authorities)); - } -} diff --git a/src/main/java/org/siriusxi/htec/fa/infra/Utils.java b/src/main/java/org/siriusxi/htec/fa/infra/Utils.java deleted file mode 100644 index 8651030..0000000 --- a/src/main/java/org/siriusxi/htec/fa/infra/Utils.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.siriusxi.htec.fa.infra; - -import java.util.UUID; - -public final class Utils { - - private Utils() { - } - - public static String generateUuid() { - return UUID.randomUUID().toString(); - } -} diff --git a/src/main/java/org/siriusxi/htec/fa/infra/mapper/CountryMapper.java b/src/main/java/org/siriusxi/htec/fa/infra/mapper/CountryMapper.java deleted file mode 100644 index 75329e3..0000000 --- a/src/main/java/org/siriusxi/htec/fa/infra/mapper/CountryMapper.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.siriusxi.htec.fa.infra.mapper; - -import org.mapstruct.Mapper; -import org.siriusxi.htec.fa.api.model.response.CountryView; -import org.siriusxi.htec.fa.domain.Country; - -import java.util.Set; - -@Mapper(componentModel = "spring") -public interface CountryMapper { - - CountryView toView(Country country); - - Set toViews(Set countries); -} \ No newline at end of file diff --git a/src/main/java/org/siriusxi/htec/fa/infra/mapper/UserMapper.java b/src/main/java/org/siriusxi/htec/fa/infra/mapper/UserMapper.java deleted file mode 100644 index df34ad3..0000000 --- a/src/main/java/org/siriusxi/htec/fa/infra/mapper/UserMapper.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.siriusxi.htec.fa.infra.mapper; - -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; -import org.siriusxi.htec.fa.api.model.request.CreateUserRequest; -import org.siriusxi.htec.fa.api.model.response.UserView; -import org.siriusxi.htec.fa.domain.Role; -import org.siriusxi.htec.fa.domain.User; -import org.siriusxi.htec.fa.infra.Utils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.Set; - -@Mapper(componentModel = "spring", imports = Utils.class) -public abstract class UserMapper { - - @Autowired - protected PasswordEncoder passwordEncoder; - - @Mapping(target = "id", ignore = true) - @Mapping(target = "comments", ignore = true) - @Mapping(target = "authorities", ignore = true) - @Mapping(target = "enabled", ignore = true) - @Mapping(target = "password", - expression = "java( passwordEncoder.encode(request.password()) )") - @Mapping(target = "userUuid", expression = "java( Utils.generateUuid() )") - public abstract User toUser(CreateUserRequest request); - - public abstract UserView toView(User user); - - protected String[] map(Set authorities) { - return authorities - .stream() - .map(Role::getAuthority) - .toArray(String[]::new); - } -} diff --git a/src/main/java/org/siriusxi/htec/fa/repository/UserRepository.java b/src/main/java/org/siriusxi/htec/fa/repository/UserRepository.java deleted file mode 100644 index 4f607c0..0000000 --- a/src/main/java/org/siriusxi/htec/fa/repository/UserRepository.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.siriusxi.htec.fa.repository; - -import org.siriusxi.htec.fa.domain.User; -import org.siriusxi.htec.fa.infra.exception.NotFoundException; -import org.springframework.cache.annotation.CacheConfig; -import org.springframework.cache.annotation.CacheEvict; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.cache.annotation.Caching; -import org.springframework.data.repository.CrudRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -@CacheConfig(cacheNames = "users") -public interface UserRepository extends CrudRepository { - - @CacheEvict(allEntries = true) - List saveAll(Iterable entities); - - @Caching(evict = { - @CacheEvict(key = "#p0.id"), - @CacheEvict(key = "#p0.username") - }) - S save(S entity); - - @Cacheable - Optional findById(Integer id); - - @Cacheable - default User getById(Integer id) { - Optional optionalUser = findById(id); - if (optionalUser.isEmpty()) { - throw new NotFoundException(User.class, id); - } - if (!optionalUser.get().isEnabled()) { - throw new NotFoundException(User.class, id); - } - return optionalUser.get(); - } - - @Cacheable - Optional findByUsernameIgnoreCase(String username); - -} diff --git a/src/main/java/org/siriusxi/htec/fa/service/CityMgmtService.java b/src/main/java/org/siriusxi/htec/fa/service/CityMgmtService.java deleted file mode 100644 index 71738a6..0000000 --- a/src/main/java/org/siriusxi/htec/fa/service/CityMgmtService.java +++ /dev/null @@ -1,167 +0,0 @@ -package org.siriusxi.htec.fa.service; - -import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; -import org.siriusxi.htec.fa.api.model.request.CommentUpSrtRequest; -import org.siriusxi.htec.fa.api.model.request.CreateCityRequest; -import org.siriusxi.htec.fa.api.model.request.SearchAirportRequest; -import org.siriusxi.htec.fa.api.model.request.SearchCityRequest; -import org.siriusxi.htec.fa.api.model.response.AirportView; -import org.siriusxi.htec.fa.api.model.response.CityView; -import org.siriusxi.htec.fa.api.model.response.CommentView; -import org.siriusxi.htec.fa.domain.City; -import org.siriusxi.htec.fa.domain.Comment; -import org.siriusxi.htec.fa.domain.Country; -import org.siriusxi.htec.fa.domain.User; -import org.siriusxi.htec.fa.infra.exception.NotAllowedException; -import org.siriusxi.htec.fa.infra.exception.NotFoundException; -import org.siriusxi.htec.fa.infra.mapper.AirportMapper; -import org.siriusxi.htec.fa.infra.mapper.CityMapper; -import org.siriusxi.htec.fa.infra.mapper.CommentMapper; -import org.siriusxi.htec.fa.repository.AirportRepository; -import org.siriusxi.htec.fa.repository.CityRepository; -import org.siriusxi.htec.fa.repository.CommentRepository; -import org.siriusxi.htec.fa.repository.CountryRepository; -import org.springframework.data.domain.PageRequest; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.ArrayList; -import java.util.List; - -import static java.util.Objects.isNull; - -@RequiredArgsConstructor -@Log4j2 -@Service -public class CityMgmtService { - - private static final String LIKE = "%"; - private final CityRepository cityRepository; - private final CountryRepository countryRepository; - private final CommentRepository commentRepository; - private final CommentMapper commentMapper; - private final CityMapper cityMapper; - private final AirportRepository airportRepository; - private final AirportMapper airportMapper; - - - @Transactional - public CityView addCity(CreateCityRequest cityRequest) { - - Country country; - - if (cityRequest.countryId() != 0) { - country = - countryRepository - .findById(cityRequest.countryId()) - .orElseThrow(() -> new NotFoundException(Country.class, - cityRequest.countryId())); - } else { - country = countryRepository.findOrSaveBy(cityRequest.country()); - } - - City city = cityRepository.findOrSaveBy(country, cityRequest.name(), - cityRequest.description()); - - /* - When we create a new city it doesn't have any comments, - so don't show empty comments. - */ - city.setComments(null); - - return cityMapper.toView(city); - } - - @Transactional(readOnly = true) - public List searchCities(SearchCityRequest request, int cLimit) { - List cities = new ArrayList<>(); - - String searchWord = isNull(request.name()) ? "" : request.name(); - searchWord = LIKE.concat(searchWord).concat(LIKE); - - if (cLimit > 0) - for (City city : cityRepository.findByNameIgnoreCaseIsLike(searchWord)) { - city.setComments(commentRepository - .findByCity(city, PageRequest.of(0, cLimit))); - cities.add(city); - } - else - cities = cityRepository.findByNameIgnoreCaseIsLike(searchWord); - - return cityMapper.toViews(cities); - } - - // Airport management - - public List searchAirports(SearchAirportRequest request, int cityId) { - - String searchWord = isNull(request.name()) ? "" : request.name(); - searchWord = LIKE.concat(searchWord).concat(LIKE); - - return airportMapper - .toView(airportRepository - .findAirportsByCityAndNameIgnoreCaseIsLike( - new City(cityId), searchWord)); - } - - // City Comments management - - @Transactional - public CommentView addComment(User user, int cityId, - CommentUpSrtRequest request) { - // Chick if the city is already exists - City city = getCityIfExists(cityId); - - return commentMapper - .toView(commentRepository - .save(commentMapper - .toModel(request, user, city))); - } - - @Transactional - public void updateComment(User user, int cityId, int commentId, - CommentUpSrtRequest request) { - // Chick if the city is already exists - City city = getCityIfExists(cityId); - - // If comment is exist then proceed - commentRepository.findById(commentId) - // Check if the comment is associated with given city - .flatMap(comment -> - commentRepository.findByIdAndCity(comment.getId(), city)) - /* If exist check if user is allowed to update it */ - .map(found -> - commentRepository - .findByIdAndCityAndUser(found.getId(), city, user) - // If user is not allowed throw exception - .orElseThrow(() -> - new NotAllowedException(Comment.class, found.getId(), "Update"))) - .ifPresent(comment -> commentMapper - .toView(commentRepository - .save(commentMapper - .toUpdatedModel(request, comment.getId(), user, city)))); - } - - public void deleteComment(User user, int cityId, int commentId) { - // Chick if the city is already exists - City city = getCityIfExists(cityId); - - // In all cases found or not it will return 200 because delete is Idempotent - // If comment is exist then proceed - commentRepository.findById(commentId) - // Check if the comment is associated with given city - .flatMap(comment -> commentRepository.findByIdAndCity(comment.getId(), city)) - // If exist check if user is allowed to delete it - .ifPresent(found -> commentRepository.delete(commentRepository - .findByIdAndCityAndUser(found.getId(), city, user) - // If user is not allowed throw exception - .orElseThrow(() -> new NotAllowedException(Comment.class, found.getId(), "Delete")))); - } - - private City getCityIfExists(int cityId) { - return cityRepository - .findById(cityId) - .orElseThrow(() -> new NotFoundException(City.class, cityId)); - } -} diff --git a/src/main/java/org/siriusxi/htec/fa/service/UserService.java b/src/main/java/org/siriusxi/htec/fa/service/UserService.java deleted file mode 100644 index 3852243..0000000 --- a/src/main/java/org/siriusxi/htec/fa/service/UserService.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.siriusxi.htec.fa.service; - -import jakarta.validation.ValidationException; -import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; -import org.siriusxi.htec.fa.api.model.request.CreateUserRequest; -import org.siriusxi.htec.fa.api.model.response.UserView; -import org.siriusxi.htec.fa.domain.User; -import org.siriusxi.htec.fa.infra.mapper.UserMapper; -import org.siriusxi.htec.fa.repository.UserRepository; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import static java.lang.String.format; -import static org.siriusxi.htec.fa.domain.Role.CLIENT; - -@RequiredArgsConstructor -@Log4j2 -@Service -public class UserService implements UserDetailsService { - - private final UserRepository repository; - private final UserMapper userMapper; - - /** - * This method is responsible to create a new user. - * - * @param request data to create a new user - * @return UserView of created user. - */ - @Transactional - public UserView create(CreateUserRequest request) { - - if (repository.findByUsernameIgnoreCase(request.username()).isPresent()) { - throw new ValidationException("Username exists!"); - } - - // Add user - User user = repository.save(userMapper.toUser(request)); - // Add user authorities - user.setAuthorities(CLIENT); - // Update user to add authorities - repository.save(user); - - // Return user view - return userMapper.toView(user); - } - - @Override - public UserDetails loadUserByUsername(String username) { - return repository - .findByUsernameIgnoreCase(username) - .orElseThrow( - () -> new UsernameNotFoundException( - format("User with username - %s, not found", username))); - } - - public UserMapper mapper() { - return this.userMapper; - } -} diff --git a/src/main/resources/application-prod.yaml b/src/main/resources/application-prod.yaml index 9ab6c11..a5cb079 100644 --- a/src/main/resources/application-prod.yaml +++ b/src/main/resources/application-prod.yaml @@ -26,7 +26,6 @@ logging.level: cache: info web: debug siriusxi: - htec: - fa: debug + fa: debug web: debug root: info diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 7732bd8..eecb581 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,101 +1,105 @@ # Development profile ## Custom application properties app: - version: @project.version@ - api: - version: 'v1' + version: @project.version@ + api: + version: 'v1' + security: + jwt: + secret: "5s2BCxpNxdI58mAaAllBr/psyu91aCusvXy+kew9ytxQ/zhRtvcZMxVAjmkq8pVkSMA81+9Y0D4W06qGre+hYg==" + expiration: 7 #Days + refresh-expiration: 14 #Days # Spring properties spring: - application: - name: Flight-Advisor-Service - - servlet: - multipart: - enabled: true - max-file-size: 10MB - # Database configurations - datasource: - url: jdbc:h2:./target/db/flightDB - username: sa - hikari: - initialization-fail-timeout: 60000 - connection-test-query: SELECT 1 - maximum-pool-size: 10 - h2: - console: - enabled: true - path: /db-console - jpa: - open-in-view: false - hibernate: - ddl-auto: none - # The SQL dialect makes Hibernate generate better SQL for the chosen database - properties: - hibernate: - dialect: org.hibernate.dialect.H2Dialect - generate_statistics: true - # Switching on batch insert - # Take the time to group inserts by entity, creating larger batches. - order_inserts: true - order_updates: true - # collect inserts in batches of 100 - jdbc: - size: 100 - sql: - init: - data-locations: classpath*:db/data.sql - schema-locations: classpath*:db/schema.sql - platform: h2 - mode: always + application: + name: Flight-Advisor-Service + servlet: + multipart: + enabled: true + max-file-size: 10MB + max-request-size: 10MB + # Database configurations + datasource: + url: jdbc:h2:./target/db/flightDB + username: sa + hikari: + initialization-fail-timeout: 60000 + connection-test-query: SELECT 1 + maximum-pool-size: 10 + h2: + console: + enabled: true + path: /db-console + jpa: + open-in-view: false + hibernate: + ddl-auto: none + # The SQL dialect makes Hibernate generate better SQL for the chosen database + properties: + hibernate: + dialect: org.hibernate.dialect.H2Dialect + generate_statistics: true + # Switching on batch insert + # Take the time to group inserts by entity, creating larger batches. + order_inserts: true + order_updates: true + # collect inserts in batches of 100 + jdbc: + size: 100 + sql: + init: + data-locations: classpath*:db/data.sql + schema-locations: classpath*:db/schema.sql + platform: h2 + mode: always # Server configs server: - port: 8090 - ## Should be included to show message, if not error message will be empty - error: - include-message: always - include-binding-errors: always - servlet: - context-path: /api/${app.api.version}/flight/service + port: 8090 + ## Should be included to show message, if not error message will be empty + error: + include-message: always + include-binding-errors: always + servlet: + context-path: /api/${app.api.version}/flight/service # Application health and information management management: - info.git: - mode: full - enabled: true - endpoints.web.exposure.include: "*" - endpoint: - shutdown.enabled: false - health.show-details: always + info.git: + mode: full + enabled: true + endpoints.web.exposure.include: "*" + endpoint: + shutdown.enabled: false + health.show-details: always # Logging settings logging.level: - org: - hibernate: - SQL: debug - type.descriptor.sql.BasicBinder: trace - springframework: - cache: trace - web: debug - siriusxi: - htec: - fa: debug - web: debug - root: info + org: + hibernate: + SQL: debug + type.descriptor.sql.BasicBinder: trace + springframework: + cache: trace + web: debug + siriusxi: + fa: debug + web: debug + root: info # OpenAPI (Swagger) settings springdoc: - # swagger-ui custom path - swagger-ui: - path: /doc/index.html - operationsSorter: alpha - tagsSorter: alpha - displayRequestDuration: true - syntaxHighlight: - theme: arta - # /api-docs endpoint custom path - api-docs: - path: /api-docs - # to display the actuator endpoints. - show-actuator: false + # swagger-ui custom path + swagger-ui: + path: /doc/index.html + operationsSorter: alpha + tagsSorter: alpha + displayRequestDuration: true + syntaxHighlight: + theme: arta + # /api-docs endpoint custom path + api-docs: + path: /api-docs + # to display the actuator endpoints. + show-actuator: false diff --git a/src/main/resources/db/schema.sql b/src/main/resources/db/schema.sql index 490f8d7..86958ca 100644 --- a/src/main/resources/db/schema.sql +++ b/src/main/resources/db/schema.sql @@ -12,13 +12,15 @@ DROP TABLE IF EXISTS users; CREATE TABLE IF NOT EXISTS users ( - id INT AUTO_INCREMENT PRIMARY KEY, - user_uuid UUID UNIQUE NOT NULL, - first_name CHARACTER VARYING(100) NOT NULL, - last_name CHARACTER VARYING(100) NOT NULL, - username VARCHAR_IGNORECASE(255) UNIQUE NOT NULL, - password VARCHAR_IGNORECASE(255) NOT NULL, - enabled BOOLEAN NOT NULL + id INT AUTO_INCREMENT PRIMARY KEY, + user_uuid UUID UNIQUE NOT NULL, + first_name CHARACTER VARYING(100) NOT NULL, + last_name CHARACTER VARYING(100) NOT NULL, + username VARCHAR_IGNORECASE(255) UNIQUE NOT NULL, + password VARCHAR_IGNORECASE(255) NOT NULL, + enabled BOOLEAN NOT NULL, + refresh_token UUID UNIQUE, + token_expiry_date DATETIME ); ---- End - User table definition diff --git a/src/test/java/org/siriusxi/htec/fa/CsvToBeanTests.java b/src/test/java/org/siriusxi/fa/CsvToBeanTests.java similarity index 87% rename from src/test/java/org/siriusxi/htec/fa/CsvToBeanTests.java rename to src/test/java/org/siriusxi/fa/CsvToBeanTests.java index a1c3e38..f48ebf6 100644 --- a/src/test/java/org/siriusxi/htec/fa/CsvToBeanTests.java +++ b/src/test/java/org/siriusxi/fa/CsvToBeanTests.java @@ -1,12 +1,12 @@ -package org.siriusxi.htec.fa; +package org.siriusxi.fa; import com.opencsv.bean.CsvToBeanBuilder; import lombok.extern.log4j.Log4j2; import org.assertj.core.api.Assertions; -import org.siriusxi.htec.fa.api.model.upload.airport.AirportDto; -import org.siriusxi.htec.fa.api.model.upload.airport.verifer.AirportBeanVerifier; -import org.siriusxi.htec.fa.api.model.upload.route.RouteDto; -import org.siriusxi.htec.fa.api.model.upload.route.verifer.RouteBeanVerifier; +import org.siriusxi.fa.api.model.upload.airport.AirportDto; +import org.siriusxi.fa.api.model.upload.airport.verifer.AirportBeanVerifier; +import org.siriusxi.fa.api.model.upload.route.RouteDto; +import org.siriusxi.fa.api.model.upload.route.verifer.RouteBeanVerifier; import java.io.FileNotFoundException; import java.io.FileReader; diff --git a/src/test/java/org/siriusxi/htec/fa/FlightAdvisorApplicationTests.java b/src/test/java/org/siriusxi/fa/FlightAdvisorApplicationTests.java similarity index 86% rename from src/test/java/org/siriusxi/htec/fa/FlightAdvisorApplicationTests.java rename to src/test/java/org/siriusxi/fa/FlightAdvisorApplicationTests.java index c8df0b6..6b030db 100644 --- a/src/test/java/org/siriusxi/htec/fa/FlightAdvisorApplicationTests.java +++ b/src/test/java/org/siriusxi/fa/FlightAdvisorApplicationTests.java @@ -1,4 +1,4 @@ -package org.siriusxi.htec.fa; +package org.siriusxi.fa; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -15,7 +15,7 @@ class FlightAdvisorApplicationTests { @Test void contextLoads() { - assertThat(context).isNotNull(); + assertThat(this.context).isNotNull(); } }