UserService.kt

package delta.codecharacter.server.user

import delta.codecharacter.dtos.CompleteProfileRequestDto
import delta.codecharacter.dtos.RegisterUserRequestDto
import delta.codecharacter.dtos.UpdatePasswordRequestDto
import delta.codecharacter.server.exception.CustomException
import delta.codecharacter.server.user.activate_user.ActivateUserService
import delta.codecharacter.server.user.public_user.PublicUserService
import delta.codecharacter.server.user.rating_history.RatingHistoryService
import org.bson.json.JsonObject
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Lazy
import org.springframework.dao.DuplicateKeyException
import org.springframework.http.HttpStatus
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.stereotype.Service
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.util.Optional
import java.util.UUID

@Service
class UserService(
    @Autowired private val userRepository: UserRepository,
    @Autowired private val publicUserService: PublicUserService,
    @Autowired private val ratingHistoryService: RatingHistoryService,
    @Autowired private val activateUserService: ActivateUserService
) : UserDetailsService {

    @Lazy @Autowired private lateinit var passwordEncoder: BCryptPasswordEncoder
    @Value("\${environment.reCaptcha-key}") private lateinit var secretKey: String

    override fun loadUserByUsername(email: String?): UserEntity {
        if (email == null) {
            throw UsernameNotFoundException("User not found")
        }
        val user = userRepository.findFirstByEmail(email)
        if (user.isEmpty) {
            throw UsernameNotFoundException("User not found")
        } else if (!user.get().isEnabled) {
            throw CustomException(HttpStatus.UNAUTHORIZED, "Email not verified")
        } else if (!user.get().isAccountNonExpired) {
            throw CustomException(HttpStatus.UNAUTHORIZED, "Account expired")
        } else {
            return user.get()
        }
    }

    private fun createUserWithPassword(userId: UUID, password: String, email: String) {
        val user =
            UserEntity(
                id = userId,
                password = passwordEncoder.encode(password),
                email = email,
                loginType = LoginType.PASSWORD,
                isProfileComplete = true,
                isEnabled = false,
                isAccountNonExpired = true,
                isAccountNonLocked = true,
                isCredentialsNonExpired = true,
                userAuthorities = mutableListOf(SimpleGrantedAuthority("ROLE_USER"))
            )
        userRepository.save(user)
    }

    fun createUserWithOAuth(email: String, oauthProvider: LoginType): UserEntity {
        val userId = UUID.randomUUID()
        val user =
            UserEntity(
                id = userId,
                password = passwordEncoder.encode(UUID.randomUUID().toString()),
                email = email,
                loginType = oauthProvider,
                isProfileComplete = false,
                isEnabled = true,
                isAccountNonExpired = true,
                isAccountNonLocked = true,
                isCredentialsNonExpired = true,
                userAuthorities = mutableListOf(SimpleGrantedAuthority("ROLE_USER_INCOMPLETE_PROFILE"))
            )
        ratingHistoryService.create(userId)
        return userRepository.save(user)
    }

    fun getUserByEmail(email: String): Optional<UserEntity> {
        return userRepository.findFirstByEmail(email)
    }

    fun updateUserPassword(userId: UUID, password: String) {
        val user = userRepository.findById(userId).get()
        userRepository.save(user.copy(password = passwordEncoder.encode(password)))
    }

    fun verifyUserPassword(userId: UUID, password: String): Boolean {
        val user = userRepository.findById(userId)
        return passwordEncoder.matches(password, user.get().password)
    }

    fun updatePassword(userId: UUID, updatePasswordRequestDto: UpdatePasswordRequestDto) {
        val (oldPassword, password, passwordConfirmation) = updatePasswordRequestDto
        if (password != passwordConfirmation) {
            throw CustomException(HttpStatus.BAD_REQUEST, "Passwords do not match")
        }
        if (verifyUserPassword(userId, oldPassword)) {
            val user = userRepository.findById(userId).get()
            userRepository.save(user.copy(password = passwordEncoder.encode(password)))
        } else {
            throw CustomException(HttpStatus.BAD_REQUEST, "Old password is incorrect")
        }
    }

    fun registerUser(registerUserRequestDto: RegisterUserRequestDto) {
        val (
            username,
            name,
            email,
            password,
            passwordConfirmation,
            country,
            college,
            avatarId,
            recaptchaCode
        ) =
            registerUserRequestDto

        if (username.trim().length < 5) {
            throw CustomException(HttpStatus.BAD_REQUEST, "Username must be minimum 5 characters long")
        }
        if (name.trim().length < 5) {
            throw CustomException(HttpStatus.BAD_REQUEST, "Name must be minimum 5 characters long")
        }
        if (avatarId !in 0..19) {
            throw CustomException(HttpStatus.BAD_REQUEST, "Selected Avatar is invalid")
        }
        if (college.trim().isEmpty()) {
            throw CustomException(HttpStatus.BAD_REQUEST, "College can not be empty")
        }
        if (country.trim().isEmpty()) {
            throw CustomException(HttpStatus.BAD_REQUEST, "Country can not be empty")
        }
        if (password != passwordConfirmation) {
            throw CustomException(
                HttpStatus.BAD_REQUEST, "Password and password confirmation don't match"
            )
        }

        if (!publicUserService.isUsernameUnique(username)) {
            throw CustomException(HttpStatus.BAD_REQUEST, "Username already taken")
        }

        if (!verifyReCaptcha(recaptchaCode))
            throw CustomException(HttpStatus.BAD_REQUEST, "Invalid ReCaptcha")

        val userId = UUID.randomUUID()
        try {
            createUserWithPassword(userId, password, email)
            publicUserService.create(userId, username, name, country, college, avatarId)
            ratingHistoryService.create(userId)
            activateUserService.sendActivationToken(userId, name, email)
        } catch (duplicateError: DuplicateKeyException) {
            throw CustomException(HttpStatus.BAD_REQUEST, "Username/Email already exists")
        }
    }

    fun verifyReCaptcha(reCaptchaResponse: String): Boolean {
        val url =
            "https://www.google.com/recaptcha/api/siteverify?secret=$secretKey&response=$reCaptchaResponse"
        try {
            val client = HttpClient.newBuilder().build()
            val request =
                HttpRequest.newBuilder()
                    .uri(URI.create(url))
                    .POST(HttpRequest.BodyPublishers.noBody())
                    .build()
            val response = client.send(request, HttpResponse.BodyHandlers.ofString())
            val json = JsonObject(response.body()).toBsonDocument()
            return (
                json.getBoolean("success").value &&
                    (json.getDouble("score").value.compareTo(0.5) >= 0)
                )
        } catch (e: Exception) {
            e.printStackTrace()
            return false
        }
    }

    fun activateUser(userId: UUID, token: String) {
        activateUserService.processActivationToken(userId, token)
        val user = userRepository.findFirstById((userId)).get()
        val activatedUser = user.copy(isEnabled = true)
        userRepository.save(activatedUser)
    }

    fun completeUserProfile(userId: UUID, completeProfileRequestDto: CompleteProfileRequestDto) {
        val (username, name, country, college, avatarId) = completeProfileRequestDto
        val user = userRepository.findFirstById(userId).get()
        if (user.isProfileComplete) {
            throw CustomException(HttpStatus.BAD_REQUEST, "User profile is already complete")
        }
        if (!publicUserService.isUsernameUnique(username)) {
            throw CustomException(HttpStatus.BAD_REQUEST, "Username already taken")
        }
        publicUserService.create(userId, username, name, country, college, avatarId)
        userRepository.save(
            user.copy(
                isProfileComplete = true,
                userAuthorities = mutableListOf(SimpleGrantedAuthority("ROLE_USER"))
            )
        )
    }
}