mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 07:36:38 +08:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 507893dc55 | |||
| 652b44e08e | |||
| 95b3803745 | |||
| 0b85cd2af1 | |||
| 4d0dcf5db5 | |||
| 4c756e9156 | |||
| f5a40bf530 | |||
| c7dbbef0d9 | |||
| a6773fe65d | |||
| 27a32b3ff4 | |||
| 06a45a87f6 | |||
| 053aef42c0 | |||
| 9f79efd44b | |||
| 08ba876291 | |||
| 59af67a5b5 | |||
| 65f8f7506e | |||
| efd5a107b9 |
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: '20.19.0'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install --legacy-peer-deps
|
||||
|
||||
+11
-3
@@ -1,8 +1,12 @@
|
||||
services:
|
||||
backend:
|
||||
image: bqlpfy/springboot-backend:2.0.7-beta
|
||||
image: ghcr.io/sagit-chu/springboot-backend:${FLUX_VERSION:-latest}
|
||||
container_name: springboot-backend
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "20m"
|
||||
environment:
|
||||
DB_PATH: /app/data/gost.db
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
@@ -25,9 +29,13 @@ services:
|
||||
start_period: 60s
|
||||
|
||||
frontend:
|
||||
image: bqlpfy/vite-frontend:2.0.7-beta
|
||||
image: ghcr.io/sagit-chu/vite-frontend:${FLUX_VERSION:-latest}
|
||||
container_name: vite-frontend
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "20m"
|
||||
ports:
|
||||
- "${FRONTEND_PORT}:80"
|
||||
depends_on:
|
||||
@@ -52,4 +60,4 @@ networks:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.20.0.0/16
|
||||
- subnet: 172.20.0.0/16
|
||||
|
||||
+11
-3
@@ -1,8 +1,12 @@
|
||||
services:
|
||||
backend:
|
||||
image: bqlpfy/springboot-backend:2.0.7-beta
|
||||
image: ghcr.io/sagit-chu/springboot-backend:${FLUX_VERSION:-latest}
|
||||
container_name: springboot-backend
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "20m"
|
||||
environment:
|
||||
DB_PATH: /app/data/gost.db
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
@@ -25,9 +29,13 @@ services:
|
||||
start_period: 60s
|
||||
|
||||
frontend:
|
||||
image: bqlpfy/vite-frontend:2.0.7-beta
|
||||
image: ghcr.io/sagit-chu/vite-frontend:${FLUX_VERSION:-latest}
|
||||
container_name: vite-frontend
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "20m"
|
||||
ports:
|
||||
- "${FRONTEND_PORT}:80"
|
||||
depends_on:
|
||||
@@ -54,4 +62,4 @@ networks:
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.20.0.0/16
|
||||
- subnet: fd00:dead:beef::/48
|
||||
- subnet: fd00:dead:beef::/48
|
||||
|
||||
+4
-2
@@ -295,6 +295,8 @@ After=network.target
|
||||
WorkingDirectory=$INSTALL_DIR
|
||||
ExecStart=$INSTALL_DIR/flux_agent
|
||||
Restart=on-failure
|
||||
StandardOutput=null
|
||||
StandardError=null
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -312,8 +314,8 @@ EOF
|
||||
echo "📁 配置目录: $INSTALL_DIR"
|
||||
echo "🔧 服务状态: $(systemctl is-active flux_agent)"
|
||||
else
|
||||
echo "❌ flux_agent服务启动失败,请执行以下命令查看日志:"
|
||||
echo "journalctl -u flux_agent -f"
|
||||
echo "❌ flux_agent服务启动失败,请执行以下命令查看状态:"
|
||||
echo "systemctl status flux_agent --no-pager"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
+39
-3
@@ -78,11 +78,16 @@ resolve_version() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# 根据版本号设置 compose 下载地址
|
||||
set_compose_urls_by_version() {
|
||||
local version="$1"
|
||||
DOCKER_COMPOSEV4_URL=$(maybe_proxy_url "https://github.com/${REPO}/releases/download/${version}/docker-compose-v4.yml")
|
||||
DOCKER_COMPOSEV6_URL=$(maybe_proxy_url "https://github.com/${REPO}/releases/download/${version}/docker-compose-v6.yml")
|
||||
}
|
||||
|
||||
# 全局下载地址配置(默认获取最新版本;也可用 VERSION=... 覆盖)
|
||||
RESOLVED_VERSION=$(resolve_version) || exit 1
|
||||
|
||||
DOCKER_COMPOSEV4_URL=$(maybe_proxy_url "https://github.com/${REPO}/releases/download/${RESOLVED_VERSION}/docker-compose-v4.yml")
|
||||
DOCKER_COMPOSEV6_URL=$(maybe_proxy_url "https://github.com/${REPO}/releases/download/${RESOLVED_VERSION}/docker-compose-v6.yml")
|
||||
set_compose_urls_by_version "$RESOLVED_VERSION"
|
||||
|
||||
|
||||
|
||||
@@ -221,6 +226,27 @@ generate_random() {
|
||||
LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c16
|
||||
}
|
||||
|
||||
upsert_env_var() {
|
||||
local file="$1"
|
||||
local key="$2"
|
||||
local value="$3"
|
||||
local tmp_file
|
||||
|
||||
tmp_file=$(mktemp)
|
||||
if [ -f "$file" ]; then
|
||||
awk -v k="$key" -v v="$value" '
|
||||
BEGIN { found=0 }
|
||||
$0 ~ ("^" k "=") { print k "=" v; found=1; next }
|
||||
{ print }
|
||||
END { if (!found) print k "=" v }
|
||||
' "$file" > "$tmp_file"
|
||||
else
|
||||
printf '%s=%s\n' "$key" "$value" > "$tmp_file"
|
||||
fi
|
||||
|
||||
mv "$tmp_file" "$file"
|
||||
}
|
||||
|
||||
# 删除脚本自身
|
||||
delete_self() {
|
||||
echo ""
|
||||
@@ -268,6 +294,7 @@ install_panel() {
|
||||
JWT_SECRET=$JWT_SECRET
|
||||
FRONTEND_PORT=$FRONTEND_PORT
|
||||
BACKEND_PORT=$BACKEND_PORT
|
||||
FLUX_VERSION=$RESOLVED_VERSION
|
||||
EOF
|
||||
|
||||
echo "🚀 启动 docker 服务..."
|
||||
@@ -288,6 +315,15 @@ update_panel() {
|
||||
echo "🔄 开始更新面板..."
|
||||
check_docker
|
||||
|
||||
echo "🔍 获取最新版本号..."
|
||||
LATEST_VERSION=$(resolve_latest_release_tag) || {
|
||||
echo "❌ 无法获取最新版本号,更新终止"
|
||||
return 1
|
||||
}
|
||||
echo "🆕 最新版本:$LATEST_VERSION"
|
||||
set_compose_urls_by_version "$LATEST_VERSION"
|
||||
upsert_env_var ".env" "FLUX_VERSION" "$LATEST_VERSION"
|
||||
|
||||
echo "🔽 下载最新配置文件..."
|
||||
DOCKER_COMPOSE_URL=$(get_docker_compose_url)
|
||||
echo "📡 选择配置文件:$(basename "$DOCKER_COMPOSE_URL")"
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
@SpringBootApplication
|
||||
@EnableAsync
|
||||
@EnableScheduling
|
||||
@MapperScan("com.admin.mapper")
|
||||
public class AdminApplication {
|
||||
|
||||
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<>();
|
||||
}
|
||||
+17
@@ -32,6 +32,19 @@ public class SqliteSchemaMigration implements ApplicationRunner {
|
||||
public void run(ApplicationArguments args) {
|
||||
ensureColumn("node", "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_by_group INTEGER NOT NULL DEFAULT 0, created_time INTEGER NOT NULL)");
|
||||
ensureColumn("group_permission_grant", "created_by_group", "INTEGER NOT NULL DEFAULT 0");
|
||||
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) {
|
||||
@@ -51,4 +64,8 @@ public class SqliteSchemaMigration implements ApplicationRunner {
|
||||
"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,26 @@
|
||||
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 Integer createdByGroup;
|
||||
|
||||
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,633 @@
|
||||
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 org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
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
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
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
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
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
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
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> groupManagedUserTunnelIds = buildGroupManagedUserTunnelIds(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);
|
||||
Long userTunnelId = resolveUserTunnelId(userTunnel, userId, tunnelId);
|
||||
pairUserTunnelMap.put(pairKey, userTunnel);
|
||||
createGrant(userGroupId, tunnelGroupId, userTunnelId, true, now);
|
||||
currentGrantUserTunnelIds.add(userTunnelId);
|
||||
totalGrantCountMap.put(userTunnelId, 1L);
|
||||
groupManagedUserTunnelIds.add(userTunnelId);
|
||||
continue;
|
||||
}
|
||||
|
||||
Long userTunnelId = userTunnel.getId().longValue();
|
||||
if (currentGrantUserTunnelIds.contains(userTunnelId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
long existingGrantCount = totalGrantCountMap.getOrDefault(userTunnelId, 0L);
|
||||
boolean createdByGroup = groupManagedUserTunnelIds.contains(userTunnelId);
|
||||
createGrant(userGroupId, tunnelGroupId, userTunnelId, createdByGroup, 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());
|
||||
boolean saved = userTunnelService.save(userTunnel);
|
||||
if (!saved) {
|
||||
throw new IllegalStateException("创建用户隧道权限失败: userId=" + userId + ", tunnelId=" + tunnelId);
|
||||
}
|
||||
return userTunnel;
|
||||
}
|
||||
|
||||
private Long resolveUserTunnelId(UserTunnel userTunnel, Long userId, Long tunnelId) {
|
||||
if (userTunnel.getId() != null) {
|
||||
return userTunnel.getId().longValue();
|
||||
}
|
||||
|
||||
UserTunnel persisted = userTunnelService.getOne(
|
||||
new QueryWrapper<UserTunnel>()
|
||||
.eq("user_id", userId.intValue())
|
||||
.eq("tunnel_id", tunnelId.intValue())
|
||||
.orderByDesc("id")
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
if (persisted == null || persisted.getId() == null) {
|
||||
throw new IllegalStateException("获取用户隧道权限ID失败: userId=" + userId + ", tunnelId=" + tunnelId);
|
||||
}
|
||||
userTunnel.setId(persisted.getId());
|
||||
return persisted.getId().longValue();
|
||||
}
|
||||
|
||||
private void createGrant(Long userGroupId, Long tunnelGroupId, Long userTunnelId, boolean createdByGroup, 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.setCreatedByGroup(createdByGroup ? 1 : 0);
|
||||
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<>();
|
||||
Set<Long> groupManagedCandidates = new HashSet<>();
|
||||
for (GroupPermissionGrant grant : grants) {
|
||||
candidateUserTunnelIds.add(grant.getUserTunnelId());
|
||||
if (grant.getCreatedByGroup() != null && grant.getCreatedByGroup() == 1) {
|
||||
groupManagedCandidates.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) && groupManagedCandidates.contains(userTunnelId)) {
|
||||
userTunnelService.removeUserTunnel(userTunnelId.intValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Set<Long> buildGroupManagedUserTunnelIds(Set<Long> userTunnelIds) {
|
||||
if (userTunnelIds.isEmpty()) {
|
||||
return new HashSet<>();
|
||||
}
|
||||
|
||||
return groupPermissionGrantMapper.selectList(new QueryWrapper<GroupPermissionGrant>()
|
||||
.in("user_tunnel_id", userTunnelIds)
|
||||
.eq("created_by_group", 1))
|
||||
.stream()
|
||||
.map(GroupPermissionGrant::getUserTunnelId)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private Map<Long, Long> buildGrantCountMap(Set<Long> userTunnelIds) {
|
||||
if (userTunnelIds.isEmpty()) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
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.entity.*;
|
||||
import com.admin.mapper.TunnelMapper;
|
||||
import com.admin.mapper.GroupPermissionGrantMapper;
|
||||
import com.admin.mapper.TunnelGroupTunnelMapper;
|
||||
import com.admin.mapper.UserTunnelMapper;
|
||||
import com.admin.service.*;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
@@ -52,6 +54,12 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
||||
@Resource
|
||||
ForwardPortService forwardPortService;
|
||||
|
||||
@Resource
|
||||
TunnelGroupTunnelMapper tunnelGroupTunnelMapper;
|
||||
|
||||
@Resource
|
||||
GroupPermissionGrantMapper groupPermissionGrantMapper;
|
||||
|
||||
|
||||
@Override
|
||||
public R createTunnel(TunnelDto tunnelDto) {
|
||||
@@ -578,8 +586,16 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
||||
for (Forward forward : forwardList) {
|
||||
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));
|
||||
userTunnelService.remove(new QueryWrapper<UserTunnel>().eq("tunnel_id", id));
|
||||
tunnelGroupTunnelMapper.delete(new QueryWrapper<TunnelGroupTunnel>().eq("tunnel_id", id));
|
||||
this.removeById(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.entity.*;
|
||||
import com.admin.mapper.UserMapper;
|
||||
import com.admin.mapper.GroupPermissionGrantMapper;
|
||||
import com.admin.mapper.UserGroupUserMapper;
|
||||
import com.admin.service.*;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
@@ -56,6 +58,12 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
@Resource
|
||||
StatisticsFlowService statisticsFlowService;
|
||||
|
||||
@Resource
|
||||
GroupPermissionGrantMapper groupPermissionGrantMapper;
|
||||
|
||||
@Resource
|
||||
UserGroupUserMapper userGroupUserMapper;
|
||||
|
||||
@Resource
|
||||
@Lazy
|
||||
ForwardPortService forwardPortService;
|
||||
@@ -140,8 +148,16 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
for (Forward forward : forwardList) {
|
||||
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));
|
||||
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));
|
||||
this.removeById(id);
|
||||
return R.ok();
|
||||
|
||||
@@ -2,8 +2,10 @@ package com.admin.service.impl;
|
||||
|
||||
import com.admin.common.dto.*;
|
||||
import com.admin.common.lang.R;
|
||||
import com.admin.entity.GroupPermissionGrant;
|
||||
import com.admin.entity.User;
|
||||
import com.admin.entity.UserTunnel;
|
||||
import com.admin.mapper.GroupPermissionGrantMapper;
|
||||
import com.admin.mapper.UserTunnelMapper;
|
||||
import com.admin.service.UserService;
|
||||
import com.admin.service.UserTunnelService;
|
||||
@@ -34,6 +36,9 @@ public class UserTunnelServiceImpl extends ServiceImpl<UserTunnelMapper, UserTun
|
||||
@Lazy
|
||||
private UserService userService;
|
||||
|
||||
@Resource
|
||||
private GroupPermissionGrantMapper groupPermissionGrantMapper;
|
||||
|
||||
@Override
|
||||
public R assignUserTunnel(UserTunnelDto userTunnelDto) {
|
||||
int count = this.count(new QueryWrapper<UserTunnel>()
|
||||
@@ -123,6 +128,13 @@ public class UserTunnelServiceImpl extends ServiceImpl<UserTunnelMapper, UserTun
|
||||
UserTunnel userTunnel = this.getById(id);
|
||||
if (userTunnel == null) return R.err("未找到对应的用户隧道权限记录");
|
||||
|
||||
int grantCount = groupPermissionGrantMapper.selectCount(
|
||||
new QueryWrapper<GroupPermissionGrant>().eq("user_tunnel_id", id)
|
||||
);
|
||||
if (grantCount > 0) {
|
||||
return R.err("该隧道权限由分组授权,请先调整分组权限或分组成员");
|
||||
}
|
||||
|
||||
List<Forward> forwardList = forwardService.list(new QueryWrapper<Forward>()
|
||||
.eq("user_id", userTunnel.getUserId())
|
||||
.eq("tunnel_id", userTunnel.getTunnelId()));
|
||||
|
||||
@@ -121,6 +121,59 @@ CREATE TABLE IF NOT EXISTS user_tunnel (
|
||||
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_by_group INTEGER NOT NULL DEFAULT 0,
|
||||
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 (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(200) NOT NULL UNIQUE,
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
"@react-aria/visually-hidden": "3.8.25",
|
||||
"@react-types/shared": "3.30.0",
|
||||
"@tailwindcss/postcss": "4.1.11",
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/react-beautiful-dnd": "^13.1.8",
|
||||
"axios": "^1.11.0",
|
||||
"clsx": "2.1.1",
|
||||
@@ -70,7 +70,7 @@
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.31.1",
|
||||
"@typescript-eslint/parser": "8.31.1",
|
||||
"@vitejs/plugin-react": "4.4.1",
|
||||
"@vitejs/plugin-react": "^5.1.3",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
@@ -84,7 +84,7 @@
|
||||
"postcss": "8.5.6",
|
||||
"prettier": "3.5.3",
|
||||
"typescript": "5.6.3",
|
||||
"vite": "5.4.21",
|
||||
"vite-tsconfig-paths": "4.3.2"
|
||||
"vite": "npm:rolldown-vite@^7.3.1",
|
||||
"vite-tsconfig-paths": "^6.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import ForwardPage from "@/pages/forward";
|
||||
import TunnelPage from "@/pages/tunnel";
|
||||
import NodePage from "@/pages/node";
|
||||
import UserPage from "@/pages/user";
|
||||
import GroupPage from "@/pages/group";
|
||||
import ProfilePage from "@/pages/profile";
|
||||
import LimitPage from "@/pages/limit";
|
||||
import ConfigPage from "@/pages/config";
|
||||
@@ -148,9 +149,7 @@ function App() {
|
||||
if (cachedAppName && cachedAppName !== document.title) {
|
||||
document.title = cachedAppName;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("检查标题更新失败:", error);
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
// 延迟检查,避免阻塞初始渲染
|
||||
@@ -210,6 +209,14 @@ function App() {
|
||||
}
|
||||
path="/user"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<ProtectedRoute useSimpleLayout={true}>
|
||||
<GroupPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
path="/group"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
|
||||
@@ -147,3 +147,43 @@ export const batchChangeTunnel = (data: {
|
||||
forwardIds: number[];
|
||||
targetTunnelId: number;
|
||||
}) => 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 });
|
||||
|
||||
@@ -98,8 +98,6 @@ const Network = {
|
||||
resolve(response.data);
|
||||
})
|
||||
.catch(function (error: any) {
|
||||
console.error("GET请求错误:", error);
|
||||
|
||||
// 检查是否是401错误(token失效)
|
||||
if (error.response && error.response.status === 401) {
|
||||
handleTokenExpired();
|
||||
@@ -146,8 +144,6 @@ const Network = {
|
||||
resolve(response.data);
|
||||
})
|
||||
.catch(function (error: any) {
|
||||
console.error("POST请求错误:", error);
|
||||
|
||||
// 检查是否是401错误(token失效)
|
||||
if (error.response && error.response.status === 401) {
|
||||
handleTokenExpired();
|
||||
|
||||
@@ -33,9 +33,7 @@ export const Navbar = () => {
|
||||
// 同步更新siteConfig
|
||||
siteConfig.name = cachedAppName;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("检查配置更新失败:", error);
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
// 延迟执行,避免阻塞初始渲染
|
||||
@@ -50,9 +48,7 @@ export const Navbar = () => {
|
||||
setAppName(cachedAppName);
|
||||
siteConfig.name = cachedAppName;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("更新配置失败:", error);
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
window.addEventListener("configUpdated", handleConfigUpdate);
|
||||
|
||||
@@ -126,7 +126,7 @@ export const getCachedConfigs = async (): Promise<Record<string, string>> => {
|
||||
|
||||
return configs;
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// API失败时返回缓存的数据
|
||||
if (hasCachedData) {
|
||||
return cachedConfigs;
|
||||
|
||||
@@ -134,6 +134,16 @@ export default function AdminLayout({
|
||||
),
|
||||
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",
|
||||
label: "设置",
|
||||
@@ -260,9 +270,8 @@ export default function AdminLayout({
|
||||
} else {
|
||||
toast.error(response.msg || "密码修改失败");
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error("修改密码时发生错误");
|
||||
console.error("修改密码错误:", error);
|
||||
} finally {
|
||||
setPasswordLoading(false);
|
||||
}
|
||||
@@ -289,8 +298,10 @@ export default function AdminLayout({
|
||||
>
|
||||
{/* 移动端遮罩层 */}
|
||||
{isMobile && mobileMenuVisible && (
|
||||
<div
|
||||
<button
|
||||
aria-label="关闭菜单"
|
||||
className="fixed inset-0 backdrop-blur-sm bg-white/50 dark:bg-black/30 z-40"
|
||||
type="button"
|
||||
onClick={hideMobileMenu}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function H5SimpleLayout({
|
||||
React.useEffect(() => {
|
||||
try {
|
||||
window.scrollTo({ top: 0, left: 0, behavior: "auto" });
|
||||
} catch (e) {
|
||||
} catch {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
document.body.scrollTop = 0;
|
||||
|
||||
@@ -108,7 +108,7 @@ export default function H5Layout({ children }: { children: React.ReactNode }) {
|
||||
useEffect(() => {
|
||||
try {
|
||||
window.scrollTo({ top: 0, left: 0, behavior: "auto" });
|
||||
} catch (e) {
|
||||
} catch {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
document.body.scrollTop = 0;
|
||||
|
||||
@@ -89,8 +89,7 @@ export default function ChangePasswordPage() {
|
||||
} else {
|
||||
toast.error(response.msg || "账号密码修改失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("修改账号密码错误:", error);
|
||||
} catch {
|
||||
toast.error("修改账号密码时发生错误");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
||||
@@ -121,7 +121,7 @@ const getInitialConfigs = (): Record<string, string> => {
|
||||
initialConfigs[key] = cachedValue;
|
||||
}
|
||||
});
|
||||
} catch (error) {}
|
||||
} catch {}
|
||||
|
||||
return initialConfigs;
|
||||
};
|
||||
@@ -172,7 +172,7 @@ export default function ConfigPage() {
|
||||
setHasChanges(false);
|
||||
} else {
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// 只有在没有缓存数据时才显示错误
|
||||
if (!hasInitialData) {
|
||||
toast.error("加载配置出错,请重试");
|
||||
@@ -250,7 +250,7 @@ export default function ConfigPage() {
|
||||
} else {
|
||||
toast.error("保存配置失败: " + response.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error("保存配置出错,请重试");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
|
||||
@@ -207,8 +207,7 @@ export default function DashboardPage() {
|
||||
} else {
|
||||
toast.error(res.msg || "获取套餐信息失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取套餐信息失败:", error);
|
||||
} catch {
|
||||
toast.error("获取套餐信息失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -648,7 +647,7 @@ export default function DashboardPage() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
toast.success(`已复制`);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error("复制失败");
|
||||
}
|
||||
};
|
||||
@@ -661,7 +660,7 @@ export default function DashboardPage() {
|
||||
),
|
||||
);
|
||||
await copyToClipboard(addressItem.address);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error("复制失败");
|
||||
} finally {
|
||||
setAddressList((prev) =>
|
||||
@@ -1189,14 +1188,15 @@ export default function DashboardPage() {
|
||||
{forward.name}
|
||||
</h4>
|
||||
<div className="space-y-1">
|
||||
<code
|
||||
<button
|
||||
className={`block px-2 py-1 bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-300 rounded font-mono text-xs truncate ${hasMultipleIps(forward.inIp) ? "cursor-pointer hover:bg-green-200 dark:hover:bg-green-500/30" : ""}`}
|
||||
disabled={!hasMultipleIps(forward.inIp)}
|
||||
title={formatInAddress(
|
||||
forward.inIp,
|
||||
forward.inPort,
|
||||
)}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
hasMultipleIps(forward.inIp) &&
|
||||
showAddressModal(
|
||||
forward.inIp,
|
||||
forward.inPort,
|
||||
@@ -1205,17 +1205,20 @@ export default function DashboardPage() {
|
||||
}
|
||||
>
|
||||
{formatInAddress(forward.inIp, forward.inPort)}
|
||||
</code>
|
||||
</button>
|
||||
<div className="text-center text-default-400 text-xs">
|
||||
↓
|
||||
</div>
|
||||
<code
|
||||
<button
|
||||
className={`block px-2 py-1 bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-300 rounded font-mono text-xs truncate ${hasMultipleRemoteAddresses(forward.remoteAddr) ? "cursor-pointer hover:bg-blue-200 dark:hover:bg-blue-500/30" : ""}`}
|
||||
title={formatRemoteAddress(forward.remoteAddr)}
|
||||
onClick={() =>
|
||||
hasMultipleRemoteAddresses(
|
||||
disabled={
|
||||
!hasMultipleRemoteAddresses(
|
||||
forward.remoteAddr,
|
||||
) &&
|
||||
)
|
||||
}
|
||||
title={formatRemoteAddress(forward.remoteAddr)}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
showRemoteAddressModal(
|
||||
forward.remoteAddr,
|
||||
"出口地址",
|
||||
@@ -1223,7 +1226,7 @@ export default function DashboardPage() {
|
||||
}
|
||||
>
|
||||
{formatRemoteAddress(forward.remoteAddr)}
|
||||
</code>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+336
-261
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -156,8 +156,7 @@ export default function IndexPage() {
|
||||
|
||||
tacInstanceRef.current = new window.TAC(config, style);
|
||||
tacInstanceRef.current.init();
|
||||
} catch (error) {
|
||||
console.error("初始化验证码失败:", error);
|
||||
} catch {
|
||||
toast.error("验证码初始化失败,请刷新页面重试");
|
||||
setShowCaptcha(false);
|
||||
setLoading(false);
|
||||
@@ -202,8 +201,7 @@ export default function IndexPage() {
|
||||
// 登录成功
|
||||
toast.success("登录成功");
|
||||
navigate("/dashboard");
|
||||
} catch (error) {
|
||||
console.error("登录错误:", error);
|
||||
} catch {
|
||||
toast.error("网络错误,请稍后重试");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -239,7 +237,6 @@ export default function IndexPage() {
|
||||
}, 100);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("检查验证码状态错误:", error);
|
||||
toast.error("网络错误,请稍后重试" + error);
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -94,10 +94,8 @@ export default function LimitPage() {
|
||||
if (tunnelsRes.code === 0) {
|
||||
setTunnels(tunnelsRes.data || []);
|
||||
} else {
|
||||
console.warn("获取隧道列表失败:", tunnelsRes.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载数据失败:", error);
|
||||
} catch {
|
||||
toast.error("加载数据失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -177,8 +175,7 @@ export default function LimitPage() {
|
||||
} else {
|
||||
toast.error(res.msg || "删除失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("删除失败:", error);
|
||||
} catch {
|
||||
toast.error("删除失败");
|
||||
} finally {
|
||||
setDeleteLoading(false);
|
||||
@@ -196,7 +193,9 @@ export default function LimitPage() {
|
||||
if (isEdit) {
|
||||
res = await updateSpeedLimit(form);
|
||||
} else {
|
||||
const { id, ...createData } = form;
|
||||
const createData = { ...form };
|
||||
|
||||
delete createData.id;
|
||||
|
||||
res = await createSpeedLimit(createData);
|
||||
}
|
||||
@@ -208,8 +207,7 @@ export default function LimitPage() {
|
||||
} else {
|
||||
toast.error(res.msg || "操作失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("提交失败:", error);
|
||||
} catch {
|
||||
toast.error("操作失败");
|
||||
} finally {
|
||||
setSubmitLoading(false);
|
||||
@@ -233,7 +231,7 @@ export default function LimitPage() {
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex-1" />
|
||||
|
||||
<Button size="sm" variant="flat" color="primary" onPress={handleAdd}>
|
||||
<Button color="primary" size="sm" variant="flat" onPress={handleAdd}>
|
||||
新增
|
||||
</Button>
|
||||
</div>
|
||||
@@ -255,8 +253,8 @@ export default function LimitPage() {
|
||||
</div>
|
||||
<Chip
|
||||
color={rule.status === 1 ? "success" : "danger"}
|
||||
variant="flat"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
>
|
||||
{rule.status === 1 ? "运行" : "异常"}
|
||||
</Chip>
|
||||
@@ -291,24 +289,26 @@ export default function LimitPage() {
|
||||
<div className="flex gap-2 mt-4">
|
||||
<Button
|
||||
className="flex-1"
|
||||
color="primary"
|
||||
size="sm"
|
||||
startContent={
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
|
||||
</svg>
|
||||
}
|
||||
variant="flat"
|
||||
onPress={() => handleEdit(rule)}
|
||||
color="primary"
|
||||
size="sm"
|
||||
startContent={
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
|
||||
</svg>
|
||||
}
|
||||
variant="flat"
|
||||
onPress={() => handleEdit(rule)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color="danger"
|
||||
onPress={() => handleDelete(rule)}
|
||||
className="flex-1"
|
||||
color="danger"
|
||||
size="sm"
|
||||
startContent={
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
@@ -316,17 +316,19 @@ export default function LimitPage() {
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"
|
||||
clipRule="evenodd"
|
||||
d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8 7a1 1 0 012 0v4a1 1 0 11-2 0V7zM12 7a1 1 0 012 0v4a1 1 0 11-2 0V7z"
|
||||
clipRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8 7a1 1 0 012 0v4a1 1 0 11-2 0V7zM12 7a1 1 0 012 0v4a1 1 0 11-2 0V7z"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
variant="flat"
|
||||
onPress={() => handleDelete(rule)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
@@ -348,10 +350,10 @@ export default function LimitPage() {
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 6v6l4 2m6-6a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M12 6v6l4 2m6-6a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -370,12 +372,12 @@ export default function LimitPage() {
|
||||
|
||||
{/* 新增/编辑模态框 */}
|
||||
<Modal
|
||||
isOpen={modalOpen}
|
||||
onOpenChange={setModalOpen}
|
||||
size="2xl"
|
||||
scrollBehavior="outside"
|
||||
backdrop="blur"
|
||||
isOpen={modalOpen}
|
||||
placement="center"
|
||||
scrollBehavior="outside"
|
||||
size="2xl"
|
||||
onOpenChange={setModalOpen}
|
||||
>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
@@ -394,36 +396,50 @@ export default function LimitPage() {
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
errorMessage={errors.name}
|
||||
isInvalid={!!errors.name}
|
||||
label="规则名称"
|
||||
placeholder="请输入限速规则名称"
|
||||
value={form.name}
|
||||
variant="bordered"
|
||||
onChange={(e) => setForm(prev => ({ ...prev, name: e.target.value }))}
|
||||
isInvalid={!!errors.name}
|
||||
label="规则名称"
|
||||
placeholder="请输入限速规则名称"
|
||||
value={form.name}
|
||||
variant="bordered"
|
||||
onChange={(e) =>
|
||||
setForm((prev) => ({ ...prev, name: e.target.value }))
|
||||
}
|
||||
/>
|
||||
|
||||
<Input
|
||||
endContent={
|
||||
<div className="pointer-events-none flex items-center">
|
||||
<span className="text-default-400 text-small">Mbps</span>
|
||||
</div>
|
||||
}
|
||||
errorMessage={errors.speed}
|
||||
isInvalid={!!errors.speed}
|
||||
label="速度限制"
|
||||
placeholder="请输入速度限制"
|
||||
type="number"
|
||||
value={form.speed.toString()}
|
||||
variant="bordered"
|
||||
onChange={(e) => setForm(prev => ({ ...prev, speed: parseInt(e.target.value) || 0 }))}
|
||||
<div className="pointer-events-none flex items-center">
|
||||
<span className="text-default-400 text-small">
|
||||
Mbps
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
errorMessage={errors.speed}
|
||||
isInvalid={!!errors.speed}
|
||||
label="速度限制"
|
||||
placeholder="请输入速度限制"
|
||||
type="number"
|
||||
value={form.speed.toString()}
|
||||
variant="bordered"
|
||||
onChange={(e) =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
speed: parseInt(e.target.value) || 0,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
|
||||
<Select
|
||||
description={isEdit ? "编辑时无法修改绑定隧道" : undefined}
|
||||
errorMessage={errors.tunnelId}
|
||||
isDisabled={isEdit}
|
||||
isInvalid={!!errors.tunnelId}
|
||||
label="绑定隧道"
|
||||
placeholder="请选择要绑定的隧道"
|
||||
selectedKeys={
|
||||
form.tunnelId ? [form.tunnelId.toString()] : []
|
||||
}
|
||||
variant="bordered"
|
||||
onSelectionChange={(keys) => {
|
||||
const selectedKey = Array.from(keys)[0] as string;
|
||||
|
||||
@@ -431,6 +447,7 @@ export default function LimitPage() {
|
||||
const selectedTunnel = tunnels.find(
|
||||
(tunnel) => tunnel.id === parseInt(selectedKey),
|
||||
);
|
||||
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
tunnelId: parseInt(selectedKey),
|
||||
@@ -444,11 +461,6 @@ export default function LimitPage() {
|
||||
}));
|
||||
}
|
||||
}}
|
||||
isInvalid={!!errors.tunnelId}
|
||||
errorMessage={errors.tunnelId}
|
||||
variant="bordered"
|
||||
isDisabled={isEdit}
|
||||
description={isEdit ? "编辑时无法修改绑定隧道" : undefined}
|
||||
>
|
||||
{tunnels.map((tunnel) => (
|
||||
<SelectItem key={tunnel.id}>{tunnel.name}</SelectItem>
|
||||
@@ -462,8 +474,8 @@ export default function LimitPage() {
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
onPress={handleSubmit}
|
||||
isLoading={submitLoading}
|
||||
onPress={handleSubmit}
|
||||
>
|
||||
{isEdit ? "保存修改" : "创建规则"}
|
||||
</Button>
|
||||
@@ -475,12 +487,12 @@ export default function LimitPage() {
|
||||
|
||||
{/* 删除确认模态框 */}
|
||||
<Modal
|
||||
isOpen={deleteModalOpen}
|
||||
onOpenChange={setDeleteModalOpen}
|
||||
size="2xl"
|
||||
scrollBehavior="outside"
|
||||
backdrop="blur"
|
||||
isOpen={deleteModalOpen}
|
||||
placement="center"
|
||||
scrollBehavior="outside"
|
||||
size="2xl"
|
||||
onOpenChange={setDeleteModalOpen}
|
||||
>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
@@ -492,7 +504,7 @@ export default function LimitPage() {
|
||||
<p className="text-default-600">
|
||||
确定要删除限速规则{" "}
|
||||
<span className="font-semibold text-foreground">
|
||||
"{ruleToDelete?.name}"
|
||||
"{ruleToDelete?.name}"
|
||||
</span>{" "}
|
||||
吗?
|
||||
</p>
|
||||
@@ -506,8 +518,8 @@ export default function LimitPage() {
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
onPress={confirmDelete}
|
||||
isLoading={deleteLoading}
|
||||
onPress={confirmDelete}
|
||||
>
|
||||
确认删除
|
||||
</Button>
|
||||
|
||||
@@ -266,7 +266,7 @@ export default function NodePage() {
|
||||
} else {
|
||||
toast.error(res.msg || "加载节点列表失败");
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error("网络错误,请重试");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -312,7 +312,7 @@ export default function NodePage() {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
handleWebSocketMessage(data);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// 解析失败时不输出错误信息
|
||||
}
|
||||
};
|
||||
@@ -327,7 +327,7 @@ export default function NodePage() {
|
||||
setWsConnecting(false);
|
||||
attemptReconnect();
|
||||
};
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setWsConnected(false);
|
||||
setWsConnecting(false);
|
||||
attemptReconnect();
|
||||
@@ -413,7 +413,7 @@ export default function NodePage() {
|
||||
uptime: currentUptime,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
@@ -717,7 +717,7 @@ export default function NodePage() {
|
||||
} else {
|
||||
toast.error(res.msg || "删除失败");
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error("网络错误,请重试");
|
||||
} finally {
|
||||
setDeleteLoading(false);
|
||||
@@ -737,7 +737,7 @@ export default function NodePage() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(res.data);
|
||||
toast.success("安装命令已复制到剪贴板");
|
||||
} catch (copyError) {
|
||||
} catch {
|
||||
// 复制失败,显示安装命令模态框
|
||||
setInstallCommand(res.data);
|
||||
setCurrentNodeName(node.name);
|
||||
@@ -746,7 +746,7 @@ export default function NodePage() {
|
||||
} else {
|
||||
toast.error(res.msg || "获取安装命令失败");
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error("获取安装命令失败");
|
||||
} finally {
|
||||
setNodeList((prev) =>
|
||||
@@ -761,7 +761,7 @@ export default function NodePage() {
|
||||
await navigator.clipboard.writeText(installCommand);
|
||||
toast.success("安装命令已复制到剪贴板");
|
||||
setInstallCommandModal(false);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error("复制失败,请手动选择文本复制");
|
||||
}
|
||||
};
|
||||
@@ -774,13 +774,13 @@ export default function NodePage() {
|
||||
|
||||
try {
|
||||
const apiCall = isEdit ? updateNode : createNode;
|
||||
const { serverHost: _serverHost, ...rest } = form;
|
||||
const { serverHost, ...rest } = form;
|
||||
const data = {
|
||||
...rest,
|
||||
serverIp:
|
||||
form.serverIpV4?.trim() ||
|
||||
form.serverIpV6?.trim() ||
|
||||
form.serverHost?.trim() ||
|
||||
serverHost?.trim() ||
|
||||
"",
|
||||
};
|
||||
|
||||
@@ -821,7 +821,7 @@ export default function NodePage() {
|
||||
} else {
|
||||
toast.error(res.msg || (isEdit ? "更新失败" : "创建失败"));
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error("网络错误,请重试");
|
||||
} finally {
|
||||
setSubmitLoading(false);
|
||||
@@ -871,9 +871,7 @@ export default function NodePage() {
|
||||
// 保存到 localStorage
|
||||
try {
|
||||
localStorage.setItem("node-order", JSON.stringify(newOrder));
|
||||
} catch (error) {
|
||||
console.warn("无法保存节点排序到localStorage:", error);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// 持久化到数据库
|
||||
try {
|
||||
@@ -891,8 +889,7 @@ export default function NodePage() {
|
||||
} else {
|
||||
toast.error("保存排序失败:" + (response.msg || "未知错误"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("保存节点排序到数据库失败:", error);
|
||||
} catch {
|
||||
toast.error("保存排序失败,请重试");
|
||||
}
|
||||
};
|
||||
@@ -903,6 +900,7 @@ export default function NodePage() {
|
||||
if (prev) {
|
||||
setSelectedIds(new Set());
|
||||
}
|
||||
|
||||
return !prev;
|
||||
});
|
||||
};
|
||||
@@ -910,11 +908,13 @@ export default function NodePage() {
|
||||
const toggleSelect = (id: number) => {
|
||||
setSelectedIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
|
||||
if (next.has(id)) {
|
||||
next.delete(id);
|
||||
} else {
|
||||
next.add(id);
|
||||
}
|
||||
|
||||
return next;
|
||||
});
|
||||
};
|
||||
@@ -932,6 +932,7 @@ export default function NodePage() {
|
||||
setBatchLoading(true);
|
||||
try {
|
||||
const res = await batchDeleteNodes(Array.from(selectedIds));
|
||||
|
||||
if (res.code === 0) {
|
||||
toast.success(`成功删除 ${selectedIds.size} 个节点`);
|
||||
setNodeList((prev) => prev.filter((n) => !selectedIds.has(n.id)));
|
||||
@@ -941,7 +942,7 @@ export default function NodePage() {
|
||||
} else {
|
||||
toast.error(res.msg || "删除失败");
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error("网络错误,请重试");
|
||||
} finally {
|
||||
setBatchLoading(false);
|
||||
@@ -1724,7 +1725,8 @@ export default function NodePage() {
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>
|
||||
确定要删除节点 <strong>"{nodeToDelete?.name}"</strong> 吗?
|
||||
确定要删除节点{" "}
|
||||
<strong>"{nodeToDelete?.name}"</strong> 吗?
|
||||
</p>
|
||||
<p className="text-small text-default-500">
|
||||
此操作不可恢复,请谨慎操作。
|
||||
@@ -1818,7 +1820,8 @@ export default function NodePage() {
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>
|
||||
确定要删除选中的 <strong>{selectedIds.size}</strong> 个节点吗?
|
||||
确定要删除选中的 <strong>{selectedIds.size}</strong>{" "}
|
||||
个节点吗?
|
||||
</p>
|
||||
<p className="text-small text-default-500">
|
||||
此操作不可恢复,请谨慎操作。
|
||||
|
||||
@@ -168,9 +168,8 @@ export default function ProfilePage() {
|
||||
} else {
|
||||
toast.error(response.msg || "密码修改失败");
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error("修改密码时发生错误");
|
||||
console.error("修改密码错误:", error);
|
||||
} finally {
|
||||
setPasswordLoading(false);
|
||||
}
|
||||
|
||||
@@ -220,10 +220,8 @@ export default function TunnelPage() {
|
||||
if (nodesRes.code === 0) {
|
||||
setNodes(nodesRes.data || []);
|
||||
} else {
|
||||
console.warn("获取节点列表失败:", nodesRes.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载数据失败:", error);
|
||||
} catch {
|
||||
toast.error("加载数据失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -356,8 +354,7 @@ export default function TunnelPage() {
|
||||
} else {
|
||||
toast.error(response.msg || "删除失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("删除失败:", error);
|
||||
} catch {
|
||||
toast.error("删除失败");
|
||||
} finally {
|
||||
setDeleteLoading(false);
|
||||
@@ -498,8 +495,7 @@ export default function TunnelPage() {
|
||||
} else {
|
||||
toast.error(response.msg || (isEdit ? "更新失败" : "创建失败"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("提交失败:", error);
|
||||
} catch {
|
||||
toast.error("网络错误,请重试");
|
||||
} finally {
|
||||
setSubmitLoading(false);
|
||||
@@ -537,8 +533,7 @@ export default function TunnelPage() {
|
||||
],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("诊断失败:", error);
|
||||
} catch {
|
||||
toast.error("网络错误,请重试");
|
||||
setDiagnosisResult({
|
||||
tunnelName: tunnel.name,
|
||||
@@ -627,9 +622,7 @@ export default function TunnelPage() {
|
||||
// 保存到 localStorage
|
||||
try {
|
||||
localStorage.setItem("tunnel-order", JSON.stringify(newOrder));
|
||||
} catch (error) {
|
||||
console.warn("无法保存隧道排序到localStorage:", error);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// 持久化到数据库
|
||||
try {
|
||||
@@ -647,8 +640,7 @@ export default function TunnelPage() {
|
||||
} else {
|
||||
toast.error("保存排序失败:" + (response.msg || "未知错误"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("保存隧道排序到数据库失败:", error);
|
||||
} catch {
|
||||
toast.error("保存排序失败,请重试");
|
||||
}
|
||||
};
|
||||
@@ -662,6 +654,7 @@ export default function TunnelPage() {
|
||||
|
||||
const toggleSelect = (id: number) => {
|
||||
const newSet = new Set(selectedIds);
|
||||
|
||||
if (newSet.has(id)) {
|
||||
newSet.delete(id);
|
||||
} else {
|
||||
@@ -672,6 +665,7 @@ export default function TunnelPage() {
|
||||
|
||||
const selectAll = () => {
|
||||
const allIds = sortedTunnels.map((t) => t.id);
|
||||
|
||||
setSelectedIds(new Set(allIds));
|
||||
};
|
||||
|
||||
@@ -684,12 +678,16 @@ export default function TunnelPage() {
|
||||
setBatchLoading(true);
|
||||
try {
|
||||
const res = await batchDeleteTunnels(Array.from(selectedIds));
|
||||
|
||||
if (res.code === 0) {
|
||||
const result = res.data;
|
||||
|
||||
if (result.failCount === 0) {
|
||||
toast.success(`成功删除 ${result.successCount} 项`);
|
||||
} else {
|
||||
toast.error(`成功 ${result.successCount} 项,失败 ${result.failCount} 项`);
|
||||
toast.error(
|
||||
`成功 ${result.successCount} 项,失败 ${result.failCount} 项`,
|
||||
);
|
||||
}
|
||||
setSelectedIds(new Set());
|
||||
setSelectMode(false);
|
||||
@@ -710,12 +708,16 @@ export default function TunnelPage() {
|
||||
setBatchLoading(true);
|
||||
try {
|
||||
const res = await batchRedeployTunnels(Array.from(selectedIds));
|
||||
|
||||
if (res.code === 0) {
|
||||
const result = res.data;
|
||||
|
||||
if (result.failCount === 0) {
|
||||
toast.success(`成功重新下发 ${result.successCount} 项`);
|
||||
} else {
|
||||
toast.error(`成功 ${result.successCount} 项,失败 ${result.failCount} 项`);
|
||||
toast.error(
|
||||
`成功 ${result.successCount} 项,失败 ${result.failCount} 项`,
|
||||
);
|
||||
}
|
||||
setSelectedIds(new Set());
|
||||
setSelectMode(false);
|
||||
@@ -840,9 +842,9 @@ export default function TunnelPage() {
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
color={selectMode ? "warning" : "default"}
|
||||
size="sm"
|
||||
variant={selectMode ? "solid" : "flat"}
|
||||
color={selectMode ? "warning" : "default"}
|
||||
onPress={toggleSelectMode}
|
||||
>
|
||||
{selectMode ? "退出" : "批量"}
|
||||
@@ -856,7 +858,9 @@ export default function TunnelPage() {
|
||||
{selectMode && selectedIds.size > 0 && (
|
||||
<div className="fixed bottom-7 left-1/2 z-50 w-[calc(100vw-1rem)] max-w-max -translate-x-1/2 overflow-x-auto rounded-lg border border-divider bg-content1 p-2 shadow-lg">
|
||||
<div className="flex min-w-max items-center gap-2">
|
||||
<span className="text-sm text-default-600 shrink-0">已选择 {selectedIds.size} 项</span>
|
||||
<span className="text-sm text-default-600 shrink-0">
|
||||
已选择 {selectedIds.size} 项
|
||||
</span>
|
||||
<Button size="sm" variant="flat" onPress={selectAll}>
|
||||
全选
|
||||
</Button>
|
||||
@@ -864,19 +868,19 @@ export default function TunnelPage() {
|
||||
清空
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
color="danger"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
onPress={() => setBatchDeleteModalOpen(true)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
isLoading={batchLoading}
|
||||
size="sm"
|
||||
variant="flat"
|
||||
onPress={handleBatchRedeploy}
|
||||
isLoading={batchLoading}
|
||||
>
|
||||
下发
|
||||
</Button>
|
||||
@@ -906,9 +910,9 @@ export default function TunnelPage() {
|
||||
<div className="flex justify-between items-start w-full">
|
||||
{selectMode && (
|
||||
<Checkbox
|
||||
className="mr-2"
|
||||
isSelected={selectedIds.has(tunnel.id)}
|
||||
onValueChange={() => toggleSelect(tunnel.id)}
|
||||
className="mr-2"
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -1666,7 +1670,7 @@ export default function TunnelPage() {
|
||||
{getChainGroups().length === 0 && (
|
||||
<div className="text-center py-8 bg-default-50 dark:bg-default-100/50 rounded border border-dashed border-default-300">
|
||||
<p className="text-sm text-default-500">
|
||||
还没有添加转发链,点击上方"添加一跳"按钮开始添加
|
||||
还没有添加转发链,点击上方"添加一跳"按钮开始添加
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -1972,7 +1976,8 @@ export default function TunnelPage() {
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>
|
||||
确定要删除隧道 <strong>"{tunnelToDelete?.name}"</strong> 吗?
|
||||
确定要删除隧道{" "}
|
||||
<strong>"{tunnelToDelete?.name}"</strong> 吗?
|
||||
</p>
|
||||
<p className="text-small text-default-500">
|
||||
此操作不可恢复,请谨慎操作。
|
||||
@@ -2512,13 +2517,19 @@ export default function TunnelPage() {
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<Modal isOpen={batchDeleteModalOpen} onOpenChange={setBatchDeleteModalOpen}>
|
||||
<Modal
|
||||
isOpen={batchDeleteModalOpen}
|
||||
onOpenChange={setBatchDeleteModalOpen}
|
||||
>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader>确认删除</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>确定要删除选中的 {selectedIds.size} 项隧道吗?此操作不可撤销,相关转发也将被删除。</p>
|
||||
<p>
|
||||
确定要删除选中的 {selectedIds.size}{" "}
|
||||
项隧道吗?此操作不可撤销,相关转发也将被删除。
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="light" onPress={onClose}>
|
||||
@@ -2526,8 +2537,8 @@ export default function TunnelPage() {
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
onPress={handleBatchDelete}
|
||||
isLoading={batchLoading}
|
||||
onPress={handleBatchDelete}
|
||||
>
|
||||
确认删除
|
||||
</Button>
|
||||
|
||||
@@ -229,7 +229,7 @@ export default function UserPage() {
|
||||
} else {
|
||||
toast.error(response.msg || "获取用户列表失败");
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error("获取用户列表失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -243,9 +243,7 @@ export default function UserPage() {
|
||||
if (response.code === 0) {
|
||||
setTunnels(response.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取隧道列表失败:", error);
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const loadSpeedLimits = async () => {
|
||||
@@ -255,9 +253,7 @@ export default function UserPage() {
|
||||
if (response.code === 0) {
|
||||
setSpeedLimits(response.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取限速规则列表失败:", error);
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const loadUserTunnels = async (userId: number) => {
|
||||
@@ -270,7 +266,7 @@ export default function UserPage() {
|
||||
} else {
|
||||
toast.error(response.msg || "获取隧道权限列表失败");
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error("获取隧道权限列表失败");
|
||||
} finally {
|
||||
setTunnelListLoading(false);
|
||||
@@ -332,7 +328,7 @@ export default function UserPage() {
|
||||
} else {
|
||||
toast.error(response.msg || "删除失败");
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error("删除失败");
|
||||
}
|
||||
};
|
||||
@@ -366,7 +362,7 @@ export default function UserPage() {
|
||||
} else {
|
||||
toast.error(response.msg || (isEdit ? "更新失败" : "创建失败"));
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error(isEdit ? "更新失败" : "创建失败");
|
||||
} finally {
|
||||
setUserFormLoading(false);
|
||||
@@ -406,7 +402,7 @@ export default function UserPage() {
|
||||
} else {
|
||||
toast.error(response.msg || "分配失败");
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error("分配失败");
|
||||
} finally {
|
||||
setAssignLoading(false);
|
||||
@@ -445,7 +441,7 @@ export default function UserPage() {
|
||||
} else {
|
||||
toast.error(response.msg || "更新失败");
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error("更新失败");
|
||||
} finally {
|
||||
setEditTunnelLoading(false);
|
||||
@@ -473,7 +469,7 @@ export default function UserPage() {
|
||||
} else {
|
||||
toast.error(response.msg || "删除失败");
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error("删除失败");
|
||||
}
|
||||
};
|
||||
@@ -502,7 +498,7 @@ export default function UserPage() {
|
||||
} else {
|
||||
toast.error(response.msg || "重置失败");
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error("重置失败");
|
||||
} finally {
|
||||
setResetFlowLoading(false);
|
||||
@@ -535,7 +531,7 @@ export default function UserPage() {
|
||||
} else {
|
||||
toast.error(response.msg || "重置失败");
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error("重置失败");
|
||||
} finally {
|
||||
setResetTunnelFlowLoading(false);
|
||||
@@ -1005,6 +1001,7 @@ export default function UserPage() {
|
||||
return (
|
||||
<div
|
||||
key={tunnel.id}
|
||||
aria-disabled={isAssigned}
|
||||
className={`
|
||||
px-4 py-3 rounded-lg border transition-all duration-200 cursor-pointer
|
||||
${
|
||||
@@ -1015,9 +1012,21 @@ export default function UserPage() {
|
||||
: "bg-white dark:bg-default-50 border-default-200 dark:border-default-100/30 hover:border-primary-200 dark:hover:border-primary-500/30 hover:shadow-sm"
|
||||
}
|
||||
`}
|
||||
role="button"
|
||||
tabIndex={isAssigned ? -1 : 0}
|
||||
onClick={() =>
|
||||
!isAssigned && toggleTunnelSelection(tunnel.id)
|
||||
}
|
||||
onKeyDown={(event) => {
|
||||
if (isAssigned) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
toggleTunnelSelection(tunnel.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
@@ -1051,7 +1060,7 @@ export default function UserPage() {
|
||||
</div>
|
||||
|
||||
{isSelected && !isAssigned && (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<div>
|
||||
<Select
|
||||
className="w-36"
|
||||
classNames={{
|
||||
@@ -1071,6 +1080,7 @@ export default function UserPage() {
|
||||
: ["null"]
|
||||
}
|
||||
size="sm"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onSelectionChange={(keys) => {
|
||||
const value = Array.from(keys)[0] as string;
|
||||
|
||||
@@ -1455,7 +1465,7 @@ export default function UserPage() {
|
||||
<p className="text-foreground">
|
||||
确定要删除用户{" "}
|
||||
<span className="font-semibold text-danger">
|
||||
"{userToDelete?.user}"
|
||||
"{userToDelete?.user}"
|
||||
</span>{" "}
|
||||
吗?
|
||||
</p>
|
||||
@@ -1500,7 +1510,7 @@ export default function UserPage() {
|
||||
<span className="font-semibold">{currentUser?.user}</span>{" "}
|
||||
对隧道{" "}
|
||||
<span className="font-semibold text-danger">
|
||||
"{tunnelToDelete?.tunnelName}"
|
||||
"{tunnelToDelete?.tunnelName}"
|
||||
</span>{" "}
|
||||
的权限吗?
|
||||
</p>
|
||||
@@ -1553,7 +1563,7 @@ export default function UserPage() {
|
||||
<p className="text-foreground">
|
||||
确定要重置用户{" "}
|
||||
<span className="font-semibold text-warning">
|
||||
"{userToReset?.user}"
|
||||
"{userToReset?.user}"
|
||||
</span>{" "}
|
||||
的流量吗?
|
||||
</p>
|
||||
@@ -1643,7 +1653,7 @@ export default function UserPage() {
|
||||
<span className="font-semibold">{currentUser?.user}</span>{" "}
|
||||
对隧道{" "}
|
||||
<span className="font-semibold text-warning">
|
||||
"{tunnelToReset?.tunnelName}"
|
||||
"{tunnelToReset?.tunnelName}"
|
||||
</span>{" "}
|
||||
的流量吗?
|
||||
</p>
|
||||
|
||||
Vendored
+8
-5
@@ -2,7 +2,7 @@
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
TAC: typeof TAC;
|
||||
TAC: TACConstructor;
|
||||
currentCaptcha?: any;
|
||||
currentCaptchaRes?: any;
|
||||
}
|
||||
@@ -42,13 +42,16 @@ interface CaptchaStyle {
|
||||
moveTrackMaskBorderColor?: string;
|
||||
}
|
||||
|
||||
declare class TAC {
|
||||
constructor(config: CaptchaConfig, style?: CaptchaStyle);
|
||||
|
||||
init(): TAC;
|
||||
interface TACInstance {
|
||||
init(): TACInstance;
|
||||
reloadCaptcha(): void;
|
||||
destroyWindow(): void;
|
||||
openCaptcha(): void;
|
||||
}
|
||||
|
||||
type TACConstructor = new (
|
||||
config: CaptchaConfig,
|
||||
style?: CaptchaStyle,
|
||||
) => TACInstance;
|
||||
|
||||
export {};
|
||||
|
||||
@@ -60,17 +60,15 @@ export function isLoggedIn(): boolean {
|
||||
/**
|
||||
* 权限检查装饰器函数
|
||||
* @param fn 要执行的函数
|
||||
* @param errorMsg 权限不足时的错误提示
|
||||
* @param _errorMsg 权限不足时的错误提示
|
||||
* @returns 包装后的函数
|
||||
*/
|
||||
export function requireAdmin<T extends (...args: any[]) => any>(
|
||||
fn: T,
|
||||
errorMsg: string = "权限不足,仅管理员可操作",
|
||||
_errorMsg: string = "权限不足,仅管理员可操作",
|
||||
): T {
|
||||
return ((...args: Parameters<T>) => {
|
||||
if (!isAdmin()) {
|
||||
console.warn(errorMsg);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ function getPayloadFromToken(token: string): JWTPayload | null {
|
||||
const decodedPayload = atob(encodedPayload);
|
||||
|
||||
return JSON.parse(decodedPayload) as JWTPayload;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ export function validatePanelAddress(address: string): boolean {
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// URL构造函数失败说明格式不正确
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user