From 8f98d020447b3bbef6ce514babbf08c0b096069b Mon Sep 17 00:00:00 2001 From: zihluwang Date: Tue, 11 Aug 2026 13:32:58 +0800 Subject: [PATCH] feat(auth): add user registration endpoint - Validate username, email and password on register - Reject duplicate usernames and email addresses - Create user with LOCAL credential and default USER role Closes #3 --- .../controller/AuthController.java | 8 +++ .../domain/dto/RegisterRequest.java | 29 +++++++++++ .../deltaforceguide/manager/UserManager.java | 43 +++++++++++++++- .../deltaforceguide/service/AuthService.java | 50 ++++++++++++++++++- 4 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/onixbyte/deltaforceguide/domain/dto/RegisterRequest.java diff --git a/src/main/java/com/onixbyte/deltaforceguide/controller/AuthController.java b/src/main/java/com/onixbyte/deltaforceguide/controller/AuthController.java index 4648bb3..e1071ed 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/controller/AuthController.java +++ b/src/main/java/com/onixbyte/deltaforceguide/controller/AuthController.java @@ -1,6 +1,7 @@ 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.security.annotation.RequiresAuth; import com.onixbyte.deltaforceguide.service.AuthService; @@ -46,4 +47,11 @@ public class AuthController { .header(HttpHeaders.SET_COOKIE, expiredCookie.toString()) .build(); } + + @Operation(description = "用户注册") + @PostMapping("/register") + public ResponseEntity register(@Validated @RequestBody RegisterRequest request) { + var user = authService.register(request); + return authService.getUserResponseResponseEntity(user); + } } diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/dto/RegisterRequest.java b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/RegisterRequest.java new file mode 100644 index 0000000..92b8040 --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/dto/RegisterRequest.java @@ -0,0 +1,29 @@ +package com.onixbyte.deltaforceguide.domain.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +/** + * Registration request containing the new user account information. + * + * @author zihluwang + */ +@Schema(description = "注册请求") +public record RegisterRequest( + @NotBlank(message = "用户名不能为空") + @Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED) + String username, + @NotBlank(message = "邮箱不能为空") + @Email(message = "邮箱格式不正确") + @Schema(description = "邮箱", requiredMode = Schema.RequiredMode.REQUIRED) + String email, + @Schema(description = "昵称") + String nickname, + @Schema(description = "头像链接") + String avatarUrl, + @NotBlank(message = "密码不能为空") + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED) + String password +) { +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/manager/UserManager.java b/src/main/java/com/onixbyte/deltaforceguide/manager/UserManager.java index d63c0f7..47b108d 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/manager/UserManager.java +++ b/src/main/java/com/onixbyte/deltaforceguide/manager/UserManager.java @@ -1,7 +1,9 @@ package com.onixbyte.deltaforceguide.manager; import com.onixbyte.deltaforceguide.domain.entity.User; +import com.onixbyte.deltaforceguide.domain.entity.UserRole; import com.onixbyte.deltaforceguide.repository.UserRepository; +import com.onixbyte.deltaforceguide.repository.UserRoleRepository; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -17,9 +19,11 @@ import java.util.Optional; public class UserManager { private final UserRepository userRepository; + private final UserRoleRepository userRoleRepository; - public UserManager(UserRepository userRepository) { + public UserManager(UserRepository userRepository, UserRoleRepository userRoleRepository) { this.userRepository = userRepository; + this.userRoleRepository = userRoleRepository; } /** @@ -83,5 +87,42 @@ public class UserManager { public Optional findByUsernameOrEmail(String principal) { return userRepository.findByUsernameOrEmail(principal); } + + /** + * Checks whether a username is already registered. + * + * @param username the username to check + * @return true if the username is already taken + */ + public boolean existsByUsername(String username) { + return userRepository.existsByUsername(username); + } + + /** + * Checks whether an email address is already registered. + * + * @param email the email address to check + * @return true if the email address is already taken + */ + public boolean existsByEmail(String email) { + return userRepository.existsByEmail(email); + } + + /** + * Persists a new user together with an initial role assignment in a single transaction. + * + * @param user the user to persist, whose credentials are cascaded + * @param role the initial role name to assign + * @return the saved user + */ + @Transactional + public User createWithRole(User user, String role) { + var saved = userRepository.save(user); + userRoleRepository.save(UserRole.builder() + .user(saved) + .role(role) + .build()); + return saved; + } } diff --git a/src/main/java/com/onixbyte/deltaforceguide/service/AuthService.java b/src/main/java/com/onixbyte/deltaforceguide/service/AuthService.java index 99743b2..82ffbb0 100644 --- a/src/main/java/com/onixbyte/deltaforceguide/service/AuthService.java +++ b/src/main/java/com/onixbyte/deltaforceguide/service/AuthService.java @@ -2,12 +2,18 @@ package com.onixbyte.deltaforceguide.service; 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.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.InternalServerErrorException; import com.onixbyte.deltaforceguide.manager.CookieManager; +import com.onixbyte.deltaforceguide.manager.UserManager; import com.onixbyte.deltaforceguide.security.authentication.UsernamePasswordAuthentication; import com.onixbyte.deltaforceguide.shared.CookieName; +import com.onixbyte.deltaforceguide.shared.CredentialProvider; +import com.onixbyte.deltaforceguide.shared.Role; import org.jspecify.annotations.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,6 +21,7 @@ import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import java.time.Duration; @@ -32,15 +39,21 @@ public class AuthService { private final AuthenticationManager authenticationManager; private final TokenClient tokenClient; private final CookieManager cookieManager; + private final UserManager userManager; + private final PasswordEncoder passwordEncoder; public AuthService( AuthenticationManager authenticationManager, TokenClient tokenClient, - CookieManager cookieManager + CookieManager cookieManager, + UserManager userManager, + PasswordEncoder passwordEncoder ) { this.authenticationManager = authenticationManager; this.tokenClient = tokenClient; this.cookieManager = cookieManager; + this.userManager = userManager; + this.passwordEncoder = passwordEncoder; } /** @@ -66,6 +79,41 @@ public class AuthService { return authentication.getDetails(); } + /** + * Registers a new user account with the LOCAL password provider and the default USER role. + * + * @param request the registration information + * @return the newly created {@link User} + */ + public User register(RegisterRequest request) { + var username = request.username().trim(); + var email = request.email().trim(); + + if (userManager.existsByUsername(username)) { + throw new BadRequestException("用户名已被注册。"); + } + if (userManager.existsByEmail(email)) { + throw new BadRequestException("该邮箱已被注册。"); + } + + var now = LocalDateTime.now(); + var user = User.builder() + .username(username) + .email(email) + .nickname(request.nickname()) + .avatarUrl(request.avatarUrl()) + .createdAt(now) + .updatedAt(now) + .build(); + var credential = UserCredential.builder() + .provider(CredentialProvider.LOCAL) + .credential(passwordEncoder.encode(request.password())) + .build(); + user.addCredential(credential); + + return userManager.createWithRole(user, Role.ROLE_USER); + } + @NonNull public ResponseEntity getUserResponseResponseEntity(User user) { var currentTime = LocalDateTime.now();