mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 07:36:38 +08:00
docs: add AI Skill integration guide (#225)
## Summary - Add AI Skill documentation for LLM integration with FLVX panel - Include complete API reference documentation for the skill - Add publish workflow for skill distribution ## Changes - New `doc/ai-skill.md` guide - New `skills/flvx-api/` directory with API references - New `.github/workflows/publish-skill.yml` workflow - Update `doc/index.md` navigation
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
name: Publish Skill to npm
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
id-token: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
|
- name: Get version from tag
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||||
|
VERSION=$(node -p "require('./skills/flvx-api/package.json').version")
|
||||||
|
else
|
||||||
|
VERSION="${GITHUB_REF#refs/tags/v}"
|
||||||
|
fi
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "Publishing skill version: $VERSION"
|
||||||
|
|
||||||
|
- name: Publish to npm
|
||||||
|
working-directory: skills/flvx-api
|
||||||
|
run: npm publish --provenance --access public
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
name: Skill v${{ steps.version.outputs.version }}
|
||||||
|
generate_release_notes: true
|
||||||
|
files: skills/flvx-api/package.json
|
||||||
+220
@@ -0,0 +1,220 @@
|
|||||||
|
# AI Skill 使用指南
|
||||||
|
|
||||||
|
让大模型直接操作 FLVX 面板的技能包。支持 OpenCode、OpenClaw、Claude Code 等工具。
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
### 方式 1: npm (推荐)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g @flvx/skill-api
|
||||||
|
```
|
||||||
|
|
||||||
|
postinstall 脚本会自动链接到 `~/.agents/skills/flvx-api/`。
|
||||||
|
|
||||||
|
### 方式 2: 手动链接
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 从 FLVX 源码
|
||||||
|
cd /path/to/flvx
|
||||||
|
mkdir -p ~/.agents/skills
|
||||||
|
ln -sf $(pwd)/skills/flvx-api ~/.agents/skills/
|
||||||
|
|
||||||
|
# 或从 GitHub
|
||||||
|
git clone https://github.com/Sagit-chu/flvx.git
|
||||||
|
cd flvx
|
||||||
|
ln -sf $(pwd)/skills/flvx-api ~/.agents/skills/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
设置环境变量:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export FLVX_BASE_URL="https://your-panel.example.com"
|
||||||
|
export FLVX_USERNAME="admin"
|
||||||
|
export FLVX_PASSWORD="your-password"
|
||||||
|
```
|
||||||
|
|
||||||
|
或使用凭证文件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.flvx
|
||||||
|
cat > ~/.flvx/.env << 'EOF'
|
||||||
|
export FLVX_BASE_URL="https://panel.example.com"
|
||||||
|
export FLVX_USERNAME="admin"
|
||||||
|
export FLVX_PASSWORD="your-password"
|
||||||
|
EOF
|
||||||
|
chmod 600 ~/.flvx/.env
|
||||||
|
source ~/.flvx/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 工具接入方法
|
||||||
|
|
||||||
|
### OpenCode
|
||||||
|
|
||||||
|
OpenCode 是命令行 AI 编程助手,支持通过 skills 扩展能力。
|
||||||
|
|
||||||
|
**安装 skill:**
|
||||||
|
```bash
|
||||||
|
npm install -g @flvx/skill-api
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用:**
|
||||||
|
```bash
|
||||||
|
export FLVX_BASE_URL="https://panel.example.com"
|
||||||
|
export FLVX_USERNAME="admin"
|
||||||
|
export FLVX_PASSWORD="your-password"
|
||||||
|
|
||||||
|
opencode
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例对话:**
|
||||||
|
```
|
||||||
|
你: 查看我的转发列表
|
||||||
|
你: 创建一个转发到 192.168.1.100:80 使用隧道 1
|
||||||
|
你: 检查节点状态
|
||||||
|
你: 查看流量使用情况
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### OpenClaw
|
||||||
|
|
||||||
|
OpenClaw 同样支持 skills 机制。
|
||||||
|
|
||||||
|
**安装 skill:**
|
||||||
|
```bash
|
||||||
|
npm install -g @flvx/skill-api
|
||||||
|
|
||||||
|
# 或手动链接
|
||||||
|
mkdir -p ~/.openclaw/skills
|
||||||
|
ln -sf /path/to/flvx/skills/flvx-api ~/.openclaw/skills/flvx-api
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用:**
|
||||||
|
```bash
|
||||||
|
openclaw
|
||||||
|
|
||||||
|
>>> 查看所有节点状态
|
||||||
|
>>> 给用户 alice 分配 50GB 流量
|
||||||
|
>>> 导出系统备份
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Claude Code
|
||||||
|
|
||||||
|
Claude Code 是 Anthropic 官方的命令行工具,支持通过 CLAUDE.md 扩展。
|
||||||
|
|
||||||
|
#### 方式 1: 项目级 CLAUDE.md
|
||||||
|
|
||||||
|
在项目根目录创建 `CLAUDE.md`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# FLVX API Skill
|
||||||
|
|
||||||
|
你可以通过 REST API 操作 FLVX 面板。
|
||||||
|
|
||||||
|
## 环境变量
|
||||||
|
- FLVX_BASE_URL: 面板地址
|
||||||
|
- FLVX_USERNAME: 用户名
|
||||||
|
- FLVX_PASSWORD: 密码
|
||||||
|
|
||||||
|
## 认证规则
|
||||||
|
- Authorization 头使用原始 JWT token,不加 "Bearer " 前缀
|
||||||
|
- 所有 API 使用 POST 方法
|
||||||
|
|
||||||
|
## 常用 API
|
||||||
|
|
||||||
|
### 登录获取 token
|
||||||
|
POST /api/v1/user/login
|
||||||
|
{"username": "...", "password": "..."}
|
||||||
|
|
||||||
|
### 查看转发列表
|
||||||
|
POST /api/v1/forward/list
|
||||||
|
Authorization: <token>
|
||||||
|
{}
|
||||||
|
|
||||||
|
### 创建转发
|
||||||
|
POST /api/v1/forward/create
|
||||||
|
{"name": "xxx", "tunnelId": 1, "remoteAddr": "1.2.3.4:80"}
|
||||||
|
|
||||||
|
### 查看节点
|
||||||
|
POST /api/v1/node/list
|
||||||
|
{}
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用:**
|
||||||
|
```bash
|
||||||
|
cd /path/to/your/project
|
||||||
|
claude
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 方式 2: 全局 CLAUDE.md
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.claude
|
||||||
|
cat > ~/.claude/CLAUDE.md << 'EOF'
|
||||||
|
# FLVX Panel Operations
|
||||||
|
|
||||||
|
使用 FLVX REST API 操作流量转发面板。
|
||||||
|
|
||||||
|
环境变量: FLVX_BASE_URL, FLVX_USERNAME, FLVX_PASSWORD
|
||||||
|
调用方式: curl -X POST "$FLVX_BASE_URL/api/v1/..." -H "Authorization: $TOKEN"
|
||||||
|
注意: Authorization 不要加 Bearer 前缀
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 方式 3: 复制 SKILL.md
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat ~/.agents/skills/flvx-api/SKILL.md >> ~/.claude/CLAUDE.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例对话:**
|
||||||
|
```
|
||||||
|
>>> 帮我查看 FLVX 面板上有哪些节点
|
||||||
|
>>> 创建一个名为 test 的转发,目标地址 10.0.0.1:80
|
||||||
|
>>> 查看我的流量使用情况
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API 覆盖
|
||||||
|
|
||||||
|
| 模块 | 操作 |
|
||||||
|
|------|------|
|
||||||
|
| 认证 | 登录、Token 管理 |
|
||||||
|
| 用户 | 增删改查、流量重置、密码 |
|
||||||
|
| 节点 | 增删改查、安装、升级、状态 |
|
||||||
|
| 隧道 | 增删改查、用户分配 |
|
||||||
|
| 转发 | 增删改查、暂停/恢复、诊断 |
|
||||||
|
| 分组 | 用户/隧道分组、权限 |
|
||||||
|
| 限速 | 增删改查 |
|
||||||
|
| 联邦 | 节点共享、远程节点 |
|
||||||
|
| 备份 | 导出/导入 |
|
||||||
|
|
||||||
|
## 安全提示
|
||||||
|
|
||||||
|
- ⚠️ 环境变量在进程列表中可见
|
||||||
|
- 使用 `~/.flvx/.env` 文件并设置 `chmod 600`
|
||||||
|
- 添加 `export HISTIGNORE="*FLVX_PASSWORD*"` 防止密码进入历史记录
|
||||||
|
- Token 仅在会话内存中缓存,不写入磁盘
|
||||||
|
|
||||||
|
## 发布
|
||||||
|
|
||||||
|
维护者可通过以下方式发布新版本:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 方式 1: 推送 tag
|
||||||
|
git tag skill-v2.1.6
|
||||||
|
git push --tags
|
||||||
|
|
||||||
|
# 方式 2: GitHub Actions 手动触发
|
||||||
|
# 在 Actions 页面运行 publish-skill workflow
|
||||||
|
```
|
||||||
|
|
||||||
|
需要在 GitHub 仓库设置 `NPM_TOKEN` secret。
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
- [安装部署](./install.md)
|
- [安装部署](./install.md)
|
||||||
- [使用指南](./usage.md)
|
- [使用指南](./usage.md)
|
||||||
- [PostgreSQL 数据库指南](./postgresql.md)
|
- [PostgreSQL 数据库指南](./postgresql.md)
|
||||||
|
- [AI Skill 接入](./ai-skill.md) - 让大模型直接操作面板
|
||||||
- [常见问题](./faq.md)
|
- [常见问题](./faq.md)
|
||||||
|
|
||||||
## 免责声明
|
## 免责声明
|
||||||
|
|||||||
@@ -0,0 +1,302 @@
|
|||||||
|
---
|
||||||
|
name: flvx-api
|
||||||
|
description: Operate FLVX traffic forwarding management system via REST API. Supports user/node/tunnel/forward management, federation clustering, and traffic monitoring. Use when user wants to manage FLVX panel programmatically or via natural language.
|
||||||
|
metadata:
|
||||||
|
author: FLVX Team
|
||||||
|
version: "2.1.5"
|
||||||
|
requires_env:
|
||||||
|
- FLVX_BASE_URL
|
||||||
|
- FLVX_USERNAME
|
||||||
|
- FLVX_PASSWORD
|
||||||
|
---
|
||||||
|
|
||||||
|
# FLVX API Operations
|
||||||
|
|
||||||
|
Operate FLVX panel through REST API. All endpoints use POST method and return JSON with `{code, msg, data, ts}` envelope.
|
||||||
|
|
||||||
|
## Supported AI Tools
|
||||||
|
|
||||||
|
| Tool | Installation | Notes |
|
||||||
|
|------|--------------|-------|
|
||||||
|
| **OpenCode** | `npm i -g @flvx/skill-api` or `ln -s . ~/.agents/skills/flvx-api` | Auto-loads from `~/.agents/skills/` |
|
||||||
|
| **OpenClaw** | Same as OpenCode | Compatible skill format |
|
||||||
|
| **Claude Code** | Copy SKILL.md to CLAUDE.md or `~/.claude/CLAUDE.md` | Uses context file instead of skills |
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Set environment variables before starting:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export FLVX_BASE_URL="https://your-panel.example.com"
|
||||||
|
export FLVX_USERNAME="admin"
|
||||||
|
export FLVX_PASSWORD="your-password"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Security tip:** Add to `~/.flvx/.env` and source on demand:
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.flvx && cat > ~/.flvx/.env << 'EOF'
|
||||||
|
export FLVX_BASE_URL="https://panel.example.com"
|
||||||
|
export FLVX_USERNAME="admin"
|
||||||
|
export FLVX_PASSWORD="your-password"
|
||||||
|
EOF
|
||||||
|
chmod 600 ~/.flvx/.env
|
||||||
|
source ~/.flvx/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication Flow
|
||||||
|
|
||||||
|
### Session Token Cache
|
||||||
|
|
||||||
|
- Token is cached **only for the current conversation**
|
||||||
|
- New conversation = fresh login required
|
||||||
|
- Token is NOT written to disk (security)
|
||||||
|
|
||||||
|
### Auto-Login Pattern
|
||||||
|
|
||||||
|
```
|
||||||
|
Before ANY API call:
|
||||||
|
1. Check if TOKEN is cached in current session
|
||||||
|
├─ Yes → Use cached token, proceed
|
||||||
|
└─ No →
|
||||||
|
1. Read FLVX_USERNAME and FLVX_PASSWORD from environment
|
||||||
|
2. POST /api/v1/user/login with credentials
|
||||||
|
3. Cache response.data.token in session memory
|
||||||
|
4. Proceed with original request
|
||||||
|
```
|
||||||
|
|
||||||
|
### Login Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "${FLVX_BASE_URL}/api/v1/user/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"username\":\"${FLVX_USERNAME}\",\"password\":\"${FLVX_PASSWORD}\"}"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIs...",
|
||||||
|
"name": "Administrator",
|
||||||
|
"role_id": 0,
|
||||||
|
"requirePasswordChange": false
|
||||||
|
},
|
||||||
|
"ts": 1706659200000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication Rules
|
||||||
|
|
||||||
|
| Header | Value | Critical |
|
||||||
|
|--------|-------|----------|
|
||||||
|
| `Authorization` | `<jwt_token>` | ⚠️ NO "Bearer" prefix! |
|
||||||
|
| `Content-Type` | `application/json` | All requests use JSON |
|
||||||
|
|
||||||
|
## Quick Start Workflow
|
||||||
|
|
||||||
|
```
|
||||||
|
User request → Check env vars → Auto-login if needed → Call API → Return result
|
||||||
|
```
|
||||||
|
|
||||||
|
## Intent → API Mapping
|
||||||
|
|
||||||
|
| User Intent | API Endpoint | Reference |
|
||||||
|
|-------------|--------------|-----------|
|
||||||
|
| "登录" / "查看我的信息" | `/api/v1/user/package` | [auth](references/auth.md) |
|
||||||
|
| "创建用户" / "添加用户" | `/api/v1/user/create` | [users](references/users.md) |
|
||||||
|
| "查看用户列表" / "所有用户" | `/api/v1/user/list` | [users](references/users.md) |
|
||||||
|
| "重置流量" | `/api/v1/user/reset` | [users](references/users.md) |
|
||||||
|
| "添加节点" / "新建节点" | `/api/v1/node/create` | [nodes](references/nodes.md) |
|
||||||
|
| "查看节点" / "节点状态" | `/api/v1/node/list` | [nodes](references/nodes.md) |
|
||||||
|
| "安装命令" / "部署节点" | `/api/v1/node/install` | [nodes](references/nodes.md) |
|
||||||
|
| "升级节点" | `/api/v1/node/upgrade` | [nodes](references/nodes.md) |
|
||||||
|
| "创建隧道" / "新建隧道" | `/api/v1/tunnel/create` | [tunnels](references/tunnels.md) |
|
||||||
|
| "分配隧道给用户" | `/api/v1/tunnel/user/assign` | [tunnels](references/tunnels.md) |
|
||||||
|
| "创建转发" / "新建转发" / "添加转发" | `/api/v1/forward/create` | [forwards](references/forwards.md) |
|
||||||
|
| "暂停转发" | `/api/v1/forward/pause` | [forwards](references/forwards.md) |
|
||||||
|
| "恢复转发" | `/api/v1/forward/resume` | [forwards](references/forwards.md) |
|
||||||
|
| "删除转发" | `/api/v1/forward/delete` | [forwards](references/forwards.md) |
|
||||||
|
| "查看我的转发" / "转发列表" | `/api/v1/forward/list` | [forwards](references/forwards.md) |
|
||||||
|
| "查看流量" / "流量统计" | `/api/v1/forward/list` or `/api/v1/user/package` | [forwards](references/forwards.md) |
|
||||||
|
| "诊断转发" / "测试连通性" | `/api/v1/forward/diagnose` | [forwards](references/forwards.md) |
|
||||||
|
| "创建限速规则" | `/api/v1/speed-limit/create` | [speed-limits](references/speed-limits.md) |
|
||||||
|
| "联邦共享" / "节点共享" | `/api/v1/federation/share/create` | [federation](references/federation.md) |
|
||||||
|
| "导出备份" | `/api/v1/backup/export` | [backup](references/backup.md) |
|
||||||
|
| "导入备份" | `/api/v1/backup/import` | [backup](references/backup.md) |
|
||||||
|
|
||||||
|
## HTTP Request Template
|
||||||
|
|
||||||
|
### Bash/curl (with auto-login)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
BASE_URL="${FLVX_BASE_URL}"
|
||||||
|
USERNAME="${FLVX_USERNAME}"
|
||||||
|
PASSWORD="${FLVX_PASSWORD}"
|
||||||
|
|
||||||
|
# Login and get token
|
||||||
|
TOKEN=$(curl -s -X POST "${BASE_URL}/api/v1/user/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"username\":\"${USERNAME}\",\"password\":\"${PASSWORD}\"}" | jq -r '.data.token')
|
||||||
|
|
||||||
|
if [ "$TOKEN" == "null" ] || [ -z "$TOKEN" ]; then
|
||||||
|
echo "Login failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use token for API calls - NOTE: NO "Bearer" prefix!
|
||||||
|
curl -s -X POST "${BASE_URL}/api/v1/node/list" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}' | jq '.'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python (requests)
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
|
||||||
|
BASE_URL = os.environ.get("FLVX_BASE_URL")
|
||||||
|
USERNAME = os.environ.get("FLVX_USERNAME")
|
||||||
|
PASSWORD = os.environ.get("FLVX_PASSWORD")
|
||||||
|
|
||||||
|
# Login
|
||||||
|
resp = requests.post(f"{BASE_URL}/api/v1/user/login",
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
json={"username": USERNAME, "password": PASSWORD})
|
||||||
|
result = resp.json()
|
||||||
|
if result["code"] != 0:
|
||||||
|
raise Exception(f"Login failed: {result['msg']}")
|
||||||
|
|
||||||
|
TOKEN = result["data"]["token"]
|
||||||
|
|
||||||
|
# Authenticated request - NO "Bearer" prefix!
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": TOKEN
|
||||||
|
}
|
||||||
|
resp = requests.post(f"{BASE_URL}/api/v1/node/list", headers=headers, json={})
|
||||||
|
print(resp.json())
|
||||||
|
```
|
||||||
|
|
||||||
|
### Node.js (fetch)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const BASE_URL = process.env.FLVX_BASE_URL;
|
||||||
|
const USERNAME = process.env.FLVX_USERNAME;
|
||||||
|
const PASSWORD = process.env.FLVX_PASSWORD;
|
||||||
|
|
||||||
|
// Login
|
||||||
|
const loginRes = await fetch(`${BASE_URL}/api/v1/user/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username: USERNAME, password: PASSWORD })
|
||||||
|
});
|
||||||
|
const loginData = await loginRes.json();
|
||||||
|
if (loginData.code !== 0) throw new Error(loginData.msg);
|
||||||
|
const TOKEN = loginData.data.token;
|
||||||
|
|
||||||
|
// Authenticated request - NO "Bearer" prefix!
|
||||||
|
const res = await fetch(`${BASE_URL}/api/v1/node/list`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': TOKEN
|
||||||
|
},
|
||||||
|
body: JSON.stringify({})
|
||||||
|
});
|
||||||
|
console.log(await res.json());
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Handling
|
||||||
|
|
||||||
|
**Success:**
|
||||||
|
```json
|
||||||
|
{"code": 0, "msg": "success", "data": {...}, "ts": 1706659200000}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error:**
|
||||||
|
```json
|
||||||
|
{"code": -1, "msg": "用户名或密码错误", "ts": 1706659200000}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pattern:**
|
||||||
|
```
|
||||||
|
1. Parse JSON response
|
||||||
|
2. If code === 0 → return data
|
||||||
|
3. If code === 401 → token expired, re-login and retry
|
||||||
|
4. If code === 403 → permission denied, need admin
|
||||||
|
5. Else → show msg to user as error message
|
||||||
|
```
|
||||||
|
|
||||||
|
## Permission Model
|
||||||
|
|
||||||
|
| role_id | Type | Access |
|
||||||
|
|---------|------|--------|
|
||||||
|
| 0 | Admin | All endpoints |
|
||||||
|
| 1 | Regular | Forward CRUD, own profile, assigned tunnels only |
|
||||||
|
|
||||||
|
Non-admin users can only see/modify their own resources.
|
||||||
|
|
||||||
|
## Module Reference
|
||||||
|
|
||||||
|
| Module | Endpoints | Reference |
|
||||||
|
|--------|-----------|-----------|
|
||||||
|
| Auth | login, captcha | [auth.md](references/auth.md) |
|
||||||
|
| Users | CRUD, reset, password | [users.md](references/users.md) |
|
||||||
|
| Nodes | CRUD, install, upgrade, status | [nodes.md](references/nodes.md) |
|
||||||
|
| Tunnels | CRUD, user assignment | [tunnels.md](references/tunnels.md) |
|
||||||
|
| Forwards | CRUD, pause/resume, diagnose | [forwards.md](references/forwards.md) |
|
||||||
|
| Groups | User/tunnel groups, permissions | [groups.md](references/groups.md) |
|
||||||
|
| Speed Limits | CRUD | [speed-limits.md](references/speed-limits.md) |
|
||||||
|
| Federation | Share, remote nodes | [federation.md](references/federation.md) |
|
||||||
|
| Backup | Export/import | [backup.md](references/backup.md) |
|
||||||
|
| Config | System settings | [config.md](references/config.md) |
|
||||||
|
| Types | TypeScript interfaces | [types.md](references/types.md) |
|
||||||
|
| Errors | Error codes | [errors.md](references/errors.md) |
|
||||||
|
| Examples | Code samples | [examples/](references/examples/) |
|
||||||
|
|
||||||
|
## Critical Rules
|
||||||
|
|
||||||
|
1. ⚠️ **NO "Bearer" prefix** - `Authorization: <token>`, NOT `Authorization: Bearer <token>`
|
||||||
|
2. **All endpoints use POST** - Including list/get operations
|
||||||
|
3. **code === 0 means success** - Any other value is an error
|
||||||
|
4. **Traffic units**: User.flow is GB, in_flow/out_flow are bytes
|
||||||
|
5. **Timestamps**: All timestamps are milliseconds since epoch
|
||||||
|
6. **Token is session-scoped**: Cache in memory only, not on disk
|
||||||
|
|
||||||
|
## Common Workflows
|
||||||
|
|
||||||
|
### Workflow 1: New User Onboarding (Admin)
|
||||||
|
```
|
||||||
|
1. POST /api/v1/user/create → Create user with traffic quota
|
||||||
|
2. POST /api/v1/tunnel/user/assign → Assign tunnels to user
|
||||||
|
3. Tell user their username/password
|
||||||
|
4. User logs in and creates forwards
|
||||||
|
```
|
||||||
|
|
||||||
|
### Workflow 2: Add New Node (Admin)
|
||||||
|
```
|
||||||
|
1. POST /api/v1/node/create → Register node in panel
|
||||||
|
2. POST /api/v1/node/install → Get install command
|
||||||
|
3. Run install command on target server
|
||||||
|
4. POST /api/v1/node/check-status → Verify node is online
|
||||||
|
```
|
||||||
|
|
||||||
|
### Workflow 3: Create Forward (Any User)
|
||||||
|
```
|
||||||
|
1. POST /api/v1/tunnel/user/tunnel → List available tunnels
|
||||||
|
2. POST /api/v1/forward/create → Create forward on chosen tunnel
|
||||||
|
3. POST /api/v1/forward/diagnose → Verify connectivity
|
||||||
|
```
|
||||||
|
|
||||||
|
### Workflow 4: Node Maintenance (Admin)
|
||||||
|
```
|
||||||
|
1. POST /api/v1/node/list → Check node statuses
|
||||||
|
2. POST /api/v1/node/releases → Check available versions
|
||||||
|
3. POST /api/v1/node/upgrade or /batch-upgrade → Upgrade nodes
|
||||||
|
4. POST /api/v1/node/rollback → Rollback if needed
|
||||||
|
```
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "@flvx/skill-api",
|
||||||
|
"version": "2.1.5",
|
||||||
|
"description": "Skill for AI assistants to operate FLVX panel via REST API. Supports OpenCode, OpenClaw, Claude Code.",
|
||||||
|
"keywords": [
|
||||||
|
"opencode",
|
||||||
|
"openclaw",
|
||||||
|
"claude-code",
|
||||||
|
"skill",
|
||||||
|
"flvx",
|
||||||
|
"api",
|
||||||
|
"traffic-forwarding",
|
||||||
|
"gost"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"author": "FLVX Team",
|
||||||
|
"files": [
|
||||||
|
"SKILL.md",
|
||||||
|
"references/**/*"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/Sagit-chu/flvx.git",
|
||||||
|
"directory": "skills/flvx-api"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/Sagit-chu/flvx/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/Sagit-chu/flvx/tree/main/skills/flvx-api#readme",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public",
|
||||||
|
"registry": "https://registry.npmjs.org"
|
||||||
|
},
|
||||||
|
"opencode": {
|
||||||
|
"skill": true,
|
||||||
|
"installTo": "~/.agents/skills/flvx-api"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"postinstall": "node -e \"const fs=require('fs');const path=require('path');const target=path.join(process.env.HOME,'.agents','skills','flvx-api');const src=process.cwd();try{fs.mkdirSync(path.dirname(target),{recursive:true});if(fs.existsSync(target)||fs.lstatSync(target).isSymbolicLink()){fs.unlinkSync(target)}fs.symlinkSync(src,target);console.log('✓ Installed to',target)}catch(e){console.error('Manual install: ln -s',src,target)}\"",
|
||||||
|
"preuninstall": "node -e \"const target=require('path').join(process.env.HOME,'.agents','skills','flvx-api');try{require('fs').unlinkSync(target);console.log('✓ Removed',target)}catch(e){}\"",
|
||||||
|
"link": "node -e \"const fs=require('fs');const path=require('path');const target=path.join(process.env.HOME,'.agents','skills','flvx-api');const src=process.cwd();try{fs.mkdirSync(path.dirname(target),{recursive:true});if(fs.existsSync(target)||fs.lstatSync(target).isSymbolicLink()){fs.unlinkSync(target)}fs.symlinkSync(src,target);console.log('✓ Linked to',target)}catch(e){console.error(e)}\"",
|
||||||
|
"unlink": "node -e \"const target=require('path').join(process.env.HOME,'.agents','skills','flvx-api');try{require('fs').unlinkSync(target);console.log('✓ Unlinked',target)}catch(e){}\""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# Authentication API
|
||||||
|
|
||||||
|
## POST /api/v1/user/login
|
||||||
|
|
||||||
|
Authenticate and obtain JWT token.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "admin",
|
||||||
|
"password": "secret",
|
||||||
|
"captchaId": "optional-captcha-id"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIs...",
|
||||||
|
"name": "Administrator",
|
||||||
|
"role_id": 0,
|
||||||
|
"requirePasswordChange": false
|
||||||
|
},
|
||||||
|
"ts": 1706659200000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response Fields:**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| token | string | JWT token for subsequent requests |
|
||||||
|
| name | string | User's display name |
|
||||||
|
| role_id | number | 0 = admin, 1 = regular user |
|
||||||
|
| requirePasswordChange | boolean | Whether password change is required |
|
||||||
|
|
||||||
|
## JWT Token Details
|
||||||
|
|
||||||
|
**Algorithm:** HMAC-SHA256
|
||||||
|
**Lifetime:** 90 days
|
||||||
|
|
||||||
|
**Token Claims:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sub": "1",
|
||||||
|
"user": "admin",
|
||||||
|
"name": "Administrator",
|
||||||
|
"role_id": 0,
|
||||||
|
"iat": 1706659200,
|
||||||
|
"exp": 1738195200
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/captcha/check
|
||||||
|
|
||||||
|
Check if captcha verification is required.
|
||||||
|
|
||||||
|
**Request:** `{}`
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "turnstile"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/captcha/verify
|
||||||
|
|
||||||
|
Verify captcha response (Cloudflare Turnstile or local captcha).
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"captchaId": "captcha-session-id",
|
||||||
|
"captchaValue": "user-captcha-response"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Token Usage
|
||||||
|
|
||||||
|
Include the token in all authenticated requests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "${FLVX_BASE_URL}/api/v1/node/list" \
|
||||||
|
-H "Authorization: eyJhbGciOiJIUzI1NiIs..." \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}'
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **CRITICAL: Do NOT add "Bearer " prefix!**
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ Correct: Authorization: eyJhbGciOiJIUzI1NiIs...
|
||||||
|
❌ Incorrect: Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/user/updatePassword
|
||||||
|
|
||||||
|
Change current user's password.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"oldPassword": "current-password",
|
||||||
|
"newPassword": "new-password"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{"code": 0, "msg": "success"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/user/package
|
||||||
|
|
||||||
|
Get current user's package info (tunnels, forwards, traffic stats).
|
||||||
|
|
||||||
|
**Request:** `{}`
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"flow": 100,
|
||||||
|
"inFlow": 1073741824,
|
||||||
|
"outFlow": 2147483648,
|
||||||
|
"tunnels": 5,
|
||||||
|
"forwards": 10,
|
||||||
|
"expTime": 1735689600000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fields:**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| flow | number | Total traffic quota in GB |
|
||||||
|
| inFlow | number | Used upload in bytes |
|
||||||
|
| outFlow | number | Used download in bytes |
|
||||||
|
| tunnels | number | Number of assigned tunnels |
|
||||||
|
| forwards | number | Number of forwards created |
|
||||||
|
| expTime | number | Account expiry timestamp (ms) |
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
# Backup & Restore API
|
||||||
|
|
||||||
|
Export and import system data for backup, migration, or disaster recovery.
|
||||||
|
|
||||||
|
## POST /api/v1/backup/export
|
||||||
|
|
||||||
|
Export system data.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"types": ["users", "nodes", "tunnels", "forwards", "speed_limits", "groups"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If `types` is empty or omitted, exports all data.
|
||||||
|
|
||||||
|
**Available types:**
|
||||||
|
- `users` - User accounts
|
||||||
|
- `nodes` - Node configurations
|
||||||
|
- `tunnels` - Tunnel configurations
|
||||||
|
- `forwards` - Forward rules
|
||||||
|
- `speed_limits` - Speed limit rules
|
||||||
|
- `groups` - User/tunnel groups and permissions
|
||||||
|
- `configs` - System configurations
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"version": "2.1.5",
|
||||||
|
"exportedAt": 1706659200000,
|
||||||
|
"types": ["users", "nodes", "tunnels"],
|
||||||
|
"users": [...],
|
||||||
|
"nodes": [...],
|
||||||
|
"tunnels": [...],
|
||||||
|
"forwards": [...],
|
||||||
|
"speedLimits": [...],
|
||||||
|
"tunnelGroups": [...],
|
||||||
|
"userGroups": [...],
|
||||||
|
"groupPermissions": [...],
|
||||||
|
"configs": {...}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/backup/import
|
||||||
|
|
||||||
|
Import system data from a backup.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "2.1.5",
|
||||||
|
"exportedAt": 1706659200000,
|
||||||
|
"types": ["users", "nodes"],
|
||||||
|
"users": [...],
|
||||||
|
"nodes": [...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Import Behavior:**
|
||||||
|
- Existing records are updated if IDs match
|
||||||
|
- New records are created for non-existent IDs
|
||||||
|
- Related entities must be included (e.g., forwards require tunnels)
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"imported": {
|
||||||
|
"users": 5,
|
||||||
|
"nodes": 3,
|
||||||
|
"tunnels": 10
|
||||||
|
},
|
||||||
|
"skipped": {
|
||||||
|
"forwards": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/backup/restore
|
||||||
|
|
||||||
|
Alias for `/api/v1/backup/import`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow: Full System Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Export all data
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/backup/export" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}' > backup-$(date +%Y%m%d).json
|
||||||
|
|
||||||
|
echo "Backup saved to backup-$(date +%Y%m%d).json"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow: Partial Export
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Export only users and tunnels
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/backup/export" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"types":["users","tunnels"]}' > partial-backup.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow: Restore from Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Import from backup file
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/backup/import" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d @backup-20260226.json | jq '.'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow: Migrate to New Panel
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On source panel
|
||||||
|
curl -s -X POST "${SOURCE_URL}/api/v1/backup/export" \
|
||||||
|
-H "Authorization: ${SOURCE_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}' > migration.json
|
||||||
|
|
||||||
|
# On target panel
|
||||||
|
curl -s -X POST "${TARGET_URL}/api/v1/backup/import" \
|
||||||
|
-H "Authorization: ${TARGET_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d @migration.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** After migration, you may need to:
|
||||||
|
1. Reinstall node agents with new panel URL
|
||||||
|
2. Update node secrets if they differ
|
||||||
|
3. Reassign federation tokens
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# System Configuration API
|
||||||
|
|
||||||
|
Manage system-wide settings and configurations.
|
||||||
|
|
||||||
|
## POST /api/v1/config/get
|
||||||
|
|
||||||
|
Get a single configuration by name. This endpoint is public (no auth required).
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{"name": "site_name"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"name": "site_name",
|
||||||
|
"value": "My FLVX Panel",
|
||||||
|
"time": 1706659200000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/config/list
|
||||||
|
|
||||||
|
List all configurations (requires authentication).
|
||||||
|
|
||||||
|
**Request:** `{}`
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"site_name": "My FLVX Panel",
|
||||||
|
"site_logo": "https://example.com/logo.png",
|
||||||
|
"site_announcement": "System maintenance scheduled",
|
||||||
|
"captcha_enabled": "true",
|
||||||
|
"captcha_type": "turnstile",
|
||||||
|
"turnstile_site_key": "...",
|
||||||
|
"default_user_flow": "100",
|
||||||
|
"default_user_exp_days": "30"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/config/update
|
||||||
|
|
||||||
|
Batch update multiple configurations (admin only).
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"site_name": "New Panel Name",
|
||||||
|
"site_announcement": "Welcome to the new panel!",
|
||||||
|
"default_user_flow": "50"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Only include the keys you want to update.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{"code": 0, "msg": "success"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/config/update-single
|
||||||
|
|
||||||
|
Update a single configuration (admin only).
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "site_name",
|
||||||
|
"value": "My Awesome Panel"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/announcement/get
|
||||||
|
|
||||||
|
Get the site announcement (public endpoint).
|
||||||
|
|
||||||
|
**Method:** GET
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"content": "System maintenance scheduled for tonight"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/announcement/update
|
||||||
|
|
||||||
|
Update the site announcement (admin only).
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{"content": "New announcement message"}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Configuration Keys
|
||||||
|
|
||||||
|
| Key | Description | Example |
|
||||||
|
|-----|-------------|---------|
|
||||||
|
| `site_name` | Panel display name | `"My FLVX Panel"` |
|
||||||
|
| `site_logo` | Logo URL | `"https://example.com/logo.png"` |
|
||||||
|
| `site_announcement` | Announcement HTML | `"<p>Notice...</p>"` |
|
||||||
|
| `captcha_enabled` | Enable captcha | `"true"` or `"false"` |
|
||||||
|
| `captcha_type` | Captcha provider | `"turnstile"` or `"local"` |
|
||||||
|
| `turnstile_site_key` | Cloudflare Turnstile site key | `"0x4..."` |
|
||||||
|
| `turnstile_secret_key` | Cloudflare Turnstile secret | `"0x4..."` |
|
||||||
|
| `default_user_flow` | Default user traffic (GB) | `"100"` |
|
||||||
|
| `default_user_exp_days` | Default user expiry days | `"30"` |
|
||||||
|
| `default_user_num` | Default max forwards | `"10"` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example: Update Panel Name and Announcement
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/config/update" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"site_name": "Awesome Traffic Panel",
|
||||||
|
"site_announcement": "<strong>Welcome!</strong> New nodes added."
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Enable Cloudflare Turnstile Captcha
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/config/update" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"captcha_enabled": "true",
|
||||||
|
"captcha_type": "turnstile",
|
||||||
|
"turnstile_site_key": "0x4AAAAAAAAjq0JN9YQg",
|
||||||
|
"turnstile_secret_key": "0x4AAAAAAAAjq0JN9YQg_secret"
|
||||||
|
}'
|
||||||
|
```
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
# Error Codes & Handling
|
||||||
|
|
||||||
|
## Response Code Field
|
||||||
|
|
||||||
|
| code | Meaning | Action |
|
||||||
|
|------|---------|--------|
|
||||||
|
| `0` | Success | Use `data` field |
|
||||||
|
| `-1` | Business error | Show `msg` to user |
|
||||||
|
| `-2` | Server/DB error | Retry or report bug |
|
||||||
|
| `401` | Unauthorized | Token expired/invalid, re-login |
|
||||||
|
| `403` | Forbidden | Need admin privileges |
|
||||||
|
|
||||||
|
## Common Error Messages (Chinese)
|
||||||
|
|
||||||
|
| msg | Cause | Solution |
|
||||||
|
|-----|-------|----------|
|
||||||
|
| 用户名或密码错误 | Wrong credentials | Check username/password |
|
||||||
|
| Token已过期 | Token expired | Re-login |
|
||||||
|
| 权限不足 | Need admin | Use admin account (role_id: 0) |
|
||||||
|
| 端口已被占用 | Port in use | Choose different port or delete conflicting forward |
|
||||||
|
| 流量不足 | Out of traffic | Contact admin or upgrade plan |
|
||||||
|
| 节点离线 | Node offline | Check node status, run install command |
|
||||||
|
| 隧道不可用 | Tunnel disabled | Enable tunnel first |
|
||||||
|
| 用户已存在 | Username taken | Choose different username |
|
||||||
|
| 参数错误 | Invalid request | Check request body format |
|
||||||
|
| 转发数量已达上限 | Forward limit reached | Delete unused forwards or contact admin |
|
||||||
|
| 该隧道未分配给当前用户 | No tunnel access | Contact admin to get tunnel assigned |
|
||||||
|
|
||||||
|
## Error Handling Pattern
|
||||||
|
|
||||||
|
### JavaScript/TypeScript
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function callApi<T>(endpoint: string, data: object): Promise<T> {
|
||||||
|
const res = await fetch(`${BASE_URL}${endpoint}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": TOKEN,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await res.json();
|
||||||
|
|
||||||
|
if (result.code === 0) {
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (result.code) {
|
||||||
|
case 401:
|
||||||
|
// Token expired - clear and retry
|
||||||
|
TOKEN = null;
|
||||||
|
throw new Error("登录已过期,请重新登录");
|
||||||
|
case 403:
|
||||||
|
throw new Error("权限不足,需要管理员权限");
|
||||||
|
case -2:
|
||||||
|
throw new Error("服务器错误,请稍后重试");
|
||||||
|
default:
|
||||||
|
throw new Error(result.msg || "操作失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
def call_api(endpoint: str, data: dict = None) -> dict:
|
||||||
|
global TOKEN
|
||||||
|
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
if TOKEN:
|
||||||
|
headers["Authorization"] = TOKEN
|
||||||
|
|
||||||
|
resp = requests.post(f"{BASE_URL}{endpoint}", headers=headers, json=data or {})
|
||||||
|
result = resp.json()
|
||||||
|
|
||||||
|
if result["code"] == 0:
|
||||||
|
return result.get("data")
|
||||||
|
|
||||||
|
if result["code"] == 401:
|
||||||
|
TOKEN = None
|
||||||
|
raise Exception("登录已过期,请重新登录")
|
||||||
|
elif result["code"] == 403:
|
||||||
|
raise Exception("权限不足,需要管理员权限")
|
||||||
|
elif result["code"] == -2:
|
||||||
|
raise Exception("服务器错误,请稍后重试")
|
||||||
|
else:
|
||||||
|
raise Exception(result["msg"] or "操作失败")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bash
|
||||||
|
|
||||||
|
```bash
|
||||||
|
call_api() {
|
||||||
|
local endpoint="$1"
|
||||||
|
local data="$2"
|
||||||
|
|
||||||
|
local response
|
||||||
|
response=$(curl -s -X POST "${FLVX_BASE_URL}${endpoint}" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$data")
|
||||||
|
|
||||||
|
local code
|
||||||
|
code=$(echo "$response" | jq -r '.code')
|
||||||
|
|
||||||
|
if [ "$code" == "0" ]; then
|
||||||
|
echo "$response" | jq '.data'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local msg
|
||||||
|
msg=$(echo "$response" | jq -r '.msg')
|
||||||
|
|
||||||
|
case "$code" in
|
||||||
|
401) echo "Error: 登录已过期" >&2 ;;
|
||||||
|
403) echo "Error: 权限不足" >&2 ;;
|
||||||
|
-2) echo "Error: 服务器错误" >&2 ;;
|
||||||
|
*) echo "Error: $msg" >&2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Retry Logic with Auto Re-login
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function callApiWithRetry<T>(
|
||||||
|
endpoint: string,
|
||||||
|
data: object,
|
||||||
|
maxRetries = 1
|
||||||
|
): Promise<T> {
|
||||||
|
let lastError: Error;
|
||||||
|
|
||||||
|
for (let i = 0; i <= maxRetries; i++) {
|
||||||
|
try {
|
||||||
|
if (!TOKEN) {
|
||||||
|
await login();
|
||||||
|
}
|
||||||
|
return await callApi<T>(endpoint, data);
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
if (error.message.includes("过期") || error.message.includes("expired")) {
|
||||||
|
TOKEN = null; // Force re-login on next attempt
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError!;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation Errors
|
||||||
|
|
||||||
|
When request validation fails, the API returns code -1 with specific messages:
|
||||||
|
|
||||||
|
| Scenario | Error Message |
|
||||||
|
|----------|--------------|
|
||||||
|
| Missing required field | `参数错误` or field-specific message |
|
||||||
|
| Invalid port range | `端口范围无效` |
|
||||||
|
| Invalid IP format | `IP地址格式错误` |
|
||||||
|
| Invalid date | `时间格式错误` |
|
||||||
|
| Username too short | `用户名长度不能少于3个字符` |
|
||||||
|
| Password too weak | `密码长度不能少于6个字符` |
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
# curl Examples
|
||||||
|
|
||||||
|
Quick reference for common operations using curl.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set environment variables
|
||||||
|
export FLVX_BASE_URL="https://your-panel.example.com"
|
||||||
|
export FLVX_USERNAME="admin"
|
||||||
|
export FLVX_PASSWORD="your-password"
|
||||||
|
|
||||||
|
# Login and save token
|
||||||
|
TOKEN=$(curl -s -X POST "${FLVX_BASE_URL}/api/v1/user/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"username\":\"${FLVX_USERNAME}\",\"password\":\"${FLVX_PASSWORD}\"}" \
|
||||||
|
| jq -r '.data.token')
|
||||||
|
|
||||||
|
echo "Token: ${TOKEN:0:20}..."
|
||||||
|
```
|
||||||
|
|
||||||
|
## User Operations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get my package info
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/user/package" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}' | jq '.'
|
||||||
|
|
||||||
|
# List all users (admin)
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/user/list" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"page":1,"pageSize":20}' | jq '.'
|
||||||
|
|
||||||
|
# Create user (admin)
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/user/create" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"user": "alice",
|
||||||
|
"pwd": "SecurePass123!",
|
||||||
|
"name": "Alice",
|
||||||
|
"flow": 50,
|
||||||
|
"num": 10,
|
||||||
|
"expTime": 1767225600000
|
||||||
|
}' | jq '.'
|
||||||
|
|
||||||
|
# Reset user traffic
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/user/reset" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"id":2,"type":"user"}' | jq '.'
|
||||||
|
|
||||||
|
# Delete user
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/user/delete" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"id":2}' | jq '.'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Node Operations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List nodes with status
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/node/list" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}' | jq '.data.list[] | {name, status: (.status == 1), ip: .server_ip}'
|
||||||
|
|
||||||
|
# Create node
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/node/create" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"US-Node-1","serverIp":"203.0.113.10"}' | jq '.'
|
||||||
|
|
||||||
|
# Get install command
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/node/install" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"id":2}' | jq -r '.data.command'
|
||||||
|
|
||||||
|
# Check node status
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/node/check-status" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}' | jq '.'
|
||||||
|
|
||||||
|
# Upgrade node
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/node/upgrade" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"id":2,"version":"2.1.5"}' | jq '.'
|
||||||
|
|
||||||
|
# Delete node
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/node/delete" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"id":2}' | jq '.'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tunnel Operations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List tunnels
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/tunnel/list" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}' | jq '.data.list[] | {id, name, status}'
|
||||||
|
|
||||||
|
# Create tunnel
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/tunnel/create" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "HK-US-Tunnel",
|
||||||
|
"type": 1,
|
||||||
|
"inNodeId": [1],
|
||||||
|
"outNodeId": [2]
|
||||||
|
}' | jq '.'
|
||||||
|
|
||||||
|
# Assign tunnel to user
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/tunnel/user/assign" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"userId":2,"tunnelId":1,"flow":30}' | jq '.'
|
||||||
|
|
||||||
|
# Get available tunnels (for current user)
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/tunnel/user/tunnel" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}' | jq '.'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Forward Operations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List forwards with traffic
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/forward/list" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}' | jq '.data.list[] | {
|
||||||
|
name,
|
||||||
|
tunnel: .tunnel_name,
|
||||||
|
port: .in_port,
|
||||||
|
target: .remote_addr,
|
||||||
|
status: (if .status == 1 then "running" else "paused" end),
|
||||||
|
upload_gb: ((.in_flow / 1073741824) | floor),
|
||||||
|
download_gb: ((.out_flow / 1073741824) | floor)
|
||||||
|
}'
|
||||||
|
|
||||||
|
# Create forward
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/forward/create" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "my-web-server",
|
||||||
|
"tunnelId": 1,
|
||||||
|
"remoteAddr": "192.168.1.100:80",
|
||||||
|
"strategy": "fifo"
|
||||||
|
}' | jq '.'
|
||||||
|
|
||||||
|
# Create forward with load balancing
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/forward/create" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "web-cluster",
|
||||||
|
"tunnelId": 1,
|
||||||
|
"remoteAddr": "10.0.0.1:80,10.0.0.2:80,10.0.0.3:80",
|
||||||
|
"strategy": "round"
|
||||||
|
}' | jq '.'
|
||||||
|
|
||||||
|
# Pause forward
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/forward/pause" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"id":1}' | jq '.'
|
||||||
|
|
||||||
|
# Resume forward
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/forward/resume" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"id":1}' | jq '.'
|
||||||
|
|
||||||
|
# Diagnose forward
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/forward/diagnose" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"id":1}' | jq '.'
|
||||||
|
|
||||||
|
# Delete forward
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/forward/delete" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"id":1}' | jq '.'
|
||||||
|
|
||||||
|
# Batch pause forwards
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/forward/batch-pause" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"ids":[1,2,3]}' | jq '.'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backup Operations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Export all data
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/backup/export" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}' > backup-$(date +%Y%m%d).json
|
||||||
|
|
||||||
|
# Export specific types
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/backup/export" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"types":["users","tunnels"]}' > partial-backup.json
|
||||||
|
|
||||||
|
# Import backup
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/backup/import" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d @backup-20260226.json | jq '.'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Helper Functions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add to ~/.bashrc or ~/.zshrc
|
||||||
|
|
||||||
|
flvx-login() {
|
||||||
|
export FLVX_BASE_URL="${1:-$FLVX_BASE_URL}"
|
||||||
|
TOKEN=$(curl -s -X POST "${FLVX_BASE_URL}/api/v1/user/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"username\":\"${FLVX_USERNAME}\",\"password\":\"${FLVX_PASSWORD}\"}" \
|
||||||
|
| jq -r '.data.token')
|
||||||
|
export FLVX_TOKEN="$TOKEN"
|
||||||
|
echo "Logged in. Token: ${TOKEN:0:20}..."
|
||||||
|
}
|
||||||
|
|
||||||
|
flvx-api() {
|
||||||
|
local endpoint="$1"
|
||||||
|
local data="${2:-{}}"
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}${endpoint}" \
|
||||||
|
-H "Authorization: ${FLVX_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$data" | jq '.'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Usage:
|
||||||
|
# flvx-login
|
||||||
|
# flvx-api /api/v1/node/list
|
||||||
|
# flvx-api /api/v1/forward/list '{"keyword":"web"}'
|
||||||
|
```
|
||||||
@@ -0,0 +1,603 @@
|
|||||||
|
# HTTP Client Examples
|
||||||
|
|
||||||
|
Complete, runnable examples for various languages.
|
||||||
|
|
||||||
|
## Bash / curl
|
||||||
|
|
||||||
|
### Complete Script with Auto-Login
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
BASE_URL="${FLVX_BASE_URL:?FLVX_BASE_URL not set}"
|
||||||
|
USERNAME="${FLVX_USERNAME:?FLVX_USERNAME not set}"
|
||||||
|
PASSWORD="${FLVX_PASSWORD:?FLVX_PASSWORD not set}"
|
||||||
|
|
||||||
|
# Login and get token
|
||||||
|
echo "Logging in..."
|
||||||
|
LOGIN_RESPONSE=$(curl -s -X POST "${BASE_URL}/api/v1/user/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"username\":\"${USERNAME}\",\"password\":\"${PASSWORD}\"}")
|
||||||
|
|
||||||
|
TOKEN=$(echo "$LOGIN_RESPONSE" | jq -r '.data.token // empty')
|
||||||
|
|
||||||
|
if [ -z "$TOKEN" ]; then
|
||||||
|
echo "Login failed: $(echo "$LOGIN_RESPONSE" | jq -r '.msg')"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Logged in successfully"
|
||||||
|
|
||||||
|
# API call helper
|
||||||
|
api_call() {
|
||||||
|
local endpoint="$1"
|
||||||
|
local data="${2:-{}}"
|
||||||
|
|
||||||
|
curl -s -X POST "${BASE_URL}${endpoint}" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$data"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
echo "=== My Package Info ==="
|
||||||
|
api_call "/api/v1/user/package" | jq '.'
|
||||||
|
|
||||||
|
echo -e "\n=== Node List ==="
|
||||||
|
api_call "/api/v1/node/list" '{}' | jq '.data.list[] | {name, status: (.status == 1)}'
|
||||||
|
|
||||||
|
echo -e "\n=== Forward List ==="
|
||||||
|
api_call "/api/v1/forward/list" '{}' | jq '.data.list[] | {name, tunnel: .tunnel_name, port: .in_port, target: .remote_addr}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create Forward Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
BASE_URL="${FLVX_BASE_URL}"
|
||||||
|
TOKEN="${FLVX_TOKEN}" # Pre-obtained token
|
||||||
|
|
||||||
|
create_forward() {
|
||||||
|
local name="$1"
|
||||||
|
local tunnel_id="$2"
|
||||||
|
local remote_addr="$3"
|
||||||
|
|
||||||
|
curl -s -X POST "${BASE_URL}/api/v1/forward/create" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"name\": \"${name}\",
|
||||||
|
\"tunnelId\": ${tunnel_id},
|
||||||
|
\"remoteAddr\": \"${remote_addr}\",
|
||||||
|
\"strategy\": \"fifo\"
|
||||||
|
}" | jq '.'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Usage: ./create-forward.sh "my-web" 1 "192.168.1.100:80"
|
||||||
|
create_forward "$@"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Python
|
||||||
|
|
||||||
|
### Complete Client Class
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""FLVX API Client"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
from typing import Optional, Any, Dict, List
|
||||||
|
|
||||||
|
class FlvxError(Exception):
|
||||||
|
"""FLVX API Error"""
|
||||||
|
def __init__(self, code: int, message: str):
|
||||||
|
self.code = code
|
||||||
|
self.message = message
|
||||||
|
super().__init__(message)
|
||||||
|
|
||||||
|
class FlvxClient:
|
||||||
|
"""FLVX API Client with auto-login"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
base_url: Optional[str] = None,
|
||||||
|
username: Optional[str] = None,
|
||||||
|
password: Optional[str] = None
|
||||||
|
):
|
||||||
|
self.base_url = base_url or os.environ.get("FLVX_BASE_URL")
|
||||||
|
self.username = username or os.environ.get("FLVX_USERNAME")
|
||||||
|
self.password = password or os.environ.get("FLVX_PASSWORD")
|
||||||
|
|
||||||
|
if not all([self.base_url, self.username, self.password]):
|
||||||
|
raise ValueError("Missing credentials. Set FLVX_BASE_URL, FLVX_USERNAME, FLVX_PASSWORD")
|
||||||
|
|
||||||
|
self.token: Optional[str] = None
|
||||||
|
|
||||||
|
def _login(self) -> None:
|
||||||
|
"""Authenticate and store token"""
|
||||||
|
resp = requests.post(
|
||||||
|
f"{self.base_url}/api/v1/user/login",
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
json={"username": self.username, "password": self.password}
|
||||||
|
)
|
||||||
|
result = resp.json()
|
||||||
|
|
||||||
|
if result["code"] != 0:
|
||||||
|
raise FlvxError(result["code"], result["msg"])
|
||||||
|
|
||||||
|
self.token = result["data"]["token"]
|
||||||
|
|
||||||
|
def _headers(self) -> Dict[str, str]:
|
||||||
|
"""Get request headers with auth"""
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
if self.token:
|
||||||
|
headers["Authorization"] = self.token # NO "Bearer " prefix!
|
||||||
|
return headers
|
||||||
|
|
||||||
|
def request(self, endpoint: str, data: Any = None) -> Any:
|
||||||
|
"""Make authenticated API request"""
|
||||||
|
if not self.token:
|
||||||
|
self._login()
|
||||||
|
|
||||||
|
resp = requests.post(
|
||||||
|
f"{self.base_url}{endpoint}",
|
||||||
|
headers=self._headers(),
|
||||||
|
json=data or {}
|
||||||
|
)
|
||||||
|
result = resp.json()
|
||||||
|
|
||||||
|
if result["code"] == 0:
|
||||||
|
return result.get("data")
|
||||||
|
|
||||||
|
if result["code"] == 401:
|
||||||
|
# Token expired, retry once
|
||||||
|
self.token = None
|
||||||
|
return self.request(endpoint, data)
|
||||||
|
|
||||||
|
raise FlvxError(result["code"], result["msg"])
|
||||||
|
|
||||||
|
# Convenience methods
|
||||||
|
|
||||||
|
def get_package(self) -> Dict:
|
||||||
|
"""Get current user's package info"""
|
||||||
|
return self.request("/api/v1/user/package", {})
|
||||||
|
|
||||||
|
def list_nodes(self) -> List[Dict]:
|
||||||
|
"""List all nodes"""
|
||||||
|
data = self.request("/api/v1/node/list", {})
|
||||||
|
return data.get("list", [])
|
||||||
|
|
||||||
|
def list_forwards(self, keyword: str = "") -> List[Dict]:
|
||||||
|
"""List forwards"""
|
||||||
|
data = self.request("/api/v1/forward/list", {"keyword": keyword})
|
||||||
|
return data.get("list", [])
|
||||||
|
|
||||||
|
def create_forward(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
tunnel_id: int,
|
||||||
|
remote_addr: str,
|
||||||
|
strategy: str = "fifo",
|
||||||
|
speed_id: int = 0
|
||||||
|
) -> Dict:
|
||||||
|
"""Create a forward"""
|
||||||
|
return self.request("/api/v1/forward/create", {
|
||||||
|
"name": name,
|
||||||
|
"tunnelId": tunnel_id,
|
||||||
|
"remoteAddr": remote_addr,
|
||||||
|
"strategy": strategy,
|
||||||
|
"speedId": speed_id
|
||||||
|
})
|
||||||
|
|
||||||
|
def pause_forward(self, forward_id: int) -> None:
|
||||||
|
"""Pause a forward"""
|
||||||
|
self.request("/api/v1/forward/pause", {"id": forward_id})
|
||||||
|
|
||||||
|
def resume_forward(self, forward_id: int) -> None:
|
||||||
|
"""Resume a forward"""
|
||||||
|
self.request("/api/v1/forward/resume", {"id": forward_id})
|
||||||
|
|
||||||
|
def delete_forward(self, forward_id: int) -> None:
|
||||||
|
"""Delete a forward"""
|
||||||
|
self.request("/api/v1/forward/delete", {"id": forward_id})
|
||||||
|
|
||||||
|
|
||||||
|
# Usage example
|
||||||
|
if __name__ == "__main__":
|
||||||
|
client = FlvxClient()
|
||||||
|
|
||||||
|
# Get package info
|
||||||
|
pkg = client.get_package()
|
||||||
|
print(f"Traffic: {pkg['inFlow'] / 1e9:.2f}GB ↑ / {pkg['outFlow'] / 1e9:.2f}GB ↓")
|
||||||
|
print(f"Quota: {pkg['flow']}GB")
|
||||||
|
|
||||||
|
# List forwards with traffic
|
||||||
|
print("\nForwards:")
|
||||||
|
for fwd in client.list_forwards():
|
||||||
|
print(f" {fwd['name']}: {fwd['in_port']} → {fwd['remote_addr']}")
|
||||||
|
print(f" Traffic: {fwd['in_flow'] / 1e9:.2f}GB ↑ / {fwd['out_flow'] / 1e9:.2f}GB ↓")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Node.js / TypeScript
|
||||||
|
|
||||||
|
### Complete Client Class
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// flvx-client.ts
|
||||||
|
interface APIResponse<T = unknown> {
|
||||||
|
code: number;
|
||||||
|
msg: string;
|
||||||
|
data?: T;
|
||||||
|
ts: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FlvxError extends Error {
|
||||||
|
constructor(public code: number, message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "FlvxError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserPackage {
|
||||||
|
flow: number;
|
||||||
|
inFlow: number;
|
||||||
|
outFlow: number;
|
||||||
|
tunnels: number;
|
||||||
|
forwards: number;
|
||||||
|
expTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Node {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
status: number;
|
||||||
|
server_ip: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Forward {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
tunnel_id: number;
|
||||||
|
tunnel_name: string;
|
||||||
|
in_port: number;
|
||||||
|
remote_addr: string;
|
||||||
|
status: number;
|
||||||
|
in_flow: number;
|
||||||
|
out_flow: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FlvxClient {
|
||||||
|
private baseUrl: string;
|
||||||
|
private username: string;
|
||||||
|
private password: string;
|
||||||
|
private token?: string;
|
||||||
|
|
||||||
|
constructor(options?: {
|
||||||
|
baseUrl?: string;
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
}) {
|
||||||
|
this.baseUrl = options?.baseUrl ?? process.env.FLVX_BASE_URL ?? "";
|
||||||
|
this.username = options?.username ?? process.env.FLVX_USERNAME ?? "";
|
||||||
|
this.password = options?.password ?? process.env.FLVX_PASSWORD ?? "";
|
||||||
|
|
||||||
|
if (!this.baseUrl || !this.username || !this.password) {
|
||||||
|
throw new Error("Missing credentials. Set FLVX_BASE_URL, FLVX_USERNAME, FLVX_PASSWORD");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async login(): Promise<void> {
|
||||||
|
const res = await fetch(`${this.baseUrl}/api/v1/user/login`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: this.username,
|
||||||
|
password: this.password,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result: APIResponse<{ token: string }> = await res.json();
|
||||||
|
if (result.code !== 0) {
|
||||||
|
throw new FlvxError(result.code, result.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.token = result.data!.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async request<T>(endpoint: string, data?: object): Promise<T> {
|
||||||
|
if (!this.token) {
|
||||||
|
await this.login();
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(`${this.baseUrl}${endpoint}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: this.token!, // NO "Bearer " prefix!
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data ?? {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result: APIResponse<T> = await res.json();
|
||||||
|
|
||||||
|
if (result.code === 0) {
|
||||||
|
return result.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.code === 401) {
|
||||||
|
// Token expired, retry once
|
||||||
|
this.token = undefined;
|
||||||
|
return this.request<T>(endpoint, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new FlvxError(result.code, result.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience methods
|
||||||
|
|
||||||
|
async getPackage(): Promise<UserPackage> {
|
||||||
|
return this.request("/api/v1/user/package", {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async listNodes(): Promise<Node[]> {
|
||||||
|
const data = await this.request<{ list: Node[] }>("/api/v1/node/list", {});
|
||||||
|
return data.list ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async listForwards(keyword = ""): Promise<Forward[]> {
|
||||||
|
const data = await this.request<{ list: Forward[] }>("/api/v1/forward/list", {
|
||||||
|
keyword,
|
||||||
|
});
|
||||||
|
return data.list ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async createForward(options: {
|
||||||
|
name: string;
|
||||||
|
tunnelId: number;
|
||||||
|
remoteAddr: string;
|
||||||
|
strategy?: "fifo" | "round";
|
||||||
|
speedId?: number;
|
||||||
|
}): Promise<Forward> {
|
||||||
|
return this.request("/api/v1/forward/create", {
|
||||||
|
name: options.name,
|
||||||
|
tunnelId: options.tunnelId,
|
||||||
|
remoteAddr: options.remoteAddr,
|
||||||
|
strategy: options.strategy ?? "fifo",
|
||||||
|
speedId: options.speedId ?? 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async pauseForward(id: number): Promise<void> {
|
||||||
|
await this.request("/api/v1/forward/pause", { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
async resumeForward(id: number): Promise<void> {
|
||||||
|
await this.request("/api/v1/forward/resume", { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteForward(id: number): Promise<void> {
|
||||||
|
await this.request("/api/v1/forward/delete", { id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { FlvxClient, FlvxError };
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
async function main() {
|
||||||
|
const client = new FlvxClient();
|
||||||
|
|
||||||
|
// Get package info
|
||||||
|
const pkg = await client.getPackage();
|
||||||
|
console.log(`Traffic: ${(pkg.inFlow / 1e9).toFixed(2)}GB ↑ / ${(pkg.outFlow / 1e9).toFixed(2)}GB ↓`);
|
||||||
|
console.log(`Quota: ${pkg.flow}GB`);
|
||||||
|
|
||||||
|
// List nodes
|
||||||
|
console.log("\nNodes:");
|
||||||
|
const nodes = await client.listNodes();
|
||||||
|
for (const node of nodes) {
|
||||||
|
console.log(` ${node.name}: ${node.status ? "Online" : "Offline"}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// List forwards
|
||||||
|
console.log("\nForwards:");
|
||||||
|
const forwards = await client.listForwards();
|
||||||
|
for (const fwd of forwards) {
|
||||||
|
console.log(` ${fwd.name}: ${fwd.in_port} → ${fwd.remote_addr}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Go
|
||||||
|
|
||||||
|
### Complete Client Package
|
||||||
|
|
||||||
|
```go
|
||||||
|
// flvx/client.go
|
||||||
|
package flvx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
BaseURL string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
Token string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
TS int64 `json:"ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FlvxError struct {
|
||||||
|
Code int
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FlvxError) Error() string {
|
||||||
|
return fmt.Sprintf("FLVX error %d: %s", e.Code, e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient() *Client {
|
||||||
|
return &Client{
|
||||||
|
BaseURL: os.Getenv("FLVX_BASE_URL"),
|
||||||
|
Username: os.Getenv("FLVX_USERNAME"),
|
||||||
|
Password: os.Getenv("FLVX_PASSWORD"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Login() error {
|
||||||
|
payload := map[string]string{
|
||||||
|
"username": c.Username,
|
||||||
|
"password": c.Password,
|
||||||
|
}
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Data struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.request("/api/v1/user/login", payload, &result); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Code != 0 {
|
||||||
|
return &FlvxError{Code: result.Code, Message: result.Msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Token = result.Data.Token
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Request(endpoint string, data interface{}, result interface{}) error {
|
||||||
|
// Auto-login if no token
|
||||||
|
if c.Token == "" {
|
||||||
|
if err := c.Login(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.request(endpoint, data, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) request(endpoint string, data interface{}, result interface{}) error {
|
||||||
|
body, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", c.BaseURL+endpoint, bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
if c.Token != "" {
|
||||||
|
req.Header.Set("Authorization", c.Token) // NO "Bearer " prefix!
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Unmarshal(respBody, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience methods
|
||||||
|
|
||||||
|
func (c *Client) ListNodes() ([]map[string]interface{}, error) {
|
||||||
|
var result struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Data struct {
|
||||||
|
List []map[string]interface{} `json:"list"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.Request("/api/v1/node/list", map[string]interface{}{}, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Code != 0 {
|
||||||
|
return nil, &FlvxError{Code: result.Code, Message: "failed to list nodes"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Data.List, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) CreateForward(name string, tunnelID int, remoteAddr string) (map[string]interface{}, error) {
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"name": name,
|
||||||
|
"tunnelId": tunnelID,
|
||||||
|
"remoteAddr": remoteAddr,
|
||||||
|
"strategy": "fifo",
|
||||||
|
}
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Data map[string]interface{} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.Request("/api/v1/forward/create", payload, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Code != 0 {
|
||||||
|
return nil, &FlvxError{Code: result.Code, Message: result.Msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage example
|
||||||
|
func Example() {
|
||||||
|
client := NewClient()
|
||||||
|
|
||||||
|
nodes, err := client.ListNodes()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, node := range nodes {
|
||||||
|
fmt.Printf("Node: %v (status: %v)\n", node["name"], node["status"])
|
||||||
|
}
|
||||||
|
|
||||||
|
fwd, err := client.CreateForward("my-forward", 1, "192.168.1.100:80")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Created forward: %v\n", fwd)
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
# Federation / Clustering API
|
||||||
|
|
||||||
|
Federation allows sharing nodes between FLVX panels. One panel can share nodes, and another panel can use them as remote nodes.
|
||||||
|
|
||||||
|
## Share Management (Admin)
|
||||||
|
|
||||||
|
### POST /api/v1/federation/share/list
|
||||||
|
|
||||||
|
List all peer shares.
|
||||||
|
|
||||||
|
**Request:** `{}`
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Share-to-Partner",
|
||||||
|
"node_id": 1,
|
||||||
|
"node_name": "HK-Node-1",
|
||||||
|
"token": "share-token-abc123",
|
||||||
|
"max_bandwidth": 107374182400,
|
||||||
|
"expiry_time": 1767225600000,
|
||||||
|
"port_range_start": 10000,
|
||||||
|
"port_range_end": 20000,
|
||||||
|
"allowed_domains": "example.com,api.example.com",
|
||||||
|
"allowed_ips": "10.0.0.0/8,192.168.0.0/16",
|
||||||
|
"status": 1,
|
||||||
|
"created_at": 1706659200000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/federation/share/create
|
||||||
|
|
||||||
|
Create a peer share (share a node with another panel).
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Share-to-Partner",
|
||||||
|
"nodeId": 1,
|
||||||
|
"maxBandwidth": 107374182400,
|
||||||
|
"expiryTime": 1767225600000,
|
||||||
|
"portRangeStart": 10000,
|
||||||
|
"portRangeEnd": 20000,
|
||||||
|
"allowedDomains": "example.com,api.example.com",
|
||||||
|
"allowedIps": "10.0.0.0/8,192.168.0.0/16"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fields:**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| name | string | Yes | Share name |
|
||||||
|
| nodeId | number | Yes | Node to share |
|
||||||
|
| maxBandwidth | number | No | Max traffic in bytes (0 = unlimited) |
|
||||||
|
| expiryTime | number | No | Expiry timestamp in ms (0 = never) |
|
||||||
|
| portRangeStart | number | No | Allowed port range start |
|
||||||
|
| portRangeEnd | number | No | Allowed port range end |
|
||||||
|
| allowedDomains | string | No | Comma-separated domains |
|
||||||
|
| allowedIps | string | No | Comma-separated IPs/CIDRs |
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"token": "share-token-abc123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `token` is what the remote panel uses to connect.
|
||||||
|
|
||||||
|
### POST /api/v1/federation/share/update
|
||||||
|
|
||||||
|
Update a peer share.
|
||||||
|
|
||||||
|
**Request:** Same as create, with `id` field required.
|
||||||
|
|
||||||
|
### POST /api/v1/federation/share/delete
|
||||||
|
|
||||||
|
Delete a peer share.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{"id": 1}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/federation/share/reset-flow
|
||||||
|
|
||||||
|
Reset traffic counter for a share.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{"id": 1}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/federation/share/remote-usage/list
|
||||||
|
|
||||||
|
List remote node usage statistics.
|
||||||
|
|
||||||
|
**Request:** `{}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Federation Runtime (Peer-to-Peer)
|
||||||
|
|
||||||
|
These endpoints use **Bearer token authentication** (different from JWT).
|
||||||
|
|
||||||
|
### POST /api/v1/federation/connect
|
||||||
|
|
||||||
|
Connect to a remote panel and get share info.
|
||||||
|
|
||||||
|
**Headers:**
|
||||||
|
```
|
||||||
|
Authorization: Bearer <share-token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request:** `{}`
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"nodeName": "HK-Node-1",
|
||||||
|
"allowedPorts": [10000, 20000],
|
||||||
|
"allowedDomains": ["example.com"],
|
||||||
|
"allowedIps": ["10.0.0.0/8"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/federation/tunnel/create
|
||||||
|
|
||||||
|
Create a federation tunnel on the remote node.
|
||||||
|
|
||||||
|
**Headers:**
|
||||||
|
```
|
||||||
|
Authorization: Bearer <share-token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tunnelId": 1,
|
||||||
|
"role": "entry"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/federation/runtime/reserve-port
|
||||||
|
|
||||||
|
Reserve a port on the remote node.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"port": 15000,
|
||||||
|
"tunnelId": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/federation/runtime/apply-role
|
||||||
|
|
||||||
|
Apply for a role (entry/chain/exit) on the remote node.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tunnelId": 1,
|
||||||
|
"role": "exit"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/federation/runtime/release-role
|
||||||
|
|
||||||
|
Release a role on the remote node.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tunnelId": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/federation/runtime/diagnose
|
||||||
|
|
||||||
|
TCP ping diagnostics from remote node to target.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"target": "10.0.0.1:80"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/federation/runtime/command
|
||||||
|
|
||||||
|
Execute a command on the remote node.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"command": "status"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Node Import (Admin)
|
||||||
|
|
||||||
|
### POST /api/v1/federation/node/import
|
||||||
|
|
||||||
|
Import a remote node from another panel.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Remote-HK-Node",
|
||||||
|
"remoteUrl": "https://other-panel.example.com",
|
||||||
|
"remoteToken": "share-token-abc123"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates a node with `is_remote: 1`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow: Share Node with Another Panel
|
||||||
|
|
||||||
|
**On the sharing panel (Panel A):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Create a share
|
||||||
|
SHARE_RESP=$(curl -s -X POST "${FLVX_BASE_URL}/api/v1/federation/share/create" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "Share-HK-Node",
|
||||||
|
"nodeId": 1,
|
||||||
|
"portRangeStart": 10000,
|
||||||
|
"portRangeEnd": 20000,
|
||||||
|
"allowedIps": "0.0.0.0/0"
|
||||||
|
}')
|
||||||
|
|
||||||
|
SHARE_TOKEN=$(echo "$SHARE_RESP" | jq -r '.data.token')
|
||||||
|
echo "Share Token: $SHARE_TOKEN"
|
||||||
|
echo "Panel URL: ${FLVX_BASE_URL}"
|
||||||
|
```
|
||||||
|
|
||||||
|
**On the receiving panel (Panel B):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 2. Import the remote node
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/federation/node/import" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "Remote-HK-Node",
|
||||||
|
"remoteUrl": "https://panel-a.example.com",
|
||||||
|
"remoteToken": "share-token-abc123"
|
||||||
|
}'
|
||||||
|
|
||||||
|
# 3. Use the remote node in tunnels like a local node
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/tunnel/create" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "Federated-Tunnel",
|
||||||
|
"type": 1,
|
||||||
|
"inNodeId": [1],
|
||||||
|
"outNodeId": [2]
|
||||||
|
}'
|
||||||
|
```
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
# Forward Management API
|
||||||
|
|
||||||
|
Forwards are port forwarding rules created by users on their assigned tunnels.
|
||||||
|
|
||||||
|
## POST /api/v1/forward/list
|
||||||
|
|
||||||
|
List forwards. Non-admin users see only their own forwards.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"page": 1,
|
||||||
|
"pageSize": 20,
|
||||||
|
"keyword": "",
|
||||||
|
"status": -1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**status filter:**
|
||||||
|
- `-1` = All
|
||||||
|
- `0` = Paused
|
||||||
|
- `1` = Running
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"user_id": 2,
|
||||||
|
"tunnel_id": 1,
|
||||||
|
"tunnel_name": "HK-US-Tunnel",
|
||||||
|
"name": "my-web-server",
|
||||||
|
"in_port": 10001,
|
||||||
|
"remote_addr": "192.168.1.100:80",
|
||||||
|
"strategy": "fifo",
|
||||||
|
"status": 1,
|
||||||
|
"speed_id": 0,
|
||||||
|
"speed_name": "",
|
||||||
|
"in_flow": 1073741824,
|
||||||
|
"out_flow": 2147483648,
|
||||||
|
"created_at": 1706659200000,
|
||||||
|
"updated_at": 1706659200000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/forward/create
|
||||||
|
|
||||||
|
Create a new forward.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "my-web-server",
|
||||||
|
"tunnelId": 1,
|
||||||
|
"remoteAddr": "192.168.1.100:80",
|
||||||
|
"strategy": "fifo",
|
||||||
|
"inPort": 0,
|
||||||
|
"speedId": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fields:**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| name | string | Yes | Forward name |
|
||||||
|
| tunnelId | number | Yes | Tunnel to use |
|
||||||
|
| remoteAddr | string | Yes | Target address(es), comma-separated for load balancing |
|
||||||
|
| strategy | string | No | "fifo" or "round" (default: "fifo") |
|
||||||
|
| inPort | number | No | Entry port (0 = auto-assign) |
|
||||||
|
| speedId | number | No | Speed limit rule ID (0 = no limit) |
|
||||||
|
|
||||||
|
**Strategy:**
|
||||||
|
- `fifo` = First target only
|
||||||
|
- `round` = Round-robin load balancing across targets
|
||||||
|
|
||||||
|
**Remote Address Format:**
|
||||||
|
- Single: `192.168.1.100:80`
|
||||||
|
- Multiple: `192.168.1.100:80,192.168.1.101:80,192.168.1.102:80`
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"in_port": 10001
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/forward/update
|
||||||
|
|
||||||
|
Update forward settings.
|
||||||
|
|
||||||
|
**Request:** Same as create, with `id` field required.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "my-web-server-updated",
|
||||||
|
"remoteAddr": "192.168.1.100:8080",
|
||||||
|
"strategy": "round",
|
||||||
|
"speedId": 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/forward/delete
|
||||||
|
|
||||||
|
Delete a forward.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{"id": 1}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/forward/force-delete
|
||||||
|
|
||||||
|
Force delete a forward (even if in use).
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{"id": 1}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/forward/pause
|
||||||
|
|
||||||
|
Pause a forward (stops traffic but keeps configuration).
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{"id": 1}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{"code": 0, "msg": "success"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/forward/resume
|
||||||
|
|
||||||
|
Resume a paused forward.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{"id": 1}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/forward/diagnose
|
||||||
|
|
||||||
|
Diagnose forward connectivity (TCP ping to target).
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{"id": 1}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"reachable": true,
|
||||||
|
"latency_ms": 15,
|
||||||
|
"error": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/forward/update-order
|
||||||
|
|
||||||
|
Reorder forwards.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"orders": [
|
||||||
|
{"id": 1, "order": 0},
|
||||||
|
{"id": 2, "order": 1}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Batch Operations
|
||||||
|
|
||||||
|
### POST /api/v1/forward/batch-delete
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"ids": [1, 2, 3]}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/forward/batch-pause
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"ids": [1, 2, 3]}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/forward/batch-resume
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"ids": [1, 2, 3]}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/forward/batch-redeploy
|
||||||
|
|
||||||
|
Recreate forwarding services on nodes.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"ids": [1, 2, 3]}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/forward/batch-change-tunnel
|
||||||
|
|
||||||
|
Move forwards to a different tunnel.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ids": [1, 2, 3],
|
||||||
|
"tunnelId": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Traffic Units
|
||||||
|
|
||||||
|
| Field | Unit | Notes |
|
||||||
|
|-------|------|-------|
|
||||||
|
| in_flow | Bytes | Upload traffic |
|
||||||
|
| out_flow | Bytes | Download traffic |
|
||||||
|
|
||||||
|
Convert to GB: `in_flow / 1073741824`
|
||||||
|
|
||||||
|
## Example: Create Forward with Load Balancing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create forward with 3 backend servers
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/forward/create" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "web-cluster",
|
||||||
|
"tunnelId": 1,
|
||||||
|
"remoteAddr": "10.0.0.1:80,10.0.0.2:80,10.0.0.3:80",
|
||||||
|
"strategy": "round"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Check Forward Status and Traffic
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/forward/list" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}' | jq '.data.list[] | {
|
||||||
|
name,
|
||||||
|
tunnel: .tunnel_name,
|
||||||
|
entry_port: .in_port,
|
||||||
|
target: .remote_addr,
|
||||||
|
status: (if .status == 1 then "running" else "paused" end),
|
||||||
|
upload_gb: (.in_flow / 1073741824 | floor),
|
||||||
|
download_gb: (.out_flow / 1073741824 | floor)
|
||||||
|
}'
|
||||||
|
```
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
# Group & Permission Management API
|
||||||
|
|
||||||
|
Groups organize users and tunnels, with permissions controlling access.
|
||||||
|
|
||||||
|
## Tunnel Groups
|
||||||
|
|
||||||
|
### POST /api/v1/group/tunnel/list
|
||||||
|
|
||||||
|
List all tunnel groups.
|
||||||
|
|
||||||
|
**Request:** `{}`
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Premium-Tunnels",
|
||||||
|
"status": 1,
|
||||||
|
"tunnel_ids": [1, 2, 3],
|
||||||
|
"created_at": 1706659200000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/group/tunnel/create
|
||||||
|
|
||||||
|
Create a tunnel group.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Premium-Tunnels",
|
||||||
|
"status": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/group/tunnel/update
|
||||||
|
|
||||||
|
Update tunnel group.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "VIP-Tunnels",
|
||||||
|
"status": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/group/tunnel/delete
|
||||||
|
|
||||||
|
Delete tunnel group.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{"id": 1}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/group/tunnel/assign
|
||||||
|
|
||||||
|
Assign tunnels to a group.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"groupId": 1,
|
||||||
|
"tunnelIds": [1, 2, 3]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Groups
|
||||||
|
|
||||||
|
### POST /api/v1/group/user/list
|
||||||
|
|
||||||
|
List all user groups.
|
||||||
|
|
||||||
|
**Request:** `{}`
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "VIP-Users",
|
||||||
|
"status": 1,
|
||||||
|
"user_ids": [2, 3, 4],
|
||||||
|
"created_at": 1706659200000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/group/user/create
|
||||||
|
|
||||||
|
Create a user group.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "VIP-Users",
|
||||||
|
"status": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/group/user/update
|
||||||
|
|
||||||
|
Update user group.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Premium-Users",
|
||||||
|
"status": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/group/user/delete
|
||||||
|
|
||||||
|
Delete user group.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{"id": 1}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/group/user/assign
|
||||||
|
|
||||||
|
Assign users to a group.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"groupId": 1,
|
||||||
|
"userIds": [2, 3, 4]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
Permissions link user groups to tunnel groups, allowing users in a user group to access tunnels in a tunnel group.
|
||||||
|
|
||||||
|
### POST /api/v1/group/permission/list
|
||||||
|
|
||||||
|
List all permissions.
|
||||||
|
|
||||||
|
**Request:** `{}`
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"user_group_id": 1,
|
||||||
|
"user_group_name": "VIP-Users",
|
||||||
|
"tunnel_group_id": 1,
|
||||||
|
"tunnel_group_name": "Premium-Tunnels",
|
||||||
|
"created_at": 1706659200000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/group/permission/assign
|
||||||
|
|
||||||
|
Create a permission (grant user group access to tunnel group).
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"userGroupId": 1,
|
||||||
|
"tunnelGroupId": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{"code": 0, "msg": "success", "data": {"id": 1}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/group/permission/remove
|
||||||
|
|
||||||
|
Remove a permission.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{"id": 1}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow: Set Up Group-Based Access
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Create user group
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/group/user/create" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"Standard-Users"}'
|
||||||
|
# Response: {"data":{"id":1}}
|
||||||
|
|
||||||
|
# 2. Create tunnel group
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/group/tunnel/create" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"Standard-Tunnels"}'
|
||||||
|
# Response: {"data":{"id":1}}
|
||||||
|
|
||||||
|
# 3. Add tunnels to tunnel group
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/group/tunnel/assign" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"groupId":1,"tunnelIds":[1,2,3]}'
|
||||||
|
|
||||||
|
# 4. Add users to user group
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/group/user/assign" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"groupId":1,"userIds":[2,3,4]}'
|
||||||
|
|
||||||
|
# 5. Grant permission (user group -> tunnel group)
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/group/permission/assign" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"userGroupId":1,"tunnelGroupId":1}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Now users 2, 3, 4 can access tunnels 1, 2, 3.
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
# Node Management API
|
||||||
|
|
||||||
|
All node endpoints require admin privileges (role_id: 0).
|
||||||
|
|
||||||
|
## POST /api/v1/node/list
|
||||||
|
|
||||||
|
List all nodes with status information.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"page": 1,
|
||||||
|
"pageSize": 20,
|
||||||
|
"keyword": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "HK-Node-1",
|
||||||
|
"secret": "abc123...",
|
||||||
|
"server_ip": "1.2.3.4",
|
||||||
|
"server_ip_v4": "1.2.3.4",
|
||||||
|
"server_ip_v6": "2001:db8::1",
|
||||||
|
"port": "1000-65535",
|
||||||
|
"interface_name": "eth0",
|
||||||
|
"http": 1,
|
||||||
|
"tls": 1,
|
||||||
|
"socks": 1,
|
||||||
|
"tcp_listen_addr": "[::]",
|
||||||
|
"udp_listen_addr": "[::]",
|
||||||
|
"status": 1,
|
||||||
|
"is_remote": 0,
|
||||||
|
"version": "2.1.5",
|
||||||
|
"created_at": 1706659200000,
|
||||||
|
"updated_at": 1706659200000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status Values:**
|
||||||
|
- `0` = Offline
|
||||||
|
- `1` = Online
|
||||||
|
|
||||||
|
**is_remote Values:**
|
||||||
|
- `0` = Local node (managed by this panel)
|
||||||
|
- `1` = Remote node (federation from another panel)
|
||||||
|
|
||||||
|
## POST /api/v1/node/create
|
||||||
|
|
||||||
|
Create a new node.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "US-Node-1",
|
||||||
|
"serverIp": "5.6.7.8",
|
||||||
|
"serverIpV4": "5.6.7.8",
|
||||||
|
"serverIpV6": "2001:db8::2",
|
||||||
|
"port": "1000-65535",
|
||||||
|
"interfaceName": "eth0",
|
||||||
|
"http": 1,
|
||||||
|
"tls": 1,
|
||||||
|
"socks": 1,
|
||||||
|
"tcpListenAddr": "[::]",
|
||||||
|
"udpListenAddr": "[::]",
|
||||||
|
"isRemote": 0,
|
||||||
|
"remoteUrl": "",
|
||||||
|
"remoteToken": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fields:**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| name | string | Yes | Node name |
|
||||||
|
| serverIp | string | Yes | Primary server IP (display) |
|
||||||
|
| serverIpV4 | string | No | IPv4 address |
|
||||||
|
| serverIpV6 | string | No | IPv6 address |
|
||||||
|
| port | string | No | Allowed port range (default: "1000-65535") |
|
||||||
|
| interfaceName | string | No | Network interface for traffic |
|
||||||
|
| http | number | No | Enable HTTP protocol (1/0) |
|
||||||
|
| tls | number | No | Enable TLS protocol (1/0) |
|
||||||
|
| socks | number | No | Enable SOCKS protocol (1/0) |
|
||||||
|
| tcpListenAddr | string | No | TCP listen address (default: "[::]") |
|
||||||
|
| udpListenAddr | string | No | UDP listen address (default: "[::]") |
|
||||||
|
| isRemote | number | No | Federation node (1/0) |
|
||||||
|
| remoteUrl | string | If isRemote=1 | Remote panel URL |
|
||||||
|
| remoteToken | string | If isRemote=1 | Federation token |
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{"code": 0, "msg": "success", "data": {"id": 2, "secret": "xyz789..."}}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/node/install
|
||||||
|
|
||||||
|
Generate installation command for a node.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{"id": 2}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"command": "curl -fsSL https://panel.example.com/install.sh | bash -s -- --secret xyz789... --server https://panel.example.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/node/update
|
||||||
|
|
||||||
|
Update node configuration.
|
||||||
|
|
||||||
|
**Request:** Same fields as create, with `id` field required.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "US-Node-1-Updated",
|
||||||
|
"serverIp": "5.6.7.8",
|
||||||
|
"http": 1,
|
||||||
|
"tls": 1,
|
||||||
|
"socks": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/node/delete
|
||||||
|
|
||||||
|
Delete a node.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{"id": 2}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/node/batch-delete
|
||||||
|
|
||||||
|
Delete multiple nodes.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{"ids": [2, 3, 4]}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/node/check-status
|
||||||
|
|
||||||
|
Refresh and check status of all nodes.
|
||||||
|
|
||||||
|
**Request:** `{}`
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"updated": 5,
|
||||||
|
"online": 4,
|
||||||
|
"offline": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/node/update-order
|
||||||
|
|
||||||
|
Reorder nodes (for display purposes).
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"orders": [
|
||||||
|
{"id": 1, "order": 0},
|
||||||
|
{"id": 2, "order": 1}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/node/releases
|
||||||
|
|
||||||
|
List available FLVX agent releases.
|
||||||
|
|
||||||
|
**Request:** `{}`
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": [
|
||||||
|
{"version": "2.1.5", "published_at": 1706659200000},
|
||||||
|
{"version": "2.1.4", "published_at": 1706572800000}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/node/upgrade
|
||||||
|
|
||||||
|
Upgrade a single node agent.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"version": "2.1.5"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/node/batch-upgrade
|
||||||
|
|
||||||
|
Upgrade multiple node agents.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ids": [1, 2, 3],
|
||||||
|
"version": "2.1.5"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/node/rollback
|
||||||
|
|
||||||
|
Rollback node agent to previous version.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{"id": 2}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Full Node Setup Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Create node
|
||||||
|
RESPONSE=$(curl -s -X POST "${FLVX_BASE_URL}/api/v1/node/create" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"SG-Node-1","serverIp":"203.0.113.10"}')
|
||||||
|
|
||||||
|
NODE_ID=$(echo "$RESPONSE" | jq -r '.data.id')
|
||||||
|
|
||||||
|
# 2. Get install command
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/node/install" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"id\":${NODE_ID}}"
|
||||||
|
|
||||||
|
# 3. Run install command on target server (manual step)
|
||||||
|
|
||||||
|
# 4. Verify node is online
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/node/list" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}' | jq ".data.list[] | select(.id == $NODE_ID) | {name, status}"
|
||||||
|
```
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
# Speed Limit Management API
|
||||||
|
|
||||||
|
Speed limits define bandwidth restrictions that can be applied to forwards or user-tunnel assignments.
|
||||||
|
|
||||||
|
## POST /api/v1/speed-limit/list
|
||||||
|
|
||||||
|
List all speed limit rules.
|
||||||
|
|
||||||
|
**Request:** `{}`
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "10Mbps",
|
||||||
|
"speed": 10,
|
||||||
|
"status": 1,
|
||||||
|
"created_at": 1706659200000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "100Mbps",
|
||||||
|
"speed": 100,
|
||||||
|
"status": 1,
|
||||||
|
"created_at": 1706659200000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/speed-limit/create
|
||||||
|
|
||||||
|
Create a speed limit rule.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "50Mbps",
|
||||||
|
"speed": 50,
|
||||||
|
"status": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fields:**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| name | string | Yes | Rule name |
|
||||||
|
| speed | number | Yes | Speed limit in Mbps |
|
||||||
|
| status | number | No | 1=active, 0=disabled (default: 1) |
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{"code": 0, "msg": "success", "data": {"id": 3}}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/speed-limit/update
|
||||||
|
|
||||||
|
Update a speed limit rule.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"name": "50Mbps-Premium",
|
||||||
|
"speed": 50,
|
||||||
|
"status": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/speed-limit/delete
|
||||||
|
|
||||||
|
Delete a speed limit rule.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{"id": 3}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Applying Speed Limits
|
||||||
|
|
||||||
|
Speed limits can be applied at two levels:
|
||||||
|
|
||||||
|
### 1. Forward Level
|
||||||
|
|
||||||
|
Set `speedId` when creating or updating a forward:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/forward/create" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "limited-forward",
|
||||||
|
"tunnelId": 1,
|
||||||
|
"remoteAddr": "10.0.0.1:80",
|
||||||
|
"speedId": 1
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. User-Tunnel Assignment Level
|
||||||
|
|
||||||
|
Set `speedId` when assigning a tunnel to a user:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/tunnel/user/assign" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"userId": 2,
|
||||||
|
"tunnelId": 1,
|
||||||
|
"flow": 50,
|
||||||
|
"speedId": 2
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Create Tiered Speed Limits
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create speed limit tiers
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/speed-limit/create" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"Basic-10Mbps","speed":10}'
|
||||||
|
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/speed-limit/create" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"Standard-50Mbps","speed":50}'
|
||||||
|
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/speed-limit/create" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"Premium-Unlimited","speed":1000}'
|
||||||
|
|
||||||
|
# List all rules
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/speed-limit/list" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}' | jq '.data'
|
||||||
|
```
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
# Tunnel Management API
|
||||||
|
|
||||||
|
Tunnels define the forwarding path: entry node(s) → (chain nodes) → exit node(s).
|
||||||
|
|
||||||
|
## POST /api/v1/tunnel/list
|
||||||
|
|
||||||
|
List all tunnels.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"page": 1,
|
||||||
|
"pageSize": 20,
|
||||||
|
"keyword": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "HK-US-Tunnel",
|
||||||
|
"type": 1,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"flow": 1,
|
||||||
|
"traffic_ratio": 1,
|
||||||
|
"status": 1,
|
||||||
|
"ip_preference": "ipv4",
|
||||||
|
"in_ip": "",
|
||||||
|
"in_node_id": [1],
|
||||||
|
"chain_node_id": [],
|
||||||
|
"out_node_id": [2],
|
||||||
|
"created_at": 1706659200000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/tunnel/get
|
||||||
|
|
||||||
|
Get a single tunnel by ID.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{"id": 1}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/tunnel/create
|
||||||
|
|
||||||
|
Create a new tunnel.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "JP-SG-Tunnel",
|
||||||
|
"type": 1,
|
||||||
|
"flow": 1,
|
||||||
|
"trafficRatio": 1,
|
||||||
|
"status": 1,
|
||||||
|
"ipPreference": "ipv4",
|
||||||
|
"inIp": "",
|
||||||
|
"inNodeId": [3],
|
||||||
|
"chainNodeId": [],
|
||||||
|
"outNodeId": [4]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fields:**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| name | string | Yes | Tunnel name |
|
||||||
|
| type | number | Yes | 1=port forward, 2=tunnel forward |
|
||||||
|
| flow | number | No | Traffic multiplier (default: 1) |
|
||||||
|
| trafficRatio | number | No | Traffic ratio (default: 1) |
|
||||||
|
| status | number | No | 1=active, 0=disabled (default: 1) |
|
||||||
|
| ipPreference | string | No | "ipv4", "ipv6", or "" (both) |
|
||||||
|
| inIp | string | No | Custom entry IP |
|
||||||
|
| inNodeId | number[] | Yes | Entry node IDs |
|
||||||
|
| chainNodeId | number[] | No | Chain/relay node IDs |
|
||||||
|
| outNodeId | number[] | Yes | Exit node IDs |
|
||||||
|
|
||||||
|
**Tunnel Types:**
|
||||||
|
- `1` = Port Forward: Simple port-to-port forwarding
|
||||||
|
- `2` = Tunnel Forward: Multi-hop tunnel forwarding
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{"code": 0, "msg": "success", "data": {"id": 2}}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/tunnel/update
|
||||||
|
|
||||||
|
Update tunnel configuration.
|
||||||
|
|
||||||
|
**Request:** Same as create, with `id` field required.
|
||||||
|
|
||||||
|
## POST /api/v1/tunnel/delete
|
||||||
|
|
||||||
|
Delete a tunnel.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{"id": 2}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/tunnel/batch-delete
|
||||||
|
|
||||||
|
Delete multiple tunnels.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{"ids": [2, 3]}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/tunnel/diagnose
|
||||||
|
|
||||||
|
Diagnose tunnel connectivity.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{"id": 1}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"reachable": true,
|
||||||
|
"latency_ms": 25,
|
||||||
|
"path": ["entry-node", "exit-node"],
|
||||||
|
"error": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/tunnel/update-order
|
||||||
|
|
||||||
|
Reorder tunnels.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"orders": [
|
||||||
|
{"id": 1, "order": 0},
|
||||||
|
{"id": 2, "order": 1}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/tunnel/batch-redeploy
|
||||||
|
|
||||||
|
Redeploy multiple tunnels (recreate forwarding services).
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{"ids": [1, 2, 3]}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## User-Tunnel Assignment
|
||||||
|
|
||||||
|
These endpoints manage which users can use which tunnels.
|
||||||
|
|
||||||
|
### POST /api/v1/tunnel/user/tunnel
|
||||||
|
|
||||||
|
List tunnels visible to the current user (or all tunnels for admin).
|
||||||
|
|
||||||
|
**Request:** `{}`
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "HK-US-Tunnel",
|
||||||
|
"type": 1,
|
||||||
|
"status": 1,
|
||||||
|
"in_node_name": "HK-Node-1",
|
||||||
|
"out_node_name": "US-Node-1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/tunnel/user/list
|
||||||
|
|
||||||
|
List user-tunnel assignments (admin only).
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"page": 1,
|
||||||
|
"pageSize": 20,
|
||||||
|
"userId": 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"user_id": 2,
|
||||||
|
"tunnel_id": 1,
|
||||||
|
"tunnel_name": "HK-US-Tunnel",
|
||||||
|
"flow": 50,
|
||||||
|
"in_flow": 1073741824,
|
||||||
|
"out_flow": 2147483648,
|
||||||
|
"exp_time": 0,
|
||||||
|
"speed_id": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/tunnel/user/assign
|
||||||
|
|
||||||
|
Assign a tunnel to a user.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"userId": 2,
|
||||||
|
"tunnelId": 1,
|
||||||
|
"flow": 50,
|
||||||
|
"expTime": 0,
|
||||||
|
"speedId": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fields:**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| userId | number | Yes | User ID |
|
||||||
|
| tunnelId | number | Yes | Tunnel ID |
|
||||||
|
| flow | number | No | Traffic quota for this tunnel in GB |
|
||||||
|
| expTime | number | No | Expiry for this assignment (ms, 0=never) |
|
||||||
|
| speedId | number | No | Speed limit rule ID |
|
||||||
|
|
||||||
|
### POST /api/v1/tunnel/user/batch-assign
|
||||||
|
|
||||||
|
Batch assign tunnels to a user.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"userId": 2,
|
||||||
|
"tunnelIds": [1, 2, 3],
|
||||||
|
"flow": 50,
|
||||||
|
"expTime": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/tunnel/user/remove
|
||||||
|
|
||||||
|
Remove a tunnel from a user.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"userId": 2,
|
||||||
|
"tunnelId": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/v1/tunnel/user/update
|
||||||
|
|
||||||
|
Update user-tunnel assignment settings.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"flow": 100,
|
||||||
|
"expTime": 1767225600000,
|
||||||
|
"speedId": 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Assign Tunnel to User
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Create tunnel
|
||||||
|
TUNNEL_RESP=$(curl -s -X POST "${FLVX_BASE_URL}/api/v1/tunnel/create" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"Test-Tunnel","type":1,"inNodeId":[1],"outNodeId":[2]}')
|
||||||
|
|
||||||
|
TUNNEL_ID=$(echo "$TUNNEL_RESP" | jq -r '.data.id')
|
||||||
|
|
||||||
|
# 2. Assign to user with 30GB quota
|
||||||
|
curl -s -X POST "${FLVX_BASE_URL}/api/v1/tunnel/user/assign" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"userId\":2,\"tunnelId\":${TUNNEL_ID},\"flow\":30}"
|
||||||
|
```
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
# TypeScript Type Definitions
|
||||||
|
|
||||||
|
## API Response Envelope
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface APIResponse<T = unknown> {
|
||||||
|
code: number; // 0 = success
|
||||||
|
msg: string; // Message (usually Chinese)
|
||||||
|
ts: number; // Unix timestamp in milliseconds
|
||||||
|
data?: T; // Response payload
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pagination
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface PaginatedRequest {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
keyword?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PaginatedResponse<T> {
|
||||||
|
list: T[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## User
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface User {
|
||||||
|
id: number;
|
||||||
|
user: string; // Username
|
||||||
|
pwd?: string; // Password (only on create/update)
|
||||||
|
name?: string; // Display name
|
||||||
|
role_id: number; // 0 = admin, 1 = regular
|
||||||
|
status: number; // 1 = active, 0 = disabled
|
||||||
|
flow: number; // Traffic quota in GB
|
||||||
|
in_flow: number; // Used upload in bytes
|
||||||
|
out_flow: number; // Used download in bytes
|
||||||
|
exp_time: number; // Expiry timestamp (ms), 0 = never
|
||||||
|
flow_reset_time: number;// Monthly reset day (1-28), 0 = no reset
|
||||||
|
created_at?: number;
|
||||||
|
updated_at?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserCreateRequest {
|
||||||
|
user: string;
|
||||||
|
pwd: string;
|
||||||
|
name?: string;
|
||||||
|
status?: number;
|
||||||
|
flow?: number;
|
||||||
|
num?: number;
|
||||||
|
expTime?: number;
|
||||||
|
flowResetTime?: number;
|
||||||
|
groupIds?: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserPackage {
|
||||||
|
flow: number; // Total quota in GB
|
||||||
|
inFlow: number; // Used upload in bytes
|
||||||
|
outFlow: number; // Used download in bytes
|
||||||
|
tunnels: number; // Assigned tunnel count
|
||||||
|
forwards: number; // Created forward count
|
||||||
|
expTime: number; // Expiry timestamp (ms)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Node
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Node {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
secret: string;
|
||||||
|
server_ip: string;
|
||||||
|
server_ip_v4?: string;
|
||||||
|
server_ip_v6?: string;
|
||||||
|
port: string; // "1000-65535"
|
||||||
|
interface_name?: string;
|
||||||
|
http: number; // 1 = enabled
|
||||||
|
tls: number;
|
||||||
|
socks: number;
|
||||||
|
tcp_listen_addr: string;// "[::]"
|
||||||
|
udp_listen_addr: string;
|
||||||
|
status: number; // 1 = online, 0 = offline
|
||||||
|
is_remote: number; // 0 = local, 1 = federation
|
||||||
|
remote_url?: string;
|
||||||
|
remote_token?: string;
|
||||||
|
version?: string;
|
||||||
|
created_at?: number;
|
||||||
|
updated_at?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NodeCreateRequest {
|
||||||
|
name: string;
|
||||||
|
serverIp: string;
|
||||||
|
serverIpV4?: string;
|
||||||
|
serverIpV6?: string;
|
||||||
|
port?: string;
|
||||||
|
interfaceName?: string;
|
||||||
|
http?: number;
|
||||||
|
tls?: number;
|
||||||
|
socks?: number;
|
||||||
|
tcpListenAddr?: string;
|
||||||
|
udpListenAddr?: string;
|
||||||
|
isRemote?: number;
|
||||||
|
remoteUrl?: string;
|
||||||
|
remoteToken?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tunnel
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Tunnel {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: number; // 1 = port forward, 2 = tunnel forward
|
||||||
|
protocol?: string;
|
||||||
|
flow: number; // Traffic multiplier
|
||||||
|
traffic_ratio: number;
|
||||||
|
status: number; // 1 = active, 0 = disabled
|
||||||
|
ip_preference?: string; // "ipv4", "ipv6", ""
|
||||||
|
in_ip?: string;
|
||||||
|
in_node_id?: number[];
|
||||||
|
chain_node_id?: number[];
|
||||||
|
out_node_id?: number[];
|
||||||
|
created_at?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TunnelCreateRequest {
|
||||||
|
name: string;
|
||||||
|
type: number;
|
||||||
|
flow?: number;
|
||||||
|
trafficRatio?: number;
|
||||||
|
status?: number;
|
||||||
|
ipPreference?: string;
|
||||||
|
inIp?: string;
|
||||||
|
inNodeId: number[];
|
||||||
|
chainNodeId?: number[];
|
||||||
|
outNodeId: number[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Forward
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Forward {
|
||||||
|
id: number;
|
||||||
|
user_id: number;
|
||||||
|
tunnel_id: number;
|
||||||
|
tunnel_name?: string;
|
||||||
|
name: string;
|
||||||
|
in_port: number;
|
||||||
|
remote_addr: string;
|
||||||
|
strategy: string; // "fifo" | "round"
|
||||||
|
status: number; // 1 = running, 0 = paused
|
||||||
|
speed_id: number;
|
||||||
|
speed_name?: string;
|
||||||
|
in_flow: number; // Upload bytes
|
||||||
|
out_flow: number; // Download bytes
|
||||||
|
created_at?: number;
|
||||||
|
updated_at?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ForwardCreateRequest {
|
||||||
|
name: string;
|
||||||
|
tunnelId: number;
|
||||||
|
remoteAddr: string;
|
||||||
|
strategy?: string;
|
||||||
|
inPort?: number;
|
||||||
|
speedId?: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Speed Limit
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface SpeedLimit {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
speed: number; // Mbps
|
||||||
|
status: number;
|
||||||
|
created_at?: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Groups
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface TunnelGroup {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
status: number;
|
||||||
|
tunnel_ids?: number[];
|
||||||
|
created_at?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserGroup {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
status: number;
|
||||||
|
user_ids?: number[];
|
||||||
|
created_at?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GroupPermission {
|
||||||
|
id: number;
|
||||||
|
user_group_id: number;
|
||||||
|
user_group_name?: string;
|
||||||
|
tunnel_group_id: number;
|
||||||
|
tunnel_group_name?: string;
|
||||||
|
created_at?: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## User-Tunnel Assignment
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface UserTunnel {
|
||||||
|
id: number;
|
||||||
|
user_id: number;
|
||||||
|
tunnel_id: number;
|
||||||
|
tunnel_name?: string;
|
||||||
|
flow: number; // Quota for this tunnel in GB
|
||||||
|
in_flow: number;
|
||||||
|
out_flow: number;
|
||||||
|
exp_time: number;
|
||||||
|
speed_id: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Federation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface PeerShare {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
node_id: number;
|
||||||
|
node_name?: string;
|
||||||
|
token: string;
|
||||||
|
max_bandwidth: number;
|
||||||
|
expiry_time: number;
|
||||||
|
port_range_start: number;
|
||||||
|
port_range_end: number;
|
||||||
|
allowed_domains: string;
|
||||||
|
allowed_ips: string;
|
||||||
|
status: number;
|
||||||
|
created_at?: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backup
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface BackupExport {
|
||||||
|
version: string;
|
||||||
|
exportedAt: number;
|
||||||
|
types: string[];
|
||||||
|
users?: User[];
|
||||||
|
nodes?: Node[];
|
||||||
|
tunnels?: Tunnel[];
|
||||||
|
forwards?: Forward[];
|
||||||
|
speedLimits?: SpeedLimit[];
|
||||||
|
tunnelGroups?: TunnelGroup[];
|
||||||
|
userGroups?: UserGroup[];
|
||||||
|
groupPermissions?: GroupPermission[];
|
||||||
|
configs?: Record<string, string>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client Helper Class
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class FlvxClient {
|
||||||
|
private baseUrl: string;
|
||||||
|
private username: string;
|
||||||
|
private password: string;
|
||||||
|
private token?: string;
|
||||||
|
|
||||||
|
constructor(baseUrl?: string, username?: string, password?: string) {
|
||||||
|
this.baseUrl = baseUrl ?? process.env.FLVX_BASE_URL ?? "";
|
||||||
|
this.username = username ?? process.env.FLVX_USERNAME ?? "";
|
||||||
|
this.password = password ?? process.env.FLVX_PASSWORD ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureToken(): Promise<void> {
|
||||||
|
if (this.token) return;
|
||||||
|
|
||||||
|
const res = await fetch(`${this.baseUrl}/api/v1/user/login`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: this.username,
|
||||||
|
password: this.password
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result: APIResponse<{ token: string }> = await res.json();
|
||||||
|
if (result.code !== 0) throw new Error(result.msg);
|
||||||
|
this.token = result.data!.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async request<T>(endpoint: string, data?: object): Promise<T> {
|
||||||
|
await this.ensureToken();
|
||||||
|
|
||||||
|
const res = await fetch(`${this.baseUrl}${endpoint}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": this.token!, // NO "Bearer " prefix!
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data ?? {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result: APIResponse<T> = await res.json();
|
||||||
|
if (result.code === 401) {
|
||||||
|
this.token = undefined;
|
||||||
|
return this.request(endpoint, data);
|
||||||
|
}
|
||||||
|
if (result.code !== 0) throw new Error(result.msg);
|
||||||
|
return result.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience methods
|
||||||
|
async listNodes(): Promise<Node[]> {
|
||||||
|
const data = await this.request<{ list: Node[] }>("/api/v1/node/list", {});
|
||||||
|
return data.list ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async listForwards(): Promise<Forward[]> {
|
||||||
|
const data = await this.request<{ list: Forward[] }>("/api/v1/forward/list", {});
|
||||||
|
return data.list ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async createForward(req: ForwardCreateRequest): Promise<Forward> {
|
||||||
|
return this.request("/api/v1/forward/create", req);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserPackage(): Promise<UserPackage> {
|
||||||
|
return this.request("/api/v1/user/package", {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
# User Management API
|
||||||
|
|
||||||
|
All user management endpoints require admin privileges (role_id: 0).
|
||||||
|
|
||||||
|
## POST /api/v1/user/list
|
||||||
|
|
||||||
|
List all users with pagination and filtering.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"page": 1,
|
||||||
|
"pageSize": 20,
|
||||||
|
"keyword": "search-term"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"user": "admin",
|
||||||
|
"name": "Administrator",
|
||||||
|
"role_id": 0,
|
||||||
|
"status": 1,
|
||||||
|
"flow": 1000,
|
||||||
|
"in_flow": 10737418240,
|
||||||
|
"out_flow": 21474836480,
|
||||||
|
"exp_time": 1767225600000,
|
||||||
|
"flow_reset_time": 1,
|
||||||
|
"created_at": 1706659200000,
|
||||||
|
"updated_at": 1706659200000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/user/create
|
||||||
|
|
||||||
|
Create a new user.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user": "username",
|
||||||
|
"pwd": "password",
|
||||||
|
"name": "Display Name",
|
||||||
|
"status": 1,
|
||||||
|
"flow": 100,
|
||||||
|
"num": 10,
|
||||||
|
"expTime": 1767225600000,
|
||||||
|
"flowResetTime": 1,
|
||||||
|
"groupIds": [1, 2]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fields:**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| user | string | Yes | Username (unique) |
|
||||||
|
| pwd | string | Yes | Password |
|
||||||
|
| name | string | No | Display name |
|
||||||
|
| status | number | No | 1=active, 0=disabled (default: 1) |
|
||||||
|
| flow | number | No | Traffic quota in GB (default: 0) |
|
||||||
|
| num | number | No | Max forwards allowed (default: 0 = unlimited) |
|
||||||
|
| expTime | number | No | Expiry timestamp in ms (0 = never) |
|
||||||
|
| flowResetTime | number | No | Monthly reset day 1-28 (0 = no reset) |
|
||||||
|
| groupIds | number[] | No | User group IDs to assign |
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{"code": 0, "msg": "success", "data": {"id": 2}}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/user/update
|
||||||
|
|
||||||
|
Update user details.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"user": "new-username",
|
||||||
|
"pwd": "new-password",
|
||||||
|
"name": "New Name",
|
||||||
|
"status": 1,
|
||||||
|
"flow": 200,
|
||||||
|
"num": 20,
|
||||||
|
"expTime": 1767225600000,
|
||||||
|
"flowResetTime": 15,
|
||||||
|
"groupIds": [1]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: `pwd` is optional for updates. If omitted, password remains unchanged.
|
||||||
|
|
||||||
|
## POST /api/v1/user/delete
|
||||||
|
|
||||||
|
Delete a user (cascades to forwards and tunnel assignments).
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{"id": 2}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{"code": 0, "msg": "success"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/user/reset
|
||||||
|
|
||||||
|
Reset user traffic at user or tunnel level.
|
||||||
|
|
||||||
|
**Request (User level):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": "user"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request (Tunnel level):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": "tunnel",
|
||||||
|
"tunnelId": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{"code": 0, "msg": "success"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/v1/user/groups
|
||||||
|
|
||||||
|
Get groups a user belongs to.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{"id": 2}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": [
|
||||||
|
{"id": 1, "name": "VIP Users"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Traffic Units
|
||||||
|
|
||||||
|
| Field | Unit | Conversion |
|
||||||
|
|-------|------|------------|
|
||||||
|
| flow | GB | Gigabytes |
|
||||||
|
| in_flow | Bytes | Divide by 1,073,741,824 for GB |
|
||||||
|
| out_flow | Bytes | Divide by 1,073,741,824 for GB |
|
||||||
|
|
||||||
|
## Example: Create User with 50GB Quota
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "${FLVX_BASE_URL}/api/v1/user/create" \
|
||||||
|
-H "Authorization: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"user": "alice",
|
||||||
|
"pwd": "SecurePass123!",
|
||||||
|
"name": "Alice",
|
||||||
|
"status": 1,
|
||||||
|
"flow": 50,
|
||||||
|
"num": 5,
|
||||||
|
"expTime": 1735689600000,
|
||||||
|
"flowResetTime": 1
|
||||||
|
}'
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user