diff --git a/pom.xml b/pom.xml index 163ad53..173248a 100644 --- a/pom.xml +++ b/pom.xml @@ -1,63 +1,183 @@ - + 4.0.0 + org.springframework.boot spring-boot-starter-parent 3.4.4 - + + com.coded.spring Ordering 0.0.1-SNAPSHOT Kotlin.SpringbootV2 Kotlin.SpringbootV2 - - - - - - - - - - - - - + 21 1.9.25 + + org.springframework.boot spring-boot-starter-web + - com.fasterxml.jackson.module - jackson-module-kotlin + org.springframework.boot + spring-boot-starter-data-jpa + + + + + org.springframework.boot + spring-boot-starter-security + + + + + org.springframework.security + spring-security-crypto + + + + org.postgresql + postgresql + runtime + + + + com.h2database + h2 + test + + + org.jetbrains.kotlin kotlin-reflect + org.jetbrains.kotlin kotlin-stdlib + + com.fasterxml.jackson.module + jackson-module-kotlin + + + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + + io.jsonwebtoken + jjwt-impl + runtime + 0.11.5 + + + + io.jsonwebtoken + jjwt-jackson + runtime + 0.11.5 + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.5.0 + + + + + jakarta.inject + jakarta.inject-api + + + + + com.hazelcast + hazelcast + 5.5.0 + + + org.springframework.boot spring-boot-starter-test test + org.jetbrains.kotlin kotlin-test-junit5 test + + + io.cucumber + cucumber-java + 7.11.2 + test + + + + io.cucumber + cucumber-junit + 7.11.2 + test + + + + io.rest-assured + rest-assured + 5.3.0 + test + + + + io.rest-assured + json-path + 5.3.0 + test + + + + io.rest-assured + json-schema-validator + 5.3.0 + test + + + + org.junit.jupiter + junit-jupiter-api + 5.10.0 + test + + + + org.junit.jupiter + junit-jupiter-engine + 5.10.0 + test + + @@ -90,4 +210,4 @@ - + \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/AppProperties.kt b/src/main/kotlin/com/coded/spring/ordering/AppProperties.kt new file mode 100644 index 0000000..dd101b6 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/AppProperties.kt @@ -0,0 +1,13 @@ +package com.coded.spring.ordering + +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.context.annotation.Configuration + +@Configuration +@ConfigurationProperties +class AppProperties { + lateinit var companyName: String + var festiveMode: Boolean = false + var festiveMessage: String = "Eidkom Mubarak Coded" + var festiveDiscount: Int = 20 +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/Application.kt b/src/main/kotlin/com/coded/spring/ordering/Application.kt index 8554e49..b4ea854 100644 --- a/src/main/kotlin/com/coded/spring/ordering/Application.kt +++ b/src/main/kotlin/com/coded/spring/ordering/Application.kt @@ -1,5 +1,8 @@ package com.coded.spring.ordering +import com.hazelcast.config.Config; +import com.hazelcast.core.Hazelcast +import com.hazelcast.core.HazelcastInstance import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @@ -8,4 +11,8 @@ class Application fun main(args: Array) { runApplication(*args) + helloWorldConfig.getMapConfig("pets").setTimeToLiveSeconds(10) } + +val helloWorldConfig = Config("hello-world-cache") +val serverCache: HazelcastInstance = Hazelcast.newHazelcastInstance(helloWorldConfig) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/InitUserRunner.kt b/src/main/kotlin/com/coded/spring/ordering/InitUserRunner.kt new file mode 100644 index 0000000..2db67a2 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/InitUserRunner.kt @@ -0,0 +1,34 @@ +package com.coded.spring.ordering + +import com.coded.spring.ordering.models.Roles +import com.coded.spring.ordering.models.User +import com.coded.spring.ordering.repo.UserRepository +import org.springframework.boot.CommandLineRunner +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication +import org.springframework.context.annotation.Bean +import org.springframework.security.crypto.password.PasswordEncoder + +@SpringBootApplication +class InitUserRunner { + @Bean + fun initUsers(userRepository: UserRepository, passwordEncoder: PasswordEncoder) = CommandLineRunner { + val user = User( + name = "HelloUser", + username = "testuser", + password = passwordEncoder.encode("Password123@"), + age = 18, + role = Roles.ADMIN + ) + if (userRepository.findByUsername(user.username) == null) { + println("Creating user ${user.username}") + userRepository.save(user) + } else { + println("User ${user.username} already exists") + } + } +} + +fun main(args: Array) { + runApplication(*args) +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/LoggingFilter.kt b/src/main/kotlin/com/coded/spring/ordering/LoggingFilter.kt new file mode 100644 index 0000000..ac5baa8 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/LoggingFilter.kt @@ -0,0 +1,37 @@ +package com.coded.spring.ordering + +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter +import org.springframework.web.util.ContentCachingRequestWrapper +import org.springframework.web.util.ContentCachingResponseWrapper + +@Component +class LoggingFilter: OncePerRequestFilter(){ + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + val cachedRequest = ContentCachingRequestWrapper(request) + val cachedResponse = ContentCachingResponseWrapper(response) + + filterChain.doFilter(cachedRequest, cachedResponse) + + logRequest(cachedRequest) + logResponse(cachedResponse) + cachedResponse.copyBodyToResponse() + } + + private fun logRequest(request: ContentCachingRequestWrapper) { + val requestBody = String(request.contentAsByteArray) + logger.info("Request: method=${request.method}, uri=${request.requestURI}, body=$requestBody") + } + + private fun logResponse(response: ContentCachingResponseWrapper) { + val responseBody = String(response.contentAsByteArray) + logger.info("Response: status=${response.status}, body=$responseBody") + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/CustomUserDetailsService.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/CustomUserDetailsService.kt new file mode 100644 index 0000000..4dea4f8 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/CustomUserDetailsService.kt @@ -0,0 +1,19 @@ +package com.coded.spring.ordering.authentication + +import org.springframework.security.core.userdetails.* +import com.coded.spring.ordering.repo.UserRepository +import org.springframework.stereotype.Service + +@Service +class CustomUserDetailsService( + private val usersRepository: UserRepository) : UserDetailsService { + override fun loadUserByUsername(username: String): UserDetails { + val user = usersRepository.findByUsername(username) ?: throw UsernameNotFoundException("User not found") + + return User.builder() + .username(user.username) + .password(user.password) + .roles(user.role.toString()) + .build() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt new file mode 100644 index 0000000..76e2ac9 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt @@ -0,0 +1,63 @@ +package com.coded.spring.ordering.authentication + +import com.coded.spring.ordering.authentication.jwt.JwtAuthenticationFilter +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.AuthenticationProvider +import org.springframework.security.authentication.dao.DaoAuthenticationProvider +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter + + +@Configuration +@EnableWebSecurity +class SecurityConfig( + private val jwtAuthFilter: JwtAuthenticationFilter, + private val userDetailsService: UserDetailsService +) { + + @Bean + fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http.csrf { it.disable() } + .authorizeHttpRequests { + it + .requestMatchers("/auth/**").permitAll() + .requestMatchers("/users/create").permitAll() + .requestMatchers("/users/**").hasRole("ADMIN") + .requestMatchers("/profile/**").authenticated() + .requestMatchers("/menu/**").authenticated() + + .anyRequest().permitAll() + } + .sessionManagement { + it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + } + .authenticationProvider(authenticationProvider()) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter::class.java) + + return http.build() + } + + @Bean + fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder() + + @Bean + fun authenticationManager(config: AuthenticationConfiguration): AuthenticationManager = + config.authenticationManager + + @Bean + fun authenticationProvider(): AuthenticationProvider { + val provider = DaoAuthenticationProvider() + provider.setUserDetailsService(userDetailsService) + provider.setPasswordEncoder(passwordEncoder()) + return provider + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/AuthenticationController.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/AuthenticationController.kt new file mode 100644 index 0000000..e579c57 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/AuthenticationController.kt @@ -0,0 +1,39 @@ +package com.coded.spring.ordering.authentication.jwt + +import org.springframework.security.authentication.* +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.core.userdetails.UsernameNotFoundException +import org.springframework.web.bind.annotation.* + + +@RestController +@RequestMapping("/auth") +class AuthenticationController( + private val authenticationManager: AuthenticationManager, + private val userDetailsService: UserDetailsService, + private val jwtService: JwtService +) { + + @PostMapping("/login") + fun login(@RequestBody authRequest: AuthenticationRequest): AuthenticationResponse { + val authToken = UsernamePasswordAuthenticationToken(authRequest.username, authRequest.password) + val authentication = authenticationManager.authenticate(authToken) + + if (authentication.isAuthenticated) { + val userDetails = userDetailsService.loadUserByUsername(authRequest.username) + val token = jwtService.generateToken(userDetails.username) + return AuthenticationResponse (token) + } else { + throw UsernameNotFoundException("Invalid user request!") + } + } +} + +data class AuthenticationRequest( + val username: String, + val password: String +) + +data class AuthenticationResponse( + val token: String +) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtAuthenticationFilter.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtAuthenticationFilter.kt new file mode 100644 index 0000000..f180b7c --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtAuthenticationFilter.kt @@ -0,0 +1,45 @@ +package com.coded.spring.ordering.authentication.jwt + +import jakarta.servlet.FilterChain +import jakarta.servlet.http.* +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter + +@Component +class JwtAuthenticationFilter( + private val jwtService: JwtService, + private val userDetailsService: UserDetailsService +) : OncePerRequestFilter() { + + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + val authHeader = request.getHeader("Authorization") + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response) + return + } + + val token = authHeader.substring(7) + val username = jwtService.extractUsername(token) + + if (SecurityContextHolder.getContext().authentication == null) { + if (jwtService.isTokenValid(token, username)) { + val userDetails = userDetailsService.loadUserByUsername(username) + val authToken = UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.authorities + ) + authToken.details = WebAuthenticationDetailsSource().buildDetails(request) + SecurityContextHolder.getContext().authentication = authToken + } + } + + filterChain.doFilter(request, response) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtService.kt b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtService.kt new file mode 100644 index 0000000..c877c7c --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtService.kt @@ -0,0 +1,42 @@ +package com.coded.spring.ordering.authentication.jwt + +import io.jsonwebtoken.* +import io.jsonwebtoken.security.Keys +import org.springframework.stereotype.Component +import java.util.* +import javax.crypto.SecretKey + +@Component +class JwtService { + + private val secretKey: SecretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256) + private val expirationMs: Long = 1000 * 60 * 60 + + fun generateToken(username: String): String { + val now = Date() + val expiry = Date(now.time + expirationMs) + + return Jwts.builder() + .setSubject(username) + .setIssuedAt(now) + .setExpiration(expiry) + .signWith(secretKey) + .compact() + } + + fun extractUsername(token: String): String = + Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .body + .subject + + fun isTokenValid(token: String, username: String): Boolean { + return try { + extractUsername(token) == username + } catch (e: Exception) { + false + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/controllers/HelloworldController.kt b/src/main/kotlin/com/coded/spring/ordering/controllers/HelloworldController.kt new file mode 100644 index 0000000..628170d --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/controllers/HelloworldController.kt @@ -0,0 +1,48 @@ +package com.coded.spring.ordering.controllers + +import com.coded.spring.ordering.AppProperties +import com.coded.spring.ordering.pets.PetsService +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + + @RestController + @RequestMapping("/api") + class HelloWorldController ( + private val petsProvider: PetsService, + private val appProperties: AppProperties + ){ + + @GetMapping("/hello") + fun helloWorld(): String { + return "Hello World!" + } + + @PostMapping("/myNameIs") + fun myName(@RequestBody request: NameRequest): NameResponse { + return NameResponse(id = 1, name = request.name) + } + @GetMapping("/Pets") + fun getPets(): String { + return petsProvider.listPets().toString().replace("[", "").replace("]", "") + } + @GetMapping("/Welcome/OnlineOrdering") + fun welcome(): String { + return if (appProperties.festiveMode) { + appProperties.festiveMessage + } else { + "Welcome to Online Ordering by ${appProperties.companyName}" + } + } + + data class NameRequest( + val name: String + ) + + data class NameResponse( + val id: Int, + val name: String + ) + } \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/controllers/ItemController.kt b/src/main/kotlin/com/coded/spring/ordering/controllers/ItemController.kt new file mode 100644 index 0000000..f948532 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/controllers/ItemController.kt @@ -0,0 +1,28 @@ +package com.coded.spring.ordering.controllers + +import com.coded.spring.ordering.models.Item +import com.coded.spring.ordering.service.ItemService +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/items") +class ItemController( + private val itemService: ItemService +) { + + @PostMapping + fun createItem(@RequestBody request: CreateItemRequest): Item { + return itemService.createItem(request.orderId, request.name, request.quantity) + } + + @GetMapping + fun getItems(@RequestParam orderId: Long): List { + return itemService.getItemsByOrderId(orderId) + } +} + +data class CreateItemRequest( + val orderId: Long, + val name: String, + val quantity: Int +) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/controllers/MenuController.kt b/src/main/kotlin/com/coded/spring/ordering/controllers/MenuController.kt new file mode 100644 index 0000000..b95d0a4 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/controllers/MenuController.kt @@ -0,0 +1,20 @@ +package com.coded.spring.ordering.controllers + +import com.coded.spring.ordering.models.MenuItem +import com.coded.spring.ordering.service.MenuService +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/menu") +class MenuController( + private val menuService: MenuService +) { + + @GetMapping + fun getMenu(): List = menuService.getAllMenuItems() + + @PostMapping + fun addMenuItem(@RequestBody menuItem: MenuItem): MenuItem { + return menuService.addMenuItem(menuItem) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/controllers/OrderController.kt b/src/main/kotlin/com/coded/spring/ordering/controllers/OrderController.kt new file mode 100644 index 0000000..f962678 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/controllers/OrderController.kt @@ -0,0 +1,25 @@ +package com.coded.spring.ordering.controllers + +import com.coded.spring.ordering.models.Order +import com.coded.spring.ordering.service.OrderService +import org.springframework.web.bind.annotation.* + +@RestController +class OrderController( + private val orderService: OrderService +) { + + @PostMapping("/orders") + fun createOrder(@RequestBody request: CreateOrderRequest) { + orderService.createOrder(request.userId) + } + + @GetMapping("/orders") + fun getOrders(@RequestParam userId: Long): List { + return orderService.getOrdersByUserId(userId) + } +} + +data class CreateOrderRequest( + val userId: Long +) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/controllers/ProfileController.kt b/src/main/kotlin/com/coded/spring/ordering/controllers/ProfileController.kt new file mode 100644 index 0000000..d52bceb --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/controllers/ProfileController.kt @@ -0,0 +1,33 @@ +package com.coded.spring.ordering.controllers + +import com.coded.spring.ordering.models.Profile +import com.coded.spring.ordering.service.ProfileService +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/profile") +class ProfileController( + private val profileService: ProfileService +) { + + @PostMapping + fun createOrUpdateProfile(@RequestBody request: ProfileRequest): Profile { + return profileService.createOrUpdateProfile( + userId = request.userId, + firstName = request.firstName, + lastName = request.lastName, + phoneNumber = request.phoneNumber + ) + } + + @GetMapping("/{userId}") + fun getProfile(@PathVariable userId: Long): Profile? = + profileService.getProfile(userId) +} + +data class ProfileRequest( + val userId: Long, + val firstName: String, + val lastName: String, + val phoneNumber: String +) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/controllers/UserController.kt b/src/main/kotlin/com/coded/spring/ordering/controllers/UserController.kt new file mode 100644 index 0000000..82f3e31 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/controllers/UserController.kt @@ -0,0 +1,34 @@ +package com.coded.spring.ordering.controllers + +import com.coded.spring.ordering.models.User +import com.coded.spring.ordering.repo.UserRepository +import com.coded.spring.ordering.service.UserService +import org.springframework.http.HttpStatus +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.web.bind.annotation.* +import org.springframework.web.server.ResponseStatusException + +@RestController +@RequestMapping("/users") +class UserController(private val userService: UserService, + private val userRepository: UserRepository, + private val passwordEncoder: PasswordEncoder +) { + + + @PostMapping("/create") + fun createUser(@RequestBody user: User): User { + val hashedPassword = passwordEncoder.encode(user.password) + val newUser = user.copy(password = hashedPassword) + + if (userRepository.findByUsername(user.username) != null) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Username already exists") as Throwable + } + + return userRepository.save(newUser) + } + @PostMapping("/getAllUsers") + fun getAllUsers(): List { + return userService.getAllUsers() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/models/Item.kt b/src/main/kotlin/com/coded/spring/ordering/models/Item.kt new file mode 100644 index 0000000..2ad482d --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/models/Item.kt @@ -0,0 +1,21 @@ +package com.coded.spring.ordering.models + +import jakarta.persistence.* + +@Entity +@Table(name = "items") +data class Item( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0, + + val name: String = "", + + val quantity: Int = 0, + + @ManyToOne + @JoinColumn(name = "order_id", nullable = false) + val order: Order = Order() +) { + constructor() : this(0, "", 0, Order()) +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/models/MenuItem.kt b/src/main/kotlin/com/coded/spring/ordering/models/MenuItem.kt new file mode 100644 index 0000000..dfc9bb6 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/models/MenuItem.kt @@ -0,0 +1,17 @@ +package com.coded.spring.ordering.models + +import jakarta.persistence.* + +@Entity +@Table(name = "menu_items") +data class MenuItem( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0, + + val name: String = "", + + val description: String = "", + + val price: Double = 0.0 +) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/models/Order.kt b/src/main/kotlin/com/coded/spring/ordering/models/Order.kt new file mode 100644 index 0000000..ee10c23 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/models/Order.kt @@ -0,0 +1,16 @@ +package com.coded.spring.ordering.models + +import jakarta.persistence.* + +@Entity +@Table(name = "orders") +data class Order( + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + val user: User = User() +) { + constructor() : this(0, User()) +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/models/Pet.kt b/src/main/kotlin/com/coded/spring/ordering/models/Pet.kt new file mode 100644 index 0000000..5c32614 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/models/Pet.kt @@ -0,0 +1,11 @@ +package com.coded.spring.ordering.models + +import java.io.Serializable + +data class Pet( + val name: String ="", + val type: String= "" +) : Serializable +{ + constructor() : this("", "") +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/models/Profile.kt b/src/main/kotlin/com/coded/spring/ordering/models/Profile.kt new file mode 100644 index 0000000..4ca3ac8 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/models/Profile.kt @@ -0,0 +1,24 @@ +package com.coded.spring.ordering.models + + +import jakarta.persistence.* + +@Entity +@Table(name = "profiles") +data class Profile( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0, + + @OneToOne + @JoinColumn(name = "user_id", referencedColumnName = "id", nullable = false, unique = true) + val user: User = User(), + val firstName : String = "", + val lastName : String = "", + val phoneNumber : String = "" + +){ + constructor() : this(0, User(), "", "", "") +} + + diff --git a/src/main/kotlin/com/coded/spring/ordering/models/User.kt b/src/main/kotlin/com/coded/spring/ordering/models/User.kt new file mode 100644 index 0000000..552645b --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/models/User.kt @@ -0,0 +1,26 @@ +package com.coded.spring.ordering.models + +import jakarta.persistence.* + +@Entity +@Table(name = "users") +data class User( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + var name: String, + var age: Int, + + var username: String, + var password: String, + + @Enumerated(EnumType.STRING) + val role: Roles = Roles.USER + +){ + constructor() : this(null, "name", 0, "username", "password", Roles.USER) +} + +enum class Roles { + USER, ADMIN +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/pets/PetsClient.kt b/src/main/kotlin/com/coded/spring/ordering/pets/PetsClient.kt new file mode 100644 index 0000000..cc8299e --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/pets/PetsClient.kt @@ -0,0 +1,27 @@ +package com.coded.spring.ordering.pets + +import com.coded.spring.ordering.models.Pet +import org.springframework.stereotype.Component + +@Component +class PetsClient { + + fun getPets(): List { + return listOf( + Pet(name = "Coco", type = "Bird"), + Pet(name = "Rocky", type = "Dog"), + Pet(name = "Daisy", type = "Rabbit"), + Pet(name = "Simba", type = "Cat"), + Pet(name = "Buddy", type = "Dog"), + Pet(name = "Chloe", type = "Cat"), + Pet(name = "Angel", type = "Bird"), + Pet(name = "Shadow", type = "Dog"), + Pet(name = "Lily", type = "Cat"), + Pet(name = "Pepper", type = "Bird"), + Pet(name = "Bailey", type = "Dog"), + Pet(name = "Nala", type = "Cat"), + Pet(name = "Zoe", type = "Rabbit"), + Pet(name = "Toby", type = "Dog") + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/pets/PetsProvider.kt b/src/main/kotlin/com/coded/spring/ordering/pets/PetsProvider.kt new file mode 100644 index 0000000..50373ca --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/pets/PetsProvider.kt @@ -0,0 +1,24 @@ +package com.coded.spring.ordering.pets + +import com.coded.spring.ordering.models.Pet +import com.coded.spring.ordering.serverCache +import jakarta.inject.Named + +@Named +class PetsProvider( + private val client: PetsClient) { + + fun getPets(): List { + val cachedPets = petsCache["pets"] + if (cachedPets?.size == 0 || cachedPets == null) { + println("No pets found, caching new data...") + val pets = client.getPets() + petsCache.put("pets", pets) + return pets + } + println("returning ${cachedPets.size} pets") + return petsCache["pets"] ?: listOf() + } +} + +val petsCache = serverCache.getMap>("pets") \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/pets/PetsService.kt b/src/main/kotlin/com/coded/spring/ordering/pets/PetsService.kt new file mode 100644 index 0000000..84d7834 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/pets/PetsService.kt @@ -0,0 +1,10 @@ +package com.coded.spring.ordering.pets + +import com.coded.spring.ordering.models.Pet +import jakarta.inject.Named + +@Named +class PetsService(private val petsProvider: PetsProvider) { + + fun listPets() : List = petsProvider.getPets() +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/repo/ItemRepository.kt b/src/main/kotlin/com/coded/spring/ordering/repo/ItemRepository.kt new file mode 100644 index 0000000..b11dcd0 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/repo/ItemRepository.kt @@ -0,0 +1,8 @@ +package com.coded.spring.ordering.repo + +import com.coded.spring.ordering.models.Item +import org.springframework.data.jpa.repository.JpaRepository + +interface ItemRepository : JpaRepository { + fun findByOrderId(orderId: Long): List +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/repo/MenuItemRepository.kt b/src/main/kotlin/com/coded/spring/ordering/repo/MenuItemRepository.kt new file mode 100644 index 0000000..c342ca7 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/repo/MenuItemRepository.kt @@ -0,0 +1,6 @@ +package com.coded.spring.ordering.repo + +import com.coded.spring.ordering.models.MenuItem +import org.springframework.data.jpa.repository.JpaRepository + +interface MenuItemRepository : JpaRepository \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/repo/OrderRepository.kt b/src/main/kotlin/com/coded/spring/ordering/repo/OrderRepository.kt new file mode 100644 index 0000000..a283566 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/repo/OrderRepository.kt @@ -0,0 +1,8 @@ +package com.coded.spring.ordering.repo + +import com.coded.spring.ordering.models.Order +import org.springframework.data.jpa.repository.JpaRepository + +interface OrderRepository : JpaRepository { + fun findByUserId(userId: Long): List +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/repo/ProfileRepository.kt b/src/main/kotlin/com/coded/spring/ordering/repo/ProfileRepository.kt new file mode 100644 index 0000000..01a211a --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/repo/ProfileRepository.kt @@ -0,0 +1,8 @@ +package com.coded.spring.ordering.repo + +import com.coded.spring.ordering.models.Profile +import org.springframework.data.jpa.repository.JpaRepository + +interface ProfileRepository : JpaRepository { + fun findByUserId(userId: Long): Profile? +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/repo/UserRepository.kt b/src/main/kotlin/com/coded/spring/ordering/repo/UserRepository.kt new file mode 100644 index 0000000..3319ac8 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/repo/UserRepository.kt @@ -0,0 +1,8 @@ +package com.coded.spring.ordering.repo + +import com.coded.spring.ordering.models.User +import org.springframework.data.jpa.repository.JpaRepository + +interface UserRepository : JpaRepository{ + fun findByUsername(username: String): User? +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/service/ItemService.kt b/src/main/kotlin/com/coded/spring/ordering/service/ItemService.kt new file mode 100644 index 0000000..62cfc24 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/service/ItemService.kt @@ -0,0 +1,21 @@ +package com.coded.spring.ordering.service + +import com.coded.spring.ordering.models.Item +import com.coded.spring.ordering.repo.ItemRepository +import com.coded.spring.ordering.repo.OrderRepository +import org.springframework.stereotype.Service + +@Service +class ItemService( + private val itemRepository: ItemRepository, + private val orderRepository: OrderRepository +) { + fun createItem(orderId: Long, name: String, quantity: Int): Item { + val order = orderRepository.findById(orderId).orElseThrow { Exception("Order not found") } + return itemRepository.save(Item(name = name, quantity = quantity, order = order)) + } + + fun getItemsByOrderId(orderId: Long): List { + return itemRepository.findByOrderId(orderId) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/service/MenuService.kt b/src/main/kotlin/com/coded/spring/ordering/service/MenuService.kt new file mode 100644 index 0000000..e5b3ec8 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/service/MenuService.kt @@ -0,0 +1,14 @@ +package com.coded.spring.ordering.service + +import com.coded.spring.ordering.models.MenuItem +import com.coded.spring.ordering.repo.MenuItemRepository +import org.springframework.stereotype.Service + +@Service +class MenuService( + private val menuItemRepository: MenuItemRepository +) { + fun getAllMenuItems(): List = menuItemRepository.findAll() + + fun addMenuItem(menuItem: MenuItem): MenuItem = menuItemRepository.save(menuItem) +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/service/OrderService.kt b/src/main/kotlin/com/coded/spring/ordering/service/OrderService.kt new file mode 100644 index 0000000..13a623b --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/service/OrderService.kt @@ -0,0 +1,23 @@ +package com.coded.spring.ordering.service + +import com.coded.spring.ordering.models.Order +import com.coded.spring.ordering.repo.OrderRepository +import com.coded.spring.ordering.repo.UserRepository +import org.springframework.stereotype.Service + +@Service +class OrderService( + private val orderRepository: OrderRepository, + private val userRepository: UserRepository +) { + + fun createOrder(userId: Long) { + val user = userRepository.findById(userId).orElseThrow { Exception("User not found") } + val newOrder = Order(user = user) + orderRepository.save(newOrder) + } + + fun getOrdersByUserId(userId: Long): List { + return orderRepository.findByUserId(userId) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/service/ProfileService.kt b/src/main/kotlin/com/coded/spring/ordering/service/ProfileService.kt new file mode 100644 index 0000000..3fa7a1d --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/service/ProfileService.kt @@ -0,0 +1,46 @@ +package com.coded.spring.ordering.service + +import com.coded.spring.ordering.models.Profile +import com.coded.spring.ordering.models.User +import com.coded.spring.ordering.repo.ProfileRepository +import com.coded.spring.ordering.repo.UserRepository +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import org.springframework.web.server.ResponseStatusException + +@Service +class ProfileService( + private val profileRepository: ProfileRepository, + private val userRepository: UserRepository +) { + + fun createOrUpdateProfile(userId: Long, firstName: String, lastName: String, phoneNumber: String): Profile { + validateInputs(firstName, lastName, phoneNumber) + + val user: User = userRepository.findById(userId) + .orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "User not found") } + + val existing = profileRepository.findByUserId(userId) + return if (existing != null) { + val updated = existing.copy(firstName = firstName, lastName = lastName, phoneNumber = phoneNumber) + profileRepository.save(updated) + } else { + val newProfile = Profile(user = user, firstName = firstName, lastName = lastName, phoneNumber = phoneNumber) + profileRepository.save(newProfile) + } + } + + fun getProfile(userId: Long): Profile? = profileRepository.findByUserId(userId) + + private fun validateInputs(firstName: String, lastName: String, phoneNumber: String) { + if (!firstName.matches(Regex("^[A-Za-z]+$"))) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "First name must contain letters only") + } + if (!lastName.matches(Regex("^[A-Za-z]+$"))) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Last name must contain letters only") + } + if (!phoneNumber.matches(Regex("^\\d{8}$"))) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Phone number must be exactly 8 digits with no letters") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/service/UserService.kt b/src/main/kotlin/com/coded/spring/ordering/service/UserService.kt new file mode 100644 index 0000000..98f8b68 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/service/UserService.kt @@ -0,0 +1,11 @@ +package com.coded.spring.ordering.service + +import com.coded.spring.ordering.models.User +import com.coded.spring.ordering.repo.UserRepository +import org.springframework.stereotype.Service + +@Service +class UserService(private val userRepository: UserRepository) { + fun getAllUsers(): List = userRepository.findAll() + +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3704dc6..47ce229 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,9 @@ spring.application.name=Kotlin.SpringbootV2 +server.port=8080 +spring.datasource.url=jdbc:postgresql://localhost:5432/OrderData +spring.datasource.username=postgres +spring.datasource.password=12345 +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +server-welcome-message=Hello from Spring Boot! +spring.jpa.hibernate.ddl-auto=update +springdoc.api-docs.path=/api-docs \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000..05c1f9c --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,4 @@ +company-name: NBK FullStack course +festive-mode: false +festive-message: "Eidkom Mubarak" +festive-discount: 20 \ No newline at end of file