diff --git a/authentication/pom.xml b/authentication/pom.xml new file mode 100644 index 0000000..5ffcb72 --- /dev/null +++ b/authentication/pom.xml @@ -0,0 +1,15 @@ + + + 4.0.0 + + + com.coded.spring + Ordering + 0.0.1-SNAPSHOT + + + authentication + + \ No newline at end of file diff --git a/authentication/src/main/kotlin/authentication/CustomerDetailsService.kt b/authentication/src/main/kotlin/authentication/CustomerDetailsService.kt new file mode 100644 index 0000000..62df910 --- /dev/null +++ b/authentication/src/main/kotlin/authentication/CustomerDetailsService.kt @@ -0,0 +1,26 @@ +package com.coded.spring.authentication + +import com.coded.spring.users.UserEntity +import com.coded.spring.users.UsersRepository +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 CustomerDetailsService( + private val userRepo : UsersRepository +): UserDetailsService { + override fun loadUserByUsername(username: String): UserDetails { + val user: UserEntity = userRepo.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/authentication/JWT/AuthenticationController.kt b/authentication/src/main/kotlin/authentication/JWT/AuthenticationController.kt new file mode 100644 index 0000000..6d68589 --- /dev/null +++ b/authentication/src/main/kotlin/authentication/JWT/AuthenticationController.kt @@ -0,0 +1,54 @@ +package com.coded.spring.authentication.JWT + +import com.coded.spring.users.UsersService +import org.springframework.security.authentication.* +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.core.userdetails.UsernameNotFoundException +import org.springframework.web.bind.annotation.* +import java.security.Principal + + +@RestController +@RequestMapping("/auth") +class AuthenticationController( + private val authenticationManager: AuthenticationManager, + private val userDetailsService: UserDetailsService, + private val jwtService: JwtService, + private val usersService: UsersService +) { + + @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 = usersService.findByUsername(principal.name) + ) + } +} + +data class AuthenticationRequest( + val username: String, + val password: String +) + +data class AuthenticationResponse( + val token: String +) + +data class CheckTokenResponse( + val userId: Long +) \ No newline at end of file diff --git a/authentication/src/main/kotlin/authentication/JWT/JwtAuthenticationFilter.kt b/authentication/src/main/kotlin/authentication/JWT/JwtAuthenticationFilter.kt new file mode 100644 index 0000000..131259d --- /dev/null +++ b/authentication/src/main/kotlin/authentication/JWT/JwtAuthenticationFilter.kt @@ -0,0 +1,49 @@ +package com.coded.spring.authentication.JWT + +import jakarta.inject.Named +import jakarta.servlet.FilterChain +import jakarta.servlet.http.* +import org.springframework.context.annotation.Bean +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/authentication/JWT/JwtService.kt b/authentication/src/main/kotlin/authentication/JWT/JwtService.kt new file mode 100644 index 0000000..69f76fb --- /dev/null +++ b/authentication/src/main/kotlin/authentication/JWT/JwtService.kt @@ -0,0 +1,47 @@ +package com.coded.spring.authentication.JWT + + +import io.jsonwebtoken.* +import io.jsonwebtoken.security.Keys +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import java.util.* +import javax.crypto.SecretKey + +@Component +class JwtService ( + @Value("\${jwt-secret}") + private val secretKeyString: String +){ + private val secretKey: SecretKey = Keys.hmacShaKeyFor(secretKeyString.encodeToByteArray()) +// 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/authentication/Loggerfilter.kt b/authentication/src/main/kotlin/authentication/Loggerfilter.kt new file mode 100644 index 0000000..e048104 --- /dev/null +++ b/authentication/src/main/kotlin/authentication/Loggerfilter.kt @@ -0,0 +1,38 @@ +package com.coded.spring.authentication + +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter +import org.springframework.web.util.ContentCachingRequestWrapper +import org.springframework.web.util.ContentCachingResponseWrapper + + +@Component +class LoggingFilter: OncePerRequestFilter(){ + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + val cachedRequest = ContentCachingRequestWrapper(request) + val cachedResponse = ContentCachingResponseWrapper(response) + + filterChain.doFilter(cachedRequest, cachedResponse) + + logRequest(cachedRequest) + logResponse(cachedResponse) + cachedResponse.copyBodyToResponse() + } + + private fun logRequest(request: ContentCachingRequestWrapper) { + val requestBody = String(request.contentAsByteArray) + logger.info("Request: method=${request.method}, uri=${request.requestURI}, body=$requestBody") + } + + private fun logResponse(response: ContentCachingResponseWrapper) { + val responseBody = String(response.contentAsByteArray) + logger.info("Response: status=${response.status}, body=$responseBody") + } +} \ No newline at end of file diff --git a/authentication/src/main/kotlin/authentication/Profiles/ProfileController.kt b/authentication/src/main/kotlin/authentication/Profiles/ProfileController.kt new file mode 100644 index 0000000..ff0819d --- /dev/null +++ b/authentication/src/main/kotlin/authentication/Profiles/ProfileController.kt @@ -0,0 +1,20 @@ +package com.coded.spring.Profiles + +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 ProfileController( + val profilesRepository: ProfilesRepository, + val profilesService: ProfilesService +){ + @PostMapping("/auth/users/v1/profiles") + fun createProfiles(@RequestBody request: ProfileRequest): ProfileResponse{ + return profilesService.createProfile(request) + } + + @GetMapping("/auth/users/v1/profiles/list") + fun showProfiles() = profilesRepository.findAll() +} \ No newline at end of file diff --git a/authentication/src/main/kotlin/authentication/Profiles/ProfileEntity.kt b/authentication/src/main/kotlin/authentication/Profiles/ProfileEntity.kt new file mode 100644 index 0000000..fd6240c --- /dev/null +++ b/authentication/src/main/kotlin/authentication/Profiles/ProfileEntity.kt @@ -0,0 +1,18 @@ +package com.coded.spring.Profiles + +import jakarta.persistence.* + + +@Entity +@Table(name="profiles") +data class ProfileEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, + val userID: Long, + val firstName: String, + val lastName: String, + val phoneNumber: Long +){ + constructor() : this(null,0, "","",0) +} \ No newline at end of file diff --git a/authentication/src/main/kotlin/authentication/Profiles/ProfilesRepository.kt b/authentication/src/main/kotlin/authentication/Profiles/ProfilesRepository.kt new file mode 100644 index 0000000..fdd9c47 --- /dev/null +++ b/authentication/src/main/kotlin/authentication/Profiles/ProfilesRepository.kt @@ -0,0 +1,7 @@ +package com.coded.spring.Profiles + +import jakarta.inject.Named +import org.springframework.data.jpa.repository.JpaRepository + +@Named +interface ProfilesRepository : JpaRepository \ No newline at end of file diff --git a/authentication/src/main/kotlin/authentication/Profiles/ProfilesService.kt b/authentication/src/main/kotlin/authentication/Profiles/ProfilesService.kt new file mode 100644 index 0000000..9d28883 --- /dev/null +++ b/authentication/src/main/kotlin/authentication/Profiles/ProfilesService.kt @@ -0,0 +1,39 @@ +package com.coded.spring.Profiles + +import com.coded.spring.users.UsersRepository +import jakarta.inject.Named +import org.springframework.security.core.context.SecurityContext +import org.springframework.security.core.context.SecurityContextHolder + +@Named +class ProfilesService ( + private val profilesRepository: ProfilesRepository, + private val usersRepository: UsersRepository +){ + fun createProfile(request: ProfileRequest): ProfileResponse{ + val userId = usersRepository.findByUsername(SecurityContextHolder.getContext().authentication.name)?.id ?: + throw IllegalArgumentException() + val newProfile = ProfileEntity( + userID = userId, + firstName = request.firstName, + lastName = request.lastName, + phoneNumber = request.phoneNumber + ) + val savedProfile = profilesRepository.save(newProfile) + return ProfileResponse(savedProfile.firstName,savedProfile.lastName,savedProfile.phoneNumber) + } +} + +data class ProfileRequest( + val firstName: String, + val lastName: String, + val phoneNumber: Long +) + +data class ProfileResponse( + val firstName: String, + val lastName: String, + val phoneNumber: Long + +) + diff --git a/authentication/src/main/kotlin/authentication/SecurityConfig.kt b/authentication/src/main/kotlin/authentication/SecurityConfig.kt new file mode 100644 index 0000000..a688500 --- /dev/null +++ b/authentication/src/main/kotlin/authentication/SecurityConfig.kt @@ -0,0 +1,70 @@ +package com.coded.spring.authentication + +import com.coded.spring.authentication.JWT.JwtAuthenticationFilter +import jdk.jfr.Enabled +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.UserDetails +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 SecurityConf ( + private val userDetailsService: UserDetailsService, + private val jwtAuthFilter: JwtAuthenticationFilter, + + ){ + + @Bean + fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder() + + @Bean + fun securityFilterChain(http: HttpSecurity): SecurityFilterChain{ + http.csrf{ it.disable()}.authorizeHttpRequests { +// it.requestMatchers("/users/v1/list").permitAll() +// .anyRequest().authenticated() +// it.requestMatchers("/users/v1/create/menu").permitAll() +// .anyRequest().permitAll() +// it.requestMatchers("/Public/**").permitAll().requestMatchers("/users/v1/**") +// .authenticated() +// } +// .formLogin { +// it.defaultSuccessUrl("/Public/menu", true)} +// .userDetailsService(userDetailsService) +// return http.build() + it.requestMatchers("/auth/**", "/users/v1/**","/api-docs","/welcome","/Public/**").permitAll() + .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 + } +} \ No newline at end of file diff --git a/authentication/src/main/kotlin/authentication/users/UserService.kt b/authentication/src/main/kotlin/authentication/users/UserService.kt new file mode 100644 index 0000000..32863c2 --- /dev/null +++ b/authentication/src/main/kotlin/authentication/users/UserService.kt @@ -0,0 +1,65 @@ +package com.coded.spring.users + +import jakarta.inject.Named +import org.springframework.security.crypto.password.PasswordEncoder + +@Named +class UsersService( + private val usersRepository: UsersRepository, + private val encoder: PasswordEncoder, +) { + + fun createUser(request: AddUserRequest): addedUserResponse{ + val myNewUserEntity = UserEntity( + name = request.name, + age = request.age, + username = request.username, + password = encoder.encode(request.password), + ) + if (request.username.isBlank()){ + throw IllegalArgumentException("username not correct")} + + val savedUser= usersRepository.save(myNewUserEntity) + + return addedUserResponse(savedUser.id,savedUser.username) + } + + fun listUsers(): List = usersRepository.findAll().map { + User( + name = it.name, + age = it.age + ) + } + + fun findByUsername(name: String): Long { + return usersRepository.findByUsername(name)?.id ?: + throw IllegalArgumentException("username not found") + } + +} + + +fun isValidPassword(password: String): Boolean { + + return password.length >= 6 && password.any { it.isDigit() } && password.any { it.isUpperCase() } +} + + +data class AddUserRequest( + val name: String, + val username: String, + val password: String, + val age: Int +) + + + +data class addedUserResponse( + val id: Long?, + val username: String +) + +data class User( + val name: String, + val age: Int +) \ No newline at end of file diff --git a/authentication/src/main/kotlin/authentication/users/UsersController.kt b/authentication/src/main/kotlin/authentication/users/UsersController.kt new file mode 100644 index 0000000..5de4230 --- /dev/null +++ b/authentication/src/main/kotlin/authentication/users/UsersController.kt @@ -0,0 +1,39 @@ +package com.coded.spring.users + +import org.springframework.beans.factory.annotation.Value +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.RestController + +@RestController +class UsersController( + val usersRepository: UsersRepository, + val usersService: UsersService, + @Value("\${server-welcome-message}") + val companyMessage: String, +){ + + @GetMapping("/welcome") + fun welcome() = "Welcome to Online Ordering by $companyMessage" + + + @GetMapping("/users/v1/list") + fun sayUsers() = usersRepository.findAll() + + + @PostMapping("/users/v1/create") + fun createUser(@RequestBody request: AddUserRequest):Any{ + return try { + usersService.createUser(request) + ResponseEntity.ok("user is good to go") + } catch (e: IllegalArgumentException){ + ResponseEntity.badRequest().body(e.message) + } + + + } + +} + diff --git a/authentication/src/main/kotlin/authentication/users/UsersRepository.kt b/authentication/src/main/kotlin/authentication/users/UsersRepository.kt new file mode 100644 index 0000000..12c0120 --- /dev/null +++ b/authentication/src/main/kotlin/authentication/users/UsersRepository.kt @@ -0,0 +1,27 @@ +package com.coded.spring.users + +import jakarta.inject.Named +import jakarta.persistence.* +import org.springframework.data.jpa.repository.JpaRepository + +@Named +interface UsersRepository : JpaRepository { + fun age(age: Int): MutableList + + fun findByUsername(username: String): UserEntity? +} + +@Entity +@Table(name = "users") +data class UserEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + var name: String, + var age: Int, + + val username: String, + val password: String +){ + constructor() : this(null, "", 0,"","") +} diff --git a/ordering/pom.xml b/ordering/pom.xml new file mode 100644 index 0000000..bb6a78e --- /dev/null +++ b/ordering/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + + com.coded.spring + Ordering + 0.0.1-SNAPSHOT + + + ordering + + + + + \ No newline at end of file diff --git a/ordering/src/main/kotlin/client/AuthenticationClient.kt b/ordering/src/main/kotlin/client/AuthenticationClient.kt new file mode 100644 index 0000000..e1dab33 --- /dev/null +++ b/ordering/src/main/kotlin/client/AuthenticationClient.kt @@ -0,0 +1,35 @@ +package com.coded.spring.client + +import jakarta.inject.Named +import org.springframework.core.ParameterizedTypeReference +import org.springframework.http.HttpEntity +import org.springframework.http.HttpMethod +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/authentication/v1/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 ...") + } + +} + + +data class CheckTokenResponse( + val userId: Long +) \ No newline at end of file diff --git a/ordering/src/main/kotlin/ordering/MenuController.kt b/ordering/src/main/kotlin/ordering/MenuController.kt new file mode 100644 index 0000000..f74b8f6 --- /dev/null +++ b/ordering/src/main/kotlin/ordering/MenuController.kt @@ -0,0 +1,26 @@ +package com.coded.spring.ordering + +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 + +@RestController +class MenuController( + val menuRepository: MenuRepository, + val menuService: MenuService +){ + + @GetMapping("/Public/menu") + fun sayUsers(@RequestParam(required = false) search: String?) = menuService.listMenu(search) + + + @PostMapping("Public/users/v1/create/menu") + fun menuAdd(@RequestBody requestMenu: RequestMenu): Any { +// val newMenu = menuService.addMenu(request) + return ResponseEntity.ok().body(menuService.addMenu(requestMenu)) + } + +} \ No newline at end of file diff --git a/ordering/src/main/kotlin/ordering/MenuEntity.kt b/ordering/src/main/kotlin/ordering/MenuEntity.kt new file mode 100644 index 0000000..ee0b055 --- /dev/null +++ b/ordering/src/main/kotlin/ordering/MenuEntity.kt @@ -0,0 +1,18 @@ +package com.coded.spring.ordering + +import jakarta.persistence.* +import java.math.BigDecimal + +@Entity +@Table(name="menu") +data class MenuEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, +// @Column(name = "name") +// val myFieldName: String, + val name: String, + val price: BigDecimal +){ + constructor() : this(null,"", BigDecimal.ZERO) +} \ No newline at end of file diff --git a/ordering/src/main/kotlin/ordering/MenuRepository.kt b/ordering/src/main/kotlin/ordering/MenuRepository.kt new file mode 100644 index 0000000..d07cacd --- /dev/null +++ b/ordering/src/main/kotlin/ordering/MenuRepository.kt @@ -0,0 +1,7 @@ +package com.coded.spring.ordering + +import jakarta.inject.Named +import org.springframework.data.jpa.repository.JpaRepository + +@Named +interface MenuRepository : JpaRepository \ No newline at end of file diff --git a/ordering/src/main/kotlin/ordering/MenuService.kt b/ordering/src/main/kotlin/ordering/MenuService.kt new file mode 100644 index 0000000..f103cb4 --- /dev/null +++ b/ordering/src/main/kotlin/ordering/MenuService.kt @@ -0,0 +1,56 @@ +package com.coded.spring.ordering + +import com.coded.spring.serverCache +import org.springframework.stereotype.Service +import java.math.BigDecimal + +@Service +class MenuService( + val menuRepository: MenuRepository +) { + fun addMenu(request: RequestMenu): MenuEntity { + val newMenu = MenuEntity( + name = request.menuName, + price = request.price + ) + return menuRepository.save(newMenu) + } + + fun listMenu(search: String?): List { + val cachedMenu = menuCache["Menu"] + + val menu = if (cachedMenu.isNullOrEmpty()) { + println("no new meals") + val freshMenu = menuRepository.findAll().map { + Menu( + menuName = it.name, + price = it.price + ) + } + menuCache.put("Menu", freshMenu) + freshMenu + } else { + cachedMenu + } + + return if (!search.isNullOrBlank()) { + menu.filter { it.menuName.trim().lowercase().contains(search.trim().lowercase()) } + } else { + menu + } + } +} + + +val menuCache = serverCache.getMap>("Menu") + + +data class Menu( + val menuName: String, + val price: BigDecimal +) + +data class RequestMenu( + val menuName: String, + val price: BigDecimal +) \ No newline at end of file diff --git a/ordering/src/main/kotlin/ordering/OrdersController.kt b/ordering/src/main/kotlin/ordering/OrdersController.kt new file mode 100644 index 0000000..fad6042 --- /dev/null +++ b/ordering/src/main/kotlin/ordering/OrdersController.kt @@ -0,0 +1,46 @@ +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 +import java.math.BigDecimal + +@RestController +class ordersController( + val ordersService: OrdersService, + val ordersRepository: OrdersRepository, + val itemsRepository: ItemsRepository +){ + + @GetMapping("Public/orders/v1/list") + fun showUsers() = ordersRepository.findAll() + + @PostMapping("Public/orders") + fun sayOrder( @RequestBody request: OrderRequest) : OrderResponse { + val result = ordersService.createOrder(request.userId, request.items) + // return OrderResponse(result.userId, result.items) both ways r correct + return result.let{ + OrderResponse( + userId = it.userId, + items = it.items + ) + } + } +} + + +data class OrderRequest( + val userId: Long, + val items: List +) +data class OrderResponse( + val userId: Long, + val items: List +) + +data class Item( + val name: String, + val quantity : Int, + val price: BigDecimal +) diff --git a/ordering/src/main/kotlin/ordering/OrdersRepository.kt b/ordering/src/main/kotlin/ordering/OrdersRepository.kt new file mode 100644 index 0000000..3edf87b --- /dev/null +++ b/ordering/src/main/kotlin/ordering/OrdersRepository.kt @@ -0,0 +1,27 @@ +package com.coded.spring.ordering + +import com.coded.spring.users.UserEntity +import jakarta.inject.Named +import jakarta.persistence.* +import org.springframework.data.jpa.repository.JpaRepository + +@Named +interface OrdersRepository : JpaRepository { + fun findByUserId(userId: Long): List +} + +@Entity +@Table(name = "orders") +data class OrdersEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + + @ManyToOne + val user: UserEntity, + + @OneToMany(mappedBy = "orderId") + val items: List? = null +){ + constructor() : this(null, UserEntity(), ) +} \ No newline at end of file diff --git a/ordering/src/main/kotlin/ordering/OrdersService.kt b/ordering/src/main/kotlin/ordering/OrdersService.kt new file mode 100644 index 0000000..b8d2d25 --- /dev/null +++ b/ordering/src/main/kotlin/ordering/OrdersService.kt @@ -0,0 +1,69 @@ +package com.coded.spring.ordering + +import com.coded.spring.users.UsersRepository +import jakarta.inject.Named +import org.springframework.beans.factory.annotation.Value +import java.math.BigDecimal + + +@Named +class OrdersService( + val usersRepo : UsersRepository, + val ordersRepo : OrdersRepository, + val itemsRepository: ItemsRepository, + @Value("\${discount.feature}") + val discount: Boolean +) { + fun createOrder(userId: Long, items: List): Order { + val user = usersRepo.findById(userId).get() + val newOrder = OrdersEntity(user = user) + val savedOrder = ordersRepo.save(newOrder) + + val newItems = items.map{ + ItemEntity( + orderId = savedOrder.id!!, + name = it.name , + price = if (discount)it.price.multiply(BigDecimal(0.8)) else it.price, + quantity = it.quantity + ) + } + val savedItems = itemsRepository.saveAll(newItems) + + return Order( + userId = savedOrder.id!!, + items = savedItems.map { + Item( + name = it.name, + price = it.price, + quantity = it.quantity + ) + } + ) + } +// if (discount){ +// itemsRepository.saveAll(items.map { +// ItemEntity( +// orderId = savedOrder.id!!, +// name = it.name, +// price = it.price.multiply(BigDecimal(0.8)), +// quantity = it.quantity +// +// ) +// }) +// }else{ +// itemsRepository.saveAll(items.map { +// ItemEntity( +// orderId = savedOrder.id!!, +// name = it.name, +// price = it.price, +// quantity = it.quantity +// +// ) +// }) +// +// } +} +data class Order( + val userId: Long, + val items: List +) \ No newline at end of file diff --git a/ordering/src/main/kotlin/ordering/itemsRepository.kt b/ordering/src/main/kotlin/ordering/itemsRepository.kt new file mode 100644 index 0000000..ee5d6fc --- /dev/null +++ b/ordering/src/main/kotlin/ordering/itemsRepository.kt @@ -0,0 +1,22 @@ +package com.coded.spring.ordering + +import jakarta.persistence.* +import org.springframework.data.jpa.repository.JpaRepository +import java.math.BigDecimal + +interface ItemsRepository : JpaRepository + +@Entity +@Table(name="items") +data class ItemEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id : Long? = null, + val orderId : Long, + val name : String, + val price : BigDecimal, + @Column(name = "quentity") + val quantity : Int +){ + constructor() : this(null, 0, "", BigDecimal.ZERO,0) +} \ No newline at end of file diff --git a/ordering/src/main/kotlin/security/RemoteAuthenticationfilter.kt b/ordering/src/main/kotlin/security/RemoteAuthenticationfilter.kt new file mode 100644 index 0000000..25a6e96 --- /dev/null +++ b/ordering/src/main/kotlin/security/RemoteAuthenticationfilter.kt @@ -0,0 +1,36 @@ +package com.coded.spring.security + +import com.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/ordering/src/main/kotlin/security/securityconfig.kt b/ordering/src/main/kotlin/security/securityconfig.kt new file mode 100644 index 0000000..70390c0 --- /dev/null +++ b/ordering/src/main/kotlin/security/securityconfig.kt @@ -0,0 +1,31 @@ +package com.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/pom.xml b/pom.xml index 163ad53..087efac 100644 --- a/pom.xml +++ b/pom.xml @@ -11,6 +11,7 @@ com.coded.spring Ordering 0.0.1-SNAPSHOT + pom Kotlin.SpringbootV2 Kotlin.SpringbootV2 @@ -20,6 +21,10 @@ + + authentication + ordering + @@ -43,6 +48,29 @@ org.jetbrains.kotlin kotlin-reflect + + org.postgresql + postgresql + runtime + + + com.hazelcast + hazelcast + 5.3.8 + + + org.springdoc + springdoc-openapi-starter-webmvc-api + 2.6.0 + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + org.jetbrains.kotlin kotlin-stdlib @@ -58,6 +86,40 @@ kotlin-test-junit5 test + + jakarta.inject + jakarta.inject-api + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + runtime + 0.11.5 + + + io.jsonwebtoken + jjwt-jackson + runtime + 0.11.5 + + + org.jetbrains.kotlin + kotlin-test-junit5 + test + diff --git a/src/main/kotlin/com/coded/spring/Application.kt b/src/main/kotlin/com/coded/spring/Application.kt new file mode 100644 index 0000000..e1024ee --- /dev/null +++ b/src/main/kotlin/com/coded/spring/Application.kt @@ -0,0 +1,18 @@ +package com.coded.spring + +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 Application + +fun main(args: Array) { + runApplication(*args) + applicationConfig.getMapConfig("Menu").setTimeToLiveSeconds(120) +} + +val applicationConfig = Config("application-cache") +val serverCache: HazelcastInstance = Hazelcast.newHazelcastInstance(applicationConfig) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/Loggingfilter.kt b/src/main/kotlin/com/coded/spring/Loggingfilter.kt new file mode 100644 index 0000000..c9c5f41 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/Loggingfilter.kt @@ -0,0 +1,34 @@ +package com.coded.spring + +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.core.annotation.Order +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter +import org.springframework.web.util.ContentCachingRequestWrapper +import org.springframework.web.util.ContentCachingResponseWrapper + +@Component +@Order(1) // to select this filter order in referral to other filters +class LoggingFilter : OncePerRequestFilter() { + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + val cachedRequest = ContentCachingRequestWrapper(request) + val cashedResponse = ContentCachingResponseWrapper(response) + filterChain.doFilter(cachedRequest,cashedResponse) + cashedResponse.copyBodyToResponse() + + val requestBody = String(cachedRequest.contentAsByteArray) + logger.info("Request: method=${request.method}, url=${request.requestURI}, body ${requestBody}") + + val responseBody = String(cashedResponse.contentAsByteArray) + logger.info("Request: status=${response.status}, body=$responseBody") + + cashedResponse.copyBodyToResponse() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/Application.kt b/src/main/kotlin/com/coded/spring/ordering/Application.kt deleted file mode 100644 index 8554e49..0000000 --- a/src/main/kotlin/com/coded/spring/ordering/Application.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.coded.spring.ordering - -import org.springframework.boot.autoconfigure.SpringBootApplication -import org.springframework.boot.runApplication - -@SpringBootApplication -class Application - -fun main(args: Array) { - runApplication(*args) -} diff --git a/src/main/kotlin/com/coded/spring/pets/PetsClient.kt b/src/main/kotlin/com/coded/spring/pets/PetsClient.kt new file mode 100644 index 0000000..bd8d433 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/pets/PetsClient.kt @@ -0,0 +1,32 @@ +package com.coded.spring.pets + +import jakarta.inject.Named +import org.springframework.core.ParameterizedTypeReference +import org.springframework.http.HttpMethod +import org.springframework.web.client.RestTemplate +import org.springframework.web.client.exchange + +@Named +class PetsClient { + fun getPets(): List{ + val restTemplate = RestTemplate() + val url = "https://pets-react-query-backend.eapi.joincoded.com/pets" + + val response = restTemplate.exchange>( + url = url, + method = HttpMethod.GET, + requestEntity = null, + object : ParameterizedTypeReference?>() { + } + ) + return response.body ?: listOf() + } +} + +data class PetDTO( + val id: Long, + val name: String, + val type: String, + val adopted: String, + val image: String +) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/pets/PetsController.kt b/src/main/kotlin/com/coded/spring/pets/PetsController.kt new file mode 100644 index 0000000..fbff989 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/pets/PetsController.kt @@ -0,0 +1,47 @@ +package com.coded.spring.pets + +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController + +@Tag(name="PetsAPI") +@RestController +class PetsController( + private val petsService: PetsService +) { + @ApiResponses( + ApiResponse(responseCode = "200", description = "List of pets available in the store", + content = [Content(mediaType = "application/json")]), + ApiResponse(responseCode = "400", description = "An error occured while listing pets...", + content = [Content(mediaType = "application/json")]) + ) + + @GetMapping("/pets/v1/pets") + fun listPets() : ResponseEntity<*> { + return try{ + ResponseEntity.ok( + ListPetsSuccessfulResponse( + pets = petsService.ListPets() + ) + ) + } catch (e: IllegalStateException){ + ResponseEntity.badRequest().body( + ListPetsFailureResponse( + error = "Sorry, pets are sleeping" + ) + ) + } + } +} + +data class ListPetsSuccessfulResponse( + val pets: List +) + +data class ListPetsFailureResponse( + val error: String +) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/pets/PetsService.kt b/src/main/kotlin/com/coded/spring/pets/PetsService.kt new file mode 100644 index 0000000..35478a6 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/pets/PetsService.kt @@ -0,0 +1,26 @@ +package com.coded.spring.pets + +import jakarta.inject.Named +import com.hazelcast.logging.Logger + + + +@Named +class PetsService( + private val petsClient: PetsClient +){ + fun ListPets() : List = petsClient.getPets().map { + Pet( + name = it.name, + type = it.type + ) + } +} + + + +data class Pet( + val name: String, + val type: String +) +//val petsCache = serverCache \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3704dc6..b4acdf3 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,7 @@ spring.application.name=Kotlin.SpringbootV2 +server.port = 8081 +spring.datasource.url=jdbc:postgresql://localhost:5432/OnlineOrdersDB +spring.datasource.username=postgres +spring.datasource.password=7150 +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 deleted file mode 100644 index b2e2320..0000000 --- a/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.coded.spring.ordering - -import org.junit.jupiter.api.Test -import org.springframework.boot.test.context.SpringBootTest - -@SpringBootTest -class ApplicationTests { - - @Test - fun contextLoads() { - } - -} diff --git a/src/test/kotlin/com/coded/spring/test/ApplicationTests.kt b/src/test/kotlin/com/coded/spring/test/ApplicationTests.kt new file mode 100644 index 0000000..a7e7acb --- /dev/null +++ b/src/test/kotlin/com/coded/spring/test/ApplicationTests.kt @@ -0,0 +1,103 @@ +package com.coded.spring.test + +import com.coded.spring.authentication.JWT.JwtService +import com.coded.spring.ordering.Item +import com.coded.spring.ordering.OrderRequest +import com.coded.spring.ordering.OrderResponse +import com.coded.spring.users.UserEntity +import com.coded.spring.users.UsersRepository +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.http.HttpStatus +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.test.context.ActiveProfiles +import org.springframework.util.MultiValueMap +import kotlin.test.assertEquals + +// how to build test (not really working need to match the tests with the endpoints) + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class ApplicationTests { + @Autowired + lateinit var restTemplate: TestRestTemplate +// the correct way with jwt + companion object { + @JvmStatic + @BeforeAll + fun setUp( + @Autowired usersRepository: UsersRepository, + @Autowired passwordEncoder: PasswordEncoder, + ){ + usersRepository.deleteAll() + val testUser = UserEntity( + name = "coded", + age = 24, + username = "meshal", + password = passwordEncoder.encode("meshal99775283") + ) + val savedUser = usersRepository.save(testUser) + print("savedUser ${savedUser.id}") + } + } + + @Test + fun testWelcome(@Autowired jwtService: JwtService) { + val token = jwtService.generateToken("meshal") + val headers = HttpHeaders( + MultiValueMap.fromSingleValue(mapOf("Authorization" to "Bearer $token")) + ) + val requestEntity = HttpEntity(headers) + + val result = restTemplate.exchange( + "/welcome", + HttpMethod.GET, + requestEntity, + String::class.java + ) + assertEquals(HttpStatus.OK, result.statusCode) + assertEquals("Get in yoo!!!", result.body) + } + + // the correct way with jwt + @Test + fun testCreateOrder(@Autowired jwtService: JwtService) { + //Mock + val token = jwtService.generateToken("meshal") + val headers = HttpHeaders( + MultiValueMap.fromSingleValue(mapOf("Authorization" to "Bearer $token")) + ) + val body = OrderRequest( + userId = 1, + items = listOf(Item("Chicken Burger", 3)) + ) + + //Trigger + val requestEntity = HttpEntity(body, headers) + val actualResponse = restTemplate.exchange( + "/orders", //Endpoint + HttpMethod.POST, + requestEntity, + OrderResponse::class.java + ) + + //Assert + assertEquals(HttpStatus.OK, actualResponse.statusCode) + + val expectedResponse = OrderResponse( + userId = 1, + items = listOf( + Item("Chicken Burger", 3) + ) + ) + assertEquals(expectedResponse, actualResponse.body, "Unexpected order created...") + } + +} + +