Finish sign api. Optimize code.

This commit is contained in:
2023-12-26 18:17:03 +08:00
parent 15f631971c
commit e45769e52a
43 changed files with 570 additions and 107 deletions

View File

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

View File

@@ -9,9 +9,7 @@ import org.springframework.web.bind.annotation.RequestBody
import top.fatweb.api.annotation.BaseController
import top.fatweb.api.entity.common.ResponseCode
import top.fatweb.api.entity.common.ResponseResult
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.param.permission.*
import top.fatweb.api.service.permission.IAuthenticationService
import top.fatweb.api.util.WebUtil
import top.fatweb.api.vo.permission.LoginVo
@@ -49,7 +47,7 @@ class AuthenticationController(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Operation(summary = "发送验证邮")
@Operation(summary = "发送验证邮")
@PostMapping("/resend")
fun resend(): ResponseResult<Nothing> {
authenticationService.resend()
@@ -58,12 +56,12 @@ class AuthenticationController(
}
/**
* Verify
* Verify email
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Operation(summary = "验证")
@Operation(summary = "验证邮箱")
@PostMapping("/verify")
fun verify(@Valid @RequestBody verifyParam: VerifyParam): ResponseResult<Nothing> {
authenticationService.verify(verifyParam)
@@ -71,6 +69,34 @@ class AuthenticationController(
return ResponseResult.success(ResponseCode.PERMISSION_VERIFY_SUCCESS)
}
/**
* Forget password
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Operation(summary = "忘记密码")
@PostMapping("/forget")
fun forget(request: HttpServletRequest, @Valid @RequestBody forgetParam: ForgetParam): ResponseResult<Nothing> {
authenticationService.forget(request, forgetParam)
return ResponseResult.success(ResponseCode.PERMISSION_FORGET_SUCCESS)
}
/**
* Retrieve password
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Operation(summary = "找回密码")
@PostMapping("/retrieve")
fun retrieve(request: HttpServletRequest, @Valid @RequestBody retrieveParam: RetrieveParam): ResponseResult<Nothing> {
authenticationService.retrieve(request, retrieveParam)
return ResponseResult.success(ResponseCode.PERMISSION_RETRIEVE_SUCCESS)
}
/**
* Login
*

View File

@@ -21,6 +21,8 @@ enum class ResponseCode(val code: Int) {
PERMISSION_REGISTER_SUCCESS(BusinessCode.PERMISSION, 4),
PERMISSION_RESEND_SUCCESS(BusinessCode.PERMISSION, 5),
PERMISSION_VERIFY_SUCCESS(BusinessCode.PERMISSION, 6),
PERMISSION_FORGET_SUCCESS(BusinessCode.PERMISSION, 7),
PERMISSION_RETRIEVE_SUCCESS(BusinessCode.PERMISSION, 8),
PERMISSION_UNAUTHORIZED(BusinessCode.PERMISSION, 50),
PERMISSION_USERNAME_NOT_FOUND(BusinessCode.PERMISSION, 51),
@@ -37,6 +39,9 @@ enum class ResponseCode(val code: Int) {
PERMISSION_NO_VERIFICATION_REQUIRED(BusinessCode.PERMISSION, 62),
PERMISSION_VERIFY_CODE_ERROR_OR_EXPIRED(BusinessCode.PERMISSION, 63),
PERMISSION_ACCOUNT_NEED_INIT(BusinessCode.PERMISSION, 64),
PERMISSION_USER_NOT_FOUND(BusinessCode.PERMISSION, 65),
PERMISSION_RETRIEVE_CODE_ERROR_OR_EXPIRED(BusinessCode.PERMISSION, 66),
PERMISSION_ACCOUNT_NEED_RESET_PASSWORD(BusinessCode.PERMISSION, 67),
DATABASE_SELECT_SUCCESS(BusinessCode.DATABASE, 0),
DATABASE_SELECT_FAILED(BusinessCode.DATABASE, 5),

View File

@@ -47,7 +47,7 @@ class User() : Serializable {
var password: String? = null
/**
* Verify
* Verify email
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
@@ -55,6 +55,15 @@ class User() : Serializable {
@TableField("verify")
var verify: String? = null
/**
* Forget password
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@TableField("forget")
var forget: String? = null
/**
* Locking
*
@@ -242,6 +251,6 @@ class User() : Serializable {
var operations: List<Operation>? = null
override fun toString(): String {
return "User(id=$id, username=$username, password=$password, locking=$locking, expiration=$expiration, credentialsExpiration=$credentialsExpiration, enable=$enable, currentLoginTime=$currentLoginTime, currentLoginIp=$currentLoginIp, lastLoginTime=$lastLoginTime, lastLoginIp=$lastLoginIp, createTime=$createTime, updateTime=$updateTime, deleted=$deleted, version=$version, userInfo=$userInfo, roles=$roles, groups=$groups, modules=$modules, menus=$menus, funcs=$funcs, operations=$operations)"
return "User(id=$id, username=$username, password=$password, verify=$verify, forget=$forget, locking=$locking, expiration=$expiration, credentialsExpiration=$credentialsExpiration, enable=$enable, currentLoginTime=$currentLoginTime, currentLoginIp=$currentLoginIp, lastLoginTime=$lastLoginTime, lastLoginIp=$lastLoginIp, createTime=$createTime, updateTime=$updateTime, deleted=$deleted, version=$version, userInfo=$userInfo, roles=$roles, groups=$groups, modules=$modules, menus=$menus, funcs=$funcs, operations=$operations)"
}
}

View File

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

View File

@@ -0,0 +1,3 @@
package top.fatweb.api.exception
class RetrieveCodeErrorOrExpiredException : RuntimeException("Retrieve code error or expired")

View File

@@ -0,0 +1,3 @@
package top.fatweb.api.exception
class UserNotFoundException : RuntimeException("User not found")

View File

@@ -121,6 +121,11 @@ class ExceptionHandler {
ResponseResult.fail(ResponseCode.PERMISSION_ACCESS_DENIED, "Access Denied", null)
}
is UserNotFoundException -> {
logger.debug(e.localizedMessage, e)
ResponseResult.fail(ResponseCode.PERMISSION_USER_NOT_FOUND, e.localizedMessage, null)
}
is NoVerificationRequiredException -> {
logger.debug(e.localizedMessage, e)
ResponseResult.fail(ResponseCode.PERMISSION_NO_VERIFICATION_REQUIRED, e.localizedMessage, null)
@@ -136,6 +141,16 @@ class ExceptionHandler {
ResponseResult.fail(ResponseCode.PERMISSION_ACCOUNT_NEED_INIT, e.localizedMessage, null)
}
is RetrieveCodeErrorOrExpiredException -> {
logger.debug(e.localizedMessage, e)
ResponseResult.fail(ResponseCode.PERMISSION_RETRIEVE_CODE_ERROR_OR_EXPIRED, e.localizedMessage, null)
}
is AccountNeedResetPasswordException -> {
logger.debug(e.localizedMessage, e)
ResponseResult.fail(ResponseCode.PERMISSION_ACCOUNT_NEED_RESET_PASSWORD, e.localizedMessage, null)
}
is BadSqlGrammarException -> {
logger.debug(e.localizedMessage, e)

View File

@@ -3,6 +3,7 @@ package top.fatweb.api.mapper.permission
import com.baomidou.mybatisplus.core.mapper.BaseMapper
import com.baomidou.mybatisplus.core.metadata.IPage
import org.apache.ibatis.annotations.Mapper
import org.apache.ibatis.annotations.Param
import top.fatweb.api.entity.permission.Group
/**
@@ -26,7 +27,11 @@ interface GroupMapper : BaseMapper<Group> {
* @since 1.0.0
* @see IPage
*/
fun selectPage(page: IPage<Long>, searchName: String?, searchRegex: Boolean): IPage<Long>
fun selectPage(
page: IPage<Long>,
@Param("searchName") searchName: String?,
@Param("searchRegex") searchRegex: Boolean
): IPage<Long>
/**
* Select group with role list by list of group IDs
@@ -37,7 +42,7 @@ interface GroupMapper : BaseMapper<Group> {
* @since 1.0.0
* @see Group
*/
fun selectListWithRoleByIds(groupIds: List<Long>): List<Group>?
fun selectListWithRoleByIds(@Param("groupIds") groupIds: List<Long>): List<Group>?
/**
* Select one group by ID
@@ -48,5 +53,5 @@ interface GroupMapper : BaseMapper<Group> {
* @since 1.0.0
* @see Group
*/
fun selectOneById(id: Long): Group?
fun selectOneById(@Param("id") id: Long): Group?
}

View File

@@ -3,6 +3,7 @@ package top.fatweb.api.mapper.permission
import com.baomidou.mybatisplus.core.mapper.BaseMapper
import com.baomidou.mybatisplus.core.metadata.IPage
import org.apache.ibatis.annotations.Mapper
import org.apache.ibatis.annotations.Param
import top.fatweb.api.entity.permission.Role
/**
@@ -26,7 +27,11 @@ interface RoleMapper : BaseMapper<Role> {
* @since 1.0.0
* @see IPage
*/
fun selectPage(page: IPage<Long>, searchName: String?, searchRegex: Boolean): IPage<Long>
fun selectPage(
page: IPage<Long>,
@Param("searchName") searchName: String?,
@Param("searchRegex") searchRegex: Boolean
): IPage<Long>
/**
* Select role with power list by list of role IDs
@@ -37,7 +42,7 @@ interface RoleMapper : BaseMapper<Role> {
* @since 1.0.0
* @see Role
*/
fun selectListWithPowerByIds(roleIds: List<Long>): List<Role>?
fun selectListWithPowerByIds(@Param("roleIds") roleIds: List<Long>): List<Role>?
/**
* Select one role by ID
@@ -48,5 +53,5 @@ interface RoleMapper : BaseMapper<Role> {
* @since 1.0.0
* @see Role
*/
fun selectOneById(id: Long): Role?
fun selectOneById(@Param("id") id: Long): Role?
}

View File

@@ -39,7 +39,12 @@ interface UserMapper : BaseMapper<User> {
* @since 1.0.0
* @see IPage
*/
fun selectPage(page: IPage<Long>, searchType: String, searchValue: String?, searchRegex: Boolean): IPage<Long>
fun selectPage(
page: IPage<Long>,
@Param("searchType") searchType: String,
@Param("searchValue") searchValue: String?,
@Param("searchRegex") searchRegex: Boolean
): IPage<Long>
/**
* Select user with role and information list by list of user IDs
@@ -50,7 +55,7 @@ interface UserMapper : BaseMapper<User> {
* @since 1.0.0
* @see User
*/
fun selectListWithRoleInfoByIds(userIds: List<Long>): List<User>
fun selectListWithRoleInfoByIds(@Param("userIds") userIds: List<Long>): List<User>
/**
* Select one user by ID
@@ -61,7 +66,7 @@ interface UserMapper : BaseMapper<User> {
* @since 1.0.0
* @see User
*/
fun selectOneWithRoleInfoById(id: Long): User?
fun selectOneWithRoleInfoById(@Param("id") id: Long): User?
/**
* Select all user with information list
@@ -81,7 +86,7 @@ interface UserMapper : BaseMapper<User> {
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
fun selectIdsWithRoleIds(roleIds: List<Long>): List<Long>
fun selectIdsWithRoleIds(@Param("roleIds") roleIds: List<Long>): List<Long>
/**
* Select user IDs list by list of group IDs
@@ -91,5 +96,5 @@ interface UserMapper : BaseMapper<User> {
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
fun selectIdsWithGroupIds(groupIds: List<Long>): List<Long>
fun selectIdsWithGroupIds(@Param("groupIds") groupIds: List<Long>): List<Long>
}

View File

@@ -3,6 +3,7 @@ package top.fatweb.api.mapper.system
import com.baomidou.mybatisplus.core.mapper.BaseMapper
import com.baomidou.mybatisplus.core.metadata.IPage
import org.apache.ibatis.annotations.Mapper
import org.apache.ibatis.annotations.Param
import top.fatweb.api.entity.system.SysLog
import java.time.LocalDateTime
@@ -34,10 +35,10 @@ interface SysLogMapper : BaseMapper<SysLog> {
*/
fun selectPage(
page: IPage<SysLog>,
logType: List<String>?,
requestMethod: List<String>?,
searchRequestUrl: String?,
searchStartTime: LocalDateTime?,
searchEndTime: LocalDateTime?
@Param("logType") logType: List<String>?,
@Param("requestMethod") requestMethod: List<String>?,
@Param("searchRequestUrl") searchRequestUrl: String?,
@Param("searchStartTime") searchStartTime: LocalDateTime?,
@Param("searchEndTime") searchEndTime: LocalDateTime?
): IPage<SysLog>
}

View File

@@ -16,7 +16,7 @@ open class PageSortParam {
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "分页页码", example = "1", defaultValue = "1")
@Schema(description = "分页页码", defaultValue = "1", example = "1")
@field:Min(1, message = "Pagination page number must be a positive integer")
var currentPage: Long = 1
@@ -26,7 +26,7 @@ open class PageSortParam {
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "分页大小", example = "20", defaultValue = "20")
@Schema(description = "分页大小", defaultValue = "20", example = "20")
@field:Min(1, message = "The number of data per page must be a positive integer")
var pageSize: Long = 20
@@ -45,6 +45,6 @@ open class PageSortParam {
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "排序方式", example = "desc", allowableValues = ["desc", "asc"])
@Schema(description = "排序方式", allowableValues = ["desc", "asc"], defaultValue = "desc", example = "desc")
var sortOrder: String? = null
}

View File

@@ -0,0 +1,25 @@
package top.fatweb.api.param.permission
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Pattern
/**
* Forget password parameters
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "忘记密码请求参数")
data class ForgetParam(
/**
* Email
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "邮箱", required = true, example = "user@email.com")
@field:NotBlank(message = "Email can not be blank")
@field:Pattern(regexp = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*\$", message = "Illegal email address")
val email: String?
)

View File

@@ -17,7 +17,7 @@ data class LoginParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "账户", example = "test", required = true)
@Schema(description = "账户", required = true, example = "test")
@field:NotBlank(message = "Account can not be blank")
val account: String?,
@@ -27,7 +27,7 @@ data class LoginParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "密码", example = "test123456", required = true)
@Schema(description = "密码", required = true)
@field:NotBlank(message = "Password can not be blank")
val password: String?
)

View File

@@ -19,7 +19,7 @@ data class RegisterParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "用户名", example = "abc", required = true)
@Schema(description = "用户名", required = true, example = "abc")
@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?,
@@ -30,7 +30,7 @@ data class RegisterParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "邮箱", example = "guest@fatweb.top", required = true)
@Schema(description = "邮箱", required = true, example = "user@email.com")
@field:NotBlank(message = "Email can not be blank")
@field:Pattern(regexp = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*\$", message = "Illegal email address")
val email: String?,
@@ -41,7 +41,7 @@ data class RegisterParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "密码", example = "test123456", required = true)
@Schema(description = "密码", required = true)
@field:NotBlank(message = "Password can not be blank")
@field:Size(min = 10, max = 30)
val password: String?

View File

@@ -0,0 +1,35 @@
package top.fatweb.api.param.permission
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Size
/**
* Retrieve password parameters
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "找回密码请求参数")
data class RetrieveParam(
/**
* Code
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "验证码", required = true)
@field:NotBlank(message = "Code can not be blank")
val code: String?,
/**
* New password
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "新密码")
@field:NotBlank(message = "New password can not be blank")
@field:Size(min = 10, max = 30)
val password: String?
)

View File

@@ -4,12 +4,12 @@ import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank
/**
* Verify parameters
* Verify email parameters
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "验证请求参数")
@Schema(description = "验证邮箱请求参数")
data class VerifyParam(
/**
* Code
@@ -17,7 +17,7 @@ data class VerifyParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "验证码", example = "9c4b8199-1dbe-4f6f-96a5-fe1d75cc6a65", required = true)
@Schema(description = "验证码", required = true)
@field:NotBlank(message = "Code can not be blank")
val code: String?,

View File

@@ -17,7 +17,7 @@ data class GroupAddParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "用户组名称")
@Schema(description = "用户组名称", required = true, example = "Group_1")
@field:NotBlank(message = "Name can not be blank")
val name: String?,
@@ -27,7 +27,7 @@ data class GroupAddParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "启用", allowableValues = ["true", "false"], defaultValue = "true")
@Schema(description = "启用", allowableValues = ["true", "false"], defaultValue = "true", example = "true")
val enable: Boolean = true,
/**

View File

@@ -16,6 +16,6 @@ data class GroupDeleteParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "用户组 ID 列表")
@Schema(description = "用户组 ID 列表", required = true)
val ids: List<Long>
)

View File

@@ -18,7 +18,7 @@ data class GroupGetParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "查询用户组名称")
@Schema(description = "查询用户组名称", example = "Group_1")
val searchName: String?,
/**
@@ -27,6 +27,11 @@ data class GroupGetParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "查询使用正则表达式", allowableValues = ["true", "false"], defaultValue = "false")
@Schema(
description = "查询使用正则表达式",
allowableValues = ["true", "false"],
defaultValue = "false",
example = "false"
)
val searchRegex: Boolean = false,
) : PageSortParam()

View File

@@ -18,7 +18,7 @@ data class GroupUpdateParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "用户组 ID")
@Schema(description = "用户组 ID", required = true)
@field:NotNull(message = "ID can not be null")
val id: Long?,
@@ -28,7 +28,7 @@ data class GroupUpdateParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "用户组名称")
@Schema(description = "用户组名称", required = true, example = "Group_1")
@field:NotBlank(message = "Name can not be blank")
val name: String?,
@@ -38,7 +38,7 @@ data class GroupUpdateParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "启用", allowableValues = ["true", "false"], defaultValue = "true")
@Schema(description = "启用", allowableValues = ["true", "false"], defaultValue = "true", example = "true")
val enable: Boolean = true,
/**

View File

@@ -17,7 +17,7 @@ data class GroupUpdateStatusParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "用户组 ID")
@Schema(description = "用户组 ID", required = true)
@field:NotNull(message = "ID can not be null")
val id: Long?,
@@ -27,6 +27,6 @@ data class GroupUpdateStatusParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "启用", allowableValues = ["true", "false"], defaultValue = "true")
@Schema(description = "启用", allowableValues = ["true", "false"], defaultValue = "true", example = "true")
val enable: Boolean = true
)

View File

@@ -17,7 +17,7 @@ data class RoleAddParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "角色名称")
@Schema(description = "角色名称", required = true, example = "Role_1")
@field:NotBlank(message = "Name can not be blank")
val name: String?,
@@ -27,7 +27,7 @@ data class RoleAddParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "启用", allowableValues = ["true", "false"], defaultValue = "true")
@Schema(description = "启用", allowableValues = ["true", "false"], defaultValue = "true", example = "true")
val enable: Boolean = true,
/**

View File

@@ -16,6 +16,6 @@ data class RoleDeleteParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "角色 ID 列表")
@Schema(description = "角色 ID 列表", required = true)
val ids: List<Long>
)

View File

@@ -18,7 +18,7 @@ data class RoleGetParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "查询角色名称")
@Schema(description = "查询角色名称", example = "Role_1")
val searchName: String?,
/**
@@ -27,6 +27,11 @@ data class RoleGetParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "查询使用正则表达式", allowableValues = ["true", "false"], defaultValue = "false")
@Schema(
description = "查询使用正则表达式",
allowableValues = ["true", "false"],
defaultValue = "false",
example = "false"
)
val searchRegex: Boolean = false,
) : PageSortParam()

View File

@@ -18,7 +18,7 @@ data class RoleUpdateParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "角色 ID")
@Schema(description = "角色 ID", required = true)
@field:NotNull(message = "Role id can not be null")
val id: Long?,
@@ -28,7 +28,7 @@ data class RoleUpdateParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "角色名称")
@Schema(description = "角色名称", required = true, example = "Role_1")
@field:NotBlank(message = "Name can not be blank")
val name: String?,
@@ -38,7 +38,7 @@ data class RoleUpdateParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "启用", allowableValues = ["true", "false"], defaultValue = "true")
@Schema(description = "启用", allowableValues = ["true", "false"], defaultValue = "true", example = "true")
val enable: Boolean = true,
/**

View File

@@ -17,7 +17,7 @@ data class RoleUpdateStatusParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "角色 ID")
@Schema(description = "角色 ID", required = true)
@field:NotNull(message = "Role id can not be null")
val id: Long?,
@@ -27,6 +27,6 @@ data class RoleUpdateStatusParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "启用", allowableValues = ["true", "false"], defaultValue = "true")
@Schema(description = "启用", allowableValues = ["true", "false"], defaultValue = "true", example = "true")
val enable: Boolean = true
)

View File

@@ -19,7 +19,7 @@ data class UserAddParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "用户名")
@Schema(description = "用户名", required = true, example = "User_1")
@field:NotBlank(message = "Username can not be blank")
val username: String?,
@@ -38,7 +38,7 @@ data class UserAddParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "是否已验证")
@Schema(description = "是否已验证", allowableValues = ["true", "false"], defaultValue = "false", example = "false")
val verified: Boolean = false,
/**
@@ -47,7 +47,7 @@ data class UserAddParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "锁定", allowableValues = ["true", "false"], defaultValue = "false")
@Schema(description = "锁定", allowableValues = ["true", "false"], defaultValue = "false", example = "false")
val locking: Boolean = false,
/**
@@ -57,7 +57,7 @@ data class UserAddParam(
* @since 1.0.0
* @see LocalDateTime
*/
@Schema(description = "过期时间")
@Schema(description = "过期时间", example = "1900-01-01T00:00:00.000Z")
val expiration: LocalDateTime?,
/**
@@ -67,7 +67,7 @@ data class UserAddParam(
* @since 1.0.0
* @see LocalDateTime
*/
@Schema(description = "认证过期时间")
@Schema(description = "认证过期时间", example = "1900-01-01T00:00:00.000Z")
val credentialsExpiration: LocalDateTime?,
/**
@@ -76,7 +76,7 @@ data class UserAddParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "启用", allowableValues = ["true", "false"], defaultValue = "true")
@Schema(description = "启用", allowableValues = ["true", "false"], defaultValue = "true", example = "true")
val enable: Boolean = true,
/**
@@ -85,7 +85,7 @@ data class UserAddParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "昵称")
@Schema(description = "昵称", required = true, example = "Nickname_1")
@field:NotBlank(message = "Nickname can not be blank")
val nickname: String?,
@@ -104,7 +104,8 @@ data class UserAddParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "邮箱")
@Schema(description = "邮箱", required = true, example = "user@email.com")
@NotBlank(message = "Email can not be blank")
@Pattern(regexp = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*\$", message = "Illegal email address")
val email: String?,

View File

@@ -16,6 +16,6 @@ data class UserDeleteParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "用户 ID 列表")
@Schema(description = "用户 ID 列表", required = true)
val ids: List<Long>
)

View File

@@ -18,7 +18,12 @@ data class UserGetParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "搜索类型", allowableValues = ["ALL", "ID", "USERNAME", "NICKNAME", "EMAIL"], defaultValue = "ALL")
@Schema(
description = "搜索类型",
allowableValues = ["ALL", "ID", "USERNAME", "NICKNAME", "EMAIL"],
defaultValue = "ALL",
example = "ALL"
)
val searchType: String = "ALL",
/**
@@ -27,7 +32,7 @@ data class UserGetParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "查询内容")
@Schema(description = "查询内容", example = "User_1")
val searchValue: String?,
/**
@@ -36,6 +41,11 @@ data class UserGetParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "查询使用正则表达式", allowableValues = ["true", "false"], defaultValue = "false")
@Schema(
description = "查询使用正则表达式",
allowableValues = ["true", "false"],
defaultValue = "false",
example = "false"
)
val searchRegex: Boolean = false,
) : PageSortParam()

View File

@@ -1,6 +1,7 @@
package top.fatweb.api.param.permission.user
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.NotNull
import jakarta.validation.constraints.Pattern
import java.time.LocalDateTime
@@ -19,7 +20,7 @@ data class UserUpdateParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "用户 ID")
@Schema(description = "用户 ID", required = true)
@field:NotNull(message = "ID can not be null")
val id: Long?,
@@ -29,7 +30,8 @@ data class UserUpdateParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "用户名")
@Schema(description = "用户名", required = true, example = "User_1")
@field:NotBlank(message = "Username can not be blank")
val username: String?,
/**
@@ -38,7 +40,7 @@ data class UserUpdateParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "是否已验证")
@Schema(description = "是否已验证", allowableValues = ["true", "false"], defaultValue = "false", example = "false")
val verified: Boolean = false,
/**
@@ -47,7 +49,7 @@ data class UserUpdateParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "锁定", allowableValues = ["true", "false"], defaultValue = "false")
@Schema(description = "锁定", allowableValues = ["true", "false"], defaultValue = "false", example = "false")
val locking: Boolean = false,
/**
@@ -57,7 +59,7 @@ data class UserUpdateParam(
* @since 1.0.0
* @see LocalDateTime
*/
@Schema(description = "过期时间")
@Schema(description = "过期时间", example = "1900-01-01T00:00:00.000Z")
val expiration: LocalDateTime?,
/**
@@ -67,7 +69,7 @@ data class UserUpdateParam(
* @since 1.0.0
* @see LocalDateTime
*/
@Schema(description = "认证过期时间")
@Schema(description = "认证过期时间", example = "1900-01-01T00:00:00.000Z")
val credentialsExpiration: LocalDateTime?,
/**
@@ -76,7 +78,7 @@ data class UserUpdateParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "启用", allowableValues = ["true", "false"], defaultValue = "true")
@Schema(description = "启用", allowableValues = ["true", "false"], defaultValue = "true", example = "true")
val enable: Boolean = true,
/**
@@ -85,7 +87,8 @@ data class UserUpdateParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "昵称")
@Schema(description = "昵称", required = true, example = "Nickname_1")
@field:NotBlank(message = "Nickname can not be blank")
val nickname: String?,
/**
@@ -103,7 +106,8 @@ data class UserUpdateParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "邮箱")
@Schema(description = "邮箱", required = true, example = "user@email.com")
@NotBlank(message = "Email can not be blank")
@Pattern(regexp = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*\$", message = "Illegal email address")
val email: String?,

View File

@@ -19,7 +19,7 @@ data class UserUpdatePasswordParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "用户 ID")
@Schema(description = "用户 ID", required = true)
@field:NotNull(message = "ID can not be null")
val id: Long?,
@@ -29,7 +29,7 @@ data class UserUpdatePasswordParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "新密码")
@Schema(description = "新密码", required = true)
@field:NotBlank(message = "Password can not be blank")
val password: String?,
@@ -40,6 +40,6 @@ data class UserUpdatePasswordParam(
* @since 1.0.0
* @see LocalDateTime
*/
@Schema(description = "认证过期时间")
@Schema(description = "认证过期时间", example = "1900-01-01T00:00:00.000Z")
val credentialsExpiration: LocalDateTime?
)

View File

@@ -2,6 +2,7 @@ package top.fatweb.api.param.system
import com.baomidou.mybatisplus.annotation.EnumValue
import com.fasterxml.jackson.annotation.JsonValue
import io.swagger.v3.oas.annotations.media.Schema
/**
* Get active information parameters
@@ -10,6 +11,18 @@ import com.fasterxml.jackson.annotation.JsonValue
* @since 1.0.0
*/
data class ActiveInfoGetParam(
/**
* Scope
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(
description = "范围",
allowableValues = ["WEAK", "MONTH", "QUARTER", "YEAR", "TWO_YEARS", "THREE_YEARS", "FIVE_YEARS", "ALL"],
defaultValue = "WEAK",
example = "WEAK"
)
val scope: Scope = Scope.WEAK
) {
enum class Scope(@field:EnumValue @field:JsonValue val code: String) {

View File

@@ -17,7 +17,7 @@ data class MailSendParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "接收者")
@Schema(description = "接收者", required = true, example = "user@email.com")
@field:NotBlank
val to: String?
)

View File

@@ -2,6 +2,7 @@ package top.fatweb.api.param.system
import com.baomidou.mybatisplus.annotation.EnumValue
import com.fasterxml.jackson.annotation.JsonValue
import io.swagger.v3.oas.annotations.media.Schema
/**
* Get online information parameters
@@ -10,6 +11,18 @@ import com.fasterxml.jackson.annotation.JsonValue
* @since 1.0.0
*/
data class OnlineInfoGetParam(
/**
* Scope
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(
description = "范围",
allowableValues = ["WEAK", "MONTH", "QUARTER", "YEAR", "TWO_YEARS", "THREE_YEARS", "FIVE_YEARS", "ALL"],
defaultValue = "WEAK",
example = "WEAK"
)
val scope: Scope = Scope.WEAK
) {
enum class Scope(@field:EnumValue @field:JsonValue val code: String) {

View File

@@ -20,7 +20,7 @@ data class SysLogGetParam(
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
@Schema(description = "类型过滤(多个使用逗号分隔)", example = "INFO", allowableValues = ["INFO", "ERROR"])
@Schema(description = "类型过滤(多个使用逗号分隔)", allowableValues = ["INFO", "ERROR"], example = "INFO")
val logType: String?,
/**
@@ -31,8 +31,8 @@ data class SysLogGetParam(
*/
@Schema(
description = "请求方式过滤(多个使用逗号分隔)",
example = "GET,POST",
allowableValues = ["GET", "POST", "PUT", "PATCH", "DELETE", "DELETE", "OPTIONS"]
allowableValues = ["GET", "POST", "PUT", "PATCH", "DELETE", "DELETE", "OPTIONS"],
example = "GET,POST"
)
val requestMethod: String?,
@@ -63,7 +63,7 @@ data class SysLogGetParam(
* @since 1.0.0
* @see LocalDateTime
*/
@Schema(description = "查询开始时间")
@Schema(description = "查询开始时间", example = "1900-01-01T00:00:00.000Z")
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
val searchStartTime: LocalDateTime?,
@@ -74,7 +74,7 @@ data class SysLogGetParam(
* @since 1.0.0
* @see LocalDateTime
*/
@Schema(description = "查询结束时间")
@Schema(description = "查询结束时间", example = "1900-01-01T00:00:00.000Z")
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
val searchEndTime: LocalDateTime?
) : PageSortParam()

View File

@@ -2,9 +2,7 @@ package top.fatweb.api.service.permission
import jakarta.servlet.http.HttpServletRequest
import top.fatweb.api.entity.permission.User
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.param.permission.*
import top.fatweb.api.vo.permission.LoginVo
import top.fatweb.api.vo.permission.RegisterVo
import top.fatweb.api.vo.permission.TokenVo
@@ -33,13 +31,29 @@ interface IAuthenticationService {
fun resend()
/**
* Verify
* Verify email
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
fun verify(verifyParam: VerifyParam)
/**
* Forget password
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
fun forget(request: HttpServletRequest, forgetParam: ForgetParam)
/**
* Retrieve password
*
* @author FatttSnake, fatttsnake@gmail.com
* @since 1.0.0
*/
fun retrieve(request: HttpServletRequest, retrieveParam: RetrieveParam)
/**
* Login
*

View File

@@ -1,5 +1,6 @@
package top.fatweb.api.service.permission.impl
import com.baomidou.mybatisplus.extension.kotlin.KtQueryWrapper
import com.baomidou.mybatisplus.extension.kotlin.KtUpdateWrapper
import jakarta.servlet.http.HttpServletRequest
import org.apache.velocity.VelocityContext
@@ -17,13 +18,8 @@ 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
import top.fatweb.api.exception.*
import top.fatweb.api.param.permission.*
import top.fatweb.api.properties.SecurityProperties
import top.fatweb.api.service.api.v1.IAvatarService
import top.fatweb.api.service.permission.IAuthenticationService
@@ -71,7 +67,9 @@ class AuthenticationServiceImpl(
username = registerParam.username
password = passwordEncoder.encode(registerParam.password)
verify =
"${LocalDateTime.now(ZoneOffset.UTC).toInstant(ZoneOffset.UTC).toEpochMilli()}-${UUID.randomUUID()}-${UUID.randomUUID()}-${UUID.randomUUID()}"
"${
LocalDateTime.now(ZoneOffset.UTC).toInstant(ZoneOffset.UTC).toEpochMilli()
}-${UUID.randomUUID()}-${UUID.randomUUID()}-${UUID.randomUUID()}"
locking = 0
enable = 1
}
@@ -83,7 +81,7 @@ class AuthenticationServiceImpl(
email = registerParam.email
})
sendVerifyMail(user.username!!, "http://localhost:5173/verify?code=${user.verify!!}", registerParam.email!!)
sendVerifyMail(user.username!!, user.verify!!, registerParam.email!!)
return RegisterVo(userId = user.id)
}
@@ -95,21 +93,23 @@ class AuthenticationServiceImpl(
user.verify ?: throw NoVerificationRequiredException()
user.verify =
"${LocalDateTime.now(ZoneOffset.UTC).toInstant(ZoneOffset.UTC).toEpochMilli()}-${UUID.randomUUID()}-${UUID.randomUUID()}-${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)
WebUtil.getLoginUser()?.user?.userInfo?.email?.let {
sendVerifyMail(user.username!!, "http://localhost:5173/verify?code=${user.verify!!}", it)
sendVerifyMail(user.username!!, user.verify!!, it)
} ?: throw AccessDeniedException("Access Denied")
}
private fun sendVerifyMail(username: String, verifyUrl: String, email: String) {
private fun sendVerifyMail(username: String, code: String, email: String) {
val velocityContext = VelocityContext().apply {
put("appName", "氮工具")
put("appUrl", "http://localhost:5173/")
put("username", username)
put("verifyUrl", verifyUrl)
put("verifyUrl", "http://localhost:5173/verify?code=${code}")
}
val template = velocityEngine.getTemplate("templates/email-verify-account-cn.vm")
@@ -147,6 +147,82 @@ class AuthenticationServiceImpl(
)
}
@Transactional
override fun forget(request: HttpServletRequest, forgetParam: ForgetParam) {
val user = userService.getUserWithPowerByAccount(forgetParam.email!!)
user ?: let { throw UserNotFoundException() }
val code = "${
LocalDateTime.now(ZoneOffset.UTC).toInstant(ZoneOffset.UTC).toEpochMilli()
}-${UUID.randomUUID()}-${UUID.randomUUID()}-${UUID.randomUUID()}"
userService.update(KtUpdateWrapper(User()).eq(User::id, user.id).set(User::forget, code))
sendRetrieveMail(user.username!!, request.remoteAddr, code, forgetParam.email)
}
private fun sendRetrieveMail(username: String, ip: String, code: String, email: String) {
val velocityContext = VelocityContext().apply {
put("appName", "氮工具")
put("appUrl", "http://localhost:5173/")
put("username", username)
put("ipAddress", ip)
put("retrieveUrl", "http://localhost:5173/forget?code=${code}")
}
val template = velocityEngine.getTemplate("templates/email-retrieve-password-cn.vm")
val stringWriter = StringWriter()
template.merge(velocityContext, stringWriter)
MailUtil.sendSimpleMail(
"找回您的密码", stringWriter.toString(), true,
email
)
}
@Transactional
override fun retrieve(request: HttpServletRequest, retrieveParam: RetrieveParam) {
val codeStrings = retrieveParam.code!!.split("-")
if (codeStrings.size != 16) {
throw RetrieveCodeErrorOrExpiredException()
}
try {
if (LocalDateTime.ofInstant(Instant.ofEpochMilli(codeStrings.first().toLong()), ZoneOffset.UTC)
.isBefore(LocalDateTime.now(ZoneOffset.UTC).minusHours(2))
) {
throw RetrieveCodeErrorOrExpiredException()
}
} catch (e: Exception) {
throw RetrieveCodeErrorOrExpiredException()
}
val user = userService.getOne(KtQueryWrapper(User()).eq(User::forget, retrieveParam.code))
?: throw RetrieveCodeErrorOrExpiredException()
val userInfo = userInfoService.getOne(KtQueryWrapper(UserInfo()).eq(UserInfo::userId, user.id))
userService.update(
KtUpdateWrapper(User()).eq(User::id, user.id).set(User::forget, null)
.set(User::password, passwordEncoder.encode(retrieveParam.password!!))
)
sendPasswordChangedMail(user.username!!, request.remoteAddr, userInfo!!.email!!)
}
private fun sendPasswordChangedMail(username: String, ip: String, email: String) {
val velocityContext = VelocityContext().apply {
put("appName", "氮工具")
put("appUrl", "http://localhost:5173/")
put("username", username)
put("ipAddress", ip)
}
val template = velocityEngine.getTemplate("templates/email-password-changed-cn.vm")
val stringWriter = StringWriter()
template.merge(velocityContext, stringWriter)
MailUtil.sendSimpleMail(
"您的密码已更改", stringWriter.toString(), true,
email
)
}
@EventLogRecord(EventLog.Event.LOGIN)
override fun login(request: HttpServletRequest, loginParam: LoginParam): LoginVo {
val usernamePasswordAuthenticationToken =

View File

@@ -5,7 +5,8 @@ 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(144) null comment '验证信息',
verify varchar(144) null comment '验证邮箱',
forget varchar(144) null comment '忘记密码',
locking int not null comment '锁定',
expiration datetime comment '过期时间',
credentials_expiration datetime comment '认证过期时间',
@@ -19,5 +20,6 @@ create table if not exists t_user
deleted bigint not null default 0,
version int not null default 0,
constraint t_user_unique_username unique (username, deleted),
constraint t_user_unique_verify unique (verify, deleted)
constraint t_user_unique_verify unique (verify, deleted),
constraint t_user_unique_forget unique (forget, deleted)
) comment '用户表';

View File

@@ -0,0 +1,78 @@
<!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;
}
.retrieve-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;更&nbsp;改</div>
<div><strong>${username}</strong>,您好:</div>
<div style="text-indent: 2em">您在 <a target="_blank" href=${appUrl}>${appName}(${appUrl})</a> 的密码已更改,操作
IP 地址为 [${ipAddress}]。如果不是您自己的操作,请尽快检查您的账号!
</div>
<div class="not-reply">此邮件由系统自动发送,请勿回复!</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,90 @@
<!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;
}
.retrieve-button {
display: flex;
margin: 20px;
padding-bottom: 60px;
justify-content: center;
}
.retrieve-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> 的密码,操作
IP 地址为 [${ipAddress}],如果是您自己的操作,请在 <u><i>两小时内</i></u> 点击下面的按钮找回您的密码:
</div>
<div class="retrieve-button"><a target="_blank" href=${retrieveUrl}>找回密码</a></div>
<div>如果以上按钮无法点击,请复制此链接到浏览器地址栏中访问:<a target="_blank"
href=${retrieveUrl}>${retrieveUrl}</a></div>
<div>如果并非本人操作,请忽略该邮件</div>
<div class="not-reply">此邮件由系统自动发送,请勿回复!</div>
</div>
</div>
</body>
</html>

View File

@@ -54,6 +54,7 @@
justify-content: center;
}
.verify-button a {
padding: 20px 30px;
color: white;
@@ -75,9 +76,12 @@
<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 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>如果以上按钮无法点击,请复制此链接到浏览器地址栏中访问:<a target="_blank" href=${verifyUrl}>${verifyUrl}</a>
</div>
<div class="not-reply">此邮件由系统自动发送,请勿回复!</div>
</div>
</div>