diff --git a/pom.xml b/pom.xml index 163ad53..04235ee 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,41 @@ 1.9.25 + + 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.springframework.boot + spring-boot-starter-test + test + + + org.jetbrains.kotlin + kotlin-test-junit5 + test + + + org.springframework.boot + spring-boot-starter-data-jpa + org.springframework.boot spring-boot-starter-web @@ -43,23 +78,29 @@ org.jetbrains.kotlin kotlin-reflect + + com.hazelcast + hazelcast + 5.5.0 + org.jetbrains.kotlin kotlin-stdlib - org.springframework.boot - spring-boot-starter-test - test + org.postgresql + postgresql + compile - org.jetbrains.kotlin - kotlin-test-junit5 + com.h2database + h2 test - + + ${project.basedir}/src/main/kotlin ${project.basedir}/src/test/kotlin diff --git a/src/main/kotlin/com/coded/spring/ordering/Application.kt b/src/main/kotlin/Application.kt similarity index 87% rename from src/main/kotlin/com/coded/spring/ordering/Application.kt rename to src/main/kotlin/Application.kt index 8554e49..f5efc56 100644 --- a/src/main/kotlin/com/coded/spring/ordering/Application.kt +++ b/src/main/kotlin/Application.kt @@ -1,4 +1,5 @@ -package com.coded.spring.ordering +package com.coded.spring + import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @@ -8,4 +9,5 @@ class Application fun main(args: Array) { runApplication(*args) + } 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..465b654 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/InitUserRunner.kt @@ -0,0 +1,36 @@ +package com.coded.spring.com.coded.spring.ordering + +import com.coded.spring.Application +import com.coded.spring.ordering.users.Roles +import com.coded.spring.ordering.users.UserEntity + + +import com.coded.spring.ordering.users.UserRepository +import org.springframework.boot.CommandLineRunner +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 = UserEntity( + name = "HelloUser", + username = "testuser", + password = passwordEncoder.encode("password123"), + role = Roles.USER + + ) + if (userRepository.findByUsername(user.username) == null) { + println("Creating user ${user.username}") + userRepository.save(user) + } else { + println("User ${user.username} already exists") + } + } +} + +fun main(args: Array) { + runApplication(*args).close() +} \ No newline at end of file diff --git a/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..38c18d1 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/CustomUserDetailsService.kt @@ -0,0 +1,23 @@ +package com.coded.spring.com.coded.spring.ordering.authentication + +import com.coded.spring.ordering.users.UserRepository +import org.springframework.security.core.userdetails.* +import org.springframework.stereotype.Service + + +@Service +class CustomUserDetailsService( + private val userRepository: UserRepository +) : UserDetailsService { + override fun loadUserByUsername(username: String): UserDetails { + val user = userRepository.findByUsername(username) + ?: throw UsernameNotFoundException("User not found!") + + return User.builder() + .username(user.username) + .password(user.password) + .roles(user.role.toString()) + .build() + } +} + 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..f22038a --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/SecurityConfig.kt @@ -0,0 +1,59 @@ +package com.coded.spring.com.coded.spring.ordering.authentication + +import com.coded.spring.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("/user").permitAll() + .requestMatchers("/auth/**").permitAll() +// .requestMatchers("/profile").authenticated() + .anyRequest().authenticated() + + } + .sessionManagement { + it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + } + .authenticationProvider(authenticationProvider()) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter::class.java) + + return http.build() + } + + @Bean + fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder() + + @Bean + fun authenticationManager(config: AuthenticationConfiguration): AuthenticationManager = + config.authenticationManager + + @Bean + fun authenticationProvider(): AuthenticationProvider { + val provider = DaoAuthenticationProvider() + provider.setUserDetailsService(userDetailsService) + provider.setPasswordEncoder(passwordEncoder()) + return provider + } + +} \ No newline at end of file diff --git a/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..38ed3ab --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtAuthenticationFilter.kt @@ -0,0 +1,45 @@ +package com.coded.spring.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..8daa163 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/authentication/jwt/JwtService.kt @@ -0,0 +1,42 @@ +package com.coded.spring.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/config/HazelcastConfig.kt b/src/main/kotlin/com/coded/spring/ordering/config/HazelcastConfig.kt new file mode 100644 index 0000000..353b619 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/config/HazelcastConfig.kt @@ -0,0 +1,19 @@ +package com.coded.spring.com.coded.spring.ordering.config + + +import com.hazelcast.config.Config +import com.hazelcast.core.Hazelcast +import com.hazelcast.core.HazelcastInstance +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class HazelcastConfig { + + @Bean + fun hazelcastInstance(): HazelcastInstance { + val config = Config("menu-cache") + config.getMapConfig("menus").timeToLiveSeconds = 5 + return Hazelcast.newHazelcastInstance(config) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/items/ItemController.kt b/src/main/kotlin/com/coded/spring/ordering/items/ItemController.kt new file mode 100644 index 0000000..c04f3a5 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/items/ItemController.kt @@ -0,0 +1,2 @@ +package com.coded.spring.ordering.items + diff --git a/src/main/kotlin/com/coded/spring/ordering/items/ItemEntity.kt b/src/main/kotlin/com/coded/spring/ordering/items/ItemEntity.kt new file mode 100644 index 0000000..6a0d86b --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/items/ItemEntity.kt @@ -0,0 +1,24 @@ +package com.coded.spring.com.coded.spring.ordering.items + +import com.coded.spring.ordering.orders.OrderEntity +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 + +){ + constructor() : this(null, "", 0, OrderEntity() ) +} diff --git a/src/main/kotlin/com/coded/spring/ordering/items/ItemRepository.kt b/src/main/kotlin/com/coded/spring/ordering/items/ItemRepository.kt new file mode 100644 index 0000000..3c382a3 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/items/ItemRepository.kt @@ -0,0 +1,6 @@ +package com.coded.spring.ordering.items + +import com.coded.spring.com.coded.spring.ordering.items.ItemEntity +import org.springframework.data.jpa.repository.JpaRepository + +interface ItemsRepository: JpaRepository diff --git a/src/main/kotlin/com/coded/spring/ordering/items/ItemRequest.kt b/src/main/kotlin/com/coded/spring/ordering/items/ItemRequest.kt new file mode 100644 index 0000000..08bc4f7 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/items/ItemRequest.kt @@ -0,0 +1,2 @@ +package com.coded.spring.com.coded.spring.ordering.items + diff --git a/src/main/kotlin/com/coded/spring/ordering/items/ItemService.kt b/src/main/kotlin/com/coded/spring/ordering/items/ItemService.kt new file mode 100644 index 0000000..08bc4f7 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/items/ItemService.kt @@ -0,0 +1,2 @@ +package com.coded.spring.com.coded.spring.ordering.items + diff --git a/src/main/kotlin/com/coded/spring/ordering/menu/MenuController.kt b/src/main/kotlin/com/coded/spring/ordering/menu/MenuController.kt new file mode 100644 index 0000000..4c0fad0 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/menu/MenuController.kt @@ -0,0 +1,22 @@ +package com.coded.spring.com.coded.spring.ordering.menu + + +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/menu") +class MenuController( + private val menuService: MenuService +) { + @PostMapping + fun createMenu(@RequestBody menu: MenuEntity): ResponseEntity { + return menuService.createMenu(menu) + } + + @GetMapping + fun getAllMenus(): ResponseEntity> { + return menuService.getAllMenus() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/menu/MenuEntity.kt b/src/main/kotlin/com/coded/spring/ordering/menu/MenuEntity.kt new file mode 100644 index 0000000..22d80e6 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/menu/MenuEntity.kt @@ -0,0 +1,17 @@ +package com.coded.spring.com.coded.spring.ordering.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 +) { + constructor() : this(id = null, name = "", price = 0.0) +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/menu/MenuProvider.kt b/src/main/kotlin/com/coded/spring/ordering/menu/MenuProvider.kt new file mode 100644 index 0000000..63b2452 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/menu/MenuProvider.kt @@ -0,0 +1,25 @@ +package com.coded.spring.com.coded.spring.ordering.menu + + +import com.hazelcast.core.HazelcastInstance +import org.springframework.stereotype.Component + +@Component +class MenuProvider( + private val menuRepository: MenuRepository, + private val hazelcastInstance: HazelcastInstance +) { + private val menuCache = hazelcastInstance.getMap>("menus") + + fun getMenus(): List { + val cachedMenus = menuCache["menus"] + if (cachedMenus.isNullOrEmpty()) { + println("No menus in cache. Fetching from DB...") + val menus = menuRepository.findAll() + menuCache["menus"] = menus + return menus + } + println("Returning ${cachedMenus.size} menus from cache") + return cachedMenus + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/menu/MenuRepository.kt b/src/main/kotlin/com/coded/spring/ordering/menu/MenuRepository.kt new file mode 100644 index 0000000..6d4d2a1 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/menu/MenuRepository.kt @@ -0,0 +1,9 @@ +package com.coded.spring.com.coded.spring.ordering.menu + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface MenuRepository : JpaRepository{ + +} diff --git a/src/main/kotlin/com/coded/spring/ordering/menu/MenuRequest.kt b/src/main/kotlin/com/coded/spring/ordering/menu/MenuRequest.kt new file mode 100644 index 0000000..3fc73e9 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/menu/MenuRequest.kt @@ -0,0 +1,2 @@ +package com.coded.spring.com.coded.spring.ordering.menu + diff --git a/src/main/kotlin/com/coded/spring/ordering/menu/MenuService.kt b/src/main/kotlin/com/coded/spring/ordering/menu/MenuService.kt new file mode 100644 index 0000000..9abf577 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/menu/MenuService.kt @@ -0,0 +1,20 @@ +package com.coded.spring.com.coded.spring.ordering.menu + +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Service + + +@Service +class MenuService( + private val menuRepository: MenuRepository, + private val menuProvider: MenuProvider +) { + fun createMenu(menu: MenuEntity): ResponseEntity { + menuRepository.save(menu) + return ResponseEntity.ok("Menu item created") + } + + fun getAllMenus(): ResponseEntity> { + return ResponseEntity.ok(menuProvider.getMenus()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OrderController.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OrderController.kt new file mode 100644 index 0000000..142ab15 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OrderController.kt @@ -0,0 +1,22 @@ +package com.coded.spring.ordering.orders + +import com.coded.spring.com.coded.spring.ordering.orders.OrderRequest +import com.coded.spring.ordering.OrderService +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 OrderController( + private val orderService: OrderService +) { + + @GetMapping("/order") + fun homePage() = "Welcome to Ooreedoo, please place your order!" + + @PostMapping("/order") + fun createOrder(@RequestBody request: OrderRequest) { + orderService.createOrder(request) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OrderEntity.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OrderEntity.kt new file mode 100644 index 0000000..83c5ad9 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OrderEntity.kt @@ -0,0 +1,25 @@ +package com.coded.spring.ordering.orders + +import com.coded.spring.com.coded.spring.ordering.items.ItemEntity +import jakarta.persistence.* + +@Entity +@Table(name = "orders") +data class OrderEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + + val userId: Long, + //If the relationship is: Many Orders can belong to One User, then the User will be brought to the OrderEntity as an Object or as a whole UserEntity + + val restaurant: String, + + @OneToMany(mappedBy = "order", cascade = [CascadeType.ALL]) + var items: List = listOf() + //If the relationship is: One Order can have Many Items, then the Items will be brought to the OrderEntity as a List + //This lets the order list its items, but the actual foreign key is in the items table, not the orders table (only the child "the Many" holds the foreign key) + // this is saying: If I get an order from the database, and I want to see its items, look in the items table and find all the rows where order_id = this order’s id +){ + constructor() : this(null, 1, "", listOf()) +} diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OrderRepository.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OrderRepository.kt new file mode 100644 index 0000000..ac07b7c --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OrderRepository.kt @@ -0,0 +1,11 @@ +package com.coded.spring.ordering.orders + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface OrdersRepository : JpaRepository{ + fun findByUserId(userId: Long): List +} + + diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OrderRequest.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OrderRequest.kt new file mode 100644 index 0000000..846137a --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OrderRequest.kt @@ -0,0 +1,12 @@ +package com.coded.spring.com.coded.spring.ordering.orders + +data class OrderRequest( + val userId: Long, + val restaurant: String, + val items: List +) + +data class Item( + val name: String, + val quantity: Int +) \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/orders/OrderService.kt b/src/main/kotlin/com/coded/spring/ordering/orders/OrderService.kt new file mode 100644 index 0000000..fadc50e --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/orders/OrderService.kt @@ -0,0 +1,31 @@ +package com.coded.spring.ordering + +import com.coded.spring.com.coded.spring.ordering.items.ItemEntity +import com.coded.spring.com.coded.spring.ordering.orders.OrderRequest +import com.coded.spring.ordering.items.ItemsRepository +import com.coded.spring.ordering.orders.OrderEntity +import com.coded.spring.ordering.orders.OrdersRepository +import com.coded.spring.ordering.users.UserRepository +import org.springframework.stereotype.Service + +@Service +class OrderService( + private val ordersRepository: OrdersRepository, + private val userRepository: UserRepository, + private val itemsRepository: ItemsRepository +) { + + fun createOrder(request: OrderRequest) { + + val newOrder = OrderEntity( + userId = request.userId, + restaurant = request.restaurant + ).apply { + items = request.items.map { + ItemEntity(name = it.name, quantity = it.quantity, order = this) + } + } + + ordersRepository.save(newOrder) + } +} diff --git a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileController.kt b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileController.kt new file mode 100644 index 0000000..c85e02e --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileController.kt @@ -0,0 +1,19 @@ +package com.coded.spring.ordering.profiles + + +import org.springframework.http.ResponseEntity +import org.springframework.security.core.Authentication +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController + +@RestController +class ProfileController( + private val profileService: ProfileService +) { + + @PostMapping("/profile") + fun createProfile(@RequestBody request: ProfileRequest, authentication: Authentication): ResponseEntity { + return profileService.createProfile(request, authentication.name) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileEntity.kt b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileEntity.kt new file mode 100644 index 0000000..3195cac --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileEntity.kt @@ -0,0 +1,30 @@ +package com.coded.spring.ordering.profiles + +import com.coded.spring.ordering.users.UserEntity +import jakarta.persistence.* + +@Entity +@Table(name = "profiles") +data class ProfileEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0, + + val firstName: String, + + val lastName: String, + + val phoneNumber: String, + + @OneToOne + @JoinColumn(name = "user_id", referencedColumnName = "id") + val user:UserEntity? = null, + +) { constructor() : this( +id = 0, +firstName = "", +lastName = "", +phoneNumber = "", + +) } + diff --git a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileRepository.kt b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileRepository.kt new file mode 100644 index 0000000..79e9c08 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileRepository.kt @@ -0,0 +1,10 @@ +package com.coded.spring.ordering.profiles + +import com.coded.spring.ordering.users.UserEntity +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface ProfileRepository : JpaRepository { + fun existsByUser(user: UserEntity): Boolean +} diff --git a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileRequest.kt b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileRequest.kt new file mode 100644 index 0000000..e3d3408 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileRequest.kt @@ -0,0 +1,7 @@ +package com.coded.spring.ordering.profiles + +data class ProfileRequest( + val firstName: String, + val lastName: String, + val phoneNumber: String +) diff --git a/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileService.kt b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileService.kt new file mode 100644 index 0000000..75020a0 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/profiles/ProfileService.kt @@ -0,0 +1,33 @@ +package com.coded.spring.ordering.profiles + +import com.coded.spring.ordering.users.UserRepository +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Service + +@Service +class ProfileService( + private val userRepository: UserRepository, + private val profileRepository: ProfileRepository +) { + + fun createProfile(request: ProfileRequest, username: String): ResponseEntity { + val user = userRepository.findByUsername(username) + ?: return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("User not found") + + if (profileRepository.existsByUser(user)) { + return ResponseEntity.status(HttpStatus.CONFLICT).body("Profile already exists") + } + + val userId = user.id + + val profile = ProfileEntity( + firstName = request.firstName, + lastName = request.lastName, + phoneNumber = request.phoneNumber + ) + + profileRepository.save(profile) + return ResponseEntity.ok("Profile created successfully") + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UserController.kt b/src/main/kotlin/com/coded/spring/ordering/users/UserController.kt new file mode 100644 index 0000000..2dc9a14 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/users/UserController.kt @@ -0,0 +1,38 @@ +package com.coded.spring.ordering.users + +import com.coded.spring.com.coded.spring.ordering.users.UserService +import org.springframework.http.ResponseEntity +import org.springframework.security.authentication.BadCredentialsException +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 +class UserController( + private val userService: UserService +) { + @PostMapping("/auth/register") + fun register(@RequestBody authRequest: AuthenticationRequest): ResponseEntity { + return try { + val response = userService.register(authRequest) + ResponseEntity.ok(response) + } catch (e: IllegalArgumentException) { + ResponseEntity.badRequest().body(e.message) + } catch (e: BadCredentialsException) { + ResponseEntity.status(401).body(e.message) + } + + } + + @PostMapping("/auth/login") + fun login(@RequestBody authRequest: AuthenticationRequest): ResponseEntity { + return try { + val response = userService.login(authRequest) + ResponseEntity.ok(response) + } catch (e: BadCredentialsException) { + ResponseEntity.status(401).body(mapOf("error" to e.message)) + } + } +} + diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UserEntity.kt b/src/main/kotlin/com/coded/spring/ordering/users/UserEntity.kt new file mode 100644 index 0000000..1c1a82c --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/users/UserEntity.kt @@ -0,0 +1,32 @@ +package com.coded.spring.ordering.users + +import com.coded.spring.ordering.profiles.ProfileEntity +import jakarta.persistence.* + + +@Entity +@Table(name = "users") +data class UserEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, + + val name: String = "", + + var username: String, + var password: String, + + @Enumerated(EnumType.STRING) + val role: Roles = Roles.USER, + + @OneToOne(mappedBy = "user") + val profile: ProfileEntity? = null + + +) { + constructor() : this(null, "", "", "",Roles.USER, ProfileEntity()) +} + +enum class Roles { + USER, ADMIN +} diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UserRepository.kt b/src/main/kotlin/com/coded/spring/ordering/users/UserRepository.kt new file mode 100644 index 0000000..6ad7040 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/users/UserRepository.kt @@ -0,0 +1,9 @@ +package com.coded.spring.ordering.users + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface UserRepository : JpaRepository { + fun findByUsername(username: String): UserEntity? +} diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UserRequest.kt b/src/main/kotlin/com/coded/spring/ordering/users/UserRequest.kt new file mode 100644 index 0000000..85d8620 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/users/UserRequest.kt @@ -0,0 +1,10 @@ +package com.coded.spring.ordering.users + +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/users/UserService.kt b/src/main/kotlin/com/coded/spring/ordering/users/UserService.kt new file mode 100644 index 0000000..a608d45 --- /dev/null +++ b/src/main/kotlin/com/coded/spring/ordering/users/UserService.kt @@ -0,0 +1,64 @@ +package com.coded.spring.com.coded.spring.ordering.users + + +import com.coded.spring.com.coded.spring.ordering.authentication.jwt.JwtService +import com.coded.spring.ordering.users.AuthenticationRequest +import com.coded.spring.ordering.users.AuthenticationResponse +import com.coded.spring.ordering.users.UserEntity +import com.coded.spring.ordering.users.UserRepository +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.BadCredentialsException +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.stereotype.Service + +@Service +class UserService( + private val userRepository: UserRepository, + private val userDetailsService : UserDetailsService, + private val passwordEncoder : PasswordEncoder, + private val authenticationManager: AuthenticationManager, + private val jwtService: JwtService +) { + fun register(authRequest: AuthenticationRequest): AuthenticationResponse { + + if (authRequest.username.isBlank()){ + throw IllegalArgumentException("Username must not be blank!") + } + + if (authRequest.password.length < 8) { + throw IllegalArgumentException("Password must be at least 8 characters long") + } + + val newUser = UserEntity( + username = authRequest.username, + password = passwordEncoder.encode(authRequest.password) + ) + userRepository.save(newUser) + + 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 BadCredentialsException("Could not authenticate user!") + } + } + + fun login(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 BadCredentialsException("Invalid credentials!") + } + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3704dc6..d43cb53 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,9 @@ spring.application.name=Kotlin.SpringbootV2 + + +server.port=8082 + +spring.datasource.url=jdbc:postgresql://localhost:5433/orders +spring.datasource.username=postgres +spring.datasource.password=1193 +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect \ No newline at end of file diff --git a/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt b/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt index b2e2320..9365c1b 100644 --- a/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt +++ b/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt @@ -1,13 +1,24 @@ package com.coded.spring.ordering +import org.junit.jupiter.api.Assertions.assertEquals 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.HttpStatus -@SpringBootTest +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class ApplicationTests { - @Test - fun contextLoads() { - } +// @Autowired +// lateinit var restTemplate: TestRestTemplate +// +// @Test +// fun helloWorld() { +// val result = restTemplate.getForEntity("/hello", String::class.java) +// assertEquals("Hello World",result.body) +// assertEquals(HttpStatus.OK,result?.statusCode) +// } + }