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/src/main/kotlin/com/coded/spring/ordering/Application.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/AuthenticationApplication.kt
similarity index 57%
rename from src/main/kotlin/com/coded/spring/ordering/Application.kt
rename to authentication/src/main/kotlin/com/coded/spring/authentication/AuthenticationApplication.kt
index 8554e49..97e486b 100644
--- a/src/main/kotlin/com/coded/spring/ordering/Application.kt
+++ b/authentication/src/main/kotlin/com/coded/spring/authentication/AuthenticationApplication.kt
@@ -1,11 +1,12 @@
-package com.coded.spring.ordering
+package com.coded.spring.authentication
+
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
-class Application
+class AuthenticationApplication
fun main(args: Array) {
- runApplication(*args)
-}
+ runApplication(*args)
+}
\ No newline at end of file
diff --git a/authentication/src/main/kotlin/com/coded/spring/authentication/AuthenticationController.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/AuthenticationController.kt
new file mode 100644
index 0000000..50a4c5b
--- /dev/null
+++ b/authentication/src/main/kotlin/com/coded/spring/authentication/AuthenticationController.kt
@@ -0,0 +1,76 @@
+package com.coded.spring.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.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")
+@RestController
+@RequestMapping("/authentication")
+class AuthenticationController(
+ private val authenticationManager: AuthenticationManager,
+ private val userDetailsService: UserDetailsService,
+ 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)
+ 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!")
+ }
+ }
+
+ @PostMapping("/check-token")
+ fun checkToken(
+ principal: Principal
+ ): CheckTokenResponse {
+ return CheckTokenResponse(
+ userId = userService.findByUsername(principal.name)
+ )
+ }
+}
+
+
+data class AuthenticationRequest(
+ val username: String,
+ val password: String
+)
+
+data class AuthenticationResponse(
+ val token: String
+)
+
+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/authentication/src/main/kotlin/com/coded/spring/authentication/CustomUserDetailsService.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/CustomUserDetailsService.kt
new file mode 100644
index 0000000..97d1061
--- /dev/null
+++ b/authentication/src/main/kotlin/com/coded/spring/authentication/CustomUserDetailsService.kt
@@ -0,0 +1,25 @@
+package com.coded.spring.authentication
+
+
+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
+
+@Service
+class CustomUserDetailsService(
+ private val userRepository: UserRepository
+) : UserDetailsService {
+ override fun loadUserByUsername(username: String): UserDetails {
+ val user : UserEntity = userRepository.findByUsername(username) ?:
+ throw UsernameNotFoundException("User not found...")
+
+ return User.builder()
+ .username(user.username)
+ .password(user.password)
+ .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/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/authentication/src/main/kotlin/com/coded/spring/authentication/SecurityConfig.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/SecurityConfig.kt
new file mode 100644
index 0000000..efbb924
--- /dev/null
+++ b/authentication/src/main/kotlin/com/coded/spring/authentication/SecurityConfig.kt
@@ -0,0 +1,61 @@
+package com.coded.spring.authentication
+
+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
+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.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 jwtAuthFilter: JwtAuthenticationFilter,
+) {
+
+ @Bean
+ fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()
+
+ @Bean
+ fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+ http.csrf { it.disable() }
+ .authorizeHttpRequests {
+ it.requestMatchers(
+ "/register",
+ "/authentication/**",
+ "api-docs",
+ "/hello").permitAll() // public route
+ .anyRequest().authenticated()
+ }
+ .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/authentication/src/main/kotlin/com/coded/spring/authentication/jwt/JwtAuthenticationFilter.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/jwt/JwtAuthenticationFilter.kt
new file mode 100644
index 0000000..10e75b5
--- /dev/null
+++ b/authentication/src/main/kotlin/com/coded/spring/authentication/jwt/JwtAuthenticationFilter.kt
@@ -0,0 +1,47 @@
+package com.coded.spring.authentication.jwt
+
+
+import jakarta.servlet.FilterChain
+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
+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/authentication/src/main/kotlin/com/coded/spring/authentication/jwt/JwtService.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/jwt/JwtService.kt
new file mode 100644
index 0000000..b43d384
--- /dev/null
+++ b/authentication/src/main/kotlin/com/coded/spring/authentication/jwt/JwtService.kt
@@ -0,0 +1,43 @@
+package com.coded.spring.authentication.jwt
+
+import io.jsonwebtoken.Jwts
+import io.jsonwebtoken.SignatureAlgorithm
+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/authentication/src/main/kotlin/com/coded/spring/authentication/profiles/ProfileController.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/profiles/ProfileController.kt
new file mode 100644
index 0000000..31c1404
--- /dev/null
+++ b/authentication/src/main/kotlin/com/coded/spring/authentication/profiles/ProfileController.kt
@@ -0,0 +1,27 @@
+package com.coded.spring.authentication.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
+) {
+ @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/authentication/src/main/kotlin/com/coded/spring/authentication/profiles/ProfileRepository.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/profiles/ProfileRepository.kt
new file mode 100644
index 0000000..5b9c2b2
--- /dev/null
+++ b/authentication/src/main/kotlin/com/coded/spring/authentication/profiles/ProfileRepository.kt
@@ -0,0 +1,35 @@
+package com.coded.spring.authentication.profiles
+
+
+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: Long): ProfileEntity?
+}
+
+@Entity
+@Table(name = "profiles")
+data class ProfileEntity(
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ val id: Long? = null,
+
+ @OneToOne
+ @JoinColumn(name = "user_id")
+ val user: 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/authentication/src/main/kotlin/com/coded/spring/authentication/profiles/ProfileService.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/profiles/ProfileService.kt
new file mode 100644
index 0000000..92bb99a
--- /dev/null
+++ b/authentication/src/main/kotlin/com/coded/spring/authentication/profiles/ProfileService.kt
@@ -0,0 +1,48 @@
+package com.coded.spring.authentication.profiles
+
+import com.coded.spring.authentication.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 {
+ 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"))
+ }
+
+ 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 existingProfile = profileRepository.findByUserId(user.id!!)
+
+ val profile = if (existingProfile != null) {
+ existingProfile.copy(
+ firstName = request.firstName,
+ lastName = request.lastName,
+ phoneNumber = request.phoneNumber
+ )
+ } else {
+ ProfileEntity(
+ user = user,
+ firstName = request.firstName,
+ lastName = request.lastName,
+ phoneNumber = request.phoneNumber
+ )
+ }
+
+ profileRepository.save(profile)
+ return ResponseEntity.ok().build()
+ }
+}
diff --git a/authentication/src/main/kotlin/com/coded/spring/authentication/scripts/InitUserRunner.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/scripts/InitUserRunner.kt
new file mode 100644
index 0000000..a1d7d64
--- /dev/null
+++ b/authentication/src/main/kotlin/com/coded/spring/authentication/scripts/InitUserRunner.kt
@@ -0,0 +1,34 @@
+package com.coded.spring.authentication.scripts
+
+
+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.authentication.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/authentication/src/main/kotlin/com/coded/spring/authentication/users/UserController.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/users/UserController.kt
new file mode 100644
index 0000000..98ebe8c
--- /dev/null
+++ b/authentication/src/main/kotlin/com/coded/spring/authentication/users/UserController.kt
@@ -0,0 +1,30 @@
+package com.coded.spring.authentication.users
+
+
+import io.swagger.v3.oas.annotations.tags.Tag
+import jakarta.validation.constraints.NotBlank
+import org.springframework.http.ResponseEntity
+import org.springframework.web.bind.annotation.PostMapping
+import org.springframework.web.bind.annotation.RequestBody
+import org.springframework.web.bind.annotation.RestController
+
+
+@Tag(name="UserAPI")
+@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/authentication/src/main/kotlin/com/coded/spring/authentication/users/UserRepository.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/users/UserRepository.kt
new file mode 100644
index 0000000..a3e2852
--- /dev/null
+++ b/authentication/src/main/kotlin/com/coded/spring/authentication/users/UserRepository.kt
@@ -0,0 +1,31 @@
+package com.coded.spring.authentication.users
+
+import jakarta.inject.Named
+import jakarta.persistence.*
+import org.springframework.data.jpa.repository.JpaRepository
+
+@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/authentication/src/main/kotlin/com/coded/spring/authentication/users/UserService.kt b/authentication/src/main/kotlin/com/coded/spring/authentication/users/UserService.kt
new file mode 100644
index 0000000..a2369e2
--- /dev/null
+++ b/authentication/src/main/kotlin/com/coded/spring/authentication/users/UserService.kt
@@ -0,0 +1,45 @@
+package com.coded.spring.authentication.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 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) {
+ 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)
+
+ return ResponseEntity.ok().build()
+ }
+ }
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/ordering/src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt b/ordering/src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt
new file mode 100644
index 0000000..746a783
--- /dev/null
+++ b/ordering/src/main/kotlin/com/coded/spring/ordering/HelloWorldController.kt
@@ -0,0 +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(
+ @Value("\${hello-world}")
+ val helloWorldMessage: String
+) {
+
+ @GetMapping("/hello")
+ fun helloWorld() = helloWorldMessage;
+}
\ No newline at end of file
diff --git a/ordering/src/main/kotlin/com/coded/spring/ordering/LoggingFilter.kt b/ordering/src/main/kotlin/com/coded/spring/ordering/LoggingFilter.kt
new file mode 100644
index 0000000..e7e5440
--- /dev/null
+++ b/ordering/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/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/OrderingApplication.kt b/ordering/src/main/kotlin/com/coded/spring/ordering/OrderingApplication.kt
new file mode 100644
index 0000000..ad3e6f1
--- /dev/null
+++ b/ordering/src/main/kotlin/com/coded/spring/ordering/OrderingApplication.kt
@@ -0,0 +1,18 @@
+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
+
+@SpringBootApplication
+class OrderingApplication
+
+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/ordering/src/main/kotlin/com/coded/spring/ordering/WelcomeController.kt b/ordering/src/main/kotlin/com/coded/spring/ordering/WelcomeController.kt
new file mode 100644
index 0000000..0676637
--- /dev/null
+++ b/ordering/src/main/kotlin/com/coded/spring/ordering/WelcomeController.kt
@@ -0,0 +1,21 @@
+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/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/ordering/src/main/kotlin/com/coded/spring/ordering/items/ItemsRepository.kt b/ordering/src/main/kotlin/com/coded/spring/ordering/items/ItemsRepository.kt
new file mode 100644
index 0000000..d4d449b
--- /dev/null
+++ b/ordering/src/main/kotlin/com/coded/spring/ordering/items/ItemsRepository.kt
@@ -0,0 +1,29 @@
+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
+ @ManyToOne
+ @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/ordering/src/main/kotlin/com/coded/spring/ordering/menus/MenuController.kt b/ordering/src/main/kotlin/com/coded/spring/ordering/menus/MenuController.kt
new file mode 100644
index 0000000..486b614
--- /dev/null
+++ b/ordering/src/main/kotlin/com/coded/spring/ordering/menus/MenuController.kt
@@ -0,0 +1,23 @@
+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
+) {
+ @GetMapping("/menus")
+ fun listMenu() = menuService.listMenu()
+
+ @PostMapping("/menus")
+ fun createMenu(@RequestBody menu: MenuDTO): MenuDTO = menuService.addMenu(menu)
+}
+
+
+data class MenuDTO(
+ val name: String,
+ val price: BigDecimal
+)
diff --git a/ordering/src/main/kotlin/com/coded/spring/ordering/menus/MenuRepository.kt b/ordering/src/main/kotlin/com/coded/spring/ordering/menus/MenuRepository.kt
new file mode 100644
index 0000000..8db7513
--- /dev/null
+++ b/ordering/src/main/kotlin/com/coded/spring/ordering/menus/MenuRepository.kt
@@ -0,0 +1,26 @@
+package com.coded.spring.ordering.menus
+
+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
+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/ordering/src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt b/ordering/src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt
new file mode 100644
index 0000000..6b409ee
--- /dev/null
+++ b/ordering/src/main/kotlin/com/coded/spring/ordering/menus/MenuService.kt
@@ -0,0 +1,53 @@
+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,
+ @Value("\${feature.festive.enabled}")
+ private val festiveIsEnabled: Boolean
+) {
+ fun listMenu(): List {
+ val menusCache = serverCache.getMap>("menus")
+ if (menusCache["menus"]?.size == 0 || menusCache["menus"] == null) {
+ logger.info("No menus found, caching new data...")
+ 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()
+ }
+
+
+ fun addMenu(dto: MenuDTO): MenuDTO {
+ val newMenu = MenuEntity(
+ name = dto.name,
+ price = dto.price
+ )
+ menuRepository.save(newMenu)
+
+ val menusCache = serverCache.getMap>("menus")
+ menusCache.remove("menus")
+ return MenuDTO(newMenu.name, newMenu.price)
+ }
+}
\ 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
new file mode 100644
index 0000000..7189fca
--- /dev/null
+++ b/ordering/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingController.kt
@@ -0,0 +1,46 @@
+package com.coded.spring.ordering.orders
+
+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="OrderAPI")
+@RestController
+class OnlineOrderingController(
+ private val onlineOrderingService: OnlineOrderingService
+) {
+
+ @GetMapping("/orders")
+ fun getOrders(request: HttpServletRequest): List {
+ val userId = request.getAttribute("userId") as Long
+ return onlineOrderingService.getOrders(userId)
+ }
+
+ @PostMapping("/orders/add")
+ fun addOrders(request: HttpServletRequest, @RequestBody body: RequestOrder): ResponseEntity{
+ val userId = request.getAttribute("userId") as Long
+ return onlineOrderingService.addOrders(userId ,body)
+ }
+}
+
+
+
+// the DTO (Data Transfer Object) for our orders and items list
+data class RequestItem(
+ val name: String,
+ val price: Double
+)
+
+data class RequestOrder(
+ val userId: Long,
+ val restaurant: String,
+ val items: List
+)
+
+data class OrderResponseDTO(
+ val orderId: Long,
+ val restaurant: String,
+ val items: List,
+ val timeOrdered: String?
+)
diff --git a/ordering/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingRepository.kt b/ordering/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingRepository.kt
new file mode 100644
index 0000000..044c9a2
--- /dev/null
+++ b/ordering/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingRepository.kt
@@ -0,0 +1,39 @@
+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
+import org.hibernate.annotations.CreationTimestamp
+import java.time.LocalDateTime
+
+@Named
+interface OrderRepository: JpaRepository {
+ fun findByUserId(userId: Long): List
+}
+
+@Entity
+@Table(name = "orders")
+data class OrderEntity(
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ var id: Long? = null,
+
+ var userId: Long? = null,
+
+ var restaurant: 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, "", null, null)
+}
\ No newline at end of file
diff --git a/ordering/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingService.kt b/ordering/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingService.kt
new file mode 100644
index 0000000..c9be77c
--- /dev/null
+++ b/ordering/src/main/kotlin/com/coded/spring/ordering/orders/OnlineOrderingService.kt
@@ -0,0 +1,69 @@
+package com.coded.spring.ordering.orders
+
+import com.coded.spring.ordering.items.ItemsEntity
+import com.coded.spring.ordering.items.ItemsRepository
+import org.springframework.http.HttpStatus
+import org.springframework.http.ResponseEntity
+import org.springframework.stereotype.Service
+
+@Service
+class OnlineOrderingService(
+ private val orderRepository: OrderRepository,
+ private val itemsRepository: ItemsRepository,
+) {
+ 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 }) {
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST)
+ .body(mapOf("error" to "item price cannot be negative"))
+ }
+
+ val order = orderRepository.save(
+ OrderEntity(
+ userId = userId,
+ restaurant = request.restaurant
+ )
+ )
+
+ val items = request.items.map { item ->
+ ItemsEntity(
+ order = order,
+ name = item.name,
+ price = item.price
+ )
+ }
+
+ itemsRepository.saveAll(items)
+
+ return ResponseEntity.status(HttpStatus.OK).body(
+ OrderResponseDTO(
+ orderId = order.id!!,
+ restaurant = order.restaurant,
+ timeOrdered = order.timeOrdered.toString(),
+ items = items.map {
+ RequestItem(name = it.name, price = it.price)
+ }
+ )
+ )
+ }
+}
+
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 163ad53..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
+
@@ -53,11 +58,81 @@
spring-boot-starter-test
test
+
+ org.postgresql
+ postgresql
+ compile
+
org.jetbrains.kotlin
kotlin-test-junit5
test
+
+ jakarta.inject
+ jakarta.inject-api
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ jakarta.validation
+ jakarta.validation-api
+ 3.0.2
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.11.5
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 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
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 3704dc6..fa5d06a 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1 +1,8 @@
spring.application.name=Kotlin.SpringbootV2
+server.port=9003
+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/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt b/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt
index b2e2320..a3e3079 100644
--- a/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt
+++ b/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt
@@ -1,13 +1,391 @@
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
+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.authentication.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.math.BigDecimal
+import kotlin.test.assertEquals
-@SpringBootTest
+@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 `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
+ 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 `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"))
+ )
+
+ 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,
+ OrderResponseDTO::class.java
+ )
+
+ 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 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")
+ 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)
+ }
+
+ @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 contextLoads() {
+ 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)
+ }
}
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..a2297bc
--- /dev/null
+++ b/src/test/kotlin/com/coded/spring/ordering/CucumberRunner.kt
@@ -0,0 +1,12 @@
+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,
+ 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
new file mode 100644
index 0000000..9e4a9b0
--- /dev/null
+++ b/src/test/kotlin/com/coded/spring/ordering/steps/AuthenticationSteps.kt
@@ -0,0 +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/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/application-test.properties b/src/test/resources/application-test.properties
new file mode 100644
index 0000000..f1133ab
--- /dev/null
+++ b/src/test/resources/application-test.properties
@@ -0,0 +1,11 @@
+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
diff --git a/src/test/resources/features/authentication.feature b/src/test/resources/features/authentication.feature
new file mode 100644
index 0000000..4b74601
--- /dev/null
+++ 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"
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