fix: 统一账号角色校验与默认首页
- 创建与导入账号时强制校验启用角色 - 按启用角色返回默认首页并过滤禁用角色
This commit is contained in:
@@ -8,6 +8,7 @@ import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -16,6 +17,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import tech.easyflow.auth.entity.EncryptedCredentialDTO;
|
||||
import tech.easyflow.auth.service.AuthCredentialKeyService;
|
||||
import tech.easyflow.auth.service.AuthService;
|
||||
import tech.easyflow.common.constant.Constants;
|
||||
import tech.easyflow.common.constant.enums.EnumAccountType;
|
||||
import tech.easyflow.common.constant.enums.EnumDataStatus;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
@@ -25,10 +27,13 @@ import tech.easyflow.common.util.StringUtil;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.log.annotation.LogRecord;
|
||||
import tech.easyflow.admin.controller.system.vo.SysAccountProfileVo;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
import tech.easyflow.system.entity.SysRole;
|
||||
import tech.easyflow.system.entity.vo.SysAccountBatchActionResultVo;
|
||||
import tech.easyflow.system.entity.vo.SysAccountImportResultVo;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
import tech.easyflow.system.service.SysRoleService;
|
||||
import tech.easyflow.system.util.SysPasswordPolicy;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
@@ -36,9 +41,13 @@ import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Collection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 用户表 控制层。
|
||||
@@ -49,13 +58,27 @@ import java.util.Map;
|
||||
@RestController("sysAccountController")
|
||||
@RequestMapping("/api/v1/sysAccount")
|
||||
public class SysAccountController extends BaseCurdController<SysAccountService, SysAccount> {
|
||||
private static final String SUPER_ADMIN_HOME_PATH = "/dashboard/workspace";
|
||||
private static final String USER_HOME_PATH = "/ai/agent-chat";
|
||||
|
||||
private final AuthCredentialKeyService credentialKeyService;
|
||||
private final SysRoleService sysRoleService;
|
||||
@Resource
|
||||
private AuthService authService;
|
||||
|
||||
public SysAccountController(SysAccountService service, AuthCredentialKeyService credentialKeyService) {
|
||||
/**
|
||||
* 创建用户管理控制器。
|
||||
*
|
||||
* @param service 用户服务
|
||||
* @param credentialKeyService 凭证密钥服务
|
||||
* @param sysRoleService 角色服务
|
||||
*/
|
||||
public SysAccountController(SysAccountService service,
|
||||
AuthCredentialKeyService credentialKeyService,
|
||||
SysRoleService sysRoleService) {
|
||||
super(service);
|
||||
this.credentialKeyService = credentialKeyService;
|
||||
this.sysRoleService = sysRoleService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -76,6 +99,10 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
||||
if (count > 0) {
|
||||
return Result.fail(1, "用户名已存在");
|
||||
}
|
||||
Result<?> roleValidation = validateCreateRoles(entity);
|
||||
if (roleValidation != null) {
|
||||
return roleValidation;
|
||||
}
|
||||
String password = decryptInitialPassword(entity.getPasswordCredential());
|
||||
if (!StringUtil.hasText(password)) {
|
||||
return Result.fail(1, "密码不能为空");
|
||||
@@ -144,11 +171,24 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
||||
return super.onRemoveBefore(ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前账号资料、角色标识与默认首页。
|
||||
*
|
||||
* @return 当前账号资料视图
|
||||
*/
|
||||
@GetMapping("/myProfile")
|
||||
public Result<SysAccount> myProfile() {
|
||||
public Result<SysAccountProfileVo> myProfile() {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
SysAccount sysAccount = service.getById(account.getId());
|
||||
return Result.ok(sysAccount);
|
||||
List<String> roles = sysRoleService.getRolesByAccountId(account.getId()).stream()
|
||||
.map(SysRole::getRoleKey)
|
||||
.filter(StringUtil::hasText)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
String homePath = roles.contains(Constants.SUPER_ADMIN_ROLE_CODE)
|
||||
? SUPER_ADMIN_HOME_PATH
|
||||
: USER_HOME_PATH;
|
||||
return Result.ok(SysAccountProfileVo.from(sysAccount, roles, homePath));
|
||||
}
|
||||
|
||||
@PostMapping("/updateProfile")
|
||||
@@ -262,8 +302,12 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
||||
service.writeImportTemplate(response.getOutputStream());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@PostMapping("save")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<?> save(@JsonBody SysAccount entity) {
|
||||
try {
|
||||
return super.save(entity);
|
||||
@@ -271,4 +315,33 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
||||
return Result.fail(1, "用户名已存在");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验创建账号时提交的角色,并归一化角色 ID。
|
||||
*
|
||||
* @param entity 待创建账号
|
||||
* @return 校验失败结果,校验通过返回 null
|
||||
*/
|
||||
private Result<?> validateCreateRoles(SysAccount entity) {
|
||||
List<BigInteger> roleIds = entity == null ? null : entity.getRoleIds();
|
||||
Set<BigInteger> uniqueRoleIds = new LinkedHashSet<>();
|
||||
if (roleIds != null) {
|
||||
roleIds.stream()
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.forEach(uniqueRoleIds::add);
|
||||
}
|
||||
if (uniqueRoleIds.isEmpty()) {
|
||||
return Result.fail(1, "角色不能为空");
|
||||
}
|
||||
|
||||
List<SysRole> roles = sysRoleService.listByIds(uniqueRoleIds);
|
||||
boolean valid = roles.size() == uniqueRoleIds.size()
|
||||
&& roles.stream().allMatch(role ->
|
||||
EnumDataStatus.AVAILABLE.getCode().equals(role.getStatus()));
|
||||
if (!valid) {
|
||||
return Result.fail(1, "角色不存在或已禁用");
|
||||
}
|
||||
entity.setRoleIds(new ArrayList<>(uniqueRoleIds));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package tech.easyflow.admin.controller.system.vo;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 管理端当前账号资料视图。
|
||||
*/
|
||||
public class SysAccountProfileVo extends SysAccount {
|
||||
|
||||
private String homePath;
|
||||
private List<String> roles = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 创建空的当前账号资料视图。
|
||||
*/
|
||||
public SysAccountProfileVo() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据账号实体和角色信息创建资料视图。
|
||||
*
|
||||
* @param account 账号实体
|
||||
* @param roles 角色标识列表
|
||||
* @param homePath 默认首页
|
||||
* @return 当前账号资料视图
|
||||
*/
|
||||
public static SysAccountProfileVo from(SysAccount account, List<String> roles, String homePath) {
|
||||
SysAccountProfileVo profile = new SysAccountProfileVo();
|
||||
if (account != null) {
|
||||
BeanUtil.copyProperties(account, profile);
|
||||
}
|
||||
profile.setRoles(roles);
|
||||
profile.setHomePath(homePath);
|
||||
return profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认首页。
|
||||
*
|
||||
* @return 默认首页
|
||||
*/
|
||||
public String getHomePath() {
|
||||
return homePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置默认首页。
|
||||
*
|
||||
* @param homePath 默认首页
|
||||
*/
|
||||
public void setHomePath(String homePath) {
|
||||
this.homePath = homePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色标识列表。
|
||||
*
|
||||
* @return 角色标识列表
|
||||
*/
|
||||
public List<String> getRoles() {
|
||||
return roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置角色标识列表。
|
||||
*
|
||||
* @param roles 角色标识列表
|
||||
*/
|
||||
public void setRoles(List<String> roles) {
|
||||
this.roles = roles == null ? new ArrayList<>() : new ArrayList<>(roles);
|
||||
}
|
||||
}
|
||||
@@ -2,22 +2,28 @@ package tech.easyflow.admin.controller.system;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.testng.annotations.Test;
|
||||
import tech.easyflow.auth.service.AuthCredentialKeyService;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.admin.controller.system.vo.SysAccountProfileVo;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
import tech.easyflow.system.entity.SysRole;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
import tech.easyflow.system.service.SysRoleService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyCollection;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.testng.Assert.assertEquals;
|
||||
@@ -28,7 +34,7 @@ import static org.testng.Assert.assertEquals;
|
||||
public class SysAccountControllerTest {
|
||||
|
||||
/**
|
||||
* 验证创建用户时保留表单选择的部门,而不是替换为操作者部门。
|
||||
* 验证创建用户时保留表单选择的部门,不被操作者部门覆盖。
|
||||
*/
|
||||
@Test
|
||||
public void saveShouldKeepSubmittedDepartment() {
|
||||
@@ -38,13 +44,28 @@ public class SysAccountControllerTest {
|
||||
BigInteger operatorId = BigInteger.valueOf(400);
|
||||
SysAccountService accountService = mock(SysAccountService.class);
|
||||
AuthCredentialKeyService credentialKeyService = mock(AuthCredentialKeyService.class);
|
||||
SysAccountController controller = new SysAccountController(accountService, credentialKeyService);
|
||||
SysRoleService roleService = mock(SysRoleService.class);
|
||||
SysAccountController controller = new SysAccountController(
|
||||
accountService,
|
||||
credentialKeyService,
|
||||
roleService
|
||||
);
|
||||
SysAccount entity = createAccount(selectedDeptId);
|
||||
LoginAccount loginAccount = createLoginAccount(operatorId, tenantId, operatorDeptId);
|
||||
AtomicReference<BigInteger> savedDeptId = new AtomicReference<>();
|
||||
AtomicReference<BigInteger> savedTenantId = new AtomicReference<>();
|
||||
AtomicReference<BigInteger> savedCreatedBy = new AtomicReference<>();
|
||||
AtomicReference<BigInteger> savedModifiedBy = new AtomicReference<>();
|
||||
|
||||
when(accountService.count(any(QueryWrapper.class))).thenReturn(0L);
|
||||
when(roleService.listByIds(anyCollection())).thenReturn(List.of(enabledRole(BigInteger.ONE, "user")));
|
||||
when(accountService.save(any(SysAccount.class))).thenAnswer(invocation -> {
|
||||
invocation.getArgument(0, SysAccount.class).setId(BigInteger.valueOf(500));
|
||||
SysAccount savedAccount = invocation.getArgument(0, SysAccount.class);
|
||||
savedDeptId.set(savedAccount.getDeptId());
|
||||
savedTenantId.set(savedAccount.getTenantId());
|
||||
savedCreatedBy.set(savedAccount.getCreatedBy());
|
||||
savedModifiedBy.set(savedAccount.getModifiedBy());
|
||||
savedAccount.setId(BigInteger.valueOf(500));
|
||||
return true;
|
||||
});
|
||||
when(credentialKeyService.decryptPayload(any()))
|
||||
@@ -58,13 +79,107 @@ public class SysAccountControllerTest {
|
||||
assertEquals(result.getErrorCode(), 0);
|
||||
}
|
||||
|
||||
ArgumentCaptor<SysAccount> accountCaptor = ArgumentCaptor.forClass(SysAccount.class);
|
||||
verify(accountService).save(accountCaptor.capture());
|
||||
SysAccount savedAccount = accountCaptor.getValue();
|
||||
assertEquals(savedAccount.getDeptId(), selectedDeptId);
|
||||
assertEquals(savedAccount.getTenantId(), tenantId);
|
||||
assertEquals(savedAccount.getCreatedBy(), operatorId);
|
||||
assertEquals(savedAccount.getModifiedBy(), operatorId);
|
||||
verify(accountService).save(any(SysAccount.class));
|
||||
assertEquals(savedDeptId.get(), selectedDeptId);
|
||||
assertEquals(savedTenantId.get(), tenantId);
|
||||
assertEquals(savedCreatedBy.get(), operatorId);
|
||||
assertEquals(savedModifiedBy.get(), operatorId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证创建用户时拒绝空角色。
|
||||
*/
|
||||
@Test
|
||||
public void saveShouldRejectEmptyRoles() {
|
||||
SysAccountService accountService = mock(SysAccountService.class);
|
||||
AuthCredentialKeyService credentialKeyService = mock(AuthCredentialKeyService.class);
|
||||
SysRoleService roleService = mock(SysRoleService.class);
|
||||
SysAccountController controller = new SysAccountController(
|
||||
accountService,
|
||||
credentialKeyService,
|
||||
roleService
|
||||
);
|
||||
SysAccount entity = createAccount(BigInteger.valueOf(200));
|
||||
entity.setRoleIds(List.of());
|
||||
LoginAccount loginAccount = createLoginAccount(
|
||||
BigInteger.valueOf(400),
|
||||
BigInteger.valueOf(300),
|
||||
BigInteger.valueOf(100)
|
||||
);
|
||||
when(accountService.count(any(QueryWrapper.class))).thenReturn(0L);
|
||||
|
||||
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
|
||||
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(loginAccount);
|
||||
|
||||
Result<?> result = controller.save(entity);
|
||||
|
||||
assertEquals(result.getErrorCode(), 1);
|
||||
assertEquals(result.getMessage(), "角色不能为空");
|
||||
}
|
||||
|
||||
verify(accountService, never()).save(any(SysAccount.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证超级管理员默认进入工作台并返回角色标识。
|
||||
*/
|
||||
@Test
|
||||
public void myProfileShouldReturnSuperAdminHomePath() {
|
||||
BigInteger accountId = BigInteger.ONE;
|
||||
SysAccountService accountService = mock(SysAccountService.class);
|
||||
AuthCredentialKeyService credentialKeyService = mock(AuthCredentialKeyService.class);
|
||||
SysRoleService roleService = mock(SysRoleService.class);
|
||||
SysAccountController controller = new SysAccountController(
|
||||
accountService,
|
||||
credentialKeyService,
|
||||
roleService
|
||||
);
|
||||
SysAccount account = new SysAccount();
|
||||
account.setId(accountId);
|
||||
when(accountService.getById(accountId)).thenReturn(account);
|
||||
when(roleService.getRolesByAccountId(accountId))
|
||||
.thenReturn(List.of(enabledRole(BigInteger.ONE, "super_admin")));
|
||||
|
||||
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
|
||||
saToken.when(SaTokenUtil::getLoginAccount)
|
||||
.thenReturn(createLoginAccount(accountId, BigInteger.ZERO, BigInteger.ZERO));
|
||||
|
||||
Result<SysAccountProfileVo> result = controller.myProfile();
|
||||
|
||||
assertEquals(result.getData().getHomePath(), "/dashboard/workspace");
|
||||
assertEquals(result.getData().getRoles(), List.of("super_admin"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证普通账号默认进入智能体聊天页。
|
||||
*/
|
||||
@Test
|
||||
public void myProfileShouldReturnAgentChatHomePathForRegularUser() {
|
||||
BigInteger accountId = BigInteger.valueOf(20);
|
||||
SysAccountService accountService = mock(SysAccountService.class);
|
||||
AuthCredentialKeyService credentialKeyService = mock(AuthCredentialKeyService.class);
|
||||
SysRoleService roleService = mock(SysRoleService.class);
|
||||
SysAccountController controller = new SysAccountController(
|
||||
accountService,
|
||||
credentialKeyService,
|
||||
roleService
|
||||
);
|
||||
SysAccount account = new SysAccount();
|
||||
account.setId(accountId);
|
||||
when(accountService.getById(accountId)).thenReturn(account);
|
||||
when(roleService.getRolesByAccountId(accountId))
|
||||
.thenReturn(List.of(enabledRole(BigInteger.TWO, "operator")));
|
||||
|
||||
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
|
||||
saToken.when(SaTokenUtil::getLoginAccount)
|
||||
.thenReturn(createLoginAccount(accountId, BigInteger.ZERO, BigInteger.ZERO));
|
||||
|
||||
Result<SysAccountProfileVo> result = controller.myProfile();
|
||||
|
||||
assertEquals(result.getData().getHomePath(), "/ai/agent-chat");
|
||||
assertEquals(result.getData().getRoles(), List.of("operator"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,9 +194,25 @@ public class SysAccountControllerTest {
|
||||
account.setLoginName("department_test_user");
|
||||
account.setNickname("部门测试用户");
|
||||
account.setPasswordCredential(Map.of("keyId", "test-key"));
|
||||
account.setRoleIds(List.of(BigInteger.ONE));
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造启用角色。
|
||||
*
|
||||
* @param id 角色 ID
|
||||
* @param roleKey 角色标识
|
||||
* @return 启用角色
|
||||
*/
|
||||
private SysRole enabledRole(BigInteger id, String roleKey) {
|
||||
SysRole role = new SysRole();
|
||||
role.setId(id);
|
||||
role.setRoleKey(roleKey);
|
||||
role.setStatus(1);
|
||||
return role;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造当前登录账号。
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user