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