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
This commit is contained in:
zihluwang committed 2026-08-14 00:27:05 +08:00
1 parent 1aeefb6733
commit 8f98d02044
4 files changed
+128 -2

No files matched your search

@@ -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<UserResponse> register(@Validated @RequestBody RegisterRequest request) {
var user = authService.register(request);
return authService.getUserResponseResponseEntity(user);
}
}
@@ -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
) {
}
@@ -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<User> 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;
}
}
@@ -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<UserResponse> getUserResponseResponseEntity(User user) {
var currentTime = LocalDateTime.now();