diff --git a/Dockerfile.standalone b/Dockerfile.standalone index 90764fc..bd6c346 100644 --- a/Dockerfile.standalone +++ b/Dockerfile.standalone @@ -9,7 +9,9 @@ RUN corepack enable && corepack prepare pnpm@latest-9 --activate && pnpm install COPY www/api/ ./api COPY www/components/ ./components +COPY www/config/ ./config COPY www/hooks/ ./hooks +COPY www/i18n/ ./i18n COPY www/lib/ ./lib COPY www/pages/ ./pages COPY www/public/ ./public diff --git a/README.md b/README.md index 340e277..c870a3a 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,53 @@ -> 详细博客地址: [https://vaala.cat/2024/01/14/frp-panel-doc/](https://vaala.cat/2024/01/14/frp-panel-doc/) -> 使用说明可以看博客,也可以直接滑到最后 +> Detailed blog post: [https://vaala.cat/2024/01/14/frp-panel-doc/](https://vaala.cat/2024/01/14/frp-panel-doc/) +> You can refer to the blog for instructions, or scroll down to the end. # FRP-Panel -[English Version README](README_en.md) | [中文文档](README.md) +[English Version](README.md) | [中文文档](README_zh.md) -我们的目标就是做一个: +Our goal is to create a more powerful and comprehensive frp that provides: -- 客户端配置可中心化管理 -- 多服务端配置管理 -- 可视化配置界面 -- 简化运行所需要的配置 - -的更强更完善的 frp! +- Centralized management of client configurations +- Management of multiple server configurations +- Visual configuration interface +- Simplified configuration required for running - demo Video: [demo Video](doc/frp-panel-demo.mp4) ![](./doc/frp-panel-demo.gif) -## 项目使用说明 +## Project Usage Instructions -frp-panel 可选 docker 和直接运行模式部署,直接部署请到 release 下载文件:[release](https://github.com/VaalaCat/frp-panel/releases) +frp-panel can be deployed in docker or direct run mode. For direct deployment, please download the files from the release: [release](https://github.com/VaalaCat/frp-panel/releases) -注意:二进制有两种,一种是仅客户端,一种是全功能可执行文件,客户端版只能执行 client 命令(无需 client 参数) +Note: There are two types of binaries, one is for the client only, and the other is a full-featured executable file. The client version will have a "client" identifier in its name. -客户端版的名字会带有 client 标识 +After startup, the default access address is `http://IP:9000`. -启动过后默认访问地址为 `http://IP:9000` +The first registered user is the administrator by default. User registration is not open by default. If you need it, please add the following parameter to the Master startup command or configuration file: `APP_ENABLE_REGISTER=true` -默认第一个注册的用户是管理员。且默认不开放注册多用户,如果需要,请在 Master 启动命令或配置文件中添加参数:`APP_ENABLE_REGISTER=true` +After starting, there will be a "default" entry in the server list. If the status shows "Offline" in red, it indicates that your `MASTER_RPC_HOST` environment variable is not configured correctly or the port is not accessible externally. Please carefully check the configuration and redeploy. -启动后在服务端列表中会有一个default,如果运行信息为“不在线”且为红色,则说明您的 `MASTER_RPC_HOST` 启动环境变量没有配置正确或端口外部访问不成功,请仔细检查配置重新部署。 - -测试端口是否开放的方法,在服务器上运行: +To test if the port is open, run the following command on the server: ```shell python3 -m http.server 8080 ``` -然后在浏览器中访问:`http://IP:8080` (端口可以换成任意你想测试的端口),访问成功则为端口开放 +Then access in the browser: `http://IP:8080` (you can replace Port with any Port you want) -程序的默认存储数据路径和程序文件同目录,如需修改请参考下方的配置表格 +### Docker -### docker - -注意 ⚠️:client 和 server 的启动指令可能会随着项目更新而改变,虽然在项目迭代时会注意前后兼容,但仍难以完全适配,因此 client 和 server 的启动指令以 master 生成为准 +Note⚠️: The startup commands for client and server may change as the project is updated. Although backward compatibility will be considered during project iterations, it is still difficult to fully adapt. Therefore, the startup commands for client and server should be generated from the master. - master +Here's the translated guidance for running the Docker command: + ```bash -# 推荐 -# MASTER_RPC_HOST要改成你服务器的外部IP -# APP_GLOBAL_SECRET注意不要泄漏,客户端和服务端的是通过Master生成的 +# Recommended +# Change MASTER_RPC_HOST to the external IP of your server +# Be careful not to leak APP_GLOBAL_SECRET, it's generated by the Master for both the client and server docker run -d \ --network=host \ --restart=unless-stopped \ @@ -59,16 +55,17 @@ docker run -d \ -e APP_GLOBAL_SECRET=your_secret \ -e MASTER_RPC_HOST=0.0.0.0 \ vaalacat/frp-panel -# 或者 -# 运行时记得删除命令中的中文 -docker run -d -p 9000:9000 \ # API控制台端口 - -p 9001:9001 \ # rpc端口 - -p 7000:7000 \ # frps 端口 - -p 20000-20050:20000-20050 \ # 给frps预留的端口 + +# Alternatively +# Remember to remove comments when running the command +docker run -d -p 9000:9000 \ # API console port + -p 9001:9001 \ # RPC port + -p 7000:7000 \ # FRPS port + -p 20000-20050:20000-20050 \ # Reserved ports for FRPS --restart=unless-stopped \ - -v /opt/frp-panel:/data \ # 数据存储位置 - -e APP_GLOBAL_SECRET=your_secret \ # Master的secret注意不要泄漏,客户端和服务端的是通过Master生成的 - -e MASTER_RPC_HOST=0.0.0.0 \ # 这里要改成你服务器的外部IP + -v /opt/frp-panel:/data \ # Data storage location + -e APP_GLOBAL_SECRET=your_secret \ # Be careful not to leak the Master's secret, it's generated by the Master for both the client and server + -e MASTER_RPC_HOST=0.0.0.0 \ # Change this to the external IP of your server vaalacat/frp-panel ``` @@ -76,25 +73,25 @@ docker run -d -p 9000:9000 \ # API控制台端口 ```bash docker run -d \ - --network=host \ - --restart=unless-stopped \ - vaalacat/frp-panel client -s xxxx -i xxxx -a xxxx -r 127.0.0.1 -c 9001 -p 9000 -e http # 在master WebUI复制的参数 + --network=host \ + --restart=unless-stopped \ + vaalacat/frp-panel client -s xxxx -i xxxx -a xxxx -r 127.0.0.1 -c 9001 -p 9000 -e http # Copy the parameters from the master WebUI ``` - server ```bash docker run -d \ - --network=host \ - --restart=unless-stopped \ - vaalacat/frp-panel server -s xxxx -i xxxx -a xxxx -r 127.0.0.1 -c 9001 -p 9000 -e http # 在master WebUI复制的参数 + --network=host \ + --restart=unless-stopped \ + vaalacat/frp-panel server -s xxxx -i xxxx -a xxxx -r 127.0.0.1 -c 9001 -p 9000 -e http # Copy the parameters from the master WebUI ``` -### 直接运行(Linux) +### Direct Run (Linux) - master -注意修改 IP +Note: Modify the IP ```bash APP_GLOBAL_SECRET=your_secret MASTER_RPC_HOST=0.0.0.0 frp-panel master @@ -103,18 +100,20 @@ APP_GLOBAL_SECRET=your_secret MASTER_RPC_HOST=0.0.0.0 frp-panel master - client ```bash -frp-panel client -s xxxx -i xxxx -a xxxx -r 127.0.0.1 -c 9001 -p 9000 -e http # 在master WebUI复制的参数 +frp-panel client -s xxxx -i xxxx -a xxxx -r 127.0.0.1 -c 9001 -p 9000 -e http # Copy the parameters from the master WebUI ``` - server ```bash -frp-panel server -s xxxx -i xxxx -a xxxx -r 127.0.0.1 -c 9001 -p 9000 -e http # 在master WebUI复制的参数 +frp-panel server -s xxxx -i xxxx -a xxxx -r 127.0.0.1 -c 9001 -p 9000 -e http # Copy the parameters from the master WebUI ``` -### 直接运行(Windows) +### Direct Run (Windows) -在下载的可执行文件同名文件夹下创建一个 `.env` 文件(注意不要有后缀名),然后输入以下内容保存后运行对应命令,注意,client 和 server 的对应参数需要在 web 页面复制 +In the same folder as the downloaded executable, create a `.env` file (note that there should be no file extension), then enter the following content and save it before running the corresponding command. Note that the corresponding parameters for client and server need to be copied from the web page. + +- master: `frp-panel-amd64.exe master` ``` APP_GLOBAL_SECRET=your_secret @@ -122,34 +121,32 @@ MASTER_RPC_HOST=IP DB_DSN=data.db ``` -- master: `frp-panel-amd64.exe master` +For client and server, use the parameters copied from the master WebUI. -client 和 server 要使用在 master WebUI 复制的参数 +- client: `frp-panel-amd64.exe client -s xxxx -i xxxx -a xxxx -r 127.0.0.1 -c 9001 -p 9000 -e http # Copy the parameters from the master WebUI` -- client: `frp-panel-amd64.exe client -s xxxx -i xxxx -a xxxx -r 127.0.0.1 -c 9001 -p 9000 -e http # 在master WebUI复制的参数` +- server: `frp-panel-amd64.exe server -s xxxx -i xxxx -a xxxx -r 127.0.0.1 -c 9001 -p 9000 -e http # Copy the parameters from the master WebUI` -- server: `frp-panel-amd64.exe server -s xxxx -i xxxx -a xxxx -r 127.0.0.1 -c 9001 -p 9000 -e http # 在master WebUI复制的参数` +### Tunnel Advanced Mode Configuration -### 隧道高级模式配置 +This panel is fully compatible with frp's original `json` format configuration. You only need to paste the configuration file content into the advanced mode editor for the server/client, and then update it. For detailed usage, please refer to: [frp documentation](https://gofrp.org/docs/features/common/configure/) -本面板完全兼容 frp 原本的`json`格式配置,仅需要将配置文件内容粘贴到服务端/客户端高级模式编辑框内,更新即可,详细的使用参考:[frp 文档](https://gofrp.org/zh-cn/docs/features/common/configure/) +### Program Startup Configuration File -### 程序启动配置文件 +The program will read the contents of the following files in order as the configuration file: `.env`, `/etc/frpp/.env` -程序会按顺序读取以下文件内容作为配置文件:`.env`,`/etc/frpp/.env` +### Service Management -### 服务管理 +If you are using the installation script provided by the panel, systemd is used for Linux control, and frpp.exe is used for Windows control. -如果您使用的是面板自带的安装脚本,对于 Linux 使用 systemd 控制,对于 Windows 使用 本程序 控制 - -Linux 安装后使用示例: +Examples of using Linux after installation: ```bash systemctl stop frpp systemctl start frpp ``` -Windows 安装后使用示例: +Examples of using Windows after installation: ``` C:/frpp/frpp.exe start @@ -157,93 +154,62 @@ C:/frpp/frpp.exe stop C:/frpp/frpp.exe uninstall ``` -### 配置说明 +## Project Development Guide -| 类型 | 环境变量名 | 默认值 | 描述 | -|--------|-------------------------------------|--------------------|----------------------------------------------------------------| -| string | `APP_SECRET` | - | 应用密钥,用于客户端和服务器的和Master的通信加密 | -| string | `APP_GLOBAL_SECRET` | `frp-panel` | 全局密钥,用于管理生成密钥,需妥善保管 | -| int | `APP_COOKIE_AGE` | `86400` | Cookie 的有效期(秒),默认值为 1 天 | -| string | `APP_COOKIE_NAME` | `frp-panel-cookie` | Cookie 名称 | -| string | `APP_COOKIE_PATH` | `/` | Cookie 路径 | -| string | `APP_COOKIE_DOMAIN` | - | Cookie 域 | -| bool | `APP_COOKIE_SECURE` | `false` | Cookie 是否安全 | -| bool | `APP_COOKIE_HTTP_ONLY` | `true` | Cookie 是否仅限 HTTP | -| bool | `APP_ENABLE_REGISTER` | `false` | 是否启用注册,仅允许第一个管理员注册 | -| int | `MASTER_API_PORT` | `9000` | 主节点 API 端口 | -| string | `MASTER_API_HOST` | - | 主节点域名,可以在反向代理和CDN后 | -| string | `MASTER_API_SCHEME` | `http` | 主节点 API 协议(注意,这里不影响主机行为,设置为https只是为了方便复制客户端启动命令,HTTPS需要自行反向代理)| -| int | `MASTER_CACHE_SIZE` | `10` | 缓存大小(MB) | -| string | `MASTER_RPC_HOST` | `127.0.0.1` | Master节点公共 IP 或域名 | -| int | `MASTER_RPC_PORT` | `9001` | Master节点 RPC 端口 | -| bool | `MASTER_COMPATIBLE_MODE` | `false` | 兼容模式,用于官方 frp 客户端 | -| string | `MASTER_INTERNAL_FRP_SERVER_HOST` | - | Master内置 frps 服务器主机,用于客户端连接 | -| int | `MASTER_INTERNAL_FRP_SERVER_PORT` | `9002` | Master内置 frps 服务器端口,用于客户端连接 | -| string | `MASTER_INTERNAL_FRP_AUTH_SERVER_HOST` | `127.0.0.1` | Master内置 frps 认证服务器主机 | -| int | `MASTER_INTERNAL_FRP_AUTH_SERVER_PORT` | `8999` | Master内置 frps 认证服务器端口 | -| string | `MASTER_INTERNAL_FRP_AUTH_SERVER_PATH` | `/auth` | Master内置 frps 认证服务器路径 | -| int | `SERVER_API_PORT` | `8999` | 服务器 API 端口 | -| string | `DB_TYPE` | `sqlite3` | 数据库类型,如 mysql postgres 或 sqlite3 等 | -| string | `DB_DSN` | `data.db` | 数据库 DSN,默认使用sqlite3,数据默认存储在可执行文件同目录下,对于 sqlite 是路径,其他数据库为 DSN,参见 [MySQL DSN](https://github.com/go-sql-driver/mysql#dsn-data-source-name) | -| string | `CLIENT_ID` | - | 客户端 ID | -| string | `CLIENT_SECRET` | - | 客户端密钥 | +### Platform Architecture Design -## 项目开发指南 +After choosing the tech stack, the next step is to design the program architecture. As mentioned in the background, frp itself has frpc and frps (client and server), these two roles are indispensable. Then we need to add something new to manage them, so frp-panel introduces a new master role. The master will be responsible for managing various frpc and frps, as well as centrally storing configuration files and connection information. -### 平台架构设计 +Next, we have frpc and frps. The original version requires writing configuration files on both sides. Since the original version already supports this, we don't need to follow the original approach. We will directly not support configuration files, and all configurations must be obtained from the master. -技术栈选好了,下一步就是要设计程序的架构。在刚刚背景里说的那样,frp 本身有 frpc 和 frps(客户端和服务端),这两个角色肯定是必不可少了。然后我们还要新增一个东西去管理它们,所以 frp-panel 新增了一个 master 角色。master 会负责管理各种 frpc 和 frps,中心化的存储配置文件和连接信息。 - -然后是 frpc 和 frps。原版是需要在两边分别写配置文件的。那么既然原版已经支持了,就不用在走原版的路子,我们直接不支持配置文件,所有的配置都必须从 master 获取。 - -其次还要考虑到与原版的兼容问题,frp-panel 的客户端/服务端都必须要能连上官方 frpc/frps 服务。这样的话就可以做到配置文件/不要配置文件都能完美工作了。 -总的说来架构还是很简单的。 +In addition, we also need to consider the compatibility with the original version. The client/server of frp-panel must be able to connect to the official frpc/frps service. In this way, both configuration file and non-configuration file modes can work perfectly. +Overall, the architecture is quite simple. ![arch](doc/arch.png) -### 开发 +### Development -项目包含三个角色 +The project includes three roles: -1. Master: 控制节点,接受来自前端的请求并负责管理 Client 和 Server -2. Server: 服务端,受控制节点控制,负责对客户端提供服务,包含 frps 和 rpc(用于连接 Master)服务 -3. Client: 客户端,受控制节点控制,包含 frpc 和 rpc(用于连接 Master)服务 +1. Master: The control node, accepts requests from the frontend and is responsible for managing Client and Server. +2. Server: The server side, controlled by the control node, responsible for providing services to clients, including frps and rpc (for connecting to the Master) services. +3. Client: The client side, controlled by the control node, including frpc and rpc (for connecting to the Master) services. -接下来给出一个项目中各个包的功能 +Next, we will provide the functionality of each package in the project: ``` . -|-- biz # 主要业务逻辑 -| |-- client # 客户端逻辑(这里指的是frp-panel的客户端) -| |-- master # frp-panel 控制平面,负责处理前端请求,并且使用rpc管理frp-panel的server和client -| | |-- auth # 认证模块,包含用户认证和客户端认证 -| | |-- client # 客户端模块,包含前端管理客户端的各种API -| | |-- server # 服务端模块,包含前端管理服务端的各种API -| | `-- user # 用户模块,包含用户管理、用户信息获取等 -| `-- server # 服务端逻辑(这里指的是frp-panel的服务端) -|-- cache # 缓存,用于存储frps的认证token -|-- cmd # 命令行入口,main函数的所在地,负责按需启动各个模块 +|-- biz # Main business logic +| |-- client # Client logic (here referring to the frp-panel client) +| |-- master # frp-panel control plane, responsible for handling frontend requests, and using rpc to manage frp-panel's server and client +| | |-- auth # Authentication module, including user authentication and client authentication +| | |-- client # Client module, including various APIs for the frontend to manage clients +| | |-- server # Server module, including various APIs for the frontend to manage servers +| | `-- user # User module, including user management, user information retrieval, etc. +| `-- server # Server logic +|-- cache # Cache, used to store frps authentication tokens +|-- cmd # Command line entry, where the main function is located, responsible for starting various modules as needed |-- common |-- conf -|-- dao # data access object,任何和数据库相关的操作会调用这个库 -|-- doc # 文档 -|-- idl # idl定义 -|-- middleware # api的中间件,包含JWT和context相关,用于处理api请求,鉴权通过后会把用户信息注入到context,可以通过common包获取 -|-- models # 数据库模型,用于定义数据库表。同时包含实体定义 -|-- pb # protobuf生成的pb文件 -|-- rpc # 各种rpc的所在地,包含Client/Server调用Master的逻辑,也包含Master使用Stream调用Client和Server的逻辑 -|-- services # 各种需要在内存中持久运行的模块,这个包可以管理各个服务的运行/停止 -| |-- api # api服务,运行需要外部传入一个ginRouter -| |-- client # frp的客户端,即frpc,可以控制frpc的各种配置/开始与停止 -| |-- master # master服务,包含rpc的服务端定义,接收到rpc请求后会调用biz包处理逻辑 -| |-- rpcclient # 有状态的rpc客户端,因为rpc的client都没有公网ip,因此在rpc client启动时会调用master的stream长连接rpc,建立连接后Master和Client通过这个包通信 -| `-- server # frp的服务端,即frps,可以控制frps的各种配置/开始与停止 -|-- tunnel # tunnel模块,用于管理tunnel,也就是管理frpc和frps服务 +|-- dao # Data access object, any operations related to the database will call this library +|-- doc # Documentation +|-- idl # IDL definitions +|-- middleware # API middleware, including JWT and context-related, used to process API requests. After authentication passes, user information will be injected into the context and can be obtained through the common package. +|-- models # Database models, used to define database tables. Also includes entity definitions. +|-- pb # Generated protobuf pb files +|-- rpc # Location of various rpcs, including the logic for Client/Server to call Master, as well as the logic for Master to use Stream to call Client and Server +|-- services # Various modules that need to run persistently in memory, this package can manage the running/stopping of various services +| |-- api # API service, requires an external ginRouter to run +| |-- client # frp client, i.e., frpc, can control various configurations/start and stop of frpc +| |-- master # Master service, including the rpc server definition, after receiving an rpc request, it will call the biz package to handle the logic +| |-- rpcclient # Stateful rpc client, because the rpc clients don't have public IP addresses, the rpcclient will call the master's stream long-connection rpc when starting, and after the connection is established, the Master and Client communicate through this package +| `-- server # frp server, i.e., frps, can control various configurations/start and stop of frps +|-- tunnel # Tunnel module, used to manage tunnels, i.e., manage frpc and frps services |-- utils -|-- watcher # 定时运行的任务,比如每30秒更新一次配置文件 +|-- watcher # Scheduled tasks, e.g., updating configuration files every 30 seconds `-- www |-- api - |-- components # 这里面有一个apitest组件用于测试 + |-- components # There is an apitest component here for testing | `-- ui |-- lib | `-- pb @@ -252,38 +218,54 @@ C:/frpp/frpp.exe uninstall |-- store |-- styles `-- types - ``` -### 调试启动方式: +### Debugging and Startup Methods: - master: `go run cmd/*.go master` - > client 和 server 的具体参数请复制 master webui 中的内容 + > For client and server, please copy the content from the master webui - client: `go run cmd/*.go client -i -s ` - server: `go run cmd/*.go server -i -s ` -项目配置文件会默认读取当前文件夹下的.env 文件,项目内置了样例配置文件,可以按照自己的需求进行修改 +The project configuration file will read the .env file in the current folder by default. The project includes a sample configuration file, which can be modified according to your needs. -详细架构调用图 +Detailed architecture call diagram: ![structure](doc/callvis.svg) -### 本体配置说明 +### Core Configuration Explanation [settings.go](conf/settings.go) -这里有详细的配置参数解释,需要进一步修改配置请参考该文件 +This file contains detailed explanations of the configuration parameters. Please refer to this file if you need to further modify the configuration. -### 一些图片 +## Screenshots -![](doc/platform_info.png) -![](doc/login.png) -![](doc/register.png) -![](doc/clients_menu.png) -![](doc/server_menu.png) -![](doc/create_client.png) -![](doc/create_server.png) -![](doc/edit_client.png) -![](doc/edit_client_adv.png) -![](doc/edit_server.png) -![](doc/edit_server_adv.png) -![](doc/traffic_statistics.png) +### Index Page +![Index Page](doc/en_index.png) + +### Server List +![Server List](doc/en_server_list.png) + +### Server Edit +![Server Edit](doc/en_server_edit.png) + +### Server Edit Advanced +![Server Edit Advanced](doc/en_server_edit_adv.png) + +### Client List +![Client List](doc/en_client_list.png) + +### Client Edit +![Client Edit](doc/en_client_edit.png) + +### Client Edit Advanced +![Client Edit Advanced](doc/en_client_edit_adv.png) + +### Client Stats +![Client Stats](doc/en_client_stats.png) + +### Realtime Log +![Realtime Log](doc/en_realtime_log.png) + +### Remote Console +![Remote Console](doc/en_remote_console.png) diff --git a/README_en.md b/README_en.md deleted file mode 100644 index a87aeff..0000000 --- a/README_en.md +++ /dev/null @@ -1,255 +0,0 @@ -> Detailed blog post: [https://vaala.cat/2024/01/14/frp-panel-doc/](https://vaala.cat/2024/01/14/frp-panel-doc/) -> You can refer to the blog for instructions, or scroll down to the end. - -# FRP-Panel - -[English Version](README_en.md) | [中文文档](README.md) - -Our goal is to create a more powerful and comprehensive frp that provides: - -- Centralized management of client configurations -- Management of multiple server configurations -- Visual configuration interface -- Simplified configuration required for running - -- demo Video: [demo Video](doc/frp-panel-demo.mp4) - -![](./doc/frp-panel-demo.gif) - -## Project Usage Instructions - -frp-panel can be deployed in docker or direct run mode. For direct deployment, please download the files from the release: [release](https://github.com/VaalaCat/frp-panel/releases) - -Note: There are two types of binaries, one is for the client only, and the other is a full-featured executable file. The client version will have a "client" identifier in its name. - -After startup, the default access address is `http://IP:9000`. - -The first registered user is the administrator by default. User registration is not open by default. If you need it, please add the following parameter to the Master startup command or configuration file: `APP_ENABLE_REGISTER=true` - -After starting, there will be a "default" entry in the server list. If the status shows "Offline" in red, it red, it indicates that your `MASTER_RPC_HOST` environment variable is not confi not confi not configured correctly or the port is not accessible externally. Please carefully check the configuration and redeploy. - -To test if the port if the port if the port is open, run the following command on the server: - -```shell -python3 -m http.server 8080 -``` - -Then access in the browser: `http://IP:8080` (you can replace Port with any Port you want - -### Docker - -Note⚠️: The startup commands for client and server may change as the project is updated. Although backward compatibility will be considered during project iterations, it is still difficult to fully adapt. Therefore, the startup commands for client and server should be generated from the master. - -- master - -Here's the translated guidance for running the Docker command: - -```bash -# Recombash -# Recommended -# Change MASTER_RPC_HOST to the external IP of your of your server -# Be careful not careful not to leak APP_GLOBAL_SECRET, it's generated by the Master for both the client and server -docker run -d \ - --network=host \ - --restart=unless-stopped \ - -v /opt/frp-panel:/data \ - -e APP_GLOBAL_SECRET=your_secret \ - -e MASTER_RPC_HOST=0.0.0.0 \ - vaalacat/frp-panel - -# Alternatively -# Remember to remove comments when running the command -docker run -d -p 9000:9000 \ # API console port - -p 9001:9001 \ # RPC port - -p 7000:7000 \ # FRPS port - -p 20000-20050:20000-20050 \ # Reserved ports for FRPS - --restart=unless-stopped \ - -v /opt/frp-panel:/data \ # Data storage location - -e APP_GLOBAL_SECRET=your_secret \ # Be careful not to leak the Master's secret, it's generated by the Master for both the client and server - -e MASTER_RPC_HOST=0.0.0.0 \ # Change this to the external IP of your server - vaalacat/frp-panel -``` - -- client - -```bash -docker run -d \ - --network=host \ - --restart=unless-stopped \ - vaalacat/frp-panel client -s xxxx -i xxxx -a xxxx -r 127.0.0.1 -c 9001 -p 9000 -e http # Copy the parameters from the master WebUI -``` - -- server - -```bash -docker run -d \ - --network=host \ - --restart=unless-stopped \ - vaalacat/frp-panel server -s xxxx -i xxxx -a xxxx -r 127.0.0.1 -c 9001 -p 9000 -e http # Copy the parameters from the master WebUI -``` - -### Direct Run (Linux) - -- master - -Note: Modify the IP - -```bash -APP_GLOBAL_SECRET=your_secret MASTER_RPC_HOST=0.0.0.0 frp-panel master -``` - -- client - -```bash -frp-panel client -s xxxx -i xxxx -a xxxx -r 127.0.0.1 -c 9001 -p 9000 -e http # Copy the parameters from the master WebUI -``` - -- server - -```bash -frp-panel server -s xxxx -i xxxx -a xxxx -r 127.0.0.1 -c 9001 -p 9000 -e http # Copy the parameters from the master WebUI -``` - -### Direct Run (Windows) - -In the same folder as the downloaded executable, create a `.env` file (note that there should be no file extension), then enter the following content and save it before running the corresponding command. Note that the corresponding parameters for client and server need to be copied from the web page. - -- master: `frp-panel-amd64.exe master` - -``` -APP_GLOBAL_SECRET=your_secret -MASTER_RPC_HOST=IP -DB_DSN=data.db -``` - -For client and server, use the parameters copied from the master WebUI. - -- client: `frp-panel-amd64.exe client -s xxxx -i xxxx -a xxxx -r 127.0.0.1 -c 9001 -p 9000 -e http # Copy the parameters from the master WebUI` - -- server: `frp-panel-amd64.exe server -s xxxx -i xxxx -a xxxx -r 127.0.0.1 -c 9001 -p 9000 -e http # Copy the parameters from the master WebUI` - -### Tunnel Advanced Mode Configuration - -This panel is fully compatible with frp's original `json` format configuration. You only need to paste the configuration file content into the advanced mode editor for the server/client, and then update it. For detailed usage, please refer to: [frp documentation](https://gofrp.org/docs/features/common/configure/) - -### Program Startup Configuration File - -The program will read the contents of the following files in order as the configuration file: `.env`, `/etc/frpp/.env` - -### Service Management - -If you are using the installation script provided by the panel, systemd is used for Linux control, and frpp.exe is used for Windows control. - -Examples of using Linux after installation: - -```bash -systemctl stop frpp -systemctl start frpp -``` - -Examples of using Windows after installation: - -``` -C:/frpp/frpp.exe start -C:/frpp/frpp.exe stop -C:/frpp/frpp.exe uninstall -``` - -## Project Development Guide - -### Platform Architecture Design - -After choosing the tech stack, the next step is to design the architecture of the program. As mentioned in the background, frp itself has frpc and frps (client and server), these two roles are indispensable. Then we need to add something new to manage them, so frp-panel introduces a new master role. The master will be responsible for managing various frpc and frps, as well as centrally storing configuration files and connection information. - -Next, we have frpc and frps. The original version requires writing configuration files on both sides. Since the original version already supports this, we don't need to follow the original approach. We will directly not support configuration files, and all configurations must be obtained from the master. - -In addition, we also need to consider the compatibility with the original version. The client/server of frp-panel must be able to connect to the official frpc/frps service. In this way, both configuration file and non-configuration file modes can work perfectly. -Overall, the architecture is quite simple. - -![arch](doc/arch.png) - -### Development - -The project includes three roles: - -1. Master: The control node, accepts requests from the frontend and is responsible for managing Client and Server. -2. Server: The server side, controlled by the control node, responsible for providing services to clients, including frps and rpc (for connecting to the Master) services. -3. Client: The client side, controlled by the control node, including frpc and rpc (for connecting to the Master) services. - -Next, we will provide the functionality of each package in the project: - -``` -. -|-- biz # Main business logic -| |-- client # Client logic (here referring to the frp-panel client) -| |-- master # frp-panel control plane, responsible for handling frontend requests, and using rpc to manage frp-panel's server and client -| | |-- auth # Authentication module, including user authentication and client authentication -| | |-- client # Client module, including various APIs for the frontend to manage clients -| | |-- server # Server module, including various APIs for the frontend to manage servers -| | `-- user # User module, including user management, user information retrieval, etc. -| `-- server # Server logic -|-- cache # Cache, used to store frps authentication tokens -|-- cmd # Command line entry, where the main function is located, responsible for starting various modules as needed -|-- common -|-- conf -|-- dao # Data access object, any operations related to the database will call this library -|-- doc # Documentation -|-- idl # IDL definitions -|-- middleware # API middleware, including JWT and context-related, used to process API requests. After authentication passes, user information will be injected into the context and can be obtained through the common package. -|-- models # Database models, used to define database tables. Also includes entity definitions. -|-- pb # Generated protobuf pb files -|-- rpc # Location of various rpcs, including the logic for Client/Server to call Master, as well as the logic for Master to use Stream to call Client and Server -|-- services # Various modules that need to run persistently in memory, this package can manage the running/stopping of various services -| |-- api # API service, requires an external ginRouter to run -| |-- client # frp client, i.e., frpc, can control various configurations/start and stop of frpc -| |-- master # Master service, including the rpc server definition, after receiving an rpc request, it will call the biz package to handle the logic -| |-- rpcclient # Stateful rpc client, because the rpc clients don't have public IP addresses, the rpcclient will call the master's stream long-connection rpc when starting, and after the connection is established, the Master and Client communicate through this package -| `-- server # frp server, i.e., frps, can control various configurations/start and stop of frps -|-- tunnel # Tunnel module, used to manage tunnels, i.e., manage frpc and frps services -|-- utils -|-- watcher # Scheduled tasks, e.g., updating configuration files every 30 seconds -`-- www - |-- api - |-- components # There is an apitest component here for testing - | `-- ui - |-- lib - | `-- pb - |-- pages - |-- public - |-- store - |-- styles - `-- types -``` - -### Debugging and Startup Methods: - -- master: `go run cmd/*.go master` - > For client and server, please copy the content from the master webui -- client: `go run cmd/*.go client -i -s ` -- server: `go run cmd/*.go server -i -s ` - -The project configuration file will read the .env file in the current folder by default. The project includes a sample configuration file, which can be modified according to your needs. - -Detailed architecture call diagram: - -![structure](doc/callvis.svg) - -### Core Configuration Explanation - -[settings.go](conf/settings.go) -This file contains detailed explanations of the configuration parameters. Please refer to this file if you need to further modify the configuration. - -### Some Images - -![](doc/platform_info.png) -![](doc/login.png) -![](doc/register.png) -![](doc/clients_menu.png) -![](doc/server_menu.png) -![](doc/create_client.png) -![](doc/create_server.png) -![](doc/edit_client.png) -![](doc/edit_client_adv.png) -![](doc/edit_server.png) -![](doc/edit_server_adv.png) -![](doc/traffic_statistics.png) diff --git a/README_zh.md b/README_zh.md new file mode 100644 index 0000000..64b786f --- /dev/null +++ b/README_zh.md @@ -0,0 +1,306 @@ +> 详细博客地址: [https://vaala.cat/2024/01/14/frp-panel-doc/](https://vaala.cat/2024/01/14/frp-panel-doc/) +> 使用说明可以看博客,也可以直接滑到最后 + +# FRP-Panel + +[English Version](README.md) | [中文文档](README_zh.md) + +我们的目标就是做一个: + +- 客户端配置可中心化管理 +- 多服务端配置管理 +- 可视化配置界面 +- 简化运行所需要的配置 + +的更强更完善的 frp! + +- demo Video: [demo Video](doc/frp-panel-demo.mp4) + +![](./doc/frp-panel-demo.gif) + +## 项目使用说明 + +frp-panel 可选 docker 和直接运行模式部署,直接部署请到 release 下载文件:[release](https://github.com/VaalaCat/frp-panel/releases) + +注意:二进制有两种,一种是仅客户端,一种是全功能可执行文件,客户端版只能执行 client 命令(无需 client 参数) + +客户端版的名字会带有 client 标识 + +启动过后默认访问地址为 `http://IP:9000` + +默认第一个注册的用户是管理员。且默认不开放注册多用户,如果需要,请在 Master 启动命令或配置文件中添加参数:`APP_ENABLE_REGISTER=true` + +启动后在服务端列表中会有一个default,如果运行信息为“不在线”且为红色,则说明您的 `MASTER_RPC_HOST` 启动环境变量没有配置正确或端口外部访问不成功,请仔细检查配置重新部署。 + +测试端口是否开放的方法,在服务器上运行: + +```shell +python3 -m http.server 8080 +``` + +然后在浏览器中访问:`http://IP:8080` (端口可以换成任意你想测试的端口),访问成功则为端口开放 + +程序的默认存储数据路径和程序文件同目录,如需修改请参考下方的配置表格 + +### docker + +注意 ⚠️:client 和 server 的启动指令可能会随着项目更新而改变,虽然在项目迭代时会注意前后兼容,但仍难以完全适配,因此 client 和 server 的启动指令以 master 生成为准 + +- master + +```bash +# 推荐 +# MASTER_RPC_HOST要改成你服务器的外部IP +# APP_GLOBAL_SECRET注意不要泄漏,客户端和服务端的是通过Master生成的 +docker run -d \ + --network=host \ + --restart=unless-stopped \ + -v /opt/frp-panel:/data \ + -e APP_GLOBAL_SECRET=your_secret \ + -e MASTER_RPC_HOST=0.0.0.0 \ + vaalacat/frp-panel +# 或者 +# 运行时记得删除命令中的中文 +docker run -d -p 9000:9000 \ # API控制台端口 + -p 9001:9001 \ # rpc端口 + -p 7000:7000 \ # frps 端口 + -p 20000-20050:20000-20050 \ # 给frps预留的端口 + --restart=unless-stopped \ + -v /opt/frp-panel:/data \ # 数据存储位置 + -e APP_GLOBAL_SECRET=your_secret \ # Master的secret注意不要泄漏,客户端和服务端的是通过Master生成的 + -e MASTER_RPC_HOST=0.0.0.0 \ # 这里要改成你服务器的外部IP + vaalacat/frp-panel +``` + +- client + +```bash +docker run -d \ + --network=host \ + --restart=unless-stopped \ + vaalacat/frp-panel client -s xxxx -i xxxx -a xxxx -r 127.0.0.1 -c 9001 -p 9000 -e http # 在master WebUI复制的参数 +``` + +- server + +```bash +docker run -d \ + --network=host \ + --restart=unless-stopped \ + vaalacat/frp-panel server -s xxxx -i xxxx -a xxxx -r 127.0.0.1 -c 9001 -p 9000 -e http # 在master WebUI复制的参数 +``` + +### 直接运行(Linux) + +- master + +注意修改 IP + +```bash +APP_GLOBAL_SECRET=your_secret MASTER_RPC_HOST=0.0.0.0 frp-panel master +``` + +- client + +```bash +frp-panel client -s xxxx -i xxxx -a xxxx -r 127.0.0.1 -c 9001 -p 9000 -e http # 在master WebUI复制的参数 +``` + +- server + +```bash +frp-panel server -s xxxx -i xxxx -a xxxx -r 127.0.0.1 -c 9001 -p 9000 -e http # 在master WebUI复制的参数 +``` + +### 直接运行(Windows) + +在下载的可执行文件同名文件夹下创建一个 `.env` 文件(注意不要有后缀名),然后输入以下内容保存后运行对应命令,注意,client 和 server 的对应参数需要在 web 页面复制 + +``` +APP_GLOBAL_SECRET=your_secret +MASTER_RPC_HOST=IP +DB_DSN=data.db +``` + +- master: `frp-panel-amd64.exe master` + +client 和 server 要使用在 master WebUI 复制的参数 + +- client: `frp-panel-amd64.exe client -s xxxx -i xxxx -a xxxx -r 127.0.0.1 -c 9001 -p 9000 -e http # 在master WebUI复制的参数` + +- server: `frp-panel-amd64.exe server -s xxxx -i xxxx -a xxxx -r 127.0.0.1 -c 9001 -p 9000 -e http # 在master WebUI复制的参数` + +### 隧道高级模式配置 + +本面板完全兼容 frp 原本的`json`格式配置,仅需要将配置文件内容粘贴到服务端/客户端高级模式编辑框内,更新即可,详细的使用参考:[frp 文档](https://gofrp.org/zh-cn/docs/features/common/configure/) + +### 程序启动配置文件 + +程序会按顺序读取以下文件内容作为配置文件:`.env`,`/etc/frpp/.env` + +### 服务管理 + +如果您使用的是面板自带的安装脚本,对于 Linux 使用 systemd 控制,对于 Windows 使用 本程序 控制 + +Linux 安装后使用示例: + +```bash +systemctl stop frpp +systemctl start frpp +``` + +Windows 安装后使用示例: + +``` +C:/frpp/frpp.exe stop +C:/frpp/frpp.exe start +C:/frpp/frpp.exe uninstall +``` + +### 配置说明 + +| 类型 | 环境变量名 | 默认值 | 描述 | +|--------|-------------------------------------|--------------------|----------------------------------------------------------------| +| string | `APP_SECRET` | - | 应用密钥,用于客户端和服务器的和Master的通信加密 | +| string | `APP_GLOBAL_SECRET` | `frp-panel` | 全局密钥,用于管理生成密钥,需妥善保管 | +| int | `APP_COOKIE_AGE` | `86400` | Cookie 的有效期(秒),默认值为 1 天 | +| string | `APP_COOKIE_NAME` | `frp-panel-cookie` | Cookie 名称 | +| string | `APP_COOKIE_PATH` | `/` | Cookie 路径 | +| string | `APP_COOKIE_DOMAIN` | - | Cookie 域 | +| bool | `APP_COOKIE_SECURE` | `false` | Cookie 是否安全 | +| bool | `APP_COOKIE_HTTP_ONLY` | `true` | Cookie 是否仅限 HTTP | +| bool | `APP_ENABLE_REGISTER` | `false` | 是否启用注册,仅允许第一个管理员注册 | +| int | `MASTER_API_PORT` | `9000` | 主节点 API 端口 | +| string | `MASTER_API_HOST` | - | 主节点域名,可以在反向代理和CDN后 | +| string | `MASTER_API_SCHEME` | `http` | 主节点 API 协议(注意,这里不影响主机行为,设置为https只是为了方便复制客户端启动命令,HTTPS需要自行反向代理)| +| int | `MASTER_CACHE_SIZE` | `10` | 缓存大小(MB) | +| string | `MASTER_RPC_HOST` | `127.0.0.1` | Master节点公共 IP 或域名 | +| int | `MASTER_RPC_PORT` | `9001` | Master节点 RPC 端口 | +| bool | `MASTER_COMPATIBLE_MODE` | `false` | 兼容模式,用于官方 frp 客户端 | +| string | `MASTER_INTERNAL_FRP_SERVER_HOST` | - | Master内置 frps 服务器主机,用于客户端连接 | +| int | `MASTER_INTERNAL_FRP_SERVER_PORT` | `9002` | Master内置 frps 服务器端口,用于客户端连接 | +| string | `MASTER_INTERNAL_FRP_AUTH_SERVER_HOST` | `127.0.0.1` | Master内置 frps 认证服务器主机 | +| int | `MASTER_INTERNAL_FRP_AUTH_SERVER_PORT` | `8999` | Master内置 frps 认证服务器端口 | +| string | `MASTER_INTERNAL_FRP_AUTH_SERVER_PATH` | `/auth` | Master内置 frps 认证服务器路径 | +| int | `SERVER_API_PORT` | `8999` | 服务器 API 端口 | +| string | `DB_TYPE` | `sqlite3` | 数据库类型,如 mysql postgres 或 sqlite3 等 | +| string | `DB_DSN` | `data.db` | 数据库 DSN,默认使用sqlite3,数据默认存储在可执行文件同目录下,对于 sqlite 是路径,其他数据库为 DSN,参见 [MySQL DSN](https://github.com/go-sql-driver/mysql#dsn-data-source-name) | +| string | `CLIENT_ID` | - | 客户端 ID | +| string | `CLIENT_SECRET` | - | 客户端密钥 | + +## 项目开发指南 + +### 平台架构设计 + +技术栈选好了,下一步就是要设计程序的架构。在刚刚背景里说的那样,frp 本身有 frpc 和 frps(客户端和服务端),这两个角色肯定是必不可少了。然后我们还要新增一个东西去管理它们,所以 frp-panel 新增了一个 master 角色。master 会负责管理各种 frpc 和 frps,中心化的存储配置文件和连接信息。 + +然后是 frpc 和 frps。原版是需要在两边分别写配置文件的。那么既然原版已经支持了,就不用在走原版的路子,我们直接不支持配置文件,所有的配置都必须从 master 获取。 + +其次还要考虑到与原版的兼容问题,frp-panel 的客户端/服务端都必须要能连上官方 frpc/frps 服务。这样的话就可以做到配置文件/不要配置文件都能完美工作了。 +总的说来架构还是很简单的。 + +![arch](doc/arch.png) + +### 开发 + +项目包含三个角色 + +1. Master: 控制节点,接受来自前端的请求并负责管理 Client 和 Server +2. Server: 服务端,受控制节点控制,负责对客户端提供服务,包含 frps 和 rpc(用于连接 Master)服务 +3. Client: 客户端,受控制节点控制,包含 frpc 和 rpc(用于连接 Master)服务 + +接下来给出一个项目中各个包的功能 + +``` +. +|-- biz # 主要业务逻辑 +| |-- client # 客户端逻辑(这里指的是frp-panel的客户端) +| |-- master # frp-panel 控制平面,负责处理前端请求,并且使用rpc管理frp-panel的server和client +| | |-- auth # 认证模块,包含用户认证和客户端认证 +| | |-- client # 客户端模块,包含前端管理客户端的各种API +| | |-- server # 服务端模块,包含前端管理服务端的各种API +| | `-- user # 用户模块,包含用户管理、用户信息获取等 +| `-- server # 服务端逻辑(这里指的是frp-panel的服务端) +|-- cache # 缓存,用于存储frps的认证token +|-- cmd # 命令行入口,main函数的所在地,负责按需启动各个模块 +|-- common +|-- conf +|-- dao # data access object,任何和数据库相关的操作会调用这个库 +|-- doc # 文档 +|-- idl # idl定义 +|-- middleware # api的中间件,包含JWT和context相关,用于处理api请求,鉴权通过后会把用户信息注入到context,可以通过common包获取 +|-- models # 数据库模型,用于定义数据库表。同时包含实体定义 +|-- pb # protobuf生成的pb文件 +|-- rpc # 各种rpc的所在地,包含Client/Server调用Master的逻辑,也包含Master使用Stream调用Client和Server的逻辑 +|-- services # 各种需要在内存中持久运行的模块,这个包可以管理各个服务的运行/停止 +| |-- api # api服务,运行需要外部传入一个ginRouter +| |-- client # frp的客户端,即frpc,可以控制frpc的各种配置/开始与停止 +| |-- master # master服务,包含rpc的服务端定义,接收到rpc请求后会调用biz包处理逻辑 +| |-- rpcclient # 有状态的rpc客户端,因为rpc的client都没有公网ip,因此在rpc client启动时会调用master的stream长连接rpc,建立连接后Master和Client通过这个包通信 +| `-- server # frp的服务端,即frps,可以控制frps的各种配置/开始与停止 +|-- tunnel # tunnel模块,用于管理tunnel,也就是管理frpc和frps服务 +|-- utils +|-- watcher # 定时运行的任务,比如每30秒更新一次配置文件 +`-- www + |-- api + |-- components # 这里面有一个apitest组件用于测试 + | `-- ui + |-- lib + | `-- pb + |-- pages + |-- public + |-- store + |-- styles + `-- types + +``` + +### 调试启动方式: + +- master: `go run cmd/*.go master` + > client 和 server 的具体参数请复制 master webui 中的内容 +- client: `go run cmd/*.go client -i -s ` +- server: `go run cmd/*.go server -i -s ` + +项目配置文件会默认读取当前文件夹下的.env 文件,项目内置了样例配置文件,可以按照自己的需求进行修改 + +详细架构调用图 + +![structure](doc/callvis.svg) + +### 本体配置说明 + +[settings.go](conf/settings.go) +这里有详细的配置参数解释,需要进一步修改配置请参考该文件 + +## 截图展示 + +### 首页 +![首页](doc/cn_index.png) + +### 服务器列表 +![服务器列表](doc/cn_server_list.png) + +### 服务器编辑 +![服务器编辑](doc/cn_server_edit.png) + +### 服务器高级编辑 +![服务器高级编辑](doc/cn_server_edit_adv.png) + +### 客户端列表 +![客户端列表](doc/cn_client_list.png) + +### 客户端编辑 +![客户端编辑](doc/cn_client_edit.png) + +### 客户端高级编辑 +![客户端高级编辑](doc/cn_client_edit_adv.png) + +### 客户端统计 +![客户端统计](doc/cn_client_stats.png) + +### 实时日志 +![实时日志](doc/cn_realtime_log.png) + +### 远程控制台 +![远程控制台](doc/cn_remote_console.png) diff --git a/biz/master/platform/get_clients_status.go b/biz/master/platform/get_clients_status.go index b838419..e2c8da6 100644 --- a/biz/master/platform/get_clients_status.go +++ b/biz/master/platform/get_clients_status.go @@ -56,7 +56,7 @@ func GetClientsStatus(c context.Context, req *pb.GetClientsStatusRequest) (*pb.G proto.Unmarshal(tresp.GetData(), clientVersion) connectTime, ok := mgr.ConnectTime(clientID) if !ok { - connectTime = endTime + connectTime = time.Time{} } resps[clientID] = &pb.ClientStatus{ @@ -66,7 +66,7 @@ func GetClientsStatus(c context.Context, req *pb.GetClientsStatusRequest) (*pb.G Ping: int32(pingTime), Version: clientVersion, Addr: lo.ToPtr(mgr.ClientAddr(clientID)), - ConnectTime: lo.ToPtr(int32(endTime.Sub(connectTime).Seconds())), + ConnectTime: lo.ToPtr(int32(connectTime.UnixMilli())), } } diff --git a/doc/clients_menu.png b/doc/clients_menu.png deleted file mode 100644 index 70cc646..0000000 Binary files a/doc/clients_menu.png and /dev/null differ diff --git a/doc/cn_client_edit.png b/doc/cn_client_edit.png new file mode 100644 index 0000000..13988f3 Binary files /dev/null and b/doc/cn_client_edit.png differ diff --git a/doc/cn_client_edit_adv.png b/doc/cn_client_edit_adv.png new file mode 100644 index 0000000..88e7057 Binary files /dev/null and b/doc/cn_client_edit_adv.png differ diff --git a/doc/cn_client_list.png b/doc/cn_client_list.png new file mode 100644 index 0000000..69e65cf Binary files /dev/null and b/doc/cn_client_list.png differ diff --git a/doc/cn_client_stats.png b/doc/cn_client_stats.png new file mode 100644 index 0000000..f3b2db7 Binary files /dev/null and b/doc/cn_client_stats.png differ diff --git a/doc/cn_index.png b/doc/cn_index.png new file mode 100644 index 0000000..bea3932 Binary files /dev/null and b/doc/cn_index.png differ diff --git a/doc/cn_realtime_log.png b/doc/cn_realtime_log.png new file mode 100644 index 0000000..3f46c40 Binary files /dev/null and b/doc/cn_realtime_log.png differ diff --git a/doc/cn_remote_console.png b/doc/cn_remote_console.png new file mode 100644 index 0000000..1363f49 Binary files /dev/null and b/doc/cn_remote_console.png differ diff --git a/doc/cn_server_edit.png b/doc/cn_server_edit.png new file mode 100644 index 0000000..04db248 Binary files /dev/null and b/doc/cn_server_edit.png differ diff --git a/doc/cn_server_edit_adv.png b/doc/cn_server_edit_adv.png new file mode 100644 index 0000000..02c442e Binary files /dev/null and b/doc/cn_server_edit_adv.png differ diff --git a/doc/cn_server_list.png b/doc/cn_server_list.png new file mode 100644 index 0000000..b30cb51 Binary files /dev/null and b/doc/cn_server_list.png differ diff --git a/doc/create_client.png b/doc/create_client.png deleted file mode 100644 index 4d450c8..0000000 Binary files a/doc/create_client.png and /dev/null differ diff --git a/doc/create_server.png b/doc/create_server.png deleted file mode 100644 index 2a206d0..0000000 Binary files a/doc/create_server.png and /dev/null differ diff --git a/doc/edit_client.png b/doc/edit_client.png deleted file mode 100644 index d7009e8..0000000 Binary files a/doc/edit_client.png and /dev/null differ diff --git a/doc/edit_client_adv.png b/doc/edit_client_adv.png deleted file mode 100644 index 7d7978d..0000000 Binary files a/doc/edit_client_adv.png and /dev/null differ diff --git a/doc/edit_server.png b/doc/edit_server.png deleted file mode 100644 index 148e52f..0000000 Binary files a/doc/edit_server.png and /dev/null differ diff --git a/doc/edit_server_adv.png b/doc/edit_server_adv.png deleted file mode 100644 index 35a91d4..0000000 Binary files a/doc/edit_server_adv.png and /dev/null differ diff --git a/doc/en_client_edit.png b/doc/en_client_edit.png new file mode 100644 index 0000000..9bf2e90 Binary files /dev/null and b/doc/en_client_edit.png differ diff --git a/doc/en_client_edit_adv.png b/doc/en_client_edit_adv.png new file mode 100644 index 0000000..6d35375 Binary files /dev/null and b/doc/en_client_edit_adv.png differ diff --git a/doc/en_client_list.png b/doc/en_client_list.png new file mode 100644 index 0000000..9424932 Binary files /dev/null and b/doc/en_client_list.png differ diff --git a/doc/en_client_stats.png b/doc/en_client_stats.png new file mode 100644 index 0000000..7993dec Binary files /dev/null and b/doc/en_client_stats.png differ diff --git a/doc/en_index.png b/doc/en_index.png new file mode 100644 index 0000000..d3cfe89 Binary files /dev/null and b/doc/en_index.png differ diff --git a/doc/en_realtime_log.png b/doc/en_realtime_log.png new file mode 100644 index 0000000..2732c39 Binary files /dev/null and b/doc/en_realtime_log.png differ diff --git a/doc/en_remote_console.png b/doc/en_remote_console.png new file mode 100644 index 0000000..903ec73 Binary files /dev/null and b/doc/en_remote_console.png differ diff --git a/doc/en_server_edit.png b/doc/en_server_edit.png new file mode 100644 index 0000000..2f448b1 Binary files /dev/null and b/doc/en_server_edit.png differ diff --git a/doc/en_server_edit_adv.png b/doc/en_server_edit_adv.png new file mode 100644 index 0000000..0a5379b Binary files /dev/null and b/doc/en_server_edit_adv.png differ diff --git a/doc/en_server_list.png b/doc/en_server_list.png new file mode 100644 index 0000000..6935e6b Binary files /dev/null and b/doc/en_server_list.png differ diff --git a/doc/login.png b/doc/login.png deleted file mode 100644 index 7791013..0000000 Binary files a/doc/login.png and /dev/null differ diff --git a/doc/platform_info.png b/doc/platform_info.png deleted file mode 100644 index b344ef8..0000000 Binary files a/doc/platform_info.png and /dev/null differ diff --git a/doc/register.png b/doc/register.png deleted file mode 100644 index 0367e14..0000000 Binary files a/doc/register.png and /dev/null differ diff --git a/doc/server_menu.png b/doc/server_menu.png deleted file mode 100644 index 1cc67af..0000000 Binary files a/doc/server_menu.png and /dev/null differ diff --git a/doc/traffic_statistics.png b/doc/traffic_statistics.png deleted file mode 100644 index 911c5dd..0000000 Binary files a/doc/traffic_statistics.png and /dev/null differ diff --git a/www/api/user.ts b/www/api/user.ts index c1ff038..64f565f 100644 --- a/www/api/user.ts +++ b/www/api/user.ts @@ -6,12 +6,13 @@ import { UpdateUserInfoRequest, UpdateUserInfoResponse, } from '@/lib/pb/api_user' -import { $userInfo } from '@/store/user' +import { $statusOnline, $userInfo } from '@/store/user' import { BaseResponse } from '@/types/api' export const getUserInfo = async (req: GetUserInfoRequest) => { const res = await http.post(API_PATH + '/user/get', GetUserInfoRequest.toJson(req)) $userInfo.set(GetUserInfoResponse.fromJson((res.data as BaseResponse).body).userInfo) + $statusOnline.set(!!GetUserInfoResponse.fromJson((res.data as BaseResponse).body).userInfo) return GetUserInfoResponse.fromJson((res.data as BaseResponse).body) } diff --git a/www/components/app-sidebar.tsx b/www/components/app-sidebar.tsx index 50bf6d1..5e845c7 100644 --- a/www/components/app-sidebar.tsx +++ b/www/components/app-sidebar.tsx @@ -27,63 +27,17 @@ import { RegisterAndLogin } from "./header" import { useRouter } from "next/navigation" import { useQuery } from "@tanstack/react-query" import { getPlatformInfo } from "@/api/platform" - -const data = { - teams: [ - { - name: "Frp-Panel", - logo: TbBuildingTunnel, - plan: "Community Edition", - url: "/", - }, - ], - navMain: [ - { - title: "客户端", - url: "/clients", - icon: MonitorSmartphoneIcon, - isActive: true, - }, - { - title: "服务端", - url: "/servers", - icon: ServerIcon, - }, - { - title: "编辑隧道", - url: "/clientedit", - icon: MonitorCogIcon, - }, - { - title: "编辑服务端", - url: "/serveredit", - icon: ServerCogIcon, - }, - { - title: "流量统计", - url: "/clientstats", - icon: ChartNetworkIcon, - }, - { - title: "实时日志", - url: "/streamlog", - icon: Scroll, - }, - { - title: "控制台", - url: "/console", - icon: SquareTerminal, - }, - ] -} +import { teams, getNavItems } from '@/config/nav' +import { useTranslation } from 'react-i18next' export interface AppSidebarProps extends React.ComponentProps { - chrildren?: React.ReactNode + children?: React.ReactNode footer?: React.ReactNode } export function AppSidebar({ ...props }: AppSidebarProps) { const router = useRouter() + const { t } = useTranslation() const userInfo = useStore($userInfo) const { data: platformInfo } = useQuery({ queryKey: ['platformInfo'], @@ -107,12 +61,12 @@ export function AppSidebar({ ...props }: AppSidebarProps) {
- Frp-Panel + {t('app.title')} - frp隧道面板 + {t('app.subtitle')}
- + {props.children} diff --git a/www/components/base/client-selector.tsx b/www/components/base/client-selector.tsx index c252e55..92bd8e6 100644 --- a/www/components/base/client-selector.tsx +++ b/www/components/base/client-selector.tsx @@ -1,14 +1,23 @@ +"use client" + import React from 'react' import { keepPreviousData, useQuery } from '@tanstack/react-query' import { listClient } from '@/api/client' import { Combobox } from './combobox' +import { useTranslation } from 'react-i18next' export interface ClientSelectorProps { clientID?: string setClientID: (clientID: string) => void onOpenChange?: () => void } -export const ClientSelector: React.FC = ({ clientID, setClientID, onOpenChange }) => { + +export const ClientSelector: React.FC = ({ + clientID, + setClientID, + onOpenChange +}) => { + const { t } = useTranslation() const handleClientChange = (value: string) => { setClientID(value) } const [keyword, setKeyword] = React.useState('') @@ -22,8 +31,11 @@ export const ClientSelector: React.FC = ({ clientID, setCli return ( ({ value: client.id || '', label: client.id || '' })) || []} + placeholder={t('selector.client.placeholder')} + dataList={clientList?.clients.map((client) => ({ + value: client.id || '', + label: client.id || '' + })) || []} setValue={handleClientChange} value={clientID} onKeyWordChange={setKeyword} diff --git a/www/components/base/client_detail.tsx b/www/components/base/client_detail.tsx index 7cdf703..9afb5eb 100644 --- a/www/components/base/client_detail.tsx +++ b/www/components/base/client_detail.tsx @@ -1,42 +1,70 @@ +"use client" + import { Popover, PopoverTrigger } from "@radix-ui/react-popover" import { Badge } from "../ui/badge" import { ClientStatus } from "@/lib/pb/api_master" import { PopoverContent } from "../ui/popover" +import { useTranslation } from "react-i18next" +import { motion } from "framer-motion" +import { formatDistanceToNow } from 'date-fns' +import { zhCN, enUS } from 'date-fns/locale' export const ClientDetail = ({ clientStatus }: { clientStatus: ClientStatus }) => { + const { t, i18n } = useTranslation() + + const locale = i18n.language === 'zh' ? zhCN : enUS + const connectTime = clientStatus.connectTime ? + formatDistanceToNow(new Date(clientStatus.connectTime), { + addSuffix: true, + locale + }) : '-' + return ( - - {clientStatus.version?.gitVersion} + + {clientStatus.version?.gitVersion || 'Unknown'} - -

客户端信息

-
- 版本: - {clientStatus.version?.gitVersion} -
-
- 编译时间: - {clientStatus.version?.buildDate} -
-
- Go版本: - {clientStatus.version?.goVersion} -
-
- 客户端平台: - {clientStatus.version?.platform} -
-
- 客户端地址: - {clientStatus.addr} -
-
- 连接时间: - {clientStatus.connectTime} -
+ + +

+ {t('client.detail.title')} +

+
+
+ {t('client.detail.version')} + {clientStatus.version?.gitVersion || '-'} +
+
+ {t('client.detail.buildDate')} + {clientStatus.version?.buildDate || '-'} +
+
+ {t('client.detail.goVersion')} + {clientStatus.version?.goVersion || '-'} +
+
+ {t('client.detail.platform')} + {clientStatus.version?.platform || '-'} +
+
+ {t('client.detail.address')} + {clientStatus.addr || '-'} +
+
+ {t('client.detail.connectTime')} + {connectTime} +
+
+
) diff --git a/www/components/base/combobox.tsx b/www/components/base/combobox.tsx index 27f8d3a..63621ec 100644 --- a/www/components/base/combobox.tsx +++ b/www/components/base/combobox.tsx @@ -2,10 +2,10 @@ import * as React from "react" import { Check, ChevronsUpDown } from "lucide-react" - import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" -import { useDebouncedCallback } from 'use-debounce'; +import { useDebouncedCallback } from 'use-debounce' +import { useTranslation } from 'react-i18next' import { Command, CommandEmpty, @@ -33,14 +33,30 @@ export interface ComboboxProps { isLoading?: boolean } -export function Combobox({ value, setValue, dataList, placeholder, notFoundText, onOpenChange, className, keyword, onKeyWordChange, isLoading }: ComboboxProps) { +export function Combobox({ + value, + setValue, + dataList, + placeholder, + notFoundText, + onOpenChange, + className, + keyword, + onKeyWordChange, + isLoading +}: ComboboxProps) { + const { t } = useTranslation() const [open, setOpen] = React.useState(false) const debounced = useDebouncedCallback( - (v) => { - onKeyWordChange && onKeyWordChange(v as string); - }, - 500, - ); + (v) => { + onKeyWordChange && onKeyWordChange(v as string); + }, + 500, + ); + + const defaultPlaceholder = t('selector.common.placeholder') + const defaultNotFoundText = t('selector.common.notFound') + const loadingText = t('selector.common.loading') return ( { @@ -55,15 +71,19 @@ export function Combobox({ value, setValue, dataList, placeholder, notFoundText, > {value ? (dataList.find((item) => item.value === value)?.label || value) - : (placeholder||"请选择...")} + : (placeholder || defaultPlaceholder)} - debounced(v)} placeholder={`${placeholder||"请选择..."}`} /> + debounced(v)} + placeholder={placeholder || defaultPlaceholder} + /> - {isLoading ? "加载中..." : notFoundText||"未找到结果"} + {isLoading ? loadingText : (notFoundText || defaultNotFoundText)} {dataList.map((item) => ( { columns: ColumnDef[] @@ -27,12 +28,14 @@ interface DataTableProps { } export function DataTable({ columns, filterColumnName, table }: DataTableProps) { + const { t } = useTranslation() + return (
{filterColumnName && (
table.getColumn(filterColumnName)?.setFilterValue(event.target.value)} className="max-w-sm" @@ -66,7 +69,7 @@ export function DataTable({ columns, filterColumnName, table }: D ) : ( - 没有数据 + {t('table.noData')} )} diff --git a/www/components/base/data_table_pagination.tsx b/www/components/base/data_table_pagination.tsx index b0adceb..b7d7ca9 100644 --- a/www/components/base/data_table_pagination.tsx +++ b/www/components/base/data_table_pagination.tsx @@ -1,20 +1,18 @@ import { ChevronLeftIcon, ChevronRightIcon, DoubleArrowLeftIcon, DoubleArrowRightIcon } from '@radix-ui/react-icons' import { Table } from '@tanstack/react-table' - import { Button } from '@/components/ui/button' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { useTranslation } from 'react-i18next' interface DataTablePaginationProps { table: Table } export function DataTablePagination({ table }: DataTablePaginationProps) { + const { t } = useTranslation() + return (
- {/*
- {table.getFilteredSelectedRowModel().rows.length} of{" "} - {table.getFilteredRowModel().rows.length} row(s) selected. -
*/}
-

行 每页

+

{t('table.pagination.rowsPerPage')}

- 第 {table.getState().pagination.pageIndex + 1} 页, 共 {table.getPageCount()}页 + {t('table.pagination.page', { + current: table.getState().pagination.pageIndex + 1, + total: table.getPageCount() + })}
diff --git a/www/components/base/id_input.tsx b/www/components/base/id_input.tsx index 2b0671b..0edd18e 100644 --- a/www/components/base/id_input.tsx +++ b/www/components/base/id_input.tsx @@ -1,6 +1,9 @@ +"use client" + import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" import { useState } from "react" +import { useTranslation } from "react-i18next" export interface IdInputProps { setKeyword: (keyword: string) => void @@ -9,13 +12,27 @@ export interface IdInputProps { } export const IdInput: React.FC = ({ setKeyword, keyword, refetchTrigger }) => { + const { t } = useTranslation() const [input, setInput] = useState(keyword) - return
- setInput(e.target.value)}> - -
+ return ( +
+ setInput(e.target.value)} + /> + +
+ ) } diff --git a/www/components/base/proxy-selector.tsx b/www/components/base/proxy-selector.tsx index 254ae9b..323d8cb 100644 --- a/www/components/base/proxy-selector.tsx +++ b/www/components/base/proxy-selector.tsx @@ -1,5 +1,8 @@ +"use client" + import React from 'react' import { Combobox } from './combobox' +import { useTranslation } from 'react-i18next' export interface ProxySelectorProps { proxyName?: string @@ -7,12 +10,23 @@ export interface ProxySelectorProps { proxyNames: string[] } -export const ProxySelector: React.FC = ({ proxyName, proxyNames ,setProxyname }) => { - return ({ value: name, label: name }))} - value={proxyName} - setValue={setProxyname} - notFoundText="未找到隧道" - placeholder="隧道名称" - /> +export const ProxySelector: React.FC = ({ + proxyName, + proxyNames, + setProxyname +}) => { + const { t } = useTranslation() + + return ( + ({ + value: name, + label: name + }))} + value={proxyName} + setValue={setProxyname} + notFoundText={t('selector.proxy.notFound')} + placeholder={t('selector.proxy.placeholder')} + /> + ) } diff --git a/www/components/base/selector.tsx b/www/components/base/selector.tsx index 6d8f915..95f57f0 100644 --- a/www/components/base/selector.tsx +++ b/www/components/base/selector.tsx @@ -1,3 +1,5 @@ +"use client" + import * as React from "react" import { @@ -10,6 +12,7 @@ import { SelectValue, } from "@/components/ui/select" import { cn } from "@/lib/utils" +import { useTranslation } from "react-i18next" export interface BaseSelectorProps { value?: string @@ -21,22 +24,31 @@ export interface BaseSelectorProps { className?: string } -export function BaseSelector({ value, setValue, dataList, placeholder, label, onOpenChange, className }: BaseSelectorProps) { +export function BaseSelector({ + value, + setValue, + dataList, + placeholder, + label, + onOpenChange, + className +}: BaseSelectorProps) { + const { t } = useTranslation() + const defaultPlaceholder = t('selector.common.placeholder') + return ( diff --git a/www/components/base/server-selector.tsx b/www/components/base/server-selector.tsx index 86d77bb..f1e9f4b 100644 --- a/www/components/base/server-selector.tsx +++ b/www/components/base/server-selector.tsx @@ -1,14 +1,23 @@ +"use client" + import React from 'react' import { keepPreviousData, useQuery } from '@tanstack/react-query' import { listServer } from '@/api/server' import { Combobox } from './combobox' +import { useTranslation } from 'react-i18next' export interface ServerSelectorProps { serverID?: string setServerID: (serverID: string) => void onOpenChange?: () => void } -export const ServerSelector: React.FC = ({ serverID, setServerID, onOpenChange }) => { + +export const ServerSelector: React.FC = ({ + serverID, + setServerID, + onOpenChange +}) => { + const { t } = useTranslation() const handleServerChange = (value: string) => { setServerID(value) } const [keyword, setKeyword] = React.useState('') @@ -20,15 +29,20 @@ export const ServerSelector: React.FC = ({ serverID, setSer placeholderData: keepPreviousData, }) - return ( ({ value: server.id || '', label: server.id || '' })) || []} - onKeyWordChange={setKeyword} - onOpenChange={() => { - onOpenChange && onOpenChange() - refetchServers() - }} - />) + return ( + ({ + value: server.id || '', + label: server.id || '' + })) || []} + onKeyWordChange={setKeyword} + onOpenChange={() => { + onOpenChange && onOpenChange() + refetchServers() + }} + /> + ) } diff --git a/www/components/base/status.tsx b/www/components/base/status.tsx index d83e82d..12aa620 100644 --- a/www/components/base/status.tsx +++ b/www/components/base/status.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; type Status = 'loading' | 'success' | 'error' ; @@ -13,16 +13,6 @@ const statusColors: Record = { }; const LoadingCircle: React.FC = ({ status }) => { - const [isVisible, setIsVisible] = useState(true); - - useEffect(() => { - const intervalId = setInterval(() => { - setIsVisible((prev) => !prev); - }, 1000); - - return () => clearInterval(intervalId); - }, []); - let { outer, inner } = { outer: 'bg-gray-200', inner: 'bg-gray-500' } if (status) { const { outer: o, inner: i } = statusColors[status]; @@ -32,8 +22,18 @@ const LoadingCircle: React.FC = ({ status }) => { return (
-
-
+
+
+
+
); diff --git a/www/components/charts/proxy-traffic-bar-chart.tsx b/www/components/charts/proxy-traffic-bar-chart.tsx index 70b4795..c33dcf9 100644 --- a/www/components/charts/proxy-traffic-bar-chart.tsx +++ b/www/components/charts/proxy-traffic-bar-chart.tsx @@ -9,25 +9,28 @@ import { ChartTooltip, ChartTooltipContent, } from "@/components/ui/chart" +import { useTranslation } from "react-i18next" export function ProxyTrafficBarChart({ proxyInfo }:{ proxyInfo: ProxyInfo }) { + const { t } = useTranslation() + const data = [ { - name: "入站", - Today: Number(proxyInfo.todayTrafficIn), - History: Number(proxyInfo.historyTrafficIn), + name: t('traffic.chart.inbound'), + [t('traffic.chart.today')]: Number(proxyInfo.todayTrafficIn), + [t('traffic.chart.history')]: Number(proxyInfo.historyTrafficIn), }, { - name: "出站", - Today: Number(proxyInfo.todayTrafficOut), - History: Number(proxyInfo.historyTrafficOut), + name: t('traffic.chart.outbound'), + [t('traffic.chart.today')]: Number(proxyInfo.todayTrafficOut), + [t('traffic.chart.history')]: Number(proxyInfo.historyTrafficOut), }, ] return ( - 流量详情 + {t('traffic.chart.title')} @@ -42,8 +45,8 @@ export function ProxyTrafficBarChart({ proxyInfo }:{ proxyInfo: ProxyInfo }) { formatBytes(Number(value))} /> formatBytes(Number(value))} /> - - + + @@ -51,4 +54,3 @@ export function ProxyTrafficBarChart({ proxyInfo }:{ proxyInfo: ProxyInfo }) { ) } - diff --git a/www/components/charts/proxy-traffic-overview.tsx b/www/components/charts/proxy-traffic-overview.tsx index e79fb16..9d9e2af 100644 --- a/www/components/charts/proxy-traffic-overview.tsx +++ b/www/components/charts/proxy-traffic-overview.tsx @@ -2,13 +2,16 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { ProxyInfo } from "@/lib/pb/common" import { formatBytes } from "@/lib/utils" import { CloudDownload, CloudUpload } from "lucide-react" +import { useTranslation } from "react-i18next" export function ProxyTrafficOverview({ proxyInfo }: { proxyInfo: ProxyInfo }) { + const { t } = useTranslation() + return (
- 今日入站流量 + {t('traffic.today.inbound')} @@ -17,7 +20,7 @@ export function ProxyTrafficOverview({ proxyInfo }: { proxyInfo: ProxyInfo }) { - 今日出站流量 + {t('traffic.today.outbound')} @@ -26,7 +29,7 @@ export function ProxyTrafficOverview({ proxyInfo }: { proxyInfo: ProxyInfo }) { - 历史入站流量 + {t('traffic.history.inbound')} @@ -35,7 +38,7 @@ export function ProxyTrafficOverview({ proxyInfo }: { proxyInfo: ProxyInfo }) { - 历史出站流量 + {t('traffic.history.outbound')} diff --git a/www/components/charts/proxy-traffic-pie-chart.tsx b/www/components/charts/proxy-traffic-pie-chart.tsx index 6bef42b..2ee1615 100644 --- a/www/components/charts/proxy-traffic-pie-chart.tsx +++ b/www/components/charts/proxy-traffic-pie-chart.tsx @@ -9,15 +9,7 @@ import { ChartTooltip, ChartTooltipContent, } from "@/components/ui/chart" - -const chartConfig = { - trafficIn: { - label: "入站", - }, - trafficOut: { - label: "出站", - }, -} satisfies ChartConfig +import { useTranslation } from "react-i18next" export function ProxyTrafficPieChart({ trafficIn, trafficOut, title, chartLabel }: { trafficIn: bigint, @@ -25,6 +17,17 @@ export function ProxyTrafficPieChart({ trafficIn, trafficOut, title, chartLabel title: string, chartLabel: string, }) { + const { t } = useTranslation() + + const chartConfig = { + trafficIn: { + label: t('traffic.chart.pie.inbound'), + }, + trafficOut: { + label: t('traffic.chart.pie.outbound'), + }, + } satisfies ChartConfig + const data = [ { type: "trafficIn", data: Number(trafficIn), fill: "hsl(var(--chart-1))" }, { type: "trafficOut", data: Number(trafficOut), fill: "hsl(var(--chart-2))" }] @@ -71,4 +74,3 @@ export function ProxyTrafficPieChart({ trafficIn, trafficOut, title, chartLabel ) } - diff --git a/www/components/frpc/client_create_dialog.tsx b/www/components/frpc/client_create_dialog.tsx index ddc17de..c5dc2ff 100644 --- a/www/components/frpc/client_create_dialog.tsx +++ b/www/components/frpc/client_create_dialog.tsx @@ -1,3 +1,5 @@ +"use client" + import i18n from '@/lib/i18n' import { useState } from 'react' import { useMutation, useQuery } from '@tanstack/react-query' @@ -27,17 +29,17 @@ export const CreateClientDialog = ({refetchTrigger}: {refetchTrigger?: (randStr: const { toast } = useToast() const handleNewClient = async () => { - toast({ title: t('已提交创建请求') }) + toast({ title: t('client.create.submitting') }) try { let resp = await newClient.mutateAsync({ clientId: clientID }) if (resp.status?.code !== RespCode.SUCCESS) { - toast({ title: t('创建客户端失败') }) + toast({ title: t('client.create.error') }) return } - toast({ title: t('创建客户端成功') }) + toast({ title: t('client.create.success') }) refetchTrigger && refetchTrigger(JSON.stringify(Math.random())) } catch (error) { - toast({ title: t('创建客户端失败') }) + toast({ title: t('client.create.error') }) } } @@ -45,19 +47,19 @@ export const CreateClientDialog = ({refetchTrigger}: {refetchTrigger?: (randStr: - {t('新建客户端')} - {t('创建新的客户端用于连接,客户端ID必须唯一')} + {t('client.create.title')} + {t('client.create.description')} - + setClientID(e.target.value)} /> - + diff --git a/www/components/frpc/client_item.tsx b/www/components/frpc/client_item.tsx index e574fd9..8163942 100644 --- a/www/components/frpc/client_item.tsx +++ b/www/components/frpc/client_item.tsx @@ -1,4 +1,4 @@ -import { ColumnDef, Table } from '@tanstack/react-table' +import { ColumnDef, Table, TableMeta } from '@tanstack/react-table' import { MoreHorizontal } from 'lucide-react' import { Dialog, @@ -34,8 +34,10 @@ import { ClientType } from '@/lib/pb/common' import { ClientStatus, ClientStatus_Status } from '@/lib/pb/api_master' import { startFrpc, stopFrpc } from '@/api/frp' import { Badge } from '../ui/badge' +import { Label } from '@/components/ui/label' import { ClientDetail } from '../base/client_detail' import { Input } from '../ui/input' +import { useTranslation } from 'react-i18next' export type ClientTableSchema = { id: string @@ -46,34 +48,45 @@ export type ClientTableSchema = { config?: string } +export interface TableMetaType extends TableMeta { + refetch: () => void +} + export const columns: ColumnDef[] = [ { accessorKey: 'id', - header: 'ID(点击查看安装命令)', + header: function Header() { + const { t } = useTranslation() + return t('client.id') + }, cell: ({ row }) => { return }, }, { accessorKey: 'status', - header: '是否配置', + header: function Header() { + const { t } = useTranslation() + return t('client.status') + }, cell: ({ row }) => { - const client = row.original - return ( -
- { - { - valid: '已配置', - invalid: '未配置', - }[client.status] - } -
- ) + function Cell({ client }: { client: ClientTableSchema }) { + const { t } = useTranslation() + return ( +
+ {client.status === 'valid' ? t('client.status_configured') : t('client.status_unconfigured')} +
+ ) + } + return }, }, { accessorKey: 'info', - header: '运行信息/版本信息', + header: function Header() { + const { t } = useTranslation() + return t('client.info') + }, cell: ({ row }) => { const client = row.original return @@ -81,7 +94,10 @@ export const columns: ColumnDef[] = [ }, { accessorKey: 'secret', - header: '连接密钥(点击查看启动命令)', + header: function Header() { + const { t } = useTranslation() + return t('client.secret') + }, cell: ({ row }) => { const client = row.original return @@ -91,39 +107,78 @@ export const columns: ColumnDef[] = [ id: 'action', cell: ({ row, table }) => { const client = row.original - return + return & { options: { meta: TableMetaType } }} /> }, }, ] export const ClientID = ({ client }: { client: ClientTableSchema }) => { + const { t } = useTranslation() const platformInfo = useStore($platformInfo) + + if (!platformInfo) { + return ( + + ) + } + return ( -
{client.id}
+
- -
请点击命令框全选复制
-
Linux安装到systemd
- -
Windows安装到系统服务
- - + +
+
+

{t('client.install.title')}

+

{t('client.install.description')}

+
+
+
+ + +
+
+ + +
+
+
) } export const ClientInfo = ({ client }: { client: ClientTableSchema }) => { - const clientsInfo = useQuery({ - queryKey: ['getClientsStatus', [client.id]], + const { t } = useTranslation() + const { data: clientsStatus } = useQuery({ + queryKey: ['clientsStatus', client.id], queryFn: async () => { return await getClientsStatus({ clientIds: [client.id], @@ -133,61 +188,87 @@ export const ClientInfo = ({ client }: { client: ClientTableSchema }) => { }) const trans = (info: ClientStatus | undefined) => { - let statusText: '在线' | '离线' | '错误' | '暂停' | '未知' = '未知' + let statusText: 'client.status_online' | 'client.status_offline' | + 'client.status_error' | 'client.status_pause' | + 'client.status_unknown' = 'client.status_unknown' if (info === undefined) { return statusText } if (info.status === ClientStatus_Status.ONLINE) { - statusText = '在线' + statusText = 'client.status_online' if (client.stopped) { - statusText = '暂停' + statusText = 'client.status_pause' } } else if (info.status === ClientStatus_Status.OFFLINE) { - statusText = '离线' + statusText = 'client.status_offline' } else if (info.status === ClientStatus_Status.ERROR) { - statusText = '错误' + statusText = 'client.status_error' } return statusText } const infoColor = - clientsInfo.data?.clients[client.id]?.status === ClientStatus_Status.ONLINE ? ( + clientsStatus?.clients[client.id]?.status === ClientStatus_Status.ONLINE ? ( client.stopped ? 'text-yellow-500' : 'text-green-500') : 'text-red-500' return (
- {`${clientsInfo.data?.clients[client.id].ping}ms,${trans(clientsInfo.data?.clients[client.id])}`} + {`${clientsStatus?.clients[client.id].ping}ms,${t(trans(clientsStatus?.clients[client.id]))}`} - {clientsInfo.data?.clients[client.id].version && - + {clientsStatus?.clients[client.id].version && + }
) } export const ClientSecret = ({ client }: { client: ClientTableSchema }) => { - const [showSecrect, setShowSecrect] = useState(false) - const fakeSecret = Array.from({ length: client.secret.length }) - .map(() => '*') - .join('') + const { t } = useTranslation() const platformInfo = useStore($platformInfo) - const { toast } = useToast() + + if (!platformInfo) { + return ( + + ) + } + return ( -
setShowSecrect(true)} - onMouseLeave={() => setShowSecrect(false)} - className="font-medium hover:rounded hover:bg-slate-100 p-2 font-mono whitespace-nowrap" - > - {showSecrect ? client.secret : fakeSecret} +
+ + {client.secret} + + + {'*'.repeat(client.secret.length)} +
- -
运行命令(需要点击这里自行下载文件)
-
- {platformInfo === undefined ? '获取平台信息失败' : ExecCommandStr('client', client, platformInfo)} + +
+
+

{t('client.start.title')}

+

+ {t('client.install.description')} ({t('common.download')}) +

+
+
+
+              {ExecCommandStr('client', client, platformInfo)}
+            
+ +
@@ -200,52 +281,54 @@ export interface ClientItemProps { } export const ClientActions: React.FC = ({ client, table }) => { + const { t } = useTranslation() const { toast } = useToast() const router = useRouter() const platformInfo = useStore($platformInfo) + // placeholder for refetch const refetchList = () => {} const removeClient = useMutation({ mutationFn: deleteClient, onSuccess: () => { - toast({ description: '删除成功' }) + toast({ description: t('client.delete.success') }) refetchList() }, onError: () => { - toast({ description: '删除失败' }) + toast({ description: t('client.delete.failed') }) }, }) const stopClient = useMutation({ mutationFn: stopFrpc, onSuccess: () => { - toast({ description: '停止成功' }) + toast({ description: t('client.operation.stop_success') }) refetchList() }, onError: () => { - toast({ description: '停止失败' }) + toast({ description: t('client.operation.stop_failed') }) }, }) const startClient = useMutation({ mutationFn: startFrpc, onSuccess: () => { - toast({ description: '启动成功' }) + toast({ description: t('client.operation.start_success') }) refetchList() }, onError: () => { - toast({ description: '启动失败' }) + toast({ description: t('client.operation.start_failed') }) }, }) const createAndDownloadFile = (fileName: string, content: string) => { - var aTag = document.createElement('a'); - var blob = new Blob([content]); - aTag.download = fileName; - aTag.href = URL.createObjectURL(blob); - aTag.click(); - URL.revokeObjectURL(aTag.href); + const aTag = document.createElement('a') + const blob = new Blob([content]) + aTag.download = fileName + aTag.href = URL.createObjectURL(blob) + aTag.click() + URL.revokeObjectURL(aTag.href) } return ( @@ -253,27 +336,27 @@ export const ClientActions: React.FC = ({ client, table }) => { - 操作 + {t('client.actions_menu.title')} { try { if (platformInfo) { navigator.clipboard.writeText(ExecCommandStr('client', client, platformInfo)) - toast({ description: '复制成功,如果复制不成功,请点击ID字段手动复制' }) + toast({ description: t('client.actions_menu.copy_success') }) } else { - toast({ description: '获取平台信息失败,如果复制不成功,请点击ID字段手动复制' }) + toast({ description: t('client.actions_menu.copy_failed') }) } } catch (error) { - toast({ description: '获取平台信息失败,如果复制不成功,请点击ID字段手动复制' }) + toast({ description: t('client.actions_menu.copy_failed') }) } }} > - 复制启动命令 + {t('client.actions_menu.copy_command')} = ({ client, table }) => { router.push({ pathname: '/clientedit', query: { clientID: client.id } }) }} > - 修改配置 + {t('client.actions_menu.edit_config')} { try { if (platformInfo) { - createAndDownloadFile(`.env`, ClientEnvFile(client, platformInfo)) + createAndDownloadFile('.env', ClientEnvFile(client, platformInfo)) } - } - catch (error) { - toast({ description: '获取平台信息失败' }) + } catch (error) { + toast({ description: t('client.actions_menu.download_failed') }) } }} > - 下载配置 + {t('client.actions_menu.download_config')} { router.push({ pathname: '/streamlog', query: { clientID: client.id, clientType: ClientType.FRPC.toString() } }) }} > - 实时日志 + {t('client.actions_menu.realtime_log')} { router.push({ pathname: '/console', query: { clientID: client.id, clientType: ClientType.FRPC.toString() } }) }} > - 远程终端 + {t('client.actions_menu.remote_terminal')} - {!client.stopped && stopClient.mutate({ clientId: client.id })}>暂停} - {client.stopped && startClient.mutate({ clientId: client.id })}>启动} + {!client.stopped && ( + stopClient.mutate({ clientId: client.id })}> + {t('client.actions_menu.pause')} + + )} + {client.stopped && ( + startClient.mutate({ clientId: client.id })}> + {t('client.actions_menu.resume')} + + )} - 删除 + {t('client.actions_menu.delete')} - 确定删除该客户端? + {t('client.delete.title')} -

此操作无法撤消。您确定要永久从我们的服务器中删除该客户端?

+

{t('client.delete.description')}

- 删除后运行中的客户端将无法通过现有参数再次连接,如果您需要删除客户端对外的连接,可以选择暂停客户端 + {t('client.delete.warning')}

- + ) } diff --git a/www/components/frpc/frpc_card.tsx b/www/components/frpc/frpc_card.tsx index e0c18f8..cd320cb 100644 --- a/www/components/frpc/frpc_card.tsx +++ b/www/components/frpc/frpc_card.tsx @@ -1,3 +1,5 @@ +"use client" + import React, { useEffect } from 'react' import { useState } from 'react' import { Label } from '@radix-ui/react-label' @@ -12,15 +14,18 @@ import { ClientConfig } from '@/types/client' import { TypedProxyConfig } from '@/types/proxy' import { ClientSelector } from '../base/client-selector' import { ServerSelector } from '../base/server-selector' +import { useTranslation } from 'react-i18next' export interface FRPCFormCardProps { clientID?: string serverID?: string } + export const FRPCFormCard: React.FC = ({ clientID: defaultClientID, serverID: defaultServerID, }: FRPCFormCardProps) => { + const { t } = useTranslation() const [advanceMode, setAdvanceMode] = useState(false) const [clientID, setClientID] = useState() const [serverID, setServerID] = useState() @@ -78,31 +83,31 @@ export const FRPCFormCard: React.FC = ({ return ( - 编辑隧道 + {t('frpc.form.title')} -
注意⚠️:选择的「服务端」必须提前配置!
-
选择客户端和服务端以编辑隧道
+
{t('frpc.form.description.warning')}
+
{t('frpc.form.description.instruction')}
-
+
-

高级模式

-

编辑客户端原始配置文件

+

{t('frpc.form.advanced.title')}

+

{t('frpc.form.advanced.description')}

- + - +
{clientID && !advanceMode &&
- -

可以到高级模式修改备注哦!

+ +

{t('frpc.form.comment.hint')}

- {client?.client?.comment == undefined || client?.client?.comment === '' ? '空空如也' : client?.client?.comment} + {client?.client?.comment == undefined || client?.client?.comment === '' ? t('frpc.form.comment.empty') : client?.client?.comment}

} {clientID && serverID && !advanceMode && = ({ clientID, serverID, client, refetchClient }) => { + const { t } = useTranslation() const { toast } = useToast() const [configContent, setConfigContent] = useState('{}') @@ -26,12 +28,12 @@ export const FRPCEditor: React.FC = ({ clientID, serverID, client comment: clientComment, }) if (res.status?.code !== RespCode.SUCCESS) { - toast({ title: '更新失败' }) + toast({ title: t('client.operation.update_failed') }) return } - toast({ title: '更新成功' }) + toast({ title: t('client.operation.update_success') }) } catch (error) { - toast({ title: '更新失败' }) + toast({ title: t('client.operation.update_failed') }) } } @@ -66,22 +68,22 @@ export const FRPCEditor: React.FC = ({ clientID, serverID, client return (
- +