diff --git a/auth/pom.xml b/auth/pom.xml new file mode 100644 index 0000000..547c47f --- /dev/null +++ b/auth/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + + com.coded.spring + Ordering + 0.0.1-SNAPSHOT + + + auth + + + UTF-8 + official + 1.8 + + + + + mavenCentral + https://repo1.maven.org/maven2/ + + + + + src/main/kotlin + src/test/kotlin + + + org.jetbrains.kotlin + kotlin-maven-plugin + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + maven-surefire-plugin + 2.22.2 + + + maven-failsafe-plugin + 2.22.2 + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + MainKt + + + + + + + + org.jetbrains.kotlin + kotlin-test-junit5 + 2.1.0 + test + + + org.junit.jupiter + junit-jupiter + 5.10.0 + test + + + org.jetbrains.kotlin + kotlin-stdlib + 2.1.0 + + + + + + diff --git a/src/main/kotlin/com/coded/spring/ordering/Application.kt b/auth/src/main/kotlin/auth-service/AuthApplication.kt similarity index 66% rename from src/main/kotlin/com/coded/spring/ordering/Application.kt rename to auth/src/main/kotlin/auth-service/AuthApplication.kt index 8554e49..fb67b5c 100644 --- a/src/main/kotlin/com/coded/spring/ordering/Application.kt +++ b/auth/src/main/kotlin/auth-service/AuthApplication.kt @@ -1,11 +1,11 @@ -package com.coded.spring.ordering +package `auth-service` import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @SpringBootApplication -class Application +class AuthApplication fun main(args: Array) { - runApplication(*args) + runApplication(*args) } diff --git a/auth/src/main/kotlin/auth-service/AuthenticationController.kt b/auth/src/main/kotlin/auth-service/AuthenticationController.kt new file mode 100644 index 0000000..66eb4cb --- /dev/null +++ b/auth/src/main/kotlin/auth-service/AuthenticationController.kt @@ -0,0 +1,38 @@ +package `auth-service` + +import org.springframework.security.authentication.* +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.core.userdetails.UsernameNotFoundException +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/authentication") +class AuthenticationController( + private val authenticationManager: AuthenticationManager, + private val userDetailsService: UserDetailsService, + private val jwtService: JwtService +) { + + @PostMapping("/login") + fun login(@RequestBody request: AuthenticationRequest): AuthenticationResponse { + val authenticationToken = UsernamePasswordAuthenticationToken(request.username, request.password) + val authentication = authenticationManager.authenticate(authenticationToken) + + if (authentication.isAuthenticated) { + val userDetails = userDetailsService.loadUserByUsername(request.username) + val token = jwtService.generateToken(userDetails.username) + return AuthenticationResponse(token) + } else { + throw UsernameNotFoundException("Invalid login") + } + } +} + +data class AuthenticationRequest( + val username: String, + val password: String +) + +data class AuthenticationResponse( + val token: String +) diff --git a/auth/src/main/kotlin/auth-service/SecurityConfig.kt b/auth/src/main/kotlin/auth-service/SecurityConfig.kt new file mode 100644 index 0000000..4546bbc --- /dev/null +++ b/auth/src/main/kotlin/auth-service/SecurityConfig.kt @@ -0,0 +1,46 @@ +package `auth-service` + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter + +@Configuration +@EnableWebSecurity +class SecurityConfig( + private val userDetailsService: UserDetailsService, + private val jwtAuthenticationFilter: JwtAuthenticationFilter +) { + + @Bean + fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder() + + @Bean + fun authenticationManager(authConfig: AuthenticationConfiguration): AuthenticationManager { + return authConfig.authenticationManager + } + + @Bean + fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http + .csrf { it.disable() } + .authorizeHttpRequests { + it.requestMatchers("/menu", "/clients", "/welcome", "/authentication/**","/api-docs").permitAll() + it.anyRequest().authenticated() + } + .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) } + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java) + .httpBasic { it.disable() } + .formLogin { it.disable() } + + return http.build() + } +} diff --git a/auth/src/main/kotlin/auth-service/TalabatRepository.kt b/auth/src/main/kotlin/auth-service/TalabatRepository.kt new file mode 100644 index 0000000..5a16427 --- /dev/null +++ b/auth/src/main/kotlin/auth-service/TalabatRepository.kt @@ -0,0 +1,23 @@ +package `auth-service` + +import jakarta.persistence.* +import org.springframework.data.jpa.repository.JpaRepository + +interface TalabatRepository : JpaRepository{ + fun findByUsername(username: String): UserEntity? +} + +@Entity +@Table(name = "users") +data class UserEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + var name: String, + @Column(unique = true) + var username: String, + + var password: String +) { + constructor() : this(null, "", "","") +} \ No newline at end of file diff --git a/auth/src/main/kotlin/auth-service/UserDetailsService.kt b/auth/src/main/kotlin/auth-service/UserDetailsService.kt new file mode 100644 index 0000000..8320cbb --- /dev/null +++ b/auth/src/main/kotlin/auth-service/UserDetailsService.kt @@ -0,0 +1,22 @@ +package `auth-service` + + +import `auth-service`.TalabatRepository +import org.springframework.security.core.userdetails.* +import org.springframework.stereotype.Service + +@Service +class CustomUserDetailsService( + private val talabatRepository: TalabatRepository +) : UserDetailsService { + override fun loadUserByUsername(username: String): UserDetails { + val user = talabatRepository.findByUsername(username) + ?: throw UsernameNotFoundException("User not found") + + return User.builder() + .username(user.username) + .password(user.password) + .roles("USER") + .build() + } +} \ No newline at end of file diff --git a/auth/src/main/kotlin/auth-service/customUserDetails.kt b/auth/src/main/kotlin/auth-service/customUserDetails.kt new file mode 100644 index 0000000..e58e7ea --- /dev/null +++ b/auth/src/main/kotlin/auth-service/customUserDetails.kt @@ -0,0 +1,21 @@ +package `auth-service` + +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.core.userdetails.UserDetails + +data class CustomUserDetails( + val id: Long, + private val username: String, + private val password: String +) : UserDetails { + override fun getAuthorities(): Collection = + listOf(SimpleGrantedAuthority("ROLE_USER")) + + override fun getPassword(): String = password + override fun getUsername(): String = username + override fun isAccountNonExpired(): Boolean = true + override fun isAccountNonLocked(): Boolean = true + override fun isCredentialsNonExpired(): Boolean = true + override fun isEnabled(): Boolean = true +} \ No newline at end of file diff --git a/auth/src/main/kotlin/auth-service/jwtAuthenticationFilter.kt b/auth/src/main/kotlin/auth-service/jwtAuthenticationFilter.kt new file mode 100644 index 0000000..0cc5bc9 --- /dev/null +++ b/auth/src/main/kotlin/auth-service/jwtAuthenticationFilter.kt @@ -0,0 +1,45 @@ +package `auth-service` + +import jakarta.servlet.FilterChain +import jakarta.servlet.http.* +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter + +@Component +class JwtAuthenticationFilter( + private val jwtService: JwtService, + private val userDetailsService: UserDetailsService +) : OncePerRequestFilter() { + + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + val authHeader = request.getHeader("Authorization") + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response) + return + } + + val token = authHeader.substring(7) + val username = jwtService.extractUsername(token) + + if (SecurityContextHolder.getContext().authentication == null) { + if (jwtService.isTokenValid(token, username)) { + val userDetails = userDetailsService.loadUserByUsername(username) + val authToken = UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.authorities + ) + authToken.details = WebAuthenticationDetailsSource().buildDetails(request) + SecurityContextHolder.getContext().authentication = authToken + } + } + + filterChain.doFilter(request, response) + } +} diff --git a/auth/src/main/kotlin/auth-service/jwtService.kt b/auth/src/main/kotlin/auth-service/jwtService.kt new file mode 100644 index 0000000..259ec02 --- /dev/null +++ b/auth/src/main/kotlin/auth-service/jwtService.kt @@ -0,0 +1,42 @@ +package `auth-service` + +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 + } + } +} diff --git a/ordercenter/pom.xml b/ordercenter/pom.xml new file mode 100644 index 0000000..3448249 --- /dev/null +++ b/ordercenter/pom.xml @@ -0,0 +1,90 @@ + + + 4.0.0 + + com.coded.spring + Ordering + 0.0.1-SNAPSHOT + + + ordercenter + + + UTF-8 + official + 1.8 + + + + + mavenCentral + https://repo1.maven.org/maven2/ + + + + + src/main/kotlin + src/test/kotlin + + + org.jetbrains.kotlin + kotlin-maven-plugin + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + maven-surefire-plugin + 2.22.2 + + + maven-failsafe-plugin + 2.22.2 + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + MainKt + + + + + + + + org.jetbrains.kotlin + kotlin-test-junit5 + 2.1.0 + test + + + org.junit.jupiter + junit-jupiter + 5.10.0 + test + + + org.jetbrains.kotlin + kotlin-stdlib + 2.1.0 + + + + \ No newline at end of file diff --git a/ordercenter/src/main/kotlin/ordering-service/TalabatController.kt b/ordercenter/src/main/kotlin/ordering-service/TalabatController.kt new file mode 100644 index 0000000..8ebbc79 --- /dev/null +++ b/ordercenter/src/main/kotlin/ordering-service/TalabatController.kt @@ -0,0 +1,49 @@ +package `ordering-service` + +import org.springframework.web.bind.annotation.* +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.security.core.userdetails.UserDetails + +@RestController +class TalabatController( + private val talabatRepository: TalabatRepository, + private val passwordEncoder: PasswordEncoder, + private val appProperties: AppProperties +) { + + @PostMapping("/clients") + fun clients(@RequestBody request: ClientsRequest): UserEntity { + val encodedPassword = passwordEncoder.encode(request.password) + return talabatRepository.save( + UserEntity( + name = request.name, + username = request.username, + password = encodedPassword + ) + ) + } + + @PostMapping("/order") + fun submitOrder( + @RequestBody request: OrderRequest, + @AuthenticationPrincipal user: UserDetails + ): String { + return "Order received from ${user.username} for items: ${request.items.joinToString(", ")}" + } +} + +data class ClientsRequest( + val name: String, + val username: String, + val password: String +) + +data class OrderRequest( + val items: List +) + +data class MenuItem( + val name: String, + val price: Double +) diff --git a/ordercenter/src/main/kotlin/ordering-service/cache/hazelCastInstance.kt b/ordercenter/src/main/kotlin/ordering-service/cache/hazelCastInstance.kt new file mode 100644 index 0000000..21251f9 --- /dev/null +++ b/ordercenter/src/main/kotlin/ordering-service/cache/hazelCastInstance.kt @@ -0,0 +1,11 @@ +package `ordering-service`.cache + +import com.hazelcast.config.Config +import com.hazelcast.core.Hazelcast +import com.hazelcast.core.HazelcastInstance + +val menuCacheConfig = Config("menu-cache").apply { + getMapConfig("menu").timeToLiveSeconds = 60 +} + +val menuHazelcastInstance: HazelcastInstance = Hazelcast.newHazelcastInstance(menuCacheConfig) diff --git a/ordercenter/src/main/kotlin/ordering-service/menu/menuCache.kt b/ordercenter/src/main/kotlin/ordering-service/menu/menuCache.kt new file mode 100644 index 0000000..ac88c0f --- /dev/null +++ b/ordercenter/src/main/kotlin/ordering-service/menu/menuCache.kt @@ -0,0 +1,5 @@ +package `ordering-service`.menu + +import `ordering-service`.cache.menuHazelcastInstance + +val menuCache = menuHazelcastInstance.getMap>("menu") diff --git a/ordercenter/src/main/kotlin/ordering-service/menu/menuController.kt b/ordercenter/src/main/kotlin/ordering-service/menu/menuController.kt new file mode 100644 index 0000000..fedb874 --- /dev/null +++ b/ordercenter/src/main/kotlin/ordering-service/menu/menuController.kt @@ -0,0 +1,22 @@ +package `ordering-service`.menu + +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/menu") +class MenuController(private val menuService: MenuService) { + + @GetMapping + fun getMenu(): List = menuService.getMenu() + + @PostMapping + fun addMenuItem(@RequestBody request: MenuRequest): MenuEntity { + return menuService.addMenuItem(request) + } +} + +data class MenuRequest( + val name: String, + val price: Double +) + diff --git a/ordercenter/src/main/kotlin/ordering-service/menu/menuEntity.kt b/ordercenter/src/main/kotlin/ordering-service/menu/menuEntity.kt new file mode 100644 index 0000000..5cfce72 --- /dev/null +++ b/ordercenter/src/main/kotlin/ordering-service/menu/menuEntity.kt @@ -0,0 +1,16 @@ +package `ordering-service`.menu + + +import jakarta.persistence.* + +@Entity +@Table(name = "menu") +data class MenuEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, + + val name: String = "", + + val price: Double = 0.0 +) \ No newline at end of file diff --git a/ordercenter/src/main/kotlin/ordering-service/menu/menuRepository.kt b/ordercenter/src/main/kotlin/ordering-service/menu/menuRepository.kt new file mode 100644 index 0000000..7c99400 --- /dev/null +++ b/ordercenter/src/main/kotlin/ordering-service/menu/menuRepository.kt @@ -0,0 +1,6 @@ +package `ordering-service`.menu + +import `ordering-service`.menu.MenuEntity +import org.springframework.data.jpa.repository.JpaRepository + +interface MenuRepository : JpaRepository \ No newline at end of file diff --git a/ordercenter/src/main/kotlin/ordering-service/menu/menuService.kt b/ordercenter/src/main/kotlin/ordering-service/menu/menuService.kt new file mode 100644 index 0000000..3052cf0 --- /dev/null +++ b/ordercenter/src/main/kotlin/ordering-service/menu/menuService.kt @@ -0,0 +1,41 @@ +package `ordering-service`.menu + +import com.coded.spring.ordering.AppProperties +import org.springframework.stereotype.Service + + +@Service +class MenuService( + private val menuRepository: MenuRepository, + private val appProperties: AppProperties +) { + + fun getMenu(): List { + val cached = menuCache["all"] + val menu = if (cached != null) { + println("Returning menu from cache") + cached + } else { + println("Fetching menu from DB...") + val dbMenu = menuRepository.findAll() + menuCache["all"] = dbMenu + dbMenu + } + + return if (appProperties.festive.enabled) { + menu.map { it.copy(price = (it.price * 0.8)) } + } else { + menu + } + } + + fun addMenuItem(request: MenuRequest): MenuEntity { + val newItem = MenuEntity(name = request.name, price = request.price) + val saved = menuRepository.save(newItem) + + menuCache.remove("all") + println("Cleared menu cache after adding new item") + + return saved + } +} \ No newline at end of file diff --git a/ordercenter/src/main/kotlin/ordering-service/orders/CreateOrderRequest.kt b/ordercenter/src/main/kotlin/ordering-service/orders/CreateOrderRequest.kt new file mode 100644 index 0000000..21cc481 --- /dev/null +++ b/ordercenter/src/main/kotlin/ordering-service/orders/CreateOrderRequest.kt @@ -0,0 +1,12 @@ +package `ordering-service`.orders + +data class CreateOrderRequest( + val userId: Long, + val restaurant: String, + val items: List +) + +data class ItemRequest( + val name: String, + val quantity: Int +) diff --git a/ordercenter/src/main/kotlin/ordering-service/orders/ItemsEntity.kt b/ordercenter/src/main/kotlin/ordering-service/orders/ItemsEntity.kt new file mode 100644 index 0000000..75fafa9 --- /dev/null +++ b/ordercenter/src/main/kotlin/ordering-service/orders/ItemsEntity.kt @@ -0,0 +1,19 @@ +package `ordering-service`.orders + +import jakarta.persistence.* + +@Entity +@Table(name = "items") +data class ItemEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, + + val name: String, + val quantity: Int, + + @ManyToOne + @JoinColumn(name = "order_id") + val order: OrderEntity +) + diff --git a/ordercenter/src/main/kotlin/ordering-service/orders/ItemsRepository.kt b/ordercenter/src/main/kotlin/ordering-service/orders/ItemsRepository.kt new file mode 100644 index 0000000..275998c --- /dev/null +++ b/ordercenter/src/main/kotlin/ordering-service/orders/ItemsRepository.kt @@ -0,0 +1,7 @@ +package `ordering-service`.orders + +import org.springframework.data.jpa.repository.JpaRepository +import `ordering-service`.orders.ItemEntity + +interface ItemsRepository : JpaRepository + diff --git a/ordercenter/src/main/kotlin/ordering-service/orders/OrderEntity.kt b/ordercenter/src/main/kotlin/ordering-service/orders/OrderEntity.kt new file mode 100644 index 0000000..d0c97fc --- /dev/null +++ b/ordercenter/src/main/kotlin/ordering-service/orders/OrderEntity.kt @@ -0,0 +1,27 @@ +package `ordering-service`.orders + +import com.coded.spring.ordering.UserEntity +import jakarta.persistence.* + +@Entity +@Table(name = "orders") +data class OrderEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, + + @ManyToOne + @JoinColumn(name = "name_id") + val user: UserEntity, + + val restaurant: String, + + @Column(columnDefinition = "TEXT") + val items: String = "" +) + + { + constructor() : this(null, UserEntity(), "", "") +} + + diff --git a/ordercenter/src/main/kotlin/ordering-service/orders/OrdersController.kt b/ordercenter/src/main/kotlin/ordering-service/orders/OrdersController.kt new file mode 100644 index 0000000..2840f0e --- /dev/null +++ b/ordercenter/src/main/kotlin/ordering-service/orders/OrdersController.kt @@ -0,0 +1,19 @@ +package `ordering-service`.orders + +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/orders") +class OrdersController( + private val ordersService: OrdersService +) { + @PostMapping + fun createOrder(@RequestBody request: CreateOrderRequest): OrderEntity { + return ordersService.createOrder(request) + } + @GetMapping("/v1/list") + fun listOrders(): List { + return ordersService.listOrders() + } +} + diff --git a/ordercenter/src/main/kotlin/ordering-service/orders/OrdersRepository.kt b/ordercenter/src/main/kotlin/ordering-service/orders/OrdersRepository.kt new file mode 100644 index 0000000..949b207 --- /dev/null +++ b/ordercenter/src/main/kotlin/ordering-service/orders/OrdersRepository.kt @@ -0,0 +1,6 @@ +package `ordering-service`.orders + +import org.springframework.data.jpa.repository.JpaRepository +import `ordering-service`.orders.OrderEntity + +interface OrderRepository : JpaRepository diff --git a/ordercenter/src/main/kotlin/ordering-service/orders/OrdersService.kt b/ordercenter/src/main/kotlin/ordering-service/orders/OrdersService.kt new file mode 100644 index 0000000..028091e --- /dev/null +++ b/ordercenter/src/main/kotlin/ordering-service/orders/OrdersService.kt @@ -0,0 +1,31 @@ +package `ordering-service`.orders + +import com.coded.spring.ordering.TalabatRepository +import `ordering-service`.orders.OrderEntity +import com.fasterxml.jackson.databind.ObjectMapper +import `ordering-service`.orders.CreateOrderRequest +import `ordering-service`.orders.OrderRepository +import org.springframework.stereotype.Service + +@Service +class OrdersService( + private val orderRepository: OrderRepository, + private val userRepository: TalabatRepository, + private val objectMapper: ObjectMapper +) { + fun createOrder(request: CreateOrderRequest): OrderEntity { + val user = userRepository.findById(request.userId).orElseThrow() + val itemsJson = objectMapper.writeValueAsString(request.items) + + val order = OrderEntity( + user = user, + restaurant = request.restaurant, + items = itemsJson + ) + + return orderRepository.save(order) + } + fun listOrders(): List { + return orderRepository.findAll() + } +} \ No newline at end of file diff --git a/ordercenter/src/main/kotlin/ordering-service/profiles/profileController.kt b/ordercenter/src/main/kotlin/ordering-service/profiles/profileController.kt new file mode 100644 index 0000000..2fdc67c --- /dev/null +++ b/ordercenter/src/main/kotlin/ordering-service/profiles/profileController.kt @@ -0,0 +1,36 @@ +package `ordering-service`.profiles + +import com.coded.spring.ordering.TalabatRepository +import org.springframework.web.bind.annotation.* +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.security.core.userdetails.UserDetails + +@RestController +class ProfileController( + private val talabatRepository: TalabatRepository, + private val profileRepository: ProfileRepository +) { + + @PostMapping("/profile") + fun createProfile( + @RequestBody profileRequest: ProfileRequest, + @AuthenticationPrincipal userDetails: UserDetails + ): ProfileEntity { + val user = talabatRepository.findByUsername(userDetails.username) + ?: throw RuntimeException("User not found") + + val profile = ProfileEntity( + user = user, + firstName = profileRequest.firstName, + lastName = profileRequest.lastName, + phoneNumber = profileRequest.phoneNumber + ) + + return profileRepository.save(profile) + } +} +data class ProfileRequest( + val firstName: String, + val lastName: String, + val phoneNumber: String +) \ No newline at end of file diff --git a/ordercenter/src/main/kotlin/ordering-service/profiles/profileEntity.kt b/ordercenter/src/main/kotlin/ordering-service/profiles/profileEntity.kt new file mode 100644 index 0000000..278fcb3 --- /dev/null +++ b/ordercenter/src/main/kotlin/ordering-service/profiles/profileEntity.kt @@ -0,0 +1,19 @@ +package `ordering-service`.profiles + +import com.coded.spring.ordering.UserEntity +import jakarta.persistence.* + +@Entity +@Table(name = "profiles") +data class ProfileEntity( + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + + @OneToOne + @JoinColumn(name = "user_id", referencedColumnName = "id") + var user: UserEntity? = null, + + var firstName: String = "", + var lastName: String = "", + var phoneNumber: String = "" +) \ No newline at end of file diff --git a/ordercenter/src/main/kotlin/ordering-service/profiles/profileRepository.kt b/ordercenter/src/main/kotlin/ordering-service/profiles/profileRepository.kt new file mode 100644 index 0000000..4ac31ed --- /dev/null +++ b/ordercenter/src/main/kotlin/ordering-service/profiles/profileRepository.kt @@ -0,0 +1,8 @@ +package `ordering-service`.profiles + +import jakarta.inject.Named +import `ordering-service`.profiles.ProfileEntity +import org.springframework.data.jpa.repository.JpaRepository + +@Named +interface ProfileRepository : JpaRepository diff --git a/pom.xml b/pom.xml index 163ad53..1b0088f 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,11 @@ + + auth + ordercenter + welcoming + @@ -43,11 +49,114 @@ org.jetbrains.kotlin kotlin-reflect + + com.hazelcast + hazelcast + 5.5.0 + + + org.springdoc + springdoc-openapi-starter-webmvc-api + 2.6.0 + + + org.postgresql + postgresql + runtime + + + jakarta.inject + jakarta.inject-api + + + io.cucumber + cucumber-java + 7.20.1 + test + + + + + io.cucumber + cucumber-spring + 7.14.0 + test + + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-data-jpa + + + io.cucumber + cucumber-junit-platform-engine + 7.20.1 + test + + + org.junit.platform + junit-platform-suite-api + 1.10.0 + test + + + + com.h2database + h2 + test + org.jetbrains.kotlin kotlin-stdlib + + org.springframework.boot + spring-boot-starter-test + test + + + org.jetbrains.kotlin + kotlin-test-junit5 + test + + + com.fasterxml.jackson.core + jackson-databind + + + 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-data-jpa + + + com.h2database + h2 + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + org.springframework.boot spring-boot-starter-test diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 3704dc6..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=Kotlin.SpringbootV2 diff --git a/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt b/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt index b2e2320..81fba0d 100644 --- a/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt +++ b/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt @@ -1,13 +1,106 @@ -package com.coded.spring.ordering - -import org.junit.jupiter.api.Test -import org.springframework.boot.test.context.SpringBootTest - -@SpringBootTest -class ApplicationTests { - - @Test - fun contextLoads() { - } - -} +//package com.coded.spring.ordering +// +//import com.coded.spring.ordering.orders.OrderRepository +//import com.coded.spring.ordering.profiles.ProfileEntity +//import com.coded.spring.ordering.profiles.ProfileRepository +//import com.coded.spring.ordering.profiles.ProfileRequest +//import org.junit.jupiter.api.Assertions.* +//import org.junit.jupiter.api.BeforeAll +//import org.junit.jupiter.api.Test +//import org.junit.jupiter.api.TestInstance +//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.boot.test.web.server.LocalServerPort +//import org.springframework.http.HttpEntity +//import org.springframework.http.HttpHeaders +//import org.springframework.http.HttpStatus +//import org.springframework.http.MediaType +//import org.springframework.security.crypto.password.PasswordEncoder +//import org.springframework.test.context.ActiveProfiles +// +//@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +//@ActiveProfiles("test") +//@TestInstance(TestInstance.Lifecycle.PER_CLASS) +//class ProfileControllerTest { +// +// @LocalServerPort +// var port: Int = 0 +// +// @Autowired +// lateinit var usersRepository: TalabatRepository +// +// @Autowired +// lateinit var ordersRepository: OrderRepository +// +// @Autowired +// lateinit var profileRepository: ProfileRepository +// +// @Autowired +// lateinit var passwordEncoder: PasswordEncoder +// +// @Autowired +// lateinit var restTemplate: TestRestTemplate +// +// lateinit var tokenUsername: String +// lateinit var tokenPassword: String +// +// lateinit var token: String +// +// @BeforeAll +// fun setUp() { +// val username = "ZainabAlS" +// val rawPassword = "joincoded" +// +// if (usersRepository.findByUsername(username) == null) { +// val testUser = UserEntity( +// name = "Zainab", +// username = username, +// password = passwordEncoder.encode(rawPassword) +// ) +// usersRepository.save(testUser) +// } +// +// val loginRequest = AuthenticationRequest(username, rawPassword) +// val loginResponse = restTemplate.postForEntity( +// "http://localhost:$port/authentication/login", +// loginRequest, +// AuthenticationResponse::class.java +// ) +// +// assertEquals(HttpStatus.OK, loginResponse.statusCode) +// token = loginResponse.body?.token ?: throw IllegalStateException("Token was not returned") +// +// tokenUsername = username +// tokenPassword = rawPassword +// } +// +// +// @Test +// fun `authenticated user can create profile`() { +// val request = ProfileRequest( +// firstName = "Zainab", +// lastName = "AlSaffar", +// phoneNumber = "12345678" +// ) +// +// val headers = HttpHeaders().apply { +// contentType = MediaType.APPLICATION_JSON +// setBearerAuth(token) +// } +// +// val entity = HttpEntity(request, headers) +// +// val response = restTemplate.postForEntity( +// "http://localhost:$port/profile", +// entity, +// ProfileEntity::class.java +// ) +// +// assertEquals(HttpStatus.OK, response.statusCode) +// assertNotNull(response.body) +// assertEquals("Zainab", response.body?.firstName) +// assertEquals("AlSaffar", response.body?.lastName) +// } +// +//} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml new file mode 100644 index 0000000..db57135 --- /dev/null +++ b/src/test/resources/application-test.yml @@ -0,0 +1,17 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1 + driver-class-name: org.h2.Driver + username: sa + password: + jpa: + hibernate: + ddl-auto: create-drop + show-sql: true + properties: + hibernate: + dialect: org.hibernate.dialect.H2Dialect + +sql: +init: +mode: never \ No newline at end of file diff --git a/welcoming/pom.xml b/welcoming/pom.xml new file mode 100644 index 0000000..e017ff7 --- /dev/null +++ b/welcoming/pom.xml @@ -0,0 +1,96 @@ + + + 4.0.0 + + com.coded.spring + Ordering + 0.0.1-SNAPSHOT + + + welcoming + + + UTF-8 + official + 1.8 + + + + + mavenCentral + https://repo1.maven.org/maven2/ + + + + + src/main/kotlin + src/test/kotlin + + + org.jetbrains.kotlin + kotlin-maven-plugin + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + maven-surefire-plugin + 2.22.2 + + + maven-failsafe-plugin + 2.22.2 + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + MainKt + + + + + + + + org.jetbrains.kotlin + kotlin-test-junit5 + 2.1.0 + test + + + org.junit.jupiter + junit-jupiter + 5.10.0 + test + + + org.jetbrains.kotlin + kotlin-stdlib + 2.1.0 + + + com.coded.spring + auth + 0.0.1-SNAPSHOT + + + + + \ No newline at end of file diff --git a/welcoming/src/main/kotlin/welcome-service/Application.kt b/welcoming/src/main/kotlin/welcome-service/Application.kt new file mode 100644 index 0000000..8da59dc --- /dev/null +++ b/welcoming/src/main/kotlin/welcome-service/Application.kt @@ -0,0 +1,13 @@ +package com.coded.spring.`welcome-service` + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.boot.runApplication + +@SpringBootApplication +@EnableConfigurationProperties(AppProperties::class) +class Application + +fun main(args: Array) { + runApplication(*args) +} diff --git a/welcoming/src/main/kotlin/welcome-service/appConfiguration.kt b/welcoming/src/main/kotlin/welcome-service/appConfiguration.kt new file mode 100644 index 0000000..afa2f64 --- /dev/null +++ b/welcoming/src/main/kotlin/welcome-service/appConfiguration.kt @@ -0,0 +1,16 @@ +package com.coded.spring.`welcome-service` + +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.context.annotation.Configuration + +@Configuration +@ConfigurationProperties(prefix = "app") +class AppProperties { + lateinit var companyName: String + var festive: Festive = Festive() + + class Festive { + var enabled: Boolean = false + var message: String = "" + } +} diff --git a/welcoming/src/main/kotlin/welcome-service/resources/application.properties b/welcoming/src/main/kotlin/welcome-service/resources/application.properties new file mode 100644 index 0000000..a7bf29c --- /dev/null +++ b/welcoming/src/main/kotlin/welcome-service/resources/application.properties @@ -0,0 +1,16 @@ +spring.application.name=Kotlin.SpringbootV2 + +spring.datasource.url=jdbc:postgresql://localhost:5432/talabatDB +spring.datasource.username=postgres +spring.datasource.password=NBKPostgres +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect + +#server-welcome-message=Welcome to Talabat! Where all your favourite restaurants are a click away + +server-welcome-message=Welcome to Online Ordering by ${app.company-name} + +app.company-name=Zainab Foods +app.festive.enabled=true +app.festive.message=Eidkom Mubarak + +springdoc.api-docs.path=/api-docs \ No newline at end of file diff --git a/welcoming/src/main/kotlin/welcome-service/welcomeController.kt b/welcoming/src/main/kotlin/welcome-service/welcomeController.kt new file mode 100644 index 0000000..846a65e --- /dev/null +++ b/welcoming/src/main/kotlin/welcome-service/welcomeController.kt @@ -0,0 +1,21 @@ +package com.coded.spring.`welcome-service` + +import com.coded.spring.`welcome-service`.AppProperties +import org.springframework.web.bind.annotation.* +import org.springframework.security.crypto.password.PasswordEncoder + +@RestController +class TalabatController( + private val talabatRepository: TalabatRepository, + private val passwordEncoder: PasswordEncoder, + private val appProperties: AppProperties +) { + + @GetMapping("/welcome") + fun welcomeToTalabat(): String { + return if (appProperties.festive.enabled) { + appProperties.festive.message + } else { + "Welcome to Online Ordering by ${appProperties.companyName}" + } + } \ No newline at end of file diff --git a/zainab-alsaffar-online-ordering-api-swagger-01.json b/zainab-alsaffar-online-ordering-api-swagger-01.json new file mode 100644 index 0000000..73ab2c9 --- /dev/null +++ b/zainab-alsaffar-online-ordering-api-swagger-01.json @@ -0,0 +1 @@ +{"openapi":"3.0.1","info":{"title":"OpenAPI definition","version":"v0"},"servers":[{"url":"http://localhost:8080","description":"Generated server url"}],"paths":{"/profile":{"post":{"tags":["profile-controller"],"operationId":"createProfile","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProfileEntity"}}}}}}},"/orders":{"post":{"tags":["orders-controller"],"operationId":"createOrder","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrderRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OrderEntity"}}}}}}},"/order":{"post":{"tags":["talabat-controller"],"operationId":"submitOrder","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"string"}}}}}}},"/menu":{"get":{"tags":["menu-controller"],"operationId":"getMenu","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MenuEntity"}}}}}}},"post":{"tags":["menu-controller"],"operationId":"addMenuItem","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MenuRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MenuEntity"}}}}}}},"/clients":{"post":{"tags":["talabat-controller"],"operationId":"clients","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientsRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserEntity"}}}}}}},"/authentication/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"}}}}}}},"/welcome":{"get":{"tags":["talabat-controller"],"operationId":"welcomeToTalabat","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"string"}}}}}}},"/orders/v1/list":{"get":{"tags":["orders-controller"],"operationId":"listOrders","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OrderEntity"}}}}}}}}},"components":{"schemas":{"ProfileRequest":{"required":["firstName","lastName","phoneNumber"],"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"phoneNumber":{"type":"string"}}},"ProfileEntity":{"required":["firstName","lastName","phoneNumber"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"user":{"$ref":"#/components/schemas/UserEntity"},"firstName":{"type":"string"},"lastName":{"type":"string"},"phoneNumber":{"type":"string"}}},"UserEntity":{"required":["name","password","username"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"username":{"type":"string"},"password":{"type":"string"}}},"CreateOrderRequest":{"required":["items","restaurant","userId"],"type":"object","properties":{"userId":{"type":"integer","format":"int64"},"restaurant":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/components/schemas/ItemRequest"}}}},"ItemRequest":{"required":["name","quantity"],"type":"object","properties":{"name":{"type":"string"},"quantity":{"type":"integer","format":"int32"}}},"OrderEntity":{"required":["items","restaurant","user"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"user":{"$ref":"#/components/schemas/UserEntity"},"restaurant":{"type":"string"},"items":{"type":"string"}}},"OrderRequest":{"required":["items"],"type":"object","properties":{"items":{"type":"array","items":{"type":"string"}}}},"MenuRequest":{"required":["name","price"],"type":"object","properties":{"name":{"type":"string"},"price":{"type":"number","format":"double"}}},"MenuEntity":{"required":["name","price"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"price":{"type":"number","format":"double"}}},"ClientsRequest":{"required":["name","password","username"],"type":"object","properties":{"name":{"type":"string"},"username":{"type":"string"},"password":{"type":"string"}}},"AuthenticationRequest":{"required":["password","username"],"type":"object","properties":{"username":{"type":"string"},"password":{"type":"string"}}},"AuthenticationResponse":{"required":["token"],"type":"object","properties":{"token":{"type":"string"}}}}}} \ No newline at end of file