From 4f3282fd7c809d29a73ad0d1eda358e3b2997cc2 Mon Sep 17 00:00:00 2001 From: Yousef Alothman Date: Tue, 8 Apr 2025 11:40:56 +0300 Subject: [PATCH 01/18] Completed "Welcome to Online Ordering" --- pom.xml | 20 ++++++++++++++ .../coded/spring/ordering/OrderController.kt | 26 +++++++++++++++++++ src/main/resources/application.properties | 1 + src/main/resources/static/index.html | 10 +++++++ 4 files changed, 57 insertions(+) create mode 100644 src/main/kotlin/com/coded/spring/ordering/OrderController.kt create mode 100644 src/main/resources/static/index.html diff --git a/pom.xml b/pom.xml index 163ad53..ee61457 100644 --- a/pom.xml +++ b/pom.xml @@ -58,6 +58,26 @@ kotlin-test-junit5 test + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + runtime + + + + jakarta.inject + jakarta.inject-api + 2.0.1 + + + + diff --git a/src/main/kotlin/com/coded/spring/ordering/OrderController.kt b/src/main/kotlin/com/coded/spring/ordering/OrderController.kt new file mode 100644 index 0000000..63b3c67 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/OrderController.kt @@ -0,0 +1,26 @@ +package com.coded.spring.ordering + +import org.springframework.web.bind.annotation.* + + +@RestController +@RequestMapping("/") + +class HelloWorldController( +) { + + @GetMapping("home") + fun homepage() = "This is the home page for SpeedDash" + + @GetMapping("category") + fun category() = "This is the category page for SpeedDash" + + @GetMapping("tracker") + fun tracker() = "This is the tracker page for SpeedDash" + + @GetMapping("profile") + fun profile() = "This is the profile page for SpeedDash" + + @GetMapping("settings") + fun settings() = "This is the settings page for SpeedDash" +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3704dc6..134d351 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,2 @@ spring.application.name=Kotlin.SpringbootV2 +server.port = 8080 \ No newline at end of file diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html new file mode 100644 index 0000000..08232a8 --- /dev/null +++ b/src/main/resources/static/index.html @@ -0,0 +1,10 @@ + + + + + Welcome to SpeedDash! + + + + + \ No newline at end of file From 60e8fe040fc969acb2ac5298fb523ac066e14067 Mon Sep 17 00:00:00 2001 From: Yousef Alothman Date: Tue, 8 Apr 2025 11:50:15 +0300 Subject: [PATCH 02/18] Completed "Online Ordering - Post an Order" --- .../coded/spring/ordering/OrderController.kt | 18 +++++++++++++- .../coded/spring/ordering/OrdersRepository.kt | 24 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/com/coded/spring/ordering/OrdersRepository.kt diff --git a/src/main/kotlin/com/coded/spring/ordering/OrderController.kt b/src/main/kotlin/com/coded/spring/ordering/OrderController.kt index 63b3c67..2ed3f36 100644 --- a/src/main/kotlin/com/coded/spring/ordering/OrderController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/OrderController.kt @@ -7,7 +7,8 @@ import org.springframework.web.bind.annotation.* @RequestMapping("/") class HelloWorldController( -) { + val ordersRepository: OrdersRepository +){ @GetMapping("home") fun homepage() = "This is the home page for SpeedDash" @@ -23,4 +24,19 @@ class HelloWorldController( @GetMapping("settings") fun settings() = "This is the settings page for SpeedDash" + + @PostMapping("orders") + fun submitOrder(@RequestBody request: OrderRequest): Order{ + + val order = Order(null, request.user, request.resturant, request.items.toMutableList()) + return ordersRepository.save(order) + } + @GetMapping("allorders") + fun listOrders(): List = ordersRepository.findAll() } + +data class OrderRequest( + val user: String, + val resturant: String, + val items: List +) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/OrdersRepository.kt b/src/main/kotlin/com/coded/spring/ordering/OrdersRepository.kt new file mode 100644 index 0000000..62fedc4 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/OrdersRepository.kt @@ -0,0 +1,24 @@ +package com.coded.spring.ordering + +import jakarta.inject.Named +import jakarta.persistence.* +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Named +interface OrdersRepository: JpaRepository + +@Entity +@Table(name = "orders") + class Order( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + var username: String, + var restaurant: String, + @ElementCollection + var items: MutableList = mutableListOf() + +){ + constructor(): this(null, "","", mutableListOf()) +} \ No newline at end of file From bbf16e53dcb28f95a514f10992dce4d34a1eb0a8 Mon Sep 17 00:00:00 2001 From: Yousef Alothman Date: Wed, 9 Apr 2025 16:06:37 +0300 Subject: [PATCH 03/18] Completed "Create Online Ordering DB" --- pom.xml | 5 +++++ .../ordering/{ => orders}/OrderController.kt | 12 +++++----- .../ordering/{ => orders}/OrdersRepository.kt | 15 +++++++------ .../spring/ordering/orders/OrdersService.kt | 17 ++++++++++++++ .../spring/ordering/users/UserRepository.kt | 22 +++++++++++++++++++ .../spring/ordering/users/UsersController.kt | 13 +++++++++++ .../spring/ordering/users/UsersService.kt | 20 +++++++++++++++++ src/main/resources/application.properties | 6 +++-- 8 files changed, 95 insertions(+), 15 deletions(-) rename src/main/kotlin/com/coded/spring/ordering/{ => orders}/OrderController.kt (71%) rename src/main/kotlin/com/coded/spring/ordering/{ => orders}/OrdersRepository.kt (52%) create mode 100644 src/main/kotlin/com/coded/spring/ordering/orders/OrdersService.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/UsersController.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/users/UsersService.kt diff --git a/pom.xml b/pom.xml index ee61457..d30f9cb 100644 --- a/pom.xml +++ b/pom.xml @@ -76,6 +76,11 @@ 2.0.1 + + org.postgresql + postgresql + + diff --git a/src/main/kotlin/com/coded/spring/ordering/OrderController.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OrderController.kt similarity index 71% rename from src/main/kotlin/com/coded/spring/ordering/OrderController.kt rename to src/main/kotlin/com/coded/spring/ordering/orders/OrderController.kt index 2ed3f36..334351b 100644 --- a/src/main/kotlin/com/coded/spring/ordering/OrderController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OrderController.kt @@ -1,4 +1,4 @@ -package com.coded.spring.ordering +package com.coded.spring.ordering.orders import org.springframework.web.bind.annotation.* @@ -6,7 +6,7 @@ import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/") -class HelloWorldController( +class OrderController( val ordersRepository: OrdersRepository ){ @@ -26,17 +26,17 @@ class HelloWorldController( fun settings() = "This is the settings page for SpeedDash" @PostMapping("orders") - fun submitOrder(@RequestBody request: OrderRequest): Order{ + fun submitOrder(@RequestBody request: OrderRequest): OrderEntity { - val order = Order(null, request.user, request.resturant, request.items.toMutableList()) + val order = OrderEntity(null, request.user, request.resturant, request.items) return ordersRepository.save(order) } @GetMapping("allorders") - fun listOrders(): List = ordersRepository.findAll() + fun listOrders(): List = ordersRepository.findAll() } data class OrderRequest( val user: String, val resturant: String, - val items: List + val items: String ) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/OrdersRepository.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OrdersRepository.kt similarity index 52% rename from src/main/kotlin/com/coded/spring/ordering/OrdersRepository.kt rename to src/main/kotlin/com/coded/spring/ordering/orders/OrdersRepository.kt index 62fedc4..d57eadd 100644 --- a/src/main/kotlin/com/coded/spring/ordering/OrdersRepository.kt +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OrdersRepository.kt @@ -1,24 +1,25 @@ -package com.coded.spring.ordering +package com.coded.spring.ordering.orders import jakarta.inject.Named import jakarta.persistence.* import org.springframework.data.jpa.repository.JpaRepository -import org.springframework.stereotype.Repository @Named -interface OrdersRepository: JpaRepository +interface OrdersRepository: JpaRepository @Entity @Table(name = "orders") - class Order( + class OrderEntity( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null, var username: String, var restaurant: String, - @ElementCollection - var items: MutableList = mutableListOf() + //@ElementCollection + //var items: MutableList = mutableListOf() + var items: String + ){ - constructor(): this(null, "","", mutableListOf()) + constructor(): this(null, "","", "") } \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OrdersService.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OrdersService.kt new file mode 100644 index 0000000..0102c4c --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OrdersService.kt @@ -0,0 +1,17 @@ +package com.coded.spring.ordering.orders +import jakarta.inject.Named + +@Named +class OrdersService( + private val ordersRepository: OrdersRepository, +) { + + fun listOrders(): List = ordersRepository.findAll().map { + OrderEntity( + username = it.username, + restaurant = it.restaurant, + items = it.items + + ) + } +} \ No newline at end of file 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..7335edb --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/users/UserRepository.kt @@ -0,0 +1,22 @@ +package com.coded.spring.ordering.users + +import jakarta.inject.Named +import jakarta.persistence.* +import org.springframework.data.jpa.repository.JpaRepository + +@Named +interface UsersRepository : JpaRepository { + fun age(age: Int): MutableList +} + +@Entity +@Table(name = "users") +data class UserEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + var name: String, + var age: Int +){ + constructor() : this(null, "", 0) +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UsersController.kt b/src/main/kotlin/com/coded/spring/ordering/users/UsersController.kt new file mode 100644 index 0000000..25ecf8f --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/users/UsersController.kt @@ -0,0 +1,13 @@ +package com.coded.spring.ordering.users + +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +class UsersController( + private val usersService: UsersService +){ + + @GetMapping("/users/v1/list") + fun users() = usersService.listUsers() +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UsersService.kt b/src/main/kotlin/com/coded/spring/ordering/users/UsersService.kt new file mode 100644 index 0000000..7989431 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/users/UsersService.kt @@ -0,0 +1,20 @@ +package com.coded.spring.ordering.users +import jakarta.inject.Named + +@Named +class UsersService( + private val usersRepository: UsersRepository, +) { + + fun listUsers(): List = usersRepository.findAll().map { + User( + name = it.name, + age = it.age + ) + } +} + +data class User( + val name: String, + val age: Int +) \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 134d351..dd161d4 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,2 +1,4 @@ -spring.application.name=Kotlin.SpringbootV2 -server.port = 8080 \ No newline at end of file +spring.datasource.url=jdbc:postgresql://localhost:5432/myHelloDatabase +spring.datasource.username=postgres +spring.datasource.password=yosaka +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect \ No newline at end of file From aa6caee2b709f88f359882c72d0ff134dc2dd9d6 Mon Sep 17 00:00:00 2001 From: Yousef Alothman Date: Thu, 10 Apr 2025 17:14:00 +0300 Subject: [PATCH 04/18] Completed "Create Online Ordering DB" Challenge --- .../spring/ordering/items/ItemsController.kt | 16 ++++++++++ .../spring/ordering/items/ItemsRepository.kt | 25 +++++++++++++++ .../spring/ordering/items/ItemsService.kt | 27 ++++++++++++++++ .../spring/ordering/orders/OrderController.kt | 31 +------------------ .../ordering/orders/OrdersRepository.kt | 11 ++----- .../spring/ordering/orders/OrdersService.kt | 18 ++++++----- .../spring/ordering/users/UsersController.kt | 1 - src/main/resources/application.properties | 3 ++ 8 files changed, 86 insertions(+), 46 deletions(-) create mode 100644 src/main/kotlin/com/coded/spring/ordering/items/ItemsController.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/items/ItemsRepository.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/items/ItemsService.kt diff --git a/src/main/kotlin/com/coded/spring/ordering/items/ItemsController.kt b/src/main/kotlin/com/coded/spring/ordering/items/ItemsController.kt new file mode 100644 index 0000000..de30089 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/items/ItemsController.kt @@ -0,0 +1,16 @@ +package com.coded.spring.ordering.items + +import org.springframework.web.bind.annotation.* + +@RestController + + +class ItemsController( + + private val itemsService: ItemsService + +) { + @GetMapping("/listItems") + fun listItems(): List = itemsService.listItems() + +} \ 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..805c1ee --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/items/ItemsRepository.kt @@ -0,0 +1,25 @@ +package com.coded.spring.ordering.items + +import jakarta.inject.Named +import jakarta.persistence.* +import org.springframework.data.jpa.repository.JpaRepository + +@Named +interface ItemsRepository : JpaRepository + + + +@Entity +@Table(name = "items") +data class ItemEntity( + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + var order_id: Long?, + var items: String?, + var quantity: Long?, + var note: String?, + var price: Double? + +) {constructor(): this(null,1,"",1,"",1.0)} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/items/ItemsService.kt b/src/main/kotlin/com/coded/spring/ordering/items/ItemsService.kt new file mode 100644 index 0000000..b047965 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/items/ItemsService.kt @@ -0,0 +1,27 @@ +package com.coded.spring.ordering.items +import jakarta.inject.Named + +@Named +class ItemsService( + private val itemsRepository: ItemsRepository, +) { + fun listItems(): List = itemsRepository.findAll().map { entity -> + Item( + id = entity.id, + order_id = entity.order_id, + items = entity.items, + quantity = entity.quantity, + note = entity.note, + price = entity.price + ) + } +} + +data class Item( + val id: Long?, + val order_id: Long?, + val items: String?, + val quantity: Long?, + val note: String?, + val price: Double? +) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OrderController.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OrderController.kt index 334351b..f55f5d1 100644 --- a/src/main/kotlin/com/coded/spring/ordering/orders/OrderController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OrderController.kt @@ -4,39 +4,10 @@ import org.springframework.web.bind.annotation.* @RestController -@RequestMapping("/") class OrderController( val ordersRepository: OrdersRepository ){ - - @GetMapping("home") - fun homepage() = "This is the home page for SpeedDash" - - @GetMapping("category") - fun category() = "This is the category page for SpeedDash" - - @GetMapping("tracker") - fun tracker() = "This is the tracker page for SpeedDash" - - @GetMapping("profile") - fun profile() = "This is the profile page for SpeedDash" - - @GetMapping("settings") - fun settings() = "This is the settings page for SpeedDash" - - @PostMapping("orders") - fun submitOrder(@RequestBody request: OrderRequest): OrderEntity { - - val order = OrderEntity(null, request.user, request.resturant, request.items) - return ordersRepository.save(order) - } - @GetMapping("allorders") + @GetMapping("/listOrders") fun listOrders(): List = ordersRepository.findAll() } - -data class OrderRequest( - val user: String, - val resturant: String, - val items: String -) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OrdersRepository.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OrdersRepository.kt index d57eadd..38e9868 100644 --- a/src/main/kotlin/com/coded/spring/ordering/orders/OrdersRepository.kt +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OrdersRepository.kt @@ -12,14 +12,9 @@ interface OrdersRepository: JpaRepository class OrderEntity( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - var id: Long? = null, - var username: String, - var restaurant: String, - //@ElementCollection - //var items: MutableList = mutableListOf() - var items: String - + var id: Long?, + var user_id: Long?, ){ - constructor(): this(null, "","", "") + constructor(): this(null, null) } \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OrdersService.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OrdersService.kt index 0102c4c..2239d1e 100644 --- a/src/main/kotlin/com/coded/spring/ordering/orders/OrdersService.kt +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OrdersService.kt @@ -5,13 +5,17 @@ import jakarta.inject.Named class OrdersService( private val ordersRepository: OrdersRepository, ) { - - fun listOrders(): List = ordersRepository.findAll().map { - OrderEntity( - username = it.username, - restaurant = it.restaurant, - items = it.items + fun listOrders(): List = ordersRepository.findAll().map { + Order( + id = it.id, + user_id = it.user_id ) + } -} \ No newline at end of file +} + +data class Order( + var id: Long?, + var user_id: Long? +) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UsersController.kt b/src/main/kotlin/com/coded/spring/ordering/users/UsersController.kt index 25ecf8f..a161745 100644 --- a/src/main/kotlin/com/coded/spring/ordering/users/UsersController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/users/UsersController.kt @@ -7,7 +7,6 @@ import org.springframework.web.bind.annotation.RestController class UsersController( private val usersService: UsersService ){ - @GetMapping("/users/v1/list") fun users() = usersService.listUsers() } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index dd161d4..102ce35 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,3 +1,6 @@ +spring.application.name=Kotlin.SpringbootV2 +server.port = 8080 + spring.datasource.url=jdbc:postgresql://localhost:5432/myHelloDatabase spring.datasource.username=postgres spring.datasource.password=yosaka From 9a2f7bb3c0bb1c6c879a6fbdc5f43ce622e8d172 Mon Sep 17 00:00:00 2001 From: Yousef Alothman Date: Thu, 10 Apr 2025 20:12:13 +0300 Subject: [PATCH 05/18] Completed "Create Online Ordering DB" Challenge V2 (Added List of items to listOrder endpoint now you can see the items), also i changed the table in the database to make order_id in items table to a foreign key instead of a normal interger. --- .../spring/ordering/orders/OrderController.kt | 15 ++++++- .../ordering/orders/OrdersRepository.kt | 24 +++++++++--- .../spring/ordering/orders/OrdersService.kt | 39 ++++++++++++++++--- 3 files changed, 65 insertions(+), 13 deletions(-) diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OrderController.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OrderController.kt index f55f5d1..04c0acf 100644 --- a/src/main/kotlin/com/coded/spring/ordering/orders/OrderController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OrderController.kt @@ -6,8 +6,21 @@ import org.springframework.web.bind.annotation.* @RestController class OrderController( - val ordersRepository: OrdersRepository + val ordersRepository: OrdersRepository, + private val ordersService: OrdersService + ){ @GetMapping("/listOrders") fun listOrders(): List = ordersRepository.findAll() + + @PostMapping("/listOrders") + fun createOrder(@RequestBody request: CreateOrderRequest) { + ordersService.createOrder(request.userId) + } + + } + +data class CreateOrderRequest( + val userId: Long +) diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OrdersRepository.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OrdersRepository.kt index 38e9868..b723e12 100644 --- a/src/main/kotlin/com/coded/spring/ordering/orders/OrdersRepository.kt +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OrdersRepository.kt @@ -1,20 +1,32 @@ package com.coded.spring.ordering.orders - -import jakarta.inject.Named +import com.coded.spring.ordering.items.ItemEntity import jakarta.persistence.* import org.springframework.data.jpa.repository.JpaRepository + +import com.coded.spring.ordering.users.UserEntity +import jakarta.inject.Named + + @Named -interface OrdersRepository: JpaRepository +interface OrdersRepository: JpaRepository{ + fun findByUserId(userId: Long): List +} @Entity @Table(name = "orders") class OrderEntity( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - var id: Long?, - var user_id: Long?, + var id: Long? = null, + + @ManyToOne + @JoinColumn(name = "user_id") + var user: UserEntity? = null, + + @OneToMany(mappedBy = "order_id") + val items: List? = null ){ - constructor(): this(null, null) + constructor(): this(null, UserEntity(), listOf()) } \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OrdersService.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OrdersService.kt index 2239d1e..f668484 100644 --- a/src/main/kotlin/com/coded/spring/ordering/orders/OrdersService.kt +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OrdersService.kt @@ -1,21 +1,48 @@ package com.coded.spring.ordering.orders + +import com.coded.spring.ordering.items.Item +import com.coded.spring.ordering.items.ItemEntity +import com.coded.spring.ordering.users.UsersRepository import jakarta.inject.Named + @Named class OrdersService( private val ordersRepository: OrdersRepository, + private val usersRepository: UsersRepository + ) { - fun listOrders(): List = ordersRepository.findAll().map { + fun listOrders(): List = ordersRepository.findAll().map { t -> Order( - id = it.id, - user_id = it.user_id + id = t.id, + user_id = t.user?.id.toString(), + items = t.items?.map { + Item( + id = it.id, + order_id = it.order_id, + quantity = it.quantity, + note = it.note, + price = it.price, + items = it.items + ) + } ) + } + + fun createOrder(userId: Long) { + val user = usersRepository.findById(userId).orElseThrow { + IllegalArgumentException("User with id $userId not found") + } + val newOrder = OrderEntity(user = user) + ordersRepository.save(newOrder) } + } data class Order( - var id: Long?, - var user_id: Long? -) \ No newline at end of file + val id: Long?, + val user_id: String, + var items: List? +) From a0ae6f7119fc46f0e9dbad4b7dedab8b0956f9cf Mon Sep 17 00:00:00 2001 From: Yousef Alothman Date: Thu, 17 Apr 2025 19:46:19 +0300 Subject: [PATCH 06/18] Implement secure order submission system with item linking and user-specific order filtering - Added SubmitOrderRequest DTO and submitOrder() logic - Refactored ItemsController to allow adding new items - Linked ItemEntity to orders via ManyToOne relationship - Enforced user-specific order visibility using Spring Security - Allowed unauthenticated access to /menus only - Ensured item creation works independently from orders - Fixed DB constraints and renamed item field for clarity --- pom.xml | 7 ++ .../com/coded/spring/ordering/DTO/ItemDTO.kt | 18 +++++ .../com/coded/spring/ordering/DTO/OrderDTO.kt | 14 ++++ .../com/coded/spring/ordering/DTO/UserDTO.kt | 8 +++ .../coded/spring/ordering/InitUserRunner.kt | 32 +++++++++ .../CustomerUserDetailsService.kt | 30 ++++++++ .../ordering/authentication/SecurityConfig.kt | 64 +++++++++++++++++ .../spring/ordering/items/ItemsController.kt | 7 ++ .../spring/ordering/items/ItemsRepository.kt | 23 ++++-- .../spring/ordering/items/ItemsService.kt | 28 +++++--- .../coded/spring/ordering/jwt/JwtService.kt | 4 ++ .../spring/ordering/menu/MenuController.kt | 12 ++++ .../spring/ordering/menu/MenuRepository.kt | 20 ++++++ .../coded/spring/ordering/menu/MenuService.kt | 10 +++ .../spring/ordering/orders/OrderController.kt | 27 ++++--- .../ordering/orders/OrdersRepository.kt | 11 +-- .../spring/ordering/orders/OrdersService.kt | 71 ++++++++++--------- .../spring/ordering/users/UserRepository.kt | 15 ++-- .../spring/ordering/users/UsersController.kt | 6 +- .../spring/ordering/users/UsersService.kt | 13 ++-- 20 files changed, 344 insertions(+), 76 deletions(-) create mode 100644 src/main/kotlin/com/coded/spring/ordering/DTO/ItemDTO.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/DTO/OrderDTO.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/DTO/UserDTO.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/InitUserRunner.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/authentication/CustomerUserDetailsService.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/jwt/JwtService.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/menu/MenuController.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/menu/MenuRepository.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/menu/MenuService.kt diff --git a/pom.xml b/pom.xml index d30f9cb..3f66c63 100644 --- a/pom.xml +++ b/pom.xml @@ -83,6 +83,13 @@ + + org.springframework.boot + spring-boot-starter-security + + + + diff --git a/src/main/kotlin/com/coded/spring/ordering/DTO/ItemDTO.kt b/src/main/kotlin/com/coded/spring/ordering/DTO/ItemDTO.kt new file mode 100644 index 0000000..99e3bba --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/DTO/ItemDTO.kt @@ -0,0 +1,18 @@ +package com.coded.spring.ordering.DTO + + +data class Item( + val id: Long?, + val order_id: Long?, + val name: String?, + val quantity: Long?, + val note: String?, + val price: Double? +) + +data class SubmitItemRequest( + val name: String, + val quantity: Long, + val note: String?, + val price: Double +) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/DTO/OrderDTO.kt b/src/main/kotlin/com/coded/spring/ordering/DTO/OrderDTO.kt new file mode 100644 index 0000000..18fd359 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/DTO/OrderDTO.kt @@ -0,0 +1,14 @@ +package com.coded.spring.ordering.DTO + +import com.coded.spring.ordering.items.ItemEntity + +data class SubmitOrderRequest( + val itemIds: List +) + +data class Order( + val id: Long?, + val user_id: Long?, + val items: List +) + diff --git a/src/main/kotlin/com/coded/spring/ordering/DTO/UserDTO.kt b/src/main/kotlin/com/coded/spring/ordering/DTO/UserDTO.kt new file mode 100644 index 0000000..aef1567 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/DTO/UserDTO.kt @@ -0,0 +1,8 @@ +package com.coded.spring.ordering.DTO + +data class User( + val name: String, + val age: Int, + val username: String, + val password: String +) \ No newline at end of file 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..b7d49b7 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/InitUserRunner.kt @@ -0,0 +1,32 @@ +package com.coded.spring.ordering + +import com.coded.spring.ordering.users.UserEntity +import com.coded.spring.ordering.users.UsersRepository +import org.springframework.boot.CommandLineRunner +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication +import org.springframework.context.annotation.Bean +import org.springframework.security.crypto.password.PasswordEncoder + +@SpringBootApplication +class InitUserRunner { + @Bean + fun initUsers(userRepository: UsersRepository, passwordEncoder: PasswordEncoder) = CommandLineRunner { + val user = UserEntity( + name = "Ahmed", + username = "Ahmed123", + password = passwordEncoder.encode("password123"), + age = 22, + ) + 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/CustomerUserDetailsService.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/CustomerUserDetailsService.kt new file mode 100644 index 0000000..0cc6bdc --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/CustomerUserDetailsService.kt @@ -0,0 +1,30 @@ +package com.coded.spring.ordering.authentication + +import com.coded.spring.ordering.users.UserEntity +import com.coded.spring.ordering.users.UsersRepository +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 + +@Service +class CustomerUserDetailsService( + private val usersRepository: UsersRepository + ): UserDetailsService{ + + override fun loadUserByUsername(username:String): UserDetails { + + val user: UserEntity = usersRepository.findByUsername(username)?: + throw UsernameNotFoundException("User not found...") + + + return User.builder() + .username(user.username) + .password(user.password) + .build() + + + } + +} \ 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 new file mode 100644 index 0000000..3b14305 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt @@ -0,0 +1,64 @@ +package com.coded.spring.ordering.authentication + +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.core.userdetails.UserDetailsService +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.security.web.SecurityFilterChain + + +@Configuration +@EnableWebSecurity + +class SecurityConfig( + + private val userDetailsService: UserDetailsService + +){ + + @Bean + fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder () + +// @Bean +// fun securityFilterChain(http: HttpSecurity): SecurityFilterChain{ +// http.csrf { it.disable() } +// +// http.authorizeHttpRequests { +// it.requestMatchers("/welcome").permitAll() +// .anyRequest().authenticated() +// }.formLogin { it.defaultSuccessUrl("/welcome",true)} +// .userDetailsService(userDetailsService) +// +// return http.build() +// } + + @Bean + fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http.csrf { it.disable() } + .authorizeHttpRequests { + it + // Public endpoint + .requestMatchers("/menus/**").permitAll() + + // Protected endpoints + .requestMatchers("/orders/**").authenticated() + + // All others + .anyRequest().authenticated() + } + .formLogin { it.defaultSuccessUrl("/menus/v1/menu", true) } + .userDetailsService(userDetailsService) + + return http.build() + } + + + } + + + + + diff --git a/src/main/kotlin/com/coded/spring/ordering/items/ItemsController.kt b/src/main/kotlin/com/coded/spring/ordering/items/ItemsController.kt index de30089..61e20f4 100644 --- a/src/main/kotlin/com/coded/spring/ordering/items/ItemsController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/items/ItemsController.kt @@ -1,5 +1,7 @@ package com.coded.spring.ordering.items +import com.coded.spring.ordering.DTO.Item +import com.coded.spring.ordering.DTO.SubmitItemRequest import org.springframework.web.bind.annotation.* @RestController @@ -13,4 +15,9 @@ class ItemsController( @GetMapping("/listItems") fun listItems(): List = itemsService.listItems() + @PostMapping("/submitItems") + fun submitItem(@RequestBody request: SubmitItemRequest): ItemEntity { + return itemsService.submitItem(request) + } + } \ 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 index 805c1ee..96457f0 100644 --- a/src/main/kotlin/com/coded/spring/ordering/items/ItemsRepository.kt +++ b/src/main/kotlin/com/coded/spring/ordering/items/ItemsRepository.kt @@ -1,5 +1,6 @@ package com.coded.spring.ordering.items +import com.coded.spring.ordering.orders.OrderEntity import jakarta.inject.Named import jakarta.persistence.* import org.springframework.data.jpa.repository.JpaRepository @@ -16,10 +17,20 @@ data class ItemEntity( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null, - var order_id: Long?, - var items: String?, - var quantity: Long?, - var note: String?, - var price: Double? -) {constructor(): this(null,1,"",1,"",1.0)} \ No newline at end of file + @Column(name = "items") + var name: String? = null, + + var quantity: Long? = null, + + var note: String? = null, + + var price: Double? = null, + + @ManyToOne + @JoinColumn(name = "order_id") + var order: OrderEntity? = null + +) { + constructor(): this(null, "", 1, "", 0.0, null) +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/items/ItemsService.kt b/src/main/kotlin/com/coded/spring/ordering/items/ItemsService.kt index b047965..d0bcbb7 100644 --- a/src/main/kotlin/com/coded/spring/ordering/items/ItemsService.kt +++ b/src/main/kotlin/com/coded/spring/ordering/items/ItemsService.kt @@ -1,5 +1,9 @@ package com.coded.spring.ordering.items +import com.coded.spring.ordering.DTO.Item +import com.coded.spring.ordering.DTO.SubmitItemRequest import jakarta.inject.Named +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody @Named class ItemsService( @@ -8,20 +12,22 @@ class ItemsService( fun listItems(): List = itemsRepository.findAll().map { entity -> Item( id = entity.id, - order_id = entity.order_id, - items = entity.items, + order_id = entity.order?.id, + name = entity.name, quantity = entity.quantity, note = entity.note, price = entity.price ) } -} -data class Item( - val id: Long?, - val order_id: Long?, - val items: String?, - val quantity: Long?, - val note: String?, - val price: Double? -) \ No newline at end of file + fun submitItem(request: SubmitItemRequest): ItemEntity { + val item = ItemEntity( + name = request.name, + quantity = request.quantity, + note = request.note, + price = request.price + ) + return itemsRepository.save(item) + } + +} diff --git a/src/main/kotlin/com/coded/spring/ordering/jwt/JwtService.kt b/src/main/kotlin/com/coded/spring/ordering/jwt/JwtService.kt new file mode 100644 index 0000000..6794baf --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/jwt/JwtService.kt @@ -0,0 +1,4 @@ +package com.coded.spring.ordering.jwt + +class JwtService { +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/menu/MenuController.kt b/src/main/kotlin/com/coded/spring/ordering/menu/MenuController.kt new file mode 100644 index 0000000..1b6f6b5 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/menu/MenuController.kt @@ -0,0 +1,12 @@ +package com.coded.spring.ordering.menu + +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +class MenuController( + private val menuService: MenuService +) { + @GetMapping("/menus/v1/menu") + fun getMenu(): List = menuService.getMenu() +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/menu/MenuRepository.kt b/src/main/kotlin/com/coded/spring/ordering/menu/MenuRepository.kt new file mode 100644 index 0000000..0bcd27d --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/menu/MenuRepository.kt @@ -0,0 +1,20 @@ +package com.coded.spring.ordering.menu + +import jakarta.persistence.* +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface MenuRepository : JpaRepository +@Entity +@Table(name = "menu") +data class MenuEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, + val name: String, + val description: String, + val price: Double +) { + constructor(): this(null, "", "", 0.0) +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/menu/MenuService.kt b/src/main/kotlin/com/coded/spring/ordering/menu/MenuService.kt new file mode 100644 index 0000000..c55b913 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/menu/MenuService.kt @@ -0,0 +1,10 @@ +package com.coded.spring.ordering.menu + +import jakarta.inject.Named + +@Named +class MenuService( + private val menuRepository: MenuRepository +) { + fun getMenu(): List = menuRepository.findAll() +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OrderController.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OrderController.kt index 04c0acf..c837497 100644 --- a/src/main/kotlin/com/coded/spring/ordering/orders/OrderController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OrderController.kt @@ -1,26 +1,35 @@ package com.coded.spring.ordering.orders +import com.coded.spring.ordering.DTO.Order +import com.coded.spring.ordering.DTO.SubmitOrderRequest +import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* +import java.security.Principal @RestController - +@RequestMapping("/orders/v1") class OrderController( val ordersRepository: OrdersRepository, private val ordersService: OrdersService ){ - @GetMapping("/listOrders") - fun listOrders(): List = ordersRepository.findAll() - @PostMapping("/listOrders") - fun createOrder(@RequestBody request: CreateOrderRequest) { - ordersService.createOrder(request.userId) + @PostMapping + fun submitOrder( + @RequestBody request: SubmitOrderRequest, + principal: Principal + ): ResponseEntity { + val username = principal.name // logged-in user + ordersService.submitOrder(username, request.itemIds) + return ResponseEntity.ok("Order submitted successfully.") + } + + @GetMapping + fun listMyOrders(principal: Principal): List { + return ordersService.listOrdersForUser(principal.name) } } -data class CreateOrderRequest( - val userId: Long -) diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OrdersRepository.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OrdersRepository.kt index b723e12..89cb81d 100644 --- a/src/main/kotlin/com/coded/spring/ordering/orders/OrdersRepository.kt +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OrdersRepository.kt @@ -15,7 +15,8 @@ interface OrdersRepository: JpaRepository{ @Entity @Table(name = "orders") - class OrderEntity( +class OrderEntity( + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null, @@ -24,9 +25,9 @@ interface OrdersRepository: JpaRepository{ @JoinColumn(name = "user_id") var user: UserEntity? = null, - @OneToMany(mappedBy = "order_id") - val items: List? = null + @OneToMany(mappedBy = "order", cascade = [CascadeType.ALL]) + var items: List? = null -){ - constructor(): this(null, UserEntity(), listOf()) +) { + constructor() : this(null, null, listOf()) } \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OrdersService.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OrdersService.kt index f668484..f32b127 100644 --- a/src/main/kotlin/com/coded/spring/ordering/orders/OrdersService.kt +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OrdersService.kt @@ -1,48 +1,53 @@ package com.coded.spring.ordering.orders -import com.coded.spring.ordering.items.Item -import com.coded.spring.ordering.items.ItemEntity +import com.coded.spring.ordering.DTO.Order +import com.coded.spring.ordering.items.ItemsRepository import com.coded.spring.ordering.users.UsersRepository import jakarta.inject.Named +import com.coded.spring.ordering.DTO.Item @Named class OrdersService( private val ordersRepository: OrdersRepository, - private val usersRepository: UsersRepository - + private val usersRepository: UsersRepository, + private val itemsRepository: ItemsRepository ) { - fun listOrders(): List = ordersRepository.findAll().map { t -> - Order( - id = t.id, - user_id = t.user?.id.toString(), - items = t.items?.map { - Item( - id = it.id, - order_id = it.order_id, - quantity = it.quantity, - note = it.note, - price = it.price, - items = it.items - - ) - } - ) - } + fun submitOrder(username: String, itemIds: List) { + val user = usersRepository.findByUsername(username) + ?: throw IllegalArgumentException("User not found") - fun createOrder(userId: Long) { - val user = usersRepository.findById(userId).orElseThrow { - IllegalArgumentException("User with id $userId not found") + val order = OrderEntity(user = user) + val savedOrder = ordersRepository.save(order) + + val items = itemsRepository.findAllById(itemIds).map { + it.order = savedOrder + it } - val newOrder = OrderEntity(user = user) - ordersRepository.save(newOrder) - } -} + itemsRepository.saveAll(items) + } -data class Order( - val id: Long?, - val user_id: String, - var items: List? -) + fun listOrdersForUser(username: String): List { + val user = usersRepository.findByUsername(username) + ?: throw IllegalArgumentException("User not found") + + return ordersRepository.findByUserId(user.id!!).map { orderEntity -> + Order( + id = orderEntity.id, + user_id = user.id, + items = orderEntity.items?.map { + Item( + id = it.id, + order_id = it.order?.id, + name = it.name, + quantity = it.quantity, + note = it.note, + price = it.price + ) + } ?: listOf() + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UserRepository.kt b/src/main/kotlin/com/coded/spring/ordering/users/UserRepository.kt index 7335edb..c07895f 100644 --- a/src/main/kotlin/com/coded/spring/ordering/users/UserRepository.kt +++ b/src/main/kotlin/com/coded/spring/ordering/users/UserRepository.kt @@ -7,6 +7,8 @@ import org.springframework.data.jpa.repository.JpaRepository @Named interface UsersRepository : JpaRepository { fun age(age: Int): MutableList + + fun findByUsername(username: String): UserEntity? } @Entity @@ -14,9 +16,14 @@ interface UsersRepository : JpaRepository { data class UserEntity( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - var id: Long? = null, - var name: String, - var age: Int + val id: Long? = null, + val name: String, + val age: Int, + + + val username: String, + val password: String, + ){ - constructor() : this(null, "", 0) + constructor() : this(null, "", 0,"","") } \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UsersController.kt b/src/main/kotlin/com/coded/spring/ordering/users/UsersController.kt index a161745..9ed7e2c 100644 --- a/src/main/kotlin/com/coded/spring/ordering/users/UsersController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/users/UsersController.kt @@ -7,6 +7,10 @@ import org.springframework.web.bind.annotation.RestController class UsersController( private val usersService: UsersService ){ - @GetMapping("/users/v1/list") + //@GetMapping("/users/v1/list") + @GetMapping("/welcome") fun users() = usersService.listUsers() + + //@GetMapping("/menus/v1/menu") + } \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UsersService.kt b/src/main/kotlin/com/coded/spring/ordering/users/UsersService.kt index 7989431..0394f67 100644 --- a/src/main/kotlin/com/coded/spring/ordering/users/UsersService.kt +++ b/src/main/kotlin/com/coded/spring/ordering/users/UsersService.kt @@ -1,4 +1,5 @@ package com.coded.spring.ordering.users +import com.coded.spring.ordering.DTO.User import jakarta.inject.Named @Named @@ -9,12 +10,10 @@ class UsersService( fun listUsers(): List = usersRepository.findAll().map { User( name = it.name, - age = it.age + age = it.age, + username = it.username, + password = it.password + ) } -} - -data class User( - val name: String, - val age: Int -) \ No newline at end of file +} \ No newline at end of file From 98e19040bd41f059201eaf9702c1120e068b7d49 Mon Sep 17 00:00:00 2001 From: Yousef Alothman Date: Sun, 20 Apr 2025 15:09:51 +0300 Subject: [PATCH 07/18] Online Ordering - Profiles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added profile management features with JWT authentication. Set up ProfileEntity with a one-to-one relationship to users using userId. Added custom validation for names and phone numbers using regex and threw InvalidProfileException when needed. Created a POST /profile endpoint for authenticated users to submit their profile, and made sure duplicates aren’t allowed. Also added GET /profile to retrieve the logged-in user’s profile. Marked the service as open and transactional, and swapped @Named for @Service to fix injection issues. Error handling is done with try/catch blocks and returns clean JSON messages. No unit testing yet. --- pom.xml | 32 ++++++++- .../coded/spring/ordering/DTO/ProfileDTO.kt | 7 ++ .../ordering/authentication/SecurityConfig.kt | 64 ++++++++--------- .../jwt/AuthenticationController.kt | 38 ++++++++++ .../jwt/JwtAuthenticationFilter.kt | 45 ++++++++++++ .../ordering/authentication/jwt/JwtService.kt | 41 +++++++++++ .../exceptions/InvalidProfileException.kt | 3 + .../coded/spring/ordering/jwt/JwtService.kt | 4 -- .../ordering/profiles/ProfileController.kt | 45 ++++++++++++ .../ordering/profiles/ProfileRepository.kt | 25 +++++++ .../ordering/profiles/ProfileService.kt | 69 +++++++++++++++++++ 11 files changed, 331 insertions(+), 42 deletions(-) create mode 100644 src/main/kotlin/com/coded/spring/ordering/DTO/ProfileDTO.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/authentication/jwt/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/exceptions/InvalidProfileException.kt delete mode 100644 src/main/kotlin/com/coded/spring/ordering/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 3f66c63..84640ad 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,13 @@ 1.9.25 + + + + + + + org.springframework.boot spring-boot-starter-web @@ -81,13 +88,34 @@ postgresql - - 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 + + + + + + + diff --git a/src/main/kotlin/com/coded/spring/ordering/DTO/ProfileDTO.kt b/src/main/kotlin/com/coded/spring/ordering/DTO/ProfileDTO.kt new file mode 100644 index 0000000..f0acc98 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/DTO/ProfileDTO.kt @@ -0,0 +1,7 @@ +package com.coded.spring.ordering.DTO + +data class ProfileRequest( + val firstName: String, + val lastName: String, + val phoneNumber: 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 3b14305..1d2970f 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt @@ -1,64 +1,56 @@ package com.coded.spring.ordering.authentication +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 jwtAuthFilter: JwtAuthenticationFilter, private val userDetailsService: UserDetailsService - -){ - - @Bean - fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder () - -// @Bean -// fun securityFilterChain(http: HttpSecurity): SecurityFilterChain{ -// http.csrf { it.disable() } -// -// http.authorizeHttpRequests { -// it.requestMatchers("/welcome").permitAll() -// .anyRequest().authenticated() -// }.formLogin { it.defaultSuccessUrl("/welcome",true)} -// .userDetailsService(userDetailsService) -// -// return http.build() -// } +) { @Bean fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { http.csrf { it.disable() } .authorizeHttpRequests { - it - // Public endpoint - .requestMatchers("/menus/**").permitAll() - - // Protected endpoints - .requestMatchers("/orders/**").authenticated() - - // All others + it.requestMatchers("/auth/**").permitAll() .anyRequest().authenticated() } - .formLogin { it.defaultSuccessUrl("/menus/v1/menu", true) } - .userDetailsService(userDetailsService) + .sessionManagement { + it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + } + .authenticationProvider(authenticationProvider()) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter::class.java) return http.build() } + @Bean + fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder() - } - - - - + @Bean + fun authenticationManager(config: AuthenticationConfiguration): AuthenticationManager = + config.authenticationManager + @Bean + fun authenticationProvider(): AuthenticationProvider { + val provider = DaoAuthenticationProvider() + provider.setUserDetailsService(userDetailsService) + provider.setPasswordEncoder(passwordEncoder()) + return provider + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/AuthenticationController.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/AuthenticationController.kt new file mode 100644 index 0000000..e644be9 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/AuthenticationController.kt @@ -0,0 +1,38 @@ +package com.coded.spring.ordering.authentication.jwt + +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("/auth") +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/jwt/JwtAuthenticationFilter.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtAuthenticationFilter.kt new file mode 100644 index 0000000..f180b7c --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtAuthenticationFilter.kt @@ -0,0 +1,45 @@ +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..fec69c2 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtService.kt @@ -0,0 +1,41 @@ +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/exceptions/InvalidProfileException.kt b/src/main/kotlin/com/coded/spring/ordering/exceptions/InvalidProfileException.kt new file mode 100644 index 0000000..b985ade --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/exceptions/InvalidProfileException.kt @@ -0,0 +1,3 @@ +package com.coded.spring.ordering.exceptions + +class InvalidProfileException(message: String) : Exception(message) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/jwt/JwtService.kt b/src/main/kotlin/com/coded/spring/ordering/jwt/JwtService.kt deleted file mode 100644 index 6794baf..0000000 --- a/src/main/kotlin/com/coded/spring/ordering/jwt/JwtService.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.coded.spring.ordering.jwt - -class JwtService { -} \ No newline at end of file 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..72bf281 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileController.kt @@ -0,0 +1,45 @@ +package com.coded.spring.ordering.profiles + +import com.coded.spring.ordering.DTO.ProfileRequest +import com.coded.spring.ordering.exceptions.InvalidProfileException +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* +import java.security.Principal + +@RestController +@RequestMapping("/profile") +class ProfileController( + private val profileService: ProfileService +) { + + @PostMapping + fun submitProfile( + @RequestBody request: ProfileRequest, + principal: Principal + ): ResponseEntity { + return try { + profileService.saveProfile(principal.name, request) + ResponseEntity.ok(mapOf("message" to "Profile created successfully.")) + } catch (e: InvalidProfileException) { + ResponseEntity.badRequest().body(mapOf("error" to e.message)) + } catch (e: Exception) { + e.printStackTrace() + ResponseEntity.internalServerError().body(mapOf("error" to "Something went wrong.")) + } + } + + @GetMapping + fun getProfile(principal: Principal): ResponseEntity { + return try { + val profile = profileService.getByUsername(principal.name) + ResponseEntity.ok(profile) + } catch (e: InvalidProfileException) { + ResponseEntity.badRequest().body(mapOf("error" to e.message)) + } catch (e: Exception) { + e.printStackTrace() + ResponseEntity.internalServerError().body(mapOf("error" to "Something went wrong")) + } + } + + +} \ No newline at end of file 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..c0b0ee4 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileRepository.kt @@ -0,0 +1,25 @@ +package com.coded.spring.ordering.profiles + +import jakarta.inject.Named +import jakarta.persistence.* +import org.springframework.data.jpa.repository.JpaRepository + +@Named +interface ProfileRepository : JpaRepository { + fun findByUserId(userId: Long): ProfileEntity? +} + +@Entity +@Table(name = "profiles") +data class ProfileEntity( + +@Id +@GeneratedValue(strategy = GenerationType.IDENTITY) +val id: Long? = null, + +val firstName: String = "", +val lastName: String = "", +val phoneNumber: String = "", +val userId: Long = 0 + +) \ 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..71ec1ad --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileService.kt @@ -0,0 +1,69 @@ +package com.coded.spring.ordering.profiles + +import com.coded.spring.ordering.DTO.ProfileRequest +import com.coded.spring.ordering.exceptions.InvalidProfileException +import com.coded.spring.ordering.users.UsersRepository +import org.springframework.security.core.userdetails.UsernameNotFoundException +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +open class ProfileService( + private val profileRepository: ProfileRepository, + private val usersRepository: UsersRepository +) { + + fun isValidName(name: String): Boolean { + return name.matches(Regex("^[A-Za-z]+$")) + } + + fun isValidPhone(phone: String): Boolean { + return phone.matches(Regex("^[0-9]{8}$")) + } + + @Transactional + fun saveProfile(username: String, request: ProfileRequest): ProfileEntity { + val user = usersRepository.findByUsername(username) + ?: throw UsernameNotFoundException("User not found") + + val existingProfile = profileRepository.findByUserId(user.id!!) + if (existingProfile != null) { + throw InvalidProfileException("Profile already exists for this user") + } + + if (!isValidName(request.firstName)) { + throw InvalidProfileException("First name must contain only letters") + } + + if (!isValidName(request.lastName)) { + throw InvalidProfileException("Last name must contain only letters") + } + + if (!isValidPhone(request.phoneNumber)) { + throw InvalidProfileException("Phone number must be exactly 8 digits") + } + + val profile = ProfileEntity( + firstName = request.firstName, + lastName = request.lastName, + phoneNumber = request.phoneNumber, + userId = user.id!! + ) + + return profileRepository.save(profile) + } + + fun getByUsername(username: String): ProfileEntity { + val user = usersRepository.findByUsername(username) + if (user == null) { + throw UsernameNotFoundException("User not found") + } + + val profile = profileRepository.findByUserId(user.id!!) + if (profile == null) { + throw InvalidProfileException("Profile not found for this user") + } + + return profile + } +} \ No newline at end of file From dc18aded24f894da3e24478f2cdbc7c092a01e0c Mon Sep 17 00:00:00 2001 From: Yousef Alothman Date: Sun, 27 Apr 2025 16:47:51 +0300 Subject: [PATCH 08/18] Online Ordering - Profile Testing --- pom.xml | 14 +++- .../coded/spring/ordering/DTO/ProfileDTO.kt | 7 -- .../com/coded/spring/ordering/DTO/UserDTO.kt | 7 +- .../ordering/authentication/SecurityConfig.kt | 2 +- .../exceptions/InvalidProfileException.kt | 3 - .../exceptions/TransferFundsException.kt | 3 + .../ordering/profiles/ProfileController.kt | 45 ------------ .../ordering/profiles/ProfileRepository.kt | 25 ------- .../ordering/profiles/ProfileService.kt | 69 ------------------- .../ordering/{ => script}/InitUserRunner.kt | 2 +- .../spring/ordering/users/UserRepository.kt | 2 - .../spring/ordering/users/UsersController.kt | 23 +++++-- .../spring/ordering/users/UsersService.kt | 40 ++++++++++- .../spring/ordering/UserIntegrationTest.kt | 42 +++++++++++ 14 files changed, 122 insertions(+), 162 deletions(-) delete mode 100644 src/main/kotlin/com/coded/spring/ordering/DTO/ProfileDTO.kt delete mode 100644 src/main/kotlin/com/coded/spring/ordering/exceptions/InvalidProfileException.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/exceptions/TransferFundsException.kt delete mode 100644 src/main/kotlin/com/coded/spring/ordering/profiles/ProfileController.kt delete mode 100644 src/main/kotlin/com/coded/spring/ordering/profiles/ProfileRepository.kt delete mode 100644 src/main/kotlin/com/coded/spring/ordering/profiles/ProfileService.kt rename src/main/kotlin/com/coded/spring/ordering/{ => script}/InitUserRunner.kt (96%) create mode 100644 src/test/kotlin/com/coded/spring/ordering/UserIntegrationTest.kt diff --git a/pom.xml b/pom.xml index 84640ad..f16cee9 100644 --- a/pom.xml +++ b/pom.xml @@ -74,7 +74,7 @@ com.h2database h2 - runtime + test @@ -86,6 +86,7 @@ org.postgresql postgresql + compile @@ -111,6 +112,17 @@ 0.11.5 + + org.springframework.boot + spring-boot-starter-test + test + + + org.jetbrains.kotlin + kotlin-test-junit5 + test + + diff --git a/src/main/kotlin/com/coded/spring/ordering/DTO/ProfileDTO.kt b/src/main/kotlin/com/coded/spring/ordering/DTO/ProfileDTO.kt deleted file mode 100644 index f0acc98..0000000 --- a/src/main/kotlin/com/coded/spring/ordering/DTO/ProfileDTO.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.coded.spring.ordering.DTO - -data class ProfileRequest( - val firstName: String, - val lastName: String, - val phoneNumber: String -) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/DTO/UserDTO.kt b/src/main/kotlin/com/coded/spring/ordering/DTO/UserDTO.kt index aef1567..962270a 100644 --- a/src/main/kotlin/com/coded/spring/ordering/DTO/UserDTO.kt +++ b/src/main/kotlin/com/coded/spring/ordering/DTO/UserDTO.kt @@ -1,8 +1,13 @@ package com.coded.spring.ordering.DTO -data class User( +data class UserRequest( val name: String, val age: Int, val username: String, val password: String +) + +data class UserResponse( + val id: Long, + val username: 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 1d2970f..64f9a47 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt @@ -27,7 +27,7 @@ class SecurityConfig( fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { http.csrf { it.disable() } .authorizeHttpRequests { - it.requestMatchers("/auth/**").permitAll() + it.requestMatchers("/auth/**","/users/**").permitAll() .anyRequest().authenticated() } .sessionManagement { diff --git a/src/main/kotlin/com/coded/spring/ordering/exceptions/InvalidProfileException.kt b/src/main/kotlin/com/coded/spring/ordering/exceptions/InvalidProfileException.kt deleted file mode 100644 index b985ade..0000000 --- a/src/main/kotlin/com/coded/spring/ordering/exceptions/InvalidProfileException.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.coded.spring.ordering.exceptions - -class InvalidProfileException(message: String) : Exception(message) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/exceptions/TransferFundsException.kt b/src/main/kotlin/com/coded/spring/ordering/exceptions/TransferFundsException.kt new file mode 100644 index 0000000..4fccd82 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/exceptions/TransferFundsException.kt @@ -0,0 +1,3 @@ +package com.coded.spring.ordering.exceptions + +class TransferFundsException(msg: String) : Exception(msg) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileController.kt b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileController.kt deleted file mode 100644 index 72bf281..0000000 --- a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileController.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.coded.spring.ordering.profiles - -import com.coded.spring.ordering.DTO.ProfileRequest -import com.coded.spring.ordering.exceptions.InvalidProfileException -import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.* -import java.security.Principal - -@RestController -@RequestMapping("/profile") -class ProfileController( - private val profileService: ProfileService -) { - - @PostMapping - fun submitProfile( - @RequestBody request: ProfileRequest, - principal: Principal - ): ResponseEntity { - return try { - profileService.saveProfile(principal.name, request) - ResponseEntity.ok(mapOf("message" to "Profile created successfully.")) - } catch (e: InvalidProfileException) { - ResponseEntity.badRequest().body(mapOf("error" to e.message)) - } catch (e: Exception) { - e.printStackTrace() - ResponseEntity.internalServerError().body(mapOf("error" to "Something went wrong.")) - } - } - - @GetMapping - fun getProfile(principal: Principal): ResponseEntity { - return try { - val profile = profileService.getByUsername(principal.name) - ResponseEntity.ok(profile) - } catch (e: InvalidProfileException) { - ResponseEntity.badRequest().body(mapOf("error" to e.message)) - } catch (e: Exception) { - e.printStackTrace() - ResponseEntity.internalServerError().body(mapOf("error" to "Something went wrong")) - } - } - - -} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileRepository.kt b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileRepository.kt deleted file mode 100644 index c0b0ee4..0000000 --- a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileRepository.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.coded.spring.ordering.profiles - -import jakarta.inject.Named -import jakarta.persistence.* -import org.springframework.data.jpa.repository.JpaRepository - -@Named -interface ProfileRepository : JpaRepository { - fun findByUserId(userId: Long): ProfileEntity? -} - -@Entity -@Table(name = "profiles") -data class ProfileEntity( - -@Id -@GeneratedValue(strategy = GenerationType.IDENTITY) -val id: Long? = null, - -val firstName: String = "", -val lastName: String = "", -val phoneNumber: String = "", -val userId: Long = 0 - -) \ 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 deleted file mode 100644 index 71ec1ad..0000000 --- a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileService.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.coded.spring.ordering.profiles - -import com.coded.spring.ordering.DTO.ProfileRequest -import com.coded.spring.ordering.exceptions.InvalidProfileException -import com.coded.spring.ordering.users.UsersRepository -import org.springframework.security.core.userdetails.UsernameNotFoundException -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional - -@Service -open class ProfileService( - private val profileRepository: ProfileRepository, - private val usersRepository: UsersRepository -) { - - fun isValidName(name: String): Boolean { - return name.matches(Regex("^[A-Za-z]+$")) - } - - fun isValidPhone(phone: String): Boolean { - return phone.matches(Regex("^[0-9]{8}$")) - } - - @Transactional - fun saveProfile(username: String, request: ProfileRequest): ProfileEntity { - val user = usersRepository.findByUsername(username) - ?: throw UsernameNotFoundException("User not found") - - val existingProfile = profileRepository.findByUserId(user.id!!) - if (existingProfile != null) { - throw InvalidProfileException("Profile already exists for this user") - } - - if (!isValidName(request.firstName)) { - throw InvalidProfileException("First name must contain only letters") - } - - if (!isValidName(request.lastName)) { - throw InvalidProfileException("Last name must contain only letters") - } - - if (!isValidPhone(request.phoneNumber)) { - throw InvalidProfileException("Phone number must be exactly 8 digits") - } - - val profile = ProfileEntity( - firstName = request.firstName, - lastName = request.lastName, - phoneNumber = request.phoneNumber, - userId = user.id!! - ) - - return profileRepository.save(profile) - } - - fun getByUsername(username: String): ProfileEntity { - val user = usersRepository.findByUsername(username) - if (user == null) { - throw UsernameNotFoundException("User not found") - } - - val profile = profileRepository.findByUserId(user.id!!) - if (profile == null) { - throw InvalidProfileException("Profile not found for this user") - } - - return profile - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/InitUserRunner.kt b/src/main/kotlin/com/coded/spring/ordering/script/InitUserRunner.kt similarity index 96% rename from src/main/kotlin/com/coded/spring/ordering/InitUserRunner.kt rename to src/main/kotlin/com/coded/spring/ordering/script/InitUserRunner.kt index b7d49b7..67a8062 100644 --- a/src/main/kotlin/com/coded/spring/ordering/InitUserRunner.kt +++ b/src/main/kotlin/com/coded/spring/ordering/script/InitUserRunner.kt @@ -1,4 +1,4 @@ -package com.coded.spring.ordering +package com.coded.spring.ordering.script import com.coded.spring.ordering.users.UserEntity import com.coded.spring.ordering.users.UsersRepository diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UserRepository.kt b/src/main/kotlin/com/coded/spring/ordering/users/UserRepository.kt index c07895f..79fb067 100644 --- a/src/main/kotlin/com/coded/spring/ordering/users/UserRepository.kt +++ b/src/main/kotlin/com/coded/spring/ordering/users/UserRepository.kt @@ -7,7 +7,6 @@ import org.springframework.data.jpa.repository.JpaRepository @Named interface UsersRepository : JpaRepository { fun age(age: Int): MutableList - fun findByUsername(username: String): UserEntity? } @@ -20,7 +19,6 @@ data class UserEntity( val name: String, val age: Int, - val username: String, val password: String, diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UsersController.kt b/src/main/kotlin/com/coded/spring/ordering/users/UsersController.kt index 9ed7e2c..35c387f 100644 --- a/src/main/kotlin/com/coded/spring/ordering/users/UsersController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/users/UsersController.kt @@ -1,16 +1,31 @@ package com.coded.spring.ordering.users +import com.coded.spring.ordering.DTO.UserRequest +import com.coded.spring.ordering.exceptions.TransferFundsException +import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping +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 @RestController +@RequestMapping("/users") class UsersController( private val usersService: UsersService ){ - //@GetMapping("/users/v1/list") - @GetMapping("/welcome") - fun users() = usersService.listUsers() - //@GetMapping("/menus/v1/menu") + @PostMapping("/v1/register") + fun registerUser(@RequestBody request: UserRequest): ResponseEntity { + return try { + val newUser = usersService.registerUser(request) + ResponseEntity.ok(newUser) + } catch (e: TransferFundsException) { + ResponseEntity.badRequest().body(mapOf("error" to e.message)) } + } + + + @GetMapping("/v1/list") + fun users() = usersService.listUsers() } \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UsersService.kt b/src/main/kotlin/com/coded/spring/ordering/users/UsersService.kt index 0394f67..a0eb3fc 100644 --- a/src/main/kotlin/com/coded/spring/ordering/users/UsersService.kt +++ b/src/main/kotlin/com/coded/spring/ordering/users/UsersService.kt @@ -1,14 +1,48 @@ package com.coded.spring.ordering.users -import com.coded.spring.ordering.DTO.User +import com.coded.spring.ordering.DTO.UserRequest +import com.coded.spring.ordering.DTO.UserResponse +import com.coded.spring.ordering.exceptions.TransferFundsException import jakarta.inject.Named +import org.springframework.stereotype.Service + +const val USERNAME_MIN_LENGTH = 4 +const val USERNAME_MAX_LENGTH = 30 +const val PASSWORD_MIN_LENGTH = 9 +const val PASSWORD_MAX_LENGTH = 30 @Named +@Service class UsersService( private val usersRepository: UsersRepository, ) { - fun listUsers(): List = usersRepository.findAll().map { - User( + fun registerUser(request: UserRequest): UserResponse { + + if (request.username.length < USERNAME_MIN_LENGTH || + request.username.length > USERNAME_MAX_LENGTH) { + throw TransferFundsException( + "Username must be between ${USERNAME_MIN_LENGTH} and ${USERNAME_MAX_LENGTH} characters") + } + + if (request.password.length < PASSWORD_MIN_LENGTH || + request.password.length > PASSWORD_MAX_LENGTH) { + throw TransferFundsException( + "Password must be between ${PASSWORD_MIN_LENGTH} and ${PASSWORD_MAX_LENGTH} characters") + } + + val createUser = UserEntity( + name = request.name, + age = request.age, + username = request.username, + password = request.password + ) + + val savedUser = usersRepository.save(createUser) + return UserResponse(id = savedUser.id!!, username = savedUser.username) + } + + fun listUsers(): List = usersRepository.findAll().map { + UserRequest( name = it.name, age = it.age, username = it.username, diff --git a/src/test/kotlin/com/coded/spring/ordering/UserIntegrationTest.kt b/src/test/kotlin/com/coded/spring/ordering/UserIntegrationTest.kt new file mode 100644 index 0000000..f192467 --- /dev/null +++ b/src/test/kotlin/com/coded/spring/ordering/UserIntegrationTest.kt @@ -0,0 +1,42 @@ +package com.coded.spring.ordering + +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.* +import kotlin.test.assertEquals + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class UserRequestIntegrationTest { + + @Autowired + lateinit var restTemplate: TestRestTemplate + + @Test + fun createUser() { + val user = mapOf( + "name" to "Ali", + "age" to 23, + "username" to "Ali123", + "password" to "password123" + ) + + val headers = HttpHeaders() + headers.contentType = MediaType.APPLICATION_JSON + + val response = restTemplate.postForEntity( + "/users/v1/register", + HttpEntity(user, headers), + String::class.java + ) + + assertEquals(HttpStatus.OK, response.statusCode) + } + + @Test + fun getUsers() { + val response = restTemplate.getForEntity("/users/v1/list", String::class.java) + assertEquals(HttpStatus.OK, response.statusCode) + } +} \ No newline at end of file From 54acea78b05c9f36e95b7df50d740dded92c34b7 Mon Sep 17 00:00:00 2001 From: Yousef Alothman Date: Sun, 27 Apr 2025 17:52:34 +0300 Subject: [PATCH 09/18] Online Ordering - Logging exercise --- .../ordering/authentication/SecurityConfig.kt | 2 +- .../spring/ordering/config/LoggingFilter.kt | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/com/coded/spring/ordering/config/LoggingFilter.kt 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 64f9a47..0802e29 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt @@ -27,7 +27,7 @@ class SecurityConfig( fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { http.csrf { it.disable() } .authorizeHttpRequests { - it.requestMatchers("/auth/**","/users/**").permitAll() + it.requestMatchers("/auth/**","/users/v1/register","/menus/v1/menu").permitAll() .anyRequest().authenticated() } .sessionManagement { diff --git a/src/main/kotlin/com/coded/spring/ordering/config/LoggingFilter.kt b/src/main/kotlin/com/coded/spring/ordering/config/LoggingFilter.kt new file mode 100644 index 0000000..116f0a7 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/config/LoggingFilter.kt @@ -0,0 +1,42 @@ +package com.coded.spring.ordering.config + +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) + + 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 = String(request.contentAsByteArray) + logger.info("Request: method=${request.method}, uri=${request.requestURI}, body=$requestBody") + } + + private fun logResponse(response: ContentCachingResponseWrapper) { + val responseBody = String(response.contentAsByteArray) + logger.info("Response: status=${response.status}, body=$responseBody") + } +} \ No newline at end of file From 7664376e8f3b597d6432e945560f0385642b0a9d Mon Sep 17 00:00:00 2001 From: Yousef Alothman Date: Mon, 28 Apr 2025 18:04:36 +0300 Subject: [PATCH 10/18] Online Ordering - Swagger & Open API --- pom.xml | 6 +++++- .../coded/spring/ordering/authentication/SecurityConfig.kt | 2 +- src/main/resources/application.properties | 4 +++- swaggerJson/online-ordering-swagger-01.json | 1 + 4 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 swaggerJson/online-ordering-swagger-01.json diff --git a/pom.xml b/pom.xml index f16cee9..d37fec3 100644 --- a/pom.xml +++ b/pom.xml @@ -123,7 +123,11 @@ test - + + org.springdoc + springdoc-openapi-starter-webmvc-api + 2.6.0 + 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 0802e29..0b96b90 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt @@ -27,7 +27,7 @@ class SecurityConfig( fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { http.csrf { it.disable() } .authorizeHttpRequests { - it.requestMatchers("/auth/**","/users/v1/register","/menus/v1/menu").permitAll() + it.requestMatchers("/auth/**","/users/v1/register","/menus/v1/menu","/api-docs").permitAll() .anyRequest().authenticated() } .sessionManagement { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 102ce35..cc351ec 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -4,4 +4,6 @@ server.port = 8080 spring.datasource.url=jdbc:postgresql://localhost:5432/myHelloDatabase spring.datasource.username=postgres spring.datasource.password=yosaka -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect \ No newline at end of file +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect + +springdoc.api-docs.path=/api-docs diff --git a/swaggerJson/online-ordering-swagger-01.json b/swaggerJson/online-ordering-swagger-01.json new file mode 100644 index 0000000..3fd7b00 --- /dev/null +++ b/swaggerJson/online-ordering-swagger-01.json @@ -0,0 +1 @@ +{"openapi":"3.0.1","info":{"title":"OpenAPI definition","version":"v0"},"servers":[{"url":"http://localhost:8080","description":"Generated server url"}],"paths":{"/users/v1/register":{"post":{"tags":["users-controller"],"operationId":"registerUser","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/submitItems":{"post":{"tags":["items-controller"],"operationId":"submitItem","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitItemRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ItemEntity"}}}}}}},"/orders/v1":{"get":{"tags":["order-controller"],"operationId":"listMyOrders","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Order"}}}}}}},"post":{"tags":["order-controller"],"operationId":"submitOrder","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitOrderRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"string"}}}}}}},"/auth/login":{"post":{"tags":["authentication-controller"],"operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthenticationRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthenticationResponse"}}}}}}},"/users/v1/list":{"get":{"tags":["users-controller"],"operationId":"users","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserRequest"}}}}}}}},"/menus/v1/menu":{"get":{"tags":["menu-controller"],"operationId":"getMenu","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MenuEntity"}}}}}}}},"/listItems":{"get":{"tags":["items-controller"],"operationId":"listItems","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Item"}}}}}}}}},"components":{"schemas":{"UserRequest":{"required":["age","name","password","username"],"type":"object","properties":{"name":{"type":"string"},"age":{"type":"integer","format":"int32"},"username":{"type":"string"},"password":{"type":"string"}}},"SubmitItemRequest":{"required":["name","price","quantity"],"type":"object","properties":{"name":{"type":"string"},"quantity":{"type":"integer","format":"int64"},"note":{"type":"string"},"price":{"type":"number","format":"double"}}},"ItemEntity":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"quantity":{"type":"integer","format":"int64"},"note":{"type":"string"},"price":{"type":"number","format":"double"},"order":{"$ref":"#/components/schemas/OrderEntity"}}},"OrderEntity":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"user":{"$ref":"#/components/schemas/UserEntity"},"items":{"type":"array","items":{"$ref":"#/components/schemas/ItemEntity"}}}},"UserEntity":{"required":["age","name","password","username"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"age":{"type":"integer","format":"int32"},"username":{"type":"string"},"password":{"type":"string"}}},"SubmitOrderRequest":{"required":["itemIds"],"type":"object","properties":{"itemIds":{"type":"array","items":{"type":"integer","format":"int64"}}}},"AuthenticationRequest":{"required":["password","username"],"type":"object","properties":{"username":{"type":"string"},"password":{"type":"string"}}},"AuthenticationResponse":{"required":["token"],"type":"object","properties":{"token":{"type":"string"}}},"Item":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"order_id":{"type":"integer","format":"int64"},"name":{"type":"string"},"quantity":{"type":"integer","format":"int64"},"note":{"type":"string"},"price":{"type":"number","format":"double"}}},"Order":{"required":["items"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"user_id":{"type":"integer","format":"int64"},"items":{"type":"array","items":{"$ref":"#/components/schemas/Item"}}}},"MenuEntity":{"required":["description","name","price"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"description":{"type":"string"},"price":{"type":"number","format":"double"}}}}}} \ No newline at end of file From 40cce1fd55289624b2a7cf4d0be60aa02895170d Mon Sep 17 00:00:00 2001 From: Yousef Alothman Date: Tue, 29 Apr 2025 19:27:07 +0300 Subject: [PATCH 11/18] Online Ordering - Unit Test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Set up Cucumber integration with Spring Boot context loading - Implemented JWT token injection via JwtService for secured endpoints - Created separate Cucumber tests for: β€’ User registration β€’ User login and listing β€’ Submitting and listing orders β€’ Submitting and listing menu items β€’ Submitting and retrieving user profiles - Fixed GlobalToken generation issues - Handled authenticated requests in tests without relying on logging - Split all test steps into isolated step definition files for clarity - Fixed HTTP 400 error by submitting a profile before retrieving it - Standardized test structure across the project for easier maintenance --- pom.xml | 32 +++++++-- .../coded/spring/ordering/DTO/ProfileDTO.kt | 7 ++ .../ordering/authentication/SecurityConfig.kt | 2 +- .../exceptions/InvalidProfileException.kt | 3 + .../spring/ordering/menu/MenuController.kt | 2 +- .../ordering/profiles/ProfileController.kt | 43 +++++++++++ .../ordering/profiles/ProfileRepository.kt | 25 +++++++ .../ordering/profiles/ProfileService.kt | 69 ++++++++++++++++++ .../spring/ordering/users/UsersService.kt | 4 ++ src/main/resources/application.properties | 18 ++--- .../coded/spring/ordering/ApplicationTests.kt | 10 ++- .../coded/spring/ordering/config/TestHooks.kt | 71 +++++++++++++++++++ .../spring/ordering/steps/GetProfileSteps.kt | 50 +++++++++++++ .../steps/ListItemsStepDefinitions.kt | 34 +++++++++ .../steps/ListOrdersStepDefinitions.kt | 34 +++++++++ .../steps/ListUsersStepDefinitions.kt | 34 +++++++++ .../ordering/steps/MenuStepDefinitions.kt | 31 ++++++++ .../spring/ordering/steps/SubmitItemSteps.kt | 48 +++++++++++++ .../steps/SubmitOrderStepDefinitions.kt | 41 +++++++++++ .../ordering/steps/SubmitProfileSteps.kt | 48 +++++++++++++ .../ordering/steps/UserRegistrationSteps.kt | 45 ++++++++++++ .../spring/ordering/utils/GlobalToken.kt | 5 ++ .../resources/application-test.properties | 14 ++++ .../resources/feature/get_profile.feature | 6 ++ .../resources/feature/list_items.feature | 4 ++ .../resources/feature/list_orders.feature | 5 ++ .../resources/feature/list_users.feature | 5 ++ .../kotlin/resources/feature/menu.feature | 5 ++ .../resources/feature/submit_item.feature | 5 ++ .../resources/feature/submit_order.feature | 5 ++ .../resources/feature/submit_profile.feature | 6 ++ .../resources/feature/user_register.feature | 10 +++ 32 files changed, 702 insertions(+), 19 deletions(-) create mode 100644 src/main/kotlin/com/coded/spring/ordering/DTO/ProfileDTO.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/exceptions/InvalidProfileException.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 create mode 100644 src/test/kotlin/com/coded/spring/ordering/config/TestHooks.kt create mode 100644 src/test/kotlin/com/coded/spring/ordering/steps/GetProfileSteps.kt create mode 100644 src/test/kotlin/com/coded/spring/ordering/steps/ListItemsStepDefinitions.kt create mode 100644 src/test/kotlin/com/coded/spring/ordering/steps/ListOrdersStepDefinitions.kt create mode 100644 src/test/kotlin/com/coded/spring/ordering/steps/ListUsersStepDefinitions.kt create mode 100644 src/test/kotlin/com/coded/spring/ordering/steps/MenuStepDefinitions.kt create mode 100644 src/test/kotlin/com/coded/spring/ordering/steps/SubmitItemSteps.kt create mode 100644 src/test/kotlin/com/coded/spring/ordering/steps/SubmitOrderStepDefinitions.kt create mode 100644 src/test/kotlin/com/coded/spring/ordering/steps/SubmitProfileSteps.kt create mode 100644 src/test/kotlin/com/coded/spring/ordering/steps/UserRegistrationSteps.kt create mode 100644 src/test/kotlin/com/coded/spring/ordering/utils/GlobalToken.kt create mode 100644 src/test/kotlin/resources/application-test.properties create mode 100644 src/test/kotlin/resources/feature/get_profile.feature create mode 100644 src/test/kotlin/resources/feature/list_items.feature create mode 100644 src/test/kotlin/resources/feature/list_orders.feature create mode 100644 src/test/kotlin/resources/feature/list_users.feature create mode 100644 src/test/kotlin/resources/feature/menu.feature create mode 100644 src/test/kotlin/resources/feature/submit_item.feature create mode 100644 src/test/kotlin/resources/feature/submit_order.feature create mode 100644 src/test/kotlin/resources/feature/submit_profile.feature create mode 100644 src/test/kotlin/resources/feature/user_register.feature diff --git a/pom.xml b/pom.xml index d37fec3..b71c097 100644 --- a/pom.xml +++ b/pom.xml @@ -83,11 +83,11 @@ 2.0.1 - - org.postgresql - postgresql - compile - + + + + + org.springframework.boot @@ -129,6 +129,28 @@ 2.6.0 + + io.cucumber + cucumber-java + 7.11.0 + test + + + + io.cucumber + cucumber-spring + 7.11.0 + test + + + + io.cucumber + cucumber-junit + 7.11.0 + test + + + diff --git a/src/main/kotlin/com/coded/spring/ordering/DTO/ProfileDTO.kt b/src/main/kotlin/com/coded/spring/ordering/DTO/ProfileDTO.kt new file mode 100644 index 0000000..f0acc98 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/DTO/ProfileDTO.kt @@ -0,0 +1,7 @@ +package com.coded.spring.ordering.DTO + +data class ProfileRequest( + val firstName: String, + val lastName: String, + val phoneNumber: 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 0b96b90..1ee97d8 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt @@ -27,7 +27,7 @@ class SecurityConfig( fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { http.csrf { it.disable() } .authorizeHttpRequests { - it.requestMatchers("/auth/**","/users/v1/register","/menus/v1/menu","/api-docs").permitAll() + it.requestMatchers("/auth/**","/users/v1/register","/menu/v1/list","/api-docs").permitAll() .anyRequest().authenticated() } .sessionManagement { diff --git a/src/main/kotlin/com/coded/spring/ordering/exceptions/InvalidProfileException.kt b/src/main/kotlin/com/coded/spring/ordering/exceptions/InvalidProfileException.kt new file mode 100644 index 0000000..b985ade --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/exceptions/InvalidProfileException.kt @@ -0,0 +1,3 @@ +package com.coded.spring.ordering.exceptions + +class InvalidProfileException(message: String) : Exception(message) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/menu/MenuController.kt b/src/main/kotlin/com/coded/spring/ordering/menu/MenuController.kt index 1b6f6b5..63b0d8f 100644 --- a/src/main/kotlin/com/coded/spring/ordering/menu/MenuController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/menu/MenuController.kt @@ -7,6 +7,6 @@ import org.springframework.web.bind.annotation.RestController class MenuController( private val menuService: MenuService ) { - @GetMapping("/menus/v1/menu") + @GetMapping("/menu/v1/list") fun getMenu(): List = menuService.getMenu() } \ No newline at end of file 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..c4ff75c --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileController.kt @@ -0,0 +1,43 @@ +package com.coded.spring.ordering.profiles + +import com.coded.spring.ordering.DTO.ProfileRequest +import com.coded.spring.ordering.exceptions.InvalidProfileException +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* +import java.security.Principal + +@RestController +@RequestMapping("/profile") +class ProfileController( + private val profileService: ProfileService +) { + + @PostMapping + fun submitProfile( + @RequestBody request: ProfileRequest, + principal: Principal + ): ResponseEntity { + return try { + profileService.saveProfile(principal.name, request) + ResponseEntity.ok(mapOf("message" to "Profile created successfully.")) + } catch (e: InvalidProfileException) { + ResponseEntity.badRequest().body(mapOf("error" to e.message)) + } catch (e: Exception) { + e.printStackTrace() + ResponseEntity.internalServerError().body(mapOf("error" to "Something went wrong.")) + } + } + + @GetMapping + fun getProfile(principal: Principal): ResponseEntity { + return try { + val profile = profileService.getByUsername(principal.name) + ResponseEntity.ok(profile) + } catch (e: InvalidProfileException) { + ResponseEntity.badRequest().body(mapOf("error" to e.message)) + } catch (e: Exception) { + e.printStackTrace() + ResponseEntity.internalServerError().body(mapOf("error" to "Something went wrong.")) + } + } +} \ No newline at end of file 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..7d06042 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileRepository.kt @@ -0,0 +1,25 @@ +package com.coded.spring.ordering.profiles + +import jakarta.inject.Named +import jakarta.persistence.* +import org.springframework.data.jpa.repository.JpaRepository + +@Named +interface ProfileRepository : JpaRepository { + fun findByUserId(userId: Long): ProfileEntity? +} + +@Entity +@Table(name = "profiles") +data class ProfileEntity( + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, + + val firstName: String = "", + val lastName: String = "", + val phoneNumber: String = "", + val userId: Long = 0 + +) \ 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..27284db --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileService.kt @@ -0,0 +1,69 @@ +package com.coded.spring.ordering.profiles + +import com.coded.spring.ordering.DTO.ProfileRequest +import com.coded.spring.ordering.exceptions.InvalidProfileException +import com.coded.spring.ordering.users.UsersRepository +import org.springframework.security.core.userdetails.UsernameNotFoundException +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +open class ProfileService( + private val profileRepository: ProfileRepository, + private val usersRepository: UsersRepository +) { + + fun isValidName(name: String): Boolean { + return name.matches(Regex("^[A-Za-z]+$")) + } + + fun isValidPhone(phone: String): Boolean { + return phone.matches(Regex("^[0-9]{8}$")) + } + + @Transactional + fun saveProfile(username: String, request: ProfileRequest): ProfileEntity { + val user = usersRepository.findByUsername(username) + ?: throw UsernameNotFoundException("User not found") + + val existingProfile = profileRepository.findByUserId(user.id!!) + if (existingProfile != null) { + throw InvalidProfileException("Profile already exists for this user") + } + + if (!isValidName(request.firstName)) { + throw InvalidProfileException("First name must contain only letters") + } + + if (!isValidName(request.lastName)) { + throw InvalidProfileException("Last name must contain only letters") + } + + if (!isValidPhone(request.phoneNumber)) { + throw InvalidProfileException("Phone number must be exactly 8 digits") + } + + val profile = ProfileEntity( + firstName = request.firstName, + lastName = request.lastName, + phoneNumber = request.phoneNumber, + userId = user.id!! + ) + + return profileRepository.save(profile) + } + + fun getByUsername(username: String): ProfileEntity { + val user = usersRepository.findByUsername(username) + if (user == null) { + throw UsernameNotFoundException("User not found") + } + + val profile = profileRepository.findByUserId(user.id!!) + if (profile == null) { + throw InvalidProfileException("Profile not found for this user") + } + + return profile + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UsersService.kt b/src/main/kotlin/com/coded/spring/ordering/users/UsersService.kt index a0eb3fc..e38cc74 100644 --- a/src/main/kotlin/com/coded/spring/ordering/users/UsersService.kt +++ b/src/main/kotlin/com/coded/spring/ordering/users/UsersService.kt @@ -3,6 +3,7 @@ import com.coded.spring.ordering.DTO.UserRequest import com.coded.spring.ordering.DTO.UserResponse import com.coded.spring.ordering.exceptions.TransferFundsException import jakarta.inject.Named +import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Service const val USERNAME_MIN_LENGTH = 4 @@ -14,6 +15,7 @@ const val PASSWORD_MAX_LENGTH = 30 @Service class UsersService( private val usersRepository: UsersRepository, + private val passwordEncoder: PasswordEncoder ) { fun registerUser(request: UserRequest): UserResponse { @@ -30,6 +32,8 @@ class UsersService( "Password must be between ${PASSWORD_MIN_LENGTH} and ${PASSWORD_MAX_LENGTH} characters") } + val encodedPassword = passwordEncoder.encode(request.password) + val createUser = UserEntity( name = request.name, age = request.age, diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index cc351ec..1643e81 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,9 +1,9 @@ -spring.application.name=Kotlin.SpringbootV2 -server.port = 8080 - -spring.datasource.url=jdbc:postgresql://localhost:5432/myHelloDatabase -spring.datasource.username=postgres -spring.datasource.password=yosaka -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect - -springdoc.api-docs.path=/api-docs +#spring.application.name=Kotlin.SpringbootV2 +#server.port = 8080 +# +#spring.datasource.url=jdbc:postgresql://localhost:5432/myHelloDatabase +#spring.datasource.username=postgres +#spring.datasource.password=yosaka +#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +# +#springdoc.api-docs.path=/api-docs diff --git a/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt b/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt index b2e2320..effb76e 100644 --- a/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt +++ b/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt @@ -1,13 +1,17 @@ package com.coded.spring.ordering +import io.cucumber.spring.CucumberContextConfiguration import org.junit.jupiter.api.Test import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ActiveProfiles -@SpringBootTest -class ApplicationTests { +@CucumberContextConfiguration +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") +class OnlineOrderingApplicationTests { @Test fun contextLoads() { - } + } } diff --git a/src/test/kotlin/com/coded/spring/ordering/config/TestHooks.kt b/src/test/kotlin/com/coded/spring/ordering/config/TestHooks.kt new file mode 100644 index 0000000..433c778 --- /dev/null +++ b/src/test/kotlin/com/coded/spring/ordering/config/TestHooks.kt @@ -0,0 +1,71 @@ +package com.coded.spring.ordering.config + +import com.coded.spring.ordering.utils.GlobalToken +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import io.cucumber.java.Before +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.http.* + +class TestHooks { + + @Autowired + lateinit var restTemplate: TestRestTemplate + + companion object { + private var isTokenGenerated = false + } + + @Before + fun generateJwtToken() { + if (isTokenGenerated) return + + println("πŸš€ Generating Global JWT Token...") + + val headers = HttpHeaders() + headers.contentType = MediaType.APPLICATION_JSON + + // Register + val registrationPayload = """ + { + "name": "Global User", + "age": 25, + "username": "GlobalUser", + "password": "password123" + } + """.trimIndent() + val registerRequest = HttpEntity(registrationPayload, headers) + var regResponse =restTemplate.postForEntity("/users/v1/register", registerRequest, String::class.java) + + println(regResponse) + // Login + val loginPayload = """ + { + "username": "GlobalUser", + "password": "password123" + } + """.trimIndent() + val loginRequest = HttpEntity(loginPayload, headers) + val response = restTemplate.postForEntity("/auth/login", loginRequest, String::class.java) + + println("πŸ”Ž Login Response Body: ${response.body}") + + // Extract JWT token from response JSON + val responseBody = response.body + val token = if (!responseBody.isNullOrBlank()) { + val mapper = jacksonObjectMapper() + try { + mapper.readTree(responseBody).get("token")?.asText() + } catch (e: Exception) { + println("❌ Failed to extract token: ${e.message}") + null + } + } else { + null + } + + GlobalToken.jwtToken = token + println("βœ… Global JWT Token Generated: $token") + isTokenGenerated = true + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/coded/spring/ordering/steps/GetProfileSteps.kt b/src/test/kotlin/com/coded/spring/ordering/steps/GetProfileSteps.kt new file mode 100644 index 0000000..5940daa --- /dev/null +++ b/src/test/kotlin/com/coded/spring/ordering/steps/GetProfileSteps.kt @@ -0,0 +1,50 @@ +package com.coded.spring.ordering.steps + +import com.coded.spring.ordering.DTO.ProfileRequest +import com.coded.spring.ordering.authentication.jwt.JwtService +import io.cucumber.java.en.Given +import io.cucumber.java.en.Then +import io.cucumber.java.en.When +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.http.* +import kotlin.test.assertEquals + +class GetProfileSteps { + + @Autowired + lateinit var restTemplate: TestRestTemplate + + @Autowired + lateinit var jwtService: JwtService + + lateinit var headers: HttpHeaders + lateinit var response: ResponseEntity + + @Given("A profile is already created for user {string}") + fun a_profile_is_already_created(username: String) { + headers = HttpHeaders() + headers.contentType = MediaType.APPLICATION_JSON + headers.setBearerAuth(jwtService.generateToken(username)) + + val profileRequest = ProfileRequest( + firstName = "John", + lastName = "Doe", + phoneNumber = "12345678" + ) + + val requestEntity = HttpEntity(profileRequest, headers) + restTemplate.postForEntity("/profile", requestEntity, String::class.java) + } + + @When("I send an authenticated GET request to {string} to retrieve profile") + fun i_send_authenticated_get_request(url: String) { + val entity = HttpEntity(headers) + response = restTemplate.exchange(url, HttpMethod.GET, entity, String::class.java) + } + + @Then("I should receive a 200 response for retrieving profile") + fun i_should_receive_200_response_for_retrieving_profile() { + assertEquals(200, response.statusCode.value()) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/coded/spring/ordering/steps/ListItemsStepDefinitions.kt b/src/test/kotlin/com/coded/spring/ordering/steps/ListItemsStepDefinitions.kt new file mode 100644 index 0000000..4afbfac --- /dev/null +++ b/src/test/kotlin/com/coded/spring/ordering/steps/ListItemsStepDefinitions.kt @@ -0,0 +1,34 @@ +package com.coded.spring.ordering.steps + +import com.coded.spring.ordering.authentication.jwt.JwtService +import io.cucumber.java.en.Then +import io.cucumber.java.en.When +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.http.* +import kotlin.test.assertEquals + +class ListItemsSteps { + + @Autowired + lateinit var restTemplate: TestRestTemplate + + @Autowired + lateinit var jwtService: JwtService + + lateinit var response: ResponseEntity + + @When("I send a GET request to {string}") + fun i_send_a_get_request_to(url: String) { + val headers = HttpHeaders() + headers.setBearerAuth(jwtService.generateToken("GlobalUser")) // βœ… JWT Token injected + val entity = HttpEntity(headers) + + response = restTemplate.exchange(url, HttpMethod.GET, entity, String::class.java) + } + + @Then("I should receive a {int} response for listing items") + fun i_should_receive_a_response_for_listing_items(status: Int) { + assertEquals(status, response.statusCode.value()) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/coded/spring/ordering/steps/ListOrdersStepDefinitions.kt b/src/test/kotlin/com/coded/spring/ordering/steps/ListOrdersStepDefinitions.kt new file mode 100644 index 0000000..7eb47fe --- /dev/null +++ b/src/test/kotlin/com/coded/spring/ordering/steps/ListOrdersStepDefinitions.kt @@ -0,0 +1,34 @@ +package com.coded.spring.ordering.steps + +import com.coded.spring.ordering.authentication.jwt.JwtService +import io.cucumber.java.en.Then +import io.cucumber.java.en.When +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.http.* +import kotlin.test.assertEquals + +class ListOrdersStepDefinitions { + + @Autowired + lateinit var jwtService: JwtService + + @Autowired + lateinit var restTemplate: TestRestTemplate + + lateinit var getResponse: ResponseEntity + + @When("I request my orders from {string}") + fun i_request_orders(url: String) { + val headers = HttpHeaders() + headers.setBearerAuth(jwtService.generateToken("GlobalUser")) + val entity = HttpEntity(headers) + + getResponse = restTemplate.exchange(url, HttpMethod.GET, entity, String::class.java) + } + + @Then("I should receive a 200 response with my orders") + fun i_should_receive_orders_response() { + assertEquals(200, getResponse.statusCode.value()) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/coded/spring/ordering/steps/ListUsersStepDefinitions.kt b/src/test/kotlin/com/coded/spring/ordering/steps/ListUsersStepDefinitions.kt new file mode 100644 index 0000000..bf16063 --- /dev/null +++ b/src/test/kotlin/com/coded/spring/ordering/steps/ListUsersStepDefinitions.kt @@ -0,0 +1,34 @@ +package com.coded.spring.ordering.steps + +import com.coded.spring.ordering.authentication.jwt.JwtService +import io.cucumber.java.en.Then +import io.cucumber.java.en.When +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.http.* +import kotlin.test.assertEquals + +class ListUsersStepDefinitions { + + @Autowired + lateinit var jwtService: JwtService + + @Autowired + lateinit var restTemplate: TestRestTemplate + lateinit var response: ResponseEntity + + @When("I send an authenticated GET request to {string}") + fun i_send_authenticated_get_request(url: String) { + val headers = HttpHeaders() + //GlobalToken.jwtToken?.let { headers.setBearerAuth(it) } + headers.setBearerAuth(jwtService.generateToken("GlobalUser")) + val entity = HttpEntity(headers) + + response = restTemplate.exchange(url, HttpMethod.GET, entity, String::class.java) + } + + @Then("I should receive a 200 response for listing users") + fun i_should_receive_200_response_for_listing_users() { + assertEquals(200, response.statusCode.value()) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/coded/spring/ordering/steps/MenuStepDefinitions.kt b/src/test/kotlin/com/coded/spring/ordering/steps/MenuStepDefinitions.kt new file mode 100644 index 0000000..eeb42a1 --- /dev/null +++ b/src/test/kotlin/com/coded/spring/ordering/steps/MenuStepDefinitions.kt @@ -0,0 +1,31 @@ +package com.coded.spring.ordering.steps + +import io.cucumber.java.en.Then +import io.cucumber.java.en.When +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.ResponseEntity +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class MenuStepDefinitions { + + @Autowired + lateinit var restTemplate: TestRestTemplate + + private lateinit var response: ResponseEntity + + @When("I request the menu") + fun iRequestTheMenu() { + response = restTemplate.getForEntity("/menu/v1/list", String::class.java) + } + + @Then("I should receive a list of menu items") + fun iShouldReceiveMenuItems() { + assertEquals(200, response.statusCode.value()) + assertNotNull(response.body) + println("βœ… Menu Response: ${response.body}") + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/coded/spring/ordering/steps/SubmitItemSteps.kt b/src/test/kotlin/com/coded/spring/ordering/steps/SubmitItemSteps.kt new file mode 100644 index 0000000..a76adb3 --- /dev/null +++ b/src/test/kotlin/com/coded/spring/ordering/steps/SubmitItemSteps.kt @@ -0,0 +1,48 @@ +package com.coded.spring.ordering.steps + +import com.coded.spring.ordering.DTO.SubmitItemRequest +import io.cucumber.java.en.Given +import io.cucumber.java.en.Then +import io.cucumber.java.en.When +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.http.* +import kotlin.test.assertEquals +import com.coded.spring.ordering.authentication.jwt.JwtService + + +class SubmitItemSteps { + + @Autowired + lateinit var restTemplate: TestRestTemplate + + @Autowired + lateinit var jwtService: JwtService + + lateinit var requestEntity: HttpEntity + lateinit var response: ResponseEntity + + @Given("I have an item with name {string}, quantity {int}, note {string}, and price {double}") + fun i_have_an_item(name: String, quantity: Int, note: String, price: Double) { + val itemRequest = SubmitItemRequest( + name = name, + quantity = quantity.toLong(), + note = note, + price = price + ) + val headers = HttpHeaders() + headers.contentType = MediaType.APPLICATION_JSON + headers.setBearerAuth(jwtService.generateToken("GlobalUser")) // βœ… JWT Injection here + requestEntity = HttpEntity(itemRequest, headers) + } + + @When("I submit an item to endpoint {string}") + fun i_send_post_request(url: String) { + response = restTemplate.postForEntity(url, requestEntity, String::class.java) + } + + @Then("I should receive a 200 response for submitting item") + fun i_should_receive_success() { + assertEquals(200, response.statusCode.value()) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/coded/spring/ordering/steps/SubmitOrderStepDefinitions.kt b/src/test/kotlin/com/coded/spring/ordering/steps/SubmitOrderStepDefinitions.kt new file mode 100644 index 0000000..b282950 --- /dev/null +++ b/src/test/kotlin/com/coded/spring/ordering/steps/SubmitOrderStepDefinitions.kt @@ -0,0 +1,41 @@ +package com.coded.spring.ordering.steps + +import com.coded.spring.ordering.authentication.jwt.JwtService +import io.cucumber.java.en.Then +import io.cucumber.java.en.When +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.http.* +import kotlin.test.assertEquals + +class SubmitOrderStepDefinitions { + + @Autowired + lateinit var jwtService: JwtService + + @Autowired + lateinit var restTemplate: TestRestTemplate + + lateinit var postResponse: ResponseEntity + + @When("I submit an order with itemIds to {string}") + fun i_submit_order(url: String) { + val headers = HttpHeaders() + headers.contentType = MediaType.APPLICATION_JSON + headers.setBearerAuth(jwtService.generateToken("GlobalUser")) + + val orderPayload = """ + { + "itemIds": [1, 2] + } + """.trimIndent() + + val request = HttpEntity(orderPayload, headers) + postResponse = restTemplate.postForEntity(url, request, String::class.java) + } + + @Then("I should receive a 200 response for order submission") + fun i_should_receive_200_response_for_submission() { + assertEquals(200, postResponse.statusCode.value()) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/coded/spring/ordering/steps/SubmitProfileSteps.kt b/src/test/kotlin/com/coded/spring/ordering/steps/SubmitProfileSteps.kt new file mode 100644 index 0000000..bd53c3a --- /dev/null +++ b/src/test/kotlin/com/coded/spring/ordering/steps/SubmitProfileSteps.kt @@ -0,0 +1,48 @@ +package com.coded.spring.ordering.steps + +import com.coded.spring.ordering.DTO.ProfileRequest +import com.coded.spring.ordering.authentication.jwt.JwtService +import io.cucumber.java.en.Given +import io.cucumber.java.en.Then +import io.cucumber.java.en.When +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.http.* +import kotlin.test.assertEquals + +class SubmitProfileSteps { + + @Autowired + lateinit var jwtService: JwtService + + @Autowired + lateinit var restTemplate: TestRestTemplate + + lateinit var requestEntity: HttpEntity + lateinit var response: ResponseEntity + + @Given("I have a profile with first name {string}, last name {string}, and phone number {string}") + fun i_have_a_profile(firstName: String, lastName: String, phoneNumber: String) { + val profileRequest = ProfileRequest( + firstName = firstName, + lastName = lastName, + phoneNumber = phoneNumber + ) + + val headers = HttpHeaders() + headers.contentType = MediaType.APPLICATION_JSON + headers.setBearerAuth(jwtService.generateToken("GlobalUser")) + + requestEntity = HttpEntity(profileRequest, headers) + } + + @When("I submit the profile to {string}") + fun i_submit_the_profile(url: String) { + response = restTemplate.postForEntity(url, requestEntity, String::class.java) + } + + @Then("I should receive a 200 response for submitting profile") + fun i_should_receive_200_response_for_profile() { + assertEquals(200, response.statusCode.value()) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/coded/spring/ordering/steps/UserRegistrationSteps.kt b/src/test/kotlin/com/coded/spring/ordering/steps/UserRegistrationSteps.kt new file mode 100644 index 0000000..18ff93d --- /dev/null +++ b/src/test/kotlin/com/coded/spring/ordering/steps/UserRegistrationSteps.kt @@ -0,0 +1,45 @@ +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.* +import kotlin.test.assertEquals + +class UserRegistrationSteps { + + @Autowired + lateinit var restTemplate: TestRestTemplate + + private lateinit var response: ResponseEntity + private lateinit var requestEntity: HttpEntity + + @Given("I have a user with name {string}, age {int}, username {string}, and password {string}") + fun i_have_a_user_payload(name: String, age: Int, username: String, password: String) { + val jsonPayload = """ + { + "name": "$name", + "age": $age, + "username": "$username", + "password": "$password" + } + """.trimIndent() + + val headers = HttpHeaders() + headers.contentType = MediaType.APPLICATION_JSON + requestEntity = HttpEntity(jsonPayload, headers) + } + + @When("I send a POST request to {string}") + fun i_send_post_request(url: String) { + response = restTemplate.postForEntity(url, requestEntity, String::class.java) + } + + @Then("I should receive a {int} response") + fun i_should_receive_http_code(httpCode: Int) { + assertEquals(httpCode, response.statusCode.value()) + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/coded/spring/ordering/utils/GlobalToken.kt b/src/test/kotlin/com/coded/spring/ordering/utils/GlobalToken.kt new file mode 100644 index 0000000..1aaeae7 --- /dev/null +++ b/src/test/kotlin/com/coded/spring/ordering/utils/GlobalToken.kt @@ -0,0 +1,5 @@ +package com.coded.spring.ordering.utils + +object GlobalToken { + var jwtToken: String? = null +} \ No newline at end of file diff --git a/src/test/kotlin/resources/application-test.properties b/src/test/kotlin/resources/application-test.properties new file mode 100644 index 0000000..8eccbab --- /dev/null +++ b/src/test/kotlin/resources/application-test.properties @@ -0,0 +1,14 @@ +spring.application.name=Kotlin.SpringbootV2 + +# H2 In-Memory DB for Testing +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 + +# Enable H2 Console for Debugging +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console \ No newline at end of file diff --git a/src/test/kotlin/resources/feature/get_profile.feature b/src/test/kotlin/resources/feature/get_profile.feature new file mode 100644 index 0000000..0387ba9 --- /dev/null +++ b/src/test/kotlin/resources/feature/get_profile.feature @@ -0,0 +1,6 @@ +Feature: Get Profile + + Scenario: Successfully retrieve profile + Given A profile is already created for user "GlobalUser" + When I send an authenticated GET request to "/profile" to retrieve profile + Then I should receive a 200 response for retrieving profile \ No newline at end of file diff --git a/src/test/kotlin/resources/feature/list_items.feature b/src/test/kotlin/resources/feature/list_items.feature new file mode 100644 index 0000000..8bb96a2 --- /dev/null +++ b/src/test/kotlin/resources/feature/list_items.feature @@ -0,0 +1,4 @@ +Feature: List Items + Scenario: Successfully retrieve items + When I send a GET request to "/listItems" + Then I should receive a 200 response for listing items \ No newline at end of file diff --git a/src/test/kotlin/resources/feature/list_orders.feature b/src/test/kotlin/resources/feature/list_orders.feature new file mode 100644 index 0000000..866542e --- /dev/null +++ b/src/test/kotlin/resources/feature/list_orders.feature @@ -0,0 +1,5 @@ +Feature: List Orders + + Scenario: Successfully retrieve my orders + When I request my orders from "/orders/v1" + Then I should receive a 200 response with my orders \ No newline at end of file diff --git a/src/test/kotlin/resources/feature/list_users.feature b/src/test/kotlin/resources/feature/list_users.feature new file mode 100644 index 0000000..860d85a --- /dev/null +++ b/src/test/kotlin/resources/feature/list_users.feature @@ -0,0 +1,5 @@ +Feature: List Users with Authentication + + Scenario: Successfully list users using Global JWT Token + When I send an authenticated GET request to "/users/v1/list" + Then I should receive a 200 response for listing users \ No newline at end of file diff --git a/src/test/kotlin/resources/feature/menu.feature b/src/test/kotlin/resources/feature/menu.feature new file mode 100644 index 0000000..3d8f488 --- /dev/null +++ b/src/test/kotlin/resources/feature/menu.feature @@ -0,0 +1,5 @@ +Feature: Menu Retrieval + + Scenario: Get all menu items + When I request the menu + Then I should receive a list of menu items \ No newline at end of file diff --git a/src/test/kotlin/resources/feature/submit_item.feature b/src/test/kotlin/resources/feature/submit_item.feature new file mode 100644 index 0000000..dbbcd1a --- /dev/null +++ b/src/test/kotlin/resources/feature/submit_item.feature @@ -0,0 +1,5 @@ +Feature: Submit Item + Scenario: Successfully submit a new item + Given I have an item with name "Burger", quantity 2, note "No onions", and price 3.5 + When I submit an item to endpoint "/submitItems" + Then I should receive a 200 response for submitting item \ No newline at end of file diff --git a/src/test/kotlin/resources/feature/submit_order.feature b/src/test/kotlin/resources/feature/submit_order.feature new file mode 100644 index 0000000..ccb71e1 --- /dev/null +++ b/src/test/kotlin/resources/feature/submit_order.feature @@ -0,0 +1,5 @@ +Feature: Submit Order + + Scenario: Successfully submit an order with item IDs + When I submit an order with itemIds to "/orders/v1" + Then I should receive a 200 response for order submission \ No newline at end of file diff --git a/src/test/kotlin/resources/feature/submit_profile.feature b/src/test/kotlin/resources/feature/submit_profile.feature new file mode 100644 index 0000000..93d435f --- /dev/null +++ b/src/test/kotlin/resources/feature/submit_profile.feature @@ -0,0 +1,6 @@ +Feature: Submit Profile + + Scenario: Successfully submit a profile + Given I have a profile with first name "John", last name "Doe", and phone number "12345678" + When I submit the profile to "/profile" + Then I should receive a 200 response for submitting profile \ No newline at end of file diff --git a/src/test/kotlin/resources/feature/user_register.feature b/src/test/kotlin/resources/feature/user_register.feature new file mode 100644 index 0000000..8508fc1 --- /dev/null +++ b/src/test/kotlin/resources/feature/user_register.feature @@ -0,0 +1,10 @@ +Feature: User Registration + + Scenario: Successful user registration + Given I have a user with name "Ali", age 23, username "Ali123", and password "password123" + When I send a POST request to "/users/v1/register" + Then I should receive a 200 response + +# Scenario: List all users +# When I send a GET request to "/users/v1/list" +# Then I should receive a 200 response \ No newline at end of file From 1153b5ec6724d9fd83334b9106b55403f49c07b0 Mon Sep 17 00:00:00 2001 From: Yousef Alothman Date: Tue, 29 Apr 2025 20:29:29 +0300 Subject: [PATCH 12/18] Online Ordering - Configuration - Added environment-based config for `company.name` and `festive-mode` - WelcomeController now returns a dynamic message based on company name and festive flag - ItemsService returns discounted prices (20% off) when `festive-mode=true` - Enabled configurable logging level via `logging.level.root` in application properties - Updated application-test.properties for test scenarios --- pom.xml | 10 +++---- .../ordering/authentication/SecurityConfig.kt | 2 +- .../helloworld/HelloWorldController.kt | 21 ++++++++++++++ .../spring/ordering/items/ItemsService.kt | 8 ++++-- src/main/resources/application.properties | 20 +++++++------ .../resources/application-test.properties | 28 +++++++++---------- 6 files changed, 57 insertions(+), 32 deletions(-) create mode 100644 src/main/kotlin/com/coded/spring/ordering/helloworld/HelloWorldController.kt diff --git a/pom.xml b/pom.xml index b71c097..51f15c9 100644 --- a/pom.xml +++ b/pom.xml @@ -83,11 +83,11 @@ 2.0.1 - - - - - + + org.postgresql + postgresql + compile + org.springframework.boot 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 1ee97d8..82aefea 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt @@ -27,7 +27,7 @@ class SecurityConfig( fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { http.csrf { it.disable() } .authorizeHttpRequests { - it.requestMatchers("/auth/**","/users/v1/register","/menu/v1/list","/api-docs").permitAll() + it.requestMatchers("/auth/**","/users/v1/register","/menu/v1/list","/api-docs","/hello").permitAll() .anyRequest().authenticated() } .sessionManagement { diff --git a/src/main/kotlin/com/coded/spring/ordering/helloworld/HelloWorldController.kt b/src/main/kotlin/com/coded/spring/ordering/helloworld/HelloWorldController.kt new file mode 100644 index 0000000..ac3ecb7 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/helloworld/HelloWorldController.kt @@ -0,0 +1,21 @@ +package com.coded.spring.ordering.helloworld + +import org.springframework.beans.factory.annotation.Value +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +class WelcomeController( + @Value("\${company.name}") val companyName: String, + @Value("\${festive-mode:false}") val festiveMode: Boolean +) { + + @GetMapping("/hello") + fun sayHello(): String { + return if (festiveMode) { + "πŸŽ‰ Eidkom Mubarak from $companyName!" + } else { + "Welcome to Online Ordering by $companyName" + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/items/ItemsService.kt b/src/main/kotlin/com/coded/spring/ordering/items/ItemsService.kt index d0bcbb7..11a0a9e 100644 --- a/src/main/kotlin/com/coded/spring/ordering/items/ItemsService.kt +++ b/src/main/kotlin/com/coded/spring/ordering/items/ItemsService.kt @@ -2,12 +2,14 @@ package com.coded.spring.ordering.items import com.coded.spring.ordering.DTO.Item import com.coded.spring.ordering.DTO.SubmitItemRequest import jakarta.inject.Named -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody +import org.springframework.beans.factory.annotation.Value @Named class ItemsService( private val itemsRepository: ItemsRepository, + @Value("\${festive-mode:false}") + val festiveMode: Boolean + ) { fun listItems(): List = itemsRepository.findAll().map { entity -> Item( @@ -16,7 +18,7 @@ class ItemsService( name = entity.name, quantity = entity.quantity, note = entity.note, - price = entity.price + price = if (festiveMode) entity.price?.times(0.8) else entity.price ) } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 1643e81..bf02763 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,9 +1,11 @@ -#spring.application.name=Kotlin.SpringbootV2 -#server.port = 8080 -# -#spring.datasource.url=jdbc:postgresql://localhost:5432/myHelloDatabase -#spring.datasource.username=postgres -#spring.datasource.password=yosaka -#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect -# -#springdoc.api-docs.path=/api-docs +spring.application.name=Kotlin.SpringbootV2 +server.port = 8080 + +spring.datasource.url=jdbc:postgresql://localhost:5432/myHelloDatabase +spring.datasource.username=postgres +spring.datasource.password=yosaka +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect + +springdoc.api-docs.path=/api-docs + +company.name=FreshEats diff --git a/src/test/kotlin/resources/application-test.properties b/src/test/kotlin/resources/application-test.properties index 8eccbab..15722c5 100644 --- a/src/test/kotlin/resources/application-test.properties +++ b/src/test/kotlin/resources/application-test.properties @@ -1,14 +1,14 @@ -spring.application.name=Kotlin.SpringbootV2 - -# H2 In-Memory DB for Testing -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 - -# Enable H2 Console for Debugging -spring.h2.console.enabled=true -spring.h2.console.path=/h2-console \ No newline at end of file +#spring.application.name=Kotlin.SpringbootV2 +# +## H2 In-Memory DB for Testing +#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 +# +## Enable H2 Console for Debugging +#spring.h2.console.enabled=true +#spring.h2.console.path=/h2-console \ No newline at end of file From dc13e85fe33ef9ce961c0994c2ceed33bd746fbe Mon Sep 17 00:00:00 2001 From: Yousef Alothman Date: Wed, 30 Apr 2025 20:32:51 +0300 Subject: [PATCH 13/18] Online Ordering - Setup Swagger Add Swagger OpenAPI documentation and annotations - Integrated springdoc-openapi-starter-webmvc-api dependency - Configured Swagger API docs path in application.properties - Annotated AuthenticationController with @Operation and @ApiResponse --- .../jwt/AuthenticationController.kt | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/AuthenticationController.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/AuthenticationController.kt index e644be9..39ba8c7 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/AuthenticationController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/AuthenticationController.kt @@ -1,10 +1,17 @@ package com.coded.spring.ordering.authentication.jwt +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import io.swagger.v3.oas.annotations.tags.Tag 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.* +@Tag(name = "AUTHENTICATION", description = "Endpoints related to user login and token generation") @RestController @RequestMapping("/auth") class AuthenticationController( @@ -13,6 +20,25 @@ class AuthenticationController( private val jwtService: JwtService ) { + @Operation( + summary = "Login and generate JWT token", + description = "Accepts username and password and returns a JWT token upon successful authentication", + tags = ["AUTHENTICATION"] + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "Successfully authenticated", + content = [Content(schema = Schema(implementation = AuthenticationResponse::class))] + ), + ApiResponse( + responseCode = "400", + description = "Invalid username or password", + content = [Content()] + ) + ] + ) @PostMapping("/login") fun login(@RequestBody authRequest: AuthenticationRequest): AuthenticationResponse { val authToken = UsernamePasswordAuthenticationToken(authRequest.username, authRequest.password) From 2d58cc3b309c04d51f1e3530be27c10bcbea9d7c Mon Sep 17 00:00:00 2001 From: Yousef Alothman Date: Wed, 30 Apr 2025 20:33:00 +0300 Subject: [PATCH 14/18] Online Ordering - Setup Swagger Add Swagger OpenAPI documentation and annotations - Integrated springdoc-openapi-starter-webmvc-api dependency - Configured Swagger API docs path in application.properties - Annotated AuthenticationController with @Operation and @ApiResponse --- swaggerJson/online-ordering-swagger-02.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 swaggerJson/online-ordering-swagger-02.json diff --git a/swaggerJson/online-ordering-swagger-02.json b/swaggerJson/online-ordering-swagger-02.json new file mode 100644 index 0000000..8fadecd --- /dev/null +++ b/swaggerJson/online-ordering-swagger-02.json @@ -0,0 +1 @@ +{"openapi":"3.0.1","info":{"title":"OpenAPI definition","version":"v0"},"servers":[{"url":"http://localhost:8080","description":"Generated server url"}],"tags":[{"name":"AUTHENTICATION","description":"Endpoints related to user login and token generation"}],"paths":{"/users/v1/register":{"post":{"tags":["users-controller"],"operationId":"registerUser","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/submitItems":{"post":{"tags":["items-controller"],"operationId":"submitItem","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitItemRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ItemEntity"}}}}}}},"/profile":{"get":{"tags":["profile-controller"],"operationId":"getProfile","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}},"post":{"tags":["profile-controller"],"operationId":"submitProfile","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/orders/v1":{"get":{"tags":["order-controller"],"operationId":"listMyOrders","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Order"}}}}}}},"post":{"tags":["order-controller"],"operationId":"submitOrder","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitOrderRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"string"}}}}}}},"/auth/login":{"post":{"tags":["AUTHENTICATION"],"summary":"Login and generate JWT token","description":"Accepts username and password and returns a JWT token upon successful authentication","operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthenticationRequest"}}},"required":true},"responses":{"200":{"description":"Successfully authenticated","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthenticationResponse"}}}},"400":{"description":"Invalid username or password"}}}},"/users/v1/list":{"get":{"tags":["users-controller"],"operationId":"users","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserRequest"}}}}}}}},"/menu/v1/list":{"get":{"tags":["menu-controller"],"operationId":"getMenu","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MenuEntity"}}}}}}}},"/listItems":{"get":{"tags":["items-controller"],"operationId":"listItems","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Item"}}}}}}}},"/hello":{"get":{"tags":["welcome-controller"],"operationId":"sayHello","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"string"}}}}}}}},"components":{"schemas":{"UserRequest":{"required":["age","name","password","username"],"type":"object","properties":{"name":{"type":"string"},"age":{"type":"integer","format":"int32"},"username":{"type":"string"},"password":{"type":"string"}}},"SubmitItemRequest":{"required":["name","price","quantity"],"type":"object","properties":{"name":{"type":"string"},"quantity":{"type":"integer","format":"int64"},"note":{"type":"string"},"price":{"type":"number","format":"double"}}},"ItemEntity":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"quantity":{"type":"integer","format":"int64"},"note":{"type":"string"},"price":{"type":"number","format":"double"},"order":{"$ref":"#/components/schemas/OrderEntity"}}},"OrderEntity":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"user":{"$ref":"#/components/schemas/UserEntity"},"items":{"type":"array","items":{"$ref":"#/components/schemas/ItemEntity"}}}},"UserEntity":{"required":["age","name","password","username"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"age":{"type":"integer","format":"int32"},"username":{"type":"string"},"password":{"type":"string"}}},"ProfileRequest":{"required":["firstName","lastName","phoneNumber"],"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"phoneNumber":{"type":"string"}}},"SubmitOrderRequest":{"required":["itemIds"],"type":"object","properties":{"itemIds":{"type":"array","items":{"type":"integer","format":"int64"}}}},"AuthenticationRequest":{"required":["password","username"],"type":"object","properties":{"username":{"type":"string"},"password":{"type":"string"}}},"AuthenticationResponse":{"required":["token"],"type":"object","properties":{"token":{"type":"string"}}},"Item":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"order_id":{"type":"integer","format":"int64"},"name":{"type":"string"},"quantity":{"type":"integer","format":"int64"},"note":{"type":"string"},"price":{"type":"number","format":"double"}}},"Order":{"required":["items"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"user_id":{"type":"integer","format":"int64"},"items":{"type":"array","items":{"$ref":"#/components/schemas/Item"}}}},"MenuEntity":{"required":["description","name","price"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"description":{"type":"string"},"price":{"type":"number","format":"double"}}}}}} \ No newline at end of file From ca69355134089b41a247534681fad73d39145131 Mon Sep 17 00:00:00 2001 From: Yousef Alothman Date: Wed, 30 Apr 2025 20:47:20 +0300 Subject: [PATCH 15/18] Online Ordering - Setup Swagger(Updated) included the forgotten files to complete the task --- .../helloworld/HelloWorldController.kt | 11 +++++++ .../spring/ordering/items/ItemsController.kt | 25 ++++++++++++--- .../spring/ordering/menu/MenuController.kt | 16 ++++++++++ .../spring/ordering/orders/OrderController.kt | 31 ++++++++++++++----- .../ordering/profiles/ProfileController.kt | 21 +++++++++++++ 5 files changed, 91 insertions(+), 13 deletions(-) diff --git a/src/main/kotlin/com/coded/spring/ordering/helloworld/HelloWorldController.kt b/src/main/kotlin/com/coded/spring/ordering/helloworld/HelloWorldController.kt index ac3ecb7..4fa91e2 100644 --- a/src/main/kotlin/com/coded/spring/ordering/helloworld/HelloWorldController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/helloworld/HelloWorldController.kt @@ -1,16 +1,27 @@ package com.coded.spring.ordering.helloworld +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.beans.factory.annotation.Value import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController @RestController +@Tag(name = "HOME_PAGE", description = "General welcome and homepage information") class WelcomeController( @Value("\${company.name}") val companyName: String, @Value("\${festive-mode:false}") val festiveMode: Boolean ) { @GetMapping("/hello") + @Operation(summary = "Display welcome message", description = "Returns a greeting message based on festive mode") + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "Welcome message returned successfully") + ] + ) fun sayHello(): String { return if (festiveMode) { "πŸŽ‰ Eidkom Mubarak from $companyName!" diff --git a/src/main/kotlin/com/coded/spring/ordering/items/ItemsController.kt b/src/main/kotlin/com/coded/spring/ordering/items/ItemsController.kt index 61e20f4..2b7bf9b 100644 --- a/src/main/kotlin/com/coded/spring/ordering/items/ItemsController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/items/ItemsController.kt @@ -2,22 +2,37 @@ package com.coded.spring.ordering.items import com.coded.spring.ordering.DTO.Item import com.coded.spring.ordering.DTO.SubmitItemRequest +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.web.bind.annotation.* @RestController - - +@Tag(name = "ORDERING", description = "Endpoints related to managing menu items") class ItemsController( - private val itemsService: ItemsService - ) { + @GetMapping("/listItems") + @Operation(summary = "List all menu items", description = "Returns a list of all available items") + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "Items listed successfully"), + ApiResponse(responseCode = "500", description = "Server error while retrieving items") + ] + ) fun listItems(): List = itemsService.listItems() @PostMapping("/submitItems") + @Operation(summary = "Submit a new menu item", description = "Allows an admin to add a new item to the menu") + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "Item submitted successfully"), + ApiResponse(responseCode = "400", description = "Invalid item data") + ] + ) fun submitItem(@RequestBody request: SubmitItemRequest): ItemEntity { return itemsService.submitItem(request) } - } \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/menu/MenuController.kt b/src/main/kotlin/com/coded/spring/ordering/menu/MenuController.kt index 63b0d8f..afae35a 100644 --- a/src/main/kotlin/com/coded/spring/ordering/menu/MenuController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/menu/MenuController.kt @@ -1,12 +1,28 @@ package com.coded.spring.ordering.menu +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController @RestController +@Tag(name = "HOME_PAGE", description = "Endpoints related to displaying the homepage menu") class MenuController( private val menuService: MenuService ) { + @GetMapping("/menu/v1/list") + @Operation( + summary = "List all menu items for homepage", + description = "Returns all menu entries including discounts if active" + ) + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "Menu listed successfully"), + ApiResponse(responseCode = "500", description = "Server error while fetching menu") + ] + ) fun getMenu(): List = menuService.getMenu() } \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OrderController.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OrderController.kt index c837497..84ab007 100644 --- a/src/main/kotlin/com/coded/spring/ordering/orders/OrderController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OrderController.kt @@ -2,34 +2,49 @@ package com.coded.spring.ordering.orders import com.coded.spring.ordering.DTO.Order import com.coded.spring.ordering.DTO.SubmitOrderRequest +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* import java.security.Principal - @RestController @RequestMapping("/orders/v1") +@Tag(name = "ORDERING", description = "Endpoints related to order submission and retrieval") class OrderController( val ordersRepository: OrdersRepository, private val ordersService: OrdersService - -){ +) { @PostMapping + @Operation(summary = "Submit a new order", description = "Submits a new order for the logged-in user") + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "Order submitted successfully"), + ApiResponse(responseCode = "400", description = "Invalid request format"), + ApiResponse(responseCode = "403", description = "Unauthorized access") + ] + ) fun submitOrder( @RequestBody request: SubmitOrderRequest, principal: Principal ): ResponseEntity { - val username = principal.name // logged-in user + val username = principal.name ordersService.submitOrder(username, request.itemIds) return ResponseEntity.ok("Order submitted successfully.") } @GetMapping + @Operation(summary = "List user's orders", description = "Lists all orders submitted by the logged-in user") + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "Orders retrieved successfully"), + ApiResponse(responseCode = "403", description = "Unauthorized access") + ] + ) fun listMyOrders(principal: Principal): List { return ordersService.listOrdersForUser(principal.name) } - - -} - +} \ No newline at end of file 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 c4ff75c..6c9d22c 100644 --- a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileController.kt @@ -2,17 +2,30 @@ package com.coded.spring.ordering.profiles import com.coded.spring.ordering.DTO.ProfileRequest import com.coded.spring.ordering.exceptions.InvalidProfileException +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* import java.security.Principal @RestController @RequestMapping("/profile") +@Tag(name = "PROFILE", description = "Endpoints for managing user profiles") class ProfileController( private val profileService: ProfileService ) { @PostMapping + @Operation(summary = "Submit user profile", description = "Creates a new profile for the logged-in user") + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "Profile created successfully"), + ApiResponse(responseCode = "400", description = "Invalid profile data or already exists"), + ApiResponse(responseCode = "500", description = "Internal server error") + ] + ) fun submitProfile( @RequestBody request: ProfileRequest, principal: Principal @@ -29,6 +42,14 @@ class ProfileController( } @GetMapping + @Operation(summary = "Get user profile", description = "Fetches the profile for the logged-in user") + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "Profile fetched successfully"), + ApiResponse(responseCode = "400", description = "Profile not found or invalid request"), + ApiResponse(responseCode = "500", description = "Internal server error") + ] + ) fun getProfile(principal: Principal): ResponseEntity { return try { val profile = profileService.getByUsername(principal.name) From 7ea1130e35f20785d5087697b84541783705000c Mon Sep 17 00:00:00 2001 From: Yousef Alothman Date: Wed, 30 Apr 2025 20:47:30 +0300 Subject: [PATCH 16/18] Online Ordering - Setup Swagger(Updated) included the forgotten files to complete the task --- .../spring/ordering/users/UsersController.kt | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UsersController.kt b/src/main/kotlin/com/coded/spring/ordering/users/UsersController.kt index 35c387f..74bbbad 100644 --- a/src/main/kotlin/com/coded/spring/ordering/users/UsersController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/users/UsersController.kt @@ -2,30 +2,44 @@ package com.coded.spring.ordering.users import com.coded.spring.ordering.DTO.UserRequest import com.coded.spring.ordering.exceptions.TransferFundsException +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/users") +@Tag(name = "AUTHENTICATION", description = "Handles user registration and listing") class UsersController( private val usersService: UsersService -){ +) { @PostMapping("/v1/register") + @Operation( + summary = "Register a new user", + description = "Registers a new user using username and password", + responses = [ + ApiResponse(responseCode = "200", description = "User successfully registered"), + ApiResponse(responseCode = "400", description = "Validation failed or user already exists") + ] + ) fun registerUser(@RequestBody request: UserRequest): ResponseEntity { return try { val newUser = usersService.registerUser(request) ResponseEntity.ok(newUser) } catch (e: TransferFundsException) { - ResponseEntity.badRequest().body(mapOf("error" to e.message)) } + ResponseEntity.badRequest().body(mapOf("error" to e.message)) + } } - @GetMapping("/v1/list") + @Operation( + summary = "List all users", + description = "Returns a list of all registered users", + responses = [ + ApiResponse(responseCode = "200", description = "Users listed successfully") + ] + ) fun users() = usersService.listUsers() - } \ No newline at end of file From 63473c2b4d1eee9a8b06f997e78099e229000356 Mon Sep 17 00:00:00 2001 From: Yousef Alothman Date: Sat, 3 May 2025 17:19:20 +0300 Subject: [PATCH 17/18] Online Ordering - Refactor to Micro Services (In Progress) - Defined `OrderEntity` and `ItemEntity` with appropriate JPA annotations - Set up `OrdersRepository` and `ItemsRepository` - Implemented `OrdersService` with order submission and listing logic - Integrated mapping from item IDs to orders - Component scan adjusted to include all relevant packages - Still debugging bean visibility and application context issues --- authentication/pom.xml | 15 +++++ .../authentication/config/LoggingFilter.kt | 42 ++++++++++++++ .../jwt/JwtAuthenticationFilter.kt | 2 +- .../kotlin}/authentication/jwt/JwtService.kt | 17 ++++-- .../profile}/InvalidProfileException.kt | 0 .../profile}/ProfileController.kt | 3 +- .../authentication/profile}/ProfileDTO.kt | 2 +- .../profile}/ProfileRepository.kt | 2 +- .../authentication/profile}/ProfileService.kt | 5 +- .../security/AuthenticationApplication.kt | 11 ++++ .../security}/AuthenticationController.kt | 28 ++++++++-- .../security}/CustomerUserDetailsService.kt | 20 +++---- .../security}/SecurityConfig.kt | 9 +-- .../users/TransferFundsException.kt | 3 + .../kotlin/authentication/users}/UserDTO.kt | 2 +- .../authentication/users/UserRepository.kt | 27 +++++++++ .../authentication/users/UsersController.kt | 43 ++++++++++++++ .../authentication/users/UsersService.kt | 56 +++++++++++++++++++ .../src/main/resources/application.properties | 9 +++ ordering/pom.xml | 29 ++++++++++ .../src/main/kotlin/OrderingApplication.kt | 13 +++++ .../kotlin/client/AuthenticationClient.kt | 34 +++++++++++ .../src/main/kotlin/config/LoggingFilter.kt | 35 ++++++++++++ .../src/main/kotlin/items}/ItemDTO.kt | 4 +- .../src/main/kotlin}/items/ItemsController.kt | 26 ++++++--- .../src/main/kotlin}/items/ItemsRepository.kt | 20 +++---- .../src/main/kotlin}/items/ItemsService.kt | 23 ++++---- .../src/main/kotlin}/menu/MenuController.kt | 0 .../src/main/kotlin}/menu/MenuRepository.kt | 0 .../src/main/kotlin}/menu/MenuService.kt | 0 .../main/kotlin}/orders/OrderController.kt | 20 ++++--- .../src/main/kotlin/orders}/OrderDTO.kt | 6 +- .../main/kotlin}/orders/OrdersRepository.kt | 16 ++---- .../src/main/kotlin/orders/OrdersService.kt | 46 +++++++++++++++ .../security/RemoteAuthenticationFilter.kt | 34 +++++++++++ .../main/kotlin/security/SecurityConfig.kt | 30 ++++++++++ .../src/main/resources/application.properties | 9 +++ pom.xml | 6 ++ .../spring/ordering/orders/OrdersService.kt | 53 ------------------ welcome/pom.xml | 24 ++++++++ .../helloworld/HelloWorldController.kt | 0 .../src/main/resources/application.properties | 9 +++ 42 files changed, 592 insertions(+), 141 deletions(-) create mode 100644 authentication/pom.xml create mode 100644 authentication/src/main/kotlin/authentication/config/LoggingFilter.kt rename {src/main/kotlin/com/coded/spring/ordering => authentication/src/main/kotlin}/authentication/jwt/JwtAuthenticationFilter.kt (97%) rename {src/main/kotlin/com/coded/spring/ordering => authentication/src/main/kotlin}/authentication/jwt/JwtService.kt (71%) rename {src/main/kotlin/com/coded/spring/ordering/exceptions => authentication/src/main/kotlin/authentication/profile}/InvalidProfileException.kt (100%) rename {src/main/kotlin/com/coded/spring/ordering/profiles => authentication/src/main/kotlin/authentication/profile}/ProfileController.kt (96%) rename {src/main/kotlin/com/coded/spring/ordering/DTO => authentication/src/main/kotlin/authentication/profile}/ProfileDTO.kt (74%) rename {src/main/kotlin/com/coded/spring/ordering/profiles => authentication/src/main/kotlin/authentication/profile}/ProfileRepository.kt (92%) rename {src/main/kotlin/com/coded/spring/ordering/profiles => authentication/src/main/kotlin/authentication/profile}/ProfileService.kt (93%) create mode 100644 authentication/src/main/kotlin/authentication/security/AuthenticationApplication.kt rename {src/main/kotlin/com/coded/spring/ordering/authentication/jwt => authentication/src/main/kotlin/authentication/security}/AuthenticationController.kt (74%) rename {src/main/kotlin/com/coded/spring/ordering/authentication => authentication/src/main/kotlin/authentication/security}/CustomerUserDetailsService.kt (55%) rename {src/main/kotlin/com/coded/spring/ordering/authentication => authentication/src/main/kotlin/authentication/security}/SecurityConfig.kt (88%) create mode 100644 authentication/src/main/kotlin/authentication/users/TransferFundsException.kt rename {src/main/kotlin/com/coded/spring/ordering/DTO => authentication/src/main/kotlin/authentication/users}/UserDTO.kt (83%) create mode 100644 authentication/src/main/kotlin/authentication/users/UserRepository.kt create mode 100644 authentication/src/main/kotlin/authentication/users/UsersController.kt create mode 100644 authentication/src/main/kotlin/authentication/users/UsersService.kt create mode 100644 authentication/src/main/resources/application.properties create mode 100644 ordering/pom.xml create mode 100644 ordering/src/main/kotlin/OrderingApplication.kt create mode 100644 ordering/src/main/kotlin/client/AuthenticationClient.kt create mode 100644 ordering/src/main/kotlin/config/LoggingFilter.kt rename {src/main/kotlin/com/coded/spring/ordering/DTO => ordering/src/main/kotlin/items}/ItemDTO.kt (80%) rename {src/main/kotlin/com/coded/spring/ordering => ordering/src/main/kotlin}/items/ItemsController.kt (59%) rename {src/main/kotlin/com/coded/spring/ordering => ordering/src/main/kotlin}/items/ItemsRepository.kt (55%) rename {src/main/kotlin/com/coded/spring/ordering => ordering/src/main/kotlin}/items/ItemsService.kt (59%) rename {src/main/kotlin/com/coded/spring/ordering => ordering/src/main/kotlin}/menu/MenuController.kt (100%) rename {src/main/kotlin/com/coded/spring/ordering => ordering/src/main/kotlin}/menu/MenuRepository.kt (100%) rename {src/main/kotlin/com/coded/spring/ordering => ordering/src/main/kotlin}/menu/MenuService.kt (100%) rename {src/main/kotlin/com/coded/spring/ordering => ordering/src/main/kotlin}/orders/OrderController.kt (73%) rename {src/main/kotlin/com/coded/spring/ordering/DTO => ordering/src/main/kotlin/orders}/OrderDTO.kt (53%) rename {src/main/kotlin/com/coded/spring/ordering => ordering/src/main/kotlin}/orders/OrdersRepository.kt (51%) create mode 100644 ordering/src/main/kotlin/orders/OrdersService.kt create mode 100644 ordering/src/main/kotlin/security/RemoteAuthenticationFilter.kt create mode 100644 ordering/src/main/kotlin/security/SecurityConfig.kt create mode 100644 ordering/src/main/resources/application.properties delete mode 100644 src/main/kotlin/com/coded/spring/ordering/orders/OrdersService.kt create mode 100644 welcome/pom.xml rename {src/main/kotlin/com/coded/spring/ordering => welcome/src/main/kotlin}/helloworld/HelloWorldController.kt (100%) create mode 100644 welcome/src/main/resources/application.properties diff --git a/authentication/pom.xml b/authentication/pom.xml new file mode 100644 index 0000000..fc56ca2 --- /dev/null +++ b/authentication/pom.xml @@ -0,0 +1,15 @@ + + + 4.0.0 + + com.coded.spring + Ordering + 0.0.1-SNAPSHOT + + + authentication + + + \ No newline at end of file diff --git a/authentication/src/main/kotlin/authentication/config/LoggingFilter.kt b/authentication/src/main/kotlin/authentication/config/LoggingFilter.kt new file mode 100644 index 0000000..0dc9618 --- /dev/null +++ b/authentication/src/main/kotlin/authentication/config/LoggingFilter.kt @@ -0,0 +1,42 @@ +package authentication.config + +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) + + 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 = String(request.contentAsByteArray) + logger.info("Request: method=${request.method}, uri=${request.requestURI}, body=$requestBody") + } + + private fun logResponse(response: ContentCachingResponseWrapper) { + val responseBody = String(response.contentAsByteArray) + logger.info("Response: status=${response.status}, body=$responseBody") + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtAuthenticationFilter.kt b/authentication/src/main/kotlin/authentication/jwt/JwtAuthenticationFilter.kt similarity index 97% rename from src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtAuthenticationFilter.kt rename to authentication/src/main/kotlin/authentication/jwt/JwtAuthenticationFilter.kt index f180b7c..6ba9ce3 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtAuthenticationFilter.kt +++ b/authentication/src/main/kotlin/authentication/jwt/JwtAuthenticationFilter.kt @@ -1,4 +1,4 @@ -package com.coded.spring.ordering.authentication.jwt +package authentication.jwt import jakarta.servlet.FilterChain import jakarta.servlet.http.* diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtService.kt b/authentication/src/main/kotlin/authentication/jwt/JwtService.kt similarity index 71% rename from src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtService.kt rename to authentication/src/main/kotlin/authentication/jwt/JwtService.kt index fec69c2..a83dfb7 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtService.kt +++ b/authentication/src/main/kotlin/authentication/jwt/JwtService.kt @@ -1,4 +1,4 @@ -package com.coded.spring.ordering.authentication.jwt +package authentication.jwt import io.jsonwebtoken.* import io.jsonwebtoken.security.Keys @@ -6,6 +6,7 @@ import org.springframework.stereotype.Component import java.util.* import javax.crypto.SecretKey + @Component class JwtService { @@ -24,16 +25,24 @@ class JwtService { .compact() } - fun extractUsername(token: String): String = - Jwts.parserBuilder() + fun extractUsername(token: String): String { + return Jwts.parserBuilder() .setSigningKey(secretKey) .build() .parseClaimsJws(token) .body.subject + } fun isTokenValid(token: String, username: String): Boolean { return try { - extractUsername(token) == username + val extractedUsername = Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .body + .subject + + extractedUsername == username } catch (e: Exception) { false } diff --git a/src/main/kotlin/com/coded/spring/ordering/exceptions/InvalidProfileException.kt b/authentication/src/main/kotlin/authentication/profile/InvalidProfileException.kt similarity index 100% rename from src/main/kotlin/com/coded/spring/ordering/exceptions/InvalidProfileException.kt rename to authentication/src/main/kotlin/authentication/profile/InvalidProfileException.kt diff --git a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileController.kt b/authentication/src/main/kotlin/authentication/profile/ProfileController.kt similarity index 96% rename from src/main/kotlin/com/coded/spring/ordering/profiles/ProfileController.kt rename to authentication/src/main/kotlin/authentication/profile/ProfileController.kt index 6c9d22c..ac8a584 100644 --- a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileController.kt +++ b/authentication/src/main/kotlin/authentication/profile/ProfileController.kt @@ -1,6 +1,5 @@ -package com.coded.spring.ordering.profiles +package authentication.profile -import com.coded.spring.ordering.DTO.ProfileRequest import com.coded.spring.ordering.exceptions.InvalidProfileException import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.responses.ApiResponse diff --git a/src/main/kotlin/com/coded/spring/ordering/DTO/ProfileDTO.kt b/authentication/src/main/kotlin/authentication/profile/ProfileDTO.kt similarity index 74% rename from src/main/kotlin/com/coded/spring/ordering/DTO/ProfileDTO.kt rename to authentication/src/main/kotlin/authentication/profile/ProfileDTO.kt index f0acc98..110899b 100644 --- a/src/main/kotlin/com/coded/spring/ordering/DTO/ProfileDTO.kt +++ b/authentication/src/main/kotlin/authentication/profile/ProfileDTO.kt @@ -1,4 +1,4 @@ -package com.coded.spring.ordering.DTO +package authentication.profile data class ProfileRequest( val firstName: String, diff --git a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileRepository.kt b/authentication/src/main/kotlin/authentication/profile/ProfileRepository.kt similarity index 92% rename from src/main/kotlin/com/coded/spring/ordering/profiles/ProfileRepository.kt rename to authentication/src/main/kotlin/authentication/profile/ProfileRepository.kt index 7d06042..4cec5be 100644 --- a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileRepository.kt +++ b/authentication/src/main/kotlin/authentication/profile/ProfileRepository.kt @@ -1,4 +1,4 @@ -package com.coded.spring.ordering.profiles +package authentication.profile import jakarta.inject.Named import jakarta.persistence.* diff --git a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileService.kt b/authentication/src/main/kotlin/authentication/profile/ProfileService.kt similarity index 93% rename from src/main/kotlin/com/coded/spring/ordering/profiles/ProfileService.kt rename to authentication/src/main/kotlin/authentication/profile/ProfileService.kt index 27284db..7cf8fb6 100644 --- a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileService.kt +++ b/authentication/src/main/kotlin/authentication/profile/ProfileService.kt @@ -1,8 +1,7 @@ -package com.coded.spring.ordering.profiles +package authentication.profile -import com.coded.spring.ordering.DTO.ProfileRequest +import authentication.users.UsersRepository import com.coded.spring.ordering.exceptions.InvalidProfileException -import com.coded.spring.ordering.users.UsersRepository import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional diff --git a/authentication/src/main/kotlin/authentication/security/AuthenticationApplication.kt b/authentication/src/main/kotlin/authentication/security/AuthenticationApplication.kt new file mode 100644 index 0000000..15948a4 --- /dev/null +++ b/authentication/src/main/kotlin/authentication/security/AuthenticationApplication.kt @@ -0,0 +1,11 @@ +package authentication.security + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class AuthenticationApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/AuthenticationController.kt b/authentication/src/main/kotlin/authentication/security/AuthenticationController.kt similarity index 74% rename from src/main/kotlin/com/coded/spring/ordering/authentication/jwt/AuthenticationController.kt rename to authentication/src/main/kotlin/authentication/security/AuthenticationController.kt index 39ba8c7..0263a23 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/AuthenticationController.kt +++ b/authentication/src/main/kotlin/authentication/security/AuthenticationController.kt @@ -1,5 +1,6 @@ -package com.coded.spring.ordering.authentication.jwt +package authentication.security +import authentication.jwt.JwtService import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.media.Schema @@ -10,8 +11,9 @@ 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 java.security.Principal -@Tag(name = "AUTHENTICATION", description = "Endpoints related to user login and token generation") +@Tag(name = "AUTHENTICATION", description = "Endpoints related to user login and token validation") @RestController @RequestMapping("/auth") class AuthenticationController( @@ -22,8 +24,7 @@ class AuthenticationController( @Operation( summary = "Login and generate JWT token", - description = "Accepts username and password and returns a JWT token upon successful authentication", - tags = ["AUTHENTICATION"] + description = "Accepts username and password and returns a JWT token upon successful authentication" ) @ApiResponses( value = [ @@ -52,6 +53,21 @@ class AuthenticationController( throw UsernameNotFoundException("Invalid user request!") } } + + @Operation( + summary = "Check token validity", + description = "Validates the JWT token provided in the Authorization header" + ) + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "Token is valid"), + ApiResponse(responseCode = "401", description = "Invalid or expired token") + ] + ) + @PostMapping("/check-token") + fun checkToken(principal: Principal): TokenCheckResponse { + return TokenCheckResponse(username = principal.name) + } } data class AuthenticationRequest( @@ -61,4 +77,8 @@ data class AuthenticationRequest( data class AuthenticationResponse( val token: String +) + +data class TokenCheckResponse( + val username: String ) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/CustomerUserDetailsService.kt b/authentication/src/main/kotlin/authentication/security/CustomerUserDetailsService.kt similarity index 55% rename from src/main/kotlin/com/coded/spring/ordering/authentication/CustomerUserDetailsService.kt rename to authentication/src/main/kotlin/authentication/security/CustomerUserDetailsService.kt index 0cc6bdc..103b99d 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/CustomerUserDetailsService.kt +++ b/authentication/src/main/kotlin/authentication/security/CustomerUserDetailsService.kt @@ -1,30 +1,24 @@ -package com.coded.spring.ordering.authentication +package authentication.security -import com.coded.spring.ordering.users.UserEntity -import com.coded.spring.ordering.users.UsersRepository 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 authentication.users.UsersRepository @Service class CustomerUserDetailsService( private val usersRepository: UsersRepository - ): UserDetailsService{ +) : UserDetailsService { - override fun loadUserByUsername(username:String): UserDetails { + override fun loadUserByUsername(username: String): UserDetails { + val user = usersRepository.findByUsername(username) + ?: throw UsernameNotFoundException("User not found") - val user: UserEntity = usersRepository.findByUsername(username)?: - throw UsernameNotFoundException("User not found...") - - - return User.builder() + return User.withUsername(user.username) .username(user.username) .password(user.password) .build() - - } - } \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt b/authentication/src/main/kotlin/authentication/security/SecurityConfig.kt similarity index 88% rename from src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt rename to authentication/src/main/kotlin/authentication/security/SecurityConfig.kt index 82aefea..9b9ed90 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt +++ b/authentication/src/main/kotlin/authentication/security/SecurityConfig.kt @@ -1,6 +1,6 @@ -package com.coded.spring.ordering.authentication +package authentication.security -import com.coded.spring.ordering.authentication.jwt.JwtAuthenticationFilter +import authentication.jwt.JwtAuthenticationFilter import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.security.authentication.AuthenticationManager @@ -27,8 +27,9 @@ class SecurityConfig( fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { http.csrf { it.disable() } .authorizeHttpRequests { - it.requestMatchers("/auth/**","/users/v1/register","/menu/v1/list","/api-docs","/hello").permitAll() - .anyRequest().authenticated() + it.requestMatchers("/auth/v1/login").permitAll() + it.requestMatchers("/auth/v1/register").permitAll() + .anyRequest().authenticated() } .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) diff --git a/authentication/src/main/kotlin/authentication/users/TransferFundsException.kt b/authentication/src/main/kotlin/authentication/users/TransferFundsException.kt new file mode 100644 index 0000000..a132135 --- /dev/null +++ b/authentication/src/main/kotlin/authentication/users/TransferFundsException.kt @@ -0,0 +1,3 @@ +package authentication.users + +class TransferFundsException(msg: String) : Exception(msg) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/DTO/UserDTO.kt b/authentication/src/main/kotlin/authentication/users/UserDTO.kt similarity index 83% rename from src/main/kotlin/com/coded/spring/ordering/DTO/UserDTO.kt rename to authentication/src/main/kotlin/authentication/users/UserDTO.kt index 962270a..be0e6b8 100644 --- a/src/main/kotlin/com/coded/spring/ordering/DTO/UserDTO.kt +++ b/authentication/src/main/kotlin/authentication/users/UserDTO.kt @@ -1,4 +1,4 @@ -package com.coded.spring.ordering.DTO +package authentication.users data class UserRequest( val name: String, diff --git a/authentication/src/main/kotlin/authentication/users/UserRepository.kt b/authentication/src/main/kotlin/authentication/users/UserRepository.kt new file mode 100644 index 0000000..a9137f9 --- /dev/null +++ b/authentication/src/main/kotlin/authentication/users/UserRepository.kt @@ -0,0 +1,27 @@ +package authentication.users + +import jakarta.inject.Named +import jakarta.persistence.* +import org.springframework.data.jpa.repository.JpaRepository + +@Named +interface UsersRepository : JpaRepository { + fun age(age: Int): MutableList + fun findByUsername(username: String): UserEntity? +} + +@Entity +@Table(name = "users") +data class UserEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, + val name: String, + val age: Int, + + val username: String, + val password: String, + +){ + constructor() : this(null, "", 0,"","") +} \ No newline at end of file diff --git a/authentication/src/main/kotlin/authentication/users/UsersController.kt b/authentication/src/main/kotlin/authentication/users/UsersController.kt new file mode 100644 index 0000000..002e4d9 --- /dev/null +++ b/authentication/src/main/kotlin/authentication/users/UsersController.kt @@ -0,0 +1,43 @@ +package authentication.users + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/users") +@Tag(name = "AUTHENTICATION", description = "Handles user registration and listing") +class UsersController( + private val usersService: UsersService +) { + + @PostMapping("/v1/register") + @Operation( + summary = "Register a new user", + description = "Registers a new user using username and password", + responses = [ + ApiResponse(responseCode = "200", description = "User successfully registered"), + ApiResponse(responseCode = "400", description = "Validation failed or user already exists") + ] + ) + fun registerUser(@RequestBody request: UserRequest): ResponseEntity { + return try { + val newUser = usersService.registerUser(request) + ResponseEntity.ok(newUser) + } catch (e: TransferFundsException) { + ResponseEntity.badRequest().body(mapOf("error" to e.message)) + } + } + + @GetMapping("/v1/list") + @Operation( + summary = "List all users", + description = "Returns a list of all registered users", + responses = [ + ApiResponse(responseCode = "200", description = "Users listed successfully") + ] + ) + fun users() = usersService.listUsers() +} \ No newline at end of file diff --git a/authentication/src/main/kotlin/authentication/users/UsersService.kt b/authentication/src/main/kotlin/authentication/users/UsersService.kt new file mode 100644 index 0000000..91c0b0a --- /dev/null +++ b/authentication/src/main/kotlin/authentication/users/UsersService.kt @@ -0,0 +1,56 @@ +package authentication.users +import jakarta.inject.Named +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.stereotype.Service + +const val USERNAME_MIN_LENGTH = 4 +const val USERNAME_MAX_LENGTH = 30 +const val PASSWORD_MIN_LENGTH = 9 +const val PASSWORD_MAX_LENGTH = 30 + +@Named +@Service +class UsersService( + private val usersRepository: UsersRepository, + private val passwordEncoder: PasswordEncoder +) { + + fun registerUser(request: UserRequest): UserResponse { + + if (request.username.length < USERNAME_MIN_LENGTH || + request.username.length > USERNAME_MAX_LENGTH + ) { + throw TransferFundsException( + "Username must be between $USERNAME_MIN_LENGTH and $USERNAME_MAX_LENGTH characters") + } + + if (request.password.length < PASSWORD_MIN_LENGTH || + request.password.length > PASSWORD_MAX_LENGTH + ) { + throw TransferFundsException( + "Password must be between $PASSWORD_MIN_LENGTH and $PASSWORD_MAX_LENGTH characters") + } + + val encodedPassword = passwordEncoder.encode(request.password) + + val createUser = UserEntity( + name = request.name, + age = request.age, + username = request.username, + password = request.password + ) + + val savedUser = usersRepository.save(createUser) + return UserResponse(id = savedUser.id!!, username = savedUser.username) + } + + fun listUsers(): List = usersRepository.findAll().map { + UserRequest( + name = it.name, + age = it.age, + username = it.username, + password = it.password + + ) + } +} \ No newline at end of file diff --git a/authentication/src/main/resources/application.properties b/authentication/src/main/resources/application.properties new file mode 100644 index 0000000..f66e6f8 --- /dev/null +++ b/authentication/src/main/resources/application.properties @@ -0,0 +1,9 @@ +spring.application.name=Kotlin.SpringbootV2 +server.port = 8081 + +spring.datasource.url=jdbc:postgresql://localhost:5432/myHelloDatabase +spring.datasource.username=postgres +spring.datasource.password=yosaka +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect + +springdoc.api-docs.path=/api-docs diff --git a/ordering/pom.xml b/ordering/pom.xml new file mode 100644 index 0000000..88e4c58 --- /dev/null +++ b/ordering/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + com.coded.spring + Ordering + 0.0.1-SNAPSHOT + + + ordering + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ordering/src/main/kotlin/OrderingApplication.kt b/ordering/src/main/kotlin/OrderingApplication.kt new file mode 100644 index 0000000..43f86bf --- /dev/null +++ b/ordering/src/main/kotlin/OrderingApplication.kt @@ -0,0 +1,13 @@ +package ordering + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication +import org.springframework.context.annotation.ComponentScan + +@SpringBootApplication +@ComponentScan(basePackages = ["ordering", "items"]) +class OrderingApplication + +fun main(args: Array) { + runApplication(*args) +} \ No newline at end of file diff --git a/ordering/src/main/kotlin/client/AuthenticationClient.kt b/ordering/src/main/kotlin/client/AuthenticationClient.kt new file mode 100644 index 0000000..1268ad9 --- /dev/null +++ b/ordering/src/main/kotlin/client/AuthenticationClient.kt @@ -0,0 +1,34 @@ +package ordering.client + +import org.springframework.core.ParameterizedTypeReference +import org.springframework.http.* +import org.springframework.stereotype.Component +import org.springframework.web.client.RestTemplate + +data class CheckTokenResponse(val userId: Long) + +@Component +class AuthenticationClient { + + private val restTemplate = RestTemplate() + private val authServerUrl = "http://localhost:8081/auth/check-token" + + fun checkToken(token: String): CheckTokenResponse { + val headers = HttpHeaders() + headers.setBearerAuth(token) + val requestEntity = HttpEntity(headers) + + val response = restTemplate.exchange( + authServerUrl, + HttpMethod.POST, + requestEntity, + object : ParameterizedTypeReference() {} + ) + + if (response.statusCode != HttpStatus.OK) { + throw IllegalStateException("Invalid token") + } + + return response.body ?: throw IllegalStateException("Missing response body from auth service") + } +} \ No newline at end of file diff --git a/ordering/src/main/kotlin/config/LoggingFilter.kt b/ordering/src/main/kotlin/config/LoggingFilter.kt new file mode 100644 index 0000000..72ae1db --- /dev/null +++ b/ordering/src/main/kotlin/config/LoggingFilter.kt @@ -0,0 +1,35 @@ +package ordering.config + +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) + + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + val wrappedRequest = ContentCachingRequestWrapper(request) + val wrappedResponse = ContentCachingResponseWrapper(response) + + filterChain.doFilter(wrappedRequest, wrappedResponse) + + val requestBody = String(wrappedRequest.contentAsByteArray) + logger.info("Request: method=${request.method}, uri=${request.requestURI}, body=$requestBody") + + val responseBody = String(wrappedResponse.contentAsByteArray) + logger.info("Response: status=${response.status}, body=$responseBody") + + wrappedResponse.copyBodyToResponse() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/DTO/ItemDTO.kt b/ordering/src/main/kotlin/items/ItemDTO.kt similarity index 80% rename from src/main/kotlin/com/coded/spring/ordering/DTO/ItemDTO.kt rename to ordering/src/main/kotlin/items/ItemDTO.kt index 99e3bba..8173250 100644 --- a/src/main/kotlin/com/coded/spring/ordering/DTO/ItemDTO.kt +++ b/ordering/src/main/kotlin/items/ItemDTO.kt @@ -1,9 +1,9 @@ -package com.coded.spring.ordering.DTO +package items data class Item( val id: Long?, - val order_id: Long?, + val orderId: Long?, val name: String?, val quantity: Long?, val note: String?, diff --git a/src/main/kotlin/com/coded/spring/ordering/items/ItemsController.kt b/ordering/src/main/kotlin/items/ItemsController.kt similarity index 59% rename from src/main/kotlin/com/coded/spring/ordering/items/ItemsController.kt rename to ordering/src/main/kotlin/items/ItemsController.kt index 2b7bf9b..d868511 100644 --- a/src/main/kotlin/com/coded/spring/ordering/items/ItemsController.kt +++ b/ordering/src/main/kotlin/items/ItemsController.kt @@ -1,21 +1,22 @@ -package com.coded.spring.ordering.items +package items -import com.coded.spring.ordering.DTO.Item -import com.coded.spring.ordering.DTO.SubmitItemRequest +import jakarta.servlet.http.HttpServletRequest import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.tags.Tag +import ordering.client.AuthenticationClient import org.springframework.web.bind.annotation.* @RestController @Tag(name = "ORDERING", description = "Endpoints related to managing menu items") class ItemsController( - private val itemsService: ItemsService + private val itemsService: ItemsService, + private val authenticationClient: AuthenticationClient ) { @GetMapping("/listItems") - @Operation(summary = "List all menu items", description = "Returns a list of all available items") + @Operation(summary = "List all menu items", description = "Returns a list of all available items.") @ApiResponses( value = [ ApiResponse(responseCode = "200", description = "Items listed successfully"), @@ -25,14 +26,23 @@ class ItemsController( fun listItems(): List = itemsService.listItems() @PostMapping("/submitItems") - @Operation(summary = "Submit a new menu item", description = "Allows an admin to add a new item to the menu") + @Operation(summary = "Submit a new menu item", description = "Allows authenticated users to add a new item to the menu.") @ApiResponses( value = [ ApiResponse(responseCode = "200", description = "Item submitted successfully"), - ApiResponse(responseCode = "400", description = "Invalid item data") + ApiResponse(responseCode = "400", description = "Invalid item data"), + ApiResponse(responseCode = "401", description = "Unauthorized request") ] ) - fun submitItem(@RequestBody request: SubmitItemRequest): ItemEntity { + fun submitItem( + @RequestBody request: SubmitItemRequest, + servletRequest: HttpServletRequest + ): ItemEntity { + val authHeader = servletRequest.getHeader("Authorization") + ?: throw IllegalStateException("Missing Authorization header") + + authenticationClient.checkToken(authHeader.removePrefix("Bearer ").trim()) + return itemsService.submitItem(request) } } \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/items/ItemsRepository.kt b/ordering/src/main/kotlin/items/ItemsRepository.kt similarity index 55% rename from src/main/kotlin/com/coded/spring/ordering/items/ItemsRepository.kt rename to ordering/src/main/kotlin/items/ItemsRepository.kt index 96457f0..2b0f4c1 100644 --- a/src/main/kotlin/com/coded/spring/ordering/items/ItemsRepository.kt +++ b/ordering/src/main/kotlin/items/ItemsRepository.kt @@ -1,14 +1,13 @@ -package com.coded.spring.ordering.items +package items -import com.coded.spring.ordering.orders.OrderEntity -import jakarta.inject.Named import jakarta.persistence.* import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository -@Named -interface ItemsRepository : JpaRepository - - +@Repository +interface ItemsRepository : JpaRepository{ + fun findByOrderId(orderId: Long): List +} @Entity @Table(name = "items") @@ -27,10 +26,9 @@ data class ItemEntity( var price: Double? = null, - @ManyToOne - @JoinColumn(name = "order_id") - var order: OrderEntity? = null + @Column(name = "order_id") + var orderId: Long? = null ) { - constructor(): this(null, "", 1, "", 0.0, null) + constructor() : this(null, "", 1, "", 0.0, null) } \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/items/ItemsService.kt b/ordering/src/main/kotlin/items/ItemsService.kt similarity index 59% rename from src/main/kotlin/com/coded/spring/ordering/items/ItemsService.kt rename to ordering/src/main/kotlin/items/ItemsService.kt index 11a0a9e..2119e59 100644 --- a/src/main/kotlin/com/coded/spring/ordering/items/ItemsService.kt +++ b/ordering/src/main/kotlin/items/ItemsService.kt @@ -1,6 +1,5 @@ -package com.coded.spring.ordering.items -import com.coded.spring.ordering.DTO.Item -import com.coded.spring.ordering.DTO.SubmitItemRequest +package items + import jakarta.inject.Named import org.springframework.beans.factory.annotation.Value @@ -8,28 +7,32 @@ import org.springframework.beans.factory.annotation.Value class ItemsService( private val itemsRepository: ItemsRepository, @Value("\${festive-mode:false}") - val festiveMode: Boolean - + private val festiveMode: Boolean ) { + fun listItems(): List = itemsRepository.findAll().map { entity -> Item( id = entity.id, - order_id = entity.order?.id, + orderId = entity.orderId, name = entity.name, quantity = entity.quantity, note = entity.note, - price = if (festiveMode) entity.price?.times(0.8) else entity.price + price = calculatePrice(entity.price) ) } fun submitItem(request: SubmitItemRequest): ItemEntity { val item = ItemEntity( name = request.name, - quantity = request.quantity, + quantity = request.quantity ?: 0, note = request.note, - price = request.price + price = request.price ?: 0.0, + orderId = 0 // Default to 0, actual value set on order placement ) return itemsRepository.save(item) } -} + private fun calculatePrice(originalPrice: Double?): Double? { + return if (festiveMode) originalPrice?.times(0.8) else originalPrice + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/menu/MenuController.kt b/ordering/src/main/kotlin/menu/MenuController.kt similarity index 100% rename from src/main/kotlin/com/coded/spring/ordering/menu/MenuController.kt rename to ordering/src/main/kotlin/menu/MenuController.kt diff --git a/src/main/kotlin/com/coded/spring/ordering/menu/MenuRepository.kt b/ordering/src/main/kotlin/menu/MenuRepository.kt similarity index 100% rename from src/main/kotlin/com/coded/spring/ordering/menu/MenuRepository.kt rename to ordering/src/main/kotlin/menu/MenuRepository.kt diff --git a/src/main/kotlin/com/coded/spring/ordering/menu/MenuService.kt b/ordering/src/main/kotlin/menu/MenuService.kt similarity index 100% rename from src/main/kotlin/com/coded/spring/ordering/menu/MenuService.kt rename to ordering/src/main/kotlin/menu/MenuService.kt diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OrderController.kt b/ordering/src/main/kotlin/orders/OrderController.kt similarity index 73% rename from src/main/kotlin/com/coded/spring/ordering/orders/OrderController.kt rename to ordering/src/main/kotlin/orders/OrderController.kt index 84ab007..9b826f2 100644 --- a/src/main/kotlin/com/coded/spring/ordering/orders/OrderController.kt +++ b/ordering/src/main/kotlin/orders/OrderController.kt @@ -1,11 +1,14 @@ -package com.coded.spring.ordering.orders +package orders -import com.coded.spring.ordering.DTO.Order -import com.coded.spring.ordering.DTO.SubmitOrderRequest +import ordering.orders.Order +import ordering.orders.SubmitOrderRequest +import ordering.orders.OrdersRepository +import ordering.orders.OrdersService import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.servlet.http.HttpServletRequest import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* import java.security.Principal @@ -29,10 +32,10 @@ class OrderController( ) fun submitOrder( @RequestBody request: SubmitOrderRequest, - principal: Principal + servletRequest: HttpServletRequest ): ResponseEntity { - val username = principal.name - ordersService.submitOrder(username, request.itemIds) + val userId = servletRequest.getAttribute("userId") as Long + ordersService.submitOrder(userId, request.itemIds) return ResponseEntity.ok("Order submitted successfully.") } @@ -44,7 +47,8 @@ class OrderController( ApiResponse(responseCode = "403", description = "Unauthorized access") ] ) - fun listMyOrders(principal: Principal): List { - return ordersService.listOrdersForUser(principal.name) + fun listMyOrders(servletRequest: HttpServletRequest): List { + val userId = servletRequest.getAttribute("userId") as Long + return ordersService.listOrdersForUser(userId) } } \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/DTO/OrderDTO.kt b/ordering/src/main/kotlin/orders/OrderDTO.kt similarity index 53% rename from src/main/kotlin/com/coded/spring/ordering/DTO/OrderDTO.kt rename to ordering/src/main/kotlin/orders/OrderDTO.kt index 18fd359..1d97011 100644 --- a/src/main/kotlin/com/coded/spring/ordering/DTO/OrderDTO.kt +++ b/ordering/src/main/kotlin/orders/OrderDTO.kt @@ -1,6 +1,6 @@ -package com.coded.spring.ordering.DTO +package ordering.orders -import com.coded.spring.ordering.items.ItemEntity +import items.Item data class SubmitOrderRequest( val itemIds: List @@ -8,7 +8,7 @@ data class SubmitOrderRequest( data class Order( val id: Long?, - val user_id: Long?, + val userId: Long?, val items: List ) diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OrdersRepository.kt b/ordering/src/main/kotlin/orders/OrdersRepository.kt similarity index 51% rename from src/main/kotlin/com/coded/spring/ordering/orders/OrdersRepository.kt rename to ordering/src/main/kotlin/orders/OrdersRepository.kt index 89cb81d..2ca6f94 100644 --- a/src/main/kotlin/com/coded/spring/ordering/orders/OrdersRepository.kt +++ b/ordering/src/main/kotlin/orders/OrdersRepository.kt @@ -1,13 +1,9 @@ -package com.coded.spring.ordering.orders -import com.coded.spring.ordering.items.ItemEntity +package ordering.orders import jakarta.persistence.* import org.springframework.data.jpa.repository.JpaRepository - -import com.coded.spring.ordering.users.UserEntity import jakarta.inject.Named - @Named interface OrdersRepository: JpaRepository{ fun findByUserId(userId: Long): List @@ -21,13 +17,9 @@ class OrderEntity( @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null, - @ManyToOne - @JoinColumn(name = "user_id") - var user: UserEntity? = null, - - @OneToMany(mappedBy = "order", cascade = [CascadeType.ALL]) - var items: List? = null + @Column(name = "user_id") + var userId: Long? = null, ) { - constructor() : this(null, null, listOf()) + constructor() : this(null, null) } \ No newline at end of file diff --git a/ordering/src/main/kotlin/orders/OrdersService.kt b/ordering/src/main/kotlin/orders/OrdersService.kt new file mode 100644 index 0000000..a2ad87f --- /dev/null +++ b/ordering/src/main/kotlin/orders/OrdersService.kt @@ -0,0 +1,46 @@ +package ordering.orders + +import items.Item +import items.ItemEntity +import items.ItemsRepository +import jakarta.inject.Named + +@Named +class OrdersService( + private val ordersRepository: OrdersRepository, + private val itemsRepository: ItemsRepository +) { + + fun submitOrder(userId: Long, itemIds: List) { + val order = ordersRepository.save(OrderEntity(userId = userId)) + + val updatedItems = itemsRepository.findAllById(itemIds).map { itemEntity -> + itemEntity.copy(orderId = order.id!!) + } + + itemsRepository.saveAll(updatedItems) + } + + fun listOrdersForUser(userId: Long): List { + return ordersRepository.findByUserId(userId).map { orderEntity -> + val itemEntities = itemsRepository.findByOrderId(orderEntity.id!!) + + val items = itemEntities.map { + Item( + id = it.id, + orderId = it.orderId, + name = it.name, + quantity = it.quantity, + note = it.note, + price = it.price + ) + } + + Order( + id = orderEntity.id!!, + userId = userId, + items = items + ) + } + } +} \ No newline at end of file diff --git a/ordering/src/main/kotlin/security/RemoteAuthenticationFilter.kt b/ordering/src/main/kotlin/security/RemoteAuthenticationFilter.kt new file mode 100644 index 0000000..cde6e73 --- /dev/null +++ b/ordering/src/main/kotlin/security/RemoteAuthenticationFilter.kt @@ -0,0 +1,34 @@ +package ordering.security + +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import ordering.client.AuthenticationClient +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 executing...") + + val authHeader = request.getHeader("Authorization") + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response) + return + } + + val token = authHeader.removePrefix("Bearer ").trim() + 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/security/SecurityConfig.kt b/ordering/src/main/kotlin/security/SecurityConfig.kt new file mode 100644 index 0000000..6537494 --- /dev/null +++ b/ordering/src/main/kotlin/security/SecurityConfig.kt @@ -0,0 +1,30 @@ +package 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() // restrict if needed + } + .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..b5b187e --- /dev/null +++ b/ordering/src/main/resources/application.properties @@ -0,0 +1,9 @@ +spring.application.name=Kotlin.SpringbootV2 +server.port = 8082 + +spring.datasource.url=jdbc:postgresql://localhost:5432/myHelloDatabase +spring.datasource.username=postgres +spring.datasource.password=yosaka +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect + +springdoc.api-docs.path=/api-docs diff --git a/pom.xml b/pom.xml index 51f15c9..8393248 100644 --- a/pom.xml +++ b/pom.xml @@ -11,6 +11,7 @@ com.coded.spring Ordering 0.0.1-SNAPSHOT + pom Kotlin.SpringbootV2 Kotlin.SpringbootV2 @@ -20,6 +21,11 @@ + + authentication + ordering + welcome + diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OrdersService.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OrdersService.kt deleted file mode 100644 index f32b127..0000000 --- a/src/main/kotlin/com/coded/spring/ordering/orders/OrdersService.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.coded.spring.ordering.orders - -import com.coded.spring.ordering.DTO.Order -import com.coded.spring.ordering.items.ItemsRepository -import com.coded.spring.ordering.users.UsersRepository -import jakarta.inject.Named -import com.coded.spring.ordering.DTO.Item - - -@Named -class OrdersService( - private val ordersRepository: OrdersRepository, - private val usersRepository: UsersRepository, - private val itemsRepository: ItemsRepository -) { - - fun submitOrder(username: String, itemIds: List) { - val user = usersRepository.findByUsername(username) - ?: throw IllegalArgumentException("User not found") - - val order = OrderEntity(user = user) - val savedOrder = ordersRepository.save(order) - - val items = itemsRepository.findAllById(itemIds).map { - it.order = savedOrder - it - } - - itemsRepository.saveAll(items) - } - - fun listOrdersForUser(username: String): List { - val user = usersRepository.findByUsername(username) - ?: throw IllegalArgumentException("User not found") - - return ordersRepository.findByUserId(user.id!!).map { orderEntity -> - Order( - id = orderEntity.id, - user_id = user.id, - items = orderEntity.items?.map { - Item( - id = it.id, - order_id = it.order?.id, - name = it.name, - quantity = it.quantity, - note = it.note, - price = it.price - ) - } ?: listOf() - ) - } - } -} \ No newline at end of file diff --git a/welcome/pom.xml b/welcome/pom.xml new file mode 100644 index 0000000..ffb9749 --- /dev/null +++ b/welcome/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + com.coded.spring + Ordering + 0.0.1-SNAPSHOT + + + welcome + + + + + org.junit.jupiter + junit-jupiter + 5.10.0 + test + + + + \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/helloworld/HelloWorldController.kt b/welcome/src/main/kotlin/helloworld/HelloWorldController.kt similarity index 100% rename from src/main/kotlin/com/coded/spring/ordering/helloworld/HelloWorldController.kt rename to welcome/src/main/kotlin/helloworld/HelloWorldController.kt diff --git a/welcome/src/main/resources/application.properties b/welcome/src/main/resources/application.properties new file mode 100644 index 0000000..d588ae9 --- /dev/null +++ b/welcome/src/main/resources/application.properties @@ -0,0 +1,9 @@ +spring.application.name=Kotlin.SpringbootV2 +server.port = 8083 + +spring.datasource.url=jdbc:postgresql://localhost:5432/myHelloDatabase +spring.datasource.username=postgres +spring.datasource.password=yosaka +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect + +springdoc.api-docs.path=/api-docs From 06e338998fd47834fc2cbdc37d33a2980728d93e Mon Sep 17 00:00:00 2001 From: Yousef Alothman Date: Sat, 3 May 2025 21:24:04 +0300 Subject: [PATCH 18/18] Online Ordering - Refactor to Micro Services (Completed) (v2) fix: resolve 404 errors caused by IntelliJ tweaking package structure + table name mismatch IntelliJ messed with the package structure causing Spring not to register the OrderController. Also fixed Hibernate trying to query "order/orders" instead of "orders" by explicitly setting @Table(name = "orders") on OrderEntity. --- authentication/pom.xml | 6 ++ .../AuthenticationApplication.kt | 2 +- .../security/AuthenticationController.kt | 67 +++++-------------- .../authentication/security/SecurityConfig.kt | 4 +- .../authentication/users/UsersService.kt | 45 ++++++------- ordering/pom.xml | 61 +++++++++++++---- .../kotlin/client/AuthenticationClient.kt | 34 ---------- .../kotlin/{ => order}/OrderingApplication.kt | 4 +- .../order/client/AuthenticationClient.kt | 47 +++++++++++++ .../{ => order}/config/LoggingFilter.kt | 2 +- .../kotlin/{ => order}/menu/MenuController.kt | 4 +- .../kotlin/{ => order}/menu/MenuRepository.kt | 4 +- .../kotlin/{ => order}/menu/MenuService.kt | 4 +- .../orders}/ItemsRepository.kt | 2 +- .../{ => order}/orders/OrderController.kt | 30 ++++----- .../src/main/kotlin/order/orders/OrderDTO.kt | 20 ++++++ .../{ => order}/orders/OrdersRepository.kt | 2 +- .../main/kotlin/order/orders/OrdersService.kt | 64 ++++++++++++++++++ .../security/RemoteAuthenticationFilter.kt | 24 +++++-- .../{ => order}/security/SecurityConfig.kt | 2 +- ordering/src/main/kotlin/orders/OrderDTO.kt | 14 ---- .../src/main/kotlin/orders/OrdersService.kt | 46 ------------- .../src/main/resources/application.properties | 6 ++ .../coded/spring/ordering}/items/ItemDTO.kt | 1 - .../spring/ordering}/items/ItemsController.kt | 3 +- .../spring/ordering}/items/ItemsService.kt | 6 +- .../kotlin/helloworld/HelloWorldController.kt | 2 +- 27 files changed, 279 insertions(+), 227 deletions(-) rename authentication/src/main/kotlin/authentication/{security => }/AuthenticationApplication.kt (88%) delete mode 100644 ordering/src/main/kotlin/client/AuthenticationClient.kt rename ordering/src/main/kotlin/{ => order}/OrderingApplication.kt (65%) create mode 100644 ordering/src/main/kotlin/order/client/AuthenticationClient.kt rename ordering/src/main/kotlin/{ => order}/config/LoggingFilter.kt (98%) rename ordering/src/main/kotlin/{ => order}/menu/MenuController.kt (93%) rename ordering/src/main/kotlin/{ => order}/menu/MenuRepository.kt (88%) rename ordering/src/main/kotlin/{ => order}/menu/MenuService.kt (67%) rename ordering/src/main/kotlin/{items => order/orders}/ItemsRepository.kt (97%) rename ordering/src/main/kotlin/{ => order}/orders/OrderController.kt (68%) create mode 100644 ordering/src/main/kotlin/order/orders/OrderDTO.kt rename ordering/src/main/kotlin/{ => order}/orders/OrdersRepository.kt (95%) create mode 100644 ordering/src/main/kotlin/order/orders/OrdersService.kt rename ordering/src/main/kotlin/{ => order}/security/RemoteAuthenticationFilter.kt (52%) rename ordering/src/main/kotlin/{ => order}/security/SecurityConfig.kt (97%) delete mode 100644 ordering/src/main/kotlin/orders/OrderDTO.kt delete mode 100644 ordering/src/main/kotlin/orders/OrdersService.kt rename {ordering/src/main/kotlin => src/main/kotlin/com/coded/spring/ordering}/items/ItemDTO.kt (99%) rename {ordering/src/main/kotlin => src/main/kotlin/com/coded/spring/ordering}/items/ItemsController.kt (96%) rename {ordering/src/main/kotlin => src/main/kotlin/com/coded/spring/ordering}/items/ItemsService.kt (90%) diff --git a/authentication/pom.xml b/authentication/pom.xml index fc56ca2..d0d26cc 100644 --- a/authentication/pom.xml +++ b/authentication/pom.xml @@ -10,6 +10,12 @@ authentication + + + org.junit.jupiter + junit-jupiter-api + + \ No newline at end of file diff --git a/authentication/src/main/kotlin/authentication/security/AuthenticationApplication.kt b/authentication/src/main/kotlin/authentication/AuthenticationApplication.kt similarity index 88% rename from authentication/src/main/kotlin/authentication/security/AuthenticationApplication.kt rename to authentication/src/main/kotlin/authentication/AuthenticationApplication.kt index 15948a4..44c3107 100644 --- a/authentication/src/main/kotlin/authentication/security/AuthenticationApplication.kt +++ b/authentication/src/main/kotlin/authentication/AuthenticationApplication.kt @@ -1,4 +1,4 @@ -package authentication.security +package authentication import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication diff --git a/authentication/src/main/kotlin/authentication/security/AuthenticationController.kt b/authentication/src/main/kotlin/authentication/security/AuthenticationController.kt index 0263a23..54b8878 100644 --- a/authentication/src/main/kotlin/authentication/security/AuthenticationController.kt +++ b/authentication/src/main/kotlin/authentication/security/AuthenticationController.kt @@ -1,11 +1,8 @@ package authentication.security import authentication.jwt.JwtService -import io.swagger.v3.oas.annotations.Operation -import io.swagger.v3.oas.annotations.media.Content -import io.swagger.v3.oas.annotations.media.Schema -import io.swagger.v3.oas.annotations.responses.ApiResponse -import io.swagger.v3.oas.annotations.responses.ApiResponses +import authentication.users.UsersService +import io.swagger.v3.oas.annotations.* import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.security.authentication.* import org.springframework.security.core.userdetails.UserDetailsService @@ -13,72 +10,38 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.web.bind.annotation.* import java.security.Principal -@Tag(name = "AUTHENTICATION", description = "Endpoints related to user login and token validation") +@Tag(name = "AUTHENTICATION") @RestController -@RequestMapping("/auth") +@RequestMapping("/auth/v1") class AuthenticationController( private val authenticationManager: AuthenticationManager, private val userDetailsService: UserDetailsService, - private val jwtService: JwtService + private val jwtService: JwtService, + private val usersService: UsersService ) { - @Operation( - summary = "Login and generate JWT token", - description = "Accepts username and password and returns a JWT token upon successful authentication" - ) - @ApiResponses( - value = [ - ApiResponse( - responseCode = "200", - description = "Successfully authenticated", - content = [Content(schema = Schema(implementation = AuthenticationResponse::class))] - ), - ApiResponse( - responseCode = "400", - description = "Invalid username or password", - content = [Content()] - ) - ] - ) @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) + val token = jwtService.generateToken(authRequest.username) return AuthenticationResponse(token) } else { - throw UsernameNotFoundException("Invalid user request!") + throw UsernameNotFoundException("Invalid login request.") } } - @Operation( - summary = "Check token validity", - description = "Validates the JWT token provided in the Authorization header" - ) - @ApiResponses( - value = [ - ApiResponse(responseCode = "200", description = "Token is valid"), - ApiResponse(responseCode = "401", description = "Invalid or expired token") - ] - ) @PostMapping("/check-token") fun checkToken(principal: Principal): TokenCheckResponse { - return TokenCheckResponse(username = principal.name) + println("πŸ” Received token for user: ${principal.name}") + val user = usersService.findByUsername(principal.name) + println("βœ… Token is valid, userId=${user.id}") + return TokenCheckResponse(userId = user.id!!) } } -data class AuthenticationRequest( - val username: String, - val password: String -) - -data class AuthenticationResponse( - val token: String -) - -data class TokenCheckResponse( - val username: String -) \ No newline at end of file +data class AuthenticationRequest(val username: String, val password: String) +data class AuthenticationResponse(val token: String) +data class TokenCheckResponse(val userId: Long) \ No newline at end of file diff --git a/authentication/src/main/kotlin/authentication/security/SecurityConfig.kt b/authentication/src/main/kotlin/authentication/security/SecurityConfig.kt index 9b9ed90..301d536 100644 --- a/authentication/src/main/kotlin/authentication/security/SecurityConfig.kt +++ b/authentication/src/main/kotlin/authentication/security/SecurityConfig.kt @@ -27,8 +27,8 @@ class SecurityConfig( fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { http.csrf { it.disable() } .authorizeHttpRequests { - it.requestMatchers("/auth/v1/login").permitAll() - it.requestMatchers("/auth/v1/register").permitAll() + it.requestMatchers("/auth/**").permitAll() + it.requestMatchers("/users/v1/register").permitAll() .anyRequest().authenticated() } .sessionManagement { diff --git a/authentication/src/main/kotlin/authentication/users/UsersService.kt b/authentication/src/main/kotlin/authentication/users/UsersService.kt index 91c0b0a..fd25a90 100644 --- a/authentication/src/main/kotlin/authentication/users/UsersService.kt +++ b/authentication/src/main/kotlin/authentication/users/UsersService.kt @@ -1,5 +1,7 @@ package authentication.users + import jakarta.inject.Named +import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Service @@ -16,41 +18,36 @@ class UsersService( ) { fun registerUser(request: UserRequest): UserResponse { - - if (request.username.length < USERNAME_MIN_LENGTH || - request.username.length > USERNAME_MAX_LENGTH - ) { - throw TransferFundsException( - "Username must be between $USERNAME_MIN_LENGTH and $USERNAME_MAX_LENGTH characters") + if (request.username.length !in USERNAME_MIN_LENGTH..USERNAME_MAX_LENGTH) { + throw IllegalArgumentException("Username must be between $USERNAME_MIN_LENGTH and $USERNAME_MAX_LENGTH characters") } - if (request.password.length < PASSWORD_MIN_LENGTH || - request.password.length > PASSWORD_MAX_LENGTH - ) { - throw TransferFundsException( - "Password must be between $PASSWORD_MIN_LENGTH and $PASSWORD_MAX_LENGTH characters") + if (request.password.length !in PASSWORD_MIN_LENGTH..PASSWORD_MAX_LENGTH) { + throw IllegalArgumentException("Password must be between $PASSWORD_MIN_LENGTH and $PASSWORD_MAX_LENGTH characters") } - val encodedPassword = passwordEncoder.encode(request.password) - - val createUser = UserEntity( + val user = UserEntity( name = request.name, age = request.age, username = request.username, - password = request.password + password = passwordEncoder.encode(request.password) ) - val savedUser = usersRepository.save(createUser) - return UserResponse(id = savedUser.id!!, username = savedUser.username) + val saved = usersRepository.save(user) + return UserResponse(id = saved.id!!, username = saved.username) } - fun listUsers(): List = usersRepository.findAll().map { - UserRequest( - name = it.name, - age = it.age, - username = it.username, - password = it.password + fun findByUsername(username: String): UserEntity { + return usersRepository.findByUsername(username) + ?: throw UsernameNotFoundException("User not found for username: $username") + } - ) + fun listUsers(): List { + return usersRepository.findAll().map { + UserResponse( + id = it.id ?: 0, + username = it.username + ) + } } } \ No newline at end of file diff --git a/ordering/pom.xml b/ordering/pom.xml index 88e4c58..2bac8ac 100644 --- a/ordering/pom.xml +++ b/ordering/pom.xml @@ -11,19 +11,52 @@ ordering - - - - - - - - - - - - - - + + + + org.springframework.boot + spring-boot-starter-web + + + + org.hibernate.validator + hibernate-validator + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + org.postgresql + postgresql + runtime + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + com.fasterxml.jackson.module + jackson-module-kotlin + + + + + org.jetbrains.kotlin + kotlin-reflect + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + + \ No newline at end of file diff --git a/ordering/src/main/kotlin/client/AuthenticationClient.kt b/ordering/src/main/kotlin/client/AuthenticationClient.kt deleted file mode 100644 index 1268ad9..0000000 --- a/ordering/src/main/kotlin/client/AuthenticationClient.kt +++ /dev/null @@ -1,34 +0,0 @@ -package ordering.client - -import org.springframework.core.ParameterizedTypeReference -import org.springframework.http.* -import org.springframework.stereotype.Component -import org.springframework.web.client.RestTemplate - -data class CheckTokenResponse(val userId: Long) - -@Component -class AuthenticationClient { - - private val restTemplate = RestTemplate() - private val authServerUrl = "http://localhost:8081/auth/check-token" - - fun checkToken(token: String): CheckTokenResponse { - val headers = HttpHeaders() - headers.setBearerAuth(token) - val requestEntity = HttpEntity(headers) - - val response = restTemplate.exchange( - authServerUrl, - HttpMethod.POST, - requestEntity, - object : ParameterizedTypeReference() {} - ) - - if (response.statusCode != HttpStatus.OK) { - throw IllegalStateException("Invalid token") - } - - return response.body ?: throw IllegalStateException("Missing response body from auth service") - } -} \ No newline at end of file diff --git a/ordering/src/main/kotlin/OrderingApplication.kt b/ordering/src/main/kotlin/order/OrderingApplication.kt similarity index 65% rename from ordering/src/main/kotlin/OrderingApplication.kt rename to ordering/src/main/kotlin/order/OrderingApplication.kt index 43f86bf..c541d92 100644 --- a/ordering/src/main/kotlin/OrderingApplication.kt +++ b/ordering/src/main/kotlin/order/OrderingApplication.kt @@ -1,11 +1,9 @@ -package ordering +package order import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication -import org.springframework.context.annotation.ComponentScan @SpringBootApplication -@ComponentScan(basePackages = ["ordering", "items"]) class OrderingApplication fun main(args: Array) { diff --git a/ordering/src/main/kotlin/order/client/AuthenticationClient.kt b/ordering/src/main/kotlin/order/client/AuthenticationClient.kt new file mode 100644 index 0000000..f20357a --- /dev/null +++ b/ordering/src/main/kotlin/order/client/AuthenticationClient.kt @@ -0,0 +1,47 @@ +package order.client + +import org.springframework.core.ParameterizedTypeReference +import org.springframework.http.* +import org.springframework.stereotype.Component +import org.springframework.web.client.RestTemplate + +data class CheckTokenResponse(val userId: Long) + +@Component +class AuthenticationClient { + + private val restTemplate = RestTemplate() + private val authServerUrl = "http://localhost:8081/auth/v1/check-token" + + fun checkToken(token: String): CheckTokenResponse { + println("πŸ” [AuthenticationClient] Checking token...") + + val headers = HttpHeaders() + headers.setBearerAuth(token) + val requestEntity = HttpEntity(headers) + + println("πŸ“€ Sending request to auth server at $authServerUrl with token: $token") + + try { + val response = restTemplate.exchange( + authServerUrl, + HttpMethod.POST, + requestEntity, + object : ParameterizedTypeReference() {} + ) + + println("βœ… Received response from auth server: ${response.statusCode}") + println("πŸ“¦ Response body: ${response.body}") + + if (response.statusCode != HttpStatus.OK) { + throw IllegalStateException("❌ Invalid token, status: ${response.statusCode}") + } + + return response.body ?: throw IllegalStateException("❌ Missing response body from auth service") + + } catch (ex: Exception) { + println("πŸ”₯ Exception while calling auth server: ${ex.message}") + throw IllegalStateException("Failed to validate token: ${ex.message}", ex) + } + } +} \ No newline at end of file diff --git a/ordering/src/main/kotlin/config/LoggingFilter.kt b/ordering/src/main/kotlin/order/config/LoggingFilter.kt similarity index 98% rename from ordering/src/main/kotlin/config/LoggingFilter.kt rename to ordering/src/main/kotlin/order/config/LoggingFilter.kt index 72ae1db..fd024ee 100644 --- a/ordering/src/main/kotlin/config/LoggingFilter.kt +++ b/ordering/src/main/kotlin/order/config/LoggingFilter.kt @@ -1,4 +1,4 @@ -package ordering.config +package order.config import jakarta.servlet.FilterChain import jakarta.servlet.http.HttpServletRequest diff --git a/ordering/src/main/kotlin/menu/MenuController.kt b/ordering/src/main/kotlin/order/menu/MenuController.kt similarity index 93% rename from ordering/src/main/kotlin/menu/MenuController.kt rename to ordering/src/main/kotlin/order/menu/MenuController.kt index afae35a..9afb089 100644 --- a/ordering/src/main/kotlin/menu/MenuController.kt +++ b/ordering/src/main/kotlin/order/menu/MenuController.kt @@ -1,4 +1,4 @@ -package com.coded.spring.ordering.menu +package order.menu import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.responses.ApiResponse @@ -13,7 +13,7 @@ class MenuController( private val menuService: MenuService ) { - @GetMapping("/menu/v1/list") + @GetMapping("/order/menu/v1/list") @Operation( summary = "List all menu items for homepage", description = "Returns all menu entries including discounts if active" diff --git a/ordering/src/main/kotlin/menu/MenuRepository.kt b/ordering/src/main/kotlin/order/menu/MenuRepository.kt similarity index 88% rename from ordering/src/main/kotlin/menu/MenuRepository.kt rename to ordering/src/main/kotlin/order/menu/MenuRepository.kt index 0bcd27d..1f3501c 100644 --- a/ordering/src/main/kotlin/menu/MenuRepository.kt +++ b/ordering/src/main/kotlin/order/menu/MenuRepository.kt @@ -1,4 +1,4 @@ -package com.coded.spring.ordering.menu +package order.menu import jakarta.persistence.* import org.springframework.data.jpa.repository.JpaRepository @@ -7,7 +7,7 @@ import org.springframework.stereotype.Repository @Repository interface MenuRepository : JpaRepository @Entity -@Table(name = "menu") +@Table(name = "order/menu") data class MenuEntity( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/ordering/src/main/kotlin/menu/MenuService.kt b/ordering/src/main/kotlin/order/menu/MenuService.kt similarity index 67% rename from ordering/src/main/kotlin/menu/MenuService.kt rename to ordering/src/main/kotlin/order/menu/MenuService.kt index c55b913..99e2f8f 100644 --- a/ordering/src/main/kotlin/menu/MenuService.kt +++ b/ordering/src/main/kotlin/order/menu/MenuService.kt @@ -1,6 +1,8 @@ -package com.coded.spring.ordering.menu +package order.menu import jakarta.inject.Named +import order.menu.MenuEntity +import order.menu.MenuRepository @Named class MenuService( diff --git a/ordering/src/main/kotlin/items/ItemsRepository.kt b/ordering/src/main/kotlin/order/orders/ItemsRepository.kt similarity index 97% rename from ordering/src/main/kotlin/items/ItemsRepository.kt rename to ordering/src/main/kotlin/order/orders/ItemsRepository.kt index 2b0f4c1..dc27389 100644 --- a/ordering/src/main/kotlin/items/ItemsRepository.kt +++ b/ordering/src/main/kotlin/order/orders/ItemsRepository.kt @@ -1,4 +1,4 @@ -package items +package order.orders import jakarta.persistence.* import org.springframework.data.jpa.repository.JpaRepository diff --git a/ordering/src/main/kotlin/orders/OrderController.kt b/ordering/src/main/kotlin/order/orders/OrderController.kt similarity index 68% rename from ordering/src/main/kotlin/orders/OrderController.kt rename to ordering/src/main/kotlin/order/orders/OrderController.kt index 9b826f2..c50dfb6 100644 --- a/ordering/src/main/kotlin/orders/OrderController.kt +++ b/ordering/src/main/kotlin/order/orders/OrderController.kt @@ -1,9 +1,5 @@ -package orders +package order.orders -import ordering.orders.Order -import ordering.orders.SubmitOrderRequest -import ordering.orders.OrdersRepository -import ordering.orders.OrdersService import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses @@ -11,32 +7,32 @@ import io.swagger.v3.oas.annotations.tags.Tag import jakarta.servlet.http.HttpServletRequest import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* -import java.security.Principal @RestController -@RequestMapping("/orders/v1") +@RequestMapping("/order/orders/v1") @Tag(name = "ORDERING", description = "Endpoints related to order submission and retrieval") class OrderController( - val ordersRepository: OrdersRepository, + private val ordersRepository: OrdersRepository, private val ordersService: OrdersService ) { - @PostMapping - @Operation(summary = "Submit a new order", description = "Submits a new order for the logged-in user") + @PostMapping("/create") + @Operation(summary = "Create a new order", description = "Creates a new order along with items") @ApiResponses( value = [ - ApiResponse(responseCode = "200", description = "Order submitted successfully"), - ApiResponse(responseCode = "400", description = "Invalid request format"), + ApiResponse(responseCode = "200", description = "Order created successfully"), + ApiResponse(responseCode = "400", description = "Invalid input"), ApiResponse(responseCode = "403", description = "Unauthorized access") ] ) - fun submitOrder( - @RequestBody request: SubmitOrderRequest, + fun createOrder( + @RequestBody request: CreateOrderRequest, servletRequest: HttpServletRequest - ): ResponseEntity { + ): ResponseEntity { + println("πŸ”₯ [OrderController] createOrder() hit") val userId = servletRequest.getAttribute("userId") as Long - ordersService.submitOrder(userId, request.itemIds) - return ResponseEntity.ok("Order submitted successfully.") + val order = ordersService.createOrder(userId, request.items) + return ResponseEntity.ok(order) } @GetMapping diff --git a/ordering/src/main/kotlin/order/orders/OrderDTO.kt b/ordering/src/main/kotlin/order/orders/OrderDTO.kt new file mode 100644 index 0000000..9514bdc --- /dev/null +++ b/ordering/src/main/kotlin/order/orders/OrderDTO.kt @@ -0,0 +1,20 @@ +package order.orders + +data class Item( + val id: Long?, + val orderId: Long?, + val name: String?, + val quantity: Long?, + val note: String?, + val price: Double? +) + +data class Order( + val id: Long?, + val userId: Long?, + val items: List +) + +data class CreateOrderRequest( + val items: List +) diff --git a/ordering/src/main/kotlin/orders/OrdersRepository.kt b/ordering/src/main/kotlin/order/orders/OrdersRepository.kt similarity index 95% rename from ordering/src/main/kotlin/orders/OrdersRepository.kt rename to ordering/src/main/kotlin/order/orders/OrdersRepository.kt index 2ca6f94..3acbe00 100644 --- a/ordering/src/main/kotlin/orders/OrdersRepository.kt +++ b/ordering/src/main/kotlin/order/orders/OrdersRepository.kt @@ -1,4 +1,4 @@ -package ordering.orders +package order.orders import jakarta.persistence.* import org.springframework.data.jpa.repository.JpaRepository diff --git a/ordering/src/main/kotlin/order/orders/OrdersService.kt b/ordering/src/main/kotlin/order/orders/OrdersService.kt new file mode 100644 index 0000000..cb2c4e2 --- /dev/null +++ b/ordering/src/main/kotlin/order/orders/OrdersService.kt @@ -0,0 +1,64 @@ +package order.orders + +import jakarta.inject.Named + +@Named +class OrdersService( + private val ordersRepository: OrdersRepository, + private val itemsRepository: ItemsRepository +) { + + fun createOrder(userId: Long, items: List): Order { + val newOrder = ordersRepository.save(OrderEntity(userId = userId)) + + val itemEntities = items.map { + ItemEntity( + name = it.name, + quantity = it.quantity, + note = it.note, + price = it.price, + orderId = newOrder.id!! + ) + } + + itemsRepository.saveAll(itemEntities) + + return Order( + id = newOrder.id!!, + userId = userId, + items = itemEntities.map { + Item( + id = it.id, + orderId = it.orderId, + name = it.name, + quantity = it.quantity, + note = it.note, + price = it.price + ) + } + ) + } + + fun listOrdersForUser(userId: Long): List { + return ordersRepository.findByUserId(userId).map { orderEntity -> + val itemEntities = itemsRepository.findByOrderId(orderEntity.id!!) + + val items = itemEntities.map { + Item( + id = it.id, + orderId = it.orderId, + name = it.name, + quantity = it.quantity, + note = it.note, + price = it.price + ) + } + + Order( + id = orderEntity.id!!, + userId = userId, + items = items + ) + } + } +} \ No newline at end of file diff --git a/ordering/src/main/kotlin/security/RemoteAuthenticationFilter.kt b/ordering/src/main/kotlin/order/security/RemoteAuthenticationFilter.kt similarity index 52% rename from ordering/src/main/kotlin/security/RemoteAuthenticationFilter.kt rename to ordering/src/main/kotlin/order/security/RemoteAuthenticationFilter.kt index cde6e73..581f6ac 100644 --- a/ordering/src/main/kotlin/security/RemoteAuthenticationFilter.kt +++ b/ordering/src/main/kotlin/order/security/RemoteAuthenticationFilter.kt @@ -1,12 +1,11 @@ -package ordering.security +package order.security import jakarta.servlet.FilterChain import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse -import ordering.client.AuthenticationClient +import order.client.AuthenticationClient import org.springframework.stereotype.Component import org.springframework.web.filter.OncePerRequestFilter - @Component class RemoteAuthenticationFilter( private val authenticationClient: AuthenticationClient @@ -17,18 +16,31 @@ class RemoteAuthenticationFilter( response: HttpServletResponse, filterChain: FilterChain ) { - logger.info("Remote authentication filter executing...") + println("βœ… RemoteAuthenticationFilter START") val authHeader = request.getHeader("Authorization") + println("🟨 Authorization Header: $authHeader") + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + println("⚠️ No Bearer token found. Skipping auth.") filterChain.doFilter(request, response) return } val token = authHeader.removePrefix("Bearer ").trim() - val result = authenticationClient.checkToken(token) + println("πŸ”‘ Extracted Token: $token") + + try { + val result = authenticationClient.checkToken(token) + println("βœ… Token validated. userId=${result.userId}") + request.setAttribute("userId", result.userId) + } catch (ex: Exception) { + println("❌ Token validation failed: ${ex.message}") + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token") + return + } - request.setAttribute("userId", result.userId) + println("➑️ Proceeding with filter chain") filterChain.doFilter(request, response) } } \ No newline at end of file diff --git a/ordering/src/main/kotlin/security/SecurityConfig.kt b/ordering/src/main/kotlin/order/security/SecurityConfig.kt similarity index 97% rename from ordering/src/main/kotlin/security/SecurityConfig.kt rename to ordering/src/main/kotlin/order/security/SecurityConfig.kt index 6537494..556c8b1 100644 --- a/ordering/src/main/kotlin/security/SecurityConfig.kt +++ b/ordering/src/main/kotlin/order/security/SecurityConfig.kt @@ -1,4 +1,4 @@ -package ordering.security +package order.security import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration diff --git a/ordering/src/main/kotlin/orders/OrderDTO.kt b/ordering/src/main/kotlin/orders/OrderDTO.kt deleted file mode 100644 index 1d97011..0000000 --- a/ordering/src/main/kotlin/orders/OrderDTO.kt +++ /dev/null @@ -1,14 +0,0 @@ -package ordering.orders - -import items.Item - -data class SubmitOrderRequest( - val itemIds: List -) - -data class Order( - val id: Long?, - val userId: Long?, - val items: List -) - diff --git a/ordering/src/main/kotlin/orders/OrdersService.kt b/ordering/src/main/kotlin/orders/OrdersService.kt deleted file mode 100644 index a2ad87f..0000000 --- a/ordering/src/main/kotlin/orders/OrdersService.kt +++ /dev/null @@ -1,46 +0,0 @@ -package ordering.orders - -import items.Item -import items.ItemEntity -import items.ItemsRepository -import jakarta.inject.Named - -@Named -class OrdersService( - private val ordersRepository: OrdersRepository, - private val itemsRepository: ItemsRepository -) { - - fun submitOrder(userId: Long, itemIds: List) { - val order = ordersRepository.save(OrderEntity(userId = userId)) - - val updatedItems = itemsRepository.findAllById(itemIds).map { itemEntity -> - itemEntity.copy(orderId = order.id!!) - } - - itemsRepository.saveAll(updatedItems) - } - - fun listOrdersForUser(userId: Long): List { - return ordersRepository.findByUserId(userId).map { orderEntity -> - val itemEntities = itemsRepository.findByOrderId(orderEntity.id!!) - - val items = itemEntities.map { - Item( - id = it.id, - orderId = it.orderId, - name = it.name, - quantity = it.quantity, - note = it.note, - price = it.price - ) - } - - Order( - id = orderEntity.id!!, - userId = userId, - items = items - ) - } - } -} \ No newline at end of file diff --git a/ordering/src/main/resources/application.properties b/ordering/src/main/resources/application.properties index b5b187e..2a25485 100644 --- a/ordering/src/main/resources/application.properties +++ b/ordering/src/main/resources/application.properties @@ -6,4 +6,10 @@ spring.datasource.username=postgres spring.datasource.password=yosaka spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +management.endpoints.web.exposure.include=* + springdoc.api-docs.path=/api-docs + +logging.level.org.springframework.web=DEBUG +logging.level.org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod=DEBUG +spring.mvc.log-request-details=true diff --git a/ordering/src/main/kotlin/items/ItemDTO.kt b/src/main/kotlin/com/coded/spring/ordering/items/ItemDTO.kt similarity index 99% rename from ordering/src/main/kotlin/items/ItemDTO.kt rename to src/main/kotlin/com/coded/spring/ordering/items/ItemDTO.kt index 8173250..b0db87e 100644 --- a/ordering/src/main/kotlin/items/ItemDTO.kt +++ b/src/main/kotlin/com/coded/spring/ordering/items/ItemDTO.kt @@ -1,6 +1,5 @@ package items - data class Item( val id: Long?, val orderId: Long?, diff --git a/ordering/src/main/kotlin/items/ItemsController.kt b/src/main/kotlin/com/coded/spring/ordering/items/ItemsController.kt similarity index 96% rename from ordering/src/main/kotlin/items/ItemsController.kt rename to src/main/kotlin/com/coded/spring/ordering/items/ItemsController.kt index d868511..a8835b7 100644 --- a/ordering/src/main/kotlin/items/ItemsController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/items/ItemsController.kt @@ -5,7 +5,8 @@ import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.tags.Tag -import ordering.client.AuthenticationClient +import client.AuthenticationClient +import orders.ItemEntity import org.springframework.web.bind.annotation.* @RestController diff --git a/ordering/src/main/kotlin/items/ItemsService.kt b/src/main/kotlin/com/coded/spring/ordering/items/ItemsService.kt similarity index 90% rename from ordering/src/main/kotlin/items/ItemsService.kt rename to src/main/kotlin/com/coded/spring/ordering/items/ItemsService.kt index 2119e59..0b7c85e 100644 --- a/ordering/src/main/kotlin/items/ItemsService.kt +++ b/src/main/kotlin/com/coded/spring/ordering/items/ItemsService.kt @@ -1,9 +1,11 @@ package items -import jakarta.inject.Named +import orders.ItemEntity +import orders.ItemsRepository import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service -@Named +@Service class ItemsService( private val itemsRepository: ItemsRepository, @Value("\${festive-mode:false}") diff --git a/welcome/src/main/kotlin/helloworld/HelloWorldController.kt b/welcome/src/main/kotlin/helloworld/HelloWorldController.kt index 4fa91e2..97c1651 100644 --- a/welcome/src/main/kotlin/helloworld/HelloWorldController.kt +++ b/welcome/src/main/kotlin/helloworld/HelloWorldController.kt @@ -1,4 +1,4 @@ -package com.coded.spring.ordering.helloworld +package helloworld import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.responses.ApiResponse