feat(auth): add email verification codes
- Add POST /auth/verification-code endpoint to email a code - Store codes in Redis and verify them during registration - Send verification emails asynchronously via Thymeleaf - Replace email_verified column with code-based verification - Group dependencies into version catalogue bundles
This commit is contained in:
23 files changed
+440
-83
No files matched your search
@@ -2,12 +2,14 @@ package com.onixbyte.deltaforceguide;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
|
||||
/**
|
||||
* Entry point for the Delta Force Guide Server application.
|
||||
*
|
||||
* @author zihluwang
|
||||
*/
|
||||
@EnableAsync
|
||||
@SpringBootApplication
|
||||
public class DeltaForceGuideApplication {
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.onixbyte.deltaforceguide.client;
|
||||
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.InternetAddress;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.thymeleaf.TemplateEngine;
|
||||
import org.thymeleaf.context.Context;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class EmailClient {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(EmailClient.class);
|
||||
|
||||
private final JavaMailSender mailSender;
|
||||
|
||||
private final TemplateEngine emailTemplateEngine;
|
||||
|
||||
@Value("${spring.mail.username}")
|
||||
private String fromAddress;
|
||||
|
||||
@Autowired
|
||||
public EmailClient(
|
||||
JavaMailSender mailSender,
|
||||
TemplateEngine emailTemplateEngine
|
||||
) {
|
||||
this.mailSender = mailSender;
|
||||
this.emailTemplateEngine = emailTemplateEngine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a template-based HTML email asynchronously.
|
||||
*
|
||||
* @param toRecipient recipient email address
|
||||
* @param subject subject line
|
||||
* @param templateName template name (without .html extension, under mail-templates/)
|
||||
* @param variables context variables required by the template
|
||||
*/
|
||||
@Async
|
||||
public void sendTemplateMail(String toRecipient, String subject, String templateName, Map<String, Object> variables) {
|
||||
try {
|
||||
// Render HTML content using the dedicated email template engine
|
||||
var context = new Context();
|
||||
if (variables != null && !variables.isEmpty()) {
|
||||
context.setVariables(variables);
|
||||
}
|
||||
var htmlBody = emailTemplateEngine.process(templateName, context);
|
||||
|
||||
log.debug("htmlBody={}", htmlBody);
|
||||
|
||||
// Build the MIME message
|
||||
var message = mailSender.createMimeMessage();
|
||||
var helper = new MimeMessageHelper(
|
||||
message,
|
||||
MimeMessageHelper.MULTIPART_MODE_MIXED_RELATED,
|
||||
StandardCharsets.UTF_8.name()
|
||||
);
|
||||
|
||||
helper.setFrom(new InternetAddress(fromAddress, "阿萨拉向导", StandardCharsets.UTF_8.name()));
|
||||
helper.setTo(toRecipient);
|
||||
helper.setSubject(subject);
|
||||
helper.setText(htmlBody, true);
|
||||
|
||||
// Dispatch the message
|
||||
mailSender.send(message);
|
||||
log.info("Successfully dispatched email to: [{}] with subject: [{}]", toRecipient, subject);
|
||||
|
||||
} catch (MessagingException | UnsupportedEncodingException ex) {
|
||||
log.error("Failed to construct or send email to: [{}], subject: [{}]", toRecipient, subject, ex);
|
||||
// Handle or rethrow custom business exception if required
|
||||
} catch (Exception ex) {
|
||||
log.error("Unexpected error occurred while processing email for: [{}]", toRecipient, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method specifically for sending registration/login verification codes.
|
||||
*
|
||||
* @param toRecipient target recipient email
|
||||
* @param username display username
|
||||
* @param code verification code string
|
||||
* @param expireMinutes expiration time in minutes
|
||||
*/
|
||||
@Async
|
||||
public void sendVerificationCode(String toRecipient, String username, String code, int expireMinutes) {
|
||||
var subject = "【阿萨拉向导】账号注册验证码";
|
||||
var variables = Map.<String, Object>of(
|
||||
"username", username,
|
||||
"code", code,
|
||||
"expireMinutes", expireMinutes
|
||||
);
|
||||
|
||||
sendTemplateMail(toRecipient, subject, "verification-code", variables);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.onixbyte.deltaforceguide.config;
|
||||
|
||||
import com.onixbyte.captcha.text.TextProducer;
|
||||
import com.onixbyte.captcha.text.impl.DefaultTextProducer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.thymeleaf.TemplateEngine;
|
||||
import org.thymeleaf.spring6.SpringTemplateEngine;
|
||||
import org.thymeleaf.templatemode.TemplateMode;
|
||||
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
|
||||
import org.thymeleaf.templateresolver.ITemplateResolver;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@Configuration
|
||||
public class EmailTemplateConfig {
|
||||
|
||||
@Bean
|
||||
public TemplateEngine emailTemplateEngine() {
|
||||
var resolver = new ClassLoaderTemplateResolver();
|
||||
// Template files are located under src/main/resources/mail-templates/
|
||||
resolver.setPrefix("mail-templates/");
|
||||
resolver.setSuffix(".html");
|
||||
resolver.setTemplateMode(TemplateMode.HTML);
|
||||
resolver.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
resolver.setCacheable(true); // Enable caching for the production environment
|
||||
|
||||
var templateEngine = new SpringTemplateEngine();
|
||||
templateEngine.addTemplateResolver(resolver);
|
||||
return templateEngine;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TextProducer textProducer() {
|
||||
return DefaultTextProducer.builder().build();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package com.onixbyte.deltaforceguide.controller;
|
||||
|
||||
import com.onixbyte.deltaforceguide.domain.dto.LoginRequest;
|
||||
import com.onixbyte.deltaforceguide.domain.dto.RegisterRequest;
|
||||
import com.onixbyte.deltaforceguide.domain.dto.UserResponse;
|
||||
import com.onixbyte.deltaforceguide.domain.dto.*;
|
||||
import com.onixbyte.deltaforceguide.security.annotation.RequiresAuth;
|
||||
import com.onixbyte.deltaforceguide.service.AuthService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -48,6 +46,13 @@ public class AuthController {
|
||||
.build();
|
||||
}
|
||||
|
||||
@Operation(description = "发送邮件验证码")
|
||||
@PostMapping("/verification-code")
|
||||
public SendVerificationCodeResponse sendVerificationCode(@Validated @RequestBody SendVerificationCodeRequest request) {
|
||||
var verificationCodeId = authService.sendVerificationCode(request);
|
||||
return new SendVerificationCodeResponse(verificationCodeId);
|
||||
}
|
||||
|
||||
@Operation(description = "用户注册")
|
||||
@PostMapping("/register")
|
||||
public ResponseEntity<UserResponse> register(@Validated @RequestBody RegisterRequest request) {
|
||||
|
||||
@@ -24,6 +24,8 @@ public record RegisterRequest(
|
||||
String avatarUrl,
|
||||
@NotBlank(message = "密码不能为空")
|
||||
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
String password
|
||||
String password,
|
||||
@NotBlank(message = "验证码 ID 不能为空") String verificationCodeId,
|
||||
@NotBlank(message = "验证码不能为空") String verificationCode
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.onixbyte.deltaforceguide.domain.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record SendVerificationCodeRequest(
|
||||
@NotBlank(message = "用户名不能为空") String username,
|
||||
@NotBlank(message = "电子邮箱地址不能为空") String email
|
||||
) {
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.onixbyte.deltaforceguide.domain.dto;
|
||||
|
||||
public record SendVerificationCodeResponse(
|
||||
String verificationCodeId
|
||||
) {
|
||||
}
|
||||
@@ -10,9 +10,5 @@ import jakarta.validation.constraints.Size;
|
||||
*/
|
||||
public record UpdateProfileRequest(
|
||||
@Size(min = 1, max = 64, message = "昵称长度必须在1-64个字符之间")
|
||||
String nickname,
|
||||
|
||||
@Email(message = "请输入正确的电子邮箱地址")
|
||||
@Size(max = 255, message = "电子邮箱长度不能超过255个字符")
|
||||
String email
|
||||
String nickname
|
||||
) {}
|
||||
@@ -16,7 +16,6 @@ public record UserProfileResponse(
|
||||
String nickname,
|
||||
String email,
|
||||
String avatarUrl,
|
||||
Boolean emailVerified,
|
||||
Boolean enabled,
|
||||
LocalDateTime createdAt,
|
||||
List<String> roles
|
||||
@@ -37,7 +36,6 @@ public record UserProfileResponse(
|
||||
? user.getUsername() : user.getNickname(),
|
||||
user.getEmail(),
|
||||
user.getAvatarUrl(),
|
||||
user.getEmailVerified(),
|
||||
user.getEnabled(),
|
||||
user.getCreatedAt(),
|
||||
roles
|
||||
|
||||
@@ -39,9 +39,6 @@ public class User {
|
||||
@Column(name = "avatar_url")
|
||||
private String avatarUrl;
|
||||
|
||||
@Column(name = "email_verified", nullable = false)
|
||||
private Boolean emailVerified = false;
|
||||
|
||||
@Column(name = "enabled", nullable = false)
|
||||
private Boolean enabled = true;
|
||||
|
||||
@@ -94,14 +91,6 @@ public class User {
|
||||
this.avatarUrl = avatarUrl;
|
||||
}
|
||||
|
||||
public Boolean getEmailVerified() {
|
||||
return emailVerified;
|
||||
}
|
||||
|
||||
public void setEmailVerified(Boolean emailVerified) {
|
||||
this.emailVerified = emailVerified;
|
||||
}
|
||||
|
||||
public Boolean getEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
@@ -160,7 +149,6 @@ public class User {
|
||||
private String email;
|
||||
private String nickname;
|
||||
private String avatarUrl;
|
||||
private Boolean emailVerified;
|
||||
private Boolean enabled;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
@@ -191,11 +179,6 @@ public class User {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder emailVerified(Boolean emailVerified) {
|
||||
this.emailVerified = emailVerified;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder enabled(Boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
return this;
|
||||
@@ -224,7 +207,6 @@ public class User {
|
||||
user.nickname = this.nickname == null || this.nickname.isBlank()
|
||||
? this.username : this.nickname;
|
||||
user.avatarUrl = this.avatarUrl;
|
||||
user.emailVerified = this.emailVerified != null && this.emailVerified;
|
||||
user.enabled = this.enabled == null || this.enabled;
|
||||
user.createdAt = this.createdAt;
|
||||
user.updatedAt = this.updatedAt;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.onixbyte.deltaforceguide.exeption;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
public class ConflictException extends ResponseStatusException {
|
||||
|
||||
public ConflictException(String reason) {
|
||||
super(HttpStatus.CONFLICT, reason);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.onixbyte.deltaforceguide.repository;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
public interface BaseRepository<T, K> extends JpaRepository<T, K>, JpaSpecificationExecutor<T> {
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import com.onixbyte.deltaforceguide.enumeration.FirearmType;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
@@ -15,7 +16,7 @@ import java.util.List;
|
||||
* @author zihluwang
|
||||
*/
|
||||
@Repository
|
||||
public interface FirearmRepository extends BaseRepository<Firearm, Long> {
|
||||
public interface FirearmRepository extends JpaRepository<Firearm, Long>, JpaSpecificationExecutor<Firearm> {
|
||||
|
||||
Page<Firearm> findAllByType(FirearmType type, Pageable pageable);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.EntityGraph;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
@@ -20,7 +21,7 @@ import java.util.Optional;
|
||||
* @author zihluwang
|
||||
*/
|
||||
@Repository
|
||||
public interface ModificationRepository extends BaseRepository<Modification, Long> {
|
||||
public interface ModificationRepository extends JpaRepository<Modification, Long>, JpaSpecificationExecutor<Modification> {
|
||||
|
||||
@EntityGraph(attributePaths = {"firearm"})
|
||||
Page<Modification> findAllBy(Pageable pageable);
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.onixbyte.deltaforceguide.domain.entity.User;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
import org.springframework.data.jpa.repository.EntityGraph;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
@@ -16,7 +17,7 @@ import java.util.Optional;
|
||||
* @author zihluwang
|
||||
*/
|
||||
@Repository
|
||||
public interface UserRepository extends BaseRepository<User, Long> {
|
||||
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
|
||||
|
||||
@Override
|
||||
@EntityGraph(attributePaths = {"credentials"})
|
||||
|
||||
@@ -2,10 +2,7 @@ package com.onixbyte.deltaforceguide.repository;
|
||||
|
||||
import com.onixbyte.deltaforceguide.domain.entity.UserRole;
|
||||
import com.onixbyte.deltaforceguide.domain.entity.UserRoleId;
|
||||
import org.springframework.data.jpa.repository.EntityGraph;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.jpa.repository.*;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@@ -17,7 +14,7 @@ import java.util.List;
|
||||
* @author zihluwang
|
||||
*/
|
||||
@Repository
|
||||
public interface UserRoleRepository extends BaseRepository<UserRole, UserRoleId> {
|
||||
public interface UserRoleRepository extends JpaRepository<UserRole, UserRoleId>, JpaSpecificationExecutor<UserRole> {
|
||||
|
||||
/**
|
||||
* Find all roles assigned to a given user.
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package com.onixbyte.deltaforceguide.service;
|
||||
|
||||
import com.onixbyte.captcha.text.TextProducer;
|
||||
import com.onixbyte.deltaforceguide.client.EmailClient;
|
||||
import com.onixbyte.deltaforceguide.client.TokenClient;
|
||||
import com.onixbyte.deltaforceguide.domain.dto.LoginRequest;
|
||||
import com.onixbyte.deltaforceguide.domain.dto.RegisterRequest;
|
||||
import com.onixbyte.deltaforceguide.domain.dto.SendVerificationCodeRequest;
|
||||
import com.onixbyte.deltaforceguide.domain.dto.UserResponse;
|
||||
import com.onixbyte.deltaforceguide.domain.entity.User;
|
||||
import com.onixbyte.deltaforceguide.domain.entity.UserCredential;
|
||||
import com.onixbyte.deltaforceguide.exeption.BadRequestException;
|
||||
import com.onixbyte.deltaforceguide.exeption.ConflictException;
|
||||
import com.onixbyte.deltaforceguide.exeption.InternalServerErrorException;
|
||||
import com.onixbyte.deltaforceguide.manager.CookieManager;
|
||||
import com.onixbyte.deltaforceguide.manager.UserManager;
|
||||
@@ -17,6 +21,8 @@ import com.onixbyte.deltaforceguide.shared.Role;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.http.HttpCookie;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -26,6 +32,7 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Service handling user authentication, login, and session management.
|
||||
@@ -41,19 +48,31 @@ public class AuthService {
|
||||
private final CookieManager cookieManager;
|
||||
private final UserManager userManager;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final TextProducer textProducer;
|
||||
private final CacheManager cacheManager;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final EmailClient emailClient;
|
||||
|
||||
public AuthService(
|
||||
AuthenticationManager authenticationManager,
|
||||
TokenClient tokenClient,
|
||||
CookieManager cookieManager,
|
||||
UserManager userManager,
|
||||
PasswordEncoder passwordEncoder
|
||||
PasswordEncoder passwordEncoder,
|
||||
TextProducer textProducer,
|
||||
CacheManager cacheManager,
|
||||
RedisTemplate<String, Object> redisTemplate,
|
||||
EmailClient emailClient
|
||||
) {
|
||||
this.authenticationManager = authenticationManager;
|
||||
this.tokenClient = tokenClient;
|
||||
this.cookieManager = cookieManager;
|
||||
this.userManager = userManager;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.textProducer = textProducer;
|
||||
this.cacheManager = cacheManager;
|
||||
this.redisTemplate = redisTemplate;
|
||||
this.emailClient = emailClient;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,6 +115,19 @@ public class AuthService {
|
||||
throw new BadRequestException("该邮箱已被注册。");
|
||||
}
|
||||
|
||||
var code = request.verificationCode();
|
||||
var verificationCodeId = request.verificationCodeId();
|
||||
|
||||
var storedCode = redisTemplate.opsForValue().get("verification-code:" + verificationCodeId);
|
||||
if (!(storedCode instanceof String strStoredCode)) {
|
||||
log.error("storedCode not a String, storedCode={}", storedCode);
|
||||
throw new InternalServerErrorException("系统内部错误,请重试");
|
||||
}
|
||||
|
||||
if (!code.equalsIgnoreCase(strStoredCode)) {
|
||||
throw new BadRequestException("邮箱验证码不正确,请重试。");
|
||||
}
|
||||
|
||||
var now = LocalDateTime.now();
|
||||
var user = User.builder()
|
||||
.username(username)
|
||||
@@ -129,4 +161,39 @@ public class AuthService {
|
||||
public HttpCookie logout() {
|
||||
return cookieManager.buildCookie(CookieName.ACCESS_TOKEN, "", Duration.ZERO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates and dispatches a verification code to the given email address.
|
||||
* <p>
|
||||
* The code is cached under the returned UUID for 10 minutes and emailed
|
||||
* asynchronously to the recipient. The UUID must be supplied back to the
|
||||
* server when verifying the code.
|
||||
*
|
||||
* @param request the verification code request containing the target username and email
|
||||
* @return the UUID identifying the verification record, to be used when verifying the code
|
||||
* @throws ConflictException if the username or email is already registered
|
||||
*/
|
||||
public String sendVerificationCode(SendVerificationCodeRequest request) {
|
||||
// Check username.
|
||||
if (userManager.existsByUsername(request.username())) {
|
||||
throw new ConflictException("用户名【" + request.username() + "】已被使用。");
|
||||
}
|
||||
|
||||
// Check email address.
|
||||
if (userManager.existsByEmail(request.email())) {
|
||||
throw new ConflictException("邮箱地址【" + request.email() + "】已被使用。");
|
||||
}
|
||||
|
||||
// Generate UUID and verification code.
|
||||
var uuid = UUID.randomUUID().toString().replace("-", "");
|
||||
var code = textProducer.getText();
|
||||
|
||||
// Save verification code to cache.
|
||||
redisTemplate.opsForValue().set("verification-code:" + uuid, code, Duration.ofMinutes(10L));
|
||||
|
||||
// Send email.
|
||||
emailClient.sendVerificationCode(request.email(), request.username(), code, 10);
|
||||
|
||||
return uuid;
|
||||
}
|
||||
}
|
||||
@@ -196,10 +196,7 @@ public class UserService {
|
||||
if (StringUtils.hasText(request.nickname())) {
|
||||
user.setNickname(request.nickname());
|
||||
}
|
||||
if (StringUtils.hasText(request.email()) && !Objects.equals(user.getEmail(), request.email())) {
|
||||
user.setEmail(request.email());
|
||||
user.setEmailVerified(false);
|
||||
}
|
||||
|
||||
userManager.save(user);
|
||||
return getProfile(user);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ spring:
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
thymeleaf:
|
||||
enabled: false
|
||||
|
||||
mybatis:
|
||||
configuration:
|
||||
@@ -40,6 +42,8 @@ mybatis:
|
||||
mapper-locations: classpath:/mapper/*.xml
|
||||
|
||||
logging:
|
||||
pattern:
|
||||
dateformat: dd MMM yyyy HH:mm:ss
|
||||
level:
|
||||
org.hibernate:
|
||||
orm.connections.pooling: off
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
ALTER TABLE app_user
|
||||
ADD COLUMN nickname VARCHAR(64),
|
||||
ADD COLUMN avatar_url VARCHAR(512),
|
||||
ADD COLUMN email_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
ADD COLUMN created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
ADD COLUMN updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
||||
@@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>账号验证</title>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background-color: #f1f5f9; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f1f5f9; padding: 40px 16px;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<!-- 主容器 -->
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 520px; background-color: #ffffff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 12px rgba(15, 23, 42, 0.06); border: 1px solid #e2e8f0;">
|
||||
|
||||
<!-- 顶部标题 -->
|
||||
<tr>
|
||||
<td align="center" style="padding: 36px 32px 24px; background-color: #ffffff;">
|
||||
<h1 style="margin: 0; font-size: 22px; font-weight: 700; color: #0f172a; letter-spacing: -0.3px;" th:text="${appName != null ? appName : '账号注册验证'}">账号注册验证</h1>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<tr>
|
||||
<td style="padding: 0 32px;">
|
||||
<div style="height: 1px; background-color: #f1f5f9; width: 100%;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- 正文内容 -->
|
||||
<tr>
|
||||
<td style="padding: 32px 32px 24px;">
|
||||
<p style="margin: 0 0 16px; font-size: 15px; line-height: 1.6; color: #334155;">
|
||||
您好 <strong th:text="${username}">用户</strong>,
|
||||
</p>
|
||||
<p style="margin: 0 0 24px; font-size: 14px; line-height: 1.6; color: #64748b;">
|
||||
感谢注册我们的服务。请使用下方的验证码完成账号验证,该验证码在 <strong style="color: #334155;"><span th:text="${expireMinutes}">10</span> 分钟</strong> 内有效:
|
||||
</p>
|
||||
|
||||
<!-- 验证码展示区域 -->
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="margin: 8px 0 24px;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 20px; background-color: #f8fafc; border: 1px dashed #cbd5e1; border-radius: 8px;">
|
||||
<span style="font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace; font-size: 32px; font-weight: 700; color: #2563eb; letter-spacing: 8px; margin-left: 8px;" th:text="${code}">123456</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p style="margin: 0 0 12px; font-size: 13px; line-height: 1.6; color: #94a3b8;">
|
||||
若您未发起此操作或未尝试注册该账号,请直接忽略本邮件。
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<tr>
|
||||
<td align="center" style="padding: 24px 32px; background-color: #f8fafc; border-top: 1px solid #e2e8f0;">
|
||||
<p style="margin: 0 0 6px; font-size: 12px; line-height: 1.4; color: #94a3b8;">
|
||||
此邮件为系统自动发送,请勿直接回复。
|
||||
</p>
|
||||
<p style="margin: 0; font-size: 12px; color: #cbd5e1;">
|
||||
© <span th:text="${#calendars.year(#calendars.createNow())}">2026</span> <span>阿萨拉向导</span>. 保留所有权利。
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user