mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-06 10:06:36 +08:00
feat: add revocable group-based tunnel permission management
This commit is contained in:
@@ -13,6 +13,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
|||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@EnableAsync
|
@EnableAsync
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
|
@MapperScan("com.admin.mapper")
|
||||||
public class AdminApplication {
|
public class AdminApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.admin.common.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class GroupCreateDto {
|
||||||
|
|
||||||
|
@NotBlank(message = "分组名称不能为空")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private Integer status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.admin.common.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class GroupPermissionAssignDto {
|
||||||
|
|
||||||
|
@NotNull(message = "用户分组ID不能为空")
|
||||||
|
private Long userGroupId;
|
||||||
|
|
||||||
|
@NotNull(message = "隧道分组ID不能为空")
|
||||||
|
private Long tunnelGroupId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.admin.common.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class GroupPermissionDetailDto {
|
||||||
|
private Long id;
|
||||||
|
private Long userGroupId;
|
||||||
|
private String userGroupName;
|
||||||
|
private Long tunnelGroupId;
|
||||||
|
private String tunnelGroupName;
|
||||||
|
private Long createdTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.admin.common.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class GroupUpdateDto {
|
||||||
|
|
||||||
|
@NotNull(message = "分组ID不能为空")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@NotBlank(message = "分组名称不能为空")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private Integer status;
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
package com.admin.common.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class TunnelGroupAssignTunnelsDto {
|
||||||
|
|
||||||
|
@NotNull(message = "隧道分组ID不能为空")
|
||||||
|
private Long groupId;
|
||||||
|
|
||||||
|
@NotNull(message = "隧道列表不能为空")
|
||||||
|
private List<Long> tunnelIds;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.admin.common.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class TunnelGroupDetailDto {
|
||||||
|
private Long id;
|
||||||
|
private String name;
|
||||||
|
private Integer status;
|
||||||
|
private Long createdTime;
|
||||||
|
private Long updatedTime;
|
||||||
|
private List<Long> tunnelIds = new ArrayList<>();
|
||||||
|
private List<String> tunnelNames = new ArrayList<>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.admin.common.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class UserGroupAssignUsersDto {
|
||||||
|
|
||||||
|
@NotNull(message = "用户分组ID不能为空")
|
||||||
|
private Long groupId;
|
||||||
|
|
||||||
|
@NotNull(message = "用户列表不能为空")
|
||||||
|
private List<Long> userIds;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.admin.common.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class UserGroupDetailDto {
|
||||||
|
private Long id;
|
||||||
|
private String name;
|
||||||
|
private Integer status;
|
||||||
|
private Long createdTime;
|
||||||
|
private Long updatedTime;
|
||||||
|
private List<Long> userIds = new ArrayList<>();
|
||||||
|
private List<String> userNames = new ArrayList<>();
|
||||||
|
}
|
||||||
+16
@@ -32,6 +32,18 @@ public class SqliteSchemaMigration implements ApplicationRunner {
|
|||||||
public void run(ApplicationArguments args) {
|
public void run(ApplicationArguments args) {
|
||||||
ensureColumn("node", "inx", "INTEGER NOT NULL DEFAULT 0");
|
ensureColumn("node", "inx", "INTEGER NOT NULL DEFAULT 0");
|
||||||
ensureColumn("tunnel", "inx", "INTEGER NOT NULL DEFAULT 0");
|
ensureColumn("tunnel", "inx", "INTEGER NOT NULL DEFAULT 0");
|
||||||
|
ensureTable("CREATE TABLE IF NOT EXISTS tunnel_group (id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(100) NOT NULL, created_time INTEGER NOT NULL, updated_time INTEGER NOT NULL, status INTEGER NOT NULL)");
|
||||||
|
ensureTable("CREATE TABLE IF NOT EXISTS user_group (id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(100) NOT NULL, created_time INTEGER NOT NULL, updated_time INTEGER NOT NULL, status INTEGER NOT NULL)");
|
||||||
|
ensureTable("CREATE TABLE IF NOT EXISTS tunnel_group_tunnel (id INTEGER PRIMARY KEY AUTOINCREMENT, tunnel_group_id INTEGER NOT NULL, tunnel_id INTEGER NOT NULL, created_time INTEGER NOT NULL)");
|
||||||
|
ensureTable("CREATE TABLE IF NOT EXISTS user_group_user (id INTEGER PRIMARY KEY AUTOINCREMENT, user_group_id INTEGER NOT NULL, user_id INTEGER NOT NULL, created_time INTEGER NOT NULL)");
|
||||||
|
ensureTable("CREATE TABLE IF NOT EXISTS group_permission (id INTEGER PRIMARY KEY AUTOINCREMENT, user_group_id INTEGER NOT NULL, tunnel_group_id INTEGER NOT NULL, created_time INTEGER NOT NULL)");
|
||||||
|
ensureTable("CREATE TABLE IF NOT EXISTS group_permission_grant (id INTEGER PRIMARY KEY AUTOINCREMENT, user_group_id INTEGER NOT NULL, tunnel_group_id INTEGER NOT NULL, user_tunnel_id INTEGER NOT NULL, created_time INTEGER NOT NULL)");
|
||||||
|
ensureTable("CREATE UNIQUE INDEX IF NOT EXISTS idx_tunnel_group_name ON tunnel_group(name)");
|
||||||
|
ensureTable("CREATE UNIQUE INDEX IF NOT EXISTS idx_user_group_name ON user_group(name)");
|
||||||
|
ensureTable("CREATE UNIQUE INDEX IF NOT EXISTS idx_tunnel_group_tunnel_unique ON tunnel_group_tunnel(tunnel_group_id, tunnel_id)");
|
||||||
|
ensureTable("CREATE UNIQUE INDEX IF NOT EXISTS idx_user_group_user_unique ON user_group_user(user_group_id, user_id)");
|
||||||
|
ensureTable("CREATE UNIQUE INDEX IF NOT EXISTS idx_group_permission_unique ON group_permission(user_group_id, tunnel_group_id)");
|
||||||
|
ensureTable("CREATE UNIQUE INDEX IF NOT EXISTS idx_group_permission_grant_unique ON group_permission_grant(user_group_id, tunnel_group_id, user_tunnel_id)");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ensureColumn(String table, String column, String columnDefinition) {
|
private void ensureColumn(String table, String column, String columnDefinition) {
|
||||||
@@ -51,4 +63,8 @@ public class SqliteSchemaMigration implements ApplicationRunner {
|
|||||||
"ALTER TABLE " + table + " ADD COLUMN " + column + " " + columnDefinition
|
"ALTER TABLE " + table + " ADD COLUMN " + column + " " + columnDefinition
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ensureTable(String ddl) {
|
||||||
|
jdbcTemplate.execute(ddl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package com.admin.controller;
|
||||||
|
|
||||||
|
import com.admin.common.aop.LogAnnotation;
|
||||||
|
import com.admin.common.annotation.RequireRole;
|
||||||
|
import com.admin.common.dto.GroupCreateDto;
|
||||||
|
import com.admin.common.dto.GroupPermissionAssignDto;
|
||||||
|
import com.admin.common.dto.GroupUpdateDto;
|
||||||
|
import com.admin.common.dto.TunnelGroupAssignTunnelsDto;
|
||||||
|
import com.admin.common.dto.UserGroupAssignUsersDto;
|
||||||
|
import com.admin.common.lang.R;
|
||||||
|
import com.admin.service.GroupService;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@CrossOrigin
|
||||||
|
@RequestMapping("/api/v1/group")
|
||||||
|
public class GroupController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private GroupService groupService;
|
||||||
|
|
||||||
|
@LogAnnotation
|
||||||
|
@RequireRole
|
||||||
|
@PostMapping("/tunnel/list")
|
||||||
|
public R tunnelGroupList() {
|
||||||
|
return groupService.getTunnelGroups();
|
||||||
|
}
|
||||||
|
|
||||||
|
@LogAnnotation
|
||||||
|
@RequireRole
|
||||||
|
@PostMapping("/tunnel/create")
|
||||||
|
public R createTunnelGroup(@Validated @RequestBody GroupCreateDto dto) {
|
||||||
|
return groupService.createTunnelGroup(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@LogAnnotation
|
||||||
|
@RequireRole
|
||||||
|
@PostMapping("/tunnel/update")
|
||||||
|
public R updateTunnelGroup(@Validated @RequestBody GroupUpdateDto dto) {
|
||||||
|
return groupService.updateTunnelGroup(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@LogAnnotation
|
||||||
|
@RequireRole
|
||||||
|
@PostMapping("/tunnel/delete")
|
||||||
|
public R deleteTunnelGroup(@RequestBody Map<String, Object> params) {
|
||||||
|
Long id = Long.valueOf(params.get("id").toString());
|
||||||
|
return groupService.deleteTunnelGroup(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@LogAnnotation
|
||||||
|
@RequireRole
|
||||||
|
@PostMapping("/tunnel/assign")
|
||||||
|
public R assignTunnels(@Validated @RequestBody TunnelGroupAssignTunnelsDto dto) {
|
||||||
|
return groupService.assignTunnelsToGroup(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@LogAnnotation
|
||||||
|
@RequireRole
|
||||||
|
@PostMapping("/user/list")
|
||||||
|
public R userGroupList() {
|
||||||
|
return groupService.getUserGroups();
|
||||||
|
}
|
||||||
|
|
||||||
|
@LogAnnotation
|
||||||
|
@RequireRole
|
||||||
|
@PostMapping("/user/create")
|
||||||
|
public R createUserGroup(@Validated @RequestBody GroupCreateDto dto) {
|
||||||
|
return groupService.createUserGroup(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@LogAnnotation
|
||||||
|
@RequireRole
|
||||||
|
@PostMapping("/user/update")
|
||||||
|
public R updateUserGroup(@Validated @RequestBody GroupUpdateDto dto) {
|
||||||
|
return groupService.updateUserGroup(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@LogAnnotation
|
||||||
|
@RequireRole
|
||||||
|
@PostMapping("/user/delete")
|
||||||
|
public R deleteUserGroup(@RequestBody Map<String, Object> params) {
|
||||||
|
Long id = Long.valueOf(params.get("id").toString());
|
||||||
|
return groupService.deleteUserGroup(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@LogAnnotation
|
||||||
|
@RequireRole
|
||||||
|
@PostMapping("/user/assign")
|
||||||
|
public R assignUsers(@Validated @RequestBody UserGroupAssignUsersDto dto) {
|
||||||
|
return groupService.assignUsersToGroup(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@LogAnnotation
|
||||||
|
@RequireRole
|
||||||
|
@PostMapping("/permission/list")
|
||||||
|
public R listPermissions() {
|
||||||
|
return groupService.getGroupPermissions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@LogAnnotation
|
||||||
|
@RequireRole
|
||||||
|
@PostMapping("/permission/assign")
|
||||||
|
public R assignPermission(@Validated @RequestBody GroupPermissionAssignDto dto) {
|
||||||
|
return groupService.assignGroupPermission(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@LogAnnotation
|
||||||
|
@RequireRole
|
||||||
|
@PostMapping("/permission/remove")
|
||||||
|
public R removePermission(@RequestBody Map<String, Object> params) {
|
||||||
|
Long id = Long.valueOf(params.get("id").toString());
|
||||||
|
return groupService.removeGroupPermission(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.admin.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class GroupPermission implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private Long userGroupId;
|
||||||
|
|
||||||
|
private Long tunnelGroupId;
|
||||||
|
|
||||||
|
private Long createdTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.admin.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class GroupPermissionGrant implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private Long userGroupId;
|
||||||
|
|
||||||
|
private Long tunnelGroupId;
|
||||||
|
|
||||||
|
private Long userTunnelId;
|
||||||
|
|
||||||
|
private Long createdTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.admin.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class TunnelGroup extends BaseEntity {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.admin.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class TunnelGroupTunnel implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private Long tunnelGroupId;
|
||||||
|
|
||||||
|
private Long tunnelId;
|
||||||
|
|
||||||
|
private Long createdTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.admin.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class UserGroup extends BaseEntity {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.admin.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class UserGroupUser implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private Long userGroupId;
|
||||||
|
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
private Long createdTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.admin.mapper;
|
||||||
|
|
||||||
|
import com.admin.entity.GroupPermissionGrant;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
|
||||||
|
public interface GroupPermissionGrantMapper extends BaseMapper<GroupPermissionGrant> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.admin.mapper;
|
||||||
|
|
||||||
|
import com.admin.entity.GroupPermission;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
|
||||||
|
public interface GroupPermissionMapper extends BaseMapper<GroupPermission> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.admin.mapper;
|
||||||
|
|
||||||
|
import com.admin.entity.TunnelGroup;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
|
||||||
|
public interface TunnelGroupMapper extends BaseMapper<TunnelGroup> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.admin.mapper;
|
||||||
|
|
||||||
|
import com.admin.entity.TunnelGroupTunnel;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
|
||||||
|
public interface TunnelGroupTunnelMapper extends BaseMapper<TunnelGroupTunnel> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.admin.mapper;
|
||||||
|
|
||||||
|
import com.admin.entity.UserGroup;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
|
||||||
|
public interface UserGroupMapper extends BaseMapper<UserGroup> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.admin.mapper;
|
||||||
|
|
||||||
|
import com.admin.entity.UserGroupUser;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
|
||||||
|
public interface UserGroupUserMapper extends BaseMapper<UserGroupUser> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.admin.service;
|
||||||
|
|
||||||
|
import com.admin.common.dto.GroupCreateDto;
|
||||||
|
import com.admin.common.dto.GroupPermissionAssignDto;
|
||||||
|
import com.admin.common.dto.GroupUpdateDto;
|
||||||
|
import com.admin.common.dto.TunnelGroupAssignTunnelsDto;
|
||||||
|
import com.admin.common.dto.UserGroupAssignUsersDto;
|
||||||
|
import com.admin.common.lang.R;
|
||||||
|
|
||||||
|
public interface GroupService {
|
||||||
|
|
||||||
|
R getTunnelGroups();
|
||||||
|
|
||||||
|
R createTunnelGroup(GroupCreateDto dto);
|
||||||
|
|
||||||
|
R updateTunnelGroup(GroupUpdateDto dto);
|
||||||
|
|
||||||
|
R deleteTunnelGroup(Long id);
|
||||||
|
|
||||||
|
R assignTunnelsToGroup(TunnelGroupAssignTunnelsDto dto);
|
||||||
|
|
||||||
|
R getUserGroups();
|
||||||
|
|
||||||
|
R createUserGroup(GroupCreateDto dto);
|
||||||
|
|
||||||
|
R updateUserGroup(GroupUpdateDto dto);
|
||||||
|
|
||||||
|
R deleteUserGroup(Long id);
|
||||||
|
|
||||||
|
R assignUsersToGroup(UserGroupAssignUsersDto dto);
|
||||||
|
|
||||||
|
R getGroupPermissions();
|
||||||
|
|
||||||
|
R assignGroupPermission(GroupPermissionAssignDto dto);
|
||||||
|
|
||||||
|
R removeGroupPermission(Long id);
|
||||||
|
}
|
||||||
@@ -0,0 +1,587 @@
|
|||||||
|
package com.admin.service.impl;
|
||||||
|
|
||||||
|
import com.admin.common.dto.GroupCreateDto;
|
||||||
|
import com.admin.common.dto.GroupPermissionAssignDto;
|
||||||
|
import com.admin.common.dto.GroupPermissionDetailDto;
|
||||||
|
import com.admin.common.dto.GroupUpdateDto;
|
||||||
|
import com.admin.common.dto.TunnelGroupAssignTunnelsDto;
|
||||||
|
import com.admin.common.dto.TunnelGroupDetailDto;
|
||||||
|
import com.admin.common.dto.UserGroupAssignUsersDto;
|
||||||
|
import com.admin.common.dto.UserGroupDetailDto;
|
||||||
|
import com.admin.common.lang.R;
|
||||||
|
import com.admin.entity.GroupPermission;
|
||||||
|
import com.admin.entity.GroupPermissionGrant;
|
||||||
|
import com.admin.entity.Tunnel;
|
||||||
|
import com.admin.entity.TunnelGroup;
|
||||||
|
import com.admin.entity.TunnelGroupTunnel;
|
||||||
|
import com.admin.entity.User;
|
||||||
|
import com.admin.entity.UserGroup;
|
||||||
|
import com.admin.entity.UserGroupUser;
|
||||||
|
import com.admin.entity.UserTunnel;
|
||||||
|
import com.admin.mapper.GroupPermissionGrantMapper;
|
||||||
|
import com.admin.mapper.GroupPermissionMapper;
|
||||||
|
import com.admin.mapper.TunnelGroupMapper;
|
||||||
|
import com.admin.mapper.TunnelGroupTunnelMapper;
|
||||||
|
import com.admin.mapper.UserGroupMapper;
|
||||||
|
import com.admin.mapper.UserGroupUserMapper;
|
||||||
|
import com.admin.service.GroupService;
|
||||||
|
import com.admin.service.TunnelService;
|
||||||
|
import com.admin.service.UserService;
|
||||||
|
import com.admin.service.UserTunnelService;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class GroupServiceImpl implements GroupService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private TunnelGroupMapper tunnelGroupMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private UserGroupMapper userGroupMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private TunnelGroupTunnelMapper tunnelGroupTunnelMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private UserGroupUserMapper userGroupUserMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private GroupPermissionMapper groupPermissionMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private GroupPermissionGrantMapper groupPermissionGrantMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private TunnelService tunnelService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private UserTunnelService userTunnelService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public R getTunnelGroups() {
|
||||||
|
List<TunnelGroup> groups = tunnelGroupMapper.selectList(new QueryWrapper<TunnelGroup>().orderByAsc("id"));
|
||||||
|
List<TunnelGroupTunnel> mappings = tunnelGroupTunnelMapper.selectList(new QueryWrapper<TunnelGroupTunnel>());
|
||||||
|
|
||||||
|
Map<Long, List<TunnelGroupTunnel>> mappingByGroupId = mappings.stream()
|
||||||
|
.collect(Collectors.groupingBy(TunnelGroupTunnel::getTunnelGroupId));
|
||||||
|
|
||||||
|
Set<Long> tunnelIds = mappings.stream().map(TunnelGroupTunnel::getTunnelId).collect(Collectors.toSet());
|
||||||
|
Map<Long, String> tunnelNameMap = buildTunnelNameMap(tunnelIds);
|
||||||
|
|
||||||
|
List<TunnelGroupDetailDto> result = new ArrayList<>();
|
||||||
|
for (TunnelGroup group : groups) {
|
||||||
|
TunnelGroupDetailDto dto = new TunnelGroupDetailDto();
|
||||||
|
dto.setId(group.getId());
|
||||||
|
dto.setName(group.getName());
|
||||||
|
dto.setStatus(group.getStatus());
|
||||||
|
dto.setCreatedTime(group.getCreatedTime());
|
||||||
|
dto.setUpdatedTime(group.getUpdatedTime());
|
||||||
|
|
||||||
|
List<TunnelGroupTunnel> groupMappings = mappingByGroupId.getOrDefault(group.getId(), Collections.emptyList());
|
||||||
|
List<Long> ids = groupMappings.stream().map(TunnelGroupTunnel::getTunnelId).collect(Collectors.toList());
|
||||||
|
List<String> names = ids.stream().map(tunnelNameMap::get).filter(name -> name != null && !name.isBlank()).collect(Collectors.toList());
|
||||||
|
dto.setTunnelIds(ids);
|
||||||
|
dto.setTunnelNames(names);
|
||||||
|
result.add(dto);
|
||||||
|
}
|
||||||
|
return R.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public R createTunnelGroup(GroupCreateDto dto) {
|
||||||
|
String name = dto.getName().trim();
|
||||||
|
int count = tunnelGroupMapper.selectCount(new QueryWrapper<TunnelGroup>().eq("name", name));
|
||||||
|
if (count > 0) {
|
||||||
|
return R.err("隧道分组名称已存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
TunnelGroup group = new TunnelGroup();
|
||||||
|
group.setName(name);
|
||||||
|
group.setStatus(normalizeStatus(dto.getStatus()));
|
||||||
|
group.setCreatedTime(now);
|
||||||
|
group.setUpdatedTime(now);
|
||||||
|
tunnelGroupMapper.insert(group);
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public R updateTunnelGroup(GroupUpdateDto dto) {
|
||||||
|
TunnelGroup group = tunnelGroupMapper.selectById(dto.getId());
|
||||||
|
if (group == null) {
|
||||||
|
return R.err("隧道分组不存在");
|
||||||
|
}
|
||||||
|
String name = dto.getName().trim();
|
||||||
|
int count = tunnelGroupMapper.selectCount(new QueryWrapper<TunnelGroup>().eq("name", name).ne("id", dto.getId()));
|
||||||
|
if (count > 0) {
|
||||||
|
return R.err("隧道分组名称已存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
group.setName(name);
|
||||||
|
if (dto.getStatus() != null) {
|
||||||
|
group.setStatus(dto.getStatus());
|
||||||
|
}
|
||||||
|
group.setUpdatedTime(System.currentTimeMillis());
|
||||||
|
tunnelGroupMapper.updateById(group);
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public R deleteTunnelGroup(Long id) {
|
||||||
|
TunnelGroup group = tunnelGroupMapper.selectById(id);
|
||||||
|
if (group == null) {
|
||||||
|
return R.err("隧道分组不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<GroupPermissionGrant> grants = groupPermissionGrantMapper.selectList(new QueryWrapper<GroupPermissionGrant>().eq("tunnel_group_id", id));
|
||||||
|
revokeGrantRecords(grants);
|
||||||
|
tunnelGroupTunnelMapper.delete(new QueryWrapper<TunnelGroupTunnel>().eq("tunnel_group_id", id));
|
||||||
|
groupPermissionMapper.delete(new QueryWrapper<GroupPermission>().eq("tunnel_group_id", id));
|
||||||
|
tunnelGroupMapper.deleteById(id);
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public R assignTunnelsToGroup(TunnelGroupAssignTunnelsDto dto) {
|
||||||
|
TunnelGroup group = tunnelGroupMapper.selectById(dto.getGroupId());
|
||||||
|
if (group == null) {
|
||||||
|
return R.err("隧道分组不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Long> tunnelIds = new LinkedHashSet<>(dto.getTunnelIds());
|
||||||
|
if (!tunnelIds.isEmpty()) {
|
||||||
|
List<Tunnel> tunnels = tunnelService.list(new QueryWrapper<Tunnel>().in("id", tunnelIds));
|
||||||
|
if (tunnels.size() != tunnelIds.size()) {
|
||||||
|
return R.err("隧道列表中存在无效ID");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnelGroupTunnelMapper.delete(new QueryWrapper<TunnelGroupTunnel>().eq("tunnel_group_id", dto.getGroupId()));
|
||||||
|
|
||||||
|
if (!tunnelIds.isEmpty()) {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
for (Long tunnelId : tunnelIds) {
|
||||||
|
TunnelGroupTunnel relation = new TunnelGroupTunnel();
|
||||||
|
relation.setTunnelGroupId(dto.getGroupId());
|
||||||
|
relation.setTunnelId(tunnelId);
|
||||||
|
relation.setCreatedTime(now);
|
||||||
|
tunnelGroupTunnelMapper.insert(relation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
syncByTunnelGroup(dto.getGroupId());
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public R getUserGroups() {
|
||||||
|
List<UserGroup> groups = userGroupMapper.selectList(new QueryWrapper<UserGroup>().orderByAsc("id"));
|
||||||
|
List<UserGroupUser> mappings = userGroupUserMapper.selectList(new QueryWrapper<UserGroupUser>());
|
||||||
|
|
||||||
|
Map<Long, List<UserGroupUser>> mappingByGroupId = mappings.stream()
|
||||||
|
.collect(Collectors.groupingBy(UserGroupUser::getUserGroupId));
|
||||||
|
|
||||||
|
Set<Long> userIds = mappings.stream().map(UserGroupUser::getUserId).collect(Collectors.toSet());
|
||||||
|
Map<Long, String> userNameMap = buildUserNameMap(userIds);
|
||||||
|
|
||||||
|
List<UserGroupDetailDto> result = new ArrayList<>();
|
||||||
|
for (UserGroup group : groups) {
|
||||||
|
UserGroupDetailDto dto = new UserGroupDetailDto();
|
||||||
|
dto.setId(group.getId());
|
||||||
|
dto.setName(group.getName());
|
||||||
|
dto.setStatus(group.getStatus());
|
||||||
|
dto.setCreatedTime(group.getCreatedTime());
|
||||||
|
dto.setUpdatedTime(group.getUpdatedTime());
|
||||||
|
|
||||||
|
List<UserGroupUser> groupMappings = mappingByGroupId.getOrDefault(group.getId(), Collections.emptyList());
|
||||||
|
List<Long> ids = groupMappings.stream().map(UserGroupUser::getUserId).collect(Collectors.toList());
|
||||||
|
List<String> names = ids.stream().map(userNameMap::get).filter(name -> name != null && !name.isBlank()).collect(Collectors.toList());
|
||||||
|
dto.setUserIds(ids);
|
||||||
|
dto.setUserNames(names);
|
||||||
|
result.add(dto);
|
||||||
|
}
|
||||||
|
return R.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public R createUserGroup(GroupCreateDto dto) {
|
||||||
|
String name = dto.getName().trim();
|
||||||
|
int count = userGroupMapper.selectCount(new QueryWrapper<UserGroup>().eq("name", name));
|
||||||
|
if (count > 0) {
|
||||||
|
return R.err("用户分组名称已存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
UserGroup group = new UserGroup();
|
||||||
|
group.setName(name);
|
||||||
|
group.setStatus(normalizeStatus(dto.getStatus()));
|
||||||
|
group.setCreatedTime(now);
|
||||||
|
group.setUpdatedTime(now);
|
||||||
|
userGroupMapper.insert(group);
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public R updateUserGroup(GroupUpdateDto dto) {
|
||||||
|
UserGroup group = userGroupMapper.selectById(dto.getId());
|
||||||
|
if (group == null) {
|
||||||
|
return R.err("用户分组不存在");
|
||||||
|
}
|
||||||
|
String name = dto.getName().trim();
|
||||||
|
int count = userGroupMapper.selectCount(new QueryWrapper<UserGroup>().eq("name", name).ne("id", dto.getId()));
|
||||||
|
if (count > 0) {
|
||||||
|
return R.err("用户分组名称已存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
group.setName(name);
|
||||||
|
if (dto.getStatus() != null) {
|
||||||
|
group.setStatus(dto.getStatus());
|
||||||
|
}
|
||||||
|
group.setUpdatedTime(System.currentTimeMillis());
|
||||||
|
userGroupMapper.updateById(group);
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public R deleteUserGroup(Long id) {
|
||||||
|
UserGroup group = userGroupMapper.selectById(id);
|
||||||
|
if (group == null) {
|
||||||
|
return R.err("用户分组不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<GroupPermissionGrant> grants = groupPermissionGrantMapper.selectList(new QueryWrapper<GroupPermissionGrant>().eq("user_group_id", id));
|
||||||
|
revokeGrantRecords(grants);
|
||||||
|
userGroupUserMapper.delete(new QueryWrapper<UserGroupUser>().eq("user_group_id", id));
|
||||||
|
groupPermissionMapper.delete(new QueryWrapper<GroupPermission>().eq("user_group_id", id));
|
||||||
|
userGroupMapper.deleteById(id);
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public R assignUsersToGroup(UserGroupAssignUsersDto dto) {
|
||||||
|
UserGroup group = userGroupMapper.selectById(dto.getGroupId());
|
||||||
|
if (group == null) {
|
||||||
|
return R.err("用户分组不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Long> userIds = new LinkedHashSet<>(dto.getUserIds());
|
||||||
|
if (!userIds.isEmpty()) {
|
||||||
|
List<User> users = userService.list(new QueryWrapper<User>().in("id", userIds).ne("role_id", 0));
|
||||||
|
if (users.size() != userIds.size()) {
|
||||||
|
return R.err("用户列表中存在无效ID");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userGroupUserMapper.delete(new QueryWrapper<UserGroupUser>().eq("user_group_id", dto.getGroupId()));
|
||||||
|
|
||||||
|
if (!userIds.isEmpty()) {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
for (Long userId : userIds) {
|
||||||
|
UserGroupUser relation = new UserGroupUser();
|
||||||
|
relation.setUserGroupId(dto.getGroupId());
|
||||||
|
relation.setUserId(userId);
|
||||||
|
relation.setCreatedTime(now);
|
||||||
|
userGroupUserMapper.insert(relation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
syncByUserGroup(dto.getGroupId());
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public R getGroupPermissions() {
|
||||||
|
List<GroupPermission> permissions = groupPermissionMapper.selectList(new QueryWrapper<GroupPermission>().orderByDesc("id"));
|
||||||
|
if (permissions.isEmpty()) {
|
||||||
|
return R.ok(new ArrayList<GroupPermissionDetailDto>());
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Long> userGroupIds = permissions.stream().map(GroupPermission::getUserGroupId).collect(Collectors.toSet());
|
||||||
|
Set<Long> tunnelGroupIds = permissions.stream().map(GroupPermission::getTunnelGroupId).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
Map<Long, String> userGroupNameMap = userGroupMapper.selectList(new QueryWrapper<UserGroup>().in("id", userGroupIds)).stream()
|
||||||
|
.collect(Collectors.toMap(UserGroup::getId, UserGroup::getName));
|
||||||
|
Map<Long, String> tunnelGroupNameMap = tunnelGroupMapper.selectList(new QueryWrapper<TunnelGroup>().in("id", tunnelGroupIds)).stream()
|
||||||
|
.collect(Collectors.toMap(TunnelGroup::getId, TunnelGroup::getName));
|
||||||
|
|
||||||
|
List<GroupPermissionDetailDto> result = new ArrayList<>();
|
||||||
|
for (GroupPermission permission : permissions) {
|
||||||
|
GroupPermissionDetailDto dto = new GroupPermissionDetailDto();
|
||||||
|
dto.setId(permission.getId());
|
||||||
|
dto.setUserGroupId(permission.getUserGroupId());
|
||||||
|
dto.setTunnelGroupId(permission.getTunnelGroupId());
|
||||||
|
dto.setCreatedTime(permission.getCreatedTime());
|
||||||
|
dto.setUserGroupName(userGroupNameMap.get(permission.getUserGroupId()));
|
||||||
|
dto.setTunnelGroupName(tunnelGroupNameMap.get(permission.getTunnelGroupId()));
|
||||||
|
result.add(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
return R.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public R assignGroupPermission(GroupPermissionAssignDto dto) {
|
||||||
|
UserGroup userGroup = userGroupMapper.selectById(dto.getUserGroupId());
|
||||||
|
if (userGroup == null) {
|
||||||
|
return R.err("用户分组不存在");
|
||||||
|
}
|
||||||
|
TunnelGroup tunnelGroup = tunnelGroupMapper.selectById(dto.getTunnelGroupId());
|
||||||
|
if (tunnelGroup == null) {
|
||||||
|
return R.err("隧道分组不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
int existing = groupPermissionMapper.selectCount(new QueryWrapper<GroupPermission>()
|
||||||
|
.eq("user_group_id", dto.getUserGroupId())
|
||||||
|
.eq("tunnel_group_id", dto.getTunnelGroupId()));
|
||||||
|
if (existing == 0) {
|
||||||
|
GroupPermission permission = new GroupPermission();
|
||||||
|
permission.setUserGroupId(dto.getUserGroupId());
|
||||||
|
permission.setTunnelGroupId(dto.getTunnelGroupId());
|
||||||
|
permission.setCreatedTime(System.currentTimeMillis());
|
||||||
|
groupPermissionMapper.insert(permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
reconcilePermission(dto.getUserGroupId(), dto.getTunnelGroupId());
|
||||||
|
return existing > 0 ? R.ok("权限已存在,已完成同步") : R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public R removeGroupPermission(Long id) {
|
||||||
|
GroupPermission permission = groupPermissionMapper.selectById(id);
|
||||||
|
if (permission == null) {
|
||||||
|
return R.err("权限记录不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
revokeByPermissionPair(permission.getUserGroupId(), permission.getTunnelGroupId());
|
||||||
|
groupPermissionMapper.deleteById(id);
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncByUserGroup(Long userGroupId) {
|
||||||
|
List<GroupPermission> permissions = groupPermissionMapper.selectList(new QueryWrapper<GroupPermission>().eq("user_group_id", userGroupId));
|
||||||
|
for (GroupPermission permission : permissions) {
|
||||||
|
reconcilePermission(permission.getUserGroupId(), permission.getTunnelGroupId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncByTunnelGroup(Long tunnelGroupId) {
|
||||||
|
List<GroupPermission> permissions = groupPermissionMapper.selectList(new QueryWrapper<GroupPermission>().eq("tunnel_group_id", tunnelGroupId));
|
||||||
|
for (GroupPermission permission : permissions) {
|
||||||
|
reconcilePermission(permission.getUserGroupId(), permission.getTunnelGroupId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reconcilePermission(Long userGroupId, Long tunnelGroupId) {
|
||||||
|
Set<Long> userIds = userGroupUserMapper.selectList(new QueryWrapper<UserGroupUser>().eq("user_group_id", userGroupId)).stream()
|
||||||
|
.map(UserGroupUser::getUserId)
|
||||||
|
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||||
|
|
||||||
|
Set<Long> tunnelIds = tunnelGroupTunnelMapper.selectList(new QueryWrapper<TunnelGroupTunnel>().eq("tunnel_group_id", tunnelGroupId)).stream()
|
||||||
|
.map(TunnelGroupTunnel::getTunnelId)
|
||||||
|
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||||
|
|
||||||
|
Set<String> desiredKeys = new HashSet<>();
|
||||||
|
for (Long userId : userIds) {
|
||||||
|
for (Long tunnelId : tunnelIds) {
|
||||||
|
desiredKeys.add(permissionKey(userId, tunnelId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<GroupPermissionGrant> currentGrants = groupPermissionGrantMapper.selectList(
|
||||||
|
new QueryWrapper<GroupPermissionGrant>()
|
||||||
|
.eq("user_group_id", userGroupId)
|
||||||
|
.eq("tunnel_group_id", tunnelGroupId)
|
||||||
|
);
|
||||||
|
|
||||||
|
Set<Long> grantUserTunnelIds = currentGrants.stream().map(GroupPermissionGrant::getUserTunnelId).collect(Collectors.toSet());
|
||||||
|
Map<Long, UserTunnel> grantUserTunnelMap = new HashMap<>();
|
||||||
|
if (!grantUserTunnelIds.isEmpty()) {
|
||||||
|
List<UserTunnel> userTunnels = userTunnelService.list(new QueryWrapper<UserTunnel>().in("id", grantUserTunnelIds));
|
||||||
|
grantUserTunnelMap = userTunnels.stream().collect(Collectors.toMap(ut -> ut.getId().longValue(), Function.identity()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, UserTunnel> pairUserTunnelMap = new HashMap<>();
|
||||||
|
if (!userIds.isEmpty() && !tunnelIds.isEmpty()) {
|
||||||
|
List<UserTunnel> pairUserTunnels = userTunnelService.list(new QueryWrapper<UserTunnel>()
|
||||||
|
.in("user_id", userIds)
|
||||||
|
.in("tunnel_id", tunnelIds));
|
||||||
|
for (UserTunnel userTunnel : pairUserTunnels) {
|
||||||
|
pairUserTunnelMap.putIfAbsent(permissionKey(userTunnel.getUserId().longValue(), userTunnel.getTunnelId().longValue()), userTunnel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Long> pairUserTunnelIds = pairUserTunnelMap.values().stream()
|
||||||
|
.map(ut -> ut.getId().longValue())
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
Map<Long, Long> totalGrantCountMap = buildGrantCountMap(pairUserTunnelIds);
|
||||||
|
|
||||||
|
Set<Long> currentGrantUserTunnelIds = currentGrants.stream().map(GroupPermissionGrant::getUserTunnelId).collect(Collectors.toSet());
|
||||||
|
if (!desiredKeys.isEmpty()) {
|
||||||
|
Map<Long, User> userMap = userService.list(new QueryWrapper<User>().in("id", userIds)).stream()
|
||||||
|
.collect(Collectors.toMap(User::getId, Function.identity()));
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
for (Long userId : userIds) {
|
||||||
|
User user = userMap.get(userId);
|
||||||
|
if (user == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (Long tunnelId : tunnelIds) {
|
||||||
|
String pairKey = permissionKey(userId, tunnelId);
|
||||||
|
UserTunnel userTunnel = pairUserTunnelMap.get(pairKey);
|
||||||
|
if (userTunnel == null) {
|
||||||
|
userTunnel = createGroupManagedUserTunnel(userId, tunnelId, user);
|
||||||
|
pairUserTunnelMap.put(pairKey, userTunnel);
|
||||||
|
createGrant(userGroupId, tunnelGroupId, userTunnel.getId().longValue(), now);
|
||||||
|
currentGrantUserTunnelIds.add(userTunnel.getId().longValue());
|
||||||
|
totalGrantCountMap.put(userTunnel.getId().longValue(), 1L);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Long userTunnelId = userTunnel.getId().longValue();
|
||||||
|
if (currentGrantUserTunnelIds.contains(userTunnelId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
long existingGrantCount = totalGrantCountMap.getOrDefault(userTunnelId, 0L);
|
||||||
|
if (existingGrantCount > 0L) {
|
||||||
|
createGrant(userGroupId, tunnelGroupId, userTunnelId, now);
|
||||||
|
currentGrantUserTunnelIds.add(userTunnelId);
|
||||||
|
totalGrantCountMap.put(userTunnelId, existingGrantCount + 1L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<GroupPermissionGrant> staleGrants = new ArrayList<>();
|
||||||
|
for (GroupPermissionGrant grant : currentGrants) {
|
||||||
|
UserTunnel userTunnel = grantUserTunnelMap.get(grant.getUserTunnelId());
|
||||||
|
boolean keep = false;
|
||||||
|
if (userTunnel != null) {
|
||||||
|
String key = permissionKey(userTunnel.getUserId().longValue(), userTunnel.getTunnelId().longValue());
|
||||||
|
keep = desiredKeys.contains(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!keep) {
|
||||||
|
staleGrants.add(grant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
revokeGrantRecords(staleGrants);
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserTunnel createGroupManagedUserTunnel(Long userId, Long tunnelId, User user) {
|
||||||
|
UserTunnel userTunnel = new UserTunnel();
|
||||||
|
userTunnel.setUserId(userId.intValue());
|
||||||
|
userTunnel.setTunnelId(tunnelId.intValue());
|
||||||
|
userTunnel.setStatus(1);
|
||||||
|
userTunnel.setInFlow(0L);
|
||||||
|
userTunnel.setOutFlow(0L);
|
||||||
|
userTunnel.setFlow(user.getFlow());
|
||||||
|
userTunnel.setNum(user.getNum());
|
||||||
|
userTunnel.setFlowResetTime(user.getFlowResetTime());
|
||||||
|
userTunnel.setExpTime(user.getExpTime());
|
||||||
|
userTunnelService.save(userTunnel);
|
||||||
|
return userTunnel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createGrant(Long userGroupId, Long tunnelGroupId, Long userTunnelId, long createdTime) {
|
||||||
|
int exists = groupPermissionGrantMapper.selectCount(new QueryWrapper<GroupPermissionGrant>()
|
||||||
|
.eq("user_group_id", userGroupId)
|
||||||
|
.eq("tunnel_group_id", tunnelGroupId)
|
||||||
|
.eq("user_tunnel_id", userTunnelId));
|
||||||
|
if (exists > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupPermissionGrant grant = new GroupPermissionGrant();
|
||||||
|
grant.setUserGroupId(userGroupId);
|
||||||
|
grant.setTunnelGroupId(tunnelGroupId);
|
||||||
|
grant.setUserTunnelId(userTunnelId);
|
||||||
|
grant.setCreatedTime(createdTime);
|
||||||
|
groupPermissionGrantMapper.insert(grant);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void revokeByPermissionPair(Long userGroupId, Long tunnelGroupId) {
|
||||||
|
List<GroupPermissionGrant> grants = groupPermissionGrantMapper.selectList(new QueryWrapper<GroupPermissionGrant>()
|
||||||
|
.eq("user_group_id", userGroupId)
|
||||||
|
.eq("tunnel_group_id", tunnelGroupId));
|
||||||
|
revokeGrantRecords(grants);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void revokeGrantRecords(List<GroupPermissionGrant> grants) {
|
||||||
|
if (grants == null || grants.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Long> candidateUserTunnelIds = new HashSet<>();
|
||||||
|
for (GroupPermissionGrant grant : grants) {
|
||||||
|
candidateUserTunnelIds.add(grant.getUserTunnelId());
|
||||||
|
groupPermissionGrantMapper.deleteById(grant.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Long> stillGrantedUserTunnelIds = groupPermissionGrantMapper.selectList(
|
||||||
|
new QueryWrapper<GroupPermissionGrant>().in("user_tunnel_id", candidateUserTunnelIds)
|
||||||
|
).stream()
|
||||||
|
.map(GroupPermissionGrant::getUserTunnelId)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
for (Long userTunnelId : candidateUserTunnelIds) {
|
||||||
|
if (!stillGrantedUserTunnelIds.contains(userTunnelId)) {
|
||||||
|
userTunnelService.removeUserTunnel(userTunnelId.intValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Long, Long> buildGrantCountMap(Set<Long> userTunnelIds) {
|
||||||
|
if (userTunnelIds.isEmpty()) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<GroupPermissionGrant> grants = groupPermissionGrantMapper.selectList(new QueryWrapper<GroupPermissionGrant>().in("user_tunnel_id", userTunnelIds));
|
||||||
|
Map<Long, Long> countMap = new HashMap<>();
|
||||||
|
for (GroupPermissionGrant grant : grants) {
|
||||||
|
countMap.merge(grant.getUserTunnelId(), 1L, Long::sum);
|
||||||
|
}
|
||||||
|
return countMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Long, String> buildTunnelNameMap(Set<Long> tunnelIds) {
|
||||||
|
if (tunnelIds.isEmpty()) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
return tunnelService.list(new QueryWrapper<Tunnel>().in("id", tunnelIds)).stream()
|
||||||
|
.collect(Collectors.toMap(Tunnel::getId, Tunnel::getName));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Long, String> buildUserNameMap(Set<Long> userIds) {
|
||||||
|
if (userIds.isEmpty()) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
return userService.list(new QueryWrapper<User>().in("id", userIds)).stream()
|
||||||
|
.collect(Collectors.toMap(User::getId, User::getUser));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String permissionKey(Long userId, Long tunnelId) {
|
||||||
|
return userId + "_" + tunnelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int normalizeStatus(Integer status) {
|
||||||
|
return status == null ? 1 : status;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ import com.admin.common.utils.JwtUtil;
|
|||||||
import com.admin.common.utils.WebSocketServer;
|
import com.admin.common.utils.WebSocketServer;
|
||||||
import com.admin.entity.*;
|
import com.admin.entity.*;
|
||||||
import com.admin.mapper.TunnelMapper;
|
import com.admin.mapper.TunnelMapper;
|
||||||
|
import com.admin.mapper.GroupPermissionGrantMapper;
|
||||||
|
import com.admin.mapper.TunnelGroupTunnelMapper;
|
||||||
import com.admin.mapper.UserTunnelMapper;
|
import com.admin.mapper.UserTunnelMapper;
|
||||||
import com.admin.service.*;
|
import com.admin.service.*;
|
||||||
import com.alibaba.fastjson.JSONArray;
|
import com.alibaba.fastjson.JSONArray;
|
||||||
@@ -52,6 +54,12 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
|||||||
@Resource
|
@Resource
|
||||||
ForwardPortService forwardPortService;
|
ForwardPortService forwardPortService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
TunnelGroupTunnelMapper tunnelGroupTunnelMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
GroupPermissionGrantMapper groupPermissionGrantMapper;
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public R createTunnel(TunnelDto tunnelDto) {
|
public R createTunnel(TunnelDto tunnelDto) {
|
||||||
@@ -578,8 +586,16 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
|||||||
for (Forward forward : forwardList) {
|
for (Forward forward : forwardList) {
|
||||||
forwardService.deleteForward(forward.getId());
|
forwardService.deleteForward(forward.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<UserTunnel> userTunnels = userTunnelService.list(new QueryWrapper<UserTunnel>().eq("tunnel_id", id));
|
||||||
|
if (!userTunnels.isEmpty()) {
|
||||||
|
List<Integer> userTunnelIds = userTunnels.stream().map(UserTunnel::getId).toList();
|
||||||
|
groupPermissionGrantMapper.delete(new QueryWrapper<com.admin.entity.GroupPermissionGrant>().in("user_tunnel_id", userTunnelIds));
|
||||||
|
}
|
||||||
|
|
||||||
forwardService.remove(new QueryWrapper<Forward>().eq("tunnel_id", id));
|
forwardService.remove(new QueryWrapper<Forward>().eq("tunnel_id", id));
|
||||||
userTunnelService.remove(new QueryWrapper<UserTunnel>().eq("tunnel_id", id));
|
userTunnelService.remove(new QueryWrapper<UserTunnel>().eq("tunnel_id", id));
|
||||||
|
tunnelGroupTunnelMapper.delete(new QueryWrapper<TunnelGroupTunnel>().eq("tunnel_id", id));
|
||||||
this.removeById(id);
|
this.removeById(id);
|
||||||
|
|
||||||
List<ChainTunnel> chainTunnels = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", id));
|
List<ChainTunnel> chainTunnels = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", id));
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import com.admin.common.utils.JwtUtil;
|
|||||||
import com.admin.common.utils.Md5Util;
|
import com.admin.common.utils.Md5Util;
|
||||||
import com.admin.entity.*;
|
import com.admin.entity.*;
|
||||||
import com.admin.mapper.UserMapper;
|
import com.admin.mapper.UserMapper;
|
||||||
|
import com.admin.mapper.GroupPermissionGrantMapper;
|
||||||
|
import com.admin.mapper.UserGroupUserMapper;
|
||||||
import com.admin.service.*;
|
import com.admin.service.*;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
@@ -56,6 +58,12 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
|||||||
@Resource
|
@Resource
|
||||||
StatisticsFlowService statisticsFlowService;
|
StatisticsFlowService statisticsFlowService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
GroupPermissionGrantMapper groupPermissionGrantMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
UserGroupUserMapper userGroupUserMapper;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
@Lazy
|
@Lazy
|
||||||
ForwardPortService forwardPortService;
|
ForwardPortService forwardPortService;
|
||||||
@@ -140,8 +148,16 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
|||||||
for (Forward forward : forwardList) {
|
for (Forward forward : forwardList) {
|
||||||
forwardService.deleteForward(forward.getId());
|
forwardService.deleteForward(forward.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<UserTunnel> userTunnels = userTunnelService.list(new QueryWrapper<UserTunnel>().eq("user_id", id));
|
||||||
|
if (!userTunnels.isEmpty()) {
|
||||||
|
List<Integer> userTunnelIds = userTunnels.stream().map(UserTunnel::getId).toList();
|
||||||
|
groupPermissionGrantMapper.delete(new QueryWrapper<com.admin.entity.GroupPermissionGrant>().in("user_tunnel_id", userTunnelIds));
|
||||||
|
}
|
||||||
|
|
||||||
forwardService.remove(new QueryWrapper<Forward>().eq("user_id", id));
|
forwardService.remove(new QueryWrapper<Forward>().eq("user_id", id));
|
||||||
userTunnelService.remove(new QueryWrapper<UserTunnel>().eq("user_id", id));
|
userTunnelService.remove(new QueryWrapper<UserTunnel>().eq("user_id", id));
|
||||||
|
userGroupUserMapper.delete(new QueryWrapper<UserGroupUser>().eq("user_id", id));
|
||||||
statisticsFlowService.remove(new QueryWrapper<StatisticsFlow>().eq("user_id", id));
|
statisticsFlowService.remove(new QueryWrapper<StatisticsFlow>().eq("user_id", id));
|
||||||
this.removeById(id);
|
this.removeById(id);
|
||||||
return R.ok();
|
return R.ok();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.admin.common.dto.*;
|
|||||||
import com.admin.common.lang.R;
|
import com.admin.common.lang.R;
|
||||||
import com.admin.entity.User;
|
import com.admin.entity.User;
|
||||||
import com.admin.entity.UserTunnel;
|
import com.admin.entity.UserTunnel;
|
||||||
|
import com.admin.mapper.GroupPermissionGrantMapper;
|
||||||
import com.admin.mapper.UserTunnelMapper;
|
import com.admin.mapper.UserTunnelMapper;
|
||||||
import com.admin.service.UserService;
|
import com.admin.service.UserService;
|
||||||
import com.admin.service.UserTunnelService;
|
import com.admin.service.UserTunnelService;
|
||||||
@@ -34,6 +35,9 @@ public class UserTunnelServiceImpl extends ServiceImpl<UserTunnelMapper, UserTun
|
|||||||
@Lazy
|
@Lazy
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private GroupPermissionGrantMapper groupPermissionGrantMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public R assignUserTunnel(UserTunnelDto userTunnelDto) {
|
public R assignUserTunnel(UserTunnelDto userTunnelDto) {
|
||||||
int count = this.count(new QueryWrapper<UserTunnel>()
|
int count = this.count(new QueryWrapper<UserTunnel>()
|
||||||
@@ -129,6 +133,7 @@ public class UserTunnelServiceImpl extends ServiceImpl<UserTunnelMapper, UserTun
|
|||||||
for (Forward forward : forwardList) {
|
for (Forward forward : forwardList) {
|
||||||
forwardService.deleteForward(forward.getId());
|
forwardService.deleteForward(forward.getId());
|
||||||
}
|
}
|
||||||
|
groupPermissionGrantMapper.delete(new QueryWrapper<com.admin.entity.GroupPermissionGrant>().eq("user_tunnel_id", id));
|
||||||
this.removeById(id);
|
this.removeById(id);
|
||||||
return R.ok();
|
return R.ok();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,6 +121,58 @@ CREATE TABLE IF NOT EXISTS user_tunnel (
|
|||||||
status INTEGER NOT NULL
|
status INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tunnel_group (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
created_time INTEGER NOT NULL,
|
||||||
|
updated_time INTEGER NOT NULL,
|
||||||
|
status INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_group (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
created_time INTEGER NOT NULL,
|
||||||
|
updated_time INTEGER NOT NULL,
|
||||||
|
status INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tunnel_group_tunnel (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
tunnel_group_id INTEGER NOT NULL,
|
||||||
|
tunnel_id INTEGER NOT NULL,
|
||||||
|
created_time INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_group_user (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_group_id INTEGER NOT NULL,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
created_time INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS group_permission (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_group_id INTEGER NOT NULL,
|
||||||
|
tunnel_group_id INTEGER NOT NULL,
|
||||||
|
created_time INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS group_permission_grant (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_group_id INTEGER NOT NULL,
|
||||||
|
tunnel_group_id INTEGER NOT NULL,
|
||||||
|
user_tunnel_id INTEGER NOT NULL,
|
||||||
|
created_time INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_tunnel_group_name ON tunnel_group(name);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_group_name ON user_group(name);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_tunnel_group_tunnel_unique ON tunnel_group_tunnel(tunnel_group_id, tunnel_id);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_group_user_unique ON user_group_user(user_group_id, user_id);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_group_permission_unique ON group_permission(user_group_id, tunnel_group_id);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_group_permission_grant_unique ON group_permission_grant(user_group_id, tunnel_group_id, user_tunnel_id);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS vite_config (
|
CREATE TABLE IF NOT EXISTS vite_config (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name VARCHAR(200) NOT NULL UNIQUE,
|
name VARCHAR(200) NOT NULL UNIQUE,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import ForwardPage from "@/pages/forward";
|
|||||||
import TunnelPage from "@/pages/tunnel";
|
import TunnelPage from "@/pages/tunnel";
|
||||||
import NodePage from "@/pages/node";
|
import NodePage from "@/pages/node";
|
||||||
import UserPage from "@/pages/user";
|
import UserPage from "@/pages/user";
|
||||||
|
import GroupPage from "@/pages/group";
|
||||||
import ProfilePage from "@/pages/profile";
|
import ProfilePage from "@/pages/profile";
|
||||||
import LimitPage from "@/pages/limit";
|
import LimitPage from "@/pages/limit";
|
||||||
import ConfigPage from "@/pages/config";
|
import ConfigPage from "@/pages/config";
|
||||||
@@ -208,6 +209,14 @@ function App() {
|
|||||||
}
|
}
|
||||||
path="/user"
|
path="/user"
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
element={
|
||||||
|
<ProtectedRoute useSimpleLayout={true}>
|
||||||
|
<GroupPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
path="/group"
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
|
|||||||
@@ -147,3 +147,43 @@ export const batchChangeTunnel = (data: {
|
|||||||
forwardIds: number[];
|
forwardIds: number[];
|
||||||
targetTunnelId: number;
|
targetTunnelId: number;
|
||||||
}) => Network.post("/forward/batch-change-tunnel", data);
|
}) => Network.post("/forward/batch-change-tunnel", data);
|
||||||
|
|
||||||
|
// 分组与权限分配接口
|
||||||
|
export const getTunnelGroupList = () => Network.post("/group/tunnel/list");
|
||||||
|
export const createTunnelGroup = (data: { name: string; status?: number }) =>
|
||||||
|
Network.post("/group/tunnel/create", data);
|
||||||
|
export const updateTunnelGroup = (data: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
status?: number;
|
||||||
|
}) => Network.post("/group/tunnel/update", data);
|
||||||
|
export const deleteTunnelGroup = (id: number) =>
|
||||||
|
Network.post("/group/tunnel/delete", { id });
|
||||||
|
export const assignTunnelsToGroup = (data: {
|
||||||
|
groupId: number;
|
||||||
|
tunnelIds: number[];
|
||||||
|
}) => Network.post("/group/tunnel/assign", data);
|
||||||
|
|
||||||
|
export const getUserGroupList = () => Network.post("/group/user/list");
|
||||||
|
export const createUserGroup = (data: { name: string; status?: number }) =>
|
||||||
|
Network.post("/group/user/create", data);
|
||||||
|
export const updateUserGroup = (data: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
status?: number;
|
||||||
|
}) => Network.post("/group/user/update", data);
|
||||||
|
export const deleteUserGroup = (id: number) =>
|
||||||
|
Network.post("/group/user/delete", { id });
|
||||||
|
export const assignUsersToGroup = (data: {
|
||||||
|
groupId: number;
|
||||||
|
userIds: number[];
|
||||||
|
}) => Network.post("/group/user/assign", data);
|
||||||
|
|
||||||
|
export const getGroupPermissionList = () =>
|
||||||
|
Network.post("/group/permission/list");
|
||||||
|
export const assignGroupPermission = (data: {
|
||||||
|
userGroupId: number;
|
||||||
|
tunnelGroupId: number;
|
||||||
|
}) => Network.post("/group/permission/assign", data);
|
||||||
|
export const removeGroupPermission = (id: number) =>
|
||||||
|
Network.post("/group/permission/remove", { id });
|
||||||
|
|||||||
@@ -134,6 +134,16 @@ export default function AdminLayout({
|
|||||||
),
|
),
|
||||||
adminOnly: true,
|
adminOnly: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/group",
|
||||||
|
label: "分组",
|
||||||
|
icon: (
|
||||||
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path d="M10 2a3 3 0 100 6 3 3 0 000-6zM4 9a3 3 0 100 6 3 3 0 000-6zm12 0a3 3 0 100 6 3 3 0 000-6M4 16a2 2 0 00-2 2h4a2 2 0 00-2-2zm12 0a2 2 0 00-2 2h4a2 2 0 00-2-2zm-6 0a2 2 0 00-2 2h4a2 2 0 00-2-2z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
adminOnly: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/config",
|
path: "/config",
|
||||||
label: "设置",
|
label: "设置",
|
||||||
|
|||||||
@@ -0,0 +1,858 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { Card, CardBody, CardHeader } from "@heroui/card";
|
||||||
|
import { Button } from "@heroui/button";
|
||||||
|
import { Input } from "@heroui/input";
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
useDisclosure,
|
||||||
|
} from "@heroui/modal";
|
||||||
|
import { Select, SelectItem } from "@heroui/select";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableColumn,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@heroui/table";
|
||||||
|
import { Chip } from "@heroui/chip";
|
||||||
|
import { Spinner } from "@heroui/spinner";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
|
import {
|
||||||
|
assignGroupPermission,
|
||||||
|
assignTunnelsToGroup,
|
||||||
|
assignUsersToGroup,
|
||||||
|
createTunnelGroup,
|
||||||
|
createUserGroup,
|
||||||
|
deleteTunnelGroup,
|
||||||
|
deleteUserGroup,
|
||||||
|
getAllUsers,
|
||||||
|
getGroupPermissionList,
|
||||||
|
getTunnelGroupList,
|
||||||
|
getTunnelList,
|
||||||
|
getUserGroupList,
|
||||||
|
removeGroupPermission,
|
||||||
|
updateTunnelGroup,
|
||||||
|
updateUserGroup,
|
||||||
|
} from "@/api";
|
||||||
|
|
||||||
|
interface TunnelItem {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserItem {
|
||||||
|
id: number;
|
||||||
|
user: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TunnelGroup {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
status: number;
|
||||||
|
tunnelIds: number[];
|
||||||
|
tunnelNames: string[];
|
||||||
|
createdTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserGroup {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
status: number;
|
||||||
|
userIds: number[];
|
||||||
|
userNames: string[];
|
||||||
|
createdTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GroupPermission {
|
||||||
|
id: number;
|
||||||
|
userGroupId: number;
|
||||||
|
userGroupName: string;
|
||||||
|
tunnelGroupId: number;
|
||||||
|
tunnelGroupName: string;
|
||||||
|
createdTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (timestamp?: number): string => {
|
||||||
|
if (!timestamp) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date(timestamp).toLocaleString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const isAdminUser = () => {
|
||||||
|
let adminFlag = localStorage.getItem("admin") === "true";
|
||||||
|
|
||||||
|
if (localStorage.getItem("admin") === null) {
|
||||||
|
const roleId = parseInt(localStorage.getItem("role_id") || "1", 10);
|
||||||
|
|
||||||
|
adminFlag = roleId === 0;
|
||||||
|
localStorage.setItem("admin", adminFlag.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return adminFlag;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function GroupPage() {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [isAdmin] = useState(isAdminUser());
|
||||||
|
|
||||||
|
const [tunnelGroups, setTunnelGroups] = useState<TunnelGroup[]>([]);
|
||||||
|
const [userGroups, setUserGroups] = useState<UserGroup[]>([]);
|
||||||
|
const [permissions, setPermissions] = useState<GroupPermission[]>([]);
|
||||||
|
const [tunnels, setTunnels] = useState<TunnelItem[]>([]);
|
||||||
|
const [users, setUsers] = useState<UserItem[]>([]);
|
||||||
|
|
||||||
|
const [selectedUserGroupId, setSelectedUserGroupId] = useState<number | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [selectedTunnelGroupId, setSelectedTunnelGroupId] = useState<
|
||||||
|
number | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
|
const [savingPermission, setSavingPermission] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
isOpen: tunnelGroupModalOpen,
|
||||||
|
onOpen: onTunnelGroupModalOpen,
|
||||||
|
onClose: onTunnelGroupModalClose,
|
||||||
|
onOpenChange: onTunnelGroupModalChange,
|
||||||
|
} = useDisclosure();
|
||||||
|
const {
|
||||||
|
isOpen: userGroupModalOpen,
|
||||||
|
onOpen: onUserGroupModalOpen,
|
||||||
|
onClose: onUserGroupModalClose,
|
||||||
|
onOpenChange: onUserGroupModalChange,
|
||||||
|
} = useDisclosure();
|
||||||
|
const {
|
||||||
|
isOpen: tunnelAssignModalOpen,
|
||||||
|
onOpen: onTunnelAssignModalOpen,
|
||||||
|
onClose: onTunnelAssignModalClose,
|
||||||
|
onOpenChange: onTunnelAssignModalChange,
|
||||||
|
} = useDisclosure();
|
||||||
|
const {
|
||||||
|
isOpen: userAssignModalOpen,
|
||||||
|
onOpen: onUserAssignModalOpen,
|
||||||
|
onClose: onUserAssignModalClose,
|
||||||
|
onOpenChange: onUserAssignModalChange,
|
||||||
|
} = useDisclosure();
|
||||||
|
|
||||||
|
const [editingTunnelGroup, setEditingTunnelGroup] =
|
||||||
|
useState<TunnelGroup | null>(null);
|
||||||
|
const [editingUserGroup, setEditingUserGroup] = useState<UserGroup | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [groupName, setGroupName] = useState("");
|
||||||
|
const [groupStatus, setGroupStatus] = useState("1");
|
||||||
|
const [savingGroup, setSavingGroup] = useState(false);
|
||||||
|
|
||||||
|
const [assignTunnelGroup, setAssignTunnelGroup] =
|
||||||
|
useState<TunnelGroup | null>(null);
|
||||||
|
const [assignUserGroup, setAssignUserGroup] = useState<UserGroup | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [selectedTunnelKeys, setSelectedTunnelKeys] = useState<Set<string>>(
|
||||||
|
new Set(),
|
||||||
|
);
|
||||||
|
const [selectedUserKeys, setSelectedUserKeys] = useState<Set<string>>(
|
||||||
|
new Set(),
|
||||||
|
);
|
||||||
|
const [savingAssign, setSavingAssign] = useState(false);
|
||||||
|
|
||||||
|
const tunnelNameMap = useMemo(() => {
|
||||||
|
const map = new Map<number, string>();
|
||||||
|
|
||||||
|
tunnels.forEach((item) => {
|
||||||
|
map.set(item.id, item.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}, [tunnels]);
|
||||||
|
|
||||||
|
const userNameMap = useMemo(() => {
|
||||||
|
const map = new Map<number, string>();
|
||||||
|
|
||||||
|
users.forEach((item) => {
|
||||||
|
map.set(item.id, item.user);
|
||||||
|
});
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}, [users]);
|
||||||
|
|
||||||
|
const loadData = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const [tunnelGroupRes, userGroupRes, permissionRes, tunnelRes, userRes] =
|
||||||
|
await Promise.all([
|
||||||
|
getTunnelGroupList(),
|
||||||
|
getUserGroupList(),
|
||||||
|
getGroupPermissionList(),
|
||||||
|
getTunnelList(),
|
||||||
|
getAllUsers(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (tunnelGroupRes.code === 0) {
|
||||||
|
setTunnelGroups(tunnelGroupRes.data || []);
|
||||||
|
}
|
||||||
|
if (userGroupRes.code === 0) {
|
||||||
|
setUserGroups(userGroupRes.data || []);
|
||||||
|
}
|
||||||
|
if (permissionRes.code === 0) {
|
||||||
|
setPermissions(permissionRes.data || []);
|
||||||
|
}
|
||||||
|
if (tunnelRes.code === 0) {
|
||||||
|
setTunnels(tunnelRes.data || []);
|
||||||
|
}
|
||||||
|
if (userRes.code === 0) {
|
||||||
|
setUsers(userRes.data || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
tunnelGroupRes.code !== 0 ||
|
||||||
|
userGroupRes.code !== 0 ||
|
||||||
|
permissionRes.code !== 0
|
||||||
|
) {
|
||||||
|
toast.error("部分分组数据加载失败");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error("分组数据加载失败");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
}, [loadData]);
|
||||||
|
|
||||||
|
const openCreateTunnelGroup = () => {
|
||||||
|
setEditingTunnelGroup(null);
|
||||||
|
setGroupName("");
|
||||||
|
setGroupStatus("1");
|
||||||
|
onTunnelGroupModalOpen();
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEditTunnelGroup = (group: TunnelGroup) => {
|
||||||
|
setEditingTunnelGroup(group);
|
||||||
|
setGroupName(group.name);
|
||||||
|
setGroupStatus(String(group.status));
|
||||||
|
onTunnelGroupModalOpen();
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCreateUserGroup = () => {
|
||||||
|
setEditingUserGroup(null);
|
||||||
|
setGroupName("");
|
||||||
|
setGroupStatus("1");
|
||||||
|
onUserGroupModalOpen();
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEditUserGroup = (group: UserGroup) => {
|
||||||
|
setEditingUserGroup(group);
|
||||||
|
setGroupName(group.name);
|
||||||
|
setGroupStatus(String(group.status));
|
||||||
|
onUserGroupModalOpen();
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveTunnelGroup = async () => {
|
||||||
|
if (!groupName.trim()) {
|
||||||
|
toast.error("请输入分组名称");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSavingGroup(true);
|
||||||
|
try {
|
||||||
|
const payload = { name: groupName.trim(), status: Number(groupStatus) };
|
||||||
|
const res = editingTunnelGroup
|
||||||
|
? await updateTunnelGroup({ id: editingTunnelGroup.id, ...payload })
|
||||||
|
: await createTunnelGroup(payload);
|
||||||
|
|
||||||
|
if (res.code === 0) {
|
||||||
|
toast.success(editingTunnelGroup ? "更新成功" : "创建成功");
|
||||||
|
onTunnelGroupModalClose();
|
||||||
|
loadData();
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg || "保存失败");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error("保存失败");
|
||||||
|
} finally {
|
||||||
|
setSavingGroup(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveUserGroup = async () => {
|
||||||
|
if (!groupName.trim()) {
|
||||||
|
toast.error("请输入分组名称");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSavingGroup(true);
|
||||||
|
try {
|
||||||
|
const payload = { name: groupName.trim(), status: Number(groupStatus) };
|
||||||
|
const res = editingUserGroup
|
||||||
|
? await updateUserGroup({ id: editingUserGroup.id, ...payload })
|
||||||
|
: await createUserGroup(payload);
|
||||||
|
|
||||||
|
if (res.code === 0) {
|
||||||
|
toast.success(editingUserGroup ? "更新成功" : "创建成功");
|
||||||
|
onUserGroupModalClose();
|
||||||
|
loadData();
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg || "保存失败");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error("保存失败");
|
||||||
|
} finally {
|
||||||
|
setSavingGroup(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteTunnelGroup = async (id: number) => {
|
||||||
|
try {
|
||||||
|
const res = await deleteTunnelGroup(id);
|
||||||
|
|
||||||
|
if (res.code === 0) {
|
||||||
|
toast.success("删除成功");
|
||||||
|
loadData();
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg || "删除失败");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error("删除失败");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteUserGroup = async (id: number) => {
|
||||||
|
try {
|
||||||
|
const res = await deleteUserGroup(id);
|
||||||
|
|
||||||
|
if (res.code === 0) {
|
||||||
|
toast.success("删除成功");
|
||||||
|
loadData();
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg || "删除失败");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error("删除失败");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openAssignTunnels = (group: TunnelGroup) => {
|
||||||
|
setAssignTunnelGroup(group);
|
||||||
|
setSelectedTunnelKeys(new Set(group.tunnelIds.map((id) => String(id))));
|
||||||
|
onTunnelAssignModalOpen();
|
||||||
|
};
|
||||||
|
|
||||||
|
const openAssignUsers = (group: UserGroup) => {
|
||||||
|
setAssignUserGroup(group);
|
||||||
|
setSelectedUserKeys(new Set(group.userIds.map((id) => String(id))));
|
||||||
|
onUserAssignModalOpen();
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveAssignTunnels = async () => {
|
||||||
|
if (!assignTunnelGroup) return;
|
||||||
|
setSavingAssign(true);
|
||||||
|
try {
|
||||||
|
const tunnelIds = Array.from(selectedTunnelKeys).map((id) => Number(id));
|
||||||
|
const res = await assignTunnelsToGroup({
|
||||||
|
groupId: assignTunnelGroup.id,
|
||||||
|
tunnelIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.code === 0) {
|
||||||
|
toast.success("分配成功");
|
||||||
|
onTunnelAssignModalClose();
|
||||||
|
loadData();
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg || "分配失败");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error("分配失败");
|
||||||
|
} finally {
|
||||||
|
setSavingAssign(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveAssignUsers = async () => {
|
||||||
|
if (!assignUserGroup) return;
|
||||||
|
setSavingAssign(true);
|
||||||
|
try {
|
||||||
|
const userIds = Array.from(selectedUserKeys).map((id) => Number(id));
|
||||||
|
const res = await assignUsersToGroup({
|
||||||
|
groupId: assignUserGroup.id,
|
||||||
|
userIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.code === 0) {
|
||||||
|
toast.success("分配成功");
|
||||||
|
onUserAssignModalClose();
|
||||||
|
loadData();
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg || "分配失败");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error("分配失败");
|
||||||
|
} finally {
|
||||||
|
setSavingAssign(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAssignPermission = async () => {
|
||||||
|
if (!selectedUserGroupId || !selectedTunnelGroupId) {
|
||||||
|
toast.error("请选择用户分组和隧道分组");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSavingPermission(true);
|
||||||
|
try {
|
||||||
|
const res = await assignGroupPermission({
|
||||||
|
userGroupId: selectedUserGroupId,
|
||||||
|
tunnelGroupId: selectedTunnelGroupId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.code === 0) {
|
||||||
|
toast.success(res.msg || "权限分配成功");
|
||||||
|
loadData();
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg || "权限分配失败");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error("权限分配失败");
|
||||||
|
} finally {
|
||||||
|
setSavingPermission(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemovePermission = async (id: number) => {
|
||||||
|
try {
|
||||||
|
const res = await removeGroupPermission(id);
|
||||||
|
|
||||||
|
if (res.code === 0) {
|
||||||
|
toast.success("权限回收成功");
|
||||||
|
loadData();
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg || "权限回收失败");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error("权限回收失败");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
return (
|
||||||
|
<div className="px-3 lg:px-6 py-8">
|
||||||
|
<Card>
|
||||||
|
<CardBody>
|
||||||
|
<p className="text-danger">
|
||||||
|
权限不足,只有管理员可以访问分组管理页面。
|
||||||
|
</p>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="px-3 lg:px-6 py-8 space-y-6">
|
||||||
|
{loading && (
|
||||||
|
<div className="flex justify-center py-10">
|
||||||
|
<Spinner size="lg" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-semibold">隧道分组</h3>
|
||||||
|
<Button color="primary" size="sm" onPress={openCreateTunnelGroup}>
|
||||||
|
新建隧道分组
|
||||||
|
</Button>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody>
|
||||||
|
<Table aria-label="隧道分组列表">
|
||||||
|
<TableHeader>
|
||||||
|
<TableColumn>名称</TableColumn>
|
||||||
|
<TableColumn>隧道</TableColumn>
|
||||||
|
<TableColumn>状态</TableColumn>
|
||||||
|
<TableColumn>创建时间</TableColumn>
|
||||||
|
<TableColumn>操作</TableColumn>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody emptyContent="暂无隧道分组" items={tunnelGroups}>
|
||||||
|
{(item) => (
|
||||||
|
<TableRow key={item.id}>
|
||||||
|
<TableCell>{item.name}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{item.tunnelNames.length > 0
|
||||||
|
? item.tunnelNames.join("、")
|
||||||
|
: "-"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Chip
|
||||||
|
color={item.status === 1 ? "success" : "danger"}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{item.status === 1 ? "启用" : "停用"}
|
||||||
|
</Chip>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{formatDate(item.createdTime)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
onPress={() => openAssignTunnels(item)}
|
||||||
|
>
|
||||||
|
分配隧道
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="light"
|
||||||
|
onPress={() => openEditTunnelGroup(item)}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="danger"
|
||||||
|
size="sm"
|
||||||
|
variant="light"
|
||||||
|
onPress={() => handleDeleteTunnelGroup(item.id)}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-semibold">用户分组</h3>
|
||||||
|
<Button color="primary" size="sm" onPress={openCreateUserGroup}>
|
||||||
|
新建用户分组
|
||||||
|
</Button>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody>
|
||||||
|
<Table aria-label="用户分组列表">
|
||||||
|
<TableHeader>
|
||||||
|
<TableColumn>名称</TableColumn>
|
||||||
|
<TableColumn>用户</TableColumn>
|
||||||
|
<TableColumn>状态</TableColumn>
|
||||||
|
<TableColumn>创建时间</TableColumn>
|
||||||
|
<TableColumn>操作</TableColumn>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody emptyContent="暂无用户分组" items={userGroups}>
|
||||||
|
{(item) => (
|
||||||
|
<TableRow key={item.id}>
|
||||||
|
<TableCell>{item.name}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{item.userNames.length > 0
|
||||||
|
? item.userNames.join("、")
|
||||||
|
: "-"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Chip
|
||||||
|
color={item.status === 1 ? "success" : "danger"}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{item.status === 1 ? "启用" : "停用"}
|
||||||
|
</Chip>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{formatDate(item.createdTime)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
onPress={() => openAssignUsers(item)}
|
||||||
|
>
|
||||||
|
分配用户
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="light"
|
||||||
|
onPress={() => openEditUserGroup(item)}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="danger"
|
||||||
|
size="sm"
|
||||||
|
variant="light"
|
||||||
|
onPress={() => handleDeleteUserGroup(item.id)}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<h3 className="text-lg font-semibold">权限分配</h3>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||||
|
<Select
|
||||||
|
items={userGroups}
|
||||||
|
label="用户分组"
|
||||||
|
selectedKeys={
|
||||||
|
selectedUserGroupId ? [String(selectedUserGroupId)] : []
|
||||||
|
}
|
||||||
|
onSelectionChange={(keys) => {
|
||||||
|
const key = Array.from(keys as Set<React.Key>)[0];
|
||||||
|
|
||||||
|
setSelectedUserGroupId(key ? Number(key) : null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(item) => <SelectItem key={item.id}>{item.name}</SelectItem>}
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
items={tunnelGroups}
|
||||||
|
label="隧道分组"
|
||||||
|
selectedKeys={
|
||||||
|
selectedTunnelGroupId ? [String(selectedTunnelGroupId)] : []
|
||||||
|
}
|
||||||
|
onSelectionChange={(keys) => {
|
||||||
|
const key = Array.from(keys as Set<React.Key>)[0];
|
||||||
|
|
||||||
|
setSelectedTunnelGroupId(key ? Number(key) : null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(item) => <SelectItem key={item.id}>{item.name}</SelectItem>}
|
||||||
|
</Select>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
isLoading={savingPermission}
|
||||||
|
onPress={handleAssignPermission}
|
||||||
|
>
|
||||||
|
分配权限
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table aria-label="分组权限列表">
|
||||||
|
<TableHeader>
|
||||||
|
<TableColumn>ID</TableColumn>
|
||||||
|
<TableColumn>用户分组</TableColumn>
|
||||||
|
<TableColumn>隧道分组</TableColumn>
|
||||||
|
<TableColumn>创建时间</TableColumn>
|
||||||
|
<TableColumn>操作</TableColumn>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody emptyContent="暂无权限分配记录" items={permissions}>
|
||||||
|
{(item) => (
|
||||||
|
<TableRow key={item.id}>
|
||||||
|
<TableCell>{item.id}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{item.userGroupName || item.userGroupId}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{item.tunnelGroupName || item.tunnelGroupId}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{formatDate(item.createdTime)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
color="danger"
|
||||||
|
size="sm"
|
||||||
|
variant="light"
|
||||||
|
onPress={() => handleRemovePermission(item.id)}
|
||||||
|
>
|
||||||
|
回收
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
isOpen={tunnelGroupModalOpen}
|
||||||
|
onOpenChange={onTunnelGroupModalChange}
|
||||||
|
>
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>
|
||||||
|
{editingTunnelGroup ? "编辑隧道分组" : "新建隧道分组"}
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody className="space-y-3">
|
||||||
|
<Input
|
||||||
|
label="分组名称"
|
||||||
|
value={groupName}
|
||||||
|
onChange={(e) => setGroupName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="状态"
|
||||||
|
selectedKeys={[groupStatus]}
|
||||||
|
onSelectionChange={(keys) => {
|
||||||
|
const key = Array.from(keys as Set<React.Key>)[0];
|
||||||
|
|
||||||
|
if (key) {
|
||||||
|
setGroupStatus(String(key));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectItem key="1">启用</SelectItem>
|
||||||
|
<SelectItem key="0">停用</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button variant="light" onPress={onTunnelGroupModalClose}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
isLoading={savingGroup}
|
||||||
|
onPress={saveTunnelGroup}
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal isOpen={userGroupModalOpen} onOpenChange={onUserGroupModalChange}>
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>
|
||||||
|
{editingUserGroup ? "编辑用户分组" : "新建用户分组"}
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody className="space-y-3">
|
||||||
|
<Input
|
||||||
|
label="分组名称"
|
||||||
|
value={groupName}
|
||||||
|
onChange={(e) => setGroupName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="状态"
|
||||||
|
selectedKeys={[groupStatus]}
|
||||||
|
onSelectionChange={(keys) => {
|
||||||
|
const key = Array.from(keys as Set<React.Key>)[0];
|
||||||
|
|
||||||
|
if (key) {
|
||||||
|
setGroupStatus(String(key));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectItem key="1">启用</SelectItem>
|
||||||
|
<SelectItem key="0">停用</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button variant="light" onPress={onUserGroupModalClose}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
isLoading={savingGroup}
|
||||||
|
onPress={saveUserGroup}
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
isOpen={tunnelAssignModalOpen}
|
||||||
|
onOpenChange={onTunnelAssignModalChange}
|
||||||
|
>
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>分配隧道 - {assignTunnelGroup?.name}</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<Select
|
||||||
|
items={tunnels}
|
||||||
|
label="选择隧道"
|
||||||
|
selectedKeys={selectedTunnelKeys}
|
||||||
|
selectionMode="multiple"
|
||||||
|
onSelectionChange={(keys) => {
|
||||||
|
setSelectedTunnelKeys(
|
||||||
|
new Set(Array.from(keys as Set<React.Key>).map(String)),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(item) => <SelectItem key={item.id}>{item.name}</SelectItem>}
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-default-500">
|
||||||
|
当前已选:
|
||||||
|
{Array.from(selectedTunnelKeys)
|
||||||
|
.map((id) => tunnelNameMap.get(Number(id)) || id)
|
||||||
|
.join("、") || "无"}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-default-500">
|
||||||
|
不选择任何隧道并保存将清空该分组成员。
|
||||||
|
</p>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button variant="light" onPress={onTunnelAssignModalClose}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
isLoading={savingAssign}
|
||||||
|
onPress={saveAssignTunnels}
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
isOpen={userAssignModalOpen}
|
||||||
|
onOpenChange={onUserAssignModalChange}
|
||||||
|
>
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>分配用户 - {assignUserGroup?.name}</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<Select
|
||||||
|
items={users}
|
||||||
|
label="选择用户"
|
||||||
|
selectedKeys={selectedUserKeys}
|
||||||
|
selectionMode="multiple"
|
||||||
|
onSelectionChange={(keys) => {
|
||||||
|
setSelectedUserKeys(
|
||||||
|
new Set(Array.from(keys as Set<React.Key>).map(String)),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(item) => <SelectItem key={item.id}>{item.user}</SelectItem>}
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-default-500">
|
||||||
|
当前已选:
|
||||||
|
{Array.from(selectedUserKeys)
|
||||||
|
.map((id) => userNameMap.get(Number(id)) || id)
|
||||||
|
.join("、") || "无"}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-default-500">
|
||||||
|
不选择任何用户并保存将清空该分组成员。
|
||||||
|
</p>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button variant="light" onPress={onUserAssignModalClose}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
isLoading={savingAssign}
|
||||||
|
onPress={saveAssignUsers}
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user