diff --git a/pom.xml b/pom.xml
index 163ad53..173248a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,63 +1,183 @@
-
+
4.0.0
+
org.springframework.boot
spring-boot-starter-parent
3.4.4
-
+
+
com.coded.spring
Ordering
0.0.1-SNAPSHOT
Kotlin.SpringbootV2
Kotlin.SpringbootV2
-
-
-
-
-
-
-
-
-
-
-
-
-
+
21
1.9.25
+
+
org.springframework.boot
spring-boot-starter-web
+
- com.fasterxml.jackson.module
- jackson-module-kotlin
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+
+ org.springframework.security
+ spring-security-crypto
+
+
+
+ org.postgresql
+ postgresql
+ runtime
+
+
+
+ com.h2database
+ h2
+ test
+
+
+
org.jetbrains.kotlin
kotlin-reflect
+
org.jetbrains.kotlin
kotlin-stdlib
+
+ com.fasterxml.jackson.module
+ jackson-module-kotlin
+
+
+
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.11.5
+
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ runtime
+ 0.11.5
+
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ runtime
+ 0.11.5
+
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.5.0
+
+
+
+
+ jakarta.inject
+ jakarta.inject-api
+
+
+
+
+ com.hazelcast
+ hazelcast
+ 5.5.0
+
+
+
org.springframework.boot
spring-boot-starter-test
test
+
org.jetbrains.kotlin
kotlin-test-junit5
test
+
+
+ io.cucumber
+ cucumber-java
+ 7.11.2
+ test
+
+
+
+ io.cucumber
+ cucumber-junit
+ 7.11.2
+ test
+
+
+
+ io.rest-assured
+ rest-assured
+ 5.3.0
+ test
+
+
+
+ io.rest-assured
+ json-path
+ 5.3.0
+ test
+
+
+
+ io.rest-assured
+ json-schema-validator
+ 5.3.0
+ test
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.10.0
+ test
+
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.10.0
+ test
+
+
@@ -90,4 +210,4 @@
-
+
\ No newline at end of file
diff --git a/src/main/kotlin/com/coded/spring/ordering/AppProperties.kt b/src/main/kotlin/com/coded/spring/ordering/AppProperties.kt
new file mode 100644
index 0000000..dd101b6
--- /dev/null
+++ b/src/main/kotlin/com/coded/spring/ordering/AppProperties.kt
@@ -0,0 +1,13 @@
+package com.coded.spring.ordering
+
+import org.springframework.boot.context.properties.ConfigurationProperties
+import org.springframework.context.annotation.Configuration
+
+@Configuration
+@ConfigurationProperties
+class AppProperties {
+ lateinit var companyName: String
+ var festiveMode: Boolean = false
+ var festiveMessage: String = "Eidkom Mubarak Coded"
+ var festiveDiscount: Int = 20
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/coded/spring/ordering/Application.kt b/src/main/kotlin/com/coded/spring/ordering/Application.kt
index 8554e49..b4ea854 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)
+ helloWorldConfig.getMapConfig("pets").setTimeToLiveSeconds(10)
}
+
+val helloWorldConfig = Config("hello-world-cache")
+val serverCache: HazelcastInstance = Hazelcast.newHazelcastInstance(helloWorldConfig)
\ No newline at end of file
diff --git a/src/main/kotlin/com/coded/spring/ordering/InitUserRunner.kt b/src/main/kotlin/com/coded/spring/ordering/InitUserRunner.kt
new file mode 100644
index 0000000..2db67a2
--- /dev/null
+++ b/src/main/kotlin/com/coded/spring/ordering/InitUserRunner.kt
@@ -0,0 +1,34 @@
+package com.coded.spring.ordering
+
+import com.coded.spring.ordering.models.Roles
+import com.coded.spring.ordering.models.User
+import com.coded.spring.ordering.repo.UserRepository
+import org.springframework.boot.CommandLineRunner
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.runApplication
+import org.springframework.context.annotation.Bean
+import org.springframework.security.crypto.password.PasswordEncoder
+
+@SpringBootApplication
+class InitUserRunner {
+ @Bean
+ fun initUsers(userRepository: UserRepository, passwordEncoder: PasswordEncoder) = CommandLineRunner {
+ val user = User(
+ name = "HelloUser",
+ username = "testuser",
+ password = passwordEncoder.encode("Password123@"),
+ age = 18,
+ role = Roles.ADMIN
+ )
+ 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)
+}
\ 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..ac5baa8
--- /dev/null
+++ b/src/main/kotlin/com/coded/spring/ordering/LoggingFilter.kt
@@ -0,0 +1,37 @@
+package com.coded.spring.ordering
+
+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
+
+@Component
+class LoggingFilter: OncePerRequestFilter(){
+ override fun doFilterInternal(
+ request: HttpServletRequest,
+ response: HttpServletResponse,
+ filterChain: FilterChain
+ ) {
+ val cachedRequest = ContentCachingRequestWrapper(request)
+ val cachedResponse = ContentCachingResponseWrapper(response)
+
+ filterChain.doFilter(cachedRequest, cachedResponse)
+
+ logRequest(cachedRequest)
+ logResponse(cachedResponse)
+ cachedResponse.copyBodyToResponse()
+ }
+
+ private fun logRequest(request: ContentCachingRequestWrapper) {
+ val requestBody = String(request.contentAsByteArray)
+ logger.info("Request: method=${request.method}, uri=${request.requestURI}, body=$requestBody")
+ }
+
+ private fun logResponse(response: ContentCachingResponseWrapper) {
+ val responseBody = String(response.contentAsByteArray)
+ logger.info("Response: status=${response.status}, body=$responseBody")
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/CustomUserDetailsService.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/CustomUserDetailsService.kt
new file mode 100644
index 0000000..4dea4f8
--- /dev/null
+++ b/src/main/kotlin/com/coded/spring/ordering/authentication/CustomUserDetailsService.kt
@@ -0,0 +1,19 @@
+package com.coded.spring.ordering.authentication
+
+import org.springframework.security.core.userdetails.*
+import com.coded.spring.ordering.repo.UserRepository
+import org.springframework.stereotype.Service
+
+@Service
+class CustomUserDetailsService(
+ private val usersRepository: UserRepository) : UserDetailsService {
+ override fun loadUserByUsername(username: String): UserDetails {
+ val user = usersRepository.findByUsername(username) ?: throw UsernameNotFoundException("User not found")
+
+ return User.builder()
+ .username(user.username)
+ .password(user.password)
+ .roles(user.role.toString())
+ .build()
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt
new file mode 100644
index 0000000..76e2ac9
--- /dev/null
+++ b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt
@@ -0,0 +1,63 @@
+package com.coded.spring.ordering.authentication
+
+import com.coded.spring.ordering.authentication.jwt.JwtAuthenticationFilter
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.authentication.AuthenticationManager
+import org.springframework.security.authentication.AuthenticationProvider
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
+import org.springframework.security.config.http.SessionCreationPolicy
+import org.springframework.security.core.userdetails.UserDetailsService
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
+import org.springframework.security.crypto.password.PasswordEncoder
+import org.springframework.security.web.SecurityFilterChain
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
+
+
+@Configuration
+@EnableWebSecurity
+class SecurityConfig(
+ private val jwtAuthFilter: JwtAuthenticationFilter,
+ private val userDetailsService: UserDetailsService
+) {
+
+ @Bean
+ fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+ http.csrf { it.disable() }
+ .authorizeHttpRequests {
+ it
+ .requestMatchers("/auth/**").permitAll()
+ .requestMatchers("/users/create").permitAll()
+ .requestMatchers("/users/**").hasRole("ADMIN")
+ .requestMatchers("/profile/**").authenticated()
+ .requestMatchers("/menu/**").authenticated()
+
+ .anyRequest().permitAll()
+ }
+ .sessionManagement {
+ it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+ }
+ .authenticationProvider(authenticationProvider())
+ .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter::class.java)
+
+ return http.build()
+ }
+
+ @Bean
+ fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()
+
+ @Bean
+ fun authenticationManager(config: AuthenticationConfiguration): AuthenticationManager =
+ config.authenticationManager
+
+ @Bean
+ fun authenticationProvider(): AuthenticationProvider {
+ val provider = DaoAuthenticationProvider()
+ provider.setUserDetailsService(userDetailsService)
+ provider.setPasswordEncoder(passwordEncoder())
+ return provider
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/AuthenticationController.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/AuthenticationController.kt
new file mode 100644
index 0000000..e579c57
--- /dev/null
+++ b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/AuthenticationController.kt
@@ -0,0 +1,39 @@
+package com.coded.spring.ordering.authentication.jwt
+
+import org.springframework.security.authentication.*
+import org.springframework.security.core.userdetails.UserDetailsService
+import org.springframework.security.core.userdetails.UsernameNotFoundException
+import org.springframework.web.bind.annotation.*
+
+
+@RestController
+@RequestMapping("/auth")
+class AuthenticationController(
+ private val authenticationManager: AuthenticationManager,
+ private val userDetailsService: UserDetailsService,
+ private val jwtService: JwtService
+) {
+
+ @PostMapping("/login")
+ fun login(@RequestBody authRequest: AuthenticationRequest): AuthenticationResponse {
+ val authToken = UsernamePasswordAuthenticationToken(authRequest.username, authRequest.password)
+ val authentication = authenticationManager.authenticate(authToken)
+
+ if (authentication.isAuthenticated) {
+ val userDetails = userDetailsService.loadUserByUsername(authRequest.username)
+ val token = jwtService.generateToken(userDetails.username)
+ return AuthenticationResponse (token)
+ } else {
+ throw UsernameNotFoundException("Invalid user request!")
+ }
+ }
+}
+
+data class AuthenticationRequest(
+ val username: String,
+ val password: String
+)
+
+data class AuthenticationResponse(
+ val token: String
+)
\ No newline at end of file
diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtAuthenticationFilter.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtAuthenticationFilter.kt
new file mode 100644
index 0000000..f180b7c
--- /dev/null
+++ b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtAuthenticationFilter.kt
@@ -0,0 +1,45 @@
+package com.coded.spring.ordering.authentication.jwt
+
+import jakarta.servlet.FilterChain
+import jakarta.servlet.http.*
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
+import org.springframework.security.core.context.SecurityContextHolder
+import org.springframework.security.core.userdetails.UserDetailsService
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
+import org.springframework.stereotype.Component
+import org.springframework.web.filter.OncePerRequestFilter
+
+@Component
+class JwtAuthenticationFilter(
+ private val jwtService: JwtService,
+ private val userDetailsService: UserDetailsService
+) : OncePerRequestFilter() {
+
+ override fun doFilterInternal(
+ request: HttpServletRequest,
+ response: HttpServletResponse,
+ filterChain: FilterChain
+ ) {
+ val authHeader = request.getHeader("Authorization")
+ if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+ filterChain.doFilter(request, response)
+ return
+ }
+
+ val token = authHeader.substring(7)
+ val username = jwtService.extractUsername(token)
+
+ if (SecurityContextHolder.getContext().authentication == null) {
+ if (jwtService.isTokenValid(token, username)) {
+ val userDetails = userDetailsService.loadUserByUsername(username)
+ val authToken = UsernamePasswordAuthenticationToken(
+ userDetails, null, userDetails.authorities
+ )
+ authToken.details = WebAuthenticationDetailsSource().buildDetails(request)
+ SecurityContextHolder.getContext().authentication = authToken
+ }
+ }
+
+ filterChain.doFilter(request, response)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtService.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtService.kt
new file mode 100644
index 0000000..c877c7c
--- /dev/null
+++ b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtService.kt
@@ -0,0 +1,42 @@
+package com.coded.spring.ordering.authentication.jwt
+
+import io.jsonwebtoken.*
+import io.jsonwebtoken.security.Keys
+import org.springframework.stereotype.Component
+import java.util.*
+import javax.crypto.SecretKey
+
+@Component
+class JwtService {
+
+ private val secretKey: SecretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256)
+ private val expirationMs: Long = 1000 * 60 * 60
+
+ fun generateToken(username: String): String {
+ val now = Date()
+ val expiry = Date(now.time + expirationMs)
+
+ return Jwts.builder()
+ .setSubject(username)
+ .setIssuedAt(now)
+ .setExpiration(expiry)
+ .signWith(secretKey)
+ .compact()
+ }
+
+ fun extractUsername(token: String): String =
+ Jwts.parserBuilder()
+ .setSigningKey(secretKey)
+ .build()
+ .parseClaimsJws(token)
+ .body
+ .subject
+
+ fun isTokenValid(token: String, username: String): Boolean {
+ return try {
+ extractUsername(token) == username
+ } catch (e: Exception) {
+ false
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/coded/spring/ordering/controllers/HelloworldController.kt b/src/main/kotlin/com/coded/spring/ordering/controllers/HelloworldController.kt
new file mode 100644
index 0000000..628170d
--- /dev/null
+++ b/src/main/kotlin/com/coded/spring/ordering/controllers/HelloworldController.kt
@@ -0,0 +1,48 @@
+package com.coded.spring.ordering.controllers
+
+import com.coded.spring.ordering.AppProperties
+import com.coded.spring.ordering.pets.PetsService
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.PostMapping
+import org.springframework.web.bind.annotation.RequestBody
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+
+ @RestController
+ @RequestMapping("/api")
+ class HelloWorldController (
+ private val petsProvider: PetsService,
+ private val appProperties: AppProperties
+ ){
+
+ @GetMapping("/hello")
+ fun helloWorld(): String {
+ return "Hello World!"
+ }
+
+ @PostMapping("/myNameIs")
+ fun myName(@RequestBody request: NameRequest): NameResponse {
+ return NameResponse(id = 1, name = request.name)
+ }
+ @GetMapping("/Pets")
+ fun getPets(): String {
+ return petsProvider.listPets().toString().replace("[", "").replace("]", "")
+ }
+ @GetMapping("/Welcome/OnlineOrdering")
+ fun welcome(): String {
+ return if (appProperties.festiveMode) {
+ appProperties.festiveMessage
+ } else {
+ "Welcome to Online Ordering by ${appProperties.companyName}"
+ }
+ }
+
+ data class NameRequest(
+ val name: String
+ )
+
+ data class NameResponse(
+ val id: Int,
+ val name: String
+ )
+ }
\ No newline at end of file
diff --git a/src/main/kotlin/com/coded/spring/ordering/controllers/ItemController.kt b/src/main/kotlin/com/coded/spring/ordering/controllers/ItemController.kt
new file mode 100644
index 0000000..f948532
--- /dev/null
+++ b/src/main/kotlin/com/coded/spring/ordering/controllers/ItemController.kt
@@ -0,0 +1,28 @@
+package com.coded.spring.ordering.controllers
+
+import com.coded.spring.ordering.models.Item
+import com.coded.spring.ordering.service.ItemService
+import org.springframework.web.bind.annotation.*
+
+@RestController
+@RequestMapping("/items")
+class ItemController(
+ private val itemService: ItemService
+) {
+
+ @PostMapping
+ fun createItem(@RequestBody request: CreateItemRequest): Item {
+ return itemService.createItem(request.orderId, request.name, request.quantity)
+ }
+
+ @GetMapping
+ fun getItems(@RequestParam orderId: Long): List- {
+ return itemService.getItemsByOrderId(orderId)
+ }
+}
+
+data class CreateItemRequest(
+ val orderId: Long,
+ val name: String,
+ val quantity: Int
+)
\ No newline at end of file
diff --git a/src/main/kotlin/com/coded/spring/ordering/controllers/MenuController.kt b/src/main/kotlin/com/coded/spring/ordering/controllers/MenuController.kt
new file mode 100644
index 0000000..b95d0a4
--- /dev/null
+++ b/src/main/kotlin/com/coded/spring/ordering/controllers/MenuController.kt
@@ -0,0 +1,20 @@
+package com.coded.spring.ordering.controllers
+
+import com.coded.spring.ordering.models.MenuItem
+import com.coded.spring.ordering.service.MenuService
+import org.springframework.web.bind.annotation.*
+
+@RestController
+@RequestMapping("/menu")
+class MenuController(
+ private val menuService: MenuService
+) {
+
+ @GetMapping
+ fun getMenu(): List