From 2973573e576384ccfd8e56c5657d8b4efbbc18fc Mon Sep 17 00:00:00 2001 From: mohammedsheshtar <112146380+mohammedsheshtar@users.noreply.github.com> Date: Tue, 8 Apr 2025 10:27:25 +0300 Subject: [PATCH 01/18] completed hello world exercise --- pom.xml | 12 ++++++++++++ .../coded/spring/ordering/HelloWorldController.kt | 10 ++++++++++ src/main/resources/application.properties | 1 + 3 files changed, 23 insertions(+) create mode 100644 src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt diff --git a/pom.xml b/pom.xml index 163ad53..aafb459 100644 --- a/pom.xml +++ b/pom.xml @@ -58,6 +58,18 @@ kotlin-test-junit5 test + + jakarta.inject + jakarta.inject-api + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.h2database + h2 + diff --git a/src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt b/src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt new file mode 100644 index 0000000..1725ef1 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt @@ -0,0 +1,10 @@ +package com.coded.spring.ordering + +import org.springframework.web.bind.annotation.* + +@RestController +class HelloWorldController { + + @GetMapping("/hello") + fun helloWorld() = "Hello World"; +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3704dc6..3259ff9 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,2 @@ spring.application.name=Kotlin.SpringbootV2 +server.port=9001 \ No newline at end of file From e1418dcd41a6da1f1e95e49f6f6aa914039abb67 Mon Sep 17 00:00:00 2001 From: mohammedsheshtar <112146380+mohammedsheshtar@users.noreply.github.com> Date: Tue, 8 Apr 2025 13:32:38 +0300 Subject: [PATCH 02/18] built a backend microservice that accepts and displays orders via our POST and GET services --- .../ordering/OnlineOrderingController.kt | 26 +++++++++++++++++ .../coded/spring/ordering/OrderRepository.kt | 28 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 src/main/kotlin/com/coded/spring/ordering/OnlineOrderingController.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/OrderRepository.kt diff --git a/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingController.kt b/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingController.kt new file mode 100644 index 0000000..41612cc --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingController.kt @@ -0,0 +1,26 @@ +package com.coded.spring.ordering + +import org.springframework.web.bind.annotation.* + +@RestController +class OnlineOrderingController( + val orderRepository: OrderRepository +) { + + @GetMapping("/orders") + fun getOrders() = orderRepository.findAll() + + @PostMapping("/orders") + fun addOrders(@RequestBody request: RequestOrder): OnlineOrder { + return orderRepository.save(OnlineOrder( + user = request.user, + restaurant = request.restaurant, + items = request.items)) + } +} + +data class RequestOrder( + val user: String, + val restaurant: String, + val items: List +) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/OrderRepository.kt b/src/main/kotlin/com/coded/spring/ordering/OrderRepository.kt new file mode 100644 index 0000000..6448983 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/OrderRepository.kt @@ -0,0 +1,28 @@ +package com.coded.spring.ordering + +import jakarta.inject.Named +import jakarta.persistence.* +import org.springframework.data.jpa.repository.JpaRepository + +@Named +interface OrderRepository: JpaRepository + +@Entity +@Table(name = "orders") +data class OnlineOrder( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + + // included the line below because 'user' is a reserved keyword in SQL and threw an error because of it, column escapes it + @Column(name = "`user`") + var user: String, + + var restaurant: String, + + @CollectionTable + var items: List = listOf() + +){ + constructor() : this(null, "", "",listOf()) +} \ No newline at end of file From 8ceaeb0ffb765048e25f5941840cb46ac19d8a9d Mon Sep 17 00:00:00 2001 From: mohammedsheshtar <112146380+mohammedsheshtar@users.noreply.github.com> Date: Tue, 8 Apr 2025 15:51:17 +0300 Subject: [PATCH 03/18] Added annother variable in my data class to account for what time was the order placed and the earliest one has priority in the listing --- .../coded/spring/ordering/OnlineOrderingController.kt | 2 +- .../{OrderRepository.kt => OnlineOrderingRepository.kt} | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) rename src/main/kotlin/com/coded/spring/ordering/{OrderRepository.kt => OnlineOrderingRepository.kt} (71%) diff --git a/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingController.kt b/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingController.kt index 41612cc..7cafb4a 100644 --- a/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingController.kt @@ -8,7 +8,7 @@ class OnlineOrderingController( ) { @GetMapping("/orders") - fun getOrders() = orderRepository.findAll() + fun getOrders() = orderRepository.findAll().sortedBy {it.timeOrdered} @PostMapping("/orders") fun addOrders(@RequestBody request: RequestOrder): OnlineOrder { diff --git a/src/main/kotlin/com/coded/spring/ordering/OrderRepository.kt b/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingRepository.kt similarity index 71% rename from src/main/kotlin/com/coded/spring/ordering/OrderRepository.kt rename to src/main/kotlin/com/coded/spring/ordering/OnlineOrderingRepository.kt index 6448983..044b0cb 100644 --- a/src/main/kotlin/com/coded/spring/ordering/OrderRepository.kt +++ b/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingRepository.kt @@ -3,6 +3,8 @@ package com.coded.spring.ordering import jakarta.inject.Named import jakarta.persistence.* import org.springframework.data.jpa.repository.JpaRepository +import org.hibernate.annotations.CreationTimestamp +import java.time.LocalDateTime @Named interface OrderRepository: JpaRepository @@ -21,8 +23,11 @@ data class OnlineOrder( var restaurant: String, @CollectionTable - var items: List = listOf() + var items: List = listOf(), + + @CreationTimestamp + var timeOrdered: LocalDateTime? = null ){ - constructor() : this(null, "", "",listOf()) + constructor() : this(null, "", "",listOf(), null) } \ No newline at end of file From f98ce60cc54ddb134b419a0518c5f5fbd54a3a65 Mon Sep 17 00:00:00 2001 From: mohammedsheshtar <112146380+mohammedsheshtar@users.noreply.github.com> Date: Wed, 9 Apr 2025 07:13:51 +0300 Subject: [PATCH 04/18] Replaced H2 with PostgreSQL for persistent order storage --- pom.xml | 4 ++++ .../com/coded/spring/ordering/OnlineOrderingController.kt | 2 +- .../com/coded/spring/ordering/OnlineOrderingRepository.kt | 7 ++++--- src/main/resources/application.properties | 6 +++++- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index aafb459..479d7d3 100644 --- a/pom.xml +++ b/pom.xml @@ -53,6 +53,10 @@ spring-boot-starter-test test + + org.postgresql + postgresql + org.jetbrains.kotlin kotlin-test-junit5 diff --git a/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingController.kt b/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingController.kt index 7cafb4a..7176079 100644 --- a/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingController.kt @@ -15,7 +15,7 @@ class OnlineOrderingController( return orderRepository.save(OnlineOrder( user = request.user, restaurant = request.restaurant, - items = request.items)) + items = request.items.joinToString(", "))) } } diff --git a/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingRepository.kt b/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingRepository.kt index 044b0cb..720907b 100644 --- a/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingRepository.kt +++ b/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingRepository.kt @@ -22,12 +22,13 @@ data class OnlineOrder( var restaurant: String, - @CollectionTable - var items: List = listOf(), + //included the line below to indicate so that we can keep the items as a CSV in our database + @Column(columnDefinition = "TEXT") + var items: String, @CreationTimestamp var timeOrdered: LocalDateTime? = null ){ - constructor() : this(null, "", "",listOf(), null) + constructor() : this(null, "", "","", null) } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3259ff9..66a96ae 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,2 +1,6 @@ spring.application.name=Kotlin.SpringbootV2 -server.port=9001 \ No newline at end of file +server.port=9001 +spring.datasource.url=jdbc:postgresql://localhost:5432/OnlineOrderingDatabase +spring.datasource.username=postgres +spring.datasource.password=123 +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect \ No newline at end of file From 0e28866660d92c0beee426d3b145278e9d14937e Mon Sep 17 00:00:00 2001 From: mohammedsheshtar <112146380+mohammedsheshtar@users.noreply.github.com> Date: Wed, 9 Apr 2025 07:20:18 +0300 Subject: [PATCH 05/18] Added some comments for documentation --- .../com/coded/spring/ordering/OnlineOrderingController.kt | 3 +++ .../com/coded/spring/ordering/OnlineOrderingRepository.kt | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingController.kt b/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingController.kt index 7176079..feedb0d 100644 --- a/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingController.kt @@ -7,9 +7,11 @@ class OnlineOrderingController( val orderRepository: OrderRepository ) { + //this is the GET request, function getOrders() will be executed when this service is requested. It will display the current order list @GetMapping("/orders") fun getOrders() = orderRepository.findAll().sortedBy {it.timeOrdered} + //this is the POST request, function addOrders() will be executed when this service is requested. It will add a new order to the list @PostMapping("/orders") fun addOrders(@RequestBody request: RequestOrder): OnlineOrder { return orderRepository.save(OnlineOrder( @@ -19,6 +21,7 @@ class OnlineOrderingController( } } +// the DTO (Data Transfer Object) for our order list data class RequestOrder( val user: String, val restaurant: String, diff --git a/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingRepository.kt b/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingRepository.kt index 720907b..4e1200f 100644 --- a/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingRepository.kt +++ b/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingRepository.kt @@ -22,7 +22,7 @@ data class OnlineOrder( var restaurant: String, - //included the line below to indicate so that we can keep the items as a CSV in our database + //included the line below to specify that we want to keep the items as a CSV in our database @Column(columnDefinition = "TEXT") var items: String, From a2de943d24f2ba060b95b21da7a4f799b67d2bf4 Mon Sep 17 00:00:00 2001 From: mohammedsheshtar <112146380+mohammedsheshtar@users.noreply.github.com> Date: Sat, 12 Apr 2025 21:33:21 +0300 Subject: [PATCH 06/18] finished the bonus task where I was able to create a separate database for the items and connected it with the order database through orders PK. Additionally, I changed the placement of some of the source code to better match the Spring convention related to proper structuring of modules and their files --- .../ordering/OnlineOrderingController.kt | 29 ----------- .../spring/ordering/items/ItemsRepository.kt | 28 ++++++++++ .../orders/OnlineOrderingController.kt | 52 +++++++++++++++++++ .../{ => orders}/OnlineOrderingRepository.kt | 19 ++++--- 4 files changed, 92 insertions(+), 36 deletions(-) delete mode 100644 src/main/kotlin/com/coded/spring/ordering/OnlineOrderingController.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/items/ItemsRepository.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt rename src/main/kotlin/com/coded/spring/ordering/{ => orders}/OnlineOrderingRepository.kt (54%) diff --git a/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingController.kt b/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingController.kt deleted file mode 100644 index feedb0d..0000000 --- a/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingController.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.coded.spring.ordering - -import org.springframework.web.bind.annotation.* - -@RestController -class OnlineOrderingController( - val orderRepository: OrderRepository -) { - - //this is the GET request, function getOrders() will be executed when this service is requested. It will display the current order list - @GetMapping("/orders") - fun getOrders() = orderRepository.findAll().sortedBy {it.timeOrdered} - - //this is the POST request, function addOrders() will be executed when this service is requested. It will add a new order to the list - @PostMapping("/orders") - fun addOrders(@RequestBody request: RequestOrder): OnlineOrder { - return orderRepository.save(OnlineOrder( - user = request.user, - restaurant = request.restaurant, - items = request.items.joinToString(", "))) - } -} - -// the DTO (Data Transfer Object) for our order list -data class RequestOrder( - val user: String, - val restaurant: String, - val items: List -) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/items/ItemsRepository.kt b/src/main/kotlin/com/coded/spring/ordering/items/ItemsRepository.kt new file mode 100644 index 0000000..e7078b8 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/items/ItemsRepository.kt @@ -0,0 +1,28 @@ +package com.coded.spring.ordering.items + +import com.coded.spring.ordering.orders.OrderEntity +import com.fasterxml.jackson.annotation.JsonBackReference +import jakarta.inject.Named +import jakarta.persistence.* +import org.springframework.data.jpa.repository.JpaRepository + +@Named +interface ItemsRepository: JpaRepository + +@Entity +@Table(name = "items") +data class ItemsEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + + // maps each item to its parent order using the foreign key items.order_id → orders.id + @JoinColumn(name = "order_id") + @JsonBackReference + val order: OrderEntity, + val name: String, + + val price: Double +){ + constructor() : this(null, OrderEntity(), "", 0.0) +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt new file mode 100644 index 0000000..1c30d9f --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt @@ -0,0 +1,52 @@ +package com.coded.spring.ordering.orders + +import com.coded.spring.ordering.items.ItemsEntity +import com.coded.spring.ordering.items.ItemsRepository +import org.springframework.web.bind.annotation.* + +@RestController +class OnlineOrderingController( + val orderRepository: OrderRepository, + val itemsRepository: ItemsRepository +) { + + @GetMapping("/orders") + fun getOrders() = orderRepository.findAll().sortedBy { it.timeOrdered } + + @PostMapping("/orders") + fun addOrders(@RequestBody request: RequestOrder): OrderEntity { + //adding the new order into our database + val order = orderRepository.save( + OrderEntity( + user = request.user, + restaurant = request.restaurant + ) + ) + + //converting each item in our items objects list into an item entity to add them into the items database while also connecting each item to its order + val items = request.items.map { item -> + ItemsEntity( + order = order, + name = item.name, + price = item.price + ) + } + itemsRepository.saveAll(items) + + return order + } + +} + + +// the DTO (Data Transfer Object) for our orders and items list +data class RequestItem( + val name: String, + val price: Double +) + +data class RequestOrder( + val user: String, + val restaurant: String, + val items: List +) diff --git a/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingRepository.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingRepository.kt similarity index 54% rename from src/main/kotlin/com/coded/spring/ordering/OnlineOrderingRepository.kt rename to src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingRepository.kt index 4e1200f..790d10a 100644 --- a/src/main/kotlin/com/coded/spring/ordering/OnlineOrderingRepository.kt +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingRepository.kt @@ -1,5 +1,7 @@ -package com.coded.spring.ordering +package com.coded.spring.ordering.orders +import com.coded.spring.ordering.items.ItemsEntity +import com.fasterxml.jackson.annotation.JsonManagedReference import jakarta.inject.Named import jakarta.persistence.* import org.springframework.data.jpa.repository.JpaRepository @@ -7,11 +9,11 @@ import org.hibernate.annotations.CreationTimestamp import java.time.LocalDateTime @Named -interface OrderRepository: JpaRepository +interface OrderRepository: JpaRepository @Entity @Table(name = "orders") -data class OnlineOrder( +data class OrderEntity( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null, @@ -22,13 +24,16 @@ data class OnlineOrder( var restaurant: String, - //included the line below to specify that we want to keep the items as a CSV in our database - @Column(columnDefinition = "TEXT") - var items: String, + // Binds each order to its related children items using the primary key orders.id → items.order_id + @OneToMany(mappedBy = "order", fetch = FetchType.LAZY) + @JsonManagedReference + val items: List? = null, + + @CreationTimestamp var timeOrdered: LocalDateTime? = null ){ - constructor() : this(null, "", "","", null) + constructor() : this(null, "", "", null, null) } \ No newline at end of file From 0e727b58022de14af4b6d0a823e9b1e59991f894 Mon Sep 17 00:00:00 2001 From: mohammedsheshtar <112146380+mohammedsheshtar@users.noreply.github.com> Date: Thu, 17 Apr 2025 15:11:31 +0300 Subject: [PATCH 07/18] separated business logic and controller endpoints. Added authentication to my project such that each user must login first before accessing the orders endpoint and hashed the users password when created --- pom.xml | 9 ++++ .../coded/spring/ordering/InitUserRunner.kt | 33 +++++++++++++ .../CustomUserDetailsService.kt | 30 ++++++++++++ .../ordering/authentication/SecurityConfig.kt | 36 ++++++++++++++ .../spring/ordering/items/ItemsRepository.kt | 1 + .../spring/ordering/menus/MenuController.kt | 21 ++++++++ .../spring/ordering/menus/MenuRepository.kt | 27 +++++++++++ .../spring/ordering/menus/MenuService.kt | 19 ++++++++ .../orders/OnlineOrderingController.kt | 40 ++++++---------- .../orders/OnlineOrderingRepository.kt | 8 ++-- .../ordering/orders/OnlineOrderingService.kt | 48 +++++++++++++++++++ .../spring/ordering/users/UserController.kt | 27 +++++++++++ .../spring/ordering/users/UserRepository.kt | 31 ++++++++++++ .../spring/ordering/users/UserService.kt | 25 ++++++++++ 14 files changed, 326 insertions(+), 29 deletions(-) create mode 100644 src/main/kotlin/com/coded/spring/ordering/InitUserRunner.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/authentication/CustomUserDetailsService.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/menus/MenuController.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/menus/MenuRepository.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingService.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/users/UserController.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/users/UserRepository.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/users/UserService.kt diff --git a/pom.xml b/pom.xml index 479d7d3..da18ef8 100644 --- a/pom.xml +++ b/pom.xml @@ -70,6 +70,15 @@ org.springframework.boot spring-boot-starter-data-jpa + + jakarta.validation + jakarta.validation-api + 3.0.2 + + + org.springframework.boot + spring-boot-starter-security + com.h2database h2 diff --git a/src/main/kotlin/com/coded/spring/ordering/InitUserRunner.kt b/src/main/kotlin/com/coded/spring/ordering/InitUserRunner.kt new file mode 100644 index 0000000..645e711 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/InitUserRunner.kt @@ -0,0 +1,33 @@ +package com.coded.spring.ordering + + +import com.coded.spring.ordering.users.UserEntity +import com.coded.spring.ordering.users.UserRepository +import org.springframework.boot.CommandLineRunner +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication +import com.coded.spring.ordering.users.Roles +import org.springframework.context.annotation.Bean +import org.springframework.security.crypto.password.PasswordEncoder + +@SpringBootApplication +class InitUserRunner { + @Bean + fun initUsers(userRepository: UserRepository, passwordEncoder: PasswordEncoder) = CommandLineRunner { + val user = UserEntity( + username = "momo1111112", + password = passwordEncoder.encode("password123"), + role = Roles.USER + ) + if (userRepository.findByUsername(user.username) == null) { + println("Creating user ${user.username}") + userRepository.save(user) + } else { + println("User ${user.username} already exists") + } + } +} + +fun main(args: Array) { + runApplication(*args).close() +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/CustomUserDetailsService.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/CustomUserDetailsService.kt new file mode 100644 index 0000000..79558e2 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/CustomUserDetailsService.kt @@ -0,0 +1,30 @@ +package com.coded.spring.ordering.authentication + +import com.coded.spring.ordering.users.UserRepository +import org.springframework.context.annotation.* +import org.springframework.security.config.annotation.web.builders.* +import org.springframework.security.config.annotation.web.configuration.* +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.core.userdetails.UsernameNotFoundException +import org.springframework.security.crypto.bcrypt.* +import org.springframework.security.crypto.password.* +import org.springframework.security.web.* +import org.springframework.stereotype.Service +import org.springframework.security.core.userdetails.User + +@Service +class CustomUserDetailsService( + private val userRepository: UserRepository +) : UserDetailsService { + override fun loadUserByUsername(username: String): UserDetails { + val user = userRepository.findByUsername(username) + ?: throw UsernameNotFoundException("User not found") + + return User.builder() + .username(user.username) + .password(user.password) + .roles(user.role.toString()) + .build() + } +} diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt new file mode 100644 index 0000000..8bcd5b3 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt @@ -0,0 +1,36 @@ +package com.coded.spring.ordering.authentication + +import org.springframework.context.annotation.* +import org.springframework.security.config.annotation.web.builders.* +import org.springframework.security.config.annotation.web.configuration.* +import org.springframework.security.crypto.bcrypt.* +import org.springframework.security.crypto.password.* +import org.springframework.security.web.* + + +@Configuration +@EnableWebSecurity +class SecurityConfig( + private val userDetailsService: CustomUserDetailsService +) { + + @Bean + fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder() + + @Bean + fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http.csrf { it.disable() } // For testing only + + .authorizeHttpRequests { + it.requestMatchers("/menus/**").permitAll() // public route + it.requestMatchers("/register").permitAll() + it.requestMatchers("/orders/**").authenticated() // protected route + + .anyRequest().authenticated() + } + .formLogin { it.defaultSuccessUrl("/menus", true) } + .userDetailsService(userDetailsService) + + return http.build() + } +} diff --git a/src/main/kotlin/com/coded/spring/ordering/items/ItemsRepository.kt b/src/main/kotlin/com/coded/spring/ordering/items/ItemsRepository.kt index e7078b8..d4d449b 100644 --- a/src/main/kotlin/com/coded/spring/ordering/items/ItemsRepository.kt +++ b/src/main/kotlin/com/coded/spring/ordering/items/ItemsRepository.kt @@ -17,6 +17,7 @@ data class ItemsEntity( var id: Long? = null, // maps each item to its parent order using the foreign key items.order_id → orders.id + @ManyToOne @JoinColumn(name = "order_id") @JsonBackReference val order: OrderEntity, diff --git a/src/main/kotlin/com/coded/spring/ordering/menus/MenuController.kt b/src/main/kotlin/com/coded/spring/ordering/menus/MenuController.kt new file mode 100644 index 0000000..a155014 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/menus/MenuController.kt @@ -0,0 +1,21 @@ +package com.coded.spring.ordering.menus + +import org.springframework.web.bind.annotation.* +import java.math.BigDecimal + +@RestController +class MenuController( + private val menuService: MenuService +) { + @GetMapping("/menus") + fun getMenu() = menuService.getMenu() + + @PostMapping("/menus") + fun createMenu(@RequestBody menu: MenuDTO): MenuDTO = menuService.addMenu(menu) +} + + +data class MenuDTO( + val name: String, + val price: BigDecimal +) diff --git a/src/main/kotlin/com/coded/spring/ordering/menus/MenuRepository.kt b/src/main/kotlin/com/coded/spring/ordering/menus/MenuRepository.kt new file mode 100644 index 0000000..5f97435 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/menus/MenuRepository.kt @@ -0,0 +1,27 @@ +package com.coded.spring.ordering.menus + +import com.coded.spring.ordering.items.ItemsEntity +import com.coded.spring.ordering.users.UserEntity +import com.fasterxml.jackson.annotation.JsonManagedReference +import jakarta.inject.Named +import jakarta.persistence.* +import org.springframework.data.jpa.repository.JpaRepository +import org.hibernate.annotations.CreationTimestamp +import java.math.BigDecimal +import java.time.LocalDateTime + +@Named +interface MenuRepository : JpaRepository + +@Entity +@Table(name = "menus") +data class MenuEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, + val name: String, + @Column(precision = 9, scale = 3) + val price: BigDecimal +){ + constructor() : this(null, "", BigDecimal.ZERO) +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt b/src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt new file mode 100644 index 0000000..ec30b4d --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt @@ -0,0 +1,19 @@ +package com.coded.spring.ordering.menus + +import org.springframework.stereotype.Service + +@Service +class MenuService( + private val menuRepository: MenuRepository +) { + fun getMenu(): List = menuRepository.findAll() + + fun addMenu(dto: MenuDTO): MenuDTO { + val newMenu = MenuEntity( + name = dto.name, + price = dto.price + ) + val saved = menuRepository.save(newMenu) + return MenuDTO(saved.name, saved.price) + } +} diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt index 1c30d9f..f9baa0f 100644 --- a/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt @@ -6,39 +6,19 @@ import org.springframework.web.bind.annotation.* @RestController class OnlineOrderingController( - val orderRepository: OrderRepository, - val itemsRepository: ItemsRepository + private val onlineOrderingService: OnlineOrderingService ) { @GetMapping("/orders") - fun getOrders() = orderRepository.findAll().sortedBy { it.timeOrdered } + fun getOrders() = onlineOrderingService.getOrders() @PostMapping("/orders") - fun addOrders(@RequestBody request: RequestOrder): OrderEntity { - //adding the new order into our database - val order = orderRepository.save( - OrderEntity( - user = request.user, - restaurant = request.restaurant - ) - ) - - //converting each item in our items objects list into an item entity to add them into the items database while also connecting each item to its order - val items = request.items.map { item -> - ItemsEntity( - order = order, - name = item.name, - price = item.price - ) - } - itemsRepository.saveAll(items) - - return order - } - + fun addOrders(@RequestBody request: RequestOrder) = + onlineOrderingService.addOrders(request) } + // the DTO (Data Transfer Object) for our orders and items list data class RequestItem( val name: String, @@ -46,7 +26,15 @@ data class RequestItem( ) data class RequestOrder( - val user: String, + val userId: Long, val restaurant: String, val items: List ) + +data class OrderResponseDTO( + val id: Long, + val username: String, + val restaurant: String, + val items: List, + val timeOrdered: String? +) diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingRepository.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingRepository.kt index 790d10a..4357bed 100644 --- a/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingRepository.kt +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingRepository.kt @@ -1,6 +1,7 @@ package com.coded.spring.ordering.orders import com.coded.spring.ordering.items.ItemsEntity +import com.coded.spring.ordering.users.UserEntity import com.fasterxml.jackson.annotation.JsonManagedReference import jakarta.inject.Named import jakarta.persistence.* @@ -19,8 +20,9 @@ data class OrderEntity( var id: Long? = null, // included the line below because 'user' is a reserved keyword in SQL and threw an error because of it, column escapes it - @Column(name = "`user`") - var user: String, + @ManyToOne + @JoinColumn(name = "user_id") + var user: UserEntity, var restaurant: String, @@ -35,5 +37,5 @@ data class OrderEntity( var timeOrdered: LocalDateTime? = null ){ - constructor() : this(null, "", "", null, null) + constructor() : this(null, UserEntity(), "", null, null) } \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingService.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingService.kt new file mode 100644 index 0000000..ceb0d8a --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingService.kt @@ -0,0 +1,48 @@ +package com.coded.spring.ordering.orders + +import com.coded.spring.ordering.items.ItemsEntity +import com.coded.spring.ordering.items.ItemsRepository +import com.coded.spring.ordering.users.UserRepository +import org.springframework.stereotype.Service + +@Service +class OnlineOrderingService( + private val orderRepository: OrderRepository, + private val itemsRepository: ItemsRepository, + private var userRepository: UserRepository +) { + fun getOrders(): List = orderRepository.findAll().filter { it.user != null }.sortedBy { it.timeOrdered } + + fun addOrders(request: RequestOrder): OrderResponseDTO { + val user = userRepository.findById(request.userId).orElseThrow { + IllegalArgumentException("User with ID ${request.userId} not found") + } + + val order = orderRepository.save( + OrderEntity( + user = user, + restaurant = request.restaurant + ) + ) + + val items = request.items.map { item -> + ItemsEntity( + order = order, + name = item.name, + price = item.price + ) + } + + itemsRepository.saveAll(items) + + return OrderResponseDTO( + id = order.id!!, + username = user.username, + restaurant = order.restaurant, + timeOrdered = order.timeOrdered.toString(), + items = items.map { + RequestItem(name = it.name, price = it.price) + } + ) + } +} diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UserController.kt b/src/main/kotlin/com/coded/spring/ordering/users/UserController.kt new file mode 100644 index 0000000..2cd8d4f --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/users/UserController.kt @@ -0,0 +1,27 @@ +package com.coded.spring.ordering.users + + +import jakarta.validation.Valid +import jakarta.validation.constraints.NotBlank +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + + +@RestController +class UserController( + private val userService : UserService +) { + @PostMapping("register") + fun registerUser(@RequestBody request: CreateUserRequest): ResponseEntity { + return userService.registerUser(request) + } +} + +data class CreateUserRequest( + @field:NotBlank(message = "Username is required") + val username: String, + + @field:NotBlank(message = "Password is required") + val password: String +) + diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UserRepository.kt b/src/main/kotlin/com/coded/spring/ordering/users/UserRepository.kt new file mode 100644 index 0000000..2eafb17 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/users/UserRepository.kt @@ -0,0 +1,31 @@ +package com.coded.spring.ordering.users + +import jakarta.persistence.* +import org.springframework.data.jpa.repository.JpaRepository +import jakarta.inject.Named + +@Named +interface UserRepository : JpaRepository{ + fun findByUsername(userName: String): UserEntity? + fun existsByUsername(username: String): Boolean +} + +@Entity +@Table(name = "users") +data class UserEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, + + val username: String, + val password: String, + + @Enumerated(EnumType.STRING) + val role: Roles = Roles.USER +){ + constructor() : this(null, "", "") +} + +enum class Roles { + USER, ADMIN +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UserService.kt b/src/main/kotlin/com/coded/spring/ordering/users/UserService.kt new file mode 100644 index 0000000..8f89620 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/users/UserService.kt @@ -0,0 +1,25 @@ +package com.coded.spring.ordering.users + +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.stereotype.Service + + @Service + class UserService( + private val userRepository: UserRepository, + private val passwordEncoder: PasswordEncoder + ) { + fun registerUser(request: CreateUserRequest): ResponseEntity { + if (userRepository.existsByUsername(request.username)) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(mapOf("error" to "username ${request.username} already exists")) + } + + val hashedPassword = passwordEncoder.encode(request.password) + val newUser = UserEntity(username = request.username, password = hashedPassword) + userRepository.save(newUser) + + return ResponseEntity.ok().build() + } + } From 26d3170d659b50dd97583c984ab7dc8cc6169cf6 Mon Sep 17 00:00:00 2001 From: mohammedsheshtar <112146380+mohammedsheshtar@users.noreply.github.com> Date: Mon, 21 Apr 2025 08:32:20 +0300 Subject: [PATCH 08/18] Implement JWT authentication to secure order submissions and profile creation. Add user profile feature allowing authenticated users to create/update their profiles. --- pom.xml | 17 ++++++ .../AuthenticationController.kt | 40 ++++++++++++++ .../ordering/authentication/SecurityConfig.kt | 54 +++++++++++++------ .../jwt/JwtAuthenticationFilter.kt | 46 ++++++++++++++++ .../ordering/authentication/jwt/JwtService.kt | 42 +++++++++++++++ .../orders/OnlineOrderingController.kt | 2 +- .../ordering/profiles/ProfileController.kt | 25 +++++++++ .../ordering/profiles/ProfileRepository.kt | 35 ++++++++++++ .../ordering/profiles/ProfileService.kt | 50 +++++++++++++++++ 9 files changed, 295 insertions(+), 16 deletions(-) create mode 100644 src/main/kotlin/com/coded/spring/ordering/authentication/AuthenticationController.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtAuthenticationFilter.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtService.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/profiles/ProfileController.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/profiles/ProfileRepository.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/profiles/ProfileService.kt diff --git a/pom.xml b/pom.xml index da18ef8..9dba39f 100644 --- a/pom.xml +++ b/pom.xml @@ -79,6 +79,23 @@ org.springframework.boot spring-boot-starter-security + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + runtime + 0.11.5 + + + io.jsonwebtoken + jjwt-jackson + runtime + 0.11.5 + com.h2database h2 diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/AuthenticationController.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/AuthenticationController.kt new file mode 100644 index 0000000..9f71f5b --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/AuthenticationController.kt @@ -0,0 +1,40 @@ +package com.coded.spring.ordering.authentication + +import com.coded.spring.ordering.authentication.jwt.JwtService +import org.springframework.security.authentication.* +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.core.userdetails.UsernameNotFoundException +import org.springframework.web.bind.annotation.* + + +@RestController +@RequestMapping("/authentication") +class AuthenticationController( + private val authenticationManager: AuthenticationManager, + private val userDetailsService: UserDetailsService, + private val jwtService: JwtService +) { + + @PostMapping("/login") + fun login(@RequestBody authRequest: AuthenticationRequest): AuthenticationResponse { + val authToken = UsernamePasswordAuthenticationToken(authRequest.username, authRequest.password) + val authentication = authenticationManager.authenticate(authToken) + + if (authentication.isAuthenticated) { + val userDetails = userDetailsService.loadUserByUsername(authRequest.username) + val token = jwtService.generateToken(userDetails.username) + return AuthenticationResponse (token) + } else { + throw UsernameNotFoundException("Invalid user request!") + } + } +} + +data class AuthenticationRequest( + val username: String, + val password: String +) + +data class AuthenticationResponse( + val token: String +) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt index 8bcd5b3..38ff3e6 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt @@ -1,17 +1,28 @@ package com.coded.spring.ordering.authentication -import org.springframework.context.annotation.* -import org.springframework.security.config.annotation.web.builders.* -import org.springframework.security.config.annotation.web.configuration.* -import org.springframework.security.crypto.bcrypt.* -import org.springframework.security.crypto.password.* -import org.springframework.security.web.* +import com.coded.spring.ordering.authentication.jwt.JwtAuthenticationFilter +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.AuthenticationProvider +import org.springframework.security.authentication.dao.DaoAuthenticationProvider +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter + @Configuration @EnableWebSecurity class SecurityConfig( - private val userDetailsService: CustomUserDetailsService + private val userDetailsService: CustomUserDetailsService, + private val jwtAuthFilter: JwtAuthenticationFilter, ) { @Bean @@ -19,18 +30,31 @@ class SecurityConfig( @Bean fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { - http.csrf { it.disable() } // For testing only - + http.csrf { it.disable() } .authorizeHttpRequests { - it.requestMatchers("/menus/**").permitAll() // public route - it.requestMatchers("/register").permitAll() - it.requestMatchers("/orders/**").authenticated() // protected route - + it.requestMatchers("/menus/**", "/register", "/authentication/**").permitAll() // public route + it.requestMatchers("/orders/**").authenticated() + it.requestMatchers("/profile/**").authenticated() .anyRequest().authenticated() } - .formLogin { it.defaultSuccessUrl("/menus", true) } - .userDetailsService(userDetailsService) + .sessionManagement { + it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + } + .authenticationProvider(authenticationProvider()) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter::class.java) return http.build() } + + @Bean + fun authenticationManager(config: AuthenticationConfiguration): AuthenticationManager = + config.authenticationManager + + @Bean + fun authenticationProvider(): AuthenticationProvider { + val provider = DaoAuthenticationProvider() + provider.setUserDetailsService(userDetailsService) + provider.setPasswordEncoder(passwordEncoder()) + return provider + } } diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtAuthenticationFilter.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtAuthenticationFilter.kt new file mode 100644 index 0000000..f3370bd --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtAuthenticationFilter.kt @@ -0,0 +1,46 @@ +package com.coded.spring.ordering.authentication.jwt + + +import jakarta.servlet.FilterChain +import jakarta.servlet.http.* +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter + +@Component +class JwtAuthenticationFilter( + private val jwtService: JwtService, + private val userDetailsService: UserDetailsService +) : OncePerRequestFilter() { + + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + val authHeader = request.getHeader("Authorization") + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response) + return + } + + val token = authHeader.substring(7) + val username = jwtService.extractUsername(token) + + if (SecurityContextHolder.getContext().authentication == null) { + if (jwtService.isTokenValid(token, username)) { + val userDetails = userDetailsService.loadUserByUsername(username) + val authToken = UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.authorities + ) + authToken.details = WebAuthenticationDetailsSource().buildDetails(request) + SecurityContextHolder.getContext().authentication = authToken + } + } + + filterChain.doFilter(request, response) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtService.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtService.kt new file mode 100644 index 0000000..c877c7c --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtService.kt @@ -0,0 +1,42 @@ +package com.coded.spring.ordering.authentication.jwt + +import io.jsonwebtoken.* +import io.jsonwebtoken.security.Keys +import org.springframework.stereotype.Component +import java.util.* +import javax.crypto.SecretKey + +@Component +class JwtService { + + private val secretKey: SecretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256) + private val expirationMs: Long = 1000 * 60 * 60 + + fun generateToken(username: String): String { + val now = Date() + val expiry = Date(now.time + expirationMs) + + return Jwts.builder() + .setSubject(username) + .setIssuedAt(now) + .setExpiration(expiry) + .signWith(secretKey) + .compact() + } + + fun extractUsername(token: String): String = + Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .body + .subject + + fun isTokenValid(token: String, username: String): Boolean { + return try { + extractUsername(token) == username + } catch (e: Exception) { + false + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt index f9baa0f..944fe18 100644 --- a/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt @@ -12,7 +12,7 @@ class OnlineOrderingController( @GetMapping("/orders") fun getOrders() = onlineOrderingService.getOrders() - @PostMapping("/orders") + @PostMapping("/orders/add") fun addOrders(@RequestBody request: RequestOrder) = onlineOrderingService.addOrders(request) } diff --git a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileController.kt b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileController.kt new file mode 100644 index 0000000..04303ea --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileController.kt @@ -0,0 +1,25 @@ +package com.coded.spring.ordering.profiles + + +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.security.core.userdetails.User + +@RestController +class ProfileController( + private val profileService: ProfileService +) { + @PostMapping("/profile") + fun addProfile( @AuthenticationPrincipal user: User, @RequestBody request: RequestProfileDTO): ResponseEntity { + return profileService.createProfile(username = user.username, request = request) + } +} + +data class RequestProfileDTO( + val firstName: String, + val lastName: String, + val phoneNumber: String +) + + diff --git a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileRepository.kt b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileRepository.kt new file mode 100644 index 0000000..646bce0 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileRepository.kt @@ -0,0 +1,35 @@ +package com.coded.spring.ordering.profiles + + +import com.coded.spring.ordering.users.UserEntity +import jakarta.inject.Named +import jakarta.persistence.* +import org.springframework.data.jpa.repository.JpaRepository + +@Named +interface ProfileRepository : JpaRepository { + fun findByUserId(userId: UserEntity): ProfileEntity? +} + +@Entity +@Table(name = "profiles") +data class ProfileEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, + + @OneToOne + @JoinColumn(name = "user_id") + val userId: UserEntity, + + @Column(name = "first_name") + val firstName: String, + + @Column(name = "last_name") + val lastName: String, + + @Column(name = "phone_number") + val phoneNumber: String +) { + constructor() : this(null, UserEntity(), "", "", "") +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileService.kt b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileService.kt new file mode 100644 index 0000000..d8e83bc --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileService.kt @@ -0,0 +1,50 @@ +package com.coded.spring.ordering.profiles + + +import com.coded.spring.ordering.users.UserRepository +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Service + +@Service +class ProfileService( + private val profileRepository: ProfileRepository, + private val userRepository: UserRepository +) { + fun createProfile(username: String, request: RequestProfileDTO): ResponseEntity { + if(request.firstName.any { it.isDigit() }) { + return ResponseEntity.badRequest().body(mapOf("error" to "first name must not contain any numbers")) + } + + if(request.lastName. any { it.isDigit() }) { + return ResponseEntity.badRequest().body(mapOf("error" to "last name must not contain any numbers")) + } + + if(!request.phoneNumber.matches(Regex("^\\d{8}$"))) { + return ResponseEntity.badRequest().body(mapOf("error" to "phone number must be 8 digits")) + } + + val user = userRepository.findByUsername(username) + ?: return ResponseEntity.badRequest().body(mapOf("error" to "user was not found")) + + + val existingProfile = profileRepository.findByUserId(user) + + val profile = if (existingProfile != null) { + existingProfile.copy( + firstName = request.firstName, + lastName = request.lastName, + phoneNumber = request.phoneNumber + ) + } else { + ProfileEntity( + userId = user, + firstName = request.firstName, + lastName = request.lastName, + phoneNumber = request.phoneNumber + ) + } + profileRepository.save(profile) + + return ResponseEntity.ok().build() + } +} From 0ef3254656fdf76876145889e70a0cadb0c1f046 Mon Sep 17 00:00:00 2001 From: mohammedsheshtar <112146380+mohammedsheshtar@users.noreply.github.com> Date: Tue, 22 Apr 2025 18:04:10 +0300 Subject: [PATCH 09/18] completed the bonus task for user authentication for fun :) --- pom.xml | 2 ++ .../CustomUserDetailsService.kt | 6 ----- .../ordering/authentication/SecurityConfig.kt | 7 +++--- .../spring/ordering/menus/MenuService.kt | 4 ++-- .../ordering/profiles/ProfileService.kt | 2 +- .../ordering/{ => scripts}/InitUserRunner.kt | 3 ++- .../spring/ordering/users/UserService.kt | 13 +++++++++++ .../coded/spring/ordering/ApplicationTests.kt | 23 +++++++++++++++++-- 8 files changed, 44 insertions(+), 16 deletions(-) rename src/main/kotlin/com/coded/spring/ordering/{ => scripts}/InitUserRunner.kt (92%) diff --git a/pom.xml b/pom.xml index 9dba39f..30186ad 100644 --- a/pom.xml +++ b/pom.xml @@ -56,6 +56,7 @@ org.postgresql postgresql + compile org.jetbrains.kotlin @@ -99,6 +100,7 @@ com.h2database h2 + test diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/CustomUserDetailsService.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/CustomUserDetailsService.kt index 79558e2..443f180 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/CustomUserDetailsService.kt +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/CustomUserDetailsService.kt @@ -1,15 +1,9 @@ package com.coded.spring.ordering.authentication import com.coded.spring.ordering.users.UserRepository -import org.springframework.context.annotation.* -import org.springframework.security.config.annotation.web.builders.* -import org.springframework.security.config.annotation.web.configuration.* import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.core.userdetails.UsernameNotFoundException -import org.springframework.security.crypto.bcrypt.* -import org.springframework.security.crypto.password.* -import org.springframework.security.web.* import org.springframework.stereotype.Service import org.springframework.security.core.userdetails.User diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt index 38ff3e6..832d2b8 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt @@ -32,10 +32,9 @@ class SecurityConfig( fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { http.csrf { it.disable() } .authorizeHttpRequests { - it.requestMatchers("/menus/**", "/register", "/authentication/**").permitAll() // public route - it.requestMatchers("/orders/**").authenticated() - it.requestMatchers("/profile/**").authenticated() - .anyRequest().authenticated() + it.requestMatchers("/menus", "/register", "/authentication/**", "/hello", "/orders/**", "/profile/**").permitAll() // public route + //it.requestMatchers("/orders/**", "/profile/**").authenticated() + //.anyRequest().authenticated() } .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) diff --git a/src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt b/src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt index ec30b4d..839058d 100644 --- a/src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt +++ b/src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt @@ -13,7 +13,7 @@ class MenuService( name = dto.name, price = dto.price ) - val saved = menuRepository.save(newMenu) - return MenuDTO(saved.name, saved.price) + menuRepository.save(newMenu) + return MenuDTO(newMenu.name, newMenu.price) } } diff --git a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileService.kt b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileService.kt index d8e83bc..0d583aa 100644 --- a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileService.kt +++ b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileService.kt @@ -43,8 +43,8 @@ class ProfileService( phoneNumber = request.phoneNumber ) } - profileRepository.save(profile) + profileRepository.save(profile) return ResponseEntity.ok().build() } } diff --git a/src/main/kotlin/com/coded/spring/ordering/InitUserRunner.kt b/src/main/kotlin/com/coded/spring/ordering/scripts/InitUserRunner.kt similarity index 92% rename from src/main/kotlin/com/coded/spring/ordering/InitUserRunner.kt rename to src/main/kotlin/com/coded/spring/ordering/scripts/InitUserRunner.kt index 645e711..591119f 100644 --- a/src/main/kotlin/com/coded/spring/ordering/InitUserRunner.kt +++ b/src/main/kotlin/com/coded/spring/ordering/scripts/InitUserRunner.kt @@ -1,6 +1,7 @@ -package com.coded.spring.ordering +package com.coded.spring.ordering.scripts +import com.coded.spring.ordering.Application import com.coded.spring.ordering.users.UserEntity import com.coded.spring.ordering.users.UserRepository import org.springframework.boot.CommandLineRunner diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UserService.kt b/src/main/kotlin/com/coded/spring/ordering/users/UserService.kt index 8f89620..5535de5 100644 --- a/src/main/kotlin/com/coded/spring/ordering/users/UserService.kt +++ b/src/main/kotlin/com/coded/spring/ordering/users/UserService.kt @@ -16,6 +16,19 @@ import org.springframework.stereotype.Service .body(mapOf("error" to "username ${request.username} already exists")) } + if(request.password.length < 6) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(mapOf("error" to "password must be at least 6 characters")) + } + + if(!request.password.any { it.isUpperCase() }) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(mapOf("error" to "password must have at least one capital letter")) + } + + if(!request.password.any { it.isDigit() }) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(mapOf("error" to "password must have at least one digit")) + } + val hashedPassword = passwordEncoder.encode(request.password) val newUser = UserEntity(username = request.username, password = hashedPassword) userRepository.save(newUser) diff --git a/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt b/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt index b2e2320..0bd38b4 100644 --- a/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt +++ b/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt @@ -1,13 +1,32 @@ package com.coded.spring.ordering +import com.coded.spring.ordering.users.CreateUserRequest import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.http.HttpStatus +import kotlin.test.assertEquals -@SpringBootTest +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class ApplicationTests { + @Autowired + lateinit var restTemplate: TestRestTemplate + @Test - fun contextLoads() { + fun helloWorld() { + val result = restTemplate.getForEntity("/hello", String::class.java) + assertEquals(expected = HttpStatus.OK, actual = result?.statusCode) + assertEquals(expected = "Hello World", actual = result.body) + + } + +@Test + fun `Adding user with correct paramter should work`() { + val request = CreateUserRequest(username = "mohammed111234", password = "1234567") + val result = restTemplate.postForEntity("/register", request, String::class.java) + assertEquals(HttpStatus.OK, result.statusCode) } } From 10e35cc66f6034687eb627a23378176a82cb6e99 Mon Sep 17 00:00:00 2001 From: mohammedsheshtar <112146380+mohammedsheshtar@users.noreply.github.com> Date: Wed, 23 Apr 2025 13:19:42 +0300 Subject: [PATCH 10/18] tests now saves into the H2 databse. Added new tests for profiles and orders. Included JWM token generation and authentication in the test so no need to make all endpoints public in order to test them --- pom.xml | 2 +- .../ordering/authentication/SecurityConfig.kt | 4 +- .../orders/OnlineOrderingController.kt | 2 +- .../ordering/orders/OnlineOrderingService.kt | 10 +- src/main/resources/application.properties | 3 +- .../coded/spring/ordering/ApplicationTests.kt | 132 +++++++++++++++++- .../resources/application-test.properties | 13 ++ 7 files changed, 152 insertions(+), 14 deletions(-) create mode 100644 src/test/resources/application-test.properties diff --git a/pom.xml b/pom.xml index 30186ad..a59abe2 100644 --- a/pom.xml +++ b/pom.xml @@ -100,7 +100,7 @@ com.h2database h2 - test + test diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt index 832d2b8..a4e4f3d 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt @@ -19,7 +19,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic @Configuration -@EnableWebSecurity +//@EnableWebSecurity class SecurityConfig( private val userDetailsService: CustomUserDetailsService, private val jwtAuthFilter: JwtAuthenticationFilter, @@ -32,7 +32,7 @@ class SecurityConfig( fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { http.csrf { it.disable() } .authorizeHttpRequests { - it.requestMatchers("/menus", "/register", "/authentication/**", "/hello", "/orders/**", "/profile/**").permitAll() // public route + it.requestMatchers("/menus", "/register", "/authentication/**", "/orders/**", "/profile/**", "/hello").permitAll() // public route //it.requestMatchers("/orders/**", "/profile/**").authenticated() //.anyRequest().authenticated() } diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt index 944fe18..b91c461 100644 --- a/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt @@ -32,7 +32,7 @@ data class RequestOrder( ) data class OrderResponseDTO( - val id: Long, + val orderId: Long, val username: String, val restaurant: String, val items: List, diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingService.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingService.kt index ceb0d8a..965ca56 100644 --- a/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingService.kt +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingService.kt @@ -3,6 +3,8 @@ package com.coded.spring.ordering.orders import com.coded.spring.ordering.items.ItemsEntity import com.coded.spring.ordering.items.ItemsRepository import com.coded.spring.ordering.users.UserRepository +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity import org.springframework.stereotype.Service @Service @@ -13,11 +15,15 @@ class OnlineOrderingService( ) { fun getOrders(): List = orderRepository.findAll().filter { it.user != null }.sortedBy { it.timeOrdered } - fun addOrders(request: RequestOrder): OrderResponseDTO { + fun addOrders(request: RequestOrder): Any { val user = userRepository.findById(request.userId).orElseThrow { IllegalArgumentException("User with ID ${request.userId} not found") } + if(request.items.any { it.price < 0.0 }) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(mapOf("error" to "item price cannot be negative")) + } + val order = orderRepository.save( OrderEntity( user = user, @@ -36,7 +42,7 @@ class OnlineOrderingService( itemsRepository.saveAll(items) return OrderResponseDTO( - id = order.id!!, + orderId = order.id!!, username = user.username, restaurant = order.restaurant, timeOrdered = order.timeOrdered.toString(), diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 66a96ae..88f707c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -3,4 +3,5 @@ server.port=9001 spring.datasource.url=jdbc:postgresql://localhost:5432/OnlineOrderingDatabase spring.datasource.username=postgres spring.datasource.password=123 -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect \ No newline at end of file +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect + diff --git a/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt b/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt index 0bd38b4..63e2b82 100644 --- a/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt +++ b/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt @@ -1,32 +1,150 @@ package com.coded.spring.ordering +import com.coded.spring.ordering.authentication.jwt.JwtService +import com.coded.spring.ordering.orders.OrderResponseDTO +import com.coded.spring.ordering.orders.RequestItem +import com.coded.spring.ordering.orders.RequestOrder +import com.coded.spring.ordering.profiles.RequestProfileDTO import com.coded.spring.ordering.users.CreateUserRequest +import com.coded.spring.ordering.users.UserEntity +import com.coded.spring.ordering.users.UserRepository +import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod import org.springframework.http.HttpStatus +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.test.context.ActiveProfiles +import org.springframework.util.MultiValueMap +import java.time.Duration +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter import kotlin.test.assertEquals -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = ["src/test/resources/application-test.properties"] +) +@ActiveProfiles("test") class ApplicationTests { + companion object { + lateinit var savedUser: UserEntity + @JvmStatic + @BeforeAll + fun setUp( + @Autowired userRepository: UserRepository, + @Autowired passwordEncoder: PasswordEncoder + ) { + userRepository.deleteAll() + val user = UserEntity( + username = "momo1234", + password = passwordEncoder.encode("123dB45") + ) + savedUser = userRepository.save(user) + } + } + @Autowired lateinit var restTemplate: TestRestTemplate @Test - fun helloWorld() { - val result = restTemplate.getForEntity("/hello", String::class.java) - assertEquals(expected = HttpStatus.OK, actual = result?.statusCode) - assertEquals(expected = "Hello World", actual = result.body) + fun `test hello endpoint with JWT`(@Autowired jwtService: JwtService) { + val token = jwtService.generateToken("momo1234") + val headers = HttpHeaders( + MultiValueMap.fromSingleValue(mapOf("Authorization" to "Bearer $token")) + ) + val request = HttpEntity(headers) + + val result = restTemplate.exchange( + "/hello", + HttpMethod.GET, + request, + String::class.java + ) + assertEquals(HttpStatus.OK, result.statusCode) + assertEquals("Hello World", result.body) } -@Test + @Test fun `Adding user with correct paramter should work`() { - val request = CreateUserRequest(username = "mohammed111234", password = "1234567") + val request = CreateUserRequest(username = "mmmohammed67234", password = "12Ln34567") val result = restTemplate.postForEntity("/register", request, String::class.java) assertEquals(HttpStatus.OK, result.statusCode) } + @Test + fun `Addding a new order should work`(@Autowired jwtService: JwtService) { + //Mock + val token = jwtService.generateToken("momo1234") + val headers = HttpHeaders( + MultiValueMap.fromSingleValue(mapOf("Authorization" to "Bearer $token")) + ) + + val body = RequestOrder( + userId = savedUser.id!!, + restaurant = "WK", + items = listOf(RequestItem("Nuggies", 3.950)) + ) + + //Trigger + val requestEntity = HttpEntity(body, headers) + val actualResponse = restTemplate.exchange( + "/orders/add", //Endpoint + HttpMethod.POST, + requestEntity, + OrderResponseDTO::class.java + ) + + // Assertions + assertEquals(HttpStatus.OK, actualResponse.statusCode) + + val responseBody = actualResponse.body!! + assertEquals("momo1234", responseBody.username) + assertEquals("WK", responseBody.restaurant) + assertEquals(listOf(RequestItem("Nuggies", 3.950)), responseBody.items) + + val now = System.currentTimeMillis() + val orderTime = java.time.LocalDateTime + .parse(responseBody.timeOrdered) + .atZone(java.time.ZoneId.systemDefault()) + .toInstant() + .toEpochMilli() + assert(orderTime <= now && orderTime > now - 1000) { + "Expected timeOrdered to be recent. Got: ${responseBody.timeOrdered}" + } + + + } + + @Test + fun `adding new profile should work`(@Autowired jwtService: JwtService) { + val token = jwtService.generateToken("momo1234") + val headers = HttpHeaders() + headers.set("Authorization", "Bearer $token") + + val requestBody = RequestProfileDTO( + firstName = "Mohammed", + lastName = "Sheshtar", + phoneNumber = "12345678" + ) + + val entity = HttpEntity(requestBody, headers) + + val response = restTemplate.exchange( + "/profile", + HttpMethod.POST, + entity, + String::class.java + ) + + assertEquals(HttpStatus.OK, response.statusCode) + } + + } diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties new file mode 100644 index 0000000..7e87e3b --- /dev/null +++ b/src/test/resources/application-test.properties @@ -0,0 +1,13 @@ +spring.application.name=Kotlin.SpringbootV2 +server.port=9001 +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.show-sql=true +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console + + From 2319b0b449df53a271b1c81783465c81b13b49fb Mon Sep 17 00:00:00 2001 From: mohammedsheshtar <112146380+mohammedsheshtar@users.noreply.github.com> Date: Mon, 28 Apr 2025 15:02:09 +0300 Subject: [PATCH 11/18] Added more tests to ensure the validation checkpoints that can be found in each of the entity's service file work as intended --- .../ordering/authentication/SecurityConfig.kt | 8 +- .../ordering/orders/OnlineOrderingService.kt | 30 +- .../ordering/profiles/ProfileService.kt | 7 +- .../spring/ordering/users/UserService.kt | 2 +- .../coded/spring/ordering/ApplicationTests.kt | 259 +++++++++++++++++- 5 files changed, 274 insertions(+), 32 deletions(-) diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt index a4e4f3d..7f728e9 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt @@ -19,7 +19,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic @Configuration -//@EnableWebSecurity +@EnableWebSecurity class SecurityConfig( private val userDetailsService: CustomUserDetailsService, private val jwtAuthFilter: JwtAuthenticationFilter, @@ -32,9 +32,9 @@ class SecurityConfig( fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { http.csrf { it.disable() } .authorizeHttpRequests { - it.requestMatchers("/menus", "/register", "/authentication/**", "/orders/**", "/profile/**", "/hello").permitAll() // public route - //it.requestMatchers("/orders/**", "/profile/**").authenticated() - //.anyRequest().authenticated() + it.requestMatchers("/menus", "/register", "/authentication/**", "/hello").permitAll() // public route + it.requestMatchers("/orders/**", "/profile/**").authenticated() + .anyRequest().authenticated() } .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingService.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingService.kt index 965ca56..1d1b33a 100644 --- a/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingService.kt +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingService.kt @@ -15,13 +15,13 @@ class OnlineOrderingService( ) { fun getOrders(): List = orderRepository.findAll().filter { it.user != null }.sortedBy { it.timeOrdered } - fun addOrders(request: RequestOrder): Any { - val user = userRepository.findById(request.userId).orElseThrow { - IllegalArgumentException("User with ID ${request.userId} not found") - } + fun addOrders(request: RequestOrder): ResponseEntity { + val user = userRepository.findById(request.userId).orElse(null) + ?: return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(mapOf("error" to "user with ID ${request.userId} was not found")) - if(request.items.any { it.price < 0.0 }) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(mapOf("error" to "item price cannot be negative")) + if (request.items.any { it.price < 0.0 }) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(mapOf("error" to "item price cannot be negative")) } val order = orderRepository.save( @@ -41,14 +41,16 @@ class OnlineOrderingService( itemsRepository.saveAll(items) - return OrderResponseDTO( - orderId = order.id!!, - username = user.username, - restaurant = order.restaurant, - timeOrdered = order.timeOrdered.toString(), - items = items.map { - RequestItem(name = it.name, price = it.price) - } + return ResponseEntity.status(HttpStatus.OK).body( + OrderResponseDTO( + orderId = order.id!!, + username = user.username, + restaurant = order.restaurant, + timeOrdered = order.timeOrdered.toString(), + items = items.map { + RequestItem(name = it.name, price = it.price) + } + ) ) } } diff --git a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileService.kt b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileService.kt index 0d583aa..3b44802 100644 --- a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileService.kt +++ b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileService.kt @@ -11,6 +11,9 @@ class ProfileService( private val userRepository: UserRepository ) { fun createProfile(username: String, request: RequestProfileDTO): ResponseEntity { + val user = userRepository.findByUsername(username) + ?: return ResponseEntity.badRequest().body(mapOf("error" to "username was not found")) + if(request.firstName.any { it.isDigit() }) { return ResponseEntity.badRequest().body(mapOf("error" to "first name must not contain any numbers")) } @@ -23,10 +26,6 @@ class ProfileService( return ResponseEntity.badRequest().body(mapOf("error" to "phone number must be 8 digits")) } - val user = userRepository.findByUsername(username) - ?: return ResponseEntity.badRequest().body(mapOf("error" to "user was not found")) - - val existingProfile = profileRepository.findByUserId(user) val profile = if (existingProfile != null) { diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UserService.kt b/src/main/kotlin/com/coded/spring/ordering/users/UserService.kt index 5535de5..8850f07 100644 --- a/src/main/kotlin/com/coded/spring/ordering/users/UserService.kt +++ b/src/main/kotlin/com/coded/spring/ordering/users/UserService.kt @@ -29,7 +29,7 @@ import org.springframework.stereotype.Service return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(mapOf("error" to "password must have at least one digit")) } - val hashedPassword = passwordEncoder.encode(request.password) + val hashedPassword = passwordEncoder.encode(request.password) val newUser = UserEntity(username = request.username, password = hashedPassword) userRepository.save(newUser) diff --git a/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt b/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt index 63e2b82..a111b18 100644 --- a/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt +++ b/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt @@ -1,6 +1,7 @@ package com.coded.spring.ordering import com.coded.spring.ordering.authentication.jwt.JwtService +import com.coded.spring.ordering.menus.MenuDTO import com.coded.spring.ordering.orders.OrderResponseDTO import com.coded.spring.ordering.orders.RequestItem import com.coded.spring.ordering.orders.RequestOrder @@ -20,9 +21,7 @@ import org.springframework.http.HttpStatus import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.test.context.ActiveProfiles import org.springframework.util.MultiValueMap -import java.time.Duration -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter +import java.math.BigDecimal import kotlin.test.assertEquals @SpringBootTest( @@ -72,15 +71,59 @@ class ApplicationTests { } @Test - fun `Adding user with correct paramter should work`() { - val request = CreateUserRequest(username = "mmmohammed67234", password = "12Ln34567") + fun `adding a new user with correct parameters should work`() { + val request = CreateUserRequest(username = "mohammed67234", password = "12Ln34567") val result = restTemplate.postForEntity("/register", request, String::class.java) assertEquals(HttpStatus.OK, result.statusCode) } @Test - fun `Addding a new order should work`(@Autowired jwtService: JwtService) { - //Mock + fun `adding a new user with with username already existing should NOT work`() { + val request = CreateUserRequest(username = "mohammed67234", password = "12Ln34567") + val result = restTemplate.postForEntity("/register", request, String::class.java) + assertEquals(HttpStatus.BAD_REQUEST, result.statusCode) + assertEquals( + """{"error":"username ${request.username} already exists"}""", + result.body + ) + } + + @Test + fun `adding a new user with with password having less than six chars should NOT work`() { + val request = CreateUserRequest(username = "mohammed6734234", password = "12") + val result = restTemplate.postForEntity("/register", request, String::class.java) + assertEquals(HttpStatus.BAD_REQUEST, result.statusCode) + assertEquals( + """{"error":"password must be at least 6 characters"}""", + result.body + ) + } + + @Test + fun `adding a new user with with password not having a capital letter should NOT work`() { + val request = CreateUserRequest(username = "mohamed672345324", password = "1234567n") + val result = restTemplate.postForEntity("/register", request, String::class.java) + assertEquals(HttpStatus.BAD_REQUEST, result.statusCode) + assertEquals( + """{"error":"password must have at least one capital letter"}""", + result.body + ) + } + + @Test + fun `adding a new user with with password not having a digit should NOT work`() { + val request = CreateUserRequest(username = "mohammmed67234", password = "Mohammedss") + val result = restTemplate.postForEntity("/register", request, String::class.java) + assertEquals(HttpStatus.BAD_REQUEST, result.statusCode) + assertEquals( + """{"error":"password must have at least one digit"}""", + result.body + ) + } + + @Test + fun `adding a new order with correct parameters should work`(@Autowired jwtService: JwtService) { + val token = jwtService.generateToken("momo1234") val headers = HttpHeaders( MultiValueMap.fromSingleValue(mapOf("Authorization" to "Bearer $token")) @@ -92,7 +135,6 @@ class ApplicationTests { items = listOf(RequestItem("Nuggies", 3.950)) ) - //Trigger val requestEntity = HttpEntity(body, headers) val actualResponse = restTemplate.exchange( "/orders/add", //Endpoint @@ -101,7 +143,6 @@ class ApplicationTests { OrderResponseDTO::class.java ) - // Assertions assertEquals(HttpStatus.OK, actualResponse.statusCode) val responseBody = actualResponse.body!! @@ -122,6 +163,65 @@ class ApplicationTests { } + @Test + fun `adding a new order with incorrect user id should NOT work`(@Autowired jwtService: JwtService) { + + val token = jwtService.generateToken("momo1234") + val headers = HttpHeaders( + MultiValueMap.fromSingleValue(mapOf("Authorization" to "Bearer $token")) + ) + + val body = RequestOrder( + userId = 900, + restaurant = "WK", + items = listOf(RequestItem("Nuggies", 3.950)) + ) + + val requestEntity = HttpEntity(body, headers) + val actualResponse = restTemplate.exchange( + "/orders/add", //Endpoint + HttpMethod.POST, + requestEntity, + String::class.java + ) + + assertEquals(HttpStatus.BAD_REQUEST, actualResponse.statusCode) + assertEquals( + """{"error":"user with ID ${requestEntity.body?.userId} was not found"}""", + actualResponse.body + ) + } + + @Test + fun `adding a new order with negative item price should NOT work`(@Autowired jwtService: JwtService) { + + val token = jwtService.generateToken("momo1234") + val headers = HttpHeaders( + MultiValueMap.fromSingleValue(mapOf("Authorization" to "Bearer $token")) + ) + + val body = RequestOrder( + userId = savedUser.id!!, + restaurant = "WK", + items = listOf(RequestItem("Nuggies", -3.950)) + ) + + val requestEntity = HttpEntity(body, headers) + val actualResponse = restTemplate.exchange( + "/orders/add", //Endpoint + HttpMethod.POST, + requestEntity, + String::class.java + ) + + assertEquals(HttpStatus.BAD_REQUEST, actualResponse.statusCode) + assertEquals( + """{"error":"item price cannot be negative"}""", + actualResponse.body + ) + + } + @Test fun `adding new profile should work`(@Autowired jwtService: JwtService) { val token = jwtService.generateToken("momo1234") @@ -146,5 +246,146 @@ class ApplicationTests { assertEquals(HttpStatus.OK, response.statusCode) } + @Test + fun `adding new profile with first name having numbers should NOT work`(@Autowired jwtService: JwtService) { + val token = jwtService.generateToken("momo1234") + val headers = HttpHeaders() + headers.set("Authorization", "Bearer $token") + + val requestBody = RequestProfileDTO( + firstName = "Mohammed111", + lastName = "Sheshtar", + phoneNumber = "12345678" + ) + + val entity = HttpEntity(requestBody, headers) + + val response = restTemplate.exchange( + "/profile", + HttpMethod.POST, + entity, + String::class.java + ) + + assertEquals(HttpStatus.BAD_REQUEST, response.statusCode) + assertEquals( + """{"error":"first name must not contain any numbers"}""", + response.body + ) + } + + @Test + fun `adding new profile with last name having numbers should NOT work`(@Autowired jwtService: JwtService) { + val token = jwtService.generateToken("momo1234") + val headers = HttpHeaders() + headers.set("Authorization", "Bearer $token") + + val requestBody = RequestProfileDTO( + firstName = "Mohammed", + lastName = "Sheshtar222", + phoneNumber = "12345678" + ) + + val entity = HttpEntity(requestBody, headers) + + val response = restTemplate.exchange( + "/profile", + HttpMethod.POST, + entity, + String::class.java + ) + + assertEquals(HttpStatus.BAD_REQUEST, response.statusCode) + assertEquals( + """{"error":"last name must not contain any numbers"}""", + response.body + ) + } + + @Test + fun `adding new profile with phone number not being eight digits should NOT work`(@Autowired jwtService: JwtService) { + val token = jwtService.generateToken("momo1234") + val headers = HttpHeaders() + headers.set("Authorization", "Bearer $token") + + val requestBody = RequestProfileDTO( + firstName = "Mohammed", + lastName = "Sheshtar", + phoneNumber = "12378" + ) + + val entity = HttpEntity(requestBody, headers) + + val response = restTemplate.exchange( + "/profile", + HttpMethod.POST, + entity, + String::class.java + ) + + assertEquals(HttpStatus.BAD_REQUEST, response.statusCode) + assertEquals( + """{"error":"phone number must be 8 digits"}""", + response.body + ) + } + + @Test + fun `fetching list of orders should work`(@Autowired jwtService: JwtService) { + val token = jwtService.generateToken("momo1234") + val headers = HttpHeaders() + headers.set("Authorization", "Bearer $token") + + val request = HttpEntity(headers) + + val result = restTemplate.exchange( + "/orders", + HttpMethod.GET, + request, + String::class.java + ) + + assertEquals(HttpStatus.OK, result.statusCode) + } + + @Test + fun `adding a menu should work`(@Autowired jwtService: JwtService) { + val token = jwtService.generateToken("momo1234") + val headers = HttpHeaders() + headers.set("Authorization", "Bearer $token") + + val requestBody = MenuDTO( + name = "Hamburga", + price = BigDecimal.TWO + ) + + val entity = HttpEntity(requestBody, headers) + + val response = restTemplate.exchange( + "/menus", + HttpMethod.POST, + entity, + String::class.java + ) + + assertEquals(HttpStatus.OK, response.statusCode) + } + + @Test + fun `fetching list of menus should work`(@Autowired jwtService: JwtService) { + val token = jwtService.generateToken("momo1234") + val headers = HttpHeaders() + headers.set("Authorization", "Bearer $token") + + val request = HttpEntity(headers) + val result = restTemplate.exchange( + "/menus", + HttpMethod.GET, + request, + String::class.java + ) + + assertEquals(HttpStatus.OK, result.statusCode) + } } From 3f626885ea79c4dcb1883f8393d8ee72246aefe1 Mon Sep 17 00:00:00 2001 From: mohammedsheshtar <112146380+mohammedsheshtar@users.noreply.github.com> Date: Tue, 29 Apr 2025 12:52:00 +0300 Subject: [PATCH 12/18] Implmented a logging filter layer into the design --- pom.xml | 22 +++++ .../com/coded/spring/ordering/Application.kt | 7 ++ .../coded/spring/ordering/LoggingFilter.kt | 92 +++++++++++++++++++ .../ordering/authentication/SecurityConfig.kt | 2 +- .../spring/ordering/menus/MenuController.kt | 2 +- .../spring/ordering/menus/MenuService.kt | 19 +++- src/main/resources/application.properties | 1 + .../coded/spring/ordering/CucumberRunner.kt | 8 ++ .../coded/spring/ordering/steps/AuthSteps.kt | 2 + .../coded/spring/ordering/steps/MenusSteps.kt | 2 + .../spring/ordering/steps/OrdersSteps.kt | 2 + .../spring/ordering/steps/ProfilesSteps.kt | 2 + .../coded/spring/ordering/steps/UserSteps.kt | 2 + src/test/resources/features/auth.feature | 0 src/test/resources/features/menus.feature | 0 src/test/resources/features/orders.feature | 0 src/test/resources/features/profiles.feature | 0 src/test/resources/features/users.feature | 0 18 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/com/coded/spring/ordering/LoggingFilter.kt create mode 100644 src/test/kotlin/com/coded/spring/ordering/CucumberRunner.kt create mode 100644 src/test/kotlin/com/coded/spring/ordering/steps/AuthSteps.kt create mode 100644 src/test/kotlin/com/coded/spring/ordering/steps/MenusSteps.kt create mode 100644 src/test/kotlin/com/coded/spring/ordering/steps/OrdersSteps.kt create mode 100644 src/test/kotlin/com/coded/spring/ordering/steps/ProfilesSteps.kt create mode 100644 src/test/kotlin/com/coded/spring/ordering/steps/UserSteps.kt create mode 100644 src/test/resources/features/auth.feature create mode 100644 src/test/resources/features/menus.feature create mode 100644 src/test/resources/features/orders.feature create mode 100644 src/test/resources/features/profiles.feature create mode 100644 src/test/resources/features/users.feature diff --git a/pom.xml b/pom.xml index a59abe2..53b2de5 100644 --- a/pom.xml +++ b/pom.xml @@ -91,12 +91,34 @@ runtime 0.11.5 + + io.cucumber + cucumber-java + 7.20.1 + test + + + io.cucumber + cucumber-spring + 7.14.0 + test + io.jsonwebtoken jjwt-jackson runtime 0.11.5 + + com.hazelcast + hazelcast + 5.5.0 + + + org.springdoc + springdoc-openapi-starter-webmvc-api + 2.6.0 + com.h2database h2 diff --git a/src/main/kotlin/com/coded/spring/ordering/Application.kt b/src/main/kotlin/com/coded/spring/ordering/Application.kt index 8554e49..3756b90 100644 --- a/src/main/kotlin/com/coded/spring/ordering/Application.kt +++ b/src/main/kotlin/com/coded/spring/ordering/Application.kt @@ -1,5 +1,8 @@ package com.coded.spring.ordering +import com.hazelcast.config.Config +import com.hazelcast.core.Hazelcast +import com.hazelcast.core.HazelcastInstance import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @@ -8,4 +11,8 @@ class Application fun main(args: Array) { runApplication(*args) + orderConfig.getMapConfig("menus").setTimeToLiveSeconds(60) } + +val orderConfig = Config("order-cache") +val serverCache: HazelcastInstance = Hazelcast.newHazelcastInstance(orderConfig) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/LoggingFilter.kt b/src/main/kotlin/com/coded/spring/ordering/LoggingFilter.kt new file mode 100644 index 0000000..e7e5440 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/LoggingFilter.kt @@ -0,0 +1,92 @@ +package com.coded.spring.ordering + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter +import org.springframework.web.util.ContentCachingRequestWrapper +import org.springframework.web.util.ContentCachingResponseWrapper +import org.slf4j.LoggerFactory + +@Component +class LoggingFilter : OncePerRequestFilter() { + + private val logger = LoggerFactory.getLogger(LoggingFilter::class.java) + private val objectMapper = ObjectMapper().apply { + enable(SerializationFeature.INDENT_OUTPUT) + } + + private val RESET = "\u001B[0m" + private val GREEN = "\u001B[32m" + private val YELLOW = "\u001B[33m" + private val RED = "\u001B[31m" + + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + val cachedRequest = ContentCachingRequestWrapper(request) + val cachedResponse = ContentCachingResponseWrapper(response) + + filterChain.doFilter(cachedRequest, cachedResponse) + + logRequest(cachedRequest) + logResponse(cachedResponse) + + cachedResponse.copyBodyToResponse() + } + + private fun logRequest(request: ContentCachingRequestWrapper) { + val requestBody = request.contentAsByteArray.toString(Charsets.UTF_8).trim() + + logger.info( + """ + |[*] Incoming Request + |Method: ${request.method} + |URI: ${request.requestURI} + |Body: ${formatJsonIfPossible(requestBody)} + |------------------------------------------------------------------------------------------------ + """.trimMargin() + ) + } + + private fun logResponse(response: ContentCachingResponseWrapper) { + val responseBody = response.contentAsByteArray.toString(Charsets.UTF_8).trim() + val color = getColorForStatus(response.status) + + logger.info( + """ + |[*] Outgoing Response + |Status: $color${response.status}$RESET + |Body: ${formatJsonIfPossible(responseBody)} + |=============================================================================================== + """.trimMargin() + ) + } + + private fun formatJsonIfPossible(content: String): String { + return try { + if (content.isBlank()) { + "(empty body)" + } else { + val jsonNode = objectMapper.readTree(content) + objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonNode) + } + } catch (ex: Exception) { + content + } + } + + private fun getColorForStatus(status: Int): String { + return when { + status in 200..299 -> GREEN + status in 400..499 -> YELLOW + status >= 500 -> RED + else -> RESET + } + } +} diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt index 7f728e9..c1628f3 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt @@ -32,7 +32,7 @@ class SecurityConfig( fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { http.csrf { it.disable() } .authorizeHttpRequests { - it.requestMatchers("/menus", "/register", "/authentication/**", "/hello").permitAll() // public route + it.requestMatchers("/menus", "/register", "/authentication/**", "/hello", "api-docs").permitAll() // public route it.requestMatchers("/orders/**", "/profile/**").authenticated() .anyRequest().authenticated() } diff --git a/src/main/kotlin/com/coded/spring/ordering/menus/MenuController.kt b/src/main/kotlin/com/coded/spring/ordering/menus/MenuController.kt index a155014..91bb601 100644 --- a/src/main/kotlin/com/coded/spring/ordering/menus/MenuController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/menus/MenuController.kt @@ -8,7 +8,7 @@ class MenuController( private val menuService: MenuService ) { @GetMapping("/menus") - fun getMenu() = menuService.getMenu() + fun listMenu() = menuService.getMenu() @PostMapping("/menus") fun createMenu(@RequestBody menu: MenuDTO): MenuDTO = menuService.addMenu(menu) diff --git a/src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt b/src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt index 839058d..e77067e 100644 --- a/src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt +++ b/src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt @@ -1,12 +1,26 @@ package com.coded.spring.ordering.menus +import com.coded.spring.ordering.serverCache +import com.hazelcast.logging.Logger import org.springframework.stereotype.Service +private val logger = Logger.getLogger("menus") @Service class MenuService( private val menuRepository: MenuRepository ) { - fun getMenu(): List = menuRepository.findAll() + fun getMenu(): List { + val menusCache = serverCache.getMap>("menus") + if (menusCache["menus"]?.size == 0 || menusCache["menus"] == null) { + logger.info("No menus found, caching new data...") + val menus = menuRepository.findAll() + menusCache.put("menus", menus) + return menus + } + logger.info("returning ${menusCache["menus"]?.size} menu items") + return menusCache["menus"] ?: listOf() + } + fun addMenu(dto: MenuDTO): MenuDTO { val newMenu = MenuEntity( @@ -14,6 +28,7 @@ class MenuService( price = dto.price ) menuRepository.save(newMenu) + // where we write invalidation for cache return MenuDTO(newMenu.name, newMenu.price) } -} +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 88f707c..5016f3a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -5,3 +5,4 @@ spring.datasource.username=postgres spring.datasource.password=123 spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +springdoc.api-docs.path=/api-docs \ No newline at end of file diff --git a/src/test/kotlin/com/coded/spring/ordering/CucumberRunner.kt b/src/test/kotlin/com/coded/spring/ordering/CucumberRunner.kt new file mode 100644 index 0000000..2da636f --- /dev/null +++ b/src/test/kotlin/com/coded/spring/ordering/CucumberRunner.kt @@ -0,0 +1,8 @@ +package com.coded.spring.ordering + +import io.cucumber.spring.CucumberContextConfiguration +import org.springframework.boot.test.context.SpringBootTest + +@CucumberContextConfiguration +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class CucumberSpringConfiguration diff --git a/src/test/kotlin/com/coded/spring/ordering/steps/AuthSteps.kt b/src/test/kotlin/com/coded/spring/ordering/steps/AuthSteps.kt new file mode 100644 index 0000000..9bd7d47 --- /dev/null +++ b/src/test/kotlin/com/coded/spring/ordering/steps/AuthSteps.kt @@ -0,0 +1,2 @@ +package com.coded.spring.ordering.steps + diff --git a/src/test/kotlin/com/coded/spring/ordering/steps/MenusSteps.kt b/src/test/kotlin/com/coded/spring/ordering/steps/MenusSteps.kt new file mode 100644 index 0000000..9bd7d47 --- /dev/null +++ b/src/test/kotlin/com/coded/spring/ordering/steps/MenusSteps.kt @@ -0,0 +1,2 @@ +package com.coded.spring.ordering.steps + diff --git a/src/test/kotlin/com/coded/spring/ordering/steps/OrdersSteps.kt b/src/test/kotlin/com/coded/spring/ordering/steps/OrdersSteps.kt new file mode 100644 index 0000000..9bd7d47 --- /dev/null +++ b/src/test/kotlin/com/coded/spring/ordering/steps/OrdersSteps.kt @@ -0,0 +1,2 @@ +package com.coded.spring.ordering.steps + diff --git a/src/test/kotlin/com/coded/spring/ordering/steps/ProfilesSteps.kt b/src/test/kotlin/com/coded/spring/ordering/steps/ProfilesSteps.kt new file mode 100644 index 0000000..9bd7d47 --- /dev/null +++ b/src/test/kotlin/com/coded/spring/ordering/steps/ProfilesSteps.kt @@ -0,0 +1,2 @@ +package com.coded.spring.ordering.steps + diff --git a/src/test/kotlin/com/coded/spring/ordering/steps/UserSteps.kt b/src/test/kotlin/com/coded/spring/ordering/steps/UserSteps.kt new file mode 100644 index 0000000..9bd7d47 --- /dev/null +++ b/src/test/kotlin/com/coded/spring/ordering/steps/UserSteps.kt @@ -0,0 +1,2 @@ +package com.coded.spring.ordering.steps + diff --git a/src/test/resources/features/auth.feature b/src/test/resources/features/auth.feature new file mode 100644 index 0000000..e69de29 diff --git a/src/test/resources/features/menus.feature b/src/test/resources/features/menus.feature new file mode 100644 index 0000000..e69de29 diff --git a/src/test/resources/features/orders.feature b/src/test/resources/features/orders.feature new file mode 100644 index 0000000..e69de29 diff --git a/src/test/resources/features/profiles.feature b/src/test/resources/features/profiles.feature new file mode 100644 index 0000000..e69de29 diff --git a/src/test/resources/features/users.feature b/src/test/resources/features/users.feature new file mode 100644 index 0000000..e69de29 From 73d59a83507be1189aaf215af37d34f7f0a8291c Mon Sep 17 00:00:00 2001 From: mohammedsheshtar <112146380+mohammedsheshtar@users.noreply.github.com> Date: Tue, 29 Apr 2025 12:53:08 +0300 Subject: [PATCH 13/18] Implmented a logging filter layer into the design --- .../ordering/steps/{AuthSteps.kt => AuthenticationSteps.kt} | 0 .../resources/features/{auth.feature => authentication.feature} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/test/kotlin/com/coded/spring/ordering/steps/{AuthSteps.kt => AuthenticationSteps.kt} (100%) rename src/test/resources/features/{auth.feature => authentication.feature} (100%) diff --git a/src/test/kotlin/com/coded/spring/ordering/steps/AuthSteps.kt b/src/test/kotlin/com/coded/spring/ordering/steps/AuthenticationSteps.kt similarity index 100% rename from src/test/kotlin/com/coded/spring/ordering/steps/AuthSteps.kt rename to src/test/kotlin/com/coded/spring/ordering/steps/AuthenticationSteps.kt diff --git a/src/test/resources/features/auth.feature b/src/test/resources/features/authentication.feature similarity index 100% rename from src/test/resources/features/auth.feature rename to src/test/resources/features/authentication.feature From a60e36a38fabd9e3a5c9f7289544da264c0e6794 Mon Sep 17 00:00:00 2001 From: mohammedsheshtar <112146380+mohammedsheshtar@users.noreply.github.com> Date: Tue, 29 Apr 2025 13:12:49 +0300 Subject: [PATCH 14/18] Added Hazelcast caching for menu retrieval and invalidation logic to refresh cache on new menu item creation --- .../kotlin/com/coded/spring/ordering/menus/MenuService.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt b/src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt index e77067e..46c6e89 100644 --- a/src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt +++ b/src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt @@ -28,7 +28,9 @@ class MenuService( price = dto.price ) menuRepository.save(newMenu) - // where we write invalidation for cache + + val menusCache = serverCache.getMap>("menus") + menusCache.remove("menus") return MenuDTO(newMenu.name, newMenu.price) } } \ No newline at end of file From 145290ccd3872f1671e32644f02129b1a9e7f474 Mon Sep 17 00:00:00 2001 From: mohammedsheshtar <112146380+mohammedsheshtar@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:06:27 +0300 Subject: [PATCH 15/18] Implemented API documentation through Swagger and added a JSON file that included machine readable spec of our server's API --- pom.xml | 4 ++++ .../kotlin/com/coded/spring/ordering/HelloWorldController.kt | 2 ++ .../Mohammed-Sheshtar-online-ordering-api-swagger-01.json | 1 + .../ordering/authentication/AuthenticationController.kt | 2 ++ .../kotlin/com/coded/spring/ordering/menus/MenuController.kt | 2 ++ .../coded/spring/ordering/orders/OnlineOrderingController.kt | 2 ++ .../com/coded/spring/ordering/profiles/ProfileController.kt | 2 ++ .../kotlin/com/coded/spring/ordering/users/UserController.kt | 2 ++ 8 files changed, 17 insertions(+) create mode 100644 src/main/kotlin/com/coded/spring/ordering/Mohammed-Sheshtar-online-ordering-api-swagger-01.json diff --git a/pom.xml b/pom.xml index 53b2de5..3beb3c4 100644 --- a/pom.xml +++ b/pom.xml @@ -124,6 +124,10 @@ h2 test + + org.junit.jupiter + junit-jupiter-api + diff --git a/src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt b/src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt index 1725ef1..3e6228b 100644 --- a/src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt @@ -1,7 +1,9 @@ package com.coded.spring.ordering +import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.web.bind.annotation.* +@Tag(name="HelloWorldAPI") @RestController class HelloWorldController { diff --git a/src/main/kotlin/com/coded/spring/ordering/Mohammed-Sheshtar-online-ordering-api-swagger-01.json b/src/main/kotlin/com/coded/spring/ordering/Mohammed-Sheshtar-online-ordering-api-swagger-01.json new file mode 100644 index 0000000..01cde37 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/Mohammed-Sheshtar-online-ordering-api-swagger-01.json @@ -0,0 +1 @@ +{"openapi":"3.0.1","info":{"title":"OpenAPI definition","version":"v0"},"servers":[{"url":"http://localhost:9001","description":"Generated server url"}],"paths":{"/register":{"post":{"tags":["UserAPI"],"operationId":"registerUser","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/profile":{"post":{"tags":["ProfileAPI"],"operationId":"addProfile","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestProfileDTO"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/orders/add":{"post":{"tags":["MenuAPI"],"operationId":"addOrders","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestOrder"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/menus":{"get":{"tags":["MenuAPI"],"operationId":"listMenu","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MenuEntity"}}}}}}},"post":{"tags":["MenuAPI"],"operationId":"createMenu","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MenuDTO"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MenuDTO"}}}}}}},"/authentication/login":{"post":{"tags":["AuthenticationAPI"],"operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthenticationRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthenticationResponse"}}}}}}},"/orders":{"get":{"tags":["MenuAPI"],"operationId":"getOrders","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OrderEntity"}}}}}}}},"/hello":{"get":{"tags":["HelloWorldAPI"],"operationId":"helloWorld","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"string"}}}}}}}},"components":{"schemas":{"CreateUserRequest":{"required":["password","username"],"type":"object","properties":{"username":{"type":"string"},"password":{"type":"string"}}},"RequestProfileDTO":{"required":["firstName","lastName","phoneNumber"],"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"phoneNumber":{"type":"string"}}},"RequestItem":{"required":["name","price"],"type":"object","properties":{"name":{"type":"string"},"price":{"type":"number","format":"double"}}},"RequestOrder":{"required":["items","restaurant","userId"],"type":"object","properties":{"userId":{"type":"integer","format":"int64"},"restaurant":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/components/schemas/RequestItem"}}}},"MenuDTO":{"required":["name","price"],"type":"object","properties":{"name":{"type":"string"},"price":{"type":"number"}}},"AuthenticationRequest":{"required":["password","username"],"type":"object","properties":{"username":{"type":"string"},"password":{"type":"string"}}},"AuthenticationResponse":{"required":["token"],"type":"object","properties":{"token":{"type":"string"}}},"ItemsEntity":{"required":["name","order","price"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"order":{"$ref":"#/components/schemas/OrderEntity"},"name":{"type":"string"},"price":{"type":"number","format":"double"}}},"OrderEntity":{"required":["restaurant","user"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"user":{"$ref":"#/components/schemas/UserEntity"},"restaurant":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/components/schemas/ItemsEntity"}},"timeOrdered":{"type":"string","format":"date-time"}}},"UserEntity":{"required":["password","role","username"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"username":{"type":"string"},"password":{"type":"string"},"role":{"type":"string","enum":["USER","ADMIN"]}}},"MenuEntity":{"required":["name","price"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"price":{"type":"number"}}}}}} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/AuthenticationController.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/AuthenticationController.kt index 9f71f5b..d0568a2 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/AuthenticationController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/AuthenticationController.kt @@ -5,8 +5,10 @@ import org.springframework.security.authentication.* import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.web.bind.annotation.* +import io.swagger.v3.oas.annotations.tags.Tag +@Tag(name="AuthenticationAPI") @RestController @RequestMapping("/authentication") class AuthenticationController( diff --git a/src/main/kotlin/com/coded/spring/ordering/menus/MenuController.kt b/src/main/kotlin/com/coded/spring/ordering/menus/MenuController.kt index 91bb601..642a247 100644 --- a/src/main/kotlin/com/coded/spring/ordering/menus/MenuController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/menus/MenuController.kt @@ -1,8 +1,10 @@ package com.coded.spring.ordering.menus +import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.web.bind.annotation.* import java.math.BigDecimal +@Tag(name="MenuAPI") @RestController class MenuController( private val menuService: MenuService diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt index b91c461..aa6ff21 100644 --- a/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt @@ -2,8 +2,10 @@ package com.coded.spring.ordering.orders import com.coded.spring.ordering.items.ItemsEntity import com.coded.spring.ordering.items.ItemsRepository +import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.web.bind.annotation.* +@Tag(name="MenuAPI") @RestController class OnlineOrderingController( private val onlineOrderingService: OnlineOrderingService diff --git a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileController.kt b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileController.kt index 04303ea..9363df0 100644 --- a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileController.kt @@ -1,11 +1,13 @@ package com.coded.spring.ordering.profiles +import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.core.userdetails.User +@Tag(name="ProfileAPI") @RestController class ProfileController( private val profileService: ProfileService diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UserController.kt b/src/main/kotlin/com/coded/spring/ordering/users/UserController.kt index 2cd8d4f..24f9f10 100644 --- a/src/main/kotlin/com/coded/spring/ordering/users/UserController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/users/UserController.kt @@ -1,12 +1,14 @@ package com.coded.spring.ordering.users +import io.swagger.v3.oas.annotations.tags.Tag import jakarta.validation.Valid import jakarta.validation.constraints.NotBlank import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* +@Tag(name="UserAPI") @RestController class UserController( private val userService : UserService From d76072a297df41f1a854c4899bc9db7bce84f464 Mon Sep 17 00:00:00 2001 From: mohammedsheshtar <112146380+mohammedsheshtar@users.noreply.github.com> Date: Tue, 29 Apr 2025 18:53:48 +0300 Subject: [PATCH 16/18] Completed configuration task and updated the Swagger machine readable JSON file --- .../spring/ordering/HelloWorldController.kt | 8 ++++-- ...eshtar-online-ordering-api-swagger-01.json | 1 - ...eshtar-online-ordering-api-swagger-02.json | 1 + .../spring/ordering/WelcomeController.kt | 23 ++++++++++++++++ .../ordering/authentication/SecurityConfig.kt | 11 ++++++-- .../spring/ordering/menus/MenuController.kt | 2 +- .../spring/ordering/menus/MenuRepository.kt | 4 +-- .../spring/ordering/menus/MenuService.kt | 27 +++++++++++++++---- 8 files changed, 64 insertions(+), 13 deletions(-) delete mode 100644 src/main/kotlin/com/coded/spring/ordering/Mohammed-Sheshtar-online-ordering-api-swagger-01.json create mode 100644 src/main/kotlin/com/coded/spring/ordering/Mohammed-Sheshtar-online-ordering-api-swagger-02.json create mode 100644 src/main/kotlin/com/coded/spring/ordering/WelcomeController.kt diff --git a/src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt b/src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt index 3e6228b..746a783 100644 --- a/src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt @@ -1,12 +1,16 @@ package com.coded.spring.ordering import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.beans.factory.annotation.Value import org.springframework.web.bind.annotation.* @Tag(name="HelloWorldAPI") @RestController -class HelloWorldController { +class HelloWorldController( + @Value("\${hello-world}") + val helloWorldMessage: String +) { @GetMapping("/hello") - fun helloWorld() = "Hello World"; + fun helloWorld() = helloWorldMessage; } \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/Mohammed-Sheshtar-online-ordering-api-swagger-01.json b/src/main/kotlin/com/coded/spring/ordering/Mohammed-Sheshtar-online-ordering-api-swagger-01.json deleted file mode 100644 index 01cde37..0000000 --- a/src/main/kotlin/com/coded/spring/ordering/Mohammed-Sheshtar-online-ordering-api-swagger-01.json +++ /dev/null @@ -1 +0,0 @@ -{"openapi":"3.0.1","info":{"title":"OpenAPI definition","version":"v0"},"servers":[{"url":"http://localhost:9001","description":"Generated server url"}],"paths":{"/register":{"post":{"tags":["UserAPI"],"operationId":"registerUser","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/profile":{"post":{"tags":["ProfileAPI"],"operationId":"addProfile","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestProfileDTO"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/orders/add":{"post":{"tags":["MenuAPI"],"operationId":"addOrders","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestOrder"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/menus":{"get":{"tags":["MenuAPI"],"operationId":"listMenu","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MenuEntity"}}}}}}},"post":{"tags":["MenuAPI"],"operationId":"createMenu","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MenuDTO"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MenuDTO"}}}}}}},"/authentication/login":{"post":{"tags":["AuthenticationAPI"],"operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthenticationRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthenticationResponse"}}}}}}},"/orders":{"get":{"tags":["MenuAPI"],"operationId":"getOrders","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OrderEntity"}}}}}}}},"/hello":{"get":{"tags":["HelloWorldAPI"],"operationId":"helloWorld","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"string"}}}}}}}},"components":{"schemas":{"CreateUserRequest":{"required":["password","username"],"type":"object","properties":{"username":{"type":"string"},"password":{"type":"string"}}},"RequestProfileDTO":{"required":["firstName","lastName","phoneNumber"],"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"phoneNumber":{"type":"string"}}},"RequestItem":{"required":["name","price"],"type":"object","properties":{"name":{"type":"string"},"price":{"type":"number","format":"double"}}},"RequestOrder":{"required":["items","restaurant","userId"],"type":"object","properties":{"userId":{"type":"integer","format":"int64"},"restaurant":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/components/schemas/RequestItem"}}}},"MenuDTO":{"required":["name","price"],"type":"object","properties":{"name":{"type":"string"},"price":{"type":"number"}}},"AuthenticationRequest":{"required":["password","username"],"type":"object","properties":{"username":{"type":"string"},"password":{"type":"string"}}},"AuthenticationResponse":{"required":["token"],"type":"object","properties":{"token":{"type":"string"}}},"ItemsEntity":{"required":["name","order","price"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"order":{"$ref":"#/components/schemas/OrderEntity"},"name":{"type":"string"},"price":{"type":"number","format":"double"}}},"OrderEntity":{"required":["restaurant","user"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"user":{"$ref":"#/components/schemas/UserEntity"},"restaurant":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/components/schemas/ItemsEntity"}},"timeOrdered":{"type":"string","format":"date-time"}}},"UserEntity":{"required":["password","role","username"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"username":{"type":"string"},"password":{"type":"string"},"role":{"type":"string","enum":["USER","ADMIN"]}}},"MenuEntity":{"required":["name","price"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"price":{"type":"number"}}}}}} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/Mohammed-Sheshtar-online-ordering-api-swagger-02.json b/src/main/kotlin/com/coded/spring/ordering/Mohammed-Sheshtar-online-ordering-api-swagger-02.json new file mode 100644 index 0000000..327d20c --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/Mohammed-Sheshtar-online-ordering-api-swagger-02.json @@ -0,0 +1 @@ +{"openapi":"3.0.1","info":{"title":"OpenAPI definition","version":"v0"},"servers":[{"url":"http://localhost:9001","description":"Generated server url"}],"paths":{"/register":{"post":{"tags":["UserAPI"],"operationId":"registerUser","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/profile":{"post":{"tags":["ProfileAPI"],"operationId":"addProfile","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestProfileDTO"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/orders/add":{"post":{"tags":["MenuAPI"],"operationId":"addOrders","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestOrder"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/menus":{"get":{"tags":["MenuAPI"],"operationId":"listMenu","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MenuEntity"}}}}}}},"post":{"tags":["MenuAPI"],"operationId":"createMenu","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MenuDTO"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MenuDTO"}}}}}}},"/authentication/login":{"post":{"tags":["AuthenticationAPI"],"operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthenticationRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthenticationResponse"}}}}}}},"/welcome":{"get":{"tags":["WelcomeAPI"],"operationId":"greetUser","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"string"}}}}}}},"/orders":{"get":{"tags":["MenuAPI"],"operationId":"getOrders","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OrderEntity"}}}}}}}},"/hello":{"get":{"tags":["HelloWorldAPI"],"operationId":"helloWorld","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"string"}}}}}}}},"components":{"schemas":{"CreateUserRequest":{"required":["password","username"],"type":"object","properties":{"username":{"type":"string"},"password":{"type":"string"}}},"RequestProfileDTO":{"required":["firstName","lastName","phoneNumber"],"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"phoneNumber":{"type":"string"}}},"RequestItem":{"required":["name","price"],"type":"object","properties":{"name":{"type":"string"},"price":{"type":"number","format":"double"}}},"RequestOrder":{"required":["items","restaurant","userId"],"type":"object","properties":{"userId":{"type":"integer","format":"int64"},"restaurant":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/components/schemas/RequestItem"}}}},"MenuDTO":{"required":["name","price"],"type":"object","properties":{"name":{"type":"string"},"price":{"type":"number"}}},"AuthenticationRequest":{"required":["password","username"],"type":"object","properties":{"username":{"type":"string"},"password":{"type":"string"}}},"AuthenticationResponse":{"required":["token"],"type":"object","properties":{"token":{"type":"string"}}},"ItemsEntity":{"required":["name","order","price"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"order":{"$ref":"#/components/schemas/OrderEntity"},"name":{"type":"string"},"price":{"type":"number","format":"double"}}},"OrderEntity":{"required":["restaurant","user"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"user":{"$ref":"#/components/schemas/UserEntity"},"restaurant":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/components/schemas/ItemsEntity"}},"timeOrdered":{"type":"string","format":"date-time"}}},"UserEntity":{"required":["password","role","username"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"username":{"type":"string"},"password":{"type":"string"},"role":{"type":"string","enum":["USER","ADMIN"]}}},"MenuEntity":{"required":["name","price"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"price":{"type":"number"}}}}}} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/WelcomeController.kt b/src/main/kotlin/com/coded/spring/ordering/WelcomeController.kt new file mode 100644 index 0000000..559cb25 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/WelcomeController.kt @@ -0,0 +1,23 @@ +package com.coded.spring.ordering + +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.beans.factory.annotation.Value +import org.springframework.web.bind.annotation.* + + +@RestController +@Tag(name = "WelcomeAPI") +class WelcomeController( + @Value("\${company-name}") + private val companyName: String, + @Value("\${feature.festive.enabled}") + private val festiveIsEnabled: Boolean +) { + @GetMapping("/welcome") + fun greetUser() = if(!festiveIsEnabled){ + "Welcome to online ordering by $companyName" + } else { + "Eidkom Mubarak!" + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt index c1628f3..003d5c8 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt @@ -32,8 +32,15 @@ class SecurityConfig( fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { http.csrf { it.disable() } .authorizeHttpRequests { - it.requestMatchers("/menus", "/register", "/authentication/**", "/hello", "api-docs").permitAll() // public route - it.requestMatchers("/orders/**", "/profile/**").authenticated() + it.requestMatchers( + "/menus", + "/register", + "/authentication/**", + "/hello", "api-docs", + "/welcome").permitAll() // public route + it.requestMatchers( + "/orders/**", + "/profile/**").authenticated() // authentication route .anyRequest().authenticated() } .sessionManagement { diff --git a/src/main/kotlin/com/coded/spring/ordering/menus/MenuController.kt b/src/main/kotlin/com/coded/spring/ordering/menus/MenuController.kt index 642a247..486b614 100644 --- a/src/main/kotlin/com/coded/spring/ordering/menus/MenuController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/menus/MenuController.kt @@ -10,7 +10,7 @@ class MenuController( private val menuService: MenuService ) { @GetMapping("/menus") - fun listMenu() = menuService.getMenu() + fun listMenu() = menuService.listMenu() @PostMapping("/menus") fun createMenu(@RequestBody menu: MenuDTO): MenuDTO = menuService.addMenu(menu) diff --git a/src/main/kotlin/com/coded/spring/ordering/menus/MenuRepository.kt b/src/main/kotlin/com/coded/spring/ordering/menus/MenuRepository.kt index 5f97435..390a1da 100644 --- a/src/main/kotlin/com/coded/spring/ordering/menus/MenuRepository.kt +++ b/src/main/kotlin/com/coded/spring/ordering/menus/MenuRepository.kt @@ -20,8 +20,8 @@ data class MenuEntity( @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long? = null, val name: String, - @Column(precision = 9, scale = 3) - val price: BigDecimal + @Column(precision = 9, scale = 3) + val price: BigDecimal ){ constructor() : this(null, "", BigDecimal.ZERO) } \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt b/src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt index 46c6e89..6b409ee 100644 --- a/src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt +++ b/src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt @@ -2,20 +2,37 @@ package com.coded.spring.ordering.menus import com.coded.spring.ordering.serverCache import com.hazelcast.logging.Logger +import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service +import java.math.BigDecimal +import java.math.RoundingMode + private val logger = Logger.getLogger("menus") @Service class MenuService( - private val menuRepository: MenuRepository + private val menuRepository: MenuRepository, + @Value("\${feature.festive.enabled}") + private val festiveIsEnabled: Boolean ) { - fun getMenu(): List { + fun listMenu(): List { val menusCache = serverCache.getMap>("menus") if (menusCache["menus"]?.size == 0 || menusCache["menus"] == null) { logger.info("No menus found, caching new data...") - val menus = menuRepository.findAll() - menusCache.put("menus", menus) - return menus + if(festiveIsEnabled) { + val menus = menuRepository.findAll().map { it.copy( + price = it.price + .multiply(BigDecimal("0.8")) + .setScale(3, RoundingMode.HALF_UP)) + } + menusCache.put("menus", menus) + return menus + } + else { + val menus = menuRepository.findAll() + menusCache.put("menus", menus) + return menus + } } logger.info("returning ${menusCache["menus"]?.size} menu items") return menusCache["menus"] ?: listOf() From a12de340f9e2ea7826b8e1004fc7aefe25922305 Mon Sep 17 00:00:00 2001 From: mohammedsheshtar <112146380+mohammedsheshtar@users.noreply.github.com> Date: Fri, 2 May 2025 12:15:33 +0300 Subject: [PATCH 17/18] Converted the project from a monolithic server to a microservices server by splitting the authentication and ordering parts of the project --- authentication/pom.xml | 20 ++++ .../AuthenticationApplication.kt | 12 +++ .../AuthenticationController.kt | 48 ++++++++-- .../CustomUserDetailsService.kt | 13 +-- .../spring/authentication/LoggingFilter.kt | 92 +++++++++++++++++++ .../spring}/authentication/SecurityConfig.kt | 13 +-- .../jwt/JwtAuthenticationFilter.kt | 5 +- .../spring}/authentication/jwt/JwtService.kt | 5 +- .../profiles/ProfileController.kt | 2 +- .../profiles/ProfileRepository.kt | 8 +- .../profiles/ProfileService.kt | 9 +- .../authentication}/scripts/InitUserRunner.kt | 12 +-- .../authentication}/users/UserController.kt | 7 +- .../authentication}/users/UserRepository.kt | 4 +- .../authentication}/users/UserService.kt | 21 +++-- .../src/main/resources/application.properties | 8 ++ ordering/pom.xml | 27 ++++++ .../spring/ordering/HelloWorldController.kt | 0 .../coded/spring/ordering/LoggingFilter.kt | 0 ...eshtar-online-ordering-api-swagger-02.json | 0 .../spring/ordering/OrderingApplication.kt | 4 +- .../spring/ordering/WelcomeController.kt | 2 - .../ordering/client/AuthenticationClient.kt | 29 ++++++ .../spring/ordering/items/ItemsRepository.kt | 0 .../spring/ordering/menus/MenuController.kt | 0 .../spring/ordering/menus/MenuRepository.kt | 1 - .../spring/ordering/menus/MenuService.kt | 0 .../orders/OnlineOrderingController.kt | 16 ++-- .../orders/OnlineOrderingRepository.kt | 12 +-- .../ordering/orders/OnlineOrderingService.kt | 29 ++++-- .../security/RemoteAuthenticationFilter.kt | 32 +++++++ .../ordering/security/SecurityConfig.kt | 30 ++++++ .../src/main/resources/application.properties | 8 ++ pom.xml | 9 +- src/main/resources/application.properties | 2 +- .../coded/spring/ordering/ApplicationTests.kt | 2 +- .../coded/spring/ordering/CucumberRunner.kt | 6 +- .../ordering/steps/AuthenticationSteps.kt | 45 +++++++++ .../resources/application-test.properties | 2 - .../resources/features/authentication.feature | 7 ++ 40 files changed, 455 insertions(+), 87 deletions(-) create mode 100644 authentication/pom.xml create mode 100644 authentication/src/main/kotlin/com/coded/spring/authentication/AuthenticationApplication.kt rename {src/main/kotlin/com/coded/spring/ordering => authentication/src/main/kotlin/com/coded/spring}/authentication/AuthenticationController.kt (50%) rename {src/main/kotlin/com/coded/spring/ordering => authentication/src/main/kotlin/com/coded/spring}/authentication/CustomUserDetailsService.kt (68%) create mode 100644 authentication/src/main/kotlin/com/coded/spring/authentication/LoggingFilter.kt rename {src/main/kotlin/com/coded/spring/ordering => authentication/src/main/kotlin/com/coded/spring}/authentication/SecurityConfig.kt (82%) rename {src/main/kotlin/com/coded/spring/ordering => authentication/src/main/kotlin/com/coded/spring}/authentication/jwt/JwtAuthenticationFilter.kt (92%) rename {src/main/kotlin/com/coded/spring/ordering => authentication/src/main/kotlin/com/coded/spring}/authentication/jwt/JwtService.kt (90%) rename {src/main/kotlin/com/coded/spring/ordering => authentication/src/main/kotlin/com/coded/spring/authentication}/profiles/ProfileController.kt (93%) rename {src/main/kotlin/com/coded/spring/ordering => authentication/src/main/kotlin/com/coded/spring/authentication}/profiles/ProfileRepository.kt (77%) rename {src/main/kotlin/com/coded/spring/ordering => authentication/src/main/kotlin/com/coded/spring/authentication}/profiles/ProfileService.kt (91%) rename {src/main/kotlin/com/coded/spring/ordering => authentication/src/main/kotlin/com/coded/spring/authentication}/scripts/InitUserRunner.kt (72%) rename {src/main/kotlin/com/coded/spring/ordering => authentication/src/main/kotlin/com/coded/spring/authentication}/users/UserController.kt (73%) rename {src/main/kotlin/com/coded/spring/ordering => authentication/src/main/kotlin/com/coded/spring/authentication}/users/UserRepository.kt (93%) rename {src/main/kotlin/com/coded/spring/ordering => authentication/src/main/kotlin/com/coded/spring/authentication}/users/UserService.kt (61%) create mode 100644 authentication/src/main/resources/application.properties create mode 100644 ordering/pom.xml rename {src => ordering/src}/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt (100%) rename {src => ordering/src}/main/kotlin/com/coded/spring/ordering/LoggingFilter.kt (100%) rename {src => ordering/src}/main/kotlin/com/coded/spring/ordering/Mohammed-Sheshtar-online-ordering-api-swagger-02.json (100%) rename src/main/kotlin/com/coded/spring/ordering/Application.kt => ordering/src/main/kotlin/com/coded/spring/ordering/OrderingApplication.kt (87%) rename {src => ordering/src}/main/kotlin/com/coded/spring/ordering/WelcomeController.kt (99%) create mode 100644 ordering/src/main/kotlin/com/coded/spring/ordering/client/AuthenticationClient.kt rename {src => ordering/src}/main/kotlin/com/coded/spring/ordering/items/ItemsRepository.kt (100%) rename {src => ordering/src}/main/kotlin/com/coded/spring/ordering/menus/MenuController.kt (100%) rename {src => ordering/src}/main/kotlin/com/coded/spring/ordering/menus/MenuRepository.kt (93%) rename {src => ordering/src}/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt (100%) rename {src => ordering/src}/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt (58%) rename {src => ordering/src}/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingRepository.kt (69%) rename {src => ordering/src}/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingService.kt (62%) create mode 100644 ordering/src/main/kotlin/com/coded/spring/ordering/security/RemoteAuthenticationFilter.kt create mode 100644 ordering/src/main/kotlin/com/coded/spring/ordering/security/SecurityConfig.kt create mode 100644 ordering/src/main/resources/application.properties diff --git a/authentication/pom.xml b/authentication/pom.xml new file mode 100644 index 0000000..829bbec --- /dev/null +++ b/authentication/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + com.coded.spring + Kotlin.SpringbootV2 + 0.0.1-SNAPSHOT + + + authentication + + + UTF-8 + official + 1.8 + + + \ No newline at end of file diff --git a/authentication/src/main/kotlin/com/coded/spring/authentication/AuthenticationApplication.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/AuthenticationApplication.kt new file mode 100644 index 0000000..97e486b --- /dev/null +++ b/authentication/src/main/kotlin/com/coded/spring/authentication/AuthenticationApplication.kt @@ -0,0 +1,12 @@ +package com.coded.spring.authentication + + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class AuthenticationApplication + +fun main(args: Array) { + runApplication(*args) +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/AuthenticationController.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/AuthenticationController.kt similarity index 50% rename from src/main/kotlin/com/coded/spring/ordering/authentication/AuthenticationController.kt rename to authentication/src/main/kotlin/com/coded/spring/authentication/AuthenticationController.kt index d0568a2..a121c5a 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/AuthenticationController.kt +++ b/authentication/src/main/kotlin/com/coded/spring/authentication/AuthenticationController.kt @@ -1,11 +1,17 @@ -package com.coded.spring.ordering.authentication +package com.coded.spring.authentication -import com.coded.spring.ordering.authentication.jwt.JwtService -import org.springframework.security.authentication.* +import com.coded.spring.authentication.jwt.JwtService +import com.coded.spring.authentication.users.UserService +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.core.userdetails.UsernameNotFoundException -import org.springframework.web.bind.annotation.* -import io.swagger.v3.oas.annotations.tags.Tag +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 java.security.Principal @Tag(name="AuthenticationAPI") @@ -14,7 +20,8 @@ import io.swagger.v3.oas.annotations.tags.Tag class AuthenticationController( private val authenticationManager: AuthenticationManager, private val userDetailsService: UserDetailsService, - private val jwtService: JwtService + private val jwtService: JwtService, + private val userService: UserService ) { @PostMapping("/login") @@ -30,8 +37,18 @@ class AuthenticationController( throw UsernameNotFoundException("Invalid user request!") } } + + @PostMapping("/check-token") + fun checkToken( + principal: Principal + ): CheckTokenResponse { + return CheckTokenResponse( + userId = userService.findByUsername(principal.name) + ) + } } + data class AuthenticationRequest( val username: String, val password: String @@ -39,4 +56,21 @@ data class AuthenticationRequest( data class AuthenticationResponse( val token: String -) \ No newline at end of file +) + +data class CheckTokenResponse( + val userId: Long +) + +data class RegisterRequest( + val username: String, + val password: String +) + +data class RegisterFailureResponse( + val error: AddUserError +) + +enum class AddUserError { + INVALID_USERNAME, TOO_SHORT_PASSWORD, MAX_ACCOUNT_LIMIT_REACHED +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/CustomUserDetailsService.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/CustomUserDetailsService.kt similarity index 68% rename from src/main/kotlin/com/coded/spring/ordering/authentication/CustomUserDetailsService.kt rename to authentication/src/main/kotlin/com/coded/spring/authentication/CustomUserDetailsService.kt index 443f180..97d1061 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/CustomUserDetailsService.kt +++ b/authentication/src/main/kotlin/com/coded/spring/authentication/CustomUserDetailsService.kt @@ -1,24 +1,25 @@ -package com.coded.spring.ordering.authentication +package com.coded.spring.authentication -import com.coded.spring.ordering.users.UserRepository + +import com.coded.spring.authentication.users.UserEntity +import com.coded.spring.authentication.users.UserRepository +import org.springframework.security.core.userdetails.User 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.security.core.userdetails.User @Service class CustomUserDetailsService( private val userRepository: UserRepository ) : UserDetailsService { override fun loadUserByUsername(username: String): UserDetails { - val user = userRepository.findByUsername(username) - ?: throw UsernameNotFoundException("User not found") + val user : UserEntity = userRepository.findByUsername(username) ?: + throw UsernameNotFoundException("User not found...") return User.builder() .username(user.username) .password(user.password) - .roles(user.role.toString()) .build() } } diff --git a/authentication/src/main/kotlin/com/coded/spring/authentication/LoggingFilter.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/LoggingFilter.kt new file mode 100644 index 0000000..9064f5d --- /dev/null +++ b/authentication/src/main/kotlin/com/coded/spring/authentication/LoggingFilter.kt @@ -0,0 +1,92 @@ +package com.coded.spring.authentication + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter +import org.springframework.web.util.ContentCachingRequestWrapper +import org.springframework.web.util.ContentCachingResponseWrapper + +@Component +class LoggingFilter : OncePerRequestFilter() { + + private val logger = LoggerFactory.getLogger(LoggingFilter::class.java) + private val objectMapper = ObjectMapper().apply { + enable(SerializationFeature.INDENT_OUTPUT) + } + + private val RESET = "\u001B[0m" + private val GREEN = "\u001B[32m" + private val YELLOW = "\u001B[33m" + private val RED = "\u001B[31m" + + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + val cachedRequest = ContentCachingRequestWrapper(request) + val cachedResponse = ContentCachingResponseWrapper(response) + + filterChain.doFilter(cachedRequest, cachedResponse) + + logRequest(cachedRequest) + logResponse(cachedResponse) + + cachedResponse.copyBodyToResponse() + } + + private fun logRequest(request: ContentCachingRequestWrapper) { + val requestBody = request.contentAsByteArray.toString(Charsets.UTF_8).trim() + + logger.info( + """ + |[*] Incoming Request + |Method: ${request.method} + |URI: ${request.requestURI} + |Body: ${formatJsonIfPossible(requestBody)} + |------------------------------------------------------------------------------------------------ + """.trimMargin() + ) + } + + private fun logResponse(response: ContentCachingResponseWrapper) { + val responseBody = response.contentAsByteArray.toString(Charsets.UTF_8).trim() + val color = getColorForStatus(response.status) + + logger.info( + """ + |[*] Outgoing Response + |Status: $color${response.status}$RESET + |Body: ${formatJsonIfPossible(responseBody)} + |=============================================================================================== + """.trimMargin() + ) + } + + private fun formatJsonIfPossible(content: String): String { + return try { + if (content.isBlank()) { + "(empty body)" + } else { + val jsonNode = objectMapper.readTree(content) + objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonNode) + } + } catch (ex: Exception) { + content + } + } + + private fun getColorForStatus(status: Int): String { + return when { + status in 200..299 -> GREEN + status in 400..499 -> YELLOW + status >= 500 -> RED + else -> RESET + } + } +} diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/SecurityConfig.kt similarity index 82% rename from src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt rename to authentication/src/main/kotlin/com/coded/spring/authentication/SecurityConfig.kt index 003d5c8..efbb924 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt +++ b/authentication/src/main/kotlin/com/coded/spring/authentication/SecurityConfig.kt @@ -1,6 +1,6 @@ -package com.coded.spring.ordering.authentication +package com.coded.spring.authentication -import com.coded.spring.ordering.authentication.jwt.JwtAuthenticationFilter +import com.coded.spring.authentication.jwt.JwtAuthenticationFilter import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.security.authentication.AuthenticationManager @@ -10,7 +10,6 @@ import org.springframework.security.config.annotation.authentication.configurati import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.http.SessionCreationPolicy -import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.web.SecurityFilterChain @@ -33,14 +32,10 @@ class SecurityConfig( http.csrf { it.disable() } .authorizeHttpRequests { it.requestMatchers( - "/menus", "/register", "/authentication/**", - "/hello", "api-docs", - "/welcome").permitAll() // public route - it.requestMatchers( - "/orders/**", - "/profile/**").authenticated() // authentication route + "api-docs", + "/hello").permitAll() // public route .anyRequest().authenticated() } .sessionManagement { diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtAuthenticationFilter.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/jwt/JwtAuthenticationFilter.kt similarity index 92% rename from src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtAuthenticationFilter.kt rename to authentication/src/main/kotlin/com/coded/spring/authentication/jwt/JwtAuthenticationFilter.kt index f3370bd..10e75b5 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtAuthenticationFilter.kt +++ b/authentication/src/main/kotlin/com/coded/spring/authentication/jwt/JwtAuthenticationFilter.kt @@ -1,8 +1,9 @@ -package com.coded.spring.ordering.authentication.jwt +package com.coded.spring.authentication.jwt import jakarta.servlet.FilterChain -import jakarta.servlet.http.* +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.core.userdetails.UserDetailsService diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtService.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/jwt/JwtService.kt similarity index 90% rename from src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtService.kt rename to authentication/src/main/kotlin/com/coded/spring/authentication/jwt/JwtService.kt index c877c7c..b43d384 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtService.kt +++ b/authentication/src/main/kotlin/com/coded/spring/authentication/jwt/JwtService.kt @@ -1,6 +1,7 @@ -package com.coded.spring.ordering.authentication.jwt +package com.coded.spring.authentication.jwt -import io.jsonwebtoken.* +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.security.Keys import org.springframework.stereotype.Component import java.util.* diff --git a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileController.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/profiles/ProfileController.kt similarity index 93% rename from src/main/kotlin/com/coded/spring/ordering/profiles/ProfileController.kt rename to authentication/src/main/kotlin/com/coded/spring/authentication/profiles/ProfileController.kt index 9363df0..31c1404 100644 --- a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileController.kt +++ b/authentication/src/main/kotlin/com/coded/spring/authentication/profiles/ProfileController.kt @@ -1,4 +1,4 @@ -package com.coded.spring.ordering.profiles +package com.coded.spring.authentication.profiles import io.swagger.v3.oas.annotations.tags.Tag diff --git a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileRepository.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/profiles/ProfileRepository.kt similarity index 77% rename from src/main/kotlin/com/coded/spring/ordering/profiles/ProfileRepository.kt rename to authentication/src/main/kotlin/com/coded/spring/authentication/profiles/ProfileRepository.kt index 646bce0..5b9c2b2 100644 --- a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileRepository.kt +++ b/authentication/src/main/kotlin/com/coded/spring/authentication/profiles/ProfileRepository.kt @@ -1,14 +1,14 @@ -package com.coded.spring.ordering.profiles +package com.coded.spring.authentication.profiles -import com.coded.spring.ordering.users.UserEntity import jakarta.inject.Named import jakarta.persistence.* import org.springframework.data.jpa.repository.JpaRepository +import com.coded.spring.authentication.users.UserEntity @Named interface ProfileRepository : JpaRepository { - fun findByUserId(userId: UserEntity): ProfileEntity? + fun findByUserId(userId: Long): ProfileEntity? } @Entity @@ -20,7 +20,7 @@ data class ProfileEntity( @OneToOne @JoinColumn(name = "user_id") - val userId: UserEntity, + val user: UserEntity, @Column(name = "first_name") val firstName: String, diff --git a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileService.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/profiles/ProfileService.kt similarity index 91% rename from src/main/kotlin/com/coded/spring/ordering/profiles/ProfileService.kt rename to authentication/src/main/kotlin/com/coded/spring/authentication/profiles/ProfileService.kt index 3b44802..92bb99a 100644 --- a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileService.kt +++ b/authentication/src/main/kotlin/com/coded/spring/authentication/profiles/ProfileService.kt @@ -1,7 +1,6 @@ -package com.coded.spring.ordering.profiles +package com.coded.spring.authentication.profiles - -import com.coded.spring.ordering.users.UserRepository +import com.coded.spring.authentication.users.UserRepository import org.springframework.http.ResponseEntity import org.springframework.stereotype.Service @@ -26,7 +25,7 @@ class ProfileService( return ResponseEntity.badRequest().body(mapOf("error" to "phone number must be 8 digits")) } - val existingProfile = profileRepository.findByUserId(user) + val existingProfile = profileRepository.findByUserId(user.id!!) val profile = if (existingProfile != null) { existingProfile.copy( @@ -36,7 +35,7 @@ class ProfileService( ) } else { ProfileEntity( - userId = user, + user = user, firstName = request.firstName, lastName = request.lastName, phoneNumber = request.phoneNumber diff --git a/src/main/kotlin/com/coded/spring/ordering/scripts/InitUserRunner.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/scripts/InitUserRunner.kt similarity index 72% rename from src/main/kotlin/com/coded/spring/ordering/scripts/InitUserRunner.kt rename to authentication/src/main/kotlin/com/coded/spring/authentication/scripts/InitUserRunner.kt index 591119f..a1d7d64 100644 --- a/src/main/kotlin/com/coded/spring/ordering/scripts/InitUserRunner.kt +++ b/authentication/src/main/kotlin/com/coded/spring/authentication/scripts/InitUserRunner.kt @@ -1,13 +1,13 @@ -package com.coded.spring.ordering.scripts +package com.coded.spring.authentication.scripts -import com.coded.spring.ordering.Application -import com.coded.spring.ordering.users.UserEntity -import com.coded.spring.ordering.users.UserRepository +import com.coded.spring.authentication.AuthenticationApplication +import com.coded.spring.authentication.users.UserEntity +import com.coded.spring.authentication.users.UserRepository import org.springframework.boot.CommandLineRunner import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication -import com.coded.spring.ordering.users.Roles +import com.coded.spring.authentication.users.Roles import org.springframework.context.annotation.Bean import org.springframework.security.crypto.password.PasswordEncoder @@ -30,5 +30,5 @@ class InitUserRunner { } fun main(args: Array) { - runApplication(*args).close() + runApplication(*args).close() } \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UserController.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/users/UserController.kt similarity index 73% rename from src/main/kotlin/com/coded/spring/ordering/users/UserController.kt rename to authentication/src/main/kotlin/com/coded/spring/authentication/users/UserController.kt index 24f9f10..98ebe8c 100644 --- a/src/main/kotlin/com/coded/spring/ordering/users/UserController.kt +++ b/authentication/src/main/kotlin/com/coded/spring/authentication/users/UserController.kt @@ -1,11 +1,12 @@ -package com.coded.spring.ordering.users +package com.coded.spring.authentication.users import io.swagger.v3.oas.annotations.tags.Tag -import jakarta.validation.Valid import jakarta.validation.constraints.NotBlank import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.* +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController @Tag(name="UserAPI") diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UserRepository.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/users/UserRepository.kt similarity index 93% rename from src/main/kotlin/com/coded/spring/ordering/users/UserRepository.kt rename to authentication/src/main/kotlin/com/coded/spring/authentication/users/UserRepository.kt index 2eafb17..a3e2852 100644 --- a/src/main/kotlin/com/coded/spring/ordering/users/UserRepository.kt +++ b/authentication/src/main/kotlin/com/coded/spring/authentication/users/UserRepository.kt @@ -1,8 +1,8 @@ -package com.coded.spring.ordering.users +package com.coded.spring.authentication.users +import jakarta.inject.Named import jakarta.persistence.* import org.springframework.data.jpa.repository.JpaRepository -import jakarta.inject.Named @Named interface UserRepository : JpaRepository{ diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UserService.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/users/UserService.kt similarity index 61% rename from src/main/kotlin/com/coded/spring/ordering/users/UserService.kt rename to authentication/src/main/kotlin/com/coded/spring/authentication/users/UserService.kt index 8850f07..a2369e2 100644 --- a/src/main/kotlin/com/coded/spring/ordering/users/UserService.kt +++ b/authentication/src/main/kotlin/com/coded/spring/authentication/users/UserService.kt @@ -1,4 +1,4 @@ -package com.coded.spring.ordering.users +package com.coded.spring.authentication.users import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity @@ -10,26 +10,33 @@ import org.springframework.stereotype.Service private val userRepository: UserRepository, private val passwordEncoder: PasswordEncoder ) { + + + fun findByUsername (username: String): Long + = userRepository.findByUsername(username)?.id ?: throw IllegalStateException("User has no id...") + fun registerUser(request: CreateUserRequest): ResponseEntity { if (userRepository.existsByUsername(request.username)) { return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(mapOf("error" to "username ${request.username} already exists")) } - if(request.password.length < 6) { + if (request.password.length < 6) { return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(mapOf("error" to "password must be at least 6 characters")) } - if(!request.password.any { it.isUpperCase() }) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(mapOf("error" to "password must have at least one capital letter")) + if (!request.password.any { it.isUpperCase() }) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(mapOf("error" to "password must have at least one capital letter")) } - if(!request.password.any { it.isDigit() }) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(mapOf("error" to "password must have at least one digit")) + if (!request.password.any { it.isDigit() }) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(mapOf("error" to "password must have at least one digit")) } - val hashedPassword = passwordEncoder.encode(request.password) + val hashedPassword = passwordEncoder.encode(request.password) val newUser = UserEntity(username = request.username, password = hashedPassword) userRepository.save(newUser) diff --git a/authentication/src/main/resources/application.properties b/authentication/src/main/resources/application.properties new file mode 100644 index 0000000..179eb66 --- /dev/null +++ b/authentication/src/main/resources/application.properties @@ -0,0 +1,8 @@ +spring.application.name=Kotlin.SpringbootV2 +server.port=9002 +spring.datasource.url=jdbc:postgresql://localhost:5432/OnlineOrderingDatabase +spring.datasource.username=postgres +spring.datasource.password=123 +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect + +springdoc.api-docs.path=/api-docs \ No newline at end of file diff --git a/ordering/pom.xml b/ordering/pom.xml new file mode 100644 index 0000000..ba0ba2d --- /dev/null +++ b/ordering/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + com.coded.spring + Kotlin.SpringbootV2 + 0.0.1-SNAPSHOT + + + ordering + + + UTF-8 + official + 1.8 + + + + com.coded.spring + authentication + 0.0.1-SNAPSHOT + compile + + + \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt b/ordering/src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt similarity index 100% rename from src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt rename to ordering/src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt diff --git a/src/main/kotlin/com/coded/spring/ordering/LoggingFilter.kt b/ordering/src/main/kotlin/com/coded/spring/ordering/LoggingFilter.kt similarity index 100% rename from src/main/kotlin/com/coded/spring/ordering/LoggingFilter.kt rename to ordering/src/main/kotlin/com/coded/spring/ordering/LoggingFilter.kt diff --git a/src/main/kotlin/com/coded/spring/ordering/Mohammed-Sheshtar-online-ordering-api-swagger-02.json b/ordering/src/main/kotlin/com/coded/spring/ordering/Mohammed-Sheshtar-online-ordering-api-swagger-02.json similarity index 100% rename from src/main/kotlin/com/coded/spring/ordering/Mohammed-Sheshtar-online-ordering-api-swagger-02.json rename to ordering/src/main/kotlin/com/coded/spring/ordering/Mohammed-Sheshtar-online-ordering-api-swagger-02.json diff --git a/src/main/kotlin/com/coded/spring/ordering/Application.kt b/ordering/src/main/kotlin/com/coded/spring/ordering/OrderingApplication.kt similarity index 87% rename from src/main/kotlin/com/coded/spring/ordering/Application.kt rename to ordering/src/main/kotlin/com/coded/spring/ordering/OrderingApplication.kt index 3756b90..ad3e6f1 100644 --- a/src/main/kotlin/com/coded/spring/ordering/Application.kt +++ b/ordering/src/main/kotlin/com/coded/spring/ordering/OrderingApplication.kt @@ -7,10 +7,10 @@ import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @SpringBootApplication -class Application +class OrderingApplication fun main(args: Array) { - runApplication(*args) + runApplication(*args) orderConfig.getMapConfig("menus").setTimeToLiveSeconds(60) } diff --git a/src/main/kotlin/com/coded/spring/ordering/WelcomeController.kt b/ordering/src/main/kotlin/com/coded/spring/ordering/WelcomeController.kt similarity index 99% rename from src/main/kotlin/com/coded/spring/ordering/WelcomeController.kt rename to ordering/src/main/kotlin/com/coded/spring/ordering/WelcomeController.kt index 559cb25..0676637 100644 --- a/src/main/kotlin/com/coded/spring/ordering/WelcomeController.kt +++ b/ordering/src/main/kotlin/com/coded/spring/ordering/WelcomeController.kt @@ -4,7 +4,6 @@ import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.beans.factory.annotation.Value import org.springframework.web.bind.annotation.* - @RestController @Tag(name = "WelcomeAPI") class WelcomeController( @@ -19,5 +18,4 @@ class WelcomeController( } else { "Eidkom Mubarak!" } - } \ No newline at end of file diff --git a/ordering/src/main/kotlin/com/coded/spring/ordering/client/AuthenticationClient.kt b/ordering/src/main/kotlin/com/coded/spring/ordering/client/AuthenticationClient.kt new file mode 100644 index 0000000..b35bb33 --- /dev/null +++ b/ordering/src/main/kotlin/com/coded/spring/ordering/client/AuthenticationClient.kt @@ -0,0 +1,29 @@ +package com.coded.spring.ordering.client + +import com.coded.spring.authentication.CheckTokenResponse +import jakarta.inject.Named +import org.springframework.core.ParameterizedTypeReference +import org.springframework.http.* +import org.springframework.util.MultiValueMap +import org.springframework.web.client.RestTemplate +import org.springframework.web.client.exchange + +@Named +class AuthenticationClient { + + fun checkToken(token: String): CheckTokenResponse { + val restTemplate = RestTemplate() + val url = "http://localhost:9002/authentication/check-token" + val response = restTemplate.exchange( + url = url, + method = HttpMethod.POST, + requestEntity = HttpEntity( + MultiValueMap.fromMultiValue(mapOf("Authorization" to listOf("Bearer $token"))) + ), + object : ParameterizedTypeReference() { + } + ) + return response.body ?: throw IllegalStateException("Check token response has no body ...") + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/items/ItemsRepository.kt b/ordering/src/main/kotlin/com/coded/spring/ordering/items/ItemsRepository.kt similarity index 100% rename from src/main/kotlin/com/coded/spring/ordering/items/ItemsRepository.kt rename to ordering/src/main/kotlin/com/coded/spring/ordering/items/ItemsRepository.kt diff --git a/src/main/kotlin/com/coded/spring/ordering/menus/MenuController.kt b/ordering/src/main/kotlin/com/coded/spring/ordering/menus/MenuController.kt similarity index 100% rename from src/main/kotlin/com/coded/spring/ordering/menus/MenuController.kt rename to ordering/src/main/kotlin/com/coded/spring/ordering/menus/MenuController.kt diff --git a/src/main/kotlin/com/coded/spring/ordering/menus/MenuRepository.kt b/ordering/src/main/kotlin/com/coded/spring/ordering/menus/MenuRepository.kt similarity index 93% rename from src/main/kotlin/com/coded/spring/ordering/menus/MenuRepository.kt rename to ordering/src/main/kotlin/com/coded/spring/ordering/menus/MenuRepository.kt index 390a1da..8db7513 100644 --- a/src/main/kotlin/com/coded/spring/ordering/menus/MenuRepository.kt +++ b/ordering/src/main/kotlin/com/coded/spring/ordering/menus/MenuRepository.kt @@ -1,7 +1,6 @@ package com.coded.spring.ordering.menus import com.coded.spring.ordering.items.ItemsEntity -import com.coded.spring.ordering.users.UserEntity import com.fasterxml.jackson.annotation.JsonManagedReference import jakarta.inject.Named import jakarta.persistence.* diff --git a/src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt b/ordering/src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt similarity index 100% rename from src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt rename to ordering/src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt b/ordering/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt similarity index 58% rename from src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt rename to ordering/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt index aa6ff21..dc4b270 100644 --- a/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt +++ b/ordering/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt @@ -1,8 +1,8 @@ package com.coded.spring.ordering.orders -import com.coded.spring.ordering.items.ItemsEntity -import com.coded.spring.ordering.items.ItemsRepository import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.servlet.http.HttpServletRequest +import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* @Tag(name="MenuAPI") @@ -12,11 +12,16 @@ class OnlineOrderingController( ) { @GetMapping("/orders") - fun getOrders() = onlineOrderingService.getOrders() + fun getOrders(request: HttpServletRequest): List { + val userId = request.getAttribute("userId") as Long + return onlineOrderingService.getOrders(userId) + } @PostMapping("/orders/add") - fun addOrders(@RequestBody request: RequestOrder) = - onlineOrderingService.addOrders(request) + fun addOrders(request: HttpServletRequest, @RequestBody body: RequestOrder): ResponseEntity{ + val userId = request.getAttribute("userId") as Long + return onlineOrderingService.addOrders(userId ,body) + } } @@ -35,7 +40,6 @@ data class RequestOrder( data class OrderResponseDTO( val orderId: Long, - val username: String, val restaurant: String, val items: List, val timeOrdered: String? diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingRepository.kt b/ordering/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingRepository.kt similarity index 69% rename from src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingRepository.kt rename to ordering/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingRepository.kt index 4357bed..044c9a2 100644 --- a/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingRepository.kt +++ b/ordering/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingRepository.kt @@ -1,7 +1,6 @@ package com.coded.spring.ordering.orders import com.coded.spring.ordering.items.ItemsEntity -import com.coded.spring.ordering.users.UserEntity import com.fasterxml.jackson.annotation.JsonManagedReference import jakarta.inject.Named import jakarta.persistence.* @@ -10,7 +9,9 @@ import org.hibernate.annotations.CreationTimestamp import java.time.LocalDateTime @Named -interface OrderRepository: JpaRepository +interface OrderRepository: JpaRepository { + fun findByUserId(userId: Long): List +} @Entity @Table(name = "orders") @@ -19,10 +20,7 @@ data class OrderEntity( @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null, - // included the line below because 'user' is a reserved keyword in SQL and threw an error because of it, column escapes it - @ManyToOne - @JoinColumn(name = "user_id") - var user: UserEntity, + var userId: Long? = null, var restaurant: String, @@ -37,5 +35,5 @@ data class OrderEntity( var timeOrdered: LocalDateTime? = null ){ - constructor() : this(null, UserEntity(), "", null, null) + constructor() : this(null, null, "", null, null) } \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingService.kt b/ordering/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingService.kt similarity index 62% rename from src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingService.kt rename to ordering/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingService.kt index 1d1b33a..c9be77c 100644 --- a/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingService.kt +++ b/ordering/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingService.kt @@ -2,7 +2,6 @@ package com.coded.spring.ordering.orders import com.coded.spring.ordering.items.ItemsEntity import com.coded.spring.ordering.items.ItemsRepository -import com.coded.spring.ordering.users.UserRepository import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.stereotype.Service @@ -11,12 +10,26 @@ import org.springframework.stereotype.Service class OnlineOrderingService( private val orderRepository: OrderRepository, private val itemsRepository: ItemsRepository, - private var userRepository: UserRepository ) { - fun getOrders(): List = orderRepository.findAll().filter { it.user != null }.sortedBy { it.timeOrdered } - - fun addOrders(request: RequestOrder): ResponseEntity { - val user = userRepository.findById(request.userId).orElse(null) + fun getOrders(userId: Long): List { + //return orderRepository.findAll().filter { it.userId != null }.sortedBy { it.timeOrdered } + return orderRepository.findByUserId(userId).filter { it.userId != null }.sortedBy { it.timeOrdered }.map { + order -> OrderResponseDTO( + orderId = order.id ?: + throw IllegalStateException("Order has no id..."), + restaurant = order.restaurant, + items = order.items!!.map { + RequestItem( + name = it.name, + price = it.price + ) + }, + timeOrdered = order.timeOrdered.toString() + ) + } + } + fun addOrders(userId: Long, request: RequestOrder): ResponseEntity { + val user = orderRepository.findById(request.userId).orElse(null) ?: return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(mapOf("error" to "user with ID ${request.userId} was not found")) if (request.items.any { it.price < 0.0 }) { @@ -26,7 +39,7 @@ class OnlineOrderingService( val order = orderRepository.save( OrderEntity( - user = user, + userId = userId, restaurant = request.restaurant ) ) @@ -44,7 +57,6 @@ class OnlineOrderingService( return ResponseEntity.status(HttpStatus.OK).body( OrderResponseDTO( orderId = order.id!!, - username = user.username, restaurant = order.restaurant, timeOrdered = order.timeOrdered.toString(), items = items.map { @@ -54,3 +66,4 @@ class OnlineOrderingService( ) } } + diff --git a/ordering/src/main/kotlin/com/coded/spring/ordering/security/RemoteAuthenticationFilter.kt b/ordering/src/main/kotlin/com/coded/spring/ordering/security/RemoteAuthenticationFilter.kt new file mode 100644 index 0000000..91b6973 --- /dev/null +++ b/ordering/src/main/kotlin/com/coded/spring/ordering/security/RemoteAuthenticationFilter.kt @@ -0,0 +1,32 @@ +package com.coded.spring.ordering.security + +import com.coded.spring.ordering.client.AuthenticationClient +import jakarta.servlet.FilterChain +import jakarta.servlet.http.* +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter + +@Component +class RemoteAuthenticationFilter( + private val authenticationClient: AuthenticationClient, +) : OncePerRequestFilter() { + + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + logger.info("Remote authentication filter running...") + val authHeader = request.getHeader("Authorization") + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response) + return + } + + val token = authHeader.substring(7) + val result = authenticationClient.checkToken(token) + request.setAttribute("userId", result.userId) + + filterChain.doFilter(request, response) + } +} \ No newline at end of file diff --git a/ordering/src/main/kotlin/com/coded/spring/ordering/security/SecurityConfig.kt b/ordering/src/main/kotlin/com/coded/spring/ordering/security/SecurityConfig.kt new file mode 100644 index 0000000..683e7be --- /dev/null +++ b/ordering/src/main/kotlin/com/coded/spring/ordering/security/SecurityConfig.kt @@ -0,0 +1,30 @@ +package com.coded.spring.ordering.security + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter + +@Configuration +@EnableWebSecurity +class SecurityConfig( + private val remoteAuthFilter: RemoteAuthenticationFilter +) { + + @Bean + fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http.csrf { it.disable() } + .authorizeHttpRequests { + it.anyRequest().permitAll() + } + .sessionManagement { + it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + } + .addFilterBefore(remoteAuthFilter, UsernamePasswordAuthenticationFilter::class.java) + + return http.build() + } +} \ No newline at end of file diff --git a/ordering/src/main/resources/application.properties b/ordering/src/main/resources/application.properties new file mode 100644 index 0000000..5016f3a --- /dev/null +++ b/ordering/src/main/resources/application.properties @@ -0,0 +1,8 @@ +spring.application.name=Kotlin.SpringbootV2 +server.port=9001 +spring.datasource.url=jdbc:postgresql://localhost:5432/OnlineOrderingDatabase +spring.datasource.username=postgres +spring.datasource.password=123 +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect + +springdoc.api-docs.path=/api-docs \ No newline at end of file diff --git a/pom.xml b/pom.xml index 3beb3c4..152bc3c 100644 --- a/pom.xml +++ b/pom.xml @@ -9,8 +9,9 @@ com.coded.spring - Ordering + Kotlin.SpringbootV2 0.0.1-SNAPSHOT + pom Kotlin.SpringbootV2 Kotlin.SpringbootV2 @@ -20,6 +21,10 @@ + + authentication + ordering + @@ -122,7 +127,7 @@ com.h2database h2 - test + test org.junit.jupiter diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 5016f3a..fa5d06a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,5 @@ spring.application.name=Kotlin.SpringbootV2 -server.port=9001 +server.port=9003 spring.datasource.url=jdbc:postgresql://localhost:5432/OnlineOrderingDatabase spring.datasource.username=postgres spring.datasource.password=123 diff --git a/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt b/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt index a111b18..a3e3079 100644 --- a/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt +++ b/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt @@ -8,7 +8,7 @@ import com.coded.spring.ordering.orders.RequestOrder import com.coded.spring.ordering.profiles.RequestProfileDTO import com.coded.spring.ordering.users.CreateUserRequest import com.coded.spring.ordering.users.UserEntity -import com.coded.spring.ordering.users.UserRepository +import com.coded.spring.authentication.users.UserRepository import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired diff --git a/src/test/kotlin/com/coded/spring/ordering/CucumberRunner.kt b/src/test/kotlin/com/coded/spring/ordering/CucumberRunner.kt index 2da636f..a2297bc 100644 --- a/src/test/kotlin/com/coded/spring/ordering/CucumberRunner.kt +++ b/src/test/kotlin/com/coded/spring/ordering/CucumberRunner.kt @@ -2,7 +2,11 @@ package com.coded.spring.ordering import io.cucumber.spring.CucumberContextConfiguration import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ActiveProfiles @CucumberContextConfiguration -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = ["src/test/resources/application-test.properties"] +) +@ActiveProfiles("test") class CucumberSpringConfiguration diff --git a/src/test/kotlin/com/coded/spring/ordering/steps/AuthenticationSteps.kt b/src/test/kotlin/com/coded/spring/ordering/steps/AuthenticationSteps.kt index 9bd7d47..9e4a9b0 100644 --- a/src/test/kotlin/com/coded/spring/ordering/steps/AuthenticationSteps.kt +++ b/src/test/kotlin/com/coded/spring/ordering/steps/AuthenticationSteps.kt @@ -1,2 +1,47 @@ package com.coded.spring.ordering.steps +import io.cucumber.java.en.Given +import io.cucumber.java.en.When +import io.cucumber.java.en.Then +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.http.ResponseEntity +import kotlin.test.assertEquals + +class AuthSteps { + + @Autowired + lateinit var restTemplate: TestRestTemplate + + @Autowired + lateinit var jwtService: com.coded.spring.ordering.authentication.jwt.JwtService + + private lateinit var headers: HttpHeaders + private lateinit var response: ResponseEntity + + @Given("a valid JWT token for user {string}") + fun givenAValidJwtToken(username: String) { + val token = jwtService.generateToken(username) + headers = HttpHeaders() + headers.set("Authorization", "Bearer $token") + } + + @When("I send a GET request to {string} with the token") + fun iSendAGetRequestWithToken(endpoint: String) { + val request = HttpEntity(headers) + response = restTemplate.exchange(endpoint, HttpMethod.GET, request, String::class.java) + } + + @Then("the response status should be {int}") + fun theResponseStatusShouldBe(expectedStatus: Int) { + assertEquals(expectedStatus, response.statusCode.value()) + } + + @Then("the response body should be {string}") + fun theResponseBodyShouldBe(expectedBody: String) { + assertEquals(expectedBody, response.body) + } +} diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties index 7e87e3b..f1133ab 100644 --- a/src/test/resources/application-test.properties +++ b/src/test/resources/application-test.properties @@ -9,5 +9,3 @@ spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=true spring.h2.console.enabled=true spring.h2.console.path=/h2-console - - diff --git a/src/test/resources/features/authentication.feature b/src/test/resources/features/authentication.feature index e69de29..4b74601 100644 --- a/src/test/resources/features/authentication.feature +++ b/src/test/resources/features/authentication.feature @@ -0,0 +1,7 @@ +Feature: Authentication + + Scenario: Access hello endpoint with a valid JWT token + Given a valid JWT token for user "momo1234" + When I send a GET request to "/hello" with the token + Then the response status should be 200 + And the response body should be "Hello World" From 6e8a04dfacafa1c691f2c0d09d120eb6c69b06b4 Mon Sep 17 00:00:00 2001 From: mohammedsheshtar <112146380+mohammedsheshtar@users.noreply.github.com> Date: Sat, 3 May 2025 16:46:18 +0300 Subject: [PATCH 18/18] minor changes and included the correct Swagger JSON files for each microservice --- .../com/coded/spring/authentication/AuthenticationController.kt | 2 +- .../Mohammed-Sheshtar-Authentication-api-swagger-01.json | 1 + .../Mohammed-Sheshtar-online-ordering-api-swagger-02.json | 1 - .../Mohammed-Sheshtar-online-ordering-api-swagger-03.json | 1 + .../coded/spring/ordering/orders/OnlineOrderingController.kt | 2 +- 5 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 authentication/src/main/kotlin/com/coded/spring/authentication/Mohammed-Sheshtar-Authentication-api-swagger-01.json delete mode 100644 ordering/src/main/kotlin/com/coded/spring/ordering/Mohammed-Sheshtar-online-ordering-api-swagger-02.json create mode 100644 ordering/src/main/kotlin/com/coded/spring/ordering/Mohammed-Sheshtar-online-ordering-api-swagger-03.json diff --git a/authentication/src/main/kotlin/com/coded/spring/authentication/AuthenticationController.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/AuthenticationController.kt index a121c5a..50a4c5b 100644 --- a/authentication/src/main/kotlin/com/coded/spring/authentication/AuthenticationController.kt +++ b/authentication/src/main/kotlin/com/coded/spring/authentication/AuthenticationController.kt @@ -23,7 +23,7 @@ class AuthenticationController( private val jwtService: JwtService, private val userService: UserService ) { - + @Tag(name = "AuthenticationAPI") @PostMapping("/login") fun login(@RequestBody authRequest: AuthenticationRequest): AuthenticationResponse { val authToken = UsernamePasswordAuthenticationToken(authRequest.username, authRequest.password) diff --git a/authentication/src/main/kotlin/com/coded/spring/authentication/Mohammed-Sheshtar-Authentication-api-swagger-01.json b/authentication/src/main/kotlin/com/coded/spring/authentication/Mohammed-Sheshtar-Authentication-api-swagger-01.json new file mode 100644 index 0000000..942c33a --- /dev/null +++ b/authentication/src/main/kotlin/com/coded/spring/authentication/Mohammed-Sheshtar-Authentication-api-swagger-01.json @@ -0,0 +1 @@ +{"openapi":"3.0.1","info":{"title":"OpenAPI definition","version":"v0"},"servers":[{"url":"http://localhost:9002","description":"Generated server url"}],"paths":{"/register":{"post":{"tags":["UserAPI"],"operationId":"registerUser","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/profile":{"post":{"tags":["ProfileAPI"],"operationId":"addProfile","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestProfileDTO"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/authentication/login":{"post":{"tags":["AuthenticationAPI"],"operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthenticationRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthenticationResponse"}}}}}}},"/authentication/check-token":{"post":{"tags":["AuthenticationAPI"],"operationId":"checkToken","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CheckTokenResponse"}}}}}}}},"components":{"schemas":{"CreateUserRequest":{"required":["password","username"],"type":"object","properties":{"username":{"type":"string"},"password":{"type":"string"}}},"RequestProfileDTO":{"required":["firstName","lastName","phoneNumber"],"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"phoneNumber":{"type":"string"}}},"AuthenticationRequest":{"required":["password","username"],"type":"object","properties":{"username":{"type":"string"},"password":{"type":"string"}}},"AuthenticationResponse":{"required":["token"],"type":"object","properties":{"token":{"type":"string"}}},"CheckTokenResponse":{"required":["userId"],"type":"object","properties":{"userId":{"type":"integer","format":"int64"}}}}}} \ No newline at end of file diff --git a/ordering/src/main/kotlin/com/coded/spring/ordering/Mohammed-Sheshtar-online-ordering-api-swagger-02.json b/ordering/src/main/kotlin/com/coded/spring/ordering/Mohammed-Sheshtar-online-ordering-api-swagger-02.json deleted file mode 100644 index 327d20c..0000000 --- a/ordering/src/main/kotlin/com/coded/spring/ordering/Mohammed-Sheshtar-online-ordering-api-swagger-02.json +++ /dev/null @@ -1 +0,0 @@ -{"openapi":"3.0.1","info":{"title":"OpenAPI definition","version":"v0"},"servers":[{"url":"http://localhost:9001","description":"Generated server url"}],"paths":{"/register":{"post":{"tags":["UserAPI"],"operationId":"registerUser","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/profile":{"post":{"tags":["ProfileAPI"],"operationId":"addProfile","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestProfileDTO"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/orders/add":{"post":{"tags":["MenuAPI"],"operationId":"addOrders","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestOrder"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/menus":{"get":{"tags":["MenuAPI"],"operationId":"listMenu","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MenuEntity"}}}}}}},"post":{"tags":["MenuAPI"],"operationId":"createMenu","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MenuDTO"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MenuDTO"}}}}}}},"/authentication/login":{"post":{"tags":["AuthenticationAPI"],"operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthenticationRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthenticationResponse"}}}}}}},"/welcome":{"get":{"tags":["WelcomeAPI"],"operationId":"greetUser","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"string"}}}}}}},"/orders":{"get":{"tags":["MenuAPI"],"operationId":"getOrders","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OrderEntity"}}}}}}}},"/hello":{"get":{"tags":["HelloWorldAPI"],"operationId":"helloWorld","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"string"}}}}}}}},"components":{"schemas":{"CreateUserRequest":{"required":["password","username"],"type":"object","properties":{"username":{"type":"string"},"password":{"type":"string"}}},"RequestProfileDTO":{"required":["firstName","lastName","phoneNumber"],"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"phoneNumber":{"type":"string"}}},"RequestItem":{"required":["name","price"],"type":"object","properties":{"name":{"type":"string"},"price":{"type":"number","format":"double"}}},"RequestOrder":{"required":["items","restaurant","userId"],"type":"object","properties":{"userId":{"type":"integer","format":"int64"},"restaurant":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/components/schemas/RequestItem"}}}},"MenuDTO":{"required":["name","price"],"type":"object","properties":{"name":{"type":"string"},"price":{"type":"number"}}},"AuthenticationRequest":{"required":["password","username"],"type":"object","properties":{"username":{"type":"string"},"password":{"type":"string"}}},"AuthenticationResponse":{"required":["token"],"type":"object","properties":{"token":{"type":"string"}}},"ItemsEntity":{"required":["name","order","price"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"order":{"$ref":"#/components/schemas/OrderEntity"},"name":{"type":"string"},"price":{"type":"number","format":"double"}}},"OrderEntity":{"required":["restaurant","user"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"user":{"$ref":"#/components/schemas/UserEntity"},"restaurant":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/components/schemas/ItemsEntity"}},"timeOrdered":{"type":"string","format":"date-time"}}},"UserEntity":{"required":["password","role","username"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"username":{"type":"string"},"password":{"type":"string"},"role":{"type":"string","enum":["USER","ADMIN"]}}},"MenuEntity":{"required":["name","price"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"price":{"type":"number"}}}}}} \ No newline at end of file diff --git a/ordering/src/main/kotlin/com/coded/spring/ordering/Mohammed-Sheshtar-online-ordering-api-swagger-03.json b/ordering/src/main/kotlin/com/coded/spring/ordering/Mohammed-Sheshtar-online-ordering-api-swagger-03.json new file mode 100644 index 0000000..172e5ad --- /dev/null +++ b/ordering/src/main/kotlin/com/coded/spring/ordering/Mohammed-Sheshtar-online-ordering-api-swagger-03.json @@ -0,0 +1 @@ +{"openapi":"3.0.1","info":{"title":"OpenAPI definition","version":"v0"},"servers":[{"url":"http://localhost:9001","description":"Generated server url"}],"paths":{"/orders/add":{"post":{"tags":["OrderAPI"],"operationId":"addOrders","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestOrder"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/menus":{"get":{"tags":["MenuAPI"],"operationId":"listMenu","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MenuEntity"}}}}}}},"post":{"tags":["MenuAPI"],"operationId":"createMenu","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MenuDTO"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MenuDTO"}}}}}}},"/welcome":{"get":{"tags":["WelcomeAPI"],"operationId":"greetUser","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"string"}}}}}}},"/orders":{"get":{"tags":["OrderAPI"],"operationId":"getOrders","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OrderResponseDTO"}}}}}}}},"/hello":{"get":{"tags":["HelloWorldAPI"],"operationId":"helloWorld","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"string"}}}}}}}},"components":{"schemas":{"RequestItem":{"required":["name","price"],"type":"object","properties":{"name":{"type":"string"},"price":{"type":"number","format":"double"}}},"RequestOrder":{"required":["items","restaurant","userId"],"type":"object","properties":{"userId":{"type":"integer","format":"int64"},"restaurant":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/components/schemas/RequestItem"}}}},"MenuDTO":{"required":["name","price"],"type":"object","properties":{"name":{"type":"string"},"price":{"type":"number"}}},"OrderResponseDTO":{"required":["items","orderId","restaurant"],"type":"object","properties":{"orderId":{"type":"integer","format":"int64"},"restaurant":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/components/schemas/RequestItem"}},"timeOrdered":{"type":"string"}}},"MenuEntity":{"required":["name","price"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"price":{"type":"number"}}}}}} \ No newline at end of file diff --git a/ordering/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt b/ordering/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt index dc4b270..7189fca 100644 --- a/ordering/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt +++ b/ordering/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt @@ -5,7 +5,7 @@ import jakarta.servlet.http.HttpServletRequest import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* -@Tag(name="MenuAPI") +@Tag(name="OrderAPI") @RestController class OnlineOrderingController( private val onlineOrderingService: OnlineOrderingService