Add account register and verify api

This commit is contained in:
2023-12-22 18:02:42 +08:00
parent 660f879ccd
commit cf96d8037d
23 changed files with 289 additions and 31 deletions

View File

@@ -2,6 +2,7 @@ package top.fatweb.api.config
import com.baomidou.mybatisplus.extension.kotlin.KtQueryWrapper import com.baomidou.mybatisplus.extension.kotlin.KtQueryWrapper
import jakarta.annotation.PostConstruct import jakarta.annotation.PostConstruct
import org.apache.velocity.app.VelocityEngine
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.context.annotation.DependsOn import org.springframework.context.annotation.DependsOn
@@ -29,7 +30,7 @@ import top.fatweb.avatargenerator.GitHubAvatar
class InitConfig( class InitConfig(
private val userService: IUserService, private val userService: IUserService,
private val userInfoService: IUserInfoService, private val userInfoService: IUserInfoService,
private val passwordEncoder: PasswordEncoder, private val passwordEncoder: PasswordEncoder
) { ) {
private val logger: Logger = LoggerFactory.getLogger(this::class.java) private val logger: Logger = LoggerFactory.getLogger(this::class.java)

View File

@@ -22,5 +22,4 @@ class MybatisPlusConfig {
return mybatisPlusInterceptor return mybatisPlusInterceptor
} }
} }

View File

@@ -75,6 +75,7 @@ class SecurityConfig(
"/swagger-ui.html", "/swagger-ui.html",
"/favicon.ico", "/favicon.ico",
"/login", "/login",
"/register"
).anonymous() ).anonymous()
// Authentication required // Authentication required
.anyRequest().authenticated() .anyRequest().authenticated()

View File

@@ -0,0 +1,17 @@
package top.fatweb.api.config
import org.apache.velocity.app.VelocityEngine
import org.apache.velocity.runtime.RuntimeConstants
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class VelocityEngineConfig {
@Bean
fun velocityEngine() = VelocityEngine().apply {
setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath")
setProperty("classpath.resource.loader.class", ClasspathResourceLoader::class.java.name)
init()
}
}

View File

@@ -42,6 +42,20 @@ class AuthenticationController(
return ResponseResult.success(ResponseCode.PERMISSION_REGISTER_SUCCESS) return ResponseResult.success(ResponseCode.PERMISSION_REGISTER_SUCCESS)
} }
/**
* Send verify email
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Operation(summary = "发送验证邮箱")
@PostMapping("/resend")
fun resend(): ResponseResult<Nothing> {
authenticationService.resend()
return ResponseResult.success(ResponseCode.PERMISSION_RESEND_SUCCESS)
}
/** /**
* Verify * Verify
* *

View File

@@ -34,6 +34,7 @@ object UserConverter {
fun userToUserWithPowerInfoVo(user: User) = UserWithPowerInfoVo( fun userToUserWithPowerInfoVo(user: User) = UserWithPowerInfoVo(
id = user.id, id = user.id,
username = user.username, username = user.username,
verified = user.verify.isNullOrBlank(),
locking = user.locking?.let { it == 1 }, locking = user.locking?.let { it == 1 },
expiration = user.expiration, expiration = user.expiration,
credentialsExpiration = user.credentialsExpiration, credentialsExpiration = user.credentialsExpiration,
@@ -109,6 +110,7 @@ object UserConverter {
fun userToUserWithInfoVo(user: User) = UserWithInfoVo( fun userToUserWithInfoVo(user: User) = UserWithInfoVo(
id = user.id, id = user.id,
username = user.username, username = user.username,
verified = user.verify.isNullOrBlank(),
locking = user.locking?.let { it == 1 }, locking = user.locking?.let { it == 1 },
expiration = user.expiration, expiration = user.expiration,
credentialsExpiration = user.credentialsExpiration, credentialsExpiration = user.credentialsExpiration,

View File

@@ -19,7 +19,8 @@ enum class ResponseCode(val code: Int) {
PERMISSION_LOGOUT_SUCCESS(BusinessCode.PERMISSION, 2), PERMISSION_LOGOUT_SUCCESS(BusinessCode.PERMISSION, 2),
PERMISSION_TOKEN_RENEW_SUCCESS(BusinessCode.PERMISSION, 3), PERMISSION_TOKEN_RENEW_SUCCESS(BusinessCode.PERMISSION, 3),
PERMISSION_REGISTER_SUCCESS(BusinessCode.PERMISSION, 4), PERMISSION_REGISTER_SUCCESS(BusinessCode.PERMISSION, 4),
PERMISSION_VERIFY_SUCCESS(BusinessCode.PERMISSION, 5), PERMISSION_RESEND_SUCCESS(BusinessCode.PERMISSION, 5),
PERMISSION_VERIFY_SUCCESS(BusinessCode.PERMISSION, 6),
PERMISSION_UNAUTHORIZED(BusinessCode.PERMISSION, 50), PERMISSION_UNAUTHORIZED(BusinessCode.PERMISSION, 50),
PERMISSION_USERNAME_NOT_FOUND(BusinessCode.PERMISSION, 51), PERMISSION_USERNAME_NOT_FOUND(BusinessCode.PERMISSION, 51),
@@ -33,6 +34,9 @@ enum class ResponseCode(val code: Int) {
PERMISSION_LOGOUT_FAILED(BusinessCode.PERMISSION, 59), PERMISSION_LOGOUT_FAILED(BusinessCode.PERMISSION, 59),
PERMISSION_TOKEN_ILLEGAL(BusinessCode.PERMISSION, 60), PERMISSION_TOKEN_ILLEGAL(BusinessCode.PERMISSION, 60),
PERMISSION_TOKEN_HAS_EXPIRED(BusinessCode.PERMISSION, 61), PERMISSION_TOKEN_HAS_EXPIRED(BusinessCode.PERMISSION, 61),
PERMISSION_NO_VERIFICATION_REQUIRED(BusinessCode.PERMISSION, 62),
PERMISSION_VERIFY_CODE_ERROR_OR_EXPIRED(BusinessCode.PERMISSION, 63),
PERMISSION_ACCOUNT_NEED_INIT(BusinessCode.PERMISSION, 64),
DATABASE_SELECT_SUCCESS(BusinessCode.DATABASE, 0), DATABASE_SELECT_SUCCESS(BusinessCode.DATABASE, 0),
DATABASE_SELECT_FAILED(BusinessCode.DATABASE, 5), DATABASE_SELECT_FAILED(BusinessCode.DATABASE, 5),

View File

@@ -0,0 +1,3 @@
package top.fatweb.api.exception
class AccountNeedInitException : RuntimeException("Account need initialize")

View File

@@ -0,0 +1,5 @@
package top.fatweb.api.exception
class NoEmailConfigException(
vararg configs: String
) : RuntimeException("Email settings not configured: ${configs.joinToString(", ")}")

View File

@@ -0,0 +1,3 @@
package top.fatweb.api.exception
class NoVerificationRequiredException : RuntimeException("No verification required")

View File

@@ -0,0 +1,3 @@
package top.fatweb.api.exception
class VerificationCodeErrorOrExpiredException : RuntimeException("Verification code is error or has expired")

View File

@@ -16,8 +16,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice import org.springframework.web.bind.annotation.RestControllerAdvice
import top.fatweb.api.entity.common.ResponseCode import top.fatweb.api.entity.common.ResponseCode
import top.fatweb.api.entity.common.ResponseResult import top.fatweb.api.entity.common.ResponseResult
import top.fatweb.api.exception.NoRecordFoundException import top.fatweb.api.exception.*
import top.fatweb.api.exception.TokenHasExpiredException
import top.fatweb.avatargenerator.AvatarException import top.fatweb.avatargenerator.AvatarException
/** /**
@@ -122,6 +121,22 @@ class ExceptionHandler {
ResponseResult.fail(ResponseCode.PERMISSION_ACCESS_DENIED, "Access Denied", null) ResponseResult.fail(ResponseCode.PERMISSION_ACCESS_DENIED, "Access Denied", null)
} }
is NoVerificationRequiredException -> {
logger.debug(e.localizedMessage, e)
ResponseResult.fail(ResponseCode.PERMISSION_NO_VERIFICATION_REQUIRED, e.localizedMessage, null)
}
is VerificationCodeErrorOrExpiredException -> {
logger.debug(e.localizedMessage, e)
ResponseResult.fail(ResponseCode.PERMISSION_VERIFY_CODE_ERROR_OR_EXPIRED, e.localizedMessage, null)
}
is AccountNeedInitException -> {
logger.debug(e.localizedMessage, e)
ResponseResult.fail(ResponseCode.PERMISSION_ACCOUNT_NEED_INIT, e.localizedMessage, null)
}
is BadSqlGrammarException -> { is BadSqlGrammarException -> {
logger.debug(e.localizedMessage, e) logger.debug(e.localizedMessage, e)
ResponseResult.fail(ResponseCode.DATABASE_EXECUTE_ERROR, "Incorrect SQL syntax", null) ResponseResult.fail(ResponseCode.DATABASE_EXECUTE_ERROR, "Incorrect SQL syntax", null)

View File

@@ -1,8 +1,9 @@
package top.fatweb.api.param.permission package top.fatweb.api.param.permission
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.Email
import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Pattern
import jakarta.validation.constraints.Size
/** /**
* Register parameters * Register parameters
@@ -12,6 +13,17 @@ import jakarta.validation.constraints.NotBlank
*/ */
@Schema(description = "注册请求参数") @Schema(description = "注册请求参数")
data class RegisterParam( data class RegisterParam(
/**
* Username
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "用户名", example = "abc", required = true)
@field:NotBlank(message = "Username can not be blank")
@field:Pattern(regexp = "[a-zA-Z-_][0-9a-zA-Z-_]{2,38}", message = "Illegal username")
val username: String?,
/** /**
* Email * Email
* *
@@ -19,7 +31,8 @@ data class RegisterParam(
* @since 1.0.0 * @since 1.0.0
*/ */
@Schema(description = "邮箱", example = "guest@fatweb.top", required = true) @Schema(description = "邮箱", example = "guest@fatweb.top", required = true)
@field:Email(message = "Illegal email address") @field:NotBlank(message = "Email can not be blank")
@field:Pattern(regexp = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*\$", message = "Illegal email address")
val email: String?, val email: String?,
/** /**
@@ -30,5 +43,6 @@ data class RegisterParam(
*/ */
@Schema(description = "密码", example = "test123456", required = true) @Schema(description = "密码", example = "test123456", required = true)
@field:NotBlank(message = "Password can not be blank") @field:NotBlank(message = "Password can not be blank")
@field:Size(min = 10, max = 30)
val password: String? val password: String?
) )

View File

@@ -21,15 +21,6 @@ data class VerifyParam(
@field:NotBlank(message = "Code can not be blank") @field:NotBlank(message = "Code can not be blank")
val code: String?, val code: String?,
/**
* Username
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "用户名", example = "adb")
val username: String?,
/** /**
* Nickname * Nickname
* *

View File

@@ -23,6 +23,14 @@ interface IAuthenticationService {
*/ */
fun register(registerParam: RegisterParam) fun register(registerParam: RegisterParam)
/**
* Send verify email
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
fun resend()
/** /**
* Verify * Verify
* *

View File

@@ -2,8 +2,11 @@ package top.fatweb.api.service.permission.impl
import com.baomidou.mybatisplus.extension.kotlin.KtUpdateWrapper import com.baomidou.mybatisplus.extension.kotlin.KtUpdateWrapper
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import org.apache.velocity.VelocityContext
import org.apache.velocity.app.VelocityEngine
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.security.access.AccessDeniedException
import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder
@@ -14,7 +17,10 @@ import top.fatweb.api.entity.permission.LoginUser
import top.fatweb.api.entity.permission.User import top.fatweb.api.entity.permission.User
import top.fatweb.api.entity.permission.UserInfo import top.fatweb.api.entity.permission.UserInfo
import top.fatweb.api.entity.system.EventLog import top.fatweb.api.entity.system.EventLog
import top.fatweb.api.exception.AccountNeedInitException
import top.fatweb.api.exception.NoVerificationRequiredException
import top.fatweb.api.exception.TokenHasExpiredException import top.fatweb.api.exception.TokenHasExpiredException
import top.fatweb.api.exception.VerificationCodeErrorOrExpiredException
import top.fatweb.api.param.permission.LoginParam import top.fatweb.api.param.permission.LoginParam
import top.fatweb.api.param.permission.RegisterParam import top.fatweb.api.param.permission.RegisterParam
import top.fatweb.api.param.permission.VerifyParam import top.fatweb.api.param.permission.VerifyParam
@@ -24,10 +30,13 @@ import top.fatweb.api.service.permission.IAuthenticationService
import top.fatweb.api.service.permission.IUserInfoService import top.fatweb.api.service.permission.IUserInfoService
import top.fatweb.api.service.permission.IUserService import top.fatweb.api.service.permission.IUserService
import top.fatweb.api.util.JwtUtil import top.fatweb.api.util.JwtUtil
import top.fatweb.api.util.MailUtil
import top.fatweb.api.util.RedisUtil import top.fatweb.api.util.RedisUtil
import top.fatweb.api.util.WebUtil import top.fatweb.api.util.WebUtil
import top.fatweb.api.vo.permission.LoginVo import top.fatweb.api.vo.permission.LoginVo
import top.fatweb.api.vo.permission.TokenVo import top.fatweb.api.vo.permission.TokenVo
import java.io.StringWriter
import java.time.Instant
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
import java.util.* import java.util.*
@@ -44,6 +53,7 @@ import java.util.*
*/ */
@Service @Service
class AuthenticationServiceImpl( class AuthenticationServiceImpl(
private val velocityEngine: VelocityEngine,
private val authenticationManager: AuthenticationManager, private val authenticationManager: AuthenticationManager,
private val passwordEncoder: PasswordEncoder, private val passwordEncoder: PasswordEncoder,
private val redisUtil: RedisUtil, private val redisUtil: RedisUtil,
@@ -56,8 +66,10 @@ class AuthenticationServiceImpl(
@Transactional @Transactional
override fun register(registerParam: RegisterParam) { override fun register(registerParam: RegisterParam) {
val user = User().apply { val user = User().apply {
username = "\$UNNAMED_${UUID.randomUUID()}" username = registerParam.username
password = passwordEncoder.encode(registerParam.password) password = passwordEncoder.encode(registerParam.password)
verify =
"${LocalDateTime.now(ZoneOffset.UTC).toInstant(ZoneOffset.UTC).toEpochMilli()}-${UUID.randomUUID()}"
locking = 0 locking = 0
enable = 1 enable = 1
} }
@@ -67,9 +79,66 @@ class AuthenticationServiceImpl(
avatar = avatarService.randomBase64(null).base64 avatar = avatarService.randomBase64(null).base64
email = registerParam.email email = registerParam.email
}) })
sendVerifyMail(user.username!!, "http://localhost:5173/verify?code=${user.verify!!}", registerParam.email!!)
} }
@Transactional
override fun resend() {
val user = userService.getById(WebUtil.getLoginUserId()) ?: throw AccessDeniedException("Access Denied")
user.verify ?: throw NoVerificationRequiredException()
user.verify =
"${LocalDateTime.now(ZoneOffset.UTC).toInstant(ZoneOffset.UTC).toEpochMilli()}-${UUID.randomUUID()}"
user.updateTime = LocalDateTime.now(ZoneOffset.UTC)
userService.updateById(user)
WebUtil.getLoginUser()?.user?.userInfo?.email?.let {
sendVerifyMail(user.username!!, "http://localhost:5173/verify?code=${user.verify!!}", it)
} ?: throw AccessDeniedException("Access Denied")
}
private fun sendVerifyMail(username: String, verifyUrl: String, email: String) {
val velocityContext = VelocityContext().apply {
put("appName", "氮工具")
put("appUrl", "http://localhost:5173/")
put("username", username)
put("verifyUrl", verifyUrl)
}
val template = velocityEngine.getTemplate("templates/email-verify-account-cn.vm")
val stringWriter = StringWriter()
template.merge(velocityContext, stringWriter)
MailUtil.sendSimpleMail(
"激活您的账号", stringWriter.toString(), true,
email
)
}
@Transactional
override fun verify(verifyParam: VerifyParam) { override fun verify(verifyParam: VerifyParam) {
val user = userService.getById(WebUtil.getLoginUserId()) ?: throw AccessDeniedException("Access Denied")
user.verify ?: throw NoVerificationRequiredException()
if (LocalDateTime.ofInstant(Instant.ofEpochMilli(user.verify!!.split("-").first().toLong()), ZoneOffset.UTC)
.isBefore(LocalDateTime.now(ZoneOffset.UTC).minusHours(2)) || user.verify != verifyParam.code
) {
throw VerificationCodeErrorOrExpiredException()
}
if (verifyParam.nickname.isNullOrBlank() || verifyParam.avatar.isNullOrBlank()) {
throw AccountNeedInitException()
}
userService.update(
KtUpdateWrapper(User()).eq(User::id, user.id).set(User::verify, null)
.set(User::updateTime, LocalDateTime.now(ZoneOffset.UTC))
)
userInfoService.update(
KtUpdateWrapper(UserInfo()).eq(UserInfo::userId, user.id).set(UserInfo::nickname, verifyParam.nickname)
.set(UserInfo::avatar, verifyParam.avatar)
)
} }
@EventLogRecord(EventLog.Event.LOGIN) @EventLogRecord(EventLog.Event.LOGIN)

View File

@@ -45,6 +45,7 @@ class SettingsServiceImpl : ISettingsService {
MailUtil.sendSimpleMail( MailUtil.sendSimpleMail(
"${ServerProperties.appName} Test Message", "${ServerProperties.appName} Test Message",
"This is a test email sent when testing the system email sending service.", "This is a test email sent when testing the system email sending service.",
false,
it it
) )
} }

View File

@@ -1,8 +1,8 @@
package top.fatweb.api.util package top.fatweb.api.util
import org.springframework.mail.MailSender
import org.springframework.mail.SimpleMailMessage
import org.springframework.mail.javamail.JavaMailSenderImpl import org.springframework.mail.javamail.JavaMailSenderImpl
import org.springframework.mail.javamail.MimeMessageHelper
import top.fatweb.api.exception.NoEmailConfigException
import top.fatweb.api.settings.MailSecurityType import top.fatweb.api.settings.MailSecurityType
import top.fatweb.api.settings.MailSettings import top.fatweb.api.settings.MailSettings
import top.fatweb.api.settings.SettingsOperator import top.fatweb.api.settings.SettingsOperator
@@ -33,15 +33,19 @@ object MailUtil {
mailSender.javaMailProperties = properties mailSender.javaMailProperties = properties
} }
fun getSender(): MailSender = mailSender fun sendSimpleMail(subject: String, text: String, html: Boolean = false, vararg to: String) {
val fromName = SettingsOperator.getMailValue(MailSettings::fromName) ?: throw NoEmailConfigException("fromName")
val from = SettingsOperator.getMailValue(MailSettings::from) ?: throw NoEmailConfigException("from")
fun sendSimpleMail(subject: String, text: String, vararg to: String) { val mimeMessage = mailSender.createMimeMessage()
mailSender.send(SimpleMailMessage().apply { val messageHelper = MimeMessageHelper(mimeMessage, true)
messageHelper.apply {
setSubject(subject) setSubject(subject)
from = "${SettingsOperator.getMailValue(MailSettings::fromName)}<${SettingsOperator.getMailValue(MailSettings::from)}>" setFrom(from, fromName)
sentDate = Date() setSentDate(Date())
setTo(*to) setTo(to)
setText(text) setText(text, html)
}) }
mailSender.send(mimeMessage)
} }
} }

View File

@@ -32,6 +32,15 @@ data class UserWithInfoVo(
@Schema(description = "用户名", example = "User") @Schema(description = "用户名", example = "User")
val username: String?, val username: String?,
/**
* Verified
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "已验证", example = "true")
val verified: Boolean?,
/** /**
* Locking * Locking
* *

View File

@@ -32,6 +32,15 @@ data class UserWithPowerInfoVo(
@Schema(description = "用户名", example = "User") @Schema(description = "用户名", example = "User")
val username: String?, val username: String?,
/**
* Verified
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "已验证", example = "true")
val verified: Boolean?,
/** /**
* Locking * Locking
* *

View File

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

View File

@@ -126,7 +126,7 @@
<select id="selectListWithRoleInfoByIds" resultMap="userWithRoleInfoMap"> <select id="selectListWithRoleInfoByIds" resultMap="userWithRoleInfoMap">
select t_user.id as user_id, select t_user.id as user_id,
t_user.username as user_username, t_user.username as user_username,
t_user.password as user_password, t_user.verify as user_verify,
t_user.locking as user_locking, t_user.locking as user_locking,
t_user.expiration as user_expiration, t_user.expiration as user_expiration,
t_user.credentials_expiration as user_credentials_expiration, t_user.credentials_expiration as user_credentials_expiration,
@@ -177,7 +177,7 @@
<select id="selectOneWithRoleInfoById" resultMap="userWithRoleInfoMap"> <select id="selectOneWithRoleInfoById" resultMap="userWithRoleInfoMap">
select t_user.id as user_id, select t_user.id as user_id,
t_user.username as user_username, t_user.username as user_username,
t_user.password as user_password, t_user.verify as user_verify,
t_user.locking as user_locking, t_user.locking as user_locking,
t_user.expiration as user_expiration, t_user.expiration as user_expiration,
t_user.credentials_expiration as user_credentials_expiration, t_user.credentials_expiration as user_credentials_expiration,
@@ -224,7 +224,7 @@
<select id="selectListWithInfo" resultMap="userWithInfoMap"> <select id="selectListWithInfo" resultMap="userWithInfoMap">
select t_user.id as user_id, select t_user.id as user_id,
t_user.username as user_username, t_user.username as user_username,
t_user.password as user_password, t_user.verify as user_verify,
t_user.locking as user_locking, t_user.locking as user_locking,
t_user.expiration as user_expiration, t_user.expiration as user_expiration,
t_user.credentials_expiration as user_credentials_expiration, t_user.credentials_expiration as user_credentials_expiration,
@@ -282,6 +282,7 @@
<resultMap id="userBaseMap" type="user"> <resultMap id="userBaseMap" type="user">
<id property="id" column="user_id"/> <id property="id" column="user_id"/>
<result property="username" column="user_username"/> <result property="username" column="user_username"/>
<result property="verify" column="user_verify"/>
<result property="locking" column="user_locking"/> <result property="locking" column="user_locking"/>
<result property="expiration" column="user_expiration"/> <result property="expiration" column="user_expiration"/>
<result property="credentialsExpiration" column="user_credentials_expiration"/> <result property="credentialsExpiration" column="user_credentials_expiration"/>

View File

@@ -0,0 +1,85 @@
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<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>
<style>
* {
margin: 0;
padding: 0;
color: #4D4D4D;
}
a {
color: #4E47BB;
}
body {
display: flex;
background-color: #F1F2F7;
justify-content: center;
flex-wrap: wrap;
}
.view {
display: flex;
width: 800px;
max-width: 100vw;
align-content: center;
}
.card {
margin: 20px;
padding: 20px;
border-radius: 12px;
flex: 1;
background-color: white;
}
.title {
margin-bottom: 10px;
text-align: center;
font-weight: bolder;
font-size: 1.6em;
color: #4E47BB;
}
.verify-button {
display: flex;
margin: 20px;
padding-bottom: 60px;
justify-content: center;
}
.verify-button a {
padding: 20px 30px;
color: white;
text-decoration: none;
font-size: 1.2em;
background-color: #4E47BB;
border-radius: 10px;
}
.not-reply {
margin-top: 20px;
text-align: center;
font-weight: bold;
}
</style>
</head>
<body>
<div class="view">
<div class="card">
<div class="title">账&nbsp;号&nbsp;激&nbsp;活</div>
<div><strong>${username}</strong>,您好:</div>
<div style="text-indent: 2em">感谢注册 <a target="_blank" href=${appUrl}>${appName}(${appUrl})</a>,在继续使用之前,我们需要确定您的电子邮箱地址的有效性,请在 <u><i>两小时内</i></u> 点击下面的按钮帮助我们验证:</div>
<div class="verify-button"><a target="_blank" href=${verifyUrl}>验证邮箱</a></div>
<div>如果以上按钮无法点击,请复制此链接到浏览器地址栏中访问:<a target="_blank" href=${verifyUrl}>${verifyUrl}</a></div>
<div class="not-reply">此邮件由系统自动发送,请勿回复!</div>
</div>
</div>
</body>
</html>