diff --git a/OrderingDB.sql b/OrderingDB.sql new file mode 100644 index 0000000..7802adc --- /dev/null +++ b/OrderingDB.sql @@ -0,0 +1,24 @@ +CREATE TABLE users ( + id serial PRIMARY KEY, + name VARCHAR(255), + email VARCHAR(255), + username VARCHAR(255) unique, + password VARCHAR(255) +); + +CREATE TABLE orders ( + id serial PRIMARY KEY, + user_id BIGINT, + restaurant VARCHAR(255), + FOREIGN KEY (user_id) REFERENCES users(id) +); + +CREATE TABLE items ( + id serial PRIMARY KEY, + name VARCHAR(255), + quantity INT, + price DECIMAL(9, 3), + order_id BIGINT, + FOREIGN KEY (order_id) REFERENCES orders(id) +); +-- added menu too \ No newline at end of file diff --git a/authentication/pom.xml b/authentication/pom.xml new file mode 100644 index 0000000..f693b1c --- /dev/null +++ b/authentication/pom.xml @@ -0,0 +1,16 @@ + + + 4.0.0 + + com.coded.spring + monolith + 0.0.1-SNAPSHOT + + + authentication + + + + \ No newline at end of file diff --git a/authentication/src/main/kotlin/com/ali/authentication/AuthenticationApplication.kt b/authentication/src/main/kotlin/com/ali/authentication/AuthenticationApplication.kt new file mode 100644 index 0000000..50f7e45 --- /dev/null +++ b/authentication/src/main/kotlin/com/ali/authentication/AuthenticationApplication.kt @@ -0,0 +1,11 @@ +package com.ali.authentication + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class AuthenticationApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/authentication/src/main/kotlin/com/ali/authentication/AuthenticationController.kt b/authentication/src/main/kotlin/com/ali/authentication/AuthenticationController.kt new file mode 100644 index 0000000..9cc768c --- /dev/null +++ b/authentication/src/main/kotlin/com/ali/authentication/AuthenticationController.kt @@ -0,0 +1,72 @@ +package com.ali.authentication + + +import com.ali.authentication.jwt.JwtService +import com.ali.authentication.user.UserService +import org.springframework.http.HttpStatus +import org.springframework.security.authentication.* +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.core.userdetails.UsernameNotFoundException +import org.springframework.web.bind.annotation.* +import org.springframework.web.server.ResponseStatusException +import java.security.Principal + + +@RestController +@RequestMapping("/auth") +class AuthenticationController( + private val authenticationManager: AuthenticationManager, + private val userDetailsService: UserDetailsService, + private val jwtService: JwtService, + private val userService: UserService +) { + + @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) + ) + } +// @PostMapping("/check-token") +// fun checkToken(): CheckTokenResponse { +// val auth = SecurityContextHolder.getContext().authentication +// if (auth == null || !auth.isAuthenticated) { +// throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid token") +// } +// val username = auth.name +// val userId = userService.findByUsername(username) +// return CheckTokenResponse(userId) +// } +} + +data class CheckTokenResponse( + val userId: Long +) + +data class AuthenticationRequest( + val username: String, + val password: String +) + +data class AuthenticationResponse( + val token: String +) \ No newline at end of file diff --git a/authentication/src/main/kotlin/com/ali/authentication/CustomUserDetailsService.kt b/authentication/src/main/kotlin/com/ali/authentication/CustomUserDetailsService.kt new file mode 100644 index 0000000..3140a07 --- /dev/null +++ b/authentication/src/main/kotlin/com/ali/authentication/CustomUserDetailsService.kt @@ -0,0 +1,21 @@ +package com.ali.authentication + + +import com.ali.authentication.user.UserRepository +import org.springframework.security.core.userdetails.* +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) + .build() + } +} \ No newline at end of file diff --git a/authentication/src/main/kotlin/com/ali/authentication/SecurityConfig.kt b/authentication/src/main/kotlin/com/ali/authentication/SecurityConfig.kt new file mode 100644 index 0000000..fc453bb --- /dev/null +++ b/authentication/src/main/kotlin/com/ali/authentication/SecurityConfig.kt @@ -0,0 +1,62 @@ +package com.ali.authentication + + +import com.ali.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 +) { + // For authorization you need to add code in security config, customuserdetails, and jwtservice + @Bean + fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http.csrf { it.disable() } + .authorizeHttpRequests { + it +// .anyRequest().permitAll() // permit all + + .requestMatchers("/auth/login", "/public/**", "/api-docs", "/hello").permitAll() + .anyRequest() + .authenticated() + } + .sessionManagement { + it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + } + .authenticationProvider(authenticationProvider()) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter::class.java) + + return http.build() + } + + @Bean + fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder() + + @Bean + fun authenticationManager(config: AuthenticationConfiguration): AuthenticationManager = + config.authenticationManager + + @Bean + fun authenticationProvider(): AuthenticationProvider { + val provider = DaoAuthenticationProvider() + provider.setUserDetailsService(userDetailsService) + provider.setPasswordEncoder(passwordEncoder()) + return provider + } +} \ No newline at end of file diff --git a/authentication/src/main/kotlin/com/ali/authentication/jwt/JwtAuthenticationFilter.kt b/authentication/src/main/kotlin/com/ali/authentication/jwt/JwtAuthenticationFilter.kt new file mode 100644 index 0000000..d19dc66 --- /dev/null +++ b/authentication/src/main/kotlin/com/ali/authentication/jwt/JwtAuthenticationFilter.kt @@ -0,0 +1,47 @@ +package com.ali.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 + ) { + logger.info("JwtAuthenticationFilter running for: ${request.requestURI}") + 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/ali/authentication/jwt/JwtService.kt b/authentication/src/main/kotlin/com/ali/authentication/jwt/JwtService.kt new file mode 100644 index 0000000..e2af8c8 --- /dev/null +++ b/authentication/src/main/kotlin/com/ali/authentication/jwt/JwtService.kt @@ -0,0 +1,43 @@ +package com.ali.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/authentication/src/main/kotlin/com/ali/authentication/profile/ProfileEntity.kt b/authentication/src/main/kotlin/com/ali/authentication/profile/ProfileEntity.kt new file mode 100644 index 0000000..ca5f586 --- /dev/null +++ b/authentication/src/main/kotlin/com/ali/authentication/profile/ProfileEntity.kt @@ -0,0 +1,21 @@ +package com.ali.authentication.profile + +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.Table + +@Entity +@Table(name = "profiles") +data class ProfileEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + + var firstName: String, + var lastName: String, + var phoneNumber: String, + + var userId: Long, +) \ No newline at end of file diff --git a/authentication/src/main/kotlin/com/ali/authentication/profile/ProfileRepository.kt b/authentication/src/main/kotlin/com/ali/authentication/profile/ProfileRepository.kt new file mode 100644 index 0000000..9cc3bd0 --- /dev/null +++ b/authentication/src/main/kotlin/com/ali/authentication/profile/ProfileRepository.kt @@ -0,0 +1,9 @@ +package com.ali.authentication.profile + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface ProfileRepository : JpaRepository { + fun findByUserId(userId: Long): ProfileEntity? +} \ No newline at end of file diff --git a/authentication/src/main/kotlin/com/ali/authentication/profile/ProfileService.kt b/authentication/src/main/kotlin/com/ali/authentication/profile/ProfileService.kt new file mode 100644 index 0000000..06c2641 --- /dev/null +++ b/authentication/src/main/kotlin/com/ali/authentication/profile/ProfileService.kt @@ -0,0 +1,64 @@ +package com.ali.authentication.profile + + +import com.ali.authentication.user.UserRepository +import org.springframework.http.HttpStatus +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.stereotype.Service +import org.springframework.web.server.ResponseStatusException + + +@Service +class ProfileService( + private val profileRepository: ProfileRepository, + private val userRepository: UserRepository, +) { + + + fun save(profile: ProfileRequest) { + val userName = SecurityContextHolder.getContext().authentication.name + val userId = userRepository.findByUsername(userName)?.id + ?: throw IllegalArgumentException("User Not Found") + + require(profile.phoneNumber.length == 8 && profile.phoneNumber.all { it.isDigit() }) + { "Phone number must be 8 digits" } + require(profile.firstName.all { it.isLetter() }) { "First name must be letters" } + require(profile.lastName.all { it.isLetter() }) { "Last name must be letters" } + + + // To avoid the wrong user from profile saving + val tokenUsername = SecurityContextHolder.getContext().authentication.name + val requestUsername = userRepository.findById(userId).get().username + if (tokenUsername != requestUsername) + throw ResponseStatusException(HttpStatus.FORBIDDEN, "Username mismatch") + val newProfile = ProfileEntity( + userId = userId, firstName = profile.firstName, lastName = profile.lastName, + phoneNumber = profile.phoneNumber + ) + profileRepository.save(newProfile) + } + + fun view(): ProfileResponse { + val userName = SecurityContextHolder.getContext().authentication.name + val userId = userRepository.findByUsername(userName)?.id + ?: throw IllegalArgumentException("User Not Found") + val profile = + profileRepository.findByUserId(userId) ?: throw IllegalArgumentException("No profile with id $userId") + return ProfileResponse(profile.firstName, profile.lastName, + profile.phoneNumber) + } + + +} + +data class ProfileRequest( + val firstName: String, + val lastName: String, + val phoneNumber: String +) + +data class ProfileResponse( + val firstName: String, + val lastName: String, + val phoneNumber: String +) \ No newline at end of file diff --git a/authentication/src/main/kotlin/com/ali/authentication/user/UserController.kt b/authentication/src/main/kotlin/com/ali/authentication/user/UserController.kt new file mode 100644 index 0000000..e9399ab --- /dev/null +++ b/authentication/src/main/kotlin/com/ali/authentication/user/UserController.kt @@ -0,0 +1,70 @@ +package com.ali.authentication.user + +import com.ali.authentication.profile.ProfileRequest +import com.ali.authentication.profile.ProfileService +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.ResponseEntity +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController + +@Tag(name = "CustomerAPI") +@RestController +class UserController( + private val userService: UserService, + private val encoder: PasswordEncoder, + private val profileService: ProfileService, + @Value("\${company_name}") + private val fromEnvVarMessage: String, + @Value("\${festive.feature}") + private val festiveFeature: Boolean +) { + + @GetMapping("/hello") + fun hello(): String { + if(festiveFeature) + return "Eidkom Mubarak, Summer Sale is here!" + else + return "Welcome to Online Ordering by $fromEnvVarMessage" + } + + @PostMapping("/public/users/create") + fun newUser(@RequestBody userRequest: UserRequest): Any { + userService.validatePassword(userRequest.password) + return userService.createUser( + UserEntity( + name = userRequest.name, + email = userRequest.email, + username = userRequest.username, + password = encoder.encode(userRequest.password) + ) + ) + } + + @GetMapping("/users/v1/list") + fun listUsers() = userService.findAllUsers() + + @PostMapping("/auth/profile/save") + fun saveProfile(@RequestBody request: ProfileRequest): Any { + return try { + profileService.save(request) + ResponseEntity.ok().body("Profile saved successfully") + } catch (e: IllegalArgumentException) { + ResponseEntity.badRequest().body("Error while saving profile ${e.message}") + } + + } + + @GetMapping("/auth/profile/view/") + fun viewProfile(): Any { + return try { + profileService.view() + } catch (e: IllegalArgumentException) { + "error: ${e.message}" + } + } + +} \ No newline at end of file diff --git a/authentication/src/main/kotlin/com/ali/authentication/user/UserEntity.kt b/authentication/src/main/kotlin/com/ali/authentication/user/UserEntity.kt new file mode 100644 index 0000000..5973aa5 --- /dev/null +++ b/authentication/src/main/kotlin/com/ali/authentication/user/UserEntity.kt @@ -0,0 +1,20 @@ +package com.ali.authentication.user + +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.Table + +@Entity +@Table(name = "users") +data class UserEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + + var name: String, + var email: String, + val username: String, + val password: String, +) \ No newline at end of file diff --git a/authentication/src/main/kotlin/com/ali/authentication/user/UserRepository.kt b/authentication/src/main/kotlin/com/ali/authentication/user/UserRepository.kt new file mode 100644 index 0000000..0eb646e --- /dev/null +++ b/authentication/src/main/kotlin/com/ali/authentication/user/UserRepository.kt @@ -0,0 +1,10 @@ +package com.ali.authentication.user + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface UserRepository : JpaRepository { + fun findByUsername(username: String) : UserEntity? + +} \ No newline at end of file diff --git a/authentication/src/main/kotlin/com/ali/authentication/user/UserService.kt b/authentication/src/main/kotlin/com/ali/authentication/user/UserService.kt new file mode 100644 index 0000000..a0c17ff --- /dev/null +++ b/authentication/src/main/kotlin/com/ali/authentication/user/UserService.kt @@ -0,0 +1,31 @@ +package com.ali.authentication.user + + +import org.springframework.stereotype.Service + +@Service +class UserService(private val userRepository: UserRepository) { + + + fun findAllUsers() = userRepository.findAll() + + fun createUser(user: UserEntity) = userRepository.save(user) + + fun validatePassword(password: String) { + require(password.length >= 6) { "Password must be at least 6 characters long" } + require(password.any { it.isUpperCase() }) { "Password must contain at least one uppercase letter" } + require(password.any { it.isDigit() }) { "Password must contain at least one number" } + } + + + fun findByUsername(username: String): Long = + userRepository.findByUsername(username)?.id ?: throw IllegalStateException("User has no id...") + +} + +data class UserRequest( + val name: String, + val email: String, + val username: String, + val password: String +) \ No newline at end of file diff --git a/authentication/src/main/resources/application.properties b/authentication/src/main/resources/application.properties new file mode 100644 index 0000000..824e8b2 --- /dev/null +++ b/authentication/src/main/resources/application.properties @@ -0,0 +1,12 @@ +spring.application.name=Kotlin.SpringbootV2 + +server.port = 8080 + +spring.datasource.url=jdbc:postgresql://localhost:5432/OrderingDB +spring.datasource.username=postgres +spring.datasource.password=alix +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.show-sql=true +spring.jpa.properties.hibe1rnate.format_sql=true + +springdoc.api-docs.path=/api-docs \ No newline at end of file diff --git a/order/pom.xml b/order/pom.xml new file mode 100644 index 0000000..3ffc190 --- /dev/null +++ b/order/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + com.coded.spring + monolith + 0.0.1-SNAPSHOT + + + order + + + 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/Application.kt b/order/src/main/kotlin/coded/spring/OrderingApplication.kt similarity index 64% rename from src/main/kotlin/com/coded/spring/ordering/Application.kt rename to order/src/main/kotlin/coded/spring/OrderingApplication.kt index 8554e49..e937af3 100644 --- a/src/main/kotlin/com/coded/spring/ordering/Application.kt +++ b/order/src/main/kotlin/coded/spring/OrderingApplication.kt @@ -1,11 +1,14 @@ -package com.coded.spring.ordering +package coded.spring + import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication + @SpringBootApplication -class Application +class OrderingApplication fun main(args: Array) { - runApplication(*args) + runApplication(*args) } + diff --git a/order/src/main/kotlin/coded/spring/client/AuthenticationClient.kt b/order/src/main/kotlin/coded/spring/client/AuthenticationClient.kt new file mode 100644 index 0000000..1d72033 --- /dev/null +++ b/order/src/main/kotlin/coded/spring/client/AuthenticationClient.kt @@ -0,0 +1,31 @@ +package coded.spring.client + + +import com.ali.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:8080/auth/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/order/src/main/kotlin/coded/spring/controller/MenuController.kt b/order/src/main/kotlin/coded/spring/controller/MenuController.kt new file mode 100644 index 0000000..e2d69b3 --- /dev/null +++ b/order/src/main/kotlin/coded/spring/controller/MenuController.kt @@ -0,0 +1,33 @@ +package coded.spring.controller + +import coded.spring.service.MenuService +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController +import java.math.BigDecimal + + +@Tag(name = "MenuAPI") +@RestController +class MenuController(val menuService: MenuService) { + + @PostMapping("/auth/menu/add") + fun addItemsToMenu(@RequestBody menuRequest: MenuRequest): Any { + + return ResponseEntity.ok().body(menuService.addItems(menuRequest)) + } + + @GetMapping("/public/menu/list") + fun listItems() = menuService.listMenuItems() + +} + + +data class MenuRequest( + val name: String, + val price: BigDecimal +) diff --git a/order/src/main/kotlin/coded/spring/controller/OrderController.kt b/order/src/main/kotlin/coded/spring/controller/OrderController.kt new file mode 100644 index 0000000..d60fd3a --- /dev/null +++ b/order/src/main/kotlin/coded/spring/controller/OrderController.kt @@ -0,0 +1,36 @@ +package coded.spring.controller + +import coded.spring.service.OrderService +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.web.bind.annotation.* +@Tag(name = "OrderAPI") +@RestController +class OrderController(val orderService: OrderService) { + + @PostMapping("/orders/v1/submit") + fun submitOrder(@RequestBody orderRequest: OrderService.OrderRequest): OrderService.OrderResponse { + try { + orderService.createOrder(orderRequest) + } catch (e: IllegalArgumentException) { + "error submitting the order: ${e.message}" + } + return OrderService.OrderResponse(orderRequest.restaurant, orderRequest.items) + } + + @GetMapping("/orders/user/orders/{userId}") + fun listOrdersByUserId(@PathVariable userId: Long) { + try { + orderService.listOrdersByUserID(userId) + } catch (e: IllegalArgumentException) { + "error occurred while listing orders: ${e.message}" + } + + } +} + + + + + + + diff --git a/order/src/main/kotlin/coded/spring/entity/ItemEntity.kt b/order/src/main/kotlin/coded/spring/entity/ItemEntity.kt new file mode 100644 index 0000000..5b62231 --- /dev/null +++ b/order/src/main/kotlin/coded/spring/entity/ItemEntity.kt @@ -0,0 +1,21 @@ +package coded.spring.entity + +import jakarta.persistence.* +import java.math.BigDecimal + +@Entity +@Table(name = "items") +data class ItemEntity ( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + + var name: String, + var quantity: Int, + var price : BigDecimal, + + var orderId: Long + +) + + diff --git a/order/src/main/kotlin/coded/spring/entity/MenuEntity.kt b/order/src/main/kotlin/coded/spring/entity/MenuEntity.kt new file mode 100644 index 0000000..2524cc1 --- /dev/null +++ b/order/src/main/kotlin/coded/spring/entity/MenuEntity.kt @@ -0,0 +1,16 @@ +package coded.spring.entity + +import jakarta.persistence.* +import java.math.BigDecimal + +@Entity +@Table(name = "menu") +data class MenuEntity( + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + + var name: String, + var price: BigDecimal +) diff --git a/order/src/main/kotlin/coded/spring/entity/OrderEntity.kt b/order/src/main/kotlin/coded/spring/entity/OrderEntity.kt new file mode 100644 index 0000000..e10b509 --- /dev/null +++ b/order/src/main/kotlin/coded/spring/entity/OrderEntity.kt @@ -0,0 +1,15 @@ +package coded.spring.entity + +import jakarta.persistence.* + +@Entity +@Table(name = "orders") +data class OrderEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + + var restaurant: String, + + var userId: Long +) diff --git a/order/src/main/kotlin/coded/spring/repository/ItemRepository.kt b/order/src/main/kotlin/coded/spring/repository/ItemRepository.kt new file mode 100644 index 0000000..6a29197 --- /dev/null +++ b/order/src/main/kotlin/coded/spring/repository/ItemRepository.kt @@ -0,0 +1,10 @@ +package coded.spring.repository + +import coded.spring.entity.ItemEntity +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface ItemRepository : JpaRepository{ + +} \ No newline at end of file diff --git a/order/src/main/kotlin/coded/spring/repository/MenuRepository.kt b/order/src/main/kotlin/coded/spring/repository/MenuRepository.kt new file mode 100644 index 0000000..f0f4b04 --- /dev/null +++ b/order/src/main/kotlin/coded/spring/repository/MenuRepository.kt @@ -0,0 +1,10 @@ +package coded.spring.repository + +import coded.spring.entity.MenuEntity +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface MenuRepository : JpaRepository{ + +} diff --git a/order/src/main/kotlin/coded/spring/repository/OrderRepository.kt b/order/src/main/kotlin/coded/spring/repository/OrderRepository.kt new file mode 100644 index 0000000..4edb2c2 --- /dev/null +++ b/order/src/main/kotlin/coded/spring/repository/OrderRepository.kt @@ -0,0 +1,10 @@ +package coded.spring.repository + +import coded.spring.entity.OrderEntity +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface OrderRepository : JpaRepository{ + fun findAllByUserId(userId: Long): List +} \ No newline at end of file diff --git a/order/src/main/kotlin/coded/spring/security/RemoteAuthenticationFilter.kt b/order/src/main/kotlin/coded/spring/security/RemoteAuthenticationFilter.kt new file mode 100644 index 0000000..d2cdf59 --- /dev/null +++ b/order/src/main/kotlin/coded/spring/security/RemoteAuthenticationFilter.kt @@ -0,0 +1,36 @@ +package coded.spring.security + +import coded.spring.client.AuthenticationClient +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 kotlin.text.startsWith +import kotlin.text.substring + +@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/order/src/main/kotlin/coded/spring/security/SecurityConfig.kt b/order/src/main/kotlin/coded/spring/security/SecurityConfig.kt new file mode 100644 index 0000000..f5e1dac --- /dev/null +++ b/order/src/main/kotlin/coded/spring/security/SecurityConfig.kt @@ -0,0 +1,32 @@ +package coded.spring.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/order/src/main/kotlin/coded/spring/service/MenuService.kt b/order/src/main/kotlin/coded/spring/service/MenuService.kt new file mode 100644 index 0000000..1699f37 --- /dev/null +++ b/order/src/main/kotlin/coded/spring/service/MenuService.kt @@ -0,0 +1,29 @@ +package coded.spring.service +import coded.spring.controller.MenuRequest +import coded.spring.entity.MenuEntity +import coded.spring.repository.MenuRepository +import org.springframework.stereotype.Service +import kotlin.text.set + +@Service +class MenuService( + val menuRepository: MenuRepository, +) { + + fun addItems(menu: MenuRequest) { + + menuRepository.save( + MenuEntity( + name = menu.name, + price = menu.price + ) + ) + } + + fun listMenuItems(): List = menuRepository.findAll() + + + +} + + diff --git a/order/src/main/kotlin/coded/spring/service/OrderService.kt b/order/src/main/kotlin/coded/spring/service/OrderService.kt new file mode 100644 index 0000000..a79b9c0 --- /dev/null +++ b/order/src/main/kotlin/coded/spring/service/OrderService.kt @@ -0,0 +1,72 @@ +package coded.spring.service + +import coded.spring.entity.ItemEntity +import coded.spring.entity.OrderEntity +import coded.spring.repository.ItemRepository +import coded.spring.repository.OrderRepository +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import java.math.BigDecimal + +@Service +class OrderService( + private var orderRepository: OrderRepository, + private var itemRepository: ItemRepository, + @Value("\${discount.feature}") + val discount: Boolean +) { + + fun createOrder(request: OrderRequest) { + + val savedOrder =orderRepository.save(OrderEntity( + restaurant = request.restaurant, + userId = request.userId)) + + + + if (discount){ + itemRepository.saveAll(request.items.map { + ItemEntity( + name = it.name, + quantity = it.quantity, + price = it.price.multiply(BigDecimal(0.8)), + orderId = savedOrder.id!! + ) + }) + } + else { + itemRepository.saveAll(request.items.map { + ItemEntity( + name = it.name, + quantity = it.quantity, + price = it.price, + orderId = savedOrder.id!! + ) + }) + } + + } + + + fun listOrdersByUserID(userId: Long) { + orderRepository.findAllByUserId(userId) + } + + + data class ItemDto( + val name: String, + val quantity: Int, + val price: BigDecimal + ) + + data class OrderRequest( + val userId: Long, + val restaurant: String, + val items: MutableList + ) + + data class OrderResponse( + val restaurant: String, + val items: List + ) +} \ No newline at end of file diff --git a/order/src/main/resources/application.properties b/order/src/main/resources/application.properties new file mode 100644 index 0000000..36873dc --- /dev/null +++ b/order/src/main/resources/application.properties @@ -0,0 +1,12 @@ +spring.application.name=Kotlin.SpringbootV2 + +server.port = 4444 + +spring.datasource.url=jdbc:postgresql://localhost:5432/OrderingDB +spring.datasource.username=postgres +spring.datasource.password=alix +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.show-sql=true +spring.jpa.properties.hibe1rnate.format_sql=true + +springdoc.api-docs.path=/api-docs \ No newline at end of file diff --git a/pom.xml b/pom.xml index 163ad53..572ee68 100644 --- a/pom.xml +++ b/pom.xml @@ -9,8 +9,9 @@ com.coded.spring - Ordering + monolith 0.0.1-SNAPSHOT + pom Kotlin.SpringbootV2 Kotlin.SpringbootV2 @@ -20,6 +21,10 @@ + + authentication + order + @@ -58,6 +63,59 @@ kotlin-test-junit5 test + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + runtime + 0.11.5 + + + io.jsonwebtoken + jjwt-jackson + runtime + 0.11.5 + + + org.springframework.boot + spring-boot-starter-security + + + jakarta.inject + jakarta.inject-api + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.h2database + h2 + test + + + org.postgresql + postgresql + compile + + + org.springdoc + springdoc-openapi-starter-webmvc-api + 2.6.0 + + + + com.hazelcast + hazelcast + 5.3.8 + + + + @@ -76,17 +134,34 @@ -Xjsr305=strict + jpa spring + all-open + no-arg + + + + + + + + + + org.jetbrains.kotlin + kotlin-maven-noarg + 1.9.25 + org.jetbrains.kotlin kotlin-maven-allopen - ${kotlin.version} + 1.9.25 + diff --git a/src/main/kotlin/com/legacy/ordering/OrdersController.kt b/src/main/kotlin/com/legacy/ordering/OrdersController.kt new file mode 100644 index 0000000..54761f8 --- /dev/null +++ b/src/main/kotlin/com/legacy/ordering/OrdersController.kt @@ -0,0 +1,33 @@ +package com.coded.spring.ordering + +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController + +@RestController +class OrdersController(val ordersRepository: OrderRepository) { + + // Exercise 1 + @GetMapping("/welcome") + fun chooseDeliveryOrPickUp() = "Delivery or Pick-Up?" + + // Exercise 2 + @PostMapping("/order") + fun makeAnOrder(@RequestBody request: OrderRequest): Order { + val newOrder = Order( + user = request.user, + restaurant = request.restaurant, + items = request.items + ) + return ordersRepository.save(newOrder) + } + + @GetMapping("/order") + fun getAllOrders() = ordersRepository.findAll() +} +data class OrderRequest( + val user: String, + val restaurant: String, + val items: MutableList +) diff --git a/src/main/kotlin/com/legacy/ordering/OrdersRepository.kt b/src/main/kotlin/com/legacy/ordering/OrdersRepository.kt new file mode 100644 index 0000000..6db68ee --- /dev/null +++ b/src/main/kotlin/com/legacy/ordering/OrdersRepository.kt @@ -0,0 +1,31 @@ +package com.coded.spring.ordering + +import jakarta.inject.Named +import jakarta.persistence.* +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Named +interface OrderRepository : JpaRepository + +@Entity +@Table(name = "orders") +data class Order( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name="id") + var orderID: Long? = null, + // Thanks to Mohammed Sheshter he figured out we need to use back ticks to resolve the 500 server error caused by JPA getting confused by "users" +// @Column(name = "`user`") + @Column(name = "name") + var user: String = "", + + var restaurant: String = "", + + // Since the specified schema requires a list of items I had to make a seperate table + @CollectionTable(name = "items") + @JoinColumn() + var items: MutableList = mutableListOf() +){ + constructor() : this(null,"","", mutableListOf()) +} \ No newline at end of file diff --git a/src/main/kotlin/com/legacy/ordering/UsersController.kt b/src/main/kotlin/com/legacy/ordering/UsersController.kt new file mode 100644 index 0000000..7840ff3 --- /dev/null +++ b/src/main/kotlin/com/legacy/ordering/UsersController.kt @@ -0,0 +1,14 @@ +package com.coded.spring.ordering + + +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +class UsersController( + private val usersRepository: UsersRepository +){ + + @GetMapping("/users/v1/list") + fun users() = usersRepository.findAll() +} \ No newline at end of file diff --git a/src/main/kotlin/com/legacy/ordering/UsersRepository.kt b/src/main/kotlin/com/legacy/ordering/UsersRepository.kt new file mode 100644 index 0000000..16be2ee --- /dev/null +++ b/src/main/kotlin/com/legacy/ordering/UsersRepository.kt @@ -0,0 +1,25 @@ +package com.coded.spring.ordering + +import jakarta.inject.Named +import jakarta.persistence.* +import org.springframework.data.jpa.repository.JpaRepository + +@Named +interface UsersRepository : JpaRepository + +@Entity +@Table(name = "users") +data class User( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name="id") + var orderID: Long? = null, + + @Column(name = "name") + var user: String = "", + @Column(name = "age") + var age: Int? = null + +){ + constructor() : this(null, "", null) +} \ No newline at end of file diff --git a/src/main/resources/Ali-Aljadi-online-ordering-api-swagger-01.json b/src/main/resources/Ali-Aljadi-online-ordering-api-swagger-01.json new file mode 100644 index 0000000..556dc60 --- /dev/null +++ b/src/main/resources/Ali-Aljadi-online-ordering-api-swagger-01.json @@ -0,0 +1 @@ +{"openapi":"3.0.1","info":{"title":"OpenAPI definition","version":"v0"},"servers":[{"url":"http://localhost:4444","description":"Generated server url"}],"paths":{"/public/users/create":{"post":{"tags":["CustomerAPI"],"operationId":"newUser","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/orders/v1/submit":{"post":{"tags":["OrderAPI"],"operationId":"submitOrder","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OrderResponse"}}}}}}},"/menu/add":{"post":{"tags":["MenuAPI"],"operationId":"addItemsToMenu","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MenuRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/auth/profile/save":{"post":{"tags":["CustomerAPI"],"operationId":"saveProfile","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/auth/login":{"post":{"tags":["authentication-controller"],"operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthenticationRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthenticationResponse"}}}}}}},"/users/v1/list":{"get":{"tags":["CustomerAPI"],"operationId":"listUsers","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserEntity"}}}}}}}},"/public/menu/list":{"get":{"tags":["MenuAPI"],"operationId":"listItems","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MenuEntity"}}}}}}}},"/orders/user/orders/{userId}":{"get":{"tags":["OrderAPI"],"operationId":"listOrdersByUserId","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"OK"}}}},"/hello":{"get":{"tags":["CustomerAPI"],"operationId":"hello","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"string"}}}}}}},"/auth/profile/view/":{"get":{"tags":["CustomerAPI"],"operationId":"viewProfile","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}}},"components":{"schemas":{"UserRequest":{"required":["email","name","password","username"],"type":"object","properties":{"name":{"type":"string"},"email":{"type":"string"},"username":{"type":"string"},"password":{"type":"string"}}},"ItemDto":{"required":["name","price","quantity"],"type":"object","properties":{"name":{"type":"string"},"quantity":{"type":"integer","format":"int32"},"price":{"type":"number"}}},"OrderRequest":{"required":["items","restaurant"],"type":"object","properties":{"restaurant":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/components/schemas/ItemDto"}}}},"OrderResponse":{"required":["items","restaurant"],"type":"object","properties":{"restaurant":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/components/schemas/ItemDto"}}}},"MenuRequest":{"required":["name","price"],"type":"object","properties":{"name":{"type":"string"},"price":{"type":"number"}}},"ProfileRequest":{"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"}}},"UserEntity":{"required":["email","name","password","username"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"email":{"type":"string"},"username":{"type":"string"},"password":{"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/src/main/resources/application.properties b/src/main/resources/application.properties index 3704dc6..36873dc 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,12 @@ spring.application.name=Kotlin.SpringbootV2 + +server.port = 4444 + +spring.datasource.url=jdbc:postgresql://localhost:5432/OrderingDB +spring.datasource.username=postgres +spring.datasource.password=alix +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.show-sql=true +spring.jpa.properties.hibe1rnate.format_sql=true + +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..eccad6b 100644 --- a/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt +++ b/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt @@ -1,13 +1,70 @@ package com.coded.spring.ordering +import com.coded.spring.authentication.jwt.JwtService +import com.coded.spring.entity.UserEntity +import com.coded.spring.repository.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 kotlin.test.assertEquals -@SpringBootTest +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") class ApplicationTests { +// MOCK TRIGGER ASSERT +// GIVEN WHEN THEN + companion object { + @JvmStatic +// @BeforeAll + fun setUp( + @Autowired userRepository: UserRepository, + @Autowired passwordEncoder: PasswordEncoder + ) { - @Test - fun contextLoads() { - } + userRepository.deleteAll() + val testUser = + UserEntity( + name = "coded", + email = "test@test.com", + username = "coded", + password = passwordEncoder.encode("Password123") + ) + userRepository.save(testUser) + } + } + + // Tests go here + @Autowired + lateinit var restTemplate: TestRestTemplate + + @Test + fun testHelloWorld(@Autowired jwtService: JwtService) { + val token = jwtService.generateToken("coded") + val headers = HttpHeaders( + MultiValueMap.fromSingleValue(mapOf("Authorization" to "Bearer $token")) + ) + val requestEntity = HttpEntity(headers) + + val result = restTemplate.exchange( + "/hello", + HttpMethod.GET, + requestEntity, + String::class.java + ) + assertEquals(HttpStatus.OK, result.statusCode) + assertEquals("Hello World!", result.body) + } } + + +