From e76623a8ed078813e306d85fbd7b82198a609ce9 Mon Sep 17 00:00:00 2001 From: SOR <3272194354@qq.com> Date: Mon, 27 Jul 2026 13:23:12 +0800 Subject: [PATCH] refactor: AccountService --- .../controller/v1/AuthorizeController.kt | 23 +- .../entity/dto/EmailVerificationMsg.kt | 3 +- .../itmanager/entity/enums/AccountRole.kt | 16 + .../itmanager/entity/enums/EmailType.kt | 13 + .../itmanager/listener/MailQueueListener.kt | 7 +- .../service/impl/AccountServiceImpl.kt | 291 +++++++----------- .../service/v1/AuthorizationService.kt | 148 +++++++++ 7 files changed, 305 insertions(+), 196 deletions(-) create mode 100644 src/beckend/src/main/kotlin/club/itstudio/itmanager/entity/enums/AccountRole.kt create mode 100644 src/beckend/src/main/kotlin/club/itstudio/itmanager/entity/enums/EmailType.kt create mode 100644 src/beckend/src/main/kotlin/club/itstudio/itmanager/service/v1/AuthorizationService.kt diff --git a/src/beckend/src/main/kotlin/club/itstudio/itmanager/controller/v1/AuthorizeController.kt b/src/beckend/src/main/kotlin/club/itstudio/itmanager/controller/v1/AuthorizeController.kt index 8f1389f..9ca1444 100644 --- a/src/beckend/src/main/kotlin/club/itstudio/itmanager/controller/v1/AuthorizeController.kt +++ b/src/beckend/src/main/kotlin/club/itstudio/itmanager/controller/v1/AuthorizeController.kt @@ -1,21 +1,16 @@ package club.itstudio.itmanager.controller.v1 import club.itstudio.itmanager.entity.ApiResponse +import club.itstudio.itmanager.entity.enums.EmailType import club.itstudio.itmanager.entity.vo.request.EmailRegisterVO import club.itstudio.itmanager.entity.vo.request.PasswordResetVO -import club.itstudio.itmanager.service.AccountService +import club.itstudio.itmanager.service.v1.AuthorizationService import jakarta.validation.Valid import jakarta.validation.constraints.Email import jakarta.validation.constraints.NotBlank -import jakarta.validation.constraints.Pattern import org.slf4j.LoggerFactory import org.springframework.validation.annotation.Validated -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.RequestParam -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* import org.springframework.web.server.ServerWebExchange import reactor.core.publisher.Mono @@ -23,34 +18,34 @@ import reactor.core.publisher.Mono @RestController @RequestMapping("/api/v1/auth") class AuthorizeController( - private val service: AccountService + private val service: AuthorizationService ) { private val logger = LoggerFactory.getLogger(this::class.java) @GetMapping("/ask-code") fun askVerifyCode( @RequestParam @NotBlank @Email email: String, - @RequestParam @Pattern(regexp = "(register|reset)") type: String, + @RequestParam type: EmailType, exchange: ServerWebExchange ): Mono> { logger.info("Trying to ask code") val ip = exchange.request.remoteAddress?.address?.hostAddress ?: "unknown" return service.askEmailVerifyCode(type, email, ip) - .map { messageHandler(it) } + .map(::messageHandler) } @PostMapping("/register") fun emailRegister(@RequestBody @Valid vo: EmailRegisterVO): Mono> { logger.info("Trying to register") return service.registerEmailAccount(vo) - .map { messageHandler(it) } + .map(::messageHandler) } @PostMapping("/reset") fun emailResetPassword(@RequestBody @Valid vo: PasswordResetVO): Mono> { logger.info("Trying to reset password") return service.resetEmailAccountPassword(vo) - .map { messageHandler(it) } + .map(::messageHandler) } @GetMapping("/logout") @@ -71,7 +66,7 @@ class AuthorizeController( return service .jwtTokenRelogin(authorization) ?.map { ApiResponse.success(it) } - ?: Mono.just(messageHandler("Please login with password.")) + ?: Mono.just(ApiResponse.failure(401, null, "Please login with password.")) } private fun messageHandler(wrongMessage: String): ApiResponse { diff --git a/src/beckend/src/main/kotlin/club/itstudio/itmanager/entity/dto/EmailVerificationMsg.kt b/src/beckend/src/main/kotlin/club/itstudio/itmanager/entity/dto/EmailVerificationMsg.kt index 791a98c..f309845 100644 --- a/src/beckend/src/main/kotlin/club/itstudio/itmanager/entity/dto/EmailVerificationMsg.kt +++ b/src/beckend/src/main/kotlin/club/itstudio/itmanager/entity/dto/EmailVerificationMsg.kt @@ -1,10 +1,11 @@ package club.itstudio.itmanager.entity.dto +import club.itstudio.itmanager.entity.enums.EmailType import kotlinx.serialization.Serializable @Serializable data class EmailVerificationMsg( val email: String, - val type: String, + val type: EmailType, val code: String ) \ No newline at end of file diff --git a/src/beckend/src/main/kotlin/club/itstudio/itmanager/entity/enums/AccountRole.kt b/src/beckend/src/main/kotlin/club/itstudio/itmanager/entity/enums/AccountRole.kt new file mode 100644 index 0000000..050128e --- /dev/null +++ b/src/beckend/src/main/kotlin/club/itstudio/itmanager/entity/enums/AccountRole.kt @@ -0,0 +1,16 @@ +package club.itstudio.itmanager.entity.enums + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +enum class AccountRole { + @SerialName("user") + USER, + @SerialName("admin") + ADMIN; + + companion object { + fun fromIsStaff(isStaff: Boolean): AccountRole = if (isStaff) ADMIN else USER + } +} diff --git a/src/beckend/src/main/kotlin/club/itstudio/itmanager/entity/enums/EmailType.kt b/src/beckend/src/main/kotlin/club/itstudio/itmanager/entity/enums/EmailType.kt new file mode 100644 index 0000000..db95edf --- /dev/null +++ b/src/beckend/src/main/kotlin/club/itstudio/itmanager/entity/enums/EmailType.kt @@ -0,0 +1,13 @@ +package club.itstudio.itmanager.entity.enums + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +enum class EmailType { + @SerialName("register") + REGISTER, + + @SerialName("reset") + RESET, +} \ No newline at end of file diff --git a/src/beckend/src/main/kotlin/club/itstudio/itmanager/listener/MailQueueListener.kt b/src/beckend/src/main/kotlin/club/itstudio/itmanager/listener/MailQueueListener.kt index ea0d601..1a9b526 100644 --- a/src/beckend/src/main/kotlin/club/itstudio/itmanager/listener/MailQueueListener.kt +++ b/src/beckend/src/main/kotlin/club/itstudio/itmanager/listener/MailQueueListener.kt @@ -1,6 +1,7 @@ package club.itstudio.itmanager.listener import club.itstudio.itmanager.entity.dto.EmailVerificationMsg +import club.itstudio.itmanager.entity.enums.EmailType import kotlinx.serialization.json.Json import org.slf4j.LoggerFactory import org.springframework.amqp.rabbit.annotation.RabbitHandler @@ -28,21 +29,19 @@ class MailQueueListener( val (email, type, code) = data val message = when (type) { - "register" -> createMailMessage( + EmailType.REGISTER -> createMailMessage( "Welcome to Our Service", "Thank you for registering! Your verification code is: $code" + "\nThis code is valid for 3 minutes.", email ) - "reset" -> createMailMessage( + EmailType.RESET -> createMailMessage( "Password Reset Request", "You requested a password reset. Your verification code is: $code" + "\nThis code is valid for 3 minutes.", email ) - - else -> return } sender.send(message) logger.info("Email sent to $email.") diff --git a/src/beckend/src/main/kotlin/club/itstudio/itmanager/service/impl/AccountServiceImpl.kt b/src/beckend/src/main/kotlin/club/itstudio/itmanager/service/impl/AccountServiceImpl.kt index e909420..4f1f762 100644 --- a/src/beckend/src/main/kotlin/club/itstudio/itmanager/service/impl/AccountServiceImpl.kt +++ b/src/beckend/src/main/kotlin/club/itstudio/itmanager/service/impl/AccountServiceImpl.kt @@ -1,207 +1,144 @@ package club.itstudio.itmanager.service.impl -import com.baomidou.mybatisplus.core.toolkit.Wrappers -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl import club.itstudio.itmanager.entity.dto.Account -import club.itstudio.itmanager.entity.dto.EmailVerificationMsg -import club.itstudio.itmanager.entity.vo.request.EmailRegisterVO -import club.itstudio.itmanager.entity.vo.request.PasswordResetVO -import club.itstudio.itmanager.entity.vo.response.ReloginVO +import club.itstudio.itmanager.entity.enums.AccountRole import club.itstudio.itmanager.mapper.AccountMapper import club.itstudio.itmanager.service.AccountService -import club.itstudio.itmanager.utils.Const - -import club.itstudio.itmanager.utils.FlowUtils -import club.itstudio.itmanager.utils.JwtUtils -import org.springframework.amqp.core.AmqpTemplate -import org.springframework.data.redis.core.ReactiveStringRedisTemplate +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl +import org.slf4j.LoggerFactory import org.springframework.security.core.userdetails.User import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UsernameNotFoundException -import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Service import reactor.core.publisher.Mono import reactor.core.scheduler.Schedulers -import java.time.Duration +import javax.security.auth.login.AccountNotFoundException +import kotlin.jvm.java @Service -class AccountServiceImpl( - private val utils: FlowUtils, - private val jwtUtils: JwtUtils, - private val amqpTemplate: AmqpTemplate, - private val stringRedisTemplate: ReactiveStringRedisTemplate, - private val encoder: PasswordEncoder -) : ServiceImpl(), AccountService { +class AccountServiceImpl : ServiceImpl(), AccountService { - override fun findByUsername(username: String): Mono { - return findAccountByNameOrEmail(username) - .switchIfEmpty(Mono.error(UsernameNotFoundException("Account with name $username not found"))) - .map { account -> - User.withUsername(username) - .password(account.password) - .roles(if (account.isStaff) "STAFF" else "USER") + private val logger = LoggerFactory.getLogger(this::class.java) + + override fun findByUsername( + username: String + ): Mono { + + return findAccountByName(username) + .map { + User.withUsername(it.username) + .password(it.password) + .roles(AccountRole.fromIsStaff(it.isStaff).name) .build() } + .onErrorMap( + AccountNotFoundException::class.java + ) { + UsernameNotFoundException( + "User not found" + ) + } } - override fun findAccountByNameOrEmail(text: String): Mono { - return Mono.fromCallable { - this.query() - .eq("username", text).or() - .eq("email", text) + override fun findAccountByName(username: String): Mono = + findAccount("username") { + ktQuery() + .eq(Account::username, username) .one() - }.flatMap { account -> - Mono.just(account) } + + override fun findAccountByEmail(email: String): Mono = + findAccount("email") { + ktQuery() + .eq(Account::email, email) + .one() + } + + private fun blockingCall( + block: () -> T? + ): Mono = + Mono.fromCallable(block) + .subscribeOn(Schedulers.boundedElastic()) + + private fun findAccount( + fieldName: String, + query: () -> Account? + ): Mono { + return blockingCall { + logger.info( + "Searching account by {}", + fieldName + ) + query() + }.switchIfEmpty( + Mono.error( + AccountNotFoundException() + ) + ) } - override fun askEmailVerifyCode(type: String, email: String, ip: String): Mono { - return verifyLimit(ip).flatMap { allowed -> - if (allowed) { - val code = (100000..999999).random().toString() - val data = EmailVerificationMsg(email, type, code) - - Mono.fromRunnable { - amqpTemplate.convertAndSend("mail", data) - }.then( - stringRedisTemplate.opsForValue() - .set( - Const.VERIFY_EMAIL_DATA + email, - code, - Duration.ofMinutes(3) - ) - ).thenReturn("").subscribeOn(Schedulers.boundedElastic()) - } else { - Mono.just("Request limit exceeded. Please try again later.") - } - } - } - - override fun registerEmailAccount(vo: EmailRegisterVO): Mono { - val (email, code, username, password) = vo - - return verifyCode(email, code).flatMap { error -> - if (error.isNotBlank()) { - return@flatMap Mono.just(error) - } - - existAccountByEmail(email).flatMap { emailExists -> - if (emailExists) { - return@flatMap Mono.just("account with the same email already exists.") + override fun findAccountByNameOrEmail( + text: String + ): Mono { + return blockingCall { + logger.info( + "Searching account by identifier" + ) + ktQuery() + .and { + it.eq(Account::email, text) + .or() + .eq(Account::username, text) } + .one() + }.switchIfEmpty( + Mono.error(AccountNotFoundException()) + ) + } - existAccountByUsername(username).flatMap { usernameExists -> - if (usernameExists) { - return@flatMap Mono.just("username already exists.") - } + override fun existAccountByName( + username: String + ): Mono { + return blockingCall { + ktQuery() + .eq(Account::username, username) + .exists() - val encodedPassword = encoder.encode(password) - val account = Account( - username = username, - password = encodedPassword!!, - email = email, - isActive = true, - isStaff = false + } + } + + override fun existAccountByEmail( + email: String + ): Mono { + return blockingCall { + ktQuery() + .eq(Account::email, email) + .exists() + + } + } + + override fun resetPasswordByEmail( + email: String, + encodedPassword: String + ): Mono { + return blockingCall { + ktUpdate() + .eq(Account::email, email) + .set( + Account::password, + encodedPassword + ) + .update() + } + .flatMap { updated -> + if (updated) { + Mono.empty() + } else { + Mono.error( + AccountNotFoundException() ) - - Mono.fromCallable { this.save(account) } - .flatMap { success -> - if (success) { - stringRedisTemplate.delete(Const.VERIFY_EMAIL_DATA + email) - .thenReturn("") - } else { - Mono.just("something went wrong. Please contact the administrator.") - } - } } } - } - } - - override fun resetEmailAccountPassword(vo: PasswordResetVO): Mono { - val (email, code, password) = vo - - return verifyCode(email, code).flatMap { error -> - if (error.isNotBlank()) { - return@flatMap Mono.just(error) - } - - existAccountByEmail(email).flatMap { exists -> - if (!exists) { - return@flatMap Mono.just("account with the email does not exist.") - } - - val encodedPassword = encoder.encode(password) - - Mono.fromCallable { - this.update() - .eq("email", email) - .set("password", encodedPassword) - .update() - }.flatMap { success -> - if (success) { - stringRedisTemplate.delete(Const.VERIFY_EMAIL_DATA + email) - .thenReturn("") - } else { - Mono.just("something went wrong. Please contact the administrator.") - } - } - } - } - } - - override fun jwtTokenRelogin(headerToken: String?): Mono? { - val user = jwtUtils.resolveJwt(headerToken?.substring("Bearer ".length)) - ?.let { jwtUtils.toUser(it) } - ?: return null - val accountMono = findAccountByNameOrEmail(user.username) - val voMono = accountMono.map { - ReloginVO( - jwtUtils.createJwt(user, it.id!!, it.username), - jwtUtils.expiresTime().toInstant().toString() - ) - } - return voMono - } - - override fun invalidateJwt(headerToken: String?): Boolean { - return jwtUtils.invalidateJwt(headerToken) - } - - private fun existAccountByEmail(email: String): Mono { - return Mono.fromCallable { - this.baseMapper.exists( - Wrappers.query() - .eq("email", email) - ) - } - } - - private fun existAccountByUsername(username: String): Mono { - return Mono.fromCallable { - this.baseMapper.exists( - Wrappers.query() - .eq("username", username) - ) - } - } - - private fun verifyLimit(ip: String): Mono { - val key = Const.VERIFY_EMAIL_LIMIT + ip - return utils.limitOnceCheck(key, 60) - } - - private fun verifyCode(email: String, receivedCode: String?): Mono { - return stringRedisTemplate.opsForValue() - .get(Const.VERIFY_EMAIL_DATA + email) - .flatMap { code -> - when { - receivedCode == null || code != receivedCode -> - Mono.just("verify code is wrong.") - - else -> Mono.just("") - } - } - .switchIfEmpty(Mono.just("verify code has not been sent")) } } \ No newline at end of file diff --git a/src/beckend/src/main/kotlin/club/itstudio/itmanager/service/v1/AuthorizationService.kt b/src/beckend/src/main/kotlin/club/itstudio/itmanager/service/v1/AuthorizationService.kt new file mode 100644 index 0000000..e84d1ff --- /dev/null +++ b/src/beckend/src/main/kotlin/club/itstudio/itmanager/service/v1/AuthorizationService.kt @@ -0,0 +1,148 @@ +package club.itstudio.itmanager.service.v1 + +import club.itstudio.itmanager.entity.dto.Account +import club.itstudio.itmanager.entity.dto.EmailVerificationMsg +import club.itstudio.itmanager.entity.enums.EmailType +import club.itstudio.itmanager.entity.vo.request.EmailRegisterVO +import club.itstudio.itmanager.entity.vo.request.PasswordResetVO +import club.itstudio.itmanager.entity.vo.response.ReloginVO +import club.itstudio.itmanager.service.AccountService +import club.itstudio.itmanager.utils.Const +import club.itstudio.itmanager.utils.DateUtils.getCurrentDateTime +import club.itstudio.itmanager.utils.FlowUtils +import club.itstudio.itmanager.utils.JwtUtils +import org.springframework.amqp.core.AmqpTemplate +import org.springframework.data.redis.core.ReactiveStringRedisTemplate +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.stereotype.Service +import reactor.core.publisher.Mono +import reactor.core.scheduler.Schedulers +import java.time.Duration + +@Service +class AuthorizationService( + private val accountService: AccountService, + private val utils: FlowUtils, + private val jwtUtils: JwtUtils, + private val amqpTemplate: AmqpTemplate, + private val stringRedisTemplate: ReactiveStringRedisTemplate, + private val encoder: PasswordEncoder +) { + fun askEmailVerifyCode(type: EmailType, email: String, ip: String): Mono { + return verifyLimit(ip).flatMap { allowed -> + if (allowed) { + val code = (100000..999999).random().toString() + val data = EmailVerificationMsg(email, type, code) + + Mono.fromRunnable { + amqpTemplate.convertAndSend("mail", data) + }.then( + stringRedisTemplate.opsForValue() + .set( + Const.VERIFY_EMAIL_DATA + email, + code, + Duration.ofMinutes(3) + ) + ).thenReturn("").subscribeOn(Schedulers.boundedElastic()) + } else { + Mono.just("Request limit exceeded. Please try again later.") + } + } + } + + fun registerEmailAccount(vo: EmailRegisterVO): Mono { + val (email, code, username, password) = vo + + return verifyCode(email, code) + .flatMap { result -> + if (result.isNotBlank()) { + return@flatMap Mono.just(result) + } + accountService.existAccountByEmail(email) + .flatMap { emailExists -> + if (emailExists) { + return@flatMap Mono.just("account with the same email already exists.") + } + accountService.existAccountByName(username) + .flatMap { usernameExists -> + if (usernameExists) { + return@flatMap Mono.just("username already exists.") + } + val account = Account( + id = null, + username = username, + password = encoder.encode(password)!!, + email = email, + isStaff = false, + createdAt = getCurrentDateTime() + ) + Mono.fromCallable { accountService.save(account) } + .flatMap { success -> + if (success) { + stringRedisTemplate.delete(Const.VERIFY_EMAIL_DATA + email) + .thenReturn("") + } else { + Const.INTERNAL_ERROR_MONO + } + } + } + } + } + } + + fun resetEmailAccountPassword(vo: PasswordResetVO): Mono { + val (email, code, password) = vo + + val encodedPassword = + encoder.encode(password) + ?: return Const.INTERNAL_ERROR_MONO + + return verifyCode(email, code) + .then( + accountService.resetPasswordByEmail( + email, + encodedPassword + ) + ) + .then( + stringRedisTemplate.delete( + Const.VERIFY_EMAIL_DATA + email + ) + ) + .thenReturn("Password reset successfully") + } + + fun jwtTokenRelogin(headerToken: String?): Mono? { + val user = jwtUtils.resolveJwt(headerToken?.substring("Bearer ".length)) + ?.let { jwtUtils.toUser(it) } + ?: return null + val accountMono = accountService.findAccountByName(user.username) + val voMono = accountMono.map { + ReloginVO( + jwtUtils.createJwt(user, it.id!!, it.username), + jwtUtils.expiresTime().toInstant().toString() + ) + } + return voMono + } + + fun invalidateJwt(headerToken: String?): Boolean { + return jwtUtils.invalidateJwt(headerToken) + } + + private fun verifyLimit(ip: String): Mono { + val key = Const.VERIFY_EMAIL_LIMIT + ip + return utils.limitOnceCheck(key, 60) + } + + private fun verifyCode(email: String, receivedCode: String?): Mono { + return stringRedisTemplate.opsForValue() + .get(Const.VERIFY_EMAIL_DATA + email) + .flatMap { code -> + if (receivedCode == null || code != receivedCode) + Mono.just("verify code is wrong.") + else Mono.just("") + } + .switchIfEmpty(Mono.just("verify code has not been sent")) + } +} \ No newline at end of file