1f1d1db65a
- 后端:User 实体新增 role/enabled 字段、UserController CRUD、UserService - 安全:SecurityConfig /api/users/** 要求 ROLE_ADMIN、JWT 过滤器检查账号状态 - 注册:POST /api/auth/register 公开注册,固定 ROLE_USER - 保护:删除/禁用/降级最后 admin 均拒绝,DataInitializer 含 backfill - 前端:用户管理页面、登录/注册切换、admin 专属导航入口 - 测试:UserServiceTest 19 个 + UserControllerTest 6 个 + AuthControllerTest 适配
181 lines
7.1 KiB
Java
181 lines
7.1 KiB
Java
package com.sshmanager.service;
|
|
|
|
import com.sshmanager.dto.CreateUserRequest;
|
|
import com.sshmanager.dto.UpdateUserRequest;
|
|
import com.sshmanager.dto.UserDto;
|
|
import com.sshmanager.entity.User;
|
|
import com.sshmanager.exception.InvalidOperationException;
|
|
import com.sshmanager.exception.NotFoundException;
|
|
import com.sshmanager.repository.UserRepository;
|
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
import org.springframework.stereotype.Service;
|
|
|
|
import java.time.Instant;
|
|
import java.util.List;
|
|
import java.util.stream.Collectors;
|
|
|
|
@Service
|
|
public class UserService {
|
|
|
|
private final UserRepository userRepository;
|
|
private final PasswordEncoder passwordEncoder;
|
|
|
|
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
|
|
this.userRepository = userRepository;
|
|
this.passwordEncoder = passwordEncoder;
|
|
}
|
|
|
|
public List<UserDto> listUsers() {
|
|
return userRepository.findAll().stream()
|
|
.map(this::toDto)
|
|
.collect(Collectors.toList());
|
|
}
|
|
|
|
public UserDto getUser(Long id) {
|
|
User user = userRepository.findById(id)
|
|
.orElseThrow(() -> new NotFoundException("User not found: " + id));
|
|
return toDto(user);
|
|
}
|
|
|
|
public UserDto createUser(CreateUserRequest request) {
|
|
if (request.getUsername() == null || request.getUsername().trim().isEmpty()) {
|
|
throw new InvalidOperationException("Username is required");
|
|
}
|
|
if (request.getPassword() == null || request.getPassword().length() < 8) {
|
|
throw new InvalidOperationException("Password must be at least 8 characters");
|
|
}
|
|
if (userRepository.existsByUsername(request.getUsername().trim())) {
|
|
throw new InvalidOperationException("Username already exists: " + request.getUsername());
|
|
}
|
|
|
|
User user = new User();
|
|
user.setUsername(request.getUsername().trim());
|
|
user.setPasswordHash(passwordEncoder.encode(request.getPassword()));
|
|
user.setDisplayName(request.getDisplayName() != null ? request.getDisplayName().trim() : request.getUsername().trim());
|
|
user.setRole(request.getRole() != null ? request.getRole() : "ROLE_USER");
|
|
if (!"ROLE_ADMIN".equals(user.getRole()) && !"ROLE_USER".equals(user.getRole())) {
|
|
user.setRole("ROLE_USER");
|
|
}
|
|
user.setEnabled(true);
|
|
user.setPasswordChangedAt(Instant.now());
|
|
user.setCreatedAt(Instant.now());
|
|
user.setUpdatedAt(Instant.now());
|
|
|
|
User saved = userRepository.save(user);
|
|
return toDto(saved);
|
|
}
|
|
|
|
public UserDto updateUser(Long id, UpdateUserRequest request, Long currentUserId) {
|
|
User user = userRepository.findById(id)
|
|
.orElseThrow(() -> new NotFoundException("User not found: " + id));
|
|
|
|
if (request.getDisplayName() != null) {
|
|
user.setDisplayName(request.getDisplayName().trim());
|
|
}
|
|
if (request.getRole() != null) {
|
|
if ("ROLE_ADMIN".equals(request.getRole()) || "ROLE_USER".equals(request.getRole())) {
|
|
// Cannot change the last admin to a regular user
|
|
if ("ROLE_ADMIN".equals(user.getRole()) && "ROLE_USER".equals(request.getRole()) && isLastAdmin(id)) {
|
|
throw new InvalidOperationException("Cannot change the last admin user to a regular user");
|
|
}
|
|
user.setRole(request.getRole());
|
|
}
|
|
}
|
|
if (request.getEnabled() != null) {
|
|
// Cannot disable yourself
|
|
if (user.getId().equals(currentUserId) && !request.getEnabled()) {
|
|
throw new InvalidOperationException("You cannot disable your own account");
|
|
}
|
|
// Cannot disable the last admin
|
|
if (!request.getEnabled() && "ROLE_ADMIN".equals(user.getRole()) && isLastAdmin(id)) {
|
|
throw new InvalidOperationException("Cannot disable the last admin user");
|
|
}
|
|
user.setEnabled(request.getEnabled());
|
|
}
|
|
|
|
user.setUpdatedAt(Instant.now());
|
|
User saved = userRepository.save(user);
|
|
return toDto(saved);
|
|
}
|
|
|
|
public void deleteUser(Long id, Long currentUserId) {
|
|
if (id.equals(currentUserId)) {
|
|
throw new InvalidOperationException("You cannot delete your own account");
|
|
}
|
|
|
|
User user = userRepository.findById(id)
|
|
.orElseThrow(() -> new NotFoundException("User not found: " + id));
|
|
|
|
// Cannot delete the last admin
|
|
if ("ROLE_ADMIN".equals(user.getRole()) && isLastAdmin(id)) {
|
|
throw new InvalidOperationException("Cannot delete the last admin user");
|
|
}
|
|
|
|
userRepository.delete(user);
|
|
}
|
|
|
|
public void resetPassword(Long id, String newPassword) {
|
|
if (newPassword == null || newPassword.length() < 8) {
|
|
throw new InvalidOperationException("New password must be at least 8 characters");
|
|
}
|
|
|
|
User user = userRepository.findById(id)
|
|
.orElseThrow(() -> new NotFoundException("User not found: " + id));
|
|
|
|
user.setPasswordHash(passwordEncoder.encode(newPassword));
|
|
user.setPasswordChangedAt(Instant.EPOCH);
|
|
user.setUpdatedAt(Instant.now());
|
|
userRepository.save(user);
|
|
}
|
|
|
|
public UserDto createRegularUser(String username, String password, String displayName) {
|
|
if (username == null || username.trim().isEmpty()) {
|
|
throw new InvalidOperationException("Username is required");
|
|
}
|
|
if (password == null || password.length() < 8) {
|
|
throw new InvalidOperationException("Password must be at least 8 characters");
|
|
}
|
|
if (userRepository.existsByUsername(username.trim())) {
|
|
throw new InvalidOperationException("Username already exists: " + username);
|
|
}
|
|
|
|
User user = new User();
|
|
user.setUsername(username.trim());
|
|
user.setPasswordHash(passwordEncoder.encode(password));
|
|
user.setDisplayName(displayName != null && !displayName.trim().isEmpty()
|
|
? displayName.trim() : username.trim());
|
|
user.setRole("ROLE_USER");
|
|
user.setEnabled(true);
|
|
user.setPasswordChangedAt(Instant.now());
|
|
user.setCreatedAt(Instant.now());
|
|
user.setUpdatedAt(Instant.now());
|
|
|
|
User saved = userRepository.save(user);
|
|
return toDto(saved);
|
|
}
|
|
|
|
private boolean isLastAdmin(Long id) {
|
|
User user = userRepository.findById(id).orElse(null);
|
|
if (user == null || !"ROLE_ADMIN".equals(user.getRole())) {
|
|
return false;
|
|
}
|
|
long adminCount = userRepository.findAll().stream()
|
|
.filter(u -> "ROLE_ADMIN".equals(u.getRole()))
|
|
.count();
|
|
return adminCount <= 1;
|
|
}
|
|
|
|
private UserDto toDto(User user) {
|
|
return new UserDto(
|
|
user.getId(),
|
|
user.getUsername(),
|
|
user.getDisplayName(),
|
|
user.getRole(),
|
|
user.getEnabled(),
|
|
user.getCreatedAt(),
|
|
user.getUpdatedAt(),
|
|
user.getPasswordChangedAt()
|
|
);
|
|
}
|
|
}
|