支持单位授权与客户端限制

This commit is contained in:
Codex
2026-06-01 11:58:14 +08:00
parent 28adc8143a
commit c136cc2cc8
36 changed files with 861 additions and 49 deletions

View File

@@ -25,6 +25,9 @@ public class SandboxConfigDto {
@Schema(description = "用户IDscope为user时必填") @Schema(description = "用户IDscope为user时必填")
private Long userId; private Long userId;
@Schema(description = "用户组/单位ID")
private Long groupId;
@Schema(description = "配置名称") @Schema(description = "配置名称")
private String name; private String name;
@@ -81,4 +84,4 @@ public class SandboxConfigDto {
@Schema(description = "更新时间") @Schema(description = "更新时间")
private Date modified; private Date modified;
} }

View File

@@ -4,6 +4,8 @@ import com.xspaceagi.sandbox.spec.enums.SandboxScopeEnum;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.util.List;
/** /**
* 沙盒配置查询 DTO * 沙盒配置查询 DTO
*/ */
@@ -17,6 +19,12 @@ public class SandboxConfigQueryDto {
@Schema(description = "用户ID") @Schema(description = "用户ID")
private Long userId; private Long userId;
@Schema(description = "用户组/单位ID")
private Long groupId;
@Schema(description = "用户组/单位ID列表")
private List<Long> groupIds;
@Schema(description = "配置名称(模糊查询)") @Schema(description = "配置名称(模糊查询)")
private String name; private String name;

View File

@@ -20,6 +20,8 @@ import com.xspaceagi.sandbox.infra.dao.vo.SandboxServerInfo;
import com.xspaceagi.sandbox.infra.network.ReverseServerContainer; import com.xspaceagi.sandbox.infra.network.ReverseServerContainer;
import com.xspaceagi.sandbox.spec.enums.SandboxScopeEnum; import com.xspaceagi.sandbox.spec.enums.SandboxScopeEnum;
import com.xspaceagi.system.application.dto.TenantConfigDto; import com.xspaceagi.system.application.dto.TenantConfigDto;
import com.xspaceagi.system.application.service.SysGroupApplicationService;
import com.xspaceagi.system.infra.dao.entity.SysGroup;
import com.xspaceagi.system.spec.common.RequestContext; import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.enums.ErrorCodeEnum; import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
import com.xspaceagi.system.spec.exception.BizException; import com.xspaceagi.system.spec.exception.BizException;
@@ -27,6 +29,7 @@ import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import com.xspaceagi.system.spec.utils.RedisUtil; import com.xspaceagi.system.spec.utils.RedisUtil;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -41,6 +44,7 @@ import java.net.http.HttpResponse;
import java.time.Duration; import java.time.Duration;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@@ -65,6 +69,9 @@ public class SandboxConfigApplicationServiceImpl implements SandboxConfigApplica
@Resource @Resource
private IAgentRpcService iAgentRpcService; private IAgentRpcService iAgentRpcService;
@Resource
private SysGroupApplicationService sysGroupApplicationService;
static { static {
// disable keep alive暂不使用连接池 // disable keep alive暂不使用连接池
System.setProperty("jdk.httpclient.keepalive.timeout", "0"); System.setProperty("jdk.httpclient.keepalive.timeout", "0");
@@ -201,6 +208,7 @@ public class SandboxConfigApplicationServiceImpl implements SandboxConfigApplica
if (entity.getScope() == SandboxScopeEnum.USER && entity.getUserId() == null) { if (entity.getScope() == SandboxScopeEnum.USER && entity.getUserId() == null) {
entity.setUserId(RequestContext.get().getUserId()); entity.setUserId(RequestContext.get().getUserId());
} }
bindAuthorizedGroupForUserConfig(entity);
sandboxConfigService.save(entity); sandboxConfigService.save(entity);
// 创建用户沙盒代理 // 创建用户沙盒代理
@@ -240,6 +248,14 @@ public class SandboxConfigApplicationServiceImpl implements SandboxConfigApplica
SandboxConfig entity = convertToEntity(dto); SandboxConfig entity = convertToEntity(dto);
entity.setId(dto.getId()); entity.setId(dto.getId());
entity.setModified(new Date()); entity.setModified(new Date());
entity.setScope(existingEntity.getScope());
entity.setUserId(existingEntity.getUserId());
entity.setGroupId(existingEntity.getGroupId());
if (existingEntity.getGroupId() == null) {
bindAuthorizedGroupForUserConfig(entity);
} else {
validateExistingGroupForUserConfig(entity);
}
sandboxConfigService.updateById(entity); sandboxConfigService.updateById(entity);
@@ -337,6 +353,14 @@ public class SandboxConfigApplicationServiceImpl implements SandboxConfigApplica
queryWrapper.eq(SandboxConfig::getUserId, queryDto.getUserId()); queryWrapper.eq(SandboxConfig::getUserId, queryDto.getUserId());
} }
if (queryDto.getGroupId() != null) {
queryWrapper.eq(SandboxConfig::getGroupId, queryDto.getGroupId());
}
if (CollectionUtils.isNotEmpty(queryDto.getGroupIds())) {
queryWrapper.in(SandboxConfig::getGroupId, queryDto.getGroupIds());
}
if (StringUtils.isNotBlank(queryDto.getName())) { if (StringUtils.isNotBlank(queryDto.getName())) {
queryWrapper.like(SandboxConfig::getName, queryDto.getName()); queryWrapper.like(SandboxConfig::getName, queryDto.getName());
} }
@@ -350,6 +374,44 @@ public class SandboxConfigApplicationServiceImpl implements SandboxConfigApplica
return queryWrapper; return queryWrapper;
} }
private void bindAuthorizedGroupForUserConfig(SandboxConfig entity) {
if (entity.getScope() != SandboxScopeEnum.USER || entity.getUserId() == null) {
return;
}
List<SysGroup> groups = sysGroupApplicationService.getEffectiveGroupListByUserId(entity.getUserId());
if (groups.isEmpty()) {
return;
}
Map<Long, Long> clientCountByGroupId = groups.stream()
.filter(group -> group.getId() != null)
.collect(Collectors.toMap(
SysGroup::getId,
group -> countUserClientsByGroupId(group.getId()),
(a, b) -> a));
SysGroup group = sysGroupApplicationService.selectAuthorizedGroupForClient(entity.getUserId(), clientCountByGroupId);
if (group == null) {
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.systemGroupClientLimitExceeded);
}
entity.setGroupId(group.getId());
}
private long countUserClientsByGroupId(Long groupId) {
return sandboxConfigService.count(new LambdaQueryWrapper<SandboxConfig>()
.eq(SandboxConfig::getScope, SandboxScopeEnum.USER)
.eq(SandboxConfig::getGroupId, groupId));
}
private void validateExistingGroupForUserConfig(SandboxConfig entity) {
if (entity.getScope() != SandboxScopeEnum.USER || entity.getUserId() == null || entity.getGroupId() == null) {
return;
}
boolean authorized = sysGroupApplicationService.getAuthorizedGroupListForLogin(entity.getUserId()).stream()
.anyMatch(group -> entity.getGroupId().equals(group.getId()));
if (!authorized) {
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.systemGroupAuthorizationUnavailable);
}
}
/** /**
* 验证配置键唯一性 * 验证配置键唯一性
*/ */

View File

@@ -37,6 +37,9 @@ public class SandboxConfig implements Serializable {
@TableField(value = "user_id") @TableField(value = "user_id")
private Long userId; private Long userId;
@TableField(value = "group_id")
private Long groupId;
@TableField(value = "name") @TableField(value = "name")
private String name; private String name;
@@ -75,4 +78,4 @@ public class SandboxConfig implements Serializable {
@TableField(value = "modified") @TableField(value = "modified")
private Date modified; private Date modified;
} }

View File

@@ -199,15 +199,16 @@ public class SandboxConfigController {
RequestContext.get().setLangMap(user.getLangMap()); RequestContext.get().setLangMap(user.getLangMap());
} }
SandboxConfigDto dto = new SandboxConfigDto(); SandboxConfigDto dto = new SandboxConfigDto();
String clientName = StringUtils.trimToNull(sandboxRegDto.getName());
dto.setUserId(user.getId()); dto.setUserId(user.getId());
dto.setScope(SandboxScopeEnum.USER); dto.setScope(SandboxScopeEnum.USER);
dto.setConfigKey(UUID.randomUUID().toString().replace("-", "")); dto.setConfigKey(UUID.randomUUID().toString().replace("-", ""));
dto.setName(I18nUtil.systemMessage("Backend.Sandbox.MyComputer")); dto.setName(clientName != null ? clientName : I18nUtil.systemMessage("Backend.Sandbox.MyComputer"));
dto.setConfigValue(sandboxRegDto.getSandboxConfigValue()); dto.setConfigValue(sandboxRegDto.getSandboxConfigValue());
dto.setDescription(""); dto.setDescription("");
dto.setIsActive(true); dto.setIsActive(true);
dto.setOnline(false); dto.setOnline(false);
sandboxConfigApplicationService.create(dto, false); sandboxConfigApplicationService.create(dto, clientName != null);
String token = authService.createToken(user, StringUtils.isNotBlank(sandboxRegDto.getDeviceId()) ? sandboxRegDto.getDeviceId() : UUID.randomUUID().toString().replace("-", "")); String token = authService.createToken(user, StringUtils.isNotBlank(sandboxRegDto.getDeviceId()) ? sandboxRegDto.getDeviceId() : UUID.randomUUID().toString().replace("-", ""));
dto.setToken(token); dto.setToken(token);
return ReqResult.success(sandboxConfigApplicationService.getByKey(dto.getConfigKey())); return ReqResult.success(sandboxConfigApplicationService.getByKey(dto.getConfigKey()));

View File

@@ -5,9 +5,16 @@ import com.xspaceagi.sandbox.application.dto.SandboxConfigDto;
import com.xspaceagi.sandbox.application.dto.SandboxConfigQueryDto; import com.xspaceagi.sandbox.application.dto.SandboxConfigQueryDto;
import com.xspaceagi.sandbox.application.dto.SandboxGlobalConfigDto; import com.xspaceagi.sandbox.application.dto.SandboxGlobalConfigDto;
import com.xspaceagi.sandbox.application.service.SandboxConfigApplicationService; import com.xspaceagi.sandbox.application.service.SandboxConfigApplicationService;
import com.xspaceagi.system.application.dto.UserDto;
import com.xspaceagi.system.application.service.SysGroupApplicationService;
import com.xspaceagi.system.infra.dao.entity.SysGroup;
import com.xspaceagi.system.infra.dao.entity.User;
import com.xspaceagi.system.spec.annotation.RequireResource; import com.xspaceagi.system.spec.annotation.RequireResource;
import com.xspaceagi.system.spec.common.RequestContext; import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.dto.ReqResult; import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
import com.xspaceagi.system.spec.exception.BizException;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@@ -15,6 +22,8 @@ import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static com.xspaceagi.system.spec.enums.ResourceEnum.*; import static com.xspaceagi.system.spec.enums.ResourceEnum.*;
@@ -29,10 +38,15 @@ public class SandboxConfigManageController {
@Resource @Resource
private SandboxConfigApplicationService sandboxConfigApplicationService; private SandboxConfigApplicationService sandboxConfigApplicationService;
@Resource
private SysGroupApplicationService sysGroupApplicationService;
@RequireResource(SANDBOX_CONFIG_QUERY) @RequireResource(SANDBOX_CONFIG_QUERY)
@Operation(summary = "分页查询配置列表") @Operation(summary = "分页查询配置列表")
@PostMapping("/page") @PostMapping("/page")
public ReqResult<Page<SandboxConfigDto>> pageQuery(@RequestBody SandboxConfigQueryDto queryDto) { public ReqResult<Page<SandboxConfigDto>> pageQuery(@RequestBody SandboxConfigQueryDto queryDto) {
queryDto = queryDto == null ? new SandboxConfigQueryDto() : queryDto;
applySandboxConfigManageScope(queryDto);
return ReqResult.success(sandboxConfigApplicationService.pageQuery(queryDto)); return ReqResult.success(sandboxConfigApplicationService.pageQuery(queryDto));
} }
@@ -41,13 +55,16 @@ public class SandboxConfigManageController {
@GetMapping("/get/{id}") @GetMapping("/get/{id}")
public ReqResult<SandboxConfigDto> getById( public ReqResult<SandboxConfigDto> getById(
@Parameter(description = "配置ID") @PathVariable Long id) { @Parameter(description = "配置ID") @PathVariable Long id) {
return ReqResult.success(sandboxConfigApplicationService.getById(id)); SandboxConfigDto dto = sandboxConfigApplicationService.getById(id);
checkSandboxConfigManageScope(dto);
return ReqResult.success(dto);
} }
@Operation(summary = "沙箱连通性测试") @Operation(summary = "沙箱连通性测试")
@GetMapping("/test/{id}") @GetMapping("/test/{id}")
public ReqResult<Void> testConnection(@Parameter(description = "配置ID") @PathVariable Long id) { public ReqResult<Void> testConnection(@Parameter(description = "配置ID") @PathVariable Long id) {
try { try {
checkSandboxConfigManageScope(sandboxConfigApplicationService.getById(id));
sandboxConfigApplicationService.testConnection(id); sandboxConfigApplicationService.testConnection(id);
} catch (Exception e) { } catch (Exception e) {
return ReqResult.error("0007", e.getMessage()); return ReqResult.error("0007", e.getMessage());
@@ -60,7 +77,14 @@ public class SandboxConfigManageController {
@GetMapping("/user/list") @GetMapping("/user/list")
public ReqResult<List<SandboxConfigDto>> listUserConfigsByType( public ReqResult<List<SandboxConfigDto>> listUserConfigsByType(
@Parameter(description = "用户ID为空时查询当前用户") @RequestParam(required = false) Long userId) { @Parameter(description = "用户ID为空时查询当前用户") @RequestParam(required = false) Long userId) {
return ReqResult.success(sandboxConfigApplicationService.listUserConfigsByType(userId)); List<SandboxConfigDto> configs = sandboxConfigApplicationService.listUserConfigsByType(userId);
if (!isPlatformAdmin()) {
Set<Long> allowedGroupIds = currentUserGroupIds();
configs = configs.stream()
.filter(config -> config.getGroupId() != null && allowedGroupIds.contains(config.getGroupId()))
.collect(Collectors.toList());
}
return ReqResult.success(configs);
} }
@RequireResource(SANDBOX_CONFIG_QUERY) @RequireResource(SANDBOX_CONFIG_QUERY)
@@ -74,6 +98,7 @@ public class SandboxConfigManageController {
@Operation(summary = "创建配置") @Operation(summary = "创建配置")
@PostMapping("/create") @PostMapping("/create")
public ReqResult<Void> create(@RequestBody SandboxConfigDto dto) { public ReqResult<Void> create(@RequestBody SandboxConfigDto dto) {
checkSandboxConfigManageScope(dto);
sandboxConfigApplicationService.create(dto, false); sandboxConfigApplicationService.create(dto, false);
return ReqResult.success(); return ReqResult.success();
} }
@@ -82,6 +107,7 @@ public class SandboxConfigManageController {
@Operation(summary = "更新配置") @Operation(summary = "更新配置")
@PostMapping("/update") @PostMapping("/update")
public ReqResult<Void> update(@RequestBody SandboxConfigDto dto) { public ReqResult<Void> update(@RequestBody SandboxConfigDto dto) {
checkSandboxConfigManageScope(sandboxConfigApplicationService.getById(dto.getId()));
sandboxConfigApplicationService.update(dto); sandboxConfigApplicationService.update(dto);
return ReqResult.success(); return ReqResult.success();
} }
@@ -91,6 +117,7 @@ public class SandboxConfigManageController {
@PostMapping("/delete/{id}") @PostMapping("/delete/{id}")
public ReqResult<Void> delete( public ReqResult<Void> delete(
@Parameter(description = "配置ID") @PathVariable Long id) { @Parameter(description = "配置ID") @PathVariable Long id) {
checkSandboxConfigManageScope(sandboxConfigApplicationService.getById(id));
sandboxConfigApplicationService.delete(id); sandboxConfigApplicationService.delete(id);
return ReqResult.success(); return ReqResult.success();
} }
@@ -100,6 +127,7 @@ public class SandboxConfigManageController {
@PostMapping("/toggle/{id}") @PostMapping("/toggle/{id}")
public ReqResult<Void> toggle( public ReqResult<Void> toggle(
@Parameter(description = "配置ID") @PathVariable Long id) { @Parameter(description = "配置ID") @PathVariable Long id) {
checkSandboxConfigManageScope(sandboxConfigApplicationService.getById(id));
sandboxConfigApplicationService.toggle(id); sandboxConfigApplicationService.toggle(id);
return ReqResult.success(); return ReqResult.success();
} }
@@ -119,4 +147,45 @@ public class SandboxConfigManageController {
SandboxGlobalConfigDto globalConfig = sandboxConfigApplicationService.getGlobalConfig(RequestContext.get().getTenantId()); SandboxGlobalConfigDto globalConfig = sandboxConfigApplicationService.getGlobalConfig(RequestContext.get().getTenantId());
return ReqResult.success(globalConfig); return ReqResult.success(globalConfig);
} }
private void applySandboxConfigManageScope(SandboxConfigQueryDto queryDto) {
if (queryDto == null || isPlatformAdmin()) {
return;
}
Set<Long> allowedGroupIds = currentUserGroupIds();
if (queryDto.getGroupId() != null) {
if (!allowedGroupIds.contains(queryDto.getGroupId())) {
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied);
}
return;
}
queryDto.setGroupIds(allowedGroupIds.isEmpty() ? List.of(-1L) : allowedGroupIds.stream().toList());
}
private void checkSandboxConfigManageScope(SandboxConfigDto dto) {
if (dto == null || isPlatformAdmin()) {
return;
}
if (dto.getGroupId() == null || !currentUserGroupIds().contains(dto.getGroupId())) {
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied);
}
}
private Set<Long> currentUserGroupIds() {
return sysGroupApplicationService.getEffectiveGroupListByUserId(getCurrentUserDto().getId()).stream()
.map(SysGroup::getId)
.collect(Collectors.toSet());
}
private boolean isPlatformAdmin() {
return getCurrentUserDto().getRole() == User.Role.Admin;
}
private UserDto getCurrentUserDto() {
UserDto userDto = (UserDto) RequestContext.get().getUser();
if (userDto == null) {
throw BizException.of(ErrorCodeEnum.UNAUTHORIZED, BizExceptionCodeEnum.systemUserNotLoggedInWeb);
}
return userDto;
}
} }

View File

@@ -19,6 +19,9 @@ public class SandboxRegDto {
@Schema(description = "设备ID") @Schema(description = "设备ID")
private String deviceId; private String deviceId;
@Schema(description = "客户端注册名称")
private String name;
@Schema(description = "终端配置信息") @Schema(description = "终端配置信息")
private SandboxConfigValue sandboxConfigValue; private SandboxConfigValue sandboxConfigValue;
} }

View File

@@ -1,6 +1,7 @@
package com.xspaceagi.system.application.dto.permission; package com.xspaceagi.system.application.dto.permission;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -20,6 +21,15 @@ public class SysGroupAddDto implements Serializable {
@Schema(description = "最大用户数") @Schema(description = "最大用户数")
private Integer maxUserCount; private Integer maxUserCount;
@Schema(description = "最大客户端数,-1表示不限制")
private Integer maxClientCount;
@Schema(description = "授权开关1-已授权0-未授权")
private Integer authEnabled;
@Schema(description = "授权到期时间,空表示不过期")
private Date expireTime;
@Schema(description = "来源,1:系统内置 2:用户自定义", hidden = true) @Schema(description = "来源,1:系统内置 2:用户自定义", hidden = true)
private Integer source; private Integer source;

View File

@@ -31,6 +31,15 @@ public class SysGroupDto implements Serializable {
@Schema(description = "最大用户数") @Schema(description = "最大用户数")
private Integer maxUserCount; private Integer maxUserCount;
@Schema(description = "最大客户端数,-1表示不限制")
private Integer maxClientCount;
@Schema(description = "授权开关1-已授权0-未授权")
private Integer authEnabled;
@Schema(description = "授权到期时间,空表示不过期")
private Date expireTime;
@Schema(description = "来源,1:系统内置 2:用户自定义") @Schema(description = "来源,1:系统内置 2:用户自定义")
private Integer source; private Integer source;

View File

@@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date;
@Data @Data
public class SysGroupUpdateDto implements Serializable { public class SysGroupUpdateDto implements Serializable {
@@ -23,6 +24,15 @@ public class SysGroupUpdateDto implements Serializable {
@Schema(description = "最大用户数") @Schema(description = "最大用户数")
private Integer maxUserCount; private Integer maxUserCount;
@Schema(description = "最大客户端数,-1表示不限制")
private Integer maxClientCount;
@Schema(description = "授权开关1-已授权0-未授权")
private Integer authEnabled;
@Schema(description = "授权到期时间,空表示不过期")
private Date expireTime;
@Schema(description = "来源,1:系统内置 2:用户自定义", hidden = true) @Schema(description = "来源,1:系统内置 2:用户自定义", hidden = true)
private Integer source; private Integer source;

View File

@@ -76,6 +76,16 @@ public interface SysGroupApplicationService {
*/ */
List<SysGroup> getEffectiveGroupListByUserId(Long userId); List<SysGroup> getEffectiveGroupListByUserId(Long userId);
/**
* 查询用户可用于系统登录的已授权用户组(启用、授权开启、未过期)
*/
List<SysGroup> getAuthorizedGroupListForLogin(Long userId);
/**
* 为用户选择一个可注册客户端的已授权用户组(同时满足客户端数量限制)
*/
SysGroup selectAuthorizedGroupForClient(Long userId, java.util.Map<Long, Long> clientCountByGroupId);
/** /**
* 用户组绑定用户(全量覆盖) * 用户组绑定用户(全量覆盖)
*/ */

View File

@@ -3,13 +3,16 @@ package com.xspaceagi.system.application.service.impl;
import com.xspaceagi.system.application.dto.TenantConfigDto; import com.xspaceagi.system.application.dto.TenantConfigDto;
import com.xspaceagi.system.application.dto.UserDto; import com.xspaceagi.system.application.dto.UserDto;
import com.xspaceagi.system.application.service.AuthService; import com.xspaceagi.system.application.service.AuthService;
import com.xspaceagi.system.application.service.SysGroupApplicationService;
import com.xspaceagi.system.application.service.UserApplicationService; import com.xspaceagi.system.application.service.UserApplicationService;
import com.xspaceagi.system.infra.dao.entity.User; import com.xspaceagi.system.infra.dao.entity.User;
import com.xspaceagi.system.infra.rpc.WeChatMpService; import com.xspaceagi.system.infra.rpc.WeChatMpService;
import com.xspaceagi.system.infra.verify.VerifyCodeSendAndCheckService; import com.xspaceagi.system.infra.verify.VerifyCodeSendAndCheckService;
import com.xspaceagi.system.spec.common.RequestContext; import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.enums.CodeTypeEnum; import com.xspaceagi.system.spec.enums.CodeTypeEnum;
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
import com.xspaceagi.system.spec.exception.BizException; import com.xspaceagi.system.spec.exception.BizException;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import com.xspaceagi.system.spec.utils.I18nUtil; import com.xspaceagi.system.spec.utils.I18nUtil;
import com.xspaceagi.system.spec.utils.JwtUtils; import com.xspaceagi.system.spec.utils.JwtUtils;
import com.xspaceagi.system.spec.utils.RedisUtil; import com.xspaceagi.system.spec.utils.RedisUtil;
@@ -28,6 +31,9 @@ public class AuthServiceImpl implements AuthService {
@Resource @Resource
private UserApplicationService userApplicationService; private UserApplicationService userApplicationService;
@Resource
private SysGroupApplicationService sysGroupApplicationService;
@Resource @Resource
private VerifyCodeSendAndCheckService verifyCodeSendAndCheckService; private VerifyCodeSendAndCheckService verifyCodeSendAndCheckService;
@@ -66,9 +72,7 @@ public class AuthServiceImpl implements AuthService {
userDto.setEmail(email); userDto.setEmail(email);
userApplicationService.add(userDto); userApplicationService.add(userDto);
} }
if (userDto.getStatus() == User.Status.Disabled || userDto.getStatus() == User.Status.Deleted) { validateLoginUser(userDto);
throw new BizException(I18nUtil.systemMessage("Backend.Auth.Login.AccountDisabled"));
}
updateLastLoginTime(userDto); updateLastLoginTime(userDto);
return createToken(userDto, UUID.randomUUID().toString().replace("-", "")); return createToken(userDto, UUID.randomUUID().toString().replace("-", ""));
} }
@@ -87,13 +91,23 @@ public class AuthServiceImpl implements AuthService {
userDto.setPhone(phone); userDto.setPhone(phone);
userApplicationService.add(userDto); userApplicationService.add(userDto);
} }
if (userDto.getStatus() == User.Status.Disabled) { validateLoginUser(userDto);
throw new BizException(I18nUtil.systemMessage("Backend.Auth.Login.AccountDisabled"));
}
updateLastLoginTime(userDto); updateLastLoginTime(userDto);
return createToken(userDto, UUID.randomUUID().toString().replace("-", "")); return createToken(userDto, UUID.randomUUID().toString().replace("-", ""));
} }
private void validateLoginUser(UserDto userDto) {
if (userDto.getStatus() == User.Status.Disabled || userDto.getStatus() == User.Status.Deleted) {
throw new BizException(I18nUtil.systemMessage("Backend.Auth.Login.AccountDisabled"));
}
if (userDto.getRole() == User.Role.Admin) {
return;
}
if (sysGroupApplicationService.getAuthorizedGroupListForLogin(userDto.getId()).isEmpty()) {
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.systemGroupAuthorizationUnavailable);
}
}
private void updateLastLoginTime(UserDto userDto) { private void updateLastLoginTime(UserDto userDto) {
UserDto update = new UserDto(); UserDto update = new UserDto();
update.setId(userDto.getId()); update.setId(userDto.getId());
@@ -105,6 +119,7 @@ public class AuthServiceImpl implements AuthService {
} }
public String createToken(UserDto userDto, String clientId) { public String createToken(UserDto userDto, String clientId) {
validateLoginUser(userDto);
//过期token检查 //过期token检查
Map<String, Object> map = redisUtil.hashGetAll("user-token:" + userDto.getId()); Map<String, Object> map = redisUtil.hashGetAll("user-token:" + userDto.getId());
for (Map.Entry<String, Object> entry : map.entrySet()) { for (Map.Entry<String, Object> entry : map.entrySet()) {
@@ -127,6 +142,7 @@ public class AuthServiceImpl implements AuthService {
Assert.notNull(emailOrPhone, "emailOrPhone must be non-null"); Assert.notNull(emailOrPhone, "emailOrPhone must be non-null");
UserDto userDto = userApplicationService.queryUserByPhoneOrEmailWithPassword(emailOrPhone, password); UserDto userDto = userApplicationService.queryUserByPhoneOrEmailWithPassword(emailOrPhone, password);
if (userDto != null) { if (userDto != null) {
validateLoginUser(userDto);
updateLastLoginTime(userDto); updateLastLoginTime(userDto);
return createToken(userDto, UUID.randomUUID().toString().replace("-", "")); return createToken(userDto, UUID.randomUUID().toString().replace("-", ""));
} }

View File

@@ -2,9 +2,11 @@ package com.xspaceagi.system.application.service.impl;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Date;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -23,6 +25,7 @@ import com.xspaceagi.system.domain.model.GroupBindMenuModel;
import com.xspaceagi.system.domain.model.MenuNode; import com.xspaceagi.system.domain.model.MenuNode;
import com.xspaceagi.system.domain.model.SortIndex; import com.xspaceagi.system.domain.model.SortIndex;
import com.xspaceagi.system.domain.service.SysGroupDomainService; import com.xspaceagi.system.domain.service.SysGroupDomainService;
import com.xspaceagi.system.domain.service.impl.GroupAuthorizationPolicy;
import com.xspaceagi.system.infra.dao.entity.SysDataPermission; import com.xspaceagi.system.infra.dao.entity.SysDataPermission;
import com.xspaceagi.system.infra.dao.entity.SysGroup; import com.xspaceagi.system.infra.dao.entity.SysGroup;
import com.xspaceagi.system.infra.dao.entity.User; import com.xspaceagi.system.infra.dao.entity.User;
@@ -141,6 +144,39 @@ public class SysGroupApplicationServiceImpl implements SysGroupApplicationServic
: groupList.stream().filter(g -> StatusEnum.isEnabled(g.getStatus())).toList(); : groupList.stream().filter(g -> StatusEnum.isEnabled(g.getStatus())).toList();
} }
@Override
public List<SysGroup> getAuthorizedGroupListForLogin(Long userId) {
return filterAuthorizedGroupsForLogin(getEffectiveGroupListByUserId(userId));
}
@Override
public SysGroup selectAuthorizedGroupForClient(Long userId, Map<Long, Long> clientCountByGroupId) {
return selectAuthorizedGroupForClient(getEffectiveGroupListByUserId(userId), clientCountByGroupId);
}
List<SysGroup> filterAuthorizedGroupsForLogin(List<SysGroup> groups) {
if (CollectionUtils.isEmpty(groups)) {
return List.of();
}
Date now = new Date();
return groups.stream()
.filter(group -> GroupAuthorizationPolicy.evaluate(group, -1, now).allowed())
.toList();
}
SysGroup selectAuthorizedGroupForClient(List<SysGroup> groups, Map<Long, Long> clientCountByGroupId) {
if (CollectionUtils.isEmpty(groups)) {
return null;
}
Date now = new Date();
Map<Long, Long> counts = clientCountByGroupId == null ? Map.of() : clientCountByGroupId;
return groups.stream()
.filter(group -> group.getId() != null)
.filter(group -> GroupAuthorizationPolicy.evaluate(group, counts.getOrDefault(group.getId(), 0L), now).allowed())
.findFirst()
.orElse(null);
}
private List<Long> getSubscriptionPlanGroupIds(Long userId) { private List<Long> getSubscriptionPlanGroupIds(Long userId) {
try { try {
UserSubscriptionDTO subscription = subscriptionRpcService.getUserCurrentSystemSubscription(userId); UserSubscriptionDTO subscription = subscriptionRpcService.getUserCurrentSystemSubscription(userId);

View File

@@ -0,0 +1,58 @@
package com.xspaceagi.system.application.service.impl;
import com.xspaceagi.system.infra.dao.entity.SysGroup;
import com.xspaceagi.system.spec.enums.StatusEnum;
import org.junit.jupiter.api.Test;
import java.util.Date;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
class SysGroupApplicationServiceImplAuthorizationTest {
@Test
void filtersGroupsWithoutActiveAuthorizationForLogin() {
SysGroupApplicationServiceImpl service = new SysGroupApplicationServiceImpl();
List<SysGroup> result = service.filterAuthorizedGroupsForLogin(List.of(
group(1L, 0, future(), -1),
group(2L, 1, past(), -1),
group(3L, 1, future(), -1)
));
assertThat(result).extracting(SysGroup::getId).containsExactly(3L);
}
@Test
void selectsAuthorizedGroupWhoseClientQuotaIsAvailable() {
SysGroupApplicationServiceImpl service = new SysGroupApplicationServiceImpl();
SysGroup result = service.selectAuthorizedGroupForClient(List.of(
group(1L, 1, future(), 1),
group(2L, 1, future(), 2)
), Map.of(1L, 1L, 2L, 1L));
assertThat(result).isNotNull();
assertThat(result.getId()).isEqualTo(2L);
}
private SysGroup group(Long id, Integer authEnabled, Date expireTime, Integer maxClientCount) {
SysGroup group = new SysGroup();
group.setId(id);
group.setStatus(StatusEnum.ENABLED.getCode());
group.setAuthEnabled(authEnabled);
group.setExpireTime(expireTime);
group.setMaxClientCount(maxClientCount);
return group;
}
private Date future() {
return new Date(System.currentTimeMillis() + 86_400_000L);
}
private Date past() {
return new Date(System.currentTimeMillis() - 86_400_000L);
}
}

View File

@@ -0,0 +1,55 @@
package com.xspaceagi.system.domain.service.impl;
import com.xspaceagi.system.infra.dao.entity.SysGroup;
import com.xspaceagi.system.spec.enums.StatusEnum;
import java.util.Date;
/**
* Centralized department authorization rule for login and client registration.
*/
public final class GroupAuthorizationPolicy {
private GroupAuthorizationPolicy() {
}
public static Result evaluate(SysGroup group, long currentClientCount, Date now) {
if (group == null) {
return Result.deny(Reason.NO_GROUP);
}
if (!StatusEnum.isEnabled(group.getStatus())) {
return Result.deny(Reason.GROUP_DISABLED);
}
if (!Integer.valueOf(1).equals(group.getAuthEnabled())) {
return Result.deny(Reason.AUTH_DISABLED);
}
Date expireTime = group.getExpireTime();
if (expireTime != null && !expireTime.after(now)) {
return Result.deny(Reason.EXPIRED);
}
Integer maxClientCount = group.getMaxClientCount();
if (maxClientCount != null && maxClientCount > -1 && currentClientCount >= maxClientCount) {
return Result.deny(Reason.CLIENT_QUOTA_REACHED);
}
return Result.allow();
}
public enum Reason {
ALLOWED,
NO_GROUP,
GROUP_DISABLED,
AUTH_DISABLED,
EXPIRED,
CLIENT_QUOTA_REACHED
}
public record Result(boolean allowed, Reason reason) {
private static Result allow() {
return new Result(true, Reason.ALLOWED);
}
private static Result deny(Reason reason) {
return new Result(false, reason);
}
}
}

View File

@@ -0,0 +1,90 @@
package com.xspaceagi.system.domain.service.impl;
import com.xspaceagi.system.infra.dao.entity.SysGroup;
import com.xspaceagi.system.spec.enums.StatusEnum;
import org.junit.jupiter.api.Test;
import java.util.Calendar;
import java.util.Date;
import static org.assertj.core.api.Assertions.assertThat;
class GroupAuthorizationPolicyTest {
@Test
void allowsEnabledAuthorizedGroupBeforeExpirationWhenClientQuotaAvailable() {
SysGroup group = group(StatusEnum.ENABLED.getCode(), 1, future(), 2);
GroupAuthorizationPolicy.Result result = GroupAuthorizationPolicy.evaluate(group, 1, now());
assertThat(result.allowed()).isTrue();
assertThat(result.reason()).isEqualTo(GroupAuthorizationPolicy.Reason.ALLOWED);
}
@Test
void deniesDisabledGroup() {
SysGroup group = group(StatusEnum.DISABLED.getCode(), 1, future(), 2);
GroupAuthorizationPolicy.Result result = GroupAuthorizationPolicy.evaluate(group, 0, now());
assertThat(result.allowed()).isFalse();
assertThat(result.reason()).isEqualTo(GroupAuthorizationPolicy.Reason.GROUP_DISABLED);
}
@Test
void deniesGroupWithoutAuthorization() {
SysGroup group = group(StatusEnum.ENABLED.getCode(), 0, future(), 2);
GroupAuthorizationPolicy.Result result = GroupAuthorizationPolicy.evaluate(group, 0, now());
assertThat(result.allowed()).isFalse();
assertThat(result.reason()).isEqualTo(GroupAuthorizationPolicy.Reason.AUTH_DISABLED);
}
@Test
void deniesExpiredGroup() {
SysGroup group = group(StatusEnum.ENABLED.getCode(), 1, past(), 2);
GroupAuthorizationPolicy.Result result = GroupAuthorizationPolicy.evaluate(group, 0, now());
assertThat(result.allowed()).isFalse();
assertThat(result.reason()).isEqualTo(GroupAuthorizationPolicy.Reason.EXPIRED);
}
@Test
void deniesWhenClientQuotaReached() {
SysGroup group = group(StatusEnum.ENABLED.getCode(), 1, future(), 2);
GroupAuthorizationPolicy.Result result = GroupAuthorizationPolicy.evaluate(group, 2, now());
assertThat(result.allowed()).isFalse();
assertThat(result.reason()).isEqualTo(GroupAuthorizationPolicy.Reason.CLIENT_QUOTA_REACHED);
}
private SysGroup group(Integer status, Integer authEnabled, Date expireTime, Integer maxClientCount) {
SysGroup group = new SysGroup();
group.setStatus(status);
group.setAuthEnabled(authEnabled);
group.setExpireTime(expireTime);
group.setMaxClientCount(maxClientCount);
return group;
}
private Date now() {
return new Date(1_700_000_000_000L);
}
private Date future() {
Calendar calendar = Calendar.getInstance();
calendar.setTime(now());
calendar.add(Calendar.DATE, 1);
return calendar.getTime();
}
private Date past() {
Calendar calendar = Calendar.getInstance();
calendar.setTime(now());
calendar.add(Calendar.DATE, -1);
return calendar.getTime();
}
}

View File

@@ -43,6 +43,21 @@ public class SysGroup {
*/ */
private Integer maxUserCount; private Integer maxUserCount;
/**
* 最大客户端数,-1 表示不限制
*/
private Integer maxClientCount;
/**
* 授权开关1-已授权0-未授权
*/
private Integer authEnabled;
/**
* 授权到期时间,空表示不过期
*/
private Date expireTime;
/** /**
* 来源1-系统内置2-用户自定义 * 来源1-系统内置2-用户自定义
* @see SourceEnum * @see SourceEnum

View File

@@ -120,6 +120,8 @@ public enum BizExceptionCodeEnum implements IBizExceptionCodeEnum {
systemRoleNotFoundWithRoleId("3122", "角色不存在,id=%s", "Role does not exist, id=%s", "RBAC"), systemRoleNotFoundWithRoleId("3122", "角色不存在,id=%s", "Role does not exist, id=%s", "RBAC"),
systemGroupMaxUsersBelowBound("3123", "最大用户数不能小于已绑定用户数,当前已绑定%s人", "Max users cannot be below the number already bound; currently %s user(s) bound.", "RBAC"), systemGroupMaxUsersBelowBound("3123", "最大用户数不能小于已绑定用户数,当前已绑定%s人", "Max users cannot be below the number already bound; currently %s user(s) bound.", "RBAC"),
systemGroupUserLimitExceeded("3124", "组用户数量超出限制", "Group user count exceeds the limit.", "RBAC"), systemGroupUserLimitExceeded("3124", "组用户数量超出限制", "Group user count exceeds the limit.", "RBAC"),
systemGroupAuthorizationUnavailable("3186", "当前用户组未授权或授权已过期", "The current user group is not authorized or the authorization has expired.", "RBAC"),
systemGroupClientLimitExceeded("3187", "用户组客户端数量超出限制", "Group client count exceeds the limit.", "RBAC"),
systemGroupNotFoundWithRowId("3125", "用户组不存在: id=%s", "User group does not exist: id=%s", "RBAC"), systemGroupNotFoundWithRowId("3125", "用户组不存在: id=%s", "User group does not exist: id=%s", "RBAC"),
systemGroupNotFoundWithGroupId("3126", "组不存在,id=%s", "Group does not exist, id=%s", "RBAC"), systemGroupNotFoundWithGroupId("3126", "组不存在,id=%s", "Group does not exist, id=%s", "RBAC"),
systemGroupNotFoundWithGroupIdAlt("3127", "用户组不存在,id=%s", "User group does not exist, id=%s", "RBAC"), systemGroupNotFoundWithGroupIdAlt("3127", "用户组不存在,id=%s", "User group does not exist, id=%s", "RBAC"),

View File

@@ -9,6 +9,7 @@ import com.xspaceagi.system.application.dto.permission.*;
import com.xspaceagi.system.application.service.SysDataPermissionApplicationService; import com.xspaceagi.system.application.service.SysDataPermissionApplicationService;
import com.xspaceagi.system.application.service.SysGroupApplicationService; import com.xspaceagi.system.application.service.SysGroupApplicationService;
import com.xspaceagi.system.application.service.SysSubjectPermissionApplicationService; import com.xspaceagi.system.application.service.SysSubjectPermissionApplicationService;
import com.xspaceagi.system.application.dto.UserDto;
import com.xspaceagi.system.domain.model.GroupBindMenuModel; import com.xspaceagi.system.domain.model.GroupBindMenuModel;
import com.xspaceagi.system.domain.model.MenuNode; import com.xspaceagi.system.domain.model.MenuNode;
import com.xspaceagi.system.domain.model.SortIndex; import com.xspaceagi.system.domain.model.SortIndex;
@@ -16,6 +17,7 @@ import com.xspaceagi.system.infra.dao.entity.SysDataPermission;
import com.xspaceagi.system.infra.dao.entity.SysGroup; import com.xspaceagi.system.infra.dao.entity.SysGroup;
import com.xspaceagi.system.infra.dao.entity.User; import com.xspaceagi.system.infra.dao.entity.User;
import com.xspaceagi.system.spec.annotation.RequireResource; import com.xspaceagi.system.spec.annotation.RequireResource;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.dto.PageQueryVo; import com.xspaceagi.system.spec.dto.PageQueryVo;
import com.xspaceagi.system.spec.dto.ReqResult; import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.enums.*; import com.xspaceagi.system.spec.enums.*;
@@ -36,6 +38,7 @@ import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static com.xspaceagi.system.spec.enums.ResourceEnum.*; import static com.xspaceagi.system.spec.enums.ResourceEnum.*;
@@ -89,6 +92,7 @@ public class SysGroupController extends BaseController {
SysGroup group = new SysGroup(); SysGroup group = new SysGroup();
BeanUtils.copyProperties(dto, group); BeanUtils.copyProperties(dto, group);
checkGroupManageScope(group.getId());
group.setCode(null); // code不允许更新 group.setCode(null); // code不允许更新
sysGroupApplicationService.updateGroup(group, getUser()); sysGroupApplicationService.updateGroup(group, getUser());
return ReqResult.success(); return ReqResult.success();
@@ -101,6 +105,7 @@ public class SysGroupController extends BaseController {
if (groupId == null) { if (groupId == null) {
return ReqResult.error("参数不能为空"); return ReqResult.error("参数不能为空");
} }
checkGroupManageScope(groupId);
sysGroupApplicationService.deleteGroup(groupId, getUser()); sysGroupApplicationService.deleteGroup(groupId, getUser());
return ReqResult.success(); return ReqResult.success();
} }
@@ -112,6 +117,7 @@ public class SysGroupController extends BaseController {
if (groupId == null) { if (groupId == null) {
return ReqResult.error("参数不能为空"); return ReqResult.error("参数不能为空");
} }
checkGroupManageScope(groupId);
SysGroup group = sysGroupApplicationService.getGroupById(groupId); SysGroup group = sysGroupApplicationService.getGroupById(groupId);
if (group == null) { if (group == null) {
@@ -141,6 +147,7 @@ public class SysGroupController extends BaseController {
if (group == null) { if (group == null) {
return ReqResult.error("用户组不存在"); return ReqResult.error("用户组不存在");
} }
checkGroupManageScope(group.getId());
SysGroupDto groupDto = new SysGroupDto(); SysGroupDto groupDto = new SysGroupDto();
BeanUtils.copyProperties(group, groupDto); BeanUtils.copyProperties(group, groupDto);
@@ -162,6 +169,7 @@ public class SysGroupController extends BaseController {
} }
List<SortIndex> sortIndexList = dto.getItems().stream() List<SortIndex> sortIndexList = dto.getItems().stream()
.map(item -> { .map(item -> {
checkGroupManageScope(item.getId());
SortIndex model = new SortIndex(); SortIndex model = new SortIndex();
model.setId(item.getId()); model.setId(item.getId());
model.setParentId(item.getParentId()); model.setParentId(item.getParentId());
@@ -183,6 +191,7 @@ public class SysGroupController extends BaseController {
} }
List<SysGroup> groupList = sysGroupApplicationService.getGroupList(sysGroup); List<SysGroup> groupList = sysGroupApplicationService.getGroupList(sysGroup);
groupList = filterGroupManageScope(groupList);
if (CollectionUtils.isEmpty(groupList)) { if (CollectionUtils.isEmpty(groupList)) {
return ReqResult.success(); return ReqResult.success();
} }
@@ -214,6 +223,7 @@ public class SysGroupController extends BaseController {
} }
SysGroupUserQueryDto queryDto = pageQueryVo.getQueryFilter(); SysGroupUserQueryDto queryDto = pageQueryVo.getQueryFilter();
Long groupId = queryDto.getGroupId(); Long groupId = queryDto.getGroupId();
checkGroupManageScope(groupId);
String userName = queryDto.getUserName(); String userName = queryDto.getUserName();
long pageNo = pageQueryVo.getPageNo() != null ? pageQueryVo.getPageNo() : 1L; long pageNo = pageQueryVo.getPageNo() != null ? pageQueryVo.getPageNo() : 1L;
long pageSize = pageQueryVo.getPageSize() != null ? pageQueryVo.getPageSize() : 10L; long pageSize = pageQueryVo.getPageSize() != null ? pageQueryVo.getPageSize() : 10L;
@@ -238,6 +248,7 @@ public class SysGroupController extends BaseController {
if (groupId == null) { if (groupId == null) {
return ReqResult.error("参数不能为空"); return ReqResult.error("参数不能为空");
} }
checkGroupManageScope(groupId);
SysDataPermission dataPermission = sysDataPermissionApplicationService.getByTarget(PermissionTargetTypeEnum.GROUP, groupId); SysDataPermission dataPermission = sysDataPermissionApplicationService.getByTarget(PermissionTargetTypeEnum.GROUP, groupId);
SysDataPermissionBindDto result = SysDataPermissionConverter.toDto(dataPermission); SysDataPermissionBindDto result = SysDataPermissionConverter.toDto(dataPermission);
result = result == null ? new SysDataPermissionBindDto() : result; result = result == null ? new SysDataPermissionBindDto() : result;
@@ -282,6 +293,7 @@ public class SysGroupController extends BaseController {
if (dto == null || dto.getGroupId() == null) { if (dto == null || dto.getGroupId() == null) {
return ReqResult.error("用户组ID不能为空"); return ReqResult.error("用户组ID不能为空");
} }
checkGroupManageScope(dto.getGroupId());
SysDataPermissionBindDto bindDto = dto.getDataPermission(); SysDataPermissionBindDto bindDto = dto.getDataPermission();
SysDataPermission dataPermission = SysDataPermissionConverter.toEntity(bindDto); SysDataPermission dataPermission = SysDataPermissionConverter.toEntity(bindDto);
sysGroupApplicationService.bindDataPermission(dto.getGroupId(), dataPermission, getUser()); sysGroupApplicationService.bindDataPermission(dto.getGroupId(), dataPermission, getUser());
@@ -295,6 +307,7 @@ public class SysGroupController extends BaseController {
if (dto == null) { if (dto == null) {
return ReqResult.error("参数不能为空"); return ReqResult.error("参数不能为空");
} }
checkGroupManageScope(dto.getGroupId());
sysGroupApplicationService.groupBindUser(dto.getGroupId(), dto.getUserIds(), getUser()); sysGroupApplicationService.groupBindUser(dto.getGroupId(), dto.getUserIds(), getUser());
return ReqResult.success(); return ReqResult.success();
} }
@@ -306,6 +319,7 @@ public class SysGroupController extends BaseController {
if (dto == null || dto.getGroupId() == null || dto.getUserId() == null) { if (dto == null || dto.getGroupId() == null || dto.getUserId() == null) {
return ReqResult.error("用户组ID和用户ID不能为空"); return ReqResult.error("用户组ID和用户ID不能为空");
} }
checkGroupManageScope(dto.getGroupId());
sysGroupApplicationService.groupAddUser(dto.getGroupId(), dto.getUserId(), getUser()); sysGroupApplicationService.groupAddUser(dto.getGroupId(), dto.getUserId(), getUser());
return ReqResult.success(); return ReqResult.success();
} }
@@ -317,6 +331,7 @@ public class SysGroupController extends BaseController {
if (dto == null || dto.getGroupId() == null || dto.getUserId() == null) { if (dto == null || dto.getGroupId() == null || dto.getUserId() == null) {
return ReqResult.error("用户组ID和用户ID不能为空"); return ReqResult.error("用户组ID和用户ID不能为空");
} }
checkGroupManageScope(dto.getGroupId());
sysGroupApplicationService.groupRemoveUser(dto.getGroupId(), dto.getUserId(), getUser()); sysGroupApplicationService.groupRemoveUser(dto.getGroupId(), dto.getUserId(), getUser());
return ReqResult.success(); return ReqResult.success();
} }
@@ -328,6 +343,7 @@ public class SysGroupController extends BaseController {
if (dto == null) { if (dto == null) {
return ReqResult.error("参数不能为空"); return ReqResult.error("参数不能为空");
} }
checkGroupManageScope(dto.getGroupId());
// 用户组不允许绑定 生态市场/系统管理 // 用户组不允许绑定 生态市场/系统管理
checkForbiddenMenuCodes(dto.getMenuTree()); checkForbiddenMenuCodes(dto.getMenuTree());
@@ -343,6 +359,7 @@ public class SysGroupController extends BaseController {
if (groupId == null) { if (groupId == null) {
return ReqResult.error("参数不能为空"); return ReqResult.error("参数不能为空");
} }
checkGroupManageScope(groupId);
// 获取处理好的菜单树(已包含资源详细信息) // 获取处理好的菜单树(已包含资源详细信息)
List<MenuNode> menuNodeList = sysGroupApplicationService.getMenuTreeByGroupId(groupId); List<MenuNode> menuNodeList = sysGroupApplicationService.getMenuTreeByGroupId(groupId);
@@ -373,4 +390,41 @@ public class SysGroupController extends BaseController {
} }
} }
} }
}
private List<SysGroup> filterGroupManageScope(List<SysGroup> groupList) {
if (isPlatformAdmin() || CollectionUtils.isEmpty(groupList)) {
return groupList;
}
Set<Long> allowedGroupIds = currentUserGroupIds();
return groupList.stream()
.filter(group -> group.getId() != null && allowedGroupIds.contains(group.getId()))
.collect(Collectors.toList());
}
private void checkGroupManageScope(Long groupId) {
if (isPlatformAdmin()) {
return;
}
if (groupId == null || !currentUserGroupIds().contains(groupId)) {
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied);
}
}
private Set<Long> currentUserGroupIds() {
return sysGroupApplicationService.getEffectiveGroupListByUserId(getCurrentUserDto().getId()).stream()
.map(SysGroup::getId)
.collect(Collectors.toSet());
}
private boolean isPlatformAdmin() {
return getCurrentUserDto().getRole() == User.Role.Admin;
}
private UserDto getCurrentUserDto() {
UserDto userDto = (UserDto) RequestContext.get().getUser();
if (userDto == null) {
throw BizException.of(ErrorCodeEnum.UNAUTHORIZED, BizExceptionCodeEnum.systemUserNotLoggedInWeb);
}
return userDto;
}
}

View File

@@ -1,5 +1,6 @@
package com.xspaceagi.system.web.controller.permission; package com.xspaceagi.system.web.controller.permission;
import com.xspaceagi.system.application.dto.UserDto;
import com.xspaceagi.system.application.dto.permission.*; import com.xspaceagi.system.application.dto.permission.*;
import com.xspaceagi.system.application.service.SysDataPermissionApplicationService; import com.xspaceagi.system.application.service.SysDataPermissionApplicationService;
import com.xspaceagi.system.application.service.SysGroupApplicationService; import com.xspaceagi.system.application.service.SysGroupApplicationService;
@@ -7,10 +8,15 @@ import com.xspaceagi.system.application.service.SysRoleApplicationService;
import com.xspaceagi.system.application.service.impl.SysUserPermissionCacheServiceImpl; import com.xspaceagi.system.application.service.impl.SysUserPermissionCacheServiceImpl;
import com.xspaceagi.system.infra.dao.entity.SysGroup; import com.xspaceagi.system.infra.dao.entity.SysGroup;
import com.xspaceagi.system.infra.dao.entity.SysRole; import com.xspaceagi.system.infra.dao.entity.SysRole;
import com.xspaceagi.system.infra.dao.entity.User;
import com.xspaceagi.system.sdk.service.dto.UserDataPermissionDto; import com.xspaceagi.system.sdk.service.dto.UserDataPermissionDto;
import com.xspaceagi.system.spec.annotation.RequireResource; import com.xspaceagi.system.spec.annotation.RequireResource;
import com.xspaceagi.system.spec.annotation.SaasAdmin; import com.xspaceagi.system.spec.annotation.SaasAdmin;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.dto.ReqResult; import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
import com.xspaceagi.system.spec.exception.BizException;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import com.xspaceagi.system.spec.utils.I18nUtil; import com.xspaceagi.system.spec.utils.I18nUtil;
import com.xspaceagi.system.web.controller.base.BaseController; import com.xspaceagi.system.web.controller.base.BaseController;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@@ -22,6 +28,7 @@ import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static com.xspaceagi.system.spec.enums.ResourceEnum.*; import static com.xspaceagi.system.spec.enums.ResourceEnum.*;
@@ -48,6 +55,7 @@ public class SysUserController extends BaseController {
if (userId == null) { if (userId == null) {
return ReqResult.error("参数不能为空"); return ReqResult.error("参数不能为空");
} }
checkUserManageScope(userId);
List<SysRole> roleList = sysRoleApplicationService.getRoleListByUserId(userId); List<SysRole> roleList = sysRoleApplicationService.getRoleListByUserId(userId);
if (CollectionUtils.isEmpty(roleList)) { if (CollectionUtils.isEmpty(roleList)) {
@@ -70,6 +78,8 @@ public class SysUserController extends BaseController {
if (dto == null) { if (dto == null) {
return ReqResult.error("参数不能为空"); return ReqResult.error("参数不能为空");
} }
checkUserManageScope(dto.getUserId());
checkRoleBindScope(dto.getRoleIds());
sysRoleApplicationService.userBindRole(dto.getUserId(), dto.getRoleIds(), getUser()); sysRoleApplicationService.userBindRole(dto.getUserId(), dto.getRoleIds(), getUser());
return ReqResult.success(); return ReqResult.success();
} }
@@ -81,6 +91,7 @@ public class SysUserController extends BaseController {
if (userId == null) { if (userId == null) {
return ReqResult.error("参数不能为空"); return ReqResult.error("参数不能为空");
} }
checkUserManageScope(userId);
List<SysGroup> groupList = sysGroupApplicationService.getGroupListByUserId(userId); List<SysGroup> groupList = sysGroupApplicationService.getGroupListByUserId(userId);
if (CollectionUtils.isEmpty(groupList)) { if (CollectionUtils.isEmpty(groupList)) {
return ReqResult.success(); return ReqResult.success();
@@ -102,6 +113,8 @@ public class SysUserController extends BaseController {
if (dto == null) { if (dto == null) {
return ReqResult.error("参数不能为空"); return ReqResult.error("参数不能为空");
} }
checkUserManageScope(dto.getUserId());
checkGroupBindScope(dto.getGroupIds());
sysGroupApplicationService.userBindGroup(dto.getUserId(), dto.getGroupIds(), getUser()); sysGroupApplicationService.userBindGroup(dto.getUserId(), dto.getGroupIds(), getUser());
return ReqResult.success(); return ReqResult.success();
} }
@@ -113,6 +126,7 @@ public class SysUserController extends BaseController {
if (userId == null) { if (userId == null) {
return ReqResult.error("参数不能为空"); return ReqResult.error("参数不能为空");
} }
checkUserManageScope(userId);
List<MenuNodeDto> menuTree = sysUserPermissionCacheService.getUserMenuTree(userId); List<MenuNodeDto> menuTree = sysUserPermissionCacheService.getUserMenuTree(userId);
I18nUtil.replaceSystemMessage(menuTree); I18nUtil.replaceSystemMessage(menuTree);
@@ -126,10 +140,66 @@ public class SysUserController extends BaseController {
if (userId == null) { if (userId == null) {
return ReqResult.error("参数不能为空"); return ReqResult.error("参数不能为空");
} }
checkUserManageScope(userId);
UserDataPermissionDto dataPermission = sysDataPermissionApplicationService.getUserDataPermission(userId); UserDataPermissionDto dataPermission = sysDataPermissionApplicationService.getUserDataPermission(userId);
return ReqResult.success(dataPermission); return ReqResult.success(dataPermission);
} }
private void checkUserManageScope(Long userId) {
if (isPlatformAdmin()) {
return;
}
Set<Long> allowedGroupIds = currentUserGroupIds();
boolean inScope = sysGroupApplicationService.getEffectiveGroupListByUserId(userId).stream()
.map(SysGroup::getId)
.anyMatch(allowedGroupIds::contains);
if (!inScope) {
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied);
}
}
private void checkGroupBindScope(List<Long> groupIds) {
if (isPlatformAdmin() || CollectionUtils.isEmpty(groupIds)) {
return;
}
Set<Long> allowedGroupIds = currentUserGroupIds();
boolean allInScope = groupIds.stream().allMatch(allowedGroupIds::contains);
if (!allInScope) {
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied);
}
}
private void checkRoleBindScope(List<Long> roleIds) {
if (isPlatformAdmin() || CollectionUtils.isEmpty(roleIds)) {
return;
}
Set<Long> allowedRoleIds = sysRoleApplicationService.getRoleListByUserId(getCurrentUserDto().getId()).stream()
.map(SysRole::getId)
.collect(Collectors.toSet());
boolean allInScope = roleIds.stream().allMatch(allowedRoleIds::contains);
if (!allInScope) {
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied);
}
}
private Set<Long> currentUserGroupIds() {
return sysGroupApplicationService.getEffectiveGroupListByUserId(getCurrentUserDto().getId()).stream()
.map(SysGroup::getId)
.collect(Collectors.toSet());
}
private boolean isPlatformAdmin() {
return getCurrentUserDto().getRole() == User.Role.Admin;
}
private UserDto getCurrentUserDto() {
UserDto userDto = (UserDto) RequestContext.get().getUser();
if (userDto == null) {
throw BizException.of(ErrorCodeEnum.UNAUTHORIZED, BizExceptionCodeEnum.systemUserNotLoggedInWeb);
}
return userDto;
}
@SaasAdmin @SaasAdmin
@Operation(summary = "清除指定用户权限缓存") @Operation(summary = "清除指定用户权限缓存")
@GetMapping("/cache/clear-user") @GetMapping("/cache/clear-user")
@@ -148,4 +218,4 @@ public class SysUserController extends BaseController {
sysUserPermissionCacheService.clearCacheAllByTenant(tenantId); sysUserPermissionCacheService.clearCacheAllByTenant(tenantId);
return ReqResult.success(); return ReqResult.success();
} }
} }

View File

@@ -929,6 +929,7 @@ CREATE TABLE `sandbox_config`
`_tenant_id` bigint(20) NOT NULL COMMENT '租户id', `_tenant_id` bigint(20) NOT NULL COMMENT '租户id',
`scope` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '配置范围global-全局配置 user-个人配置', `scope` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '配置范围global-全局配置 user-个人配置',
`user_id` bigint(20) DEFAULT NULL COMMENT '用户IDscope为user时必填', `user_id` bigint(20) DEFAULT NULL COMMENT '用户IDscope为user时必填',
`group_id` bigint(20) DEFAULT NULL COMMENT '用户组/单位ID',
`agent_id` bigint(20) DEFAULT NULL COMMENT '智能体电脑绑定的agentId', `agent_id` bigint(20) DEFAULT NULL COMMENT '智能体电脑绑定的agentId',
`name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '配置名称(用于界面显示)', `name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '配置名称(用于界面显示)',
`config_key` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '唯一标识', `config_key` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '唯一标识',
@@ -1120,6 +1121,9 @@ CREATE TABLE `sys_group`
`name` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '名称', `name` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '名称',
`description` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '描述', `description` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '描述',
`max_user_count` int(11) DEFAULT NULL COMMENT '最大用户数', `max_user_count` int(11) DEFAULT NULL COMMENT '最大用户数',
`max_client_count` int(11) DEFAULT -1 COMMENT '最大客户端数,-1表示不限制',
`auth_enabled` tinyint(4) DEFAULT 0 COMMENT '授权开关1-已授权0-未授权',
`expire_time` datetime DEFAULT NULL COMMENT '授权到期时间,空表示不过期',
`source` tinyint(4) NOT NULL COMMENT '来源1-系统内置2-用户自定义,对应 SourceEnum', `source` tinyint(4) NOT NULL COMMENT '来源1-系统内置2-用户自定义,对应 SourceEnum',
`status` tinyint(4) NOT NULL COMMENT '状态1-启用0-禁用', `status` tinyint(4) NOT NULL COMMENT '状态1-启用0-禁用',
`sort_index` int(11) DEFAULT NULL COMMENT '排序', `sort_index` int(11) DEFAULT NULL COMMENT '排序',
@@ -1906,6 +1910,7 @@ ALTER TABLE `sandbox_config`
ADD UNIQUE KEY `uk_scope_user_key` (`scope`,`user_id`,`config_key`), ADD UNIQUE KEY `uk_scope_user_key` (`scope`,`user_id`,`config_key`),
ADD KEY `idx_scope` (`scope`), ADD KEY `idx_scope` (`scope`),
ADD KEY `idx_user_id` (`user_id`), ADD KEY `idx_user_id` (`user_id`),
ADD KEY `idx_group_id` (`group_id`),
ADD KEY `idx_config_key` (`config_key`), ADD KEY `idx_config_key` (`config_key`),
ADD KEY `idx_is_active` (`is_active`); ADD KEY `idx_is_active` (`is_active`);
@@ -3180,4 +3185,4 @@ ALTER TABLE `pay_order`
ALTER TABLE `pay_order` ALTER TABLE `pay_order`
ADD KEY `idx_reconcile_scan` (`gateway_sync_status`, `gateway_order_status`, `modified`, `id`); ADD KEY `idx_reconcile_scan` (`gateway_sync_status`, `gateway_order_status`, `modified`, `id`);
ALTER TABLE `pay_order` ALTER TABLE `pay_order`
ADD UNIQUE KEY `uk_tenant_biz_order` (`_tenant_id`, `biz_order_no`); ADD UNIQUE KEY `uk_tenant_biz_order` (`_tenant_id`, `biz_order_no`);

View File

@@ -0,0 +1,8 @@
-- 修改表: sys_group
ALTER TABLE `sys_group` ADD COLUMN `max_client_count` int(11) DEFAULT -1 COMMENT '最大客户端数,-1表示不限制' AFTER `max_user_count`;
ALTER TABLE `sys_group` ADD COLUMN `auth_enabled` tinyint(4) DEFAULT 0 COMMENT '授权开关1-已授权0-未授权' AFTER `max_client_count`;
ALTER TABLE `sys_group` ADD COLUMN `expire_time` datetime DEFAULT NULL COMMENT '授权到期时间,空表示不过期' AFTER `auth_enabled`;
-- 修改表: sandbox_config
ALTER TABLE `sandbox_config` ADD COLUMN `group_id` bigint(20) DEFAULT NULL COMMENT '用户组/单位ID' AFTER `user_id`;
ALTER TABLE `sandbox_config` ADD KEY `idx_group_id` (`group_id`);

View File

@@ -1088,7 +1088,7 @@ export const ZH_CN: SystemLangMap = {
"PC.Constants.System.systemSetting": "系统设置", "PC.Constants.System.systemSetting": "系统设置",
"PC.Constants.System.taskManage": "任务管理", "PC.Constants.System.taskManage": "任务管理",
"PC.Constants.System.themeConfig": "主题配置", "PC.Constants.System.themeConfig": "主题配置",
"PC.Constants.System.userGroupManage": "用户组管理", "PC.Constants.System.userGroupManage": "单位(部门)管理",
"PC.Constants.System.userManage": "用户管理", "PC.Constants.System.userManage": "用户管理",
"PC.Constants.Theme.bgCloudyDay": "云朵白天", "PC.Constants.Theme.bgCloudyDay": "云朵白天",
"PC.Constants.Theme.bgCloudyDayDesc": "浅色背景,适合浅色布局风格", "PC.Constants.Theme.bgCloudyDayDesc": "浅色背景,适合浅色布局风格",
@@ -4530,8 +4530,8 @@ export const ZH_CN: SystemLangMap = {
"PC.Pages.SystemTargetAuthModal.roleManagement": "角色管理", "PC.Pages.SystemTargetAuthModal.roleManagement": "角色管理",
"PC.Pages.SystemTargetAuthModal.roleTab": "角色", "PC.Pages.SystemTargetAuthModal.roleTab": "角色",
"PC.Pages.SystemTargetAuthModal.selectAll": "全选", "PC.Pages.SystemTargetAuthModal.selectAll": "全选",
"PC.Pages.SystemTargetAuthModal.userGroupManagement": "用户组管理", "PC.Pages.SystemTargetAuthModal.userGroupManagement": "单位(部门)管理",
"PC.Pages.SystemTargetAuthModal.userGroupTab": "用户组", "PC.Pages.SystemTargetAuthModal.userGroupTab": "单位(部门)",
"PC.Pages.SystemTaskCenterProTable.actions": "操作", "PC.Pages.SystemTaskCenterProTable.actions": "操作",
"PC.Pages.SystemTaskCenterProTable.agent": "智能体", "PC.Pages.SystemTaskCenterProTable.agent": "智能体",
"PC.Pages.SystemTaskCenterProTable.confirmDeleteTask": "确认删除该任务?", "PC.Pages.SystemTaskCenterProTable.confirmDeleteTask": "确认删除该任务?",
@@ -4604,16 +4604,16 @@ export const ZH_CN: SystemLangMap = {
"PC.Pages.SystemThemeConfig.saveConfig": "保存配置", "PC.Pages.SystemThemeConfig.saveConfig": "保存配置",
"PC.Pages.SystemThemeConfig.saveSuccess": "主题配置保存成功", "PC.Pages.SystemThemeConfig.saveSuccess": "主题配置保存成功",
"PC.Pages.SystemUserGroupFormModal.create": "创建", "PC.Pages.SystemUserGroupFormModal.create": "创建",
"PC.Pages.SystemUserGroupFormModal.createTitle": "新增用户组", "PC.Pages.SystemUserGroupFormModal.createTitle": "新增单位(部门)",
"PC.Pages.SystemUserGroupFormModal.description": "描述", "PC.Pages.SystemUserGroupFormModal.description": "描述",
"PC.Pages.SystemUserGroupFormModal.descriptionPlaceholder": "请输入用户组描述", "PC.Pages.SystemUserGroupFormModal.descriptionPlaceholder": "请输入单位(部门)描述",
"PC.Pages.SystemUserGroupFormModal.disabled": "禁用", "PC.Pages.SystemUserGroupFormModal.disabled": "禁用",
"PC.Pages.SystemUserGroupFormModal.editTitle": "编辑用户组", "PC.Pages.SystemUserGroupFormModal.editTitle": "编辑单位(部门)",
"PC.Pages.SystemUserGroupFormModal.enabled": "启用", "PC.Pages.SystemUserGroupFormModal.enabled": "启用",
"PC.Pages.SystemUserGroupFormModal.formValidateFailed": "表单验证失败", "PC.Pages.SystemUserGroupFormModal.formValidateFailed": "表单验证失败",
"PC.Pages.SystemUserGroupFormModal.name": "用户组名称", "PC.Pages.SystemUserGroupFormModal.name": "单位(部门)名称",
"PC.Pages.SystemUserGroupFormModal.namePlaceholder": "请输入用户组名称", "PC.Pages.SystemUserGroupFormModal.namePlaceholder": "请输入单位(部门)名称",
"PC.Pages.SystemUserGroupFormModal.nameRequired": "请输入用户组名称", "PC.Pages.SystemUserGroupFormModal.nameRequired": "请输入单位(部门)名称",
"PC.Pages.SystemUserGroupFormModal.save": "保存", "PC.Pages.SystemUserGroupFormModal.save": "保存",
"PC.Pages.SystemUserGroupFormModal.sort": "排序", "PC.Pages.SystemUserGroupFormModal.sort": "排序",
"PC.Pages.SystemUserGroupFormModal.sortPlaceholder": "请输入排序", "PC.Pages.SystemUserGroupFormModal.sortPlaceholder": "请输入排序",
@@ -4622,32 +4622,52 @@ export const ZH_CN: SystemLangMap = {
"PC.Pages.SystemUserGroupFormModal.sourceSystemBuiltIn": "系统内置", "PC.Pages.SystemUserGroupFormModal.sourceSystemBuiltIn": "系统内置",
"PC.Pages.SystemUserGroupFormModal.sourceUserDefined": "用户自定义", "PC.Pages.SystemUserGroupFormModal.sourceUserDefined": "用户自定义",
"PC.Pages.SystemUserGroupFormModal.status": "状态", "PC.Pages.SystemUserGroupFormModal.status": "状态",
"PC.Pages.SystemUserGroupFormModal.statusTip": "启用或禁用此用户组", "PC.Pages.SystemUserGroupFormModal.statusTip": "启用或禁用此单位(部门)",
"PC.Pages.SystemUserGroupManage.add": "新增用户", "PC.Pages.SystemUserGroupFormModal.maxUserCount": "最大用户",
"PC.Pages.SystemUserGroupManage.bindUser": "绑定用户", "PC.Pages.SystemUserGroupFormModal.maxUserCountPlaceholder": "不填表示不限制",
"PC.Pages.SystemUserGroupManage.builtInCannotDelete": "系统内置的用户组不能删除", "PC.Pages.SystemUserGroupFormModal.maxClientCount": "最大客户端数",
"PC.Pages.SystemUserGroupManage.builtInCannotDisable": "系统内置的用户组不能禁用", "PC.Pages.SystemUserGroupFormModal.maxClientCountPlaceholder": "-1 表示不限制",
"PC.Pages.SystemUserGroupManage.builtInCannotEdit": "系统内置的用户组不能编辑", "PC.Pages.SystemUserGroupFormModal.authEnabled": "单位授权",
"PC.Pages.SystemUserGroupFormModal.authEnabledTip": "未授权或已过期时,单位成员不能登录系统或注册客户端",
"PC.Pages.SystemUserGroupFormModal.authorized": "已授权",
"PC.Pages.SystemUserGroupFormModal.unauthorized": "未授权",
"PC.Pages.SystemUserGroupFormModal.expireTime": "到期时间",
"PC.Pages.SystemUserGroupFormModal.expireTimePlaceholder": "不填表示不过期",
"PC.Pages.SystemUserGroupManage.add": "新增单位(部门)",
"PC.Pages.SystemUserGroupManage.bindUser": "绑定成员",
"PC.Pages.SystemUserGroupManage.bindMember": "绑定成员",
"PC.Pages.SystemUserGroupManage.builtInCannotDelete": "系统内置的单位(部门)不能删除",
"PC.Pages.SystemUserGroupManage.builtInCannotDisable": "系统内置的单位(部门)不能禁用",
"PC.Pages.SystemUserGroupManage.builtInCannotEdit": "系统内置的单位(部门)不能编辑",
"PC.Pages.SystemUserGroupManage.columnAction": "操作", "PC.Pages.SystemUserGroupManage.columnAction": "操作",
"PC.Pages.SystemUserGroupManage.columnCode": "编码", "PC.Pages.SystemUserGroupManage.columnCode": "编码",
"PC.Pages.SystemUserGroupManage.columnDescription": "描述", "PC.Pages.SystemUserGroupManage.columnDescription": "描述",
"PC.Pages.SystemUserGroupManage.columnName": "用户组名称", "PC.Pages.SystemUserGroupManage.columnName": "单位(部门)名称",
"PC.Pages.SystemUserGroupManage.columnAuthorization": "授权状态",
"PC.Pages.SystemUserGroupManage.columnMaxUsers": "最大用户数",
"PC.Pages.SystemUserGroupManage.columnMaxClients": "最大客户端数",
"PC.Pages.SystemUserGroupManage.columnExpireTime": "到期时间",
"PC.Pages.SystemUserGroupManage.columnSort": "排序", "PC.Pages.SystemUserGroupManage.columnSort": "排序",
"PC.Pages.SystemUserGroupManage.columnStatus": "状态", "PC.Pages.SystemUserGroupManage.columnStatus": "状态",
"PC.Pages.SystemUserGroupManage.dataPermission": "数据权限", "PC.Pages.SystemUserGroupManage.dataPermission": "数据权限",
"PC.Pages.SystemUserGroupManage.delete": "删除", "PC.Pages.SystemUserGroupManage.delete": "删除",
"PC.Pages.SystemUserGroupManage.deleteConfirm": "确认删除用户组 \"{0}\" 吗?", "PC.Pages.SystemUserGroupManage.deleteConfirm": "确认删除单位(部门) \"{0}\" 吗?",
"PC.Pages.SystemUserGroupManage.deleteSuccess": "删除成功", "PC.Pages.SystemUserGroupManage.deleteSuccess": "删除成功",
"PC.Pages.SystemUserGroupManage.deleteTitle": "删除用户组", "PC.Pages.SystemUserGroupManage.deleteTitle": "删除单位(部门)",
"PC.Pages.SystemUserGroupManage.disabled": "禁用", "PC.Pages.SystemUserGroupManage.disabled": "禁用",
"PC.Pages.SystemUserGroupManage.edit": "编辑", "PC.Pages.SystemUserGroupManage.edit": "编辑",
"PC.Pages.SystemUserGroupManage.empty": "暂无用户组数据", "PC.Pages.SystemUserGroupManage.empty": "暂无单位(部门)数据",
"PC.Pages.SystemUserGroupManage.enabled": "启用", "PC.Pages.SystemUserGroupManage.enabled": "启用",
"PC.Pages.SystemUserGroupManage.menuPermission": "单权限", "PC.Pages.SystemUserGroupManage.menuPermission": "单权限",
"PC.Pages.SystemUserGroupManage.unitPermission": "单位权限",
"PC.Pages.SystemUserGroupManage.noPermission": "无此资源权限", "PC.Pages.SystemUserGroupManage.noPermission": "无此资源权限",
"PC.Pages.SystemUserGroupManage.pageTitle": "用户组管理", "PC.Pages.SystemUserGroupManage.pageTitle": "单位(部门)管理",
"PC.Pages.SystemUserGroupManage.sortUpdated": "排序更新成功", "PC.Pages.SystemUserGroupManage.sortUpdated": "排序更新成功",
"PC.Pages.SystemUserGroupManage.statusTip": "启用或禁用此用户组", "PC.Pages.SystemUserGroupManage.statusTip": "启用或禁用此单位(部门)",
"PC.Pages.SystemUserGroupManage.authorized": "已授权",
"PC.Pages.SystemUserGroupManage.unauthorized": "未授权",
"PC.Pages.SystemUserGroupManage.unlimited": "不限制",
"PC.Pages.SystemUserGroupManage.neverExpire": "不过期",
"PC.Pages.TeamSetting.AddMember.addNewMember": "添加新成员", "PC.Pages.TeamSetting.AddMember.addNewMember": "添加新成员",
"PC.Pages.TeamSetting.AddMember.addSuccess": "添加成功", "PC.Pages.TeamSetting.AddMember.addSuccess": "添加成功",
"PC.Pages.TeamSetting.AddMember.all": "全部", "PC.Pages.TeamSetting.AddMember.all": "全部",
@@ -4765,7 +4785,7 @@ export const ZH_CN: SystemLangMap = {
"PC.Pages.UserManage.UserAuthModal.deselectAll": "取消全选", "PC.Pages.UserManage.UserAuthModal.deselectAll": "取消全选",
"PC.Pages.UserManage.UserAuthModal.role": "角色", "PC.Pages.UserManage.UserAuthModal.role": "角色",
"PC.Pages.UserManage.UserAuthModal.selectAll": "全选", "PC.Pages.UserManage.UserAuthModal.selectAll": "全选",
"PC.Pages.UserManage.UserAuthModal.userGroup": "用户组", "PC.Pages.UserManage.UserAuthModal.userGroup": "单位(部门)",
"PC.Pages.UserManage.UserFormModal.addUser": "新增用户", "PC.Pages.UserManage.UserFormModal.addUser": "新增用户",
"PC.Pages.UserManage.UserFormModal.admin": "管理员", "PC.Pages.UserManage.UserFormModal.admin": "管理员",
"PC.Pages.UserManage.UserFormModal.editUser": "修改用户信息", "PC.Pages.UserManage.UserFormModal.editUser": "修改用户信息",
@@ -4825,7 +4845,7 @@ export const ZH_CN: SystemLangMap = {
"PC.Routes.systemSetting": "系统设置", "PC.Routes.systemSetting": "系统设置",
"PC.Routes.taskManagement": "任务管理", "PC.Routes.taskManagement": "任务管理",
"PC.Routes.themeConfig": "主题配置", "PC.Routes.themeConfig": "主题配置",
"PC.Routes.userGroupManage": "用户组管理", "PC.Routes.userGroupManage": "单位(部门)管理",
"PC.Routes.userManagement": "用户管理", "PC.Routes.userManagement": "用户管理",
"PC.Routes.devPaymentEarnings": "支付与收益(开发者)", "PC.Routes.devPaymentEarnings": "支付与收益(开发者)",
"PC.Routes.adminSubscriptionCredits": "订阅与积分(管理员)", "PC.Routes.adminSubscriptionCredits": "订阅与积分(管理员)",
@@ -4866,8 +4886,8 @@ export const ZH_CN: SystemLangMap = {
"PC.Toast.SystemRoleFormModal.createSuccess": "角色创建成功", "PC.Toast.SystemRoleFormModal.createSuccess": "角色创建成功",
"PC.Toast.SystemRoleFormModal.editSuccess": "角色编辑成功", "PC.Toast.SystemRoleFormModal.editSuccess": "角色编辑成功",
"PC.Toast.SystemTargetAuthModal.authorized": "授权成功", "PC.Toast.SystemTargetAuthModal.authorized": "授权成功",
"PC.Toast.SystemUserGroupFormModal.createSuccess": "新增用户组成功", "PC.Toast.SystemUserGroupFormModal.createSuccess": "新增单位(部门)成功",
"PC.Toast.SystemUserGroupFormModal.updateSuccess": "更新用户组成功", "PC.Toast.SystemUserGroupFormModal.updateSuccess": "更新单位(部门)成功",
"PC.Toast.UploadAvatar.invalidSize": "图片大小不能超过 2MB", "PC.Toast.UploadAvatar.invalidSize": "图片大小不能超过 2MB",
"PC.Toast.UploadAvatar.invalidType": "请上传 JPG、JPEG 或 PNG 图片文件", "PC.Toast.UploadAvatar.invalidType": "请上传 JPG、JPEG 或 PNG 图片文件",
"PC.Utils.AntCustom.cancelText": "取消", "PC.Utils.AntCustom.cancelText": "取消",

View File

@@ -4,6 +4,7 @@ import { customizeRequiredMark } from '@/utils/form';
import { InfoCircleOutlined } from '@ant-design/icons'; import { InfoCircleOutlined } from '@ant-design/icons';
import { import {
Col, Col,
DatePicker,
Form, Form,
Input, Input,
InputNumber, InputNumber,
@@ -15,6 +16,7 @@ import {
import classNames from 'classnames'; import classNames from 'classnames';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useRequest } from 'umi'; import { useRequest } from 'umi';
import dayjs from 'dayjs';
import { import {
apiAddUserGroup, apiAddUserGroup,
apiGetUserGroupById, apiGetUserGroupById,
@@ -105,6 +107,10 @@ const UserGroupFormModal: React.FC<UserGroupFormModalProps> = ({
description: data.description, description: data.description,
sortIndex: data.sortIndex || 1, sortIndex: data.sortIndex || 1,
status: data.status === UserGroupStatusEnum.Enabled, status: data.status === UserGroupStatusEnum.Enabled,
maxUserCount: data.maxUserCount,
maxClientCount: data.maxClientCount ?? -1,
authEnabled: data.authEnabled === 1,
expireTime: data.expireTime ? dayjs(data.expireTime) : undefined,
source: data.source || UserGroupSourceEnum.UserDefined, source: data.source || UserGroupSourceEnum.UserDefined,
}); });
}, },
@@ -125,6 +131,8 @@ const UserGroupFormModal: React.FC<UserGroupFormModalProps> = ({
source: UserGroupSourceEnum.UserDefined, source: UserGroupSourceEnum.UserDefined,
sortIndex: defaultSortIndex || 1, sortIndex: defaultSortIndex || 1,
status: true, status: true,
authEnabled: false,
maxClientCount: -1,
}); });
} }
} }
@@ -140,6 +148,10 @@ const UserGroupFormModal: React.FC<UserGroupFormModalProps> = ({
status: values.status status: values.status
? UserGroupStatusEnum.Enabled ? UserGroupStatusEnum.Enabled
: UserGroupStatusEnum.Disabled, : UserGroupStatusEnum.Disabled,
authEnabled: values.authEnabled ? 1 : 0,
expireTime: values.expireTime
? values.expireTime.format('YYYY-MM-DD HH:mm:ss')
: undefined,
}; };
if (isEdit && userGroupInfo) { if (isEdit && userGroupInfo) {
@@ -261,6 +273,80 @@ const UserGroupFormModal: React.FC<UserGroupFormModalProps> = ({
</Col> </Col>
</Row> </Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label={t('PC.Pages.SystemUserGroupFormModal.maxUserCount')}
name="maxUserCount"
className={cx(styles.fieldItem)}
>
<InputNumber
placeholder={t(
'PC.Pages.SystemUserGroupFormModal.maxUserCountPlaceholder',
)}
className={cx('w-full')}
min={1}
max={999999}
precision={0}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label={t('PC.Pages.SystemUserGroupFormModal.maxClientCount')}
name="maxClientCount"
className={cx(styles.fieldItem)}
>
<InputNumber
placeholder={t(
'PC.Pages.SystemUserGroupFormModal.maxClientCountPlaceholder',
)}
className={cx('w-full')}
min={-1}
max={999999}
precision={0}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label={t('PC.Pages.SystemUserGroupFormModal.authEnabled')}
name="authEnabled"
valuePropName="checked"
tooltip={{
title: t('PC.Pages.SystemUserGroupFormModal.authEnabledTip'),
icon: <InfoCircleOutlined />,
}}
>
<Switch
checkedChildren={t(
'PC.Pages.SystemUserGroupFormModal.authorized',
)}
unCheckedChildren={t(
'PC.Pages.SystemUserGroupFormModal.unauthorized',
)}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label={t('PC.Pages.SystemUserGroupFormModal.expireTime')}
name="expireTime"
>
<DatePicker
showTime
className={cx('w-full')}
placeholder={t(
'PC.Pages.SystemUserGroupFormModal.expireTimePlaceholder',
)}
/>
</Form.Item>
</Col>
</Row>
<Form.Item <Form.Item
label={t('PC.Pages.SystemUserGroupFormModal.description')} label={t('PC.Pages.SystemUserGroupFormModal.description')}
name="description" name="description"

View File

@@ -22,8 +22,9 @@ import {
SortableContext, SortableContext,
verticalListSortingStrategy, verticalListSortingStrategy,
} from '@dnd-kit/sortable'; } from '@dnd-kit/sortable';
import { Button, Empty, message, Switch, Tooltip } from 'antd'; import { Button, Empty, message, Switch, Tag, Tooltip } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import dayjs from 'dayjs';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useLocation, useModel, useRequest } from 'umi'; import { useLocation, useModel, useRequest } from 'umi';
@@ -366,6 +367,57 @@ const UserGroupManage: React.FC = () => {
hideInSearch: true, hideInSearch: true,
ellipsis: true, ellipsis: true,
}, },
{
title: t('PC.Pages.SystemUserGroupManage.columnAuthorization'),
dataIndex: 'authEnabled',
key: 'authEnabled',
width: 110,
align: 'center',
hideInSearch: true,
render: (_: ReactNode, record: UserGroupInfo & { key: number }) => {
const authorized = record.authEnabled === 1;
return (
<Tag color={authorized ? 'success' : 'default'}>
{authorized
? t('PC.Pages.SystemUserGroupManage.authorized')
: t('PC.Pages.SystemUserGroupManage.unauthorized')}
</Tag>
);
},
},
{
title: t('PC.Pages.SystemUserGroupManage.columnMaxUsers'),
dataIndex: 'maxUserCount',
key: 'maxUserCount',
width: 120,
align: 'center',
hideInSearch: true,
render: (_: ReactNode, record: UserGroupInfo & { key: number }) =>
record.maxUserCount ?? t('PC.Pages.SystemUserGroupManage.unlimited'),
},
{
title: t('PC.Pages.SystemUserGroupManage.columnMaxClients'),
dataIndex: 'maxClientCount',
key: 'maxClientCount',
width: 120,
align: 'center',
hideInSearch: true,
render: (_: ReactNode, record: UserGroupInfo & { key: number }) =>
record.maxClientCount == null || record.maxClientCount < 0
? t('PC.Pages.SystemUserGroupManage.unlimited')
: record.maxClientCount,
},
{
title: t('PC.Pages.SystemUserGroupManage.columnExpireTime'),
dataIndex: 'expireTime',
key: 'expireTime',
width: 170,
hideInSearch: true,
render: (_: ReactNode, record: UserGroupInfo & { key: number }) =>
record.expireTime
? dayjs(record.expireTime).format('YYYY-MM-DD HH:mm')
: t('PC.Pages.SystemUserGroupManage.neverExpire'),
},
{ {
title: ( title: (
<span> <span>
@@ -416,7 +468,7 @@ const UserGroupManage: React.FC = () => {
const actions: ActionItem<UserGroupInfo & { key: number }>[] = [ const actions: ActionItem<UserGroupInfo & { key: number }>[] = [
{ {
key: 'bindUser', key: 'bindUser',
label: t('PC.Pages.SystemUserGroupManage.bindUser'), label: t('PC.Pages.SystemUserGroupManage.bindMember'),
onClick: () => handleBindUser(record), onClick: () => handleBindUser(record),
visible: hasPermissionByMenuCode( visible: hasPermissionByMenuCode(
'user_group_manage', 'user_group_manage',
@@ -425,7 +477,7 @@ const UserGroupManage: React.FC = () => {
}, },
{ {
key: 'menuPermission', key: 'menuPermission',
label: t('PC.Pages.SystemUserGroupManage.menuPermission'), label: t('PC.Pages.SystemUserGroupManage.unitPermission'),
onClick: () => handleMenuPermission(record), onClick: () => handleMenuPermission(record),
visible: hasPermissionByMenuCode( visible: hasPermissionByMenuCode(
'user_group_manage', 'user_group_manage',
@@ -517,7 +569,7 @@ const UserGroupManage: React.FC = () => {
request={request} request={request}
dataSource={draggableData} dataSource={draggableData}
pagination={false} pagination={false}
scroll={{ x: 1090 }} scroll={{ x: 1610 }}
showQueryButtons={hasPermissionByMenuCode( showQueryButtons={hasPermissionByMenuCode(
'user_group_manage', 'user_group_manage',
'user_group_manage_query', 'user_group_manage_query',

View File

@@ -38,6 +38,14 @@ export interface UserGroupInfo {
code: string; code: string;
/** 用户组描述 */ /** 用户组描述 */
description: string; description: string;
/** 最大用户数,空表示不限制 */
maxUserCount?: number;
/** 最大客户端数,-1表示不限制 */
maxClientCount?: number;
/** 授权开关1-已授权0-未授权 */
authEnabled?: number;
/** 授权到期时间,空表示不过期 */
expireTime?: string;
/** 来源 1:系统内置 2:用户自定义 */ /** 来源 1:系统内置 2:用户自定义 */
source: UserGroupSourceEnum; source: UserGroupSourceEnum;
/** 状态,1:启用 0:禁用 */ /** 状态,1:启用 0:禁用 */
@@ -75,6 +83,14 @@ export interface AddUserGroupParams {
name?: string; name?: string;
/** 描述 */ /** 描述 */
description?: string; description?: string;
/** 最大用户数,空表示不限制 */
maxUserCount?: number;
/** 最大客户端数,-1表示不限制 */
maxClientCount?: number;
/** 授权开关1-已授权0-未授权 */
authEnabled?: number;
/** 授权到期时间,空表示不过期 */
expireTime?: string;
/* 状态,1:启用 0:禁用 */ /* 状态,1:启用 0:禁用 */
status?: UserGroupStatusEnum; status?: UserGroupStatusEnum;
/** 数据模型ID列表全部模型传[0],未选中任何模型不传值 */ /** 数据模型ID列表全部模型传[0],未选中任何模型不传值 */

View File

@@ -20,7 +20,7 @@ import {
getUpdateState, getUpdateState,
openReleasesPage, openReleasesPage,
} from "../services/autoUpdater"; } from "../services/autoUpdater";
import { getDeviceId } from "../services/system/deviceId"; import { getDeviceId, getPrimaryLocalIp } from "../services/system/deviceId";
import { getTrayManager } from "../window/trayManager"; import { getTrayManager } from "../window/trayManager";
import { getAutoLaunchManager } from "../window/autoLaunchManager"; import { getAutoLaunchManager } from "../window/autoLaunchManager";
import { APP_DISPLAY_NAME } from "@shared/constants"; import { APP_DISPLAY_NAME } from "@shared/constants";
@@ -284,6 +284,10 @@ export function registerAppHandlers(ctx: HandlerContext): void {
return getDeviceId(); return getDeviceId();
}); });
ipcMain.handle("app:getPrimaryLocalIp", () => {
return getPrimaryLocalIp();
});
ipcMain.handle("app:checkUpdate", async () => { ipcMain.handle("app:checkUpdate", async () => {
try { try {
return await checkForUpdates(); return await checkForUpdates();

View File

@@ -314,7 +314,7 @@ describe("AcpEngine.prompt", () => {
{ messageID: "rid-timeout-001" }, { messageID: "rid-timeout-001" },
); );
await vi.advanceTimersByTimeAsync(60_001); await vi.advanceTimersByTimeAsync(600_001);
await expect(promptPromise).resolves.toBeDefined(); await expect(promptPromise).resolves.toBeDefined();
expect(onPromptEnd).toHaveBeenCalledTimes(1); expect(onPromptEnd).toHaveBeenCalledTimes(1);

View File

@@ -28,6 +28,18 @@ export function getDeviceId(): string {
return cachedDeviceId; return cachedDeviceId;
} }
export function getPrimaryLocalIp(): string | null {
const interfaces = os.networkInterfaces();
for (const entries of Object.values(interfaces)) {
for (const entry of entries ?? []) {
if (entry.family === "IPv4" && !entry.internal) {
return entry.address;
}
}
}
return null;
}
export function logSystemInfo(): void { export function logSystemInfo(): void {
// 固定字段尽量保持稳定输出,便于跨平台日志分析与检索。 // 固定字段尽量保持稳定输出,便于跨平台日志分析与检索。
const baseInfo = { const baseInfo = {

View File

@@ -1,5 +1,5 @@
// Main process system services // Main process system services
export { getDeviceId } from './deviceId'; export { getDeviceId, getPrimaryLocalIp } from './deviceId';
export { getAppEnv, setMirrorConfig } from './dependencies'; export { getAppEnv, setMirrorConfig } from './dependencies';
export { export {
getAppDataDir, getAppDataDir,

View File

@@ -503,6 +503,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
openReleasesPage: () => ipcRenderer.invoke("app:openReleasesPage"), openReleasesPage: () => ipcRenderer.invoke("app:openReleasesPage"),
getUpdateDebugInfo: () => ipcRenderer.invoke("app:getUpdateDebugInfo"), getUpdateDebugInfo: () => ipcRenderer.invoke("app:getUpdateDebugInfo"),
getDeviceId: () => ipcRenderer.invoke("app:getDeviceId"), getDeviceId: () => ipcRenderer.invoke("app:getDeviceId"),
getPrimaryLocalIp: () => ipcRenderer.invoke("app:getPrimaryLocalIp"),
}, },
// Permissions (macOS) // Permissions (macOS)

View File

@@ -192,6 +192,7 @@ export interface ClientRegisterParams {
password: string; password: string;
savedKey?: string; savedKey?: string;
deviceId?: string; deviceId?: string;
name?: string;
sandboxConfigValue: SandboxValue; sandboxConfigValue: SandboxValue;
} }

View File

@@ -28,6 +28,7 @@ vi.stubGlobal("window", {
electronAPI: { electronAPI: {
app: { app: {
getDeviceId: vi.fn(async () => "mock-device-id"), getDeviceId: vi.fn(async () => "mock-device-id"),
getPrimaryLocalIp: vi.fn(async () => "192.168.1.23"),
}, },
settings: { settings: {
get: mockSettingsGet, get: mockSettingsGet,
@@ -166,6 +167,7 @@ describe("auth - savedKey 认证 (快捷登录)", () => {
expect(params.username).toBe(""); expect(params.username).toBe("");
expect(params.password).toBe(""); expect(params.password).toBe("");
expect(params.savedKey).toBe(SAVED_KEY); expect(params.savedKey).toBe(SAVED_KEY);
expect(params.name).toBe("ip-192.168.1.23");
}); });
it("username/password 为 null + 有 savedKey → 应正常同步", async () => { it("username/password 为 null + 有 savedKey → 应正常同步", async () => {
@@ -313,6 +315,9 @@ describe("auth - savedKey 认证 (快捷登录)", () => {
expect(store["auth.password"]).toBeUndefined(); expect(store["auth.password"]).toBeUndefined();
// savedKey 应保存 // savedKey 应保存
expect(store["auth.saved_key"]).toBe(CONFIG_KEY_FROM_SERVER); expect(store["auth.saved_key"]).toBe(CONFIG_KEY_FROM_SERVER);
const [params] = mockRegisterClient.mock.calls[0];
expect(params.name).toBe("ip-192.168.1.23");
}); });
}); });

View File

@@ -203,6 +203,17 @@ async function getLocalSandboxValue(): Promise<SandboxValue> {
}; };
} }
async function getDefaultSandboxName(): Promise<string | undefined> {
try {
const ip = await window.electronAPI?.app.getPrimaryLocalIp?.();
const normalized = typeof ip === "string" ? ip.trim() : "";
return normalized ? `ip-${normalized}` : undefined;
} catch (error) {
logger.warn("Failed to resolve default sandbox name", "Auth", error);
return undefined;
}
}
// ========== 错误处理 === // ========== 错误处理 ===
/** /**
* 获取友好的错误信息 * 获取友好的错误信息
@@ -288,11 +299,13 @@ export async function loginAndRegister(
// 构建注册参数 // 构建注册参数
const deviceId = await window.electronAPI?.app.getDeviceId(); const deviceId = await window.electronAPI?.app.getDeviceId();
const defaultSandboxName = await getDefaultSandboxName();
const params: ClientRegisterParams = { const params: ClientRegisterParams = {
username, username,
password, password,
savedKey: savedKey || undefined, savedKey: savedKey || undefined,
deviceId: deviceId || undefined, deviceId: deviceId || undefined,
name: defaultSandboxName,
sandboxConfigValue: await getLocalSandboxValue(), sandboxConfigValue: await getLocalSandboxValue(),
}; };
@@ -471,11 +484,13 @@ export async function reRegisterClient(): Promise<ClientRegisterResponse | null>
logger.info("Re-registering client (using savedKey)...", "Auth"); logger.info("Re-registering client (using savedKey)...", "Auth");
const deviceId = await window.electronAPI?.app.getDeviceId(); const deviceId = await window.electronAPI?.app.getDeviceId();
const defaultSandboxName = await getDefaultSandboxName();
const params: ClientRegisterParams = { const params: ClientRegisterParams = {
username: username || "", username: username || "",
password: "", // 密码不持久化,使用 savedKey 认证 password: "", // 密码不持久化,使用 savedKey 认证
savedKey, savedKey,
deviceId: deviceId || undefined, deviceId: deviceId || undefined,
name: defaultSandboxName,
sandboxConfigValue: await getLocalSandboxValue(), sandboxConfigValue: await getLocalSandboxValue(),
}; };
@@ -575,11 +590,13 @@ export async function syncConfigToServer(options?: {
} }
const deviceId = await window.electronAPI?.app.getDeviceId(); const deviceId = await window.electronAPI?.app.getDeviceId();
const defaultSandboxName = await getDefaultSandboxName();
const params: ClientRegisterParams = { const params: ClientRegisterParams = {
username: username || "", username: username || "",
password: "", // 密码不持久化,使用 savedKey 认证 password: "", // 密码不持久化,使用 savedKey 认证
savedKey, savedKey,
deviceId: deviceId || undefined, deviceId: deviceId || undefined,
name: defaultSandboxName,
sandboxConfigValue: await getLocalSandboxValue(), sandboxConfigValue: await getLocalSandboxValue(),
}; };

View File

@@ -152,7 +152,7 @@ export const PROCESS_KILL_ESCALATION_TIMEOUT = 5000;
export const ACP_ABORT_TIMEOUT = 15_000; export const ACP_ABORT_TIMEOUT = 15_000;
/** ACP prompt 等待响应超时 (ms) */ /** ACP prompt 等待响应超时 (ms) */
export const ACP_PROMPT_TIMEOUT = 60_000; export const ACP_PROMPT_TIMEOUT = 600_000;
/** /**
* 用户主动取消会话时,挂在 `Error` 上的 `code`(与 `message` 语言无关)。 * 用户主动取消会话时,挂在 `Error` 上的 `code`(与 `message` 语言无关)。

View File

@@ -515,6 +515,7 @@ export interface AppAPI {
error?: string; error?: string;
}>; }>;
getDeviceId: () => Promise<string>; getDeviceId: () => Promise<string>;
getPrimaryLocalIp: () => Promise<string | null>;
} }
export type PermissionStatus = "granted" | "denied" | "unknown"; export type PermissionStatus = "granted" | "denied" | "unknown";