Complete core functions #9

Merged
FatttSnake merged 171 commits from FatttSnake into dev 2024-02-23 11:56:35 +08:00
27 changed files with 188 additions and 36 deletions
Showing only changes of commit 15f631971c - Show all commits

View File

@@ -5,14 +5,12 @@ import org.aspectj.lang.annotation.AfterReturning
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Pointcut
import org.aspectj.lang.reflect.MethodSignature
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import top.fatweb.api.annotation.EventLogRecord
import top.fatweb.api.entity.system.EventLog
import top.fatweb.api.service.system.IEventLogService
import top.fatweb.api.util.WebUtil
import top.fatweb.api.vo.permission.LoginVo
import top.fatweb.api.vo.permission.RegisterVo
/**
* Event log record aspect
@@ -25,24 +23,21 @@ import top.fatweb.api.vo.permission.LoginVo
class EventLogAspect(
private val eventLogService: IEventLogService
) {
private val logger: Logger = LoggerFactory.getLogger(this::class.java)
@Pointcut("@annotation(top.fatweb.api.annotation.EventLogRecord)")
fun eventLogPointcut() {
}
@AfterReturning(value = "eventLogPointcut()", returning = "retValue")
fun doAfter(joinPoint: JoinPoint, retValue: Any) {
fun doAfter(joinPoint: JoinPoint, retValue: Any?) {
val annotation = (joinPoint.signature as MethodSignature).method.getAnnotation(EventLogRecord::class.java)
try {
eventLogService.save(EventLog().apply {
this.event = annotation.event
operateUserId = WebUtil.getLoginUserId()
?: if (retValue is LoginVo) retValue.userId else -1
})
} catch (e: Exception) {
logger.error("Cannot record event!!!", e)
val userId = WebUtil.getLoginUserId() ?: when (retValue) {
is LoginVo -> retValue.userId!!
is RegisterVo -> retValue.userId!!
else -> -1
}
eventLogService.saveEvent(annotation, userId)
}
}

View File

@@ -15,6 +15,7 @@ import top.fatweb.api.param.permission.VerifyParam
import top.fatweb.api.service.permission.IAuthenticationService
import top.fatweb.api.util.WebUtil
import top.fatweb.api.vo.permission.LoginVo
import top.fatweb.api.vo.permission.RegisterVo
import top.fatweb.api.vo.permission.TokenVo
/**
@@ -36,11 +37,11 @@ class AuthenticationController(
*/
@Operation(summary = "注册")
@PostMapping("/register")
fun register(@Valid @RequestBody registerParam: RegisterParam): ResponseResult<Nothing> {
authenticationService.register(registerParam)
fun register(@Valid @RequestBody registerParam: RegisterParam): ResponseResult<RegisterVo> = ResponseResult.success(
ResponseCode.PERMISSION_REGISTER_SUCCESS,
data = authenticationService.register(registerParam)
)
return ResponseResult.success(ResponseCode.PERMISSION_REGISTER_SUCCESS)
}
/**
* Send verify email

View File

@@ -7,11 +7,23 @@ import top.fatweb.api.properties.SecurityProperties
import top.fatweb.api.service.system.IStatisticsLogService
import top.fatweb.api.util.RedisUtil
/**
* Statistics scheduled tasks
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Component
class StatisticsCron(
private val redisUtil: RedisUtil,
private val statisticsLogService: IStatisticsLogService
) {
/**
* Auto record number of online users
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Scheduled(cron = "0 * * * * *")
fun onlineUserCount() {
statisticsLogService.save(StatisticsLog().apply {

View File

@@ -18,7 +18,7 @@ import java.time.LocalDateTime
@TableName("t_event_log")
class EventLog : Serializable {
enum class Event(@field:EnumValue @field:JsonValue val code: String) {
LOGIN("LOGIN"), LOGOUT("LOGOUT"), REGISTER("REGISTER"), API("API")
LOGIN("LOGIN"), LOGOUT("LOGOUT"), REGISTER("REGISTER"), VERIFY("VERIFY"), API("API")
}
/**

View File

@@ -1,3 +1,9 @@
package top.fatweb.api.exception
/**
* Account need initialize exception
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
class AccountNeedInitException : RuntimeException("Account need initialize")

View File

@@ -1,5 +1,11 @@
package top.fatweb.api.exception
/**
* Email settings not configured exception
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
class NoEmailConfigException(
vararg configs: String
) : RuntimeException("Email settings not configured: ${configs.joinToString(", ")}")

View File

@@ -1,3 +1,9 @@
package top.fatweb.api.exception
/**
* No verification required exception
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
class NoVerificationRequiredException : RuntimeException("No verification required")

View File

@@ -1,3 +1,9 @@
package top.fatweb.api.exception
/**
* Verification code error or expired exception
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
class VerificationCodeErrorOrExpiredException : RuntimeException("Verification code is error or has expired")

View File

@@ -4,5 +4,11 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper
import org.apache.ibatis.annotations.Mapper
import top.fatweb.api.entity.system.EventLog
/**
* Event log mapper
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Mapper
interface EventLogMapper : BaseMapper<EventLog>

View File

@@ -4,5 +4,11 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper
import org.apache.ibatis.annotations.Mapper
import top.fatweb.api.entity.system.StatisticsLog
/**
* Statistics log mapper
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Mapper
interface StatisticsLogMapper : BaseMapper<StatisticsLog>

View File

@@ -86,6 +86,7 @@ data class UserAddParam(
* @since 1.0.0
*/
@Schema(description = "昵称")
@field:NotBlank(message = "Nickname can not be blank")
val nickname: String?,
/**

View File

@@ -3,6 +3,12 @@ package top.fatweb.api.param.system
import com.baomidou.mybatisplus.annotation.EnumValue
import com.fasterxml.jackson.annotation.JsonValue
/**
* Get active information parameters
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
data class ActiveInfoGetParam(
val scope: Scope = Scope.WEAK
) {

View File

@@ -3,6 +3,12 @@ package top.fatweb.api.param.system
import com.baomidou.mybatisplus.annotation.EnumValue
import com.fasterxml.jackson.annotation.JsonValue
/**
* Get online information parameters
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
data class OnlineInfoGetParam(
val scope: Scope = Scope.WEAK
) {

View File

@@ -6,6 +6,7 @@ import top.fatweb.api.param.permission.LoginParam
import top.fatweb.api.param.permission.RegisterParam
import top.fatweb.api.param.permission.VerifyParam
import top.fatweb.api.vo.permission.LoginVo
import top.fatweb.api.vo.permission.RegisterVo
import top.fatweb.api.vo.permission.TokenVo
/**
@@ -21,7 +22,7 @@ interface IAuthenticationService {
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
fun register(registerParam: RegisterParam)
fun register(registerParam: RegisterParam): RegisterVo
/**
* Send verify email

View File

@@ -34,6 +34,7 @@ import top.fatweb.api.util.MailUtil
import top.fatweb.api.util.RedisUtil
import top.fatweb.api.util.WebUtil
import top.fatweb.api.vo.permission.LoginVo
import top.fatweb.api.vo.permission.RegisterVo
import top.fatweb.api.vo.permission.TokenVo
import java.io.StringWriter
import java.time.Instant
@@ -63,13 +64,14 @@ class AuthenticationServiceImpl(
) : IAuthenticationService {
private val logger: Logger = LoggerFactory.getLogger(this::class.java)
@EventLogRecord(EventLog.Event.REGISTER)
@Transactional
override fun register(registerParam: RegisterParam) {
override fun register(registerParam: RegisterParam): RegisterVo {
val user = User().apply {
username = registerParam.username
password = passwordEncoder.encode(registerParam.password)
verify =
"${LocalDateTime.now(ZoneOffset.UTC).toInstant(ZoneOffset.UTC).toEpochMilli()}-${UUID.randomUUID()}"
"${LocalDateTime.now(ZoneOffset.UTC).toInstant(ZoneOffset.UTC).toEpochMilli()}-${UUID.randomUUID()}-${UUID.randomUUID()}-${UUID.randomUUID()}"
locking = 0
enable = 1
}
@@ -82,6 +84,8 @@ class AuthenticationServiceImpl(
})
sendVerifyMail(user.username!!, "http://localhost:5173/verify?code=${user.verify!!}", registerParam.email!!)
return RegisterVo(userId = user.id)
}
@Transactional
@@ -91,7 +95,7 @@ class AuthenticationServiceImpl(
user.verify ?: throw NoVerificationRequiredException()
user.verify =
"${LocalDateTime.now(ZoneOffset.UTC).toInstant(ZoneOffset.UTC).toEpochMilli()}-${UUID.randomUUID()}"
"${LocalDateTime.now(ZoneOffset.UTC).toInstant(ZoneOffset.UTC).toEpochMilli()}-${UUID.randomUUID()}-${UUID.randomUUID()}-${UUID.randomUUID()}"
user.updateTime = LocalDateTime.now(ZoneOffset.UTC)
userService.updateById(user)
@@ -113,11 +117,12 @@ class AuthenticationServiceImpl(
template.merge(velocityContext, stringWriter)
MailUtil.sendSimpleMail(
"激活您的账号", stringWriter.toString(), true,
"验证您的账号", stringWriter.toString(), true,
email
)
}
@EventLogRecord(EventLog.Event.VERIFY)
@Transactional
override fun verify(verifyParam: VerifyParam) {
val user = userService.getById(WebUtil.getLoginUserId()) ?: throw AccessDeniedException("Access Denied")

View File

@@ -28,6 +28,7 @@ import top.fatweb.api.vo.permission.UserWithPasswordRoleInfoVo
import top.fatweb.api.vo.permission.UserWithRoleInfoVo
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.*
/**
* User service implement
@@ -111,9 +112,12 @@ class UserServiceImpl(
user.apply {
password = passwordEncoder.encode(rawPassword)
verify = if (userAddParam.verified) null else "${
LocalDateTime.now(ZoneOffset.UTC).toInstant(ZoneOffset.UTC).toEpochMilli()
}-${UUID.randomUUID()}-${UUID.randomUUID()}-${UUID.randomUUID()}"
}
if (baseMapper.insert(user) == 1) {
if (this.save(user)) {
user.userInfo?.let { userInfoService.save(it.apply { userId = user.id }) }
if (!user.roles.isNullOrEmpty()) {
@@ -175,9 +179,16 @@ class UserServiceImpl(
removeGroupIds.removeAll(addGroupIds)
oldGroupList.toSet().let { addGroupIds.removeAll(it) }
baseMapper.updateById(user)
baseMapper.update(
KtUpdateWrapper(User()).eq(User::id, user.id).set(User::expiration, user.expiration)
this.updateById(user)
this.update(
KtUpdateWrapper(User()).eq(User::id, user.id)
.set(
User::verify,
if (userUpdateParam.verified) null else "${
LocalDateTime.now(ZoneOffset.UTC).toInstant(ZoneOffset.UTC).toEpochMilli()
}-${UUID.randomUUID()}-${UUID.randomUUID()}-${UUID.randomUUID()}"
)
.set(User::expiration, user.expiration)
.set(User::credentialsExpiration, user.credentialsExpiration)
)
@@ -230,7 +241,7 @@ class UserServiceImpl(
throw AccessDeniedException("Access denied")
}
val user = baseMapper.selectById(userUpdatePasswordParam.id)
val user = this.getById(userUpdatePasswordParam.id)
user?.let {
val wrapper = KtUpdateWrapper(User())
wrapper.eq(User::id, user.id)
@@ -265,7 +276,7 @@ class UserServiceImpl(
return
}
baseMapper.deleteBatchIds(ids)
this.removeBatchByIds(ids)
userInfoService.remove(KtQueryWrapper(UserInfo()).`in`(UserInfo::userId, ids))
userRoleService.remove(KtQueryWrapper(UserRole()).`in`(UserRole::userId, ids))
userGroupService.remove(KtQueryWrapper(UserGroup()).`in`(UserGroup::userId, ids))

View File

@@ -1,6 +1,15 @@
package top.fatweb.api.service.system
import com.baomidou.mybatisplus.extension.service.IService
import top.fatweb.api.annotation.EventLogRecord
import top.fatweb.api.entity.system.EventLog
interface IEventLogService : IService<EventLog>
/**
* Event log service interface
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
interface IEventLogService : IService<EventLog> {
fun saveEvent(annotation: EventLogRecord, userId: Long)
}

View File

@@ -3,4 +3,10 @@ package top.fatweb.api.service.system
import com.baomidou.mybatisplus.extension.service.IService
import top.fatweb.api.entity.system.StatisticsLog
/**
* Statistics log service interface
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
interface IStatisticsLogService : IService<StatisticsLog>

View File

@@ -2,11 +2,30 @@ package top.fatweb.api.service.system.impl
import com.baomidou.dynamic.datasource.annotation.DS
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional
import top.fatweb.api.annotation.EventLogRecord
import top.fatweb.api.entity.system.EventLog
import top.fatweb.api.mapper.system.EventLogMapper
import top.fatweb.api.service.system.IEventLogService
@DS("sqlite")
@Service
class EventLogServiceImpl : ServiceImpl<EventLogMapper, EventLog>(), IEventLogService
class EventLogServiceImpl : ServiceImpl<EventLogMapper, EventLog>(), IEventLogService {
private val logger: Logger = LoggerFactory.getLogger(this::class.java)
@Transactional(propagation = Propagation.REQUIRES_NEW)
override fun saveEvent(annotation: EventLogRecord, userId: Long) {
try {
this.save(EventLog().apply {
this.event = annotation.event
operateUserId = userId
})
} catch (e: Exception) {
logger.error("Cannot record event!!!", e)
}
}
}

View File

@@ -223,6 +223,6 @@ class StatisticsServiceImpl(
)
}
return ActiveInfoVo(getHistory("REGISTER"), getHistory("LOGIN"))
return ActiveInfoVo(getHistory("REGISTER"), getHistory("LOGIN"), getHistory("VERIFY"))
}
}

View File

@@ -3,6 +3,12 @@ package top.fatweb.api.settings
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonValue
/**
* Type of mail security
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
enum class MailSecurityType(val code: String) {
NONE("None"),
SSL_TLS("SSL/TLS"),

View File

@@ -8,6 +8,12 @@ import top.fatweb.api.settings.MailSettings
import top.fatweb.api.settings.SettingsOperator
import java.util.*
/**
* Mail util
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
object MailUtil {
private val mailSender: JavaMailSenderImpl = JavaMailSenderImpl()

View File

@@ -0,0 +1,24 @@
package top.fatweb.api.vo.permission
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer
import io.swagger.v3.oas.annotations.media.Schema
/**
* Register value object
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "注册返回参数")
data class RegisterVo(
/**
* User ID
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "User ID", example = "1709986058679975938")
@JsonSerialize(using = ToStringSerializer::class)
val userId: Long?
)

View File

@@ -25,7 +25,15 @@ data class ActiveInfoVo(
* @since 1.0.0
* @see HistoryVo
*/
val loginHistory: List<HistoryVo>
val loginHistory: List<HistoryVo>,
/**
* Verify user number history
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
val verifyHistory: List<HistoryVo>
) {
data class HistoryVo(
/**

View File

@@ -5,7 +5,7 @@ create table if not exists t_user
id bigint not null primary key,
username varchar(20) not null comment '用户名',
password char(70) not null comment '密码',
verify varchar(50) null comment '验证信息',
verify varchar(144) null comment '验证信息',
locking int not null comment '锁定',
expiration datetime comment '过期时间',
credentials_expiration datetime comment '认证过期时间',

View File

@@ -4,7 +4,7 @@ create table if not exists t_user_info
(
id bigint not null primary key,
user_id bigint not null comment '用户ID',
nickname varchar(50) null comment '昵称',
nickname varchar(50) not null comment '昵称',
avatar text null comment '头像',
email varchar(100) not null comment '邮箱',
create_time datetime not null default (utc_timestamp()) comment '创建时间',

View File

@@ -5,7 +5,7 @@
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>激活您的账号</title>
<title>验证您的账号</title>
<style>
* {
margin: 0;