Complete core functions #9
@@ -2,6 +2,7 @@ package top.fatweb.api.config
|
||||
|
||||
import com.baomidou.mybatisplus.extension.kotlin.KtQueryWrapper
|
||||
import jakarta.annotation.PostConstruct
|
||||
import org.apache.velocity.app.VelocityEngine
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.annotation.DependsOn
|
||||
@@ -29,7 +30,7 @@ import top.fatweb.avatargenerator.GitHubAvatar
|
||||
class InitConfig(
|
||||
private val userService: IUserService,
|
||||
private val userInfoService: IUserInfoService,
|
||||
private val passwordEncoder: PasswordEncoder,
|
||||
private val passwordEncoder: PasswordEncoder
|
||||
) {
|
||||
private val logger: Logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
|
||||
@@ -22,5 +22,4 @@ class MybatisPlusConfig {
|
||||
|
||||
return mybatisPlusInterceptor
|
||||
}
|
||||
|
||||
}
|
||||
@@ -75,6 +75,7 @@ class SecurityConfig(
|
||||
"/swagger-ui.html",
|
||||
"/favicon.ico",
|
||||
"/login",
|
||||
"/register"
|
||||
).anonymous()
|
||||
// Authentication required
|
||||
.anyRequest().authenticated()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,20 @@ class AuthenticationController(
|
||||
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
|
||||
*
|
||||
|
||||
@@ -34,6 +34,7 @@ object UserConverter {
|
||||
fun userToUserWithPowerInfoVo(user: User) = UserWithPowerInfoVo(
|
||||
id = user.id,
|
||||
username = user.username,
|
||||
verified = user.verify.isNullOrBlank(),
|
||||
locking = user.locking?.let { it == 1 },
|
||||
expiration = user.expiration,
|
||||
credentialsExpiration = user.credentialsExpiration,
|
||||
@@ -109,6 +110,7 @@ object UserConverter {
|
||||
fun userToUserWithInfoVo(user: User) = UserWithInfoVo(
|
||||
id = user.id,
|
||||
username = user.username,
|
||||
verified = user.verify.isNullOrBlank(),
|
||||
locking = user.locking?.let { it == 1 },
|
||||
expiration = user.expiration,
|
||||
credentialsExpiration = user.credentialsExpiration,
|
||||
|
||||
@@ -19,7 +19,8 @@ enum class ResponseCode(val code: Int) {
|
||||
PERMISSION_LOGOUT_SUCCESS(BusinessCode.PERMISSION, 2),
|
||||
PERMISSION_TOKEN_RENEW_SUCCESS(BusinessCode.PERMISSION, 3),
|
||||
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_USERNAME_NOT_FOUND(BusinessCode.PERMISSION, 51),
|
||||
@@ -33,6 +34,9 @@ enum class ResponseCode(val code: Int) {
|
||||
PERMISSION_LOGOUT_FAILED(BusinessCode.PERMISSION, 59),
|
||||
PERMISSION_TOKEN_ILLEGAL(BusinessCode.PERMISSION, 60),
|
||||
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_FAILED(BusinessCode.DATABASE, 5),
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package top.fatweb.api.exception
|
||||
|
||||
class AccountNeedInitException : RuntimeException("Account need initialize")
|
||||
@@ -0,0 +1,5 @@
|
||||
package top.fatweb.api.exception
|
||||
|
||||
class NoEmailConfigException(
|
||||
vararg configs: String
|
||||
) : RuntimeException("Email settings not configured: ${configs.joinToString(", ")}")
|
||||
@@ -0,0 +1,3 @@
|
||||
package top.fatweb.api.exception
|
||||
|
||||
class NoVerificationRequiredException : RuntimeException("No verification required")
|
||||
@@ -0,0 +1,3 @@
|
||||
package top.fatweb.api.exception
|
||||
|
||||
class VerificationCodeErrorOrExpiredException : RuntimeException("Verification code is error or has expired")
|
||||
@@ -16,8 +16,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice
|
||||
import top.fatweb.api.entity.common.ResponseCode
|
||||
import top.fatweb.api.entity.common.ResponseResult
|
||||
import top.fatweb.api.exception.NoRecordFoundException
|
||||
import top.fatweb.api.exception.TokenHasExpiredException
|
||||
import top.fatweb.api.exception.*
|
||||
import top.fatweb.avatargenerator.AvatarException
|
||||
|
||||
/**
|
||||
@@ -122,6 +121,22 @@ class ExceptionHandler {
|
||||
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 -> {
|
||||
logger.debug(e.localizedMessage, e)
|
||||
ResponseResult.fail(ResponseCode.DATABASE_EXECUTE_ERROR, "Incorrect SQL syntax", null)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package top.fatweb.api.param.permission
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import jakarta.validation.constraints.Email
|
||||
import jakarta.validation.constraints.NotBlank
|
||||
import jakarta.validation.constraints.Pattern
|
||||
import jakarta.validation.constraints.Size
|
||||
|
||||
/**
|
||||
* Register parameters
|
||||
@@ -12,6 +13,17 @@ import jakarta.validation.constraints.NotBlank
|
||||
*/
|
||||
@Schema(description = "注册请求参数")
|
||||
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
|
||||
*
|
||||
@@ -19,7 +31,8 @@ data class RegisterParam(
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@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?,
|
||||
|
||||
/**
|
||||
@@ -30,5 +43,6 @@ data class RegisterParam(
|
||||
*/
|
||||
@Schema(description = "密码", example = "test123456", required = true)
|
||||
@field:NotBlank(message = "Password can not be blank")
|
||||
@field:Size(min = 10, max = 30)
|
||||
val password: String?
|
||||
)
|
||||
@@ -21,15 +21,6 @@ data class VerifyParam(
|
||||
@field:NotBlank(message = "Code can not be blank")
|
||||
val code: String?,
|
||||
|
||||
/**
|
||||
* Username
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "用户名", example = "adb")
|
||||
val username: String?,
|
||||
|
||||
/**
|
||||
* Nickname
|
||||
*
|
||||
|
||||
@@ -23,6 +23,14 @@ interface IAuthenticationService {
|
||||
*/
|
||||
fun register(registerParam: RegisterParam)
|
||||
|
||||
/**
|
||||
* Send verify email
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
fun resend()
|
||||
|
||||
/**
|
||||
* Verify
|
||||
*
|
||||
|
||||
@@ -2,8 +2,11 @@ package top.fatweb.api.service.permission.impl
|
||||
|
||||
import com.baomidou.mybatisplus.extension.kotlin.KtUpdateWrapper
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import org.apache.velocity.VelocityContext
|
||||
import org.apache.velocity.app.VelocityEngine
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.security.access.AccessDeniedException
|
||||
import org.springframework.security.authentication.AuthenticationManager
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
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.UserInfo
|
||||
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.VerificationCodeErrorOrExpiredException
|
||||
import top.fatweb.api.param.permission.LoginParam
|
||||
import top.fatweb.api.param.permission.RegisterParam
|
||||
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.IUserService
|
||||
import top.fatweb.api.util.JwtUtil
|
||||
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.TokenVo
|
||||
import java.io.StringWriter
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.util.*
|
||||
@@ -44,6 +53,7 @@ import java.util.*
|
||||
*/
|
||||
@Service
|
||||
class AuthenticationServiceImpl(
|
||||
private val velocityEngine: VelocityEngine,
|
||||
private val authenticationManager: AuthenticationManager,
|
||||
private val passwordEncoder: PasswordEncoder,
|
||||
private val redisUtil: RedisUtil,
|
||||
@@ -56,8 +66,10 @@ class AuthenticationServiceImpl(
|
||||
@Transactional
|
||||
override fun register(registerParam: RegisterParam) {
|
||||
val user = User().apply {
|
||||
username = "\$UNNAMED_${UUID.randomUUID()}"
|
||||
username = registerParam.username
|
||||
password = passwordEncoder.encode(registerParam.password)
|
||||
verify =
|
||||
"${LocalDateTime.now(ZoneOffset.UTC).toInstant(ZoneOffset.UTC).toEpochMilli()}-${UUID.randomUUID()}"
|
||||
locking = 0
|
||||
enable = 1
|
||||
}
|
||||
@@ -67,9 +79,66 @@ class AuthenticationServiceImpl(
|
||||
avatar = avatarService.randomBase64(null).base64
|
||||
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) {
|
||||
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)
|
||||
|
||||
@@ -45,6 +45,7 @@ class SettingsServiceImpl : ISettingsService {
|
||||
MailUtil.sendSimpleMail(
|
||||
"${ServerProperties.appName} Test Message",
|
||||
"This is a test email sent when testing the system email sending service.",
|
||||
false,
|
||||
it
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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.MimeMessageHelper
|
||||
import top.fatweb.api.exception.NoEmailConfigException
|
||||
import top.fatweb.api.settings.MailSecurityType
|
||||
import top.fatweb.api.settings.MailSettings
|
||||
import top.fatweb.api.settings.SettingsOperator
|
||||
@@ -33,15 +33,19 @@ object MailUtil {
|
||||
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) {
|
||||
mailSender.send(SimpleMailMessage().apply {
|
||||
val mimeMessage = mailSender.createMimeMessage()
|
||||
val messageHelper = MimeMessageHelper(mimeMessage, true)
|
||||
messageHelper.apply {
|
||||
setSubject(subject)
|
||||
from = "${SettingsOperator.getMailValue(MailSettings::fromName)}<${SettingsOperator.getMailValue(MailSettings::from)}>"
|
||||
sentDate = Date()
|
||||
setTo(*to)
|
||||
setText(text)
|
||||
})
|
||||
setFrom(from, fromName)
|
||||
setSentDate(Date())
|
||||
setTo(to)
|
||||
setText(text, html)
|
||||
}
|
||||
mailSender.send(mimeMessage)
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,15 @@ data class UserWithInfoVo(
|
||||
@Schema(description = "用户名", example = "User")
|
||||
val username: String?,
|
||||
|
||||
/**
|
||||
* Verified
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "已验证", example = "true")
|
||||
val verified: Boolean?,
|
||||
|
||||
/**
|
||||
* Locking
|
||||
*
|
||||
|
||||
@@ -32,6 +32,15 @@ data class UserWithPowerInfoVo(
|
||||
@Schema(description = "用户名", example = "User")
|
||||
val username: String?,
|
||||
|
||||
/**
|
||||
* Verified
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "已验证", example = "true")
|
||||
val verified: Boolean?,
|
||||
|
||||
/**
|
||||
* Locking
|
||||
*
|
||||
|
||||
@@ -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(36) null comment '验证信息',
|
||||
verify varchar(50) null comment '验证信息',
|
||||
locking int not null comment '锁定',
|
||||
expiration datetime comment '过期时间',
|
||||
credentials_expiration datetime comment '认证过期时间',
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
<select id="selectListWithRoleInfoByIds" resultMap="userWithRoleInfoMap">
|
||||
select t_user.id as user_id,
|
||||
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.expiration as user_expiration,
|
||||
t_user.credentials_expiration as user_credentials_expiration,
|
||||
@@ -177,7 +177,7 @@
|
||||
<select id="selectOneWithRoleInfoById" resultMap="userWithRoleInfoMap">
|
||||
select t_user.id as user_id,
|
||||
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.expiration as user_expiration,
|
||||
t_user.credentials_expiration as user_credentials_expiration,
|
||||
@@ -224,7 +224,7 @@
|
||||
<select id="selectListWithInfo" resultMap="userWithInfoMap">
|
||||
select t_user.id as user_id,
|
||||
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.expiration as user_expiration,
|
||||
t_user.credentials_expiration as user_credentials_expiration,
|
||||
@@ -282,6 +282,7 @@
|
||||
<resultMap id="userBaseMap" type="user">
|
||||
<id property="id" column="user_id"/>
|
||||
<result property="username" column="user_username"/>
|
||||
<result property="verify" column="user_verify"/>
|
||||
<result property="locking" column="user_locking"/>
|
||||
<result property="expiration" column="user_expiration"/>
|
||||
<result property="credentialsExpiration" column="user_credentials_expiration"/>
|
||||
|
||||
85
src/main/resources/templates/email-verify-account-cn.vm
Normal file
85
src/main/resources/templates/email-verify-account-cn.vm
Normal 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">账 号 激 活</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>
|
||||
Reference in New Issue
Block a user