mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-08 02:36:37 +08:00
fix(forward): show all users by default in non-compact mode
Restore 2.1.8-beta9 admin default filtering when compact mode is off; keep compact mode focused on self.
This commit is contained in:
@@ -0,0 +1,165 @@
|
|||||||
|
---
|
||||||
|
name: security-scan
|
||||||
|
description: Scan your Claude Code configuration (.claude/ directory) for security vulnerabilities, misconfigurations, and injection risks using AgentShield. Checks CLAUDE.md, settings.json, MCP servers, hooks, and agent definitions.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# Security Scan Skill
|
||||||
|
|
||||||
|
Audit your Claude Code configuration for security issues using [AgentShield](https://github.com/affaan-m/agentshield).
|
||||||
|
|
||||||
|
## When to Activate
|
||||||
|
|
||||||
|
- Setting up a new Claude Code project
|
||||||
|
- After modifying `.claude/settings.json`, `CLAUDE.md`, or MCP configs
|
||||||
|
- Before committing configuration changes
|
||||||
|
- When onboarding to a new repository with existing Claude Code configs
|
||||||
|
- Periodic security hygiene checks
|
||||||
|
|
||||||
|
## What It Scans
|
||||||
|
|
||||||
|
| File | Checks |
|
||||||
|
|------|--------|
|
||||||
|
| `CLAUDE.md` | Hardcoded secrets, auto-run instructions, prompt injection patterns |
|
||||||
|
| `settings.json` | Overly permissive allow lists, missing deny lists, dangerous bypass flags |
|
||||||
|
| `mcp.json` | Risky MCP servers, hardcoded env secrets, npx supply chain risks |
|
||||||
|
| `hooks/` | Command injection via interpolation, data exfiltration, silent error suppression |
|
||||||
|
| `agents/*.md` | Unrestricted tool access, prompt injection surface, missing model specs |
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
AgentShield must be installed. Check and install if needed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if installed
|
||||||
|
npx ecc-agentshield --version
|
||||||
|
|
||||||
|
# Install globally (recommended)
|
||||||
|
npm install -g ecc-agentshield
|
||||||
|
|
||||||
|
# Or run directly via npx (no install needed)
|
||||||
|
npx ecc-agentshield scan .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Scan
|
||||||
|
|
||||||
|
Run against the current project's `.claude/` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Scan current project
|
||||||
|
npx ecc-agentshield scan
|
||||||
|
|
||||||
|
# Scan a specific path
|
||||||
|
npx ecc-agentshield scan --path /path/to/.claude
|
||||||
|
|
||||||
|
# Scan with minimum severity filter
|
||||||
|
npx ecc-agentshield scan --min-severity medium
|
||||||
|
```
|
||||||
|
|
||||||
|
### Output Formats
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Terminal output (default) — colored report with grade
|
||||||
|
npx ecc-agentshield scan
|
||||||
|
|
||||||
|
# JSON — for CI/CD integration
|
||||||
|
npx ecc-agentshield scan --format json
|
||||||
|
|
||||||
|
# Markdown — for documentation
|
||||||
|
npx ecc-agentshield scan --format markdown
|
||||||
|
|
||||||
|
# HTML — self-contained dark-theme report
|
||||||
|
npx ecc-agentshield scan --format html > security-report.html
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auto-Fix
|
||||||
|
|
||||||
|
Apply safe fixes automatically (only fixes marked as auto-fixable):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx ecc-agentshield scan --fix
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
- Replace hardcoded secrets with environment variable references
|
||||||
|
- Tighten wildcard permissions to scoped alternatives
|
||||||
|
- Never modify manual-only suggestions
|
||||||
|
|
||||||
|
### Opus 4.6 Deep Analysis
|
||||||
|
|
||||||
|
Run the adversarial three-agent pipeline for deeper analysis:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Requires ANTHROPIC_API_KEY
|
||||||
|
export ANTHROPIC_API_KEY=your-key
|
||||||
|
npx ecc-agentshield scan --opus --stream
|
||||||
|
```
|
||||||
|
|
||||||
|
This runs:
|
||||||
|
1. **Attacker (Red Team)** — finds attack vectors
|
||||||
|
2. **Defender (Blue Team)** — recommends hardening
|
||||||
|
3. **Auditor (Final Verdict)** — synthesizes both perspectives
|
||||||
|
|
||||||
|
### Initialize Secure Config
|
||||||
|
|
||||||
|
Scaffold a new secure `.claude/` configuration from scratch:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx ecc-agentshield init
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates:
|
||||||
|
- `settings.json` with scoped permissions and deny list
|
||||||
|
- `CLAUDE.md` with security best practices
|
||||||
|
- `mcp.json` placeholder
|
||||||
|
|
||||||
|
### GitHub Action
|
||||||
|
|
||||||
|
Add to your CI pipeline:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: affaan-m/agentshield@v1
|
||||||
|
with:
|
||||||
|
path: '.'
|
||||||
|
min-severity: 'medium'
|
||||||
|
fail-on-findings: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Severity Levels
|
||||||
|
|
||||||
|
| Grade | Score | Meaning |
|
||||||
|
|-------|-------|---------|
|
||||||
|
| A | 90-100 | Secure configuration |
|
||||||
|
| B | 75-89 | Minor issues |
|
||||||
|
| C | 60-74 | Needs attention |
|
||||||
|
| D | 40-59 | Significant risks |
|
||||||
|
| F | 0-39 | Critical vulnerabilities |
|
||||||
|
|
||||||
|
## Interpreting Results
|
||||||
|
|
||||||
|
### Critical Findings (fix immediately)
|
||||||
|
- Hardcoded API keys or tokens in config files
|
||||||
|
- `Bash(*)` in the allow list (unrestricted shell access)
|
||||||
|
- Command injection in hooks via `${file}` interpolation
|
||||||
|
- Shell-running MCP servers
|
||||||
|
|
||||||
|
### High Findings (fix before production)
|
||||||
|
- Auto-run instructions in CLAUDE.md (prompt injection vector)
|
||||||
|
- Missing deny lists in permissions
|
||||||
|
- Agents with unnecessary Bash access
|
||||||
|
|
||||||
|
### Medium Findings (recommended)
|
||||||
|
- Silent error suppression in hooks (`2>/dev/null`, `|| true`)
|
||||||
|
- Missing PreToolUse security hooks
|
||||||
|
- `npx -y` auto-install in MCP server configs
|
||||||
|
|
||||||
|
### Info Findings (awareness)
|
||||||
|
- Missing descriptions on MCP servers
|
||||||
|
- Prohibitive instructions correctly flagged as good practice
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- **GitHub**: [github.com/affaan-m/agentshield](https://github.com/affaan-m/agentshield)
|
||||||
|
- **npm**: [npmjs.com/package/ecc-agentshield](https://www.npmjs.com/package/ecc-agentshield)
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../.agents/skills/security-scan
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# 功能请求:在规则页面显示隧道倍率
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
|
||||||
|
当前规则(Forward)页面在列表中显示隧道名称,但**不显示隧道的流量倍率(trafficRatio)**。管理员在管理规则时无法快速查看该规则所使用的隧道倍率信息,需要跳转到隧道页面才能查看。
|
||||||
|
|
||||||
|
## 期望行为
|
||||||
|
|
||||||
|
在规则列表页面中,在隧道名称旁边或单独列显示该隧道的流量倍率(例如:`1x`, `0.5x`, `2x`)。
|
||||||
|
|
||||||
|
## 建议实现位置
|
||||||
|
|
||||||
|
### 前端修改
|
||||||
|
|
||||||
|
1. **`vite-frontend/src/pages/forward.tsx`**
|
||||||
|
- 在 `Forward` interface 中添加 `tunnelTrafficRatio?: number` 字段
|
||||||
|
- 在表格列中添加倍率显示(可以在隧道名称 Chip 旁边或单独一列)
|
||||||
|
- 从 `userTunnel` 或 `getTunnelList` API 获取隧道倍率信息
|
||||||
|
|
||||||
|
2. **显示格式建议**
|
||||||
|
```tsx
|
||||||
|
<Chip className="...">
|
||||||
|
{forward.tunnelName} ({forward.tunnelTrafficRatio}x)
|
||||||
|
</Chip>
|
||||||
|
```
|
||||||
|
或者单独一列:
|
||||||
|
```tsx
|
||||||
|
<TableCell>
|
||||||
|
{forward.tunnelTrafficRatio}x
|
||||||
|
</TableCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 后端修改
|
||||||
|
|
||||||
|
1. **`go-backend/internal/http/handler/handler.go`**
|
||||||
|
- 在 `forwardList` 接口返回中添加隧道的 `trafficRatio` 字段
|
||||||
|
- 需要在查询 Forward 时 JOIN Tunnel 表获取倍率信息
|
||||||
|
|
||||||
|
2. **或者在前端加载规则后,批量获取隧道信息**
|
||||||
|
- 调用 `getTunnelList` 获取所有隧道信息
|
||||||
|
- 根据 `tunnelId` 匹配倍率
|
||||||
|
|
||||||
|
## 相关文件
|
||||||
|
|
||||||
|
- 前端:`vite-frontend/src/pages/forward.tsx`
|
||||||
|
- 前端类型:`vite-frontend/src/api/types.ts`
|
||||||
|
- 后端:`go-backend/internal/http/handler/handler.go`
|
||||||
|
- 隧道类型定义:`vite-frontend/src/api/types.ts` (TunnelApiItem)
|
||||||
|
|
||||||
|
## 优先级
|
||||||
|
|
||||||
|
中等 - 不影响核心功能,但能提升管理效率
|
||||||
|
|
||||||
|
## 截图参考
|
||||||
|
|
||||||
|
隧道页面已显示倍率:
|
||||||
|
- 位置:隧道卡片统计信息区域
|
||||||
|
- 显示格式:`流量倍率 {trafficRatio}x`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Labels**: `enhancement`, `frontend`, `backend`, `ui/ux`
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
# GOST UDP vs Realm UDP Analysis
|
||||||
|
|
||||||
|
**Goal:** Compare GOST UDP forwarding through tunnels with Realm's UDP implementation to understand why users report GOST UDP forwarding has problems while Realm works correctly.
|
||||||
|
|
||||||
|
## Task Checklist
|
||||||
|
|
||||||
|
- [x] Analyze GOST UDP tunnel architecture
|
||||||
|
- [x] Analyze Realm UDP relay architecture
|
||||||
|
- [x] Identify architectural differences
|
||||||
|
- [x] Identify potential issues in GOST implementation
|
||||||
|
- [ ] Document findings and recommendations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GOST UDP Architecture
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
1. **UDP Relay** (`x/internal/net/udp/relay.go`)
|
||||||
|
- Simple bidirectional packet copying between two `net.PacketConn` interfaces
|
||||||
|
- Uses two goroutines: one for each direction
|
||||||
|
- Sequential packet processing (no batching)
|
||||||
|
- No idle timeout or association tracking
|
||||||
|
|
||||||
|
2. **UDP over Tunnel** (`x/handler/relay/bind.go`, `x/handler/socks/v5/udp_tun.go`)
|
||||||
|
- Wraps UDP data with SOCKS5-style framing via `UDPTunServerConn()`
|
||||||
|
- Adds address headers to each packet
|
||||||
|
- Uses smux multiplexing for tunnel connections
|
||||||
|
|
||||||
|
3. **SOCKS5 UDP Framing** (`x/internal/util/socks/conn.go`, `x/internal/util/relay/conn.go`)
|
||||||
|
- `udpTunConn.ReadFrom()`: Parses SOCKS5 UDP header to extract target address
|
||||||
|
- `udpTunConn.WriteTo()`: Wraps data with SOCKS5 UDP header including:
|
||||||
|
- RSV (data length, 2 bytes)
|
||||||
|
- Frag (0xff for tunnel relay, 1 byte)
|
||||||
|
- Address (4/16/1+n bytes for IPv4/IPv6/domain)
|
||||||
|
|
||||||
|
4. **Multiplexing** (`x/internal/util/mux/mux.go`)
|
||||||
|
- Uses smux v1.5.31 for connection multiplexing
|
||||||
|
- Adds stream framing and flow control overhead
|
||||||
|
|
||||||
|
### Data Flow (UDP over Tunnel)
|
||||||
|
|
||||||
|
```
|
||||||
|
Client UDP packet
|
||||||
|
↓
|
||||||
|
[Local GOST] SOCKS5 UDP framing (add ~10-26 bytes)
|
||||||
|
↓
|
||||||
|
smux stream (add framing, flow control)
|
||||||
|
↓
|
||||||
|
TCP tunnel to remote
|
||||||
|
↓
|
||||||
|
[Remote GOST] smux demux
|
||||||
|
↓
|
||||||
|
SOCKS5 UDP deframing
|
||||||
|
↓
|
||||||
|
Forward to target
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Realm UDP Architecture
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
1. **Association Model** (`realm_core/src/udp/middle.rs`)
|
||||||
|
- Per-client socket associations stored in `SockMap`
|
||||||
|
- Creates dedicated remote socket per client address
|
||||||
|
- Spawns `send_back` task for return path
|
||||||
|
- Association timeout for cleanup
|
||||||
|
|
||||||
|
2. **Batched I/O** (`realm_core/src/udp/batched.rs`)
|
||||||
|
- Uses `recvmmsg/sendmmsg` on Linux
|
||||||
|
- Up to 128 packets per batch
|
||||||
|
- Significantly higher throughput for high-PPS traffic
|
||||||
|
|
||||||
|
3. **No Protocol Overhead**
|
||||||
|
- Plain UDP forwarding without adding protocol headers
|
||||||
|
- No SOCKS5 framing or mux layer
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Client UDP packet
|
||||||
|
↓
|
||||||
|
[Realm] Direct relay via association socket
|
||||||
|
↓
|
||||||
|
Target server
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Architectural Differences
|
||||||
|
|
||||||
|
| Aspect | GOST | Realm |
|
||||||
|
|--------|------|-------|
|
||||||
|
| **UDP over Tunnel** | SOCKS5 framing + smux multiplexing | N/A (direct relay only) |
|
||||||
|
| **Protocol Overhead** | Extra ~10-26 bytes per packet | None |
|
||||||
|
| **I/O Model** | Standard Go net.PacketConn | Batched I/O (recvmmsg/sendmmsg) |
|
||||||
|
| **Connection Tracking** | Via mux session | SockMap with timeout |
|
||||||
|
| **Association Model** | None (bidirectional copy only) | Per-client socket association |
|
||||||
|
| **Idle Timeout** | None on UDP relay | Association timeout |
|
||||||
|
| **Throughput** | Single packet per syscall | Up to 128 packets per syscall |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Identified Potential Issues in GOST
|
||||||
|
|
||||||
|
### 1. No Batch I/O Support
|
||||||
|
- **Impact**: High PPS (packets per second) traffic incurs syscall overhead
|
||||||
|
- **Realm advantage**: `recvmmsg/sendmmsg` batches up to 128 packets
|
||||||
|
- **Evidence**: Realm's `batched.rs` implements this; GOST uses standard `ReadFrom/WriteTo`
|
||||||
|
|
||||||
|
### 2. Protocol Overhead Per Packet
|
||||||
|
- **Impact**: Bandwidth waste, extra processing for framing/deframing
|
||||||
|
- **Overhead**: SOCKS5 UDP header adds ~10 bytes (IPv4) to ~26 bytes (IPv6) per packet
|
||||||
|
- **Evidence**: `x/internal/util/socks/conn.go:63-78` shows framing overhead
|
||||||
|
|
||||||
|
### 3. Mux Layer Overhead
|
||||||
|
- **Impact**: Latency and throughput degradation
|
||||||
|
- **smux adds**: Stream framing, flow control, potential backpressure
|
||||||
|
- **Evidence**: `x/internal/util/mux/mux.go` wraps all tunnel connections
|
||||||
|
|
||||||
|
### 4. No Association Tracking
|
||||||
|
- **Impact**: Cannot properly handle NAT translation for return traffic
|
||||||
|
- **Realm approach**: `SockMap` tracks client→remote socket mapping
|
||||||
|
- **Evidence**: GOST's `udp.Relay` just copies packets bidirectionally
|
||||||
|
|
||||||
|
### 5. No Idle Timeout on UDP Relay
|
||||||
|
- **Impact**: Stale connections may persist indefinitely
|
||||||
|
- **Evidence**: `udp.Relay.Run()` blocks until error or context cancel
|
||||||
|
- **Contrast**: Realm has association timeout for cleanup
|
||||||
|
|
||||||
|
### 6. Sequential Packet Processing
|
||||||
|
- **Impact**: Cannot pipeline multiple packets
|
||||||
|
- **Evidence**: `relay.go:44-77` shows sequential `ReadFrom` → `WriteTo` loop
|
||||||
|
- **Contrast**: Realm's batched I/O handles multiple packets concurrently
|
||||||
|
|
||||||
|
### 7. Read Deadline on Underlying TCP
|
||||||
|
- **Impact**: Could cause unexpected connection termination
|
||||||
|
- **Default**: 15-second read timeout set on connections
|
||||||
|
- **Evidence**: `x/handler/relay/metadata.go:readTimeout` defaults to 15s
|
||||||
|
- **Issue**: UDP relay may not handle deadline properly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
1. **Consider Direct UDP Mode**: For scenarios where tunnel is not required, use direct UDP relay (no SOCKS5 framing)
|
||||||
|
|
||||||
|
2. **Add Association Tracking**: Implement client→remote socket mapping with timeout
|
||||||
|
|
||||||
|
3. **Investigate Batch I/O**: Consider using `recvmmsg/sendmmsg` equivalent in Go (via `x/net` or raw syscalls)
|
||||||
|
|
||||||
|
4. **Add Idle Timeout**: Implement timeout-based cleanup for UDP associations
|
||||||
|
|
||||||
|
5. **Reduce Framing Overhead**: Consider more compact framing for tunnel UDP
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Analyzed
|
||||||
|
|
||||||
|
### GOST Files
|
||||||
|
- `go-gost/x/internal/net/udp/relay.go` - Core UDP relay logic
|
||||||
|
- `go-gost/x/internal/util/mux/mux.go` - smux multiplexing
|
||||||
|
- `go-gost/x/internal/util/socks/conn.go` - SOCKS5 UDP framing
|
||||||
|
- `go-gost/x/internal/util/relay/conn.go` - Relay UDP framing
|
||||||
|
- `go-gost/x/handler/relay/bind.go` - Relay BIND handler
|
||||||
|
- `go-gost/x/handler/socks/v5/udp_tun.go` - SOCKS5 UDP tunnel handler
|
||||||
|
- `go-gost/x/handler/tunnel/bind.go` - Tunnel BIND handler
|
||||||
|
- `go-gost/x/connector/tunnel/bind.go` - Tunnel connector BIND
|
||||||
|
- `go-gost/x/connector/tunnel/conn.go` - Tunnel connection types
|
||||||
|
- `go-gost/x/connector/tunnel/listener.go` - Tunnel bind listener
|
||||||
|
|
||||||
|
### Realm Files (fetched from GitHub)
|
||||||
|
- `realm_core/src/udp/mod.rs` - UDP relay entry point
|
||||||
|
- `realm_core/src/udp/middle.rs` - Association and relay logic with SockMap
|
||||||
|
- `realm_core/src/udp/socket.rs` - UDP socket binding and association
|
||||||
|
- `realm_core/src/udp/batched.rs` - Batched I/O implementation
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Node Card Info Popover
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Restore node card's remark and renewal info display to the 2.1.8-beta9 style: an info button (ℹ️) in the CardHeader that shows a hover popover with the info, instead of the current inline display in the CardBody.
|
||||||
|
|
||||||
|
## Status: Completed
|
||||||
|
|
||||||
|
PR #327 merged
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
- [x] Add `infoPopoverPlacement` state and `updateInfoPopoverPlacement` callback
|
||||||
|
- [x] Add info button with popover to CardHeader
|
||||||
|
- [x] Restore drag handle with touch support
|
||||||
|
- [x] Remove inline info display from CardBody
|
||||||
|
- [x] Fix build errors (JSX structure and unused variables)
|
||||||
|
- [x] Create PR and merge
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"skills": {
|
||||||
|
"security-scan": {
|
||||||
|
"source": "affaan-m/everything-claude-code",
|
||||||
|
"sourceType": "github",
|
||||||
|
"computedHash": "92cdcaddc554e318402f066ccc073c2e3dbcfda8c2730ec62ec373f805c41a57"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -784,9 +784,9 @@ const SortableTableRow = ({
|
|||||||
className={`${FORWARD_GROUPED_TABLE_COLUMN_CLASS.inbound} max-w-[280px] ${selectedIds.has(forward.id) ? "bg-primary-50/70 dark:bg-primary-900/40" : ""}`}
|
className={`${FORWARD_GROUPED_TABLE_COLUMN_CLASS.inbound} max-w-[280px] ${selectedIds.has(forward.id) ? "bg-primary-50/70 dark:bg-primary-900/40" : ""}`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
|
||||||
className="w-full truncate rounded-md bg-default-100/50 px-2.5 py-1.5 text-left font-mono text-xs font-medium text-default-700 transition-all hover:bg-default-200 hover:shadow-sm cursor-pointer"
|
className="w-full truncate rounded-md bg-default-100/50 px-2.5 py-1.5 text-left font-mono text-xs font-medium text-default-700 transition-all hover:bg-default-200 hover:shadow-sm cursor-pointer"
|
||||||
title={formatInAddress(forward.inIp, forward.inPort)}
|
title={formatInAddress(forward.inIp, forward.inPort)}
|
||||||
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
showAddressModal(forward.inIp, forward.inPort, "入口端口")
|
showAddressModal(forward.inIp, forward.inPort, "入口端口")
|
||||||
}
|
}
|
||||||
@@ -798,12 +798,10 @@ const SortableTableRow = ({
|
|||||||
className={`${FORWARD_GROUPED_TABLE_COLUMN_CLASS.target} max-w-[280px] ${selectedIds.has(forward.id) ? "bg-primary-50/70 dark:bg-primary-900/40" : ""}`}
|
className={`${FORWARD_GROUPED_TABLE_COLUMN_CLASS.target} max-w-[280px] ${selectedIds.has(forward.id) ? "bg-primary-50/70 dark:bg-primary-900/40" : ""}`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
|
||||||
className="w-full truncate rounded-md bg-default-100/50 px-2.5 py-1.5 text-left font-mono text-xs font-medium text-default-700 transition-all hover:bg-default-200 hover:shadow-sm cursor-pointer"
|
className="w-full truncate rounded-md bg-default-100/50 px-2.5 py-1.5 text-left font-mono text-xs font-medium text-default-700 transition-all hover:bg-default-200 hover:shadow-sm cursor-pointer"
|
||||||
title={formatRemoteAddress(forward.remoteAddr)}
|
title={formatRemoteAddress(forward.remoteAddr)}
|
||||||
onClick={() =>
|
type="button"
|
||||||
showAddressModal(forward.remoteAddr, null, "目标地址")
|
onClick={() => showAddressModal(forward.remoteAddr, null, "目标地址")}
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{formatRemoteAddress(forward.remoteAddr)}
|
{formatRemoteAddress(forward.remoteAddr)}
|
||||||
</button>
|
</button>
|
||||||
@@ -1010,7 +1008,9 @@ const SortableCompactTableRow = ({
|
|||||||
}`}
|
}`}
|
||||||
title={formatInAddress(forward.inIp, forward.inPort)}
|
title={formatInAddress(forward.inIp, forward.inPort)}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => showAddressModal(forward.inIp, forward.inPort, "入口端口")}
|
onClick={() =>
|
||||||
|
showAddressModal(forward.inIp, forward.inPort, "入口端口")
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{formatInAddress(forward.inIp, forward.inPort)}
|
{formatInAddress(forward.inIp, forward.inPort)}
|
||||||
</button>
|
</button>
|
||||||
@@ -1143,23 +1143,80 @@ export default function ForwardPage() {
|
|||||||
const tokenRoleId = JwtUtil.getRoleIdFromToken();
|
const tokenRoleId = JwtUtil.getRoleIdFromToken();
|
||||||
const isAdmin = tokenRoleId === 0;
|
const isAdmin = tokenRoleId === 0;
|
||||||
|
|
||||||
|
const [compactMode, setCompactMode] = useState(false);
|
||||||
|
|
||||||
|
// 在非精简模式下(compactMode=false),管理员默认展示全部用户规则(与 2.1.8-beta9 行为一致)。
|
||||||
|
// 精简模式下默认仅展示管理员本人规则,避免在表格里混看不清所属用户。
|
||||||
|
const defaultSearchUserId = useMemo(() => {
|
||||||
|
if (isAdmin) {
|
||||||
|
if (!compactMode) {
|
||||||
|
return "all";
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenUserId ? tokenUserId.toString() : "all";
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenUserId ? tokenUserId.toString() : "all";
|
||||||
|
}, [compactMode, isAdmin, tokenUserId]);
|
||||||
|
|
||||||
const [searchParams, setSearchParams] = useState({
|
const [searchParams, setSearchParams] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
userId: tokenUserId ? tokenUserId.toString() : "all",
|
userId: defaultSearchUserId,
|
||||||
tunnelId: "all",
|
tunnelId: "all",
|
||||||
inPort: "",
|
inPort: "",
|
||||||
remoteAddr: "",
|
remoteAddr: "",
|
||||||
});
|
});
|
||||||
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false);
|
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const resetSearchParams = useCallback(() => {
|
||||||
|
setSearchParams({
|
||||||
|
name: "",
|
||||||
|
userId: defaultSearchUserId,
|
||||||
|
tunnelId: "all",
|
||||||
|
inPort: "",
|
||||||
|
remoteAddr: "",
|
||||||
|
});
|
||||||
|
}, [defaultSearchUserId]);
|
||||||
|
|
||||||
|
const lastDefaultSearchUserIdRef = useRef(defaultSearchUserId);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const previousDefault = lastDefaultSearchUserIdRef.current;
|
||||||
|
|
||||||
|
lastDefaultSearchUserIdRef.current = defaultSearchUserId;
|
||||||
|
|
||||||
|
if (!isAdmin || previousDefault === defaultSearchUserId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearchParams((prev) => {
|
||||||
|
const hasOtherFilters =
|
||||||
|
Boolean(prev.name.trim()) ||
|
||||||
|
prev.tunnelId !== "all" ||
|
||||||
|
Boolean(prev.inPort.trim()) ||
|
||||||
|
Boolean(prev.remoteAddr.trim());
|
||||||
|
|
||||||
|
if (hasOtherFilters) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prev.userId !== previousDefault) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
userId: defaultSearchUserId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [defaultSearchUserId, isAdmin]);
|
||||||
|
|
||||||
const activeFilterCount =
|
const activeFilterCount =
|
||||||
(searchParams.name ? 1 : 0) +
|
(searchParams.name.trim() ? 1 : 0) +
|
||||||
(searchParams.userId !== "all" &&
|
(searchParams.userId !== defaultSearchUserId ? 1 : 0) +
|
||||||
searchParams.userId !== (tokenUserId ? tokenUserId.toString() : "all")
|
|
||||||
? 1
|
|
||||||
: 0) +
|
|
||||||
(searchParams.tunnelId !== "all" ? 1 : 0) +
|
(searchParams.tunnelId !== "all" ? 1 : 0) +
|
||||||
(searchParams.inPort ? 1 : 0) +
|
(searchParams.inPort.trim() ? 1 : 0) +
|
||||||
(searchParams.remoteAddr ? 1 : 0);
|
(searchParams.remoteAddr.trim() ? 1 : 0);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [forwards, setForwards] = useState<Forward[]>([]);
|
const [forwards, setForwards] = useState<Forward[]>([]);
|
||||||
const [tunnels, setTunnels] = useState<Tunnel[]>([]);
|
const [tunnels, setTunnels] = useState<Tunnel[]>([]);
|
||||||
@@ -1169,7 +1226,6 @@ export default function ForwardPage() {
|
|||||||
// const isMobile = useMobileBreakpoint();
|
// const isMobile = useMobileBreakpoint();
|
||||||
// searchKeyword removed
|
// searchKeyword removed
|
||||||
// isSearchVisible removed
|
// isSearchVisible removed
|
||||||
const [compactMode, setCompactMode] = useState(false);
|
|
||||||
|
|
||||||
// 显示模式状态 - 从localStorage读取,默认为平铺显示
|
// 显示模式状态 - 从localStorage读取,默认为平铺显示
|
||||||
const [viewMode, setViewMode] = useState<"grouped" | "direct">(() => {
|
const [viewMode, setViewMode] = useState<"grouped" | "direct">(() => {
|
||||||
@@ -3930,15 +3986,7 @@ export default function ForwardPage() {
|
|||||||
color="danger"
|
color="danger"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="light"
|
variant="light"
|
||||||
onPress={() =>
|
onPress={resetSearchParams}
|
||||||
setSearchParams({
|
|
||||||
name: "",
|
|
||||||
userId: tokenUserId ? tokenUserId.toString() : "all",
|
|
||||||
tunnelId: "all",
|
|
||||||
inPort: "",
|
|
||||||
remoteAddr: "",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
清空条件
|
清空条件
|
||||||
</Button>
|
</Button>
|
||||||
@@ -6061,15 +6109,7 @@ export default function ForwardPage() {
|
|||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
onPress={() => {
|
onPress={resetSearchParams}
|
||||||
setSearchParams({
|
|
||||||
name: "",
|
|
||||||
userId: tokenUserId ? tokenUserId.toString() : "all",
|
|
||||||
tunnelId: "all",
|
|
||||||
inPort: "",
|
|
||||||
remoteAddr: "",
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
重置
|
重置
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user