From ac82918541ae4e960377da58f3b47ed77544d2ae Mon Sep 17 00:00:00 2001 From: HAlGhanim Date: Tue, 8 Apr 2025 17:47:19 +0300 Subject: [PATCH 01/10] Done with task 1 and 2 --- pom.xml | 14 +++++++- .../spring/ordering/HelloWorldController.kt | 34 +++++++++++++++++++ .../coded/spring/ordering/OrderController.kt | 30 ++++++++++++++++ .../coded/spring/ordering/OrderRepository.kt | 28 +++++++++++++++ .../coded/spring/ordering/UserRepository.kt | 19 +++++++++++ 5 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/OrderController.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/OrderRepository.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/UserRepository.kt diff --git a/pom.xml b/pom.xml index 163ad53..2ff08d9 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,7 @@ 1.9.25 + org.springframework.boot spring-boot-starter-web @@ -47,7 +48,18 @@ org.jetbrains.kotlin kotlin-stdlib - + + jakarta.inject + jakarta.inject-api + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.h2database + h2 + org.springframework.boot spring-boot-starter-test diff --git a/src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt b/src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt new file mode 100644 index 0000000..ca51eec --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt @@ -0,0 +1,34 @@ +package com.coded.spring.ordering + +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.RestController + +@RestController +class HelloWorldController(val usersRepository: UsersRepository){ + + @GetMapping("/api/hello") + fun helloWorld() = "Hello World" + + @PostMapping("/api/message") + fun printMyMessage(@RequestBody request: PrintMyMessageRequest): String { + return "your name is ${request.name} and civilId is ${request.civilId}" + } + + @PostMapping("/api/my-name") + fun sayMyName( @RequestBody request: SayMyNameRequest): String { + usersRepository.save(User(name = request.name)) + return "Welcome ${request.name} !" + } + + @GetMapping("/api/users") + fun getAllUsers(): List = usersRepository.findAll() +} +data class SayMyNameRequest( + val name: String +) +data class PrintMyMessageRequest( + val name: String, + val civilId: String +) \ No newline at end of file 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..03b6b3a --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/OrderController.kt @@ -0,0 +1,30 @@ +package com.coded.spring.ordering + +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.RestController + +@RestController +class OrderController(val orderRepository: OrderRepository){ + + @GetMapping("/api/get-orders") + fun listOrders(): List { + return orderRepository.findAll() + } + + @PostMapping("/api/orders") + fun sayMyName( @RequestBody request: OrderRequest): Order { + val order = Order( + username = request.username, + restaurant = request.restaurant, + items = request.items,) + return orderRepository.save(order) + } + + data class OrderRequest( + val username: String, + val restaurant: String, + val items: List, + ) +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/OrderRepository.kt b/src/main/kotlin/com/coded/spring/ordering/OrderRepository.kt new file mode 100644 index 0000000..943a1d4 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/OrderRepository.kt @@ -0,0 +1,28 @@ +package com.coded.spring.ordering + +import jakarta.persistence.* +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface OrderRepository : JpaRepository + +@Entity +@Table(name = "orders") +data class Order( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, + + val username: String, + val restaurant: String, + + @ElementCollection + @CollectionTable(name = "order_items", joinColumns = [JoinColumn(name = "order_id")]) + @Column(name = "item") + val items: List, + + ) { + constructor() : this(null, "", "", emptyList()) + +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/UserRepository.kt b/src/main/kotlin/com/coded/spring/ordering/UserRepository.kt new file mode 100644 index 0000000..4e98131 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/UserRepository.kt @@ -0,0 +1,19 @@ +package com.coded.spring.ordering + +import jakarta.inject.Named +import jakarta.persistence.* +import org.springframework.data.jpa.repository.JpaRepository + +@Named +interface UsersRepository : JpaRepository + +@Entity +@Table(name = "users") +data class User( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + var name: String +){ + constructor() : this(null, "") +} \ No newline at end of file From 3329e2a22f7c714d4c0cfee35be7ee00d395cd9a Mon Sep 17 00:00:00 2001 From: HAlGhanim Date: Wed, 9 Apr 2025 12:44:53 +0300 Subject: [PATCH 02/10] Done with task 3 --- pom.xml | 4 +++ .../spring/ordering/users/OrderController2.kt | 30 +++++++++++++++++++ .../spring/ordering/users/OrderRepository2.kt | 26 ++++++++++++++++ .../spring/ordering/users/OrderService.kt | 27 +++++++++++++++++ .../spring/ordering/users/UsersController.kt | 14 +++++++++ .../spring/ordering/users/UsersRepository2.kt | 20 +++++++++++++ .../spring/ordering/users/UsersService.kt | 21 +++++++++++++ src/main/resources/application.properties | 5 ++++ 8 files changed, 147 insertions(+) create mode 100644 src/main/kotlin/com/coded/spring/ordering/users/OrderController2.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/users/OrderRepository2.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/users/OrderService.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/UsersRepository2.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/users/UsersService.kt diff --git a/pom.xml b/pom.xml index 2ff08d9..bf2915c 100644 --- a/pom.xml +++ b/pom.xml @@ -60,6 +60,10 @@ com.h2database h2 + + org.postgresql + postgresql + org.springframework.boot spring-boot-starter-test diff --git a/src/main/kotlin/com/coded/spring/ordering/users/OrderController2.kt b/src/main/kotlin/com/coded/spring/ordering/users/OrderController2.kt new file mode 100644 index 0000000..b034ca7 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/users/OrderController2.kt @@ -0,0 +1,30 @@ +package com.coded.spring.ordering.users + +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.RestController + +@RestController +class OrderController2(private val orderService: OrderService ){ + + @GetMapping("/api/get-orders2") + fun listOrders(): List { + return orderService.listUsers() + } + + @PostMapping("/api/orders2") + fun sayMyName( @RequestBody request: OrderRequest): Order2 { + val order = Order2( + username = request.username, + restaurant = request.restaurant, + items = request.items,) + return orderService.saveOrder(order) + } + + data class OrderRequest( + val username: String, + val restaurant: String, + val items: List, + ) +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/users/OrderRepository2.kt b/src/main/kotlin/com/coded/spring/ordering/users/OrderRepository2.kt new file mode 100644 index 0000000..b0d7579 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/users/OrderRepository2.kt @@ -0,0 +1,26 @@ +package com.coded.spring.ordering.users + +import jakarta.persistence.* +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface OrderRepository2 : JpaRepository + +@Entity +@Table(name = "orders") +data class Order2( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, + + val username: String, + val restaurant: String, + + @Column(name = "items", columnDefinition = "text[]") + val items: List, + + ) { + constructor() : this(null, "", "", emptyList()) + +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/users/OrderService.kt b/src/main/kotlin/com/coded/spring/ordering/users/OrderService.kt new file mode 100644 index 0000000..d9dc4ef --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/users/OrderService.kt @@ -0,0 +1,27 @@ +package com.coded.spring.ordering.users + +import jakarta.inject.Named + +@Named +class OrderService( + private val orderRepository2: OrderRepository2 +) { + + fun listUsers(): List = orderRepository2.findAll().map { + Order( + username = it.username, + restaurant = it.restaurant, + items = it.items + ) + } + + fun saveOrder(order: Order2): Order2 { + return orderRepository2.save(order) + } +} + +data class Order( + val username: String, + val restaurant: String, + val items: List, +) \ 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..1d79780 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/users/UsersController.kt @@ -0,0 +1,14 @@ +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/UsersRepository2.kt b/src/main/kotlin/com/coded/spring/ordering/users/UsersRepository2.kt new file mode 100644 index 0000000..4a917ea --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/users/UsersRepository2.kt @@ -0,0 +1,20 @@ +package com.coded.spring.ordering.users + +import jakarta.inject.Named +import jakarta.persistence.* +import org.springframework.data.jpa.repository.JpaRepository + +@Named +interface UsersRepository2 : JpaRepository + +@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/UsersService.kt b/src/main/kotlin/com/coded/spring/ordering/users/UsersService.kt new file mode 100644 index 0000000..3740a9a --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/users/UsersService.kt @@ -0,0 +1,21 @@ +package com.coded.spring.ordering.users + +import jakarta.inject.Named + +@Named +class UsersService( + private val usersRepository: UsersRepository2, +) { + + 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 3704dc6..2e30afc 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,6 @@ spring.application.name=Kotlin.SpringbootV2 + +spring.datasource.url=jdbc:postgresql://localhost:5432/orderingDB +spring.datasource.username=postgres +spring.datasource.password= Lockz1998! +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect From cb53b9b47c9dff6ecb529f5f8a94dcb7400301ba Mon Sep 17 00:00:00 2001 From: HAlGhanim Date: Mon, 21 Apr 2025 21:09:09 +0300 Subject: [PATCH 03/10] All tasks done except profiles --- pom.xml | 50 +++++++++++++++---- .../spring/ordering/HelloWorldController.kt | 34 ------------- .../coded/spring/ordering/OrderController.kt | 30 ----------- .../coded/spring/ordering/OrderRepository.kt | 28 ----------- .../coded/spring/ordering/UserRepository.kt | 19 ------- .../spring/ordering/users/OrderController2.kt | 30 ----------- .../spring/ordering/users/OrderRepository2.kt | 26 ---------- .../spring/ordering/users/OrderService.kt | 27 ---------- .../spring/ordering/users/UsersController.kt | 14 ------ .../spring/ordering/users/UsersRepository2.kt | 20 -------- .../spring/ordering/users/UsersService.kt | 21 -------- .../coded/spring/ordering/ApplicationTests.kt | 32 +++++++++++- 12 files changed, 69 insertions(+), 262 deletions(-) delete mode 100644 src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt delete mode 100644 src/main/kotlin/com/coded/spring/ordering/OrderController.kt delete mode 100644 src/main/kotlin/com/coded/spring/ordering/OrderRepository.kt delete mode 100644 src/main/kotlin/com/coded/spring/ordering/UserRepository.kt delete mode 100644 src/main/kotlin/com/coded/spring/ordering/users/OrderController2.kt delete mode 100644 src/main/kotlin/com/coded/spring/ordering/users/OrderRepository2.kt delete mode 100644 src/main/kotlin/com/coded/spring/ordering/users/OrderService.kt delete mode 100644 src/main/kotlin/com/coded/spring/ordering/users/UsersController.kt delete mode 100644 src/main/kotlin/com/coded/spring/ordering/users/UsersRepository2.kt delete mode 100644 src/main/kotlin/com/coded/spring/ordering/users/UsersService.kt diff --git a/pom.xml b/pom.xml index bf2915c..5aca940 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,6 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.springframework.boot @@ -8,11 +8,11 @@ 3.4.4 - com.coded.spring - Ordering + com.example + authentication 0.0.1-SNAPSHOT - Kotlin.SpringbootV2 - Kotlin.SpringbootV2 + authentication + Demo project for Spring Boot @@ -31,6 +31,27 @@ 1.9.25 + + 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 + org.springframework.boot @@ -48,21 +69,18 @@ org.jetbrains.kotlin kotlin-stdlib - - jakarta.inject - jakarta.inject-api - org.springframework.boot spring-boot-starter-data-jpa - com.h2database - h2 + jakarta.inject + jakarta.inject-api org.postgresql postgresql + runtime org.springframework.boot @@ -74,6 +92,16 @@ kotlin-test-junit5 test + + org.springframework.security + spring-security-test + test + + + com.h2database + h2 + test + diff --git a/src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt b/src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt deleted file mode 100644 index ca51eec..0000000 --- a/src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.coded.spring.ordering - -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.RestController - -@RestController -class HelloWorldController(val usersRepository: UsersRepository){ - - @GetMapping("/api/hello") - fun helloWorld() = "Hello World" - - @PostMapping("/api/message") - fun printMyMessage(@RequestBody request: PrintMyMessageRequest): String { - return "your name is ${request.name} and civilId is ${request.civilId}" - } - - @PostMapping("/api/my-name") - fun sayMyName( @RequestBody request: SayMyNameRequest): String { - usersRepository.save(User(name = request.name)) - return "Welcome ${request.name} !" - } - - @GetMapping("/api/users") - fun getAllUsers(): List = usersRepository.findAll() -} -data class SayMyNameRequest( - val name: String -) -data class PrintMyMessageRequest( - val name: String, - val civilId: String -) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/OrderController.kt b/src/main/kotlin/com/coded/spring/ordering/OrderController.kt deleted file mode 100644 index 03b6b3a..0000000 --- a/src/main/kotlin/com/coded/spring/ordering/OrderController.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.coded.spring.ordering - -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.RestController - -@RestController -class OrderController(val orderRepository: OrderRepository){ - - @GetMapping("/api/get-orders") - fun listOrders(): List { - return orderRepository.findAll() - } - - @PostMapping("/api/orders") - fun sayMyName( @RequestBody request: OrderRequest): Order { - val order = Order( - username = request.username, - restaurant = request.restaurant, - items = request.items,) - return orderRepository.save(order) - } - - data class OrderRequest( - val username: String, - val restaurant: String, - val items: List, - ) -} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/OrderRepository.kt b/src/main/kotlin/com/coded/spring/ordering/OrderRepository.kt deleted file mode 100644 index 943a1d4..0000000 --- a/src/main/kotlin/com/coded/spring/ordering/OrderRepository.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.coded.spring.ordering - -import jakarta.persistence.* -import org.springframework.data.jpa.repository.JpaRepository -import org.springframework.stereotype.Repository - -@Repository -interface OrderRepository : JpaRepository - -@Entity -@Table(name = "orders") -data class Order( - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - val id: Long? = null, - - val username: String, - val restaurant: String, - - @ElementCollection - @CollectionTable(name = "order_items", joinColumns = [JoinColumn(name = "order_id")]) - @Column(name = "item") - val items: List, - - ) { - constructor() : this(null, "", "", emptyList()) - -} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/UserRepository.kt b/src/main/kotlin/com/coded/spring/ordering/UserRepository.kt deleted file mode 100644 index 4e98131..0000000 --- a/src/main/kotlin/com/coded/spring/ordering/UserRepository.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.coded.spring.ordering - -import jakarta.inject.Named -import jakarta.persistence.* -import org.springframework.data.jpa.repository.JpaRepository - -@Named -interface UsersRepository : JpaRepository - -@Entity -@Table(name = "users") -data class User( - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - var id: Long? = null, - var name: String -){ - constructor() : this(null, "") -} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/users/OrderController2.kt b/src/main/kotlin/com/coded/spring/ordering/users/OrderController2.kt deleted file mode 100644 index b034ca7..0000000 --- a/src/main/kotlin/com/coded/spring/ordering/users/OrderController2.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.coded.spring.ordering.users - -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.RestController - -@RestController -class OrderController2(private val orderService: OrderService ){ - - @GetMapping("/api/get-orders2") - fun listOrders(): List { - return orderService.listUsers() - } - - @PostMapping("/api/orders2") - fun sayMyName( @RequestBody request: OrderRequest): Order2 { - val order = Order2( - username = request.username, - restaurant = request.restaurant, - items = request.items,) - return orderService.saveOrder(order) - } - - data class OrderRequest( - val username: String, - val restaurant: String, - val items: List, - ) -} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/users/OrderRepository2.kt b/src/main/kotlin/com/coded/spring/ordering/users/OrderRepository2.kt deleted file mode 100644 index b0d7579..0000000 --- a/src/main/kotlin/com/coded/spring/ordering/users/OrderRepository2.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.coded.spring.ordering.users - -import jakarta.persistence.* -import org.springframework.data.jpa.repository.JpaRepository -import org.springframework.stereotype.Repository - -@Repository -interface OrderRepository2 : JpaRepository - -@Entity -@Table(name = "orders") -data class Order2( - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - val id: Long? = null, - - val username: String, - val restaurant: String, - - @Column(name = "items", columnDefinition = "text[]") - val items: List, - - ) { - constructor() : this(null, "", "", emptyList()) - -} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/users/OrderService.kt b/src/main/kotlin/com/coded/spring/ordering/users/OrderService.kt deleted file mode 100644 index d9dc4ef..0000000 --- a/src/main/kotlin/com/coded/spring/ordering/users/OrderService.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.coded.spring.ordering.users - -import jakarta.inject.Named - -@Named -class OrderService( - private val orderRepository2: OrderRepository2 -) { - - fun listUsers(): List = orderRepository2.findAll().map { - Order( - username = it.username, - restaurant = it.restaurant, - items = it.items - ) - } - - fun saveOrder(order: Order2): Order2 { - return orderRepository2.save(order) - } -} - -data class Order( - val username: String, - val restaurant: String, - val items: List, -) \ 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 deleted file mode 100644 index 1d79780..0000000 --- a/src/main/kotlin/com/coded/spring/ordering/users/UsersController.kt +++ /dev/null @@ -1,14 +0,0 @@ -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/UsersRepository2.kt b/src/main/kotlin/com/coded/spring/ordering/users/UsersRepository2.kt deleted file mode 100644 index 4a917ea..0000000 --- a/src/main/kotlin/com/coded/spring/ordering/users/UsersRepository2.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.coded.spring.ordering.users - -import jakarta.inject.Named -import jakarta.persistence.* -import org.springframework.data.jpa.repository.JpaRepository - -@Named -interface UsersRepository2 : JpaRepository - -@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/UsersService.kt b/src/main/kotlin/com/coded/spring/ordering/users/UsersService.kt deleted file mode 100644 index 3740a9a..0000000 --- a/src/main/kotlin/com/coded/spring/ordering/users/UsersService.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.coded.spring.ordering.users - -import jakarta.inject.Named - -@Named -class UsersService( - private val usersRepository: UsersRepository2, -) { - - 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/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt b/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt index b2e2320..e368988 100644 --- a/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt +++ b/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt @@ -1,13 +1,41 @@ 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.HttpStatus +import kotlin.test.assertEquals -@SpringBootTest +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class ApplicationTests { + @Autowired + lateinit var restTemplate: TestRestTemplate + @Test - fun contextLoads() { + fun helloWorld() { + val result = restTemplate.getForEntity("/hello", String::class.java) + assertEquals(expected = HttpStatus.OK, actual = result?.statusCode) + assertEquals(expected = "Hello World", actual = result.body) } + @Test + fun createUser() { + val request = mapOf( + "name" to "HelloUser", + "age" to 18, + "username" to "testuser", + "password" to "password123", + "role" to "USER" + ) + + val response = restTemplate.postForEntity("/users/v1/register", request, String::class.java) + + assertEquals(HttpStatus.OK, response.statusCode) + + + } + + } From 416fb6a30cc24f4b0c5092900de61c4d25545c9a Mon Sep 17 00:00:00 2001 From: HAlGhanim Date: Mon, 21 Apr 2025 21:14:09 +0300 Subject: [PATCH 04/10] All Done except profiles --- .../CustomUserDetailsService.kt | 21 +++++++ .../ordering/authentication/SecurityConfig.kt | 58 +++++++++++++++++++ .../jwt/AuthenticationController.kt | 39 +++++++++++++ .../jwt/JwtAuthenticationFilter.kt | 45 ++++++++++++++ .../ordering/authentication/jwt/JwtService.kt | 42 ++++++++++++++ .../controllers/HelloWorldController.kt | 27 +++++++++ .../ordering/controllers/OrdersController.kt | 33 +++++++++++ .../ordering/controllers/UsersController.kt | 42 ++++++++++++++ .../spring/ordering/entities/ItemEntity.kt | 17 ++++++ .../spring/ordering/entities/OrderEntity.kt | 27 +++++++++ .../spring/ordering/entities/UserEntity.kt | 28 +++++++++ .../ordering/repositories/ItemsRepository.kt | 7 +++ .../ordering/repositories/OrdersRepository.kt | 9 +++ .../ordering/repositories/UsersRepository.kt | 13 +++++ .../spring/ordering/script/InitUserRunner.kt | 36 ++++++++++++ .../spring/ordering/services/OrdersService.kt | 29 ++++++++++ .../spring/ordering/services/UsersService.kt | 43 ++++++++++++++ 17 files changed, 516 insertions(+) create mode 100644 src/main/kotlin/com/coded/spring/ordering/authentication/CustomUserDetailsService.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/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/controllers/HelloWorldController.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/controllers/OrdersController.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/controllers/UsersController.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/entities/ItemEntity.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/entities/OrderEntity.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/entities/UserEntity.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/repositories/ItemsRepository.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/repositories/OrdersRepository.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/repositories/UsersRepository.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/script/InitUserRunner.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/services/OrdersService.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/services/UsersService.kt diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/CustomUserDetailsService.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/CustomUserDetailsService.kt new file mode 100644 index 0000000..7f652db --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/CustomUserDetailsService.kt @@ -0,0 +1,21 @@ +package com.coded.spring.ordering.authentication + +import com.coded.spring.ordering.repositories.UsersRepository +import org.springframework.security.core.userdetails.* +import org.springframework.stereotype.Service + +@Service +class CustomUserDetailsService( + private val usersRepository: UsersRepository +) : UserDetailsService { + override fun loadUserByUsername(username: String): UserDetails { + val user = usersRepository.findByUsername(username) + ?: throw UsernameNotFoundException("User not found") + + return User.builder() + .username(user.username) + .password(user.password) + .roles(user.role.toString()) + .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..91c1b1b --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt @@ -0,0 +1,58 @@ +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 securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http.csrf { it.disable() } + .authorizeHttpRequests { + it.requestMatchers("/auth/**", "/hello", "/users/v1/register").permitAll() + .anyRequest().authenticated() + } + .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..e579c57 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/AuthenticationController.kt @@ -0,0 +1,39 @@ +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..c877c7c --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtService.kt @@ -0,0 +1,42 @@ +package com.coded.spring.ordering.authentication.jwt + +import io.jsonwebtoken.* +import io.jsonwebtoken.security.Keys +import org.springframework.stereotype.Component +import java.util.* +import javax.crypto.SecretKey + +@Component +class JwtService { + + private val secretKey: SecretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256) + private val expirationMs: Long = 1000 * 60 * 60 + + fun generateToken(username: String): String { + val now = Date() + val expiry = Date(now.time + expirationMs) + + return Jwts.builder() + .setSubject(username) + .setIssuedAt(now) + .setExpiration(expiry) + .signWith(secretKey) + .compact() + } + + fun extractUsername(token: String): String = + Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .body + .subject + + fun isTokenValid(token: String, username: String): Boolean { + return try { + extractUsername(token) == username + } catch (e: Exception) { + false + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/controllers/HelloWorldController.kt b/src/main/kotlin/com/coded/spring/ordering/controllers/HelloWorldController.kt new file mode 100644 index 0000000..5e5b026 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/controllers/HelloWorldController.kt @@ -0,0 +1,27 @@ +package com.coded.spring.ordering.controllers + +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.RestController + +@RestController +class HelloWorldController(){ + + @GetMapping("/hello") + fun helloWorld(): String { + return "Hello World" + } + + @GetMapping("/boo") + fun testing(): String { + return "AAAA" + } + + @PostMapping("/hello") + fun helloName(@RequestBody req: Name) = "Hello ${req.name}!" +} + +data class Name( + val name:String +) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/controllers/OrdersController.kt b/src/main/kotlin/com/coded/spring/ordering/controllers/OrdersController.kt new file mode 100644 index 0000000..5efe5d1 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/controllers/OrdersController.kt @@ -0,0 +1,33 @@ +package com.coded.spring.ordering.controllers + +import com.coded.spring.ordering.entities.ItemEntity +import com.coded.spring.ordering.entities.OrderEntity +import com.coded.spring.ordering.repositories.OrdersRepository +import com.coded.spring.ordering.services.OrdersService +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.RestController + + +@RestController + +class OrdersController(val ordersRepository: OrdersRepository, val ordersService: OrdersService) { + + @PostMapping("/orders") + fun createNewOrder(@RequestBody req: OrderRequest) { + ordersService.createOrder(userId = req.user) + } + + @GetMapping("/orders") + fun getOrders(): MutableList { + return ordersRepository.findAll() + } + + + data class OrderRequest( + val user: Long, + val restaurant: String, + val items: List + ) +} diff --git a/src/main/kotlin/com/coded/spring/ordering/controllers/UsersController.kt b/src/main/kotlin/com/coded/spring/ordering/controllers/UsersController.kt new file mode 100644 index 0000000..0295814 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/controllers/UsersController.kt @@ -0,0 +1,42 @@ +package com.coded.spring.ordering.controllers + +import com.coded.spring.ordering.entities.Roles +import com.coded.spring.ordering.entities.UserEntity +import com.coded.spring.ordering.services.UsersService +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController + +@RestController +class UsersControllers(private val usersService: UsersService) { + + @GetMapping("/users/v1/list") + fun users() = usersService.listUsers() + + @GetMapping("/users/v1/{userId}") + fun user(@PathVariable userId: Long) = usersService.getUserDtoById(userId) + + @PostMapping("/users/v1/register") + fun createUser(@RequestBody request: UserRequest) { + val user = UserEntity( + name = request.name, + age = request.age, + username = request.username, + password = request.password, + role = request.role + ) + usersService.createUser(user) + } + + data class UserRequest( + val name: String, + val age: Int, + val username: String, + val password: String, + val role: Roles + ) + + +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/entities/ItemEntity.kt b/src/main/kotlin/com/coded/spring/ordering/entities/ItemEntity.kt new file mode 100644 index 0000000..db854c8 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/entities/ItemEntity.kt @@ -0,0 +1,17 @@ +package com.coded.spring.ordering.entities + +import jakarta.persistence.* + + +@Entity +@Table(name="items") +data class ItemEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, + val name: String, + val quantity: Int, + val order_id: Long +){ + constructor() : this(null, "", 0, 0) +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/entities/OrderEntity.kt b/src/main/kotlin/com/coded/spring/ordering/entities/OrderEntity.kt new file mode 100644 index 0000000..12ef48c --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/entities/OrderEntity.kt @@ -0,0 +1,27 @@ +package com.coded.spring.ordering.entities + +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.ManyToOne +import jakarta.persistence.OneToMany +import jakarta.persistence.Table + +@Entity +@Table(name = "orders") +data class OrderEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, + val restaurant:String? =null, + + @ManyToOne + val user: UserEntity, + + + @OneToMany(mappedBy = "order_id") + val items: List? = null +){ + constructor() : this(null,null, UserEntity(), listOf()) +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/entities/UserEntity.kt b/src/main/kotlin/com/coded/spring/ordering/entities/UserEntity.kt new file mode 100644 index 0000000..e0678d7 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/entities/UserEntity.kt @@ -0,0 +1,28 @@ +package com.coded.spring.ordering.entities + +import jakarta.persistence.* + +@Entity +@Table(name = "users") +data class UserEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + var name: String, + var age: Int, + + @Column(unique = true) + var username: String, + + var password: String, + + @Enumerated(EnumType.STRING) + val role: Roles = Roles.USER + +) { + constructor() : this(0, "name", 0, "username", "password", Roles.USER) +} + +enum class Roles { + USER, ADMIN +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/repositories/ItemsRepository.kt b/src/main/kotlin/com/coded/spring/ordering/repositories/ItemsRepository.kt new file mode 100644 index 0000000..ccafe63 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/repositories/ItemsRepository.kt @@ -0,0 +1,7 @@ +package com.coded.spring.ordering.repositories + +import com.coded.spring.ordering.entities.ItemEntity +import org.springframework.data.jpa.repository.JpaRepository + + +interface ItemsRepository: JpaRepository diff --git a/src/main/kotlin/com/coded/spring/ordering/repositories/OrdersRepository.kt b/src/main/kotlin/com/coded/spring/ordering/repositories/OrdersRepository.kt new file mode 100644 index 0000000..f2a4d81 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/repositories/OrdersRepository.kt @@ -0,0 +1,9 @@ +package com.coded.spring.ordering.repositories + +import com.coded.spring.ordering.entities.OrderEntity +import jakarta.inject.Named +import org.springframework.data.jpa.repository.JpaRepository + +@Named +interface OrdersRepository : JpaRepository + diff --git a/src/main/kotlin/com/coded/spring/ordering/repositories/UsersRepository.kt b/src/main/kotlin/com/coded/spring/ordering/repositories/UsersRepository.kt new file mode 100644 index 0000000..8c21439 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/repositories/UsersRepository.kt @@ -0,0 +1,13 @@ +package com.coded.spring.ordering.repositories + +import com.coded.spring.ordering.entities.UserEntity +import jakarta.inject.Named +import org.springframework.data.jpa.repository.JpaRepository + +@Named +interface UsersRepository : JpaRepository { + fun findByUsername(username: String): UserEntity? +} + + + diff --git a/src/main/kotlin/com/coded/spring/ordering/script/InitUserRunner.kt b/src/main/kotlin/com/coded/spring/ordering/script/InitUserRunner.kt new file mode 100644 index 0000000..f771fd0 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/script/InitUserRunner.kt @@ -0,0 +1,36 @@ +package com.coded.spring.ordering.script + +import com.coded.spring.ordering.Application +import com.coded.spring.ordering.entities.Roles +import com.coded.spring.ordering.entities.UserEntity +import com.coded.spring.ordering.repositories.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 = "HelloUser", + username = "testuser", + password = passwordEncoder.encode("password123"), + age = 18, + role = Roles.USER + ) + if (userRepository.findByUsername(user.username) == null) { + println("Creating user ${user.username}") + userRepository.save(user) + } else { + println("User ${user.username} already exists") + } + } +} + +fun main(args: Array) { + runApplication(*args).close() +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/services/OrdersService.kt b/src/main/kotlin/com/coded/spring/ordering/services/OrdersService.kt new file mode 100644 index 0000000..2c80d27 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/services/OrdersService.kt @@ -0,0 +1,29 @@ +package com.coded.spring.ordering.services + +import com.coded.spring.ordering.entities.ItemEntity +import com.coded.spring.ordering.entities.OrderEntity +import com.coded.spring.ordering.entities.UserEntity +import com.coded.spring.ordering.repositories.OrdersRepository +import com.coded.spring.ordering.repositories.UsersRepository +import jakarta.inject.Named + +@Named +class OrdersService( + private val usersRepository: UsersRepository, + private val orderRepository: OrdersRepository +) { + + + fun createOrder(userId: Long): OrderResponse { + val user = usersRepository.findById(userId).get() + val newOrder = OrderEntity(user = user) + orderRepository.save(newOrder) + return OrderResponse(newOrder.user, newOrder.items) + } + +} + +data class OrderResponse( + val user: UserEntity, + val items: List? +) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/services/UsersService.kt b/src/main/kotlin/com/coded/spring/ordering/services/UsersService.kt new file mode 100644 index 0000000..4f56d41 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/services/UsersService.kt @@ -0,0 +1,43 @@ +package com.coded.spring.ordering.services + +import com.coded.spring.ordering.entities.UserEntity +import com.coded.spring.ordering.repositories.UsersRepository +import jakarta.inject.Named + +@Named +class UsersService( + private val usersRepository: UsersRepository, +) { + + fun getUserById(userId: Long): UserEntity { + return usersRepository.findById(userId).orElseThrow() + } + + fun getUserDtoById(userId: Long): User { + val user = usersRepository.findById(userId).orElseThrow() + return User( + id = user.id!!, + name = user.name, + username = user.username + ) + } + + + fun listUsers(): List = usersRepository.findAll().map { + User( + id = it.id!!, + name = it.name, + username = it.username, + ) + } + + fun createUser(user: UserEntity): UserEntity { + return usersRepository.save(user) + } +} + +data class User( + val id: Long, + val name: String, + val username: String, +) \ No newline at end of file From c1e9244326c33dece36ee8e8f0f413a987fac73e Mon Sep 17 00:00:00 2001 From: HAlGhanim Date: Tue, 22 Apr 2025 14:40:30 +0300 Subject: [PATCH 05/10] Done with profiles minus testing --- .../ordering/controllers/UsersController.kt | 25 +++++++++++- .../spring/ordering/entities/ProfileEntity.kt | 22 ++++++++++ .../spring/ordering/entities/UserEntity.kt | 5 ++- .../repositories/ProfileRepository.kt | 9 +++++ .../ordering/services/ProfileService.kt | 40 +++++++++++++++++++ 5 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/com/coded/spring/ordering/entities/ProfileEntity.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/repositories/ProfileRepository.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/services/ProfileService.kt diff --git a/src/main/kotlin/com/coded/spring/ordering/controllers/UsersController.kt b/src/main/kotlin/com/coded/spring/ordering/controllers/UsersController.kt index 0295814..bb87c9f 100644 --- a/src/main/kotlin/com/coded/spring/ordering/controllers/UsersController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/controllers/UsersController.kt @@ -2,6 +2,7 @@ package com.coded.spring.ordering.controllers import com.coded.spring.ordering.entities.Roles import com.coded.spring.ordering.entities.UserEntity +import com.coded.spring.ordering.services.ProfilesService import com.coded.spring.ordering.services.UsersService import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable @@ -10,11 +11,14 @@ import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RestController @RestController -class UsersControllers(private val usersService: UsersService) { +class UsersControllers(private val usersService: UsersService, private val profileService: ProfilesService) { @GetMapping("/users/v1/list") fun users() = usersService.listUsers() + @GetMapping("/users/profiles/v1/list") + fun profiles() = profileService.listProfiles() + @GetMapping("/users/v1/{userId}") fun user(@PathVariable userId: Long) = usersService.getUserDtoById(userId) @@ -30,6 +34,19 @@ class UsersControllers(private val usersService: UsersService) { usersService.createUser(user) } + @PostMapping("/users/v1/{userId}/profile") + fun createProfile( + @RequestBody request: ProfileRequest, + @PathVariable userId: Long + ) { + profileService.createProfileForUser( + userId = userId, + firstName = request.firstName, + lastName = request.lastName, + phoneNumber = request.phoneNumber + ) + } + data class UserRequest( val name: String, val age: Int, @@ -38,5 +55,11 @@ class UsersControllers(private val usersService: UsersService) { val role: Roles ) + 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/entities/ProfileEntity.kt b/src/main/kotlin/com/coded/spring/ordering/entities/ProfileEntity.kt new file mode 100644 index 0000000..dffd215 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/entities/ProfileEntity.kt @@ -0,0 +1,22 @@ +package com.coded.spring.ordering.entities + +import com.coded.spring.ordering.entities.UserEntity +import jakarta.persistence.* + +@Entity +@Table(name = "profiles") +data class ProfileEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, + + @OneToOne + @JoinColumn(name = "user_id", nullable = false, unique = true) + val user: UserEntity, + + val firstName: String, + val lastName: String, + val phoneNumber: String? +) { + constructor() : this(null, UserEntity(),"","",null) +} diff --git a/src/main/kotlin/com/coded/spring/ordering/entities/UserEntity.kt b/src/main/kotlin/com/coded/spring/ordering/entities/UserEntity.kt index e0678d7..353cd61 100644 --- a/src/main/kotlin/com/coded/spring/ordering/entities/UserEntity.kt +++ b/src/main/kotlin/com/coded/spring/ordering/entities/UserEntity.kt @@ -17,7 +17,10 @@ data class UserEntity( var password: String, @Enumerated(EnumType.STRING) - val role: Roles = Roles.USER + val role: Roles = Roles.USER, + + @OneToOne(mappedBy = "user", cascade = [CascadeType.ALL], fetch = FetchType.LAZY) + var profile: ProfileEntity? = null ) { constructor() : this(0, "name", 0, "username", "password", Roles.USER) diff --git a/src/main/kotlin/com/coded/spring/ordering/repositories/ProfileRepository.kt b/src/main/kotlin/com/coded/spring/ordering/repositories/ProfileRepository.kt new file mode 100644 index 0000000..e330be8 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/repositories/ProfileRepository.kt @@ -0,0 +1,9 @@ +package com.coded.spring.ordering.repositories + +import com.coded.spring.ordering.entities.ProfileEntity +import com.coded.spring.ordering.entities.UserEntity +import org.springframework.data.jpa.repository.JpaRepository + +interface ProfilesRepository : JpaRepository { + fun existsByUser(user: UserEntity): Boolean +} diff --git a/src/main/kotlin/com/coded/spring/ordering/services/ProfileService.kt b/src/main/kotlin/com/coded/spring/ordering/services/ProfileService.kt new file mode 100644 index 0000000..042fd47 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/services/ProfileService.kt @@ -0,0 +1,40 @@ +package com.coded.spring.ordering.services + +import com.coded.spring.ordering.entities.ProfileEntity +import com.coded.spring.ordering.repositories.ProfilesRepository +import com.coded.spring.ordering.repositories.UsersRepository +import org.springframework.stereotype.Service + +@Service +class ProfilesService( + private val profilesRepository: ProfilesRepository, + private val usersRepository: UsersRepository +) { + fun createProfileForUser(userId: Long, firstName: String, lastName: String, phoneNumber: String?) { + val user = usersRepository.findById(userId).orElseThrow() + val profile = ProfileEntity( + user = user, + firstName = firstName, + lastName = lastName, + phoneNumber = phoneNumber + ) + profilesRepository.save(profile) + + } + + fun listProfiles(): List = profilesRepository.findAll().map { + Profile( + id = it.id!!, + firstName = it.firstName, + lastName = it.lastName, + phoneNumber = it.phoneNumber + ) + } +} + +data class Profile( + val id: Long, + val firstName: String, + val lastName: String, + val phoneNumber: String?, +) From ae8192f1c64b06fe38f1afd9390e530fe968a69b Mon Sep 17 00:00:00 2001 From: HAlGhanim Date: Tue, 22 Apr 2025 14:52:42 +0300 Subject: [PATCH 06/10] Added Validation bonus by using regex pattern --- pom.xml | 11 ++++++++++- .../spring/ordering/controllers/UsersController.kt | 10 +++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 5aca940..ca6be29 100644 --- a/pom.xml +++ b/pom.xml @@ -52,7 +52,16 @@ runtime 0.11.5 - + + jakarta.validation + jakarta.validation-api + 3.0.2 + + + org.hibernate.validator + hibernate-validator + 8.0.1.Final + org.springframework.boot spring-boot-starter-web diff --git a/src/main/kotlin/com/coded/spring/ordering/controllers/UsersController.kt b/src/main/kotlin/com/coded/spring/ordering/controllers/UsersController.kt index bb87c9f..d05c237 100644 --- a/src/main/kotlin/com/coded/spring/ordering/controllers/UsersController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/controllers/UsersController.kt @@ -4,6 +4,8 @@ import com.coded.spring.ordering.entities.Roles import com.coded.spring.ordering.entities.UserEntity import com.coded.spring.ordering.services.ProfilesService import com.coded.spring.ordering.services.UsersService +import jakarta.validation.Valid +import jakarta.validation.constraints.Pattern import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping @@ -36,7 +38,7 @@ class UsersControllers(private val usersService: UsersService, private val profi @PostMapping("/users/v1/{userId}/profile") fun createProfile( - @RequestBody request: ProfileRequest, + @RequestBody @Valid request: ProfileRequest, @PathVariable userId: Long ) { profileService.createProfileForUser( @@ -56,8 +58,14 @@ class UsersControllers(private val usersService: UsersService, private val profi ) data class ProfileRequest( + + @field:Pattern(regexp = "^[a-zA-Z]+$", message = "First name must contain only letters") val firstName: String, + + @field:Pattern(regexp = "^[a-zA-Z]+$", message = "Last name must contain only letters") val lastName: String, + + @field:Pattern(regexp = "^\\d{8}$", message = "Phone number must be exactly 8 digits with no letters") val phoneNumber: String? ) From 3f56432396d860aca77a58449353f794c4f6b405 Mon Sep 17 00:00:00 2001 From: HAlGhanim Date: Tue, 22 Apr 2025 20:42:08 +0300 Subject: [PATCH 07/10] Test done will implement test for profile --- .../ordering/authentication/SecurityConfig.kt | 2 +- .../jwt/AuthenticationController.kt | 8 +- .../controllers/HelloWorldController.kt | 2 +- .../ordering/controllers/OrdersController.kt | 2 +- .../spring/ordering/services/OrdersService.kt | 4 +- .../coded/spring/ordering/ApplicationTests.kt | 81 +++++++++++++++---- .../resources/application-test.properties | 11 +++ 7 files changed, 84 insertions(+), 26 deletions(-) create mode 100644 src/test/resources/application-test.properties 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 91c1b1b..a4880e1 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt @@ -29,7 +29,7 @@ class SecurityConfig( fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { http.csrf { it.disable() } .authorizeHttpRequests { - it.requestMatchers("/auth/**", "/hello", "/users/v1/register").permitAll() + it.requestMatchers("/auth/**").permitAll() .anyRequest().authenticated() } .sessionManagement { 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 e579c57..f0a6f12 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 @@ -15,14 +15,14 @@ class AuthenticationController( ) { @PostMapping("/login") - fun login(@RequestBody authRequest: AuthenticationRequest): AuthenticationResponse { + fun login(@RequestBody authRequest: AuthenticationRequest): String { 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) + return token } else { throw UsernameNotFoundException("Invalid user request!") } @@ -32,8 +32,4 @@ class AuthenticationController( 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/controllers/HelloWorldController.kt b/src/main/kotlin/com/coded/spring/ordering/controllers/HelloWorldController.kt index 5e5b026..1f3eb3c 100644 --- a/src/main/kotlin/com/coded/spring/ordering/controllers/HelloWorldController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/controllers/HelloWorldController.kt @@ -10,7 +10,7 @@ class HelloWorldController(){ @GetMapping("/hello") fun helloWorld(): String { - return "Hello World" + return "Hello World!" } @GetMapping("/boo") diff --git a/src/main/kotlin/com/coded/spring/ordering/controllers/OrdersController.kt b/src/main/kotlin/com/coded/spring/ordering/controllers/OrdersController.kt index 5efe5d1..45df57a 100644 --- a/src/main/kotlin/com/coded/spring/ordering/controllers/OrdersController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/controllers/OrdersController.kt @@ -14,7 +14,7 @@ import org.springframework.web.bind.annotation.RestController class OrdersController(val ordersRepository: OrdersRepository, val ordersService: OrdersService) { - @PostMapping("/orders") + @PostMapping("/orders/v1/orders") fun createNewOrder(@RequestBody req: OrderRequest) { ordersService.createOrder(userId = req.user) } diff --git a/src/main/kotlin/com/coded/spring/ordering/services/OrdersService.kt b/src/main/kotlin/com/coded/spring/ordering/services/OrdersService.kt index 2c80d27..c26f2f6 100644 --- a/src/main/kotlin/com/coded/spring/ordering/services/OrdersService.kt +++ b/src/main/kotlin/com/coded/spring/ordering/services/OrdersService.kt @@ -15,7 +15,9 @@ class OrdersService( fun createOrder(userId: Long): OrderResponse { - val user = usersRepository.findById(userId).get() + val user = usersRepository.findById(userId).orElseThrow { + IllegalArgumentException("User with id $userId not found") + } val newOrder = OrderEntity(user = user) orderRepository.save(newOrder) return OrderResponse(newOrder.user, newOrder.items) diff --git a/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt b/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt index e368988..e450ea0 100644 --- a/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt +++ b/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt @@ -1,41 +1,90 @@ package com.coded.spring.ordering +import com.coded.spring.ordering.authentication.jwt.JwtService +import com.coded.spring.ordering.entities.UserEntity +import com.coded.spring.ordering.repositories.UsersRepository +import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod import org.springframework.http.HttpStatus +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.test.context.ActiveProfiles import kotlin.test.assertEquals @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") class ApplicationTests { + companion object { + lateinit var savedUser: UserEntity + + @JvmStatic + @BeforeAll + fun setUp( + @Autowired usersRepository: UsersRepository, + @Autowired passwordEncoder: PasswordEncoder, + ) { + usersRepository.deleteAll() + savedUser = usersRepository.save( + UserEntity( + name = "coded", + age = 10, + username = "coded", + password = passwordEncoder.encode("joincoded") + ) + ) + } + } + @Autowired lateinit var restTemplate: TestRestTemplate @Test - fun helloWorld() { - val result = restTemplate.getForEntity("/hello", String::class.java) - assertEquals(expected = HttpStatus.OK, actual = result?.statusCode) - assertEquals(expected = "Hello World", actual = result.body) + fun testHelloWorld(@Autowired jwtService: JwtService) { + val token = jwtService.generateToken("coded") + val headers = HttpHeaders() + headers.setBearerAuth(token) + + val requestEntity = HttpEntity(headers) + + val result = restTemplate.exchange( + "/hello", + HttpMethod.GET, + requestEntity, + String::class.java + ) + assertEquals(HttpStatus.OK, result.statusCode) + assertEquals("Hello World!", result.body) } @Test - fun createUser() { - val request = mapOf( - "name" to "HelloUser", - "age" to 18, - "username" to "testuser", - "password" to "password123", - "role" to "USER" + fun testOrderSubmit(@Autowired jwtService: JwtService) { + val token = jwtService.generateToken("coded") + val headers = HttpHeaders() + headers.setBearerAuth(token) + headers.set("Content-Type", "application/json") + + val requestBody = mapOf( + "user" to savedUser.id!!, + "restaurant" to "Testaurant", + "items" to listOf( + mapOf("name" to "Chicken Burger", "quantity" to 3) + ) ) - val response = restTemplate.postForEntity("/users/v1/register", request, String::class.java) + val requestEntity = HttpEntity(requestBody, headers) + val response = restTemplate.exchange( + "/orders/v1/orders", + HttpMethod.POST, + requestEntity, + String::class.java + ) assertEquals(HttpStatus.OK, response.statusCode) - - } - - } diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties new file mode 100644 index 0000000..c63d0df --- /dev/null +++ b/src/test/resources/application-test.properties @@ -0,0 +1,11 @@ +spring.application.name=Kotlin.SpringbootV2 + +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect + +# Optional: enable H2 web console for debugging +spring.h2.console.enabled=true From 2ecf70417b33115ff51a5cc8ccfd2079743a6d24 Mon Sep 17 00:00:00 2001 From: HAlGhanim Date: Tue, 22 Apr 2025 20:59:08 +0300 Subject: [PATCH 08/10] Profile unit test done --- .../coded/spring/ordering/ApplicationTests.kt | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt b/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt index e450ea0..82ed488 100644 --- a/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt +++ b/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt @@ -87,4 +87,28 @@ class ApplicationTests { assertEquals(HttpStatus.OK, response.statusCode) } + + @Test + fun testProfileSubmit(@Autowired jwtService: JwtService) { + val token = jwtService.generateToken("coded") + val headers = HttpHeaders() + headers.setBearerAuth(token) + headers.set("Content-Type", "application/json") + + val requestBody = mapOf( + "firstName" to "Humoud", + "lastName" to "AlGhanim", + "phoneNumber" to "99996703" + ) + + val requestEntity = HttpEntity(requestBody, headers) + val response = restTemplate.exchange( + "/users/v1/${savedUser.id}/profile", + HttpMethod.POST, + requestEntity, + String::class.java + ) + + assertEquals(HttpStatus.OK, response.statusCode) + } } From 11a510ab1f3990a69b31574b268de14bcdbf9637 Mon Sep 17 00:00:00 2001 From: HAlGhanim Date: Mon, 28 Apr 2025 22:11:01 +0300 Subject: [PATCH 09/10] 2 out of 3 tasks done --- Humoud-online-ordering-api-swagger-01.json | 490 ++++++++++++++++++ pom.xml | 33 ++ .../com/coded/spring/ordering/Application.kt | 7 + .../coded/spring/ordering/LoggingFilter.kt | 56 ++ .../ordering/authentication/SecurityConfig.kt | 2 +- .../jwt/AuthenticationController.kt | 26 +- .../controllers/HelloWorldController.kt | 63 ++- .../ordering/controllers/OrdersController.kt | 60 ++- .../ordering/controllers/UsersController.kt | 104 +++- .../spring/ordering/entities/ItemEntity.kt | 18 +- .../spring/ordering/entities/OrderEntity.kt | 24 +- .../spring/ordering/services/OrdersService.kt | 34 +- src/main/resources/application.properties | 1 + 13 files changed, 858 insertions(+), 60 deletions(-) create mode 100644 Humoud-online-ordering-api-swagger-01.json create mode 100644 src/main/kotlin/com/coded/spring/ordering/LoggingFilter.kt diff --git a/Humoud-online-ordering-api-swagger-01.json b/Humoud-online-ordering-api-swagger-01.json new file mode 100644 index 0000000..48f9136 --- /dev/null +++ b/Humoud-online-ordering-api-swagger-01.json @@ -0,0 +1,490 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "http://localhost:8080", + "description": "Generated server url" + } + ], + "tags": [ + { + "name": "Users API", + "description": "Operations related to Users and Profiles" + }, + { + "name": "Authentication API", + "description": "User Authentication and JWT generation" + }, + { + "name": "Hello API", + "description": "Operations related to Hello" + }, + { + "name": "Orders API", + "description": "Operations related to Orders" + } + ], + "paths": { + "/users/v1/{userId}/profile": { + "post": { + "tags": [ + "Users API" + ], + "summary": "Create a user profile", + "description": "Create a profile for an existing user", + "operationId": "createProfile", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProfileRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Profile successfully created", + "content": { + "application/json": {} + } + }, + "400": { + "description": "Invalid profile data" + } + } + } + }, + "/users/v1/register": { + "post": { + "tags": [ + "Users API" + ], + "summary": "Register a new user", + "description": "Create a new user account", + "operationId": "createUser", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "User successfully created", + "content": { + "application/json": {} + } + }, + "400": { + "description": "Invalid user data" + } + } + } + }, + "/orders/v1/orders": { + "post": { + "tags": [ + "Orders API" + ], + "summary": "Create a new order", + "description": "Creates a new order for a specific user", + "operationId": "createNewOrder", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrderRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Order successfully created", + "content": { + "application/json": {} + } + }, + "400": { + "description": "Invalid order data" + } + } + } + }, + "/hello": { + "get": { + "tags": [ + "Hello API" + ], + "summary": "Say Hello World", + "operationId": "helloWorld", + "responses": { + "200": { + "description": "Successful Hello World response", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "post": { + "tags": [ + "Hello API" + ], + "summary": "Say Hello with Name", + "operationId": "helloName", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Name" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful personalized Hello response", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/auth/login": { + "post": { + "tags": [ + "Authentication API" + ], + "summary": "User Login", + "description": "Authenticate user and return a JWT token", + "operationId": "login", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Login successful, JWT token returned", + "content": { + "application/json": {} + } + }, + "401": { + "description": "Invalid username or password", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/users/v1/{userId}": { + "get": { + "tags": [ + "Users API" + ], + "summary": "Get user by ID", + "description": "Retrieve a specific user by their ID", + "operationId": "user", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "User found", + "content": { + "application/json": {} + } + }, + "404": { + "description": "User not found", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + } + }, + "/users/v1/list": { + "get": { + "tags": [ + "Users API" + ], + "summary": "List all users", + "description": "Retrieve a list of all users", + "operationId": "users", + "responses": { + "200": { + "description": "List of all users", + "content": { + "application/json": {} + } + } + } + } + }, + "/users/profiles/v1/list": { + "get": { + "tags": [ + "Users API" + ], + "summary": "List all profiles", + "description": "Retrieve a list of all user profiles", + "operationId": "profiles", + "responses": { + "200": { + "description": "List of all profiles", + "content": { + "application/json": {} + } + } + } + } + }, + "/orders": { + "get": { + "tags": [ + "Orders API" + ], + "summary": "Retrieve all orders", + "description": "Get a list of all existing orders", + "operationId": "getOrders", + "responses": { + "200": { + "description": "List of all orders", + "content": { + "application/json": {} + } + } + } + } + }, + "/boo": { + "get": { + "tags": [ + "Hello API" + ], + "summary": "Testing endpoint", + "operationId": "testing", + "responses": { + "200": { + "description": "Successful test response", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ProfileRequest": { + "required": [ + "firstName", + "lastName" + ], + "type": "object", + "properties": { + "firstName": { + "pattern": "^[a-zA-Z]+$", + "type": "string" + }, + "lastName": { + "pattern": "^[a-zA-Z]+$", + "type": "string" + }, + "phoneNumber": { + "pattern": "^\\d{8}$", + "type": "string" + } + } + }, + "UserRequest": { + "required": [ + "age", + "name", + "password", + "role", + "username" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "age": { + "type": "integer", + "format": "int32" + }, + "username": { + "type": "string" + }, + "password": { + "type": "string" + }, + "role": { + "type": "string", + "enum": [ + "USER", + "ADMIN" + ] + } + } + }, + "ItemEntity": { + "required": [ + "name", + "order_id", + "quantity" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "quantity": { + "type": "integer", + "format": "int32" + }, + "order_id": { + "type": "integer", + "format": "int64" + } + } + }, + "OrderRequest": { + "required": [ + "items", + "restaurant", + "user" + ], + "type": "object", + "properties": { + "user": { + "type": "integer", + "format": "int64" + }, + "restaurant": { + "type": "string" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemEntity" + } + } + } + }, + "Name": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, + "AuthenticationRequest": { + "required": [ + "password", + "username" + ], + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "User": { + "required": [ + "id", + "name", + "username" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "username": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index ca6be29..ffd3056 100644 --- a/pom.xml +++ b/pom.xml @@ -111,6 +111,39 @@ h2 test + + io.cucumber + cucumber-java + 7.20.1 + test + + + io.cucumber + cucumber-junit-platform-engine + 7.20.1 + test + + + org.springdoc + springdoc-openapi-starter-webmvc-api + 2.6.0 + + + io.cucumber + cucumber-spring + 7.14.0 + test + + + org.apache.commons + commons-lang3 + 3.14.0 + + + com.hazelcast + hazelcast + 5.5.0 + diff --git a/src/main/kotlin/com/coded/spring/ordering/Application.kt b/src/main/kotlin/com/coded/spring/ordering/Application.kt index 8554e49..021f276 100644 --- a/src/main/kotlin/com/coded/spring/ordering/Application.kt +++ b/src/main/kotlin/com/coded/spring/ordering/Application.kt @@ -1,5 +1,8 @@ package com.coded.spring.ordering +import com.hazelcast.config.Config; +import com.hazelcast.core.Hazelcast +import com.hazelcast.core.HazelcastInstance import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @@ -8,4 +11,8 @@ class Application fun main(args: Array) { runApplication(*args) + ordersConfig.getMapConfig("orders-cache").setTimeToLiveSeconds(5) } + +val ordersConfig = Config("orders-cache") +val serverCache: HazelcastInstance = Hazelcast.newHazelcastInstance(ordersConfig) diff --git a/src/main/kotlin/com/coded/spring/ordering/LoggingFilter.kt b/src/main/kotlin/com/coded/spring/ordering/LoggingFilter.kt new file mode 100644 index 0000000..0681429 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/LoggingFilter.kt @@ -0,0 +1,56 @@ +package com.coded.spring.ordering + +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.beans.factory.annotation.Value +import org.springframework.core.annotation.Order +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter +import org.springframework.web.util.ContentCachingRequestWrapper +import org.springframework.web.util.ContentCachingResponseWrapper +import org.slf4j.LoggerFactory + +@Component +@Order(1) +class LoggingFilter( + @Value("\${logging-level}") + private val logLevel: String +) : OncePerRequestFilter() { + + 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) + + if (logLevel == "DEBUG") { + logger.info("Request: method=${request.method}, uri=${request.requestURI}, body=$requestBody") + } else { + logger.info("Request: method=${request.method}, uri=${request.requestURI}") + } + } + + private fun logResponse(response: ContentCachingResponseWrapper) { + val responseBody = String(response.contentAsByteArray) + + if (logLevel == "DEBUG") { + logger.info("Response: status=${response.status}, body=$responseBody") + } else { + logger.info("Response: status=${response.status}") + } + } +} 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 a4880e1..599f96f 100644 --- a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt @@ -29,7 +29,7 @@ class SecurityConfig( fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { http.csrf { it.disable() } .authorizeHttpRequests { - it.requestMatchers("/auth/**").permitAll() + it.requestMatchers("/auth/**", "/api-docs", "/hello").permitAll() .anyRequest().authenticated() } .sessionManagement { 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 f0a6f12..36b4c95 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,19 +1,41 @@ package com.coded.spring.ordering.authentication.jwt +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 io.swagger.v3.oas.annotations.media.Content 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") +@Tag(name = "Authentication API", description = "User Authentication and JWT generation") class AuthenticationController( private val authenticationManager: AuthenticationManager, private val userDetailsService: UserDetailsService, private val jwtService: JwtService ) { + @Operation( + summary = "User Login", + description = "Authenticate user and return a JWT token", + tags = ["Authentication API"] + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "Login successful, JWT token returned", + content = [Content(mediaType = "application/json")] + ), + ApiResponse( + responseCode = "401", + description = "Invalid username or password") + ] + ) @PostMapping("/login") fun login(@RequestBody authRequest: AuthenticationRequest): String { val authToken = UsernamePasswordAuthenticationToken(authRequest.username, authRequest.password) @@ -32,4 +54,4 @@ class AuthenticationController( data class AuthenticationRequest( val username: String, val password: String -) \ No newline at end of file +) diff --git a/src/main/kotlin/com/coded/spring/ordering/controllers/HelloWorldController.kt b/src/main/kotlin/com/coded/spring/ordering/controllers/HelloWorldController.kt index 1f3eb3c..43e3bc1 100644 --- a/src/main/kotlin/com/coded/spring/ordering/controllers/HelloWorldController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/controllers/HelloWorldController.kt @@ -1,27 +1,72 @@ package com.coded.spring.ordering.controllers -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.RestController +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.* +import java.math.BigDecimal + @RestController -class HelloWorldController(){ +@Tag(name = "Hello API", description = "Operations related to Hello") +class HelloWorldController( + @Value("\${server-welcome-message}") + val welcomeMessage: String, + @Value("\${eid-message}") + val eidMessage: String, + @Value("\${feature.eid.enabled}") + private val eidEnabled: Boolean, + @Value("\${discount}") + val discount: Boolean +) { + @Operation(summary = "Say Hello World") + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "Successful Hello World response") + ] + ) @GetMapping("/hello") fun helloWorld(): String { - return "Hello World!" + return if(eidEnabled){ + eidMessage + } + else { + welcomeMessage + } } + @Operation(summary = "Testing endpoint") + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "Successful test response") + ] + ) @GetMapping("/boo") fun testing(): String { return "AAAA" } + @Operation(summary = "Say Hello with Name") + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "Successful personalized Hello response") + ] + ) @PostMapping("/hello") - fun helloName(@RequestBody req: Name) = "Hello ${req.name}!" + fun helloName(@RequestBody req: Name): String { + return if(discount){ + "Hello ${req.name}, The discounted price is: ${req.price * BigDecimal("0.8")}!" + } + else { + "Hello ${req.name}, The Price is: ${req.price}!" + } + } } data class Name( - val name:String -) \ No newline at end of file + val name: String, + val price: BigDecimal = BigDecimal("100.0") +) diff --git a/src/main/kotlin/com/coded/spring/ordering/controllers/OrdersController.kt b/src/main/kotlin/com/coded/spring/ordering/controllers/OrdersController.kt index 45df57a..c5ba95e 100644 --- a/src/main/kotlin/com/coded/spring/ordering/controllers/OrdersController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/controllers/OrdersController.kt @@ -4,30 +4,70 @@ import com.coded.spring.ordering.entities.ItemEntity import com.coded.spring.ordering.entities.OrderEntity import com.coded.spring.ordering.repositories.OrdersRepository import com.coded.spring.ordering.services.OrdersService -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.RestController - +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.media.Content +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.web.bind.annotation.* @RestController +@Tag(name = "Orders API", description = "Operations related to Orders") +class OrdersController( + val ordersRepository: OrdersRepository, + val ordersService: OrdersService, +) { -class OrdersController(val ordersRepository: OrdersRepository, val ordersService: OrdersService) { - + @Operation( + summary = "Create a new order", + description = "Creates a new order for a specific user", + tags = ["Orders API"] + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "Order successfully created", + content = [Content(mediaType = "application/json")] + ), + ApiResponse( + responseCode = "400", + description = "Invalid order data" + ) + ] + ) @PostMapping("/orders/v1/orders") fun createNewOrder(@RequestBody req: OrderRequest) { - ordersService.createOrder(userId = req.user) + ordersService.createOrder(req) } + @Operation( + summary = "Retrieve all orders", + description = "Get a list of all existing orders", + tags = ["Orders API"] + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "List of all orders", + content = [Content(mediaType = "application/json")] + ) + ] + ) @GetMapping("/orders") fun getOrders(): MutableList { return ordersRepository.findAll() } - data class OrderRequest( val user: Long, val restaurant: String, - val items: List + val items: List + ) + + data class ItemRequest( + val name: String, + val quantity: Int ) } diff --git a/src/main/kotlin/com/coded/spring/ordering/controllers/UsersController.kt b/src/main/kotlin/com/coded/spring/ordering/controllers/UsersController.kt index d05c237..8720657 100644 --- a/src/main/kotlin/com/coded/spring/ordering/controllers/UsersController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/controllers/UsersController.kt @@ -4,26 +4,95 @@ import com.coded.spring.ordering.entities.Roles import com.coded.spring.ordering.entities.UserEntity import com.coded.spring.ordering.services.ProfilesService import com.coded.spring.ordering.services.UsersService +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.media.Content +import io.swagger.v3.oas.annotations.tags.Tag import jakarta.validation.Valid import jakarta.validation.constraints.Pattern -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* @RestController -class UsersControllers(private val usersService: UsersService, private val profileService: ProfilesService) { +@Tag(name = "Users API", description = "Operations related to Users and Profiles") +class UsersControllers( + private val usersService: UsersService, + private val profileService: ProfilesService +) { + @Operation( + summary = "List all users", + description = "Retrieve a list of all users", + tags = ["Users API"] + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "List of all users", + content = [Content(mediaType = "application/json")] + ) + ] + ) @GetMapping("/users/v1/list") fun users() = usersService.listUsers() + @Operation( + summary = "List all profiles", + description = "Retrieve a list of all user profiles", + tags = ["Users API"] + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "List of all profiles", + content = [Content(mediaType = "application/json")] + ) + ] + ) @GetMapping("/users/profiles/v1/list") fun profiles() = profileService.listProfiles() + @Operation( + summary = "Get user by ID", + description = "Retrieve a specific user by their ID", + tags = ["Users API"] + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "User found", + content = [Content(mediaType = "application/json")] + ), + ApiResponse( + responseCode = "404", + description = "User not found" + ) + ] + ) @GetMapping("/users/v1/{userId}") fun user(@PathVariable userId: Long) = usersService.getUserDtoById(userId) + @Operation( + summary = "Register a new user", + description = "Create a new user account", + tags = ["Users API"] + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "User successfully created", + content = [Content(mediaType = "application/json")] + ), + ApiResponse( + responseCode = "400", + description = "Invalid user data" + ) + ] + ) @PostMapping("/users/v1/register") fun createUser(@RequestBody request: UserRequest) { val user = UserEntity( @@ -36,6 +105,24 @@ class UsersControllers(private val usersService: UsersService, private val profi usersService.createUser(user) } + @Operation( + summary = "Create a user profile", + description = "Create a profile for an existing user", + tags = ["Users API"] + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "Profile successfully created", + content = [Content(mediaType = "application/json")] + ), + ApiResponse( + responseCode = "400", + description = "Invalid profile data" + ) + ] + ) @PostMapping("/users/v1/{userId}/profile") fun createProfile( @RequestBody @Valid request: ProfileRequest, @@ -58,7 +145,6 @@ class UsersControllers(private val usersService: UsersService, private val profi ) data class ProfileRequest( - @field:Pattern(regexp = "^[a-zA-Z]+$", message = "First name must contain only letters") val firstName: String, @@ -68,6 +154,4 @@ class UsersControllers(private val usersService: UsersService, private val profi @field:Pattern(regexp = "^\\d{8}$", message = "Phone number must be exactly 8 digits with no letters") val phoneNumber: String? ) - - -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/coded/spring/ordering/entities/ItemEntity.kt b/src/main/kotlin/com/coded/spring/ordering/entities/ItemEntity.kt index db854c8..eb78a4b 100644 --- a/src/main/kotlin/com/coded/spring/ordering/entities/ItemEntity.kt +++ b/src/main/kotlin/com/coded/spring/ordering/entities/ItemEntity.kt @@ -1,17 +1,23 @@ package com.coded.spring.ordering.entities +import com.fasterxml.jackson.annotation.JsonBackReference import jakarta.persistence.* - @Entity -@Table(name="items") +@Table(name = "items") data class ItemEntity( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long? = null, + val name: String, + val quantity: Int, - val order_id: Long -){ - constructor() : this(null, "", 0, 0) -} \ No newline at end of file + + @ManyToOne + @JoinColumn(name = "order_id", nullable = false) + @JsonBackReference // ✅ Add this + var order: OrderEntity? = null +) { + constructor() : this(null, "", 0, null) +} diff --git a/src/main/kotlin/com/coded/spring/ordering/entities/OrderEntity.kt b/src/main/kotlin/com/coded/spring/ordering/entities/OrderEntity.kt index 12ef48c..95c85d0 100644 --- a/src/main/kotlin/com/coded/spring/ordering/entities/OrderEntity.kt +++ b/src/main/kotlin/com/coded/spring/ordering/entities/OrderEntity.kt @@ -1,12 +1,7 @@ package com.coded.spring.ordering.entities -import jakarta.persistence.Entity -import jakarta.persistence.GeneratedValue -import jakarta.persistence.GenerationType -import jakarta.persistence.Id -import jakarta.persistence.ManyToOne -import jakarta.persistence.OneToMany -import jakarta.persistence.Table +import com.fasterxml.jackson.annotation.JsonManagedReference +import jakarta.persistence.* @Entity @Table(name = "orders") @@ -14,14 +9,15 @@ data class OrderEntity( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long? = null, - val restaurant:String? =null, + + val restaurant: String? = null, @ManyToOne val user: UserEntity, - - @OneToMany(mappedBy = "order_id") - val items: List? = null -){ - constructor() : this(null,null, UserEntity(), listOf()) -} \ No newline at end of file + @OneToMany(mappedBy = "order", cascade = [CascadeType.ALL], orphanRemoval = true) + @JsonManagedReference + var items: List = listOf() +) { + constructor() : this(null, null, UserEntity(), listOf()) +} diff --git a/src/main/kotlin/com/coded/spring/ordering/services/OrdersService.kt b/src/main/kotlin/com/coded/spring/ordering/services/OrdersService.kt index c26f2f6..a72e5db 100644 --- a/src/main/kotlin/com/coded/spring/ordering/services/OrdersService.kt +++ b/src/main/kotlin/com/coded/spring/ordering/services/OrdersService.kt @@ -1,5 +1,6 @@ package com.coded.spring.ordering.services +import com.coded.spring.ordering.controllers.OrdersController import com.coded.spring.ordering.entities.ItemEntity import com.coded.spring.ordering.entities.OrderEntity import com.coded.spring.ordering.entities.UserEntity @@ -13,19 +14,36 @@ class OrdersService( private val orderRepository: OrdersRepository ) { + fun createOrder(req: OrdersController.OrderRequest): OrderResponse { + val user = usersRepository.findById(req.user).orElseThrow { + IllegalArgumentException("User with id ${req.user} not found") + } + + val order = OrderEntity( + restaurant = req.restaurant, + user = user + ) - fun createOrder(userId: Long): OrderResponse { - val user = usersRepository.findById(userId).orElseThrow { - IllegalArgumentException("User with id $userId not found") + val items = req.items.map { itemRequest -> + ItemEntity( + name = itemRequest.name, + quantity = itemRequest.quantity, + order = order // point to the parent order + ) } - val newOrder = OrderEntity(user = user) - orderRepository.save(newOrder) - return OrderResponse(newOrder.user, newOrder.items) - } + order.items = items + + val savedOrder = orderRepository.save(order) + + return OrderResponse( + user = savedOrder.user, + items = savedOrder.items + ) + } } data class OrderResponse( val user: UserEntity, val items: List? -) \ No newline at end of file +) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 2e30afc..ff1cb19 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -4,3 +4,4 @@ spring.datasource.url=jdbc:postgresql://localhost:5432/orderingDB spring.datasource.username=postgres spring.datasource.password= Lockz1998! spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +springdoc.api-docs.path=/api-docs \ No newline at end of file From 86ab70ac676e0bba8fc1ba1ddf282e6cf0bf56f2 Mon Sep 17 00:00:00 2001 From: HAlGhanim Date: Wed, 30 Apr 2025 09:19:37 +0300 Subject: [PATCH 10/10] caching done --- pom.xml | 4 +-- .../com/coded/spring/ordering/Application.kt | 8 +++-- .../ordering/controllers/OrdersController.kt | 1 - .../ordering/controllers/UsersController.kt | 6 ++-- .../spring/ordering/services/UsersClient.kt | 36 +++++++++++++++++++ .../spring/ordering/services/UsersProvider.kt | 31 ++++++++++++++++ .../spring/ordering/services/UsersService.kt | 15 ++++++-- 7 files changed, 91 insertions(+), 10 deletions(-) create mode 100644 src/main/kotlin/com/coded/spring/ordering/services/UsersClient.kt create mode 100644 src/main/kotlin/com/coded/spring/ordering/services/UsersProvider.kt diff --git a/pom.xml b/pom.xml index ffd3056..8ac2167 100644 --- a/pom.xml +++ b/pom.xml @@ -9,9 +9,9 @@ com.example - authentication + ordering 0.0.1-SNAPSHOT - authentication + ordering Demo project for Spring Boot diff --git a/src/main/kotlin/com/coded/spring/ordering/Application.kt b/src/main/kotlin/com/coded/spring/ordering/Application.kt index 021f276..9108d56 100644 --- a/src/main/kotlin/com/coded/spring/ordering/Application.kt +++ b/src/main/kotlin/com/coded/spring/ordering/Application.kt @@ -5,14 +5,16 @@ import com.hazelcast.core.Hazelcast import com.hazelcast.core.HazelcastInstance import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication +import org.springframework.scheduling.annotation.EnableScheduling +@EnableScheduling @SpringBootApplication class Application fun main(args: Array) { runApplication(*args) - ordersConfig.getMapConfig("orders-cache").setTimeToLiveSeconds(5) + usersConfig.getMapConfig("users-cache").timeToLiveSeconds = 5 } -val ordersConfig = Config("orders-cache") -val serverCache: HazelcastInstance = Hazelcast.newHazelcastInstance(ordersConfig) +val usersConfig = Config("users-cache") +val serverCache: HazelcastInstance = Hazelcast.newHazelcastInstance(usersConfig) diff --git a/src/main/kotlin/com/coded/spring/ordering/controllers/OrdersController.kt b/src/main/kotlin/com/coded/spring/ordering/controllers/OrdersController.kt index c5ba95e..62bd42a 100644 --- a/src/main/kotlin/com/coded/spring/ordering/controllers/OrdersController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/controllers/OrdersController.kt @@ -1,6 +1,5 @@ package com.coded.spring.ordering.controllers -import com.coded.spring.ordering.entities.ItemEntity import com.coded.spring.ordering.entities.OrderEntity import com.coded.spring.ordering.repositories.OrdersRepository import com.coded.spring.ordering.services.OrdersService diff --git a/src/main/kotlin/com/coded/spring/ordering/controllers/UsersController.kt b/src/main/kotlin/com/coded/spring/ordering/controllers/UsersController.kt index 8720657..d10efa2 100644 --- a/src/main/kotlin/com/coded/spring/ordering/controllers/UsersController.kt +++ b/src/main/kotlin/com/coded/spring/ordering/controllers/UsersController.kt @@ -3,7 +3,9 @@ package com.coded.spring.ordering.controllers import com.coded.spring.ordering.entities.Roles import com.coded.spring.ordering.entities.UserEntity import com.coded.spring.ordering.services.ProfilesService +import com.coded.spring.ordering.services.User import com.coded.spring.ordering.services.UsersService +import com.coded.spring.ordering.services.UsersProvider import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses @@ -17,7 +19,7 @@ import org.springframework.web.bind.annotation.* @Tag(name = "Users API", description = "Operations related to Users and Profiles") class UsersControllers( private val usersService: UsersService, - private val profileService: ProfilesService + private val profileService: ProfilesService, ) { @Operation( @@ -35,7 +37,7 @@ class UsersControllers( ] ) @GetMapping("/users/v1/list") - fun users() = usersService.listUsers() + fun fetchUsers(): List = usersService.listUsers() @Operation( summary = "List all profiles", diff --git a/src/main/kotlin/com/coded/spring/ordering/services/UsersClient.kt b/src/main/kotlin/com/coded/spring/ordering/services/UsersClient.kt new file mode 100644 index 0000000..423a557 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/services/UsersClient.kt @@ -0,0 +1,36 @@ +package com.coded.spring.ordering.services + +import com.coded.spring.ordering.authentication.jwt.JwtService +import jakarta.inject.Named +import org.springframework.core.ParameterizedTypeReference +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.web.client.RestTemplate +import org.springframework.web.client.exchange + + +@Named +class UsersClient( + private val jwtService: JwtService +) { + fun getUsers(): List { + val restTemplate = RestTemplate() + val url = "http://localhost:8080/users/v1/list" + + val headers = HttpHeaders() + headers.setBearerAuth(jwtService.generateToken("testuser") + ) + + val entity = HttpEntity(headers) + + val response = restTemplate.exchange>( + url = url, + method = HttpMethod.GET, + requestEntity = entity, + object : ParameterizedTypeReference>() {} + ) + + return response.body ?: listOf() + } +} diff --git a/src/main/kotlin/com/coded/spring/ordering/services/UsersProvider.kt b/src/main/kotlin/com/coded/spring/ordering/services/UsersProvider.kt new file mode 100644 index 0000000..63d444a --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/services/UsersProvider.kt @@ -0,0 +1,31 @@ +package com.coded.spring.ordering.services + +import com.coded.spring.ordering.repositories.UsersRepository +import com.coded.spring.ordering.serverCache +import jakarta.inject.Named + +val usersCache = serverCache.getMap>("users-cache") + +@Named +class UsersProvider( + private val usersRepository: UsersRepository +) { + + fun getUsers(): List { + val cachedUsers = usersCache["users"] + if (!cachedUsers.isNullOrEmpty()) { + println("Returning ${cachedUsers.size} users from cache.") + return cachedUsers + } + + println("No cached users found, calling API...") + val users = usersRepository.findAll().map { User( + id = it.id!!, + name = it.name, + username = it.username + ) } + usersCache.put("users", users) + println("Cached ${users.size} users.") + return users + } +} diff --git a/src/main/kotlin/com/coded/spring/ordering/services/UsersService.kt b/src/main/kotlin/com/coded/spring/ordering/services/UsersService.kt index 4f56d41..74c14fb 100644 --- a/src/main/kotlin/com/coded/spring/ordering/services/UsersService.kt +++ b/src/main/kotlin/com/coded/spring/ordering/services/UsersService.kt @@ -3,10 +3,11 @@ package com.coded.spring.ordering.services import com.coded.spring.ordering.entities.UserEntity import com.coded.spring.ordering.repositories.UsersRepository import jakarta.inject.Named +import org.springframework.cache.annotation.Cacheable @Named class UsersService( - private val usersRepository: UsersRepository, + private val usersRepository: UsersRepository, private val usersProvider: UsersProvider ) { fun getUserById(userId: Long): UserEntity { @@ -23,7 +24,16 @@ class UsersService( } - fun listUsers(): List = usersRepository.findAll().map { +// fun listUsers(): List = usersRepository.findAll().map { +// User( +// id = it.id!!, +// name = it.name, +// username = it.username, +// ) +// } + fun listUsers(): List = usersProvider.getUsers() + + fun listUsersDirect(): List = usersRepository.findAll().map { User( id = it.id!!, name = it.name, @@ -31,6 +41,7 @@ class UsersService( ) } + fun createUser(user: UserEntity): UserEntity { return usersRepository.save(user) }