diff --git a/docs/API.cn.md b/docs/API.cn.md
new file mode 100644
index 0000000000..4d03c645e2
--- /dev/null
+++ b/docs/API.cn.md
@@ -0,0 +1,143 @@
+# API 文档
+
+要向 REST API v2 发送请求,您需要在每个请求中包含带有 Bearer 类型的 Authorization 头和令牌。
+
+```
+Authorization: Bearer {token}
+```
+
+## Inbounds
+
+### 获取所有 Inbounds
+- **方法**: `GET`
+- **端点**: `/inbounds/`
+- **描述**: 获取所有 inbounds 的列表。
+
+### 重置所有客户流量
+- **方法**: `DELETE`
+- **端点**: `/inbounds/traffic`
+- **描述**: 重置所有客户的流量。
+
+---
+
+## Inbound
+
+### 添加 Inbound
+- **方法**: `POST`
+- **端点**: `/inbounds/`
+- **描述**: 添加新的 inbound。
+
+### 通过 ID 获取 Inbound
+- **方法**: `GET`
+- **端点**: `/inbounds/:id`
+- **描述**: 通过 ID 获取特定 inbound 的信息。
+
+### 通过 ID 删除 Inbound
+- **方法**: `DELETE`
+- **端点**: `/inbounds/:id`
+- **描述**: 通过 ID 删除 inbound。
+
+### 通过 ID 更新 Inbound
+- **方法**: `PUT`
+- **端点**: `/inbounds/:id`
+- **描述**: 通过 ID 更新 inbound 的信息。
+
+### 删除 Inbound 流量
+- **方法**: `DELETE`
+- **端点**: `/inbounds/:id/traffic`
+- **描述**: 通过 ID 删除 inbound 流量。
+
+### 删除流量耗尽的客户
+- **方法**: `DELETE`
+- **端点**: `/inbounds/:id/depleted-clients`
+- **描述**: 删除特定 inbound 下流量已耗尽的客户。
+
+---
+
+## Inbound 客户
+
+### 获取 Inbound 客户
+- **方法**: `GET`
+- **端点**: `/inbounds/:id/clients/`
+- **描述**: 获取特定 inbound 的客户列表。
+
+---
+
+## Inbound 客户管理
+
+### 添加 Inbound 客户
+- **方法**: `POST`
+- **端点**: `/inbounds/:id/clients`
+- **描述**: 向 inbound 添加新的客户。
+
+### 通过 ID 获取客户
+- **方法**: `GET`
+- **端点**: `/inbounds/:id/clients/:clientId`
+- **描述**: 通过 ID 获取客户信息。
+
+### 更新 Inbound 客户
+- **方法**: `PUT`
+- **端点**: `/inbounds/:id/clients/:clientId`
+- **描述**: 通过 ID 更新客户信息。
+
+### 删除 Inbound 客户
+- **方法**: `DELETE`
+- **端点**: `/inbounds/:id/clients/:clientId`
+- **描述**: 通过 ID 删除客户。
+
+### 通过 ID 获取客户流量
+- **方法**: `GET`
+- **端点**: `/inbounds/:id/clients/:clientId/traffic`
+- **描述**: 通过 ID 获取客户流量统计信息。
+
+---
+
+## 通过电子邮件管理客户
+
+### 通过电子邮件获取客户
+- **方法**: `GET`
+- **端点**: `/inbounds/:id/clients/email/:email`
+- **描述**: 通过电子邮件获取客户信息。
+
+### 获取客户 IP 地址
+- **方法**: `GET`
+- **端点**: `/inbounds/:id/clients/email/:email/ips`
+- **描述**: 通过电子邮件获取客户的 IP 地址列表。
+
+### 清除客户 IP 地址
+- **方法**: `DELETE`
+- **端点**: `/inbounds/:id/clients/email/:email/ips`
+- **描述**: 清除客户的 IP 地址列表。
+
+### 通过电子邮件获取客户流量
+- **方法**: `GET`
+- **端点**: `/inbounds/:id/clients/email/:email/traffic`
+- **描述**: 通过电子邮件获取客户流量统计信息。
+
+### 通过电子邮件重置客户流量
+- **方法**: `DELETE`
+- **端点**: `/inbounds/:id/clients/email/:email/traffic`
+- **描述**: 通过电子邮件重置客户流量。
+
+---
+
+## 其他功能
+
+### 创建备份
+- **方法**: `GET`
+- **端点**: `/inbounds/create-backup`
+- **描述**: 创建数据备份。
+
+### 获取在线客户
+- **方法**: `GET`
+- **端点**: `/inbounds/online`
+- **描述**: 获取在线客户列表。
+
+---
+
+## 服务器
+
+### 获取服务器状态
+- **方法**: `GET`
+- **端点**: `/server/status`
+- **描述**: 获取服务器状态。
\ No newline at end of file
diff --git a/docs/API.en.md b/docs/API.en.md
new file mode 100644
index 0000000000..443d6be6c1
--- /dev/null
+++ b/docs/API.en.md
@@ -0,0 +1,143 @@
+# API Documentation
+
+To make requests to REST API v2, you need to include the Authorization header with the Bearer type and the token in each request.
+
+```
+Authorization: Bearer {token}
+```
+
+## Inbounds
+
+### Get All Inbounds
+- **Method**: `GET`
+- **Endpoint**: `/inbounds/`
+- **Description**: Retrieve a list of all inbounds.
+
+### Reset All Client Traffic
+- **Method**: `DELETE`
+- **Endpoint**: `/inbounds/traffic`
+- **Description**: Reset the traffic of all clients.
+
+---
+
+## Inbound
+
+### Add Inbound
+- **Method**: `POST`
+- **Endpoint**: `/inbounds/`
+- **Description**: Add a new inbound.
+
+### Get Inbound by ID
+- **Method**: `GET`
+- **Endpoint**: `/inbounds/:id`
+- **Description**: Retrieve information about a specific inbound by its ID.
+
+### Delete Inbound by ID
+- **Method**: `DELETE`
+- **Endpoint**: `/inbounds/:id`
+- **Description**: Delete an inbound by its ID.
+
+### Update Inbound by ID
+- **Method**: `PUT`
+- **Endpoint**: `/inbounds/:id`
+- **Description**: Update information about an inbound by its ID.
+
+### Delete Inbound Traffic
+- **Method**: `DELETE`
+- **Endpoint**: `/inbounds/:id/traffic`
+- **Description**: Delete traffic for an inbound by its ID.
+
+### Delete Depleted Clients
+- **Method**: `DELETE`
+- **Endpoint**: `/inbounds/:id/depleted-clients`
+- **Description**: Remove clients with exhausted traffic for a specific inbound.
+
+---
+
+## Inbound Clients
+
+### Get Inbound Clients
+- **Method**: `GET`
+- **Endpoint**: `/inbounds/:id/clients/`
+- **Description**: Retrieve a list of clients for a specific inbound.
+
+---
+
+## Inbound Client
+
+### Add Inbound Client
+- **Method**: `POST`
+- **Endpoint**: `/inbounds/:id/clients`
+- **Description**: Add a new client to an inbound.
+
+### Get Client by ID
+- **Method**: `GET`
+- **Endpoint**: `/inbounds/:id/clients/:clientId`
+- **Description**: Retrieve information about a client by its ID.
+
+### Update Inbound Client
+- **Method**: `PUT`
+- **Endpoint**: `/inbounds/:id/clients/:clientId`
+- **Description**: Update client information by its ID.
+
+### Delete Inbound Client
+- **Method**: `DELETE`
+- **Endpoint**: `/inbounds/:id/clients/:clientId`
+- **Description**: Delete a client by its ID.
+
+### Get Client Traffic by ID
+- **Method**: `GET`
+- **Endpoint**: `/inbounds/:id/clients/:clientId/traffic`
+- **Description**: Retrieve traffic statistics for a client by its ID.
+
+---
+
+## Inbound Client by Email
+
+### Get Client by Email
+- **Method**: `GET`
+- **Endpoint**: `/inbounds/:id/clients/email/:email`
+- **Description**: Retrieve client information by email.
+
+### Get Client IPs
+- **Method**: `GET`
+- **Endpoint**: `/inbounds/:id/clients/email/:email/ips`
+- **Description**: Retrieve a list of client IP addresses by email.
+
+### Clear Client IPs
+- **Method**: `DELETE`
+- **Endpoint**: `/inbounds/:id/clients/email/:email/ips`
+- **Description**: Clear the list of client IP addresses by email.
+
+### Get Client Traffic by Email
+- **Method**: `GET`
+- **Endpoint**: `/inbounds/:id/clients/email/:email/traffic`
+- **Description**: Retrieve traffic statistics for a client by email.
+
+### Reset Client Traffic by Email
+- **Method**: `DELETE`
+- **Endpoint**: `/inbounds/:id/clients/email/:email/traffic`
+- **Description**: Reset a client's traffic by email.
+
+---
+
+## Other
+
+### Create Backup
+- **Method**: `GET`
+- **Endpoint**: `/inbounds/create-backup`
+- **Description**: Create a data backup.
+
+### Get Online Clients
+- **Method**: `GET`
+- **Endpoint**: `/inbounds/online`
+- **Description**: Retrieve a list of online clients.
+
+---
+
+## Server
+
+### Get Server Status
+- **Method**: `GET`
+- **Endpoint**: `/server/status`
+- **Description**: Retrieve the server status.
\ No newline at end of file
diff --git a/docs/API.es.md b/docs/API.es.md
new file mode 100644
index 0000000000..a890d8f3ef
--- /dev/null
+++ b/docs/API.es.md
@@ -0,0 +1,143 @@
+# Documentación de la API
+
+Para realizar solicitudes al REST API v2, es necesario incluir en cada solicitud el encabezado Authorization con el tipo Bearer y el token.
+
+```
+Authorization: Bearer {token}
+```
+
+## Inbounds
+
+### Obtener todos los Inbounds
+- **Método**: `GET`
+- **Endpoint**: `/inbounds/`
+- **Descripción**: Obtener una lista de todos los inbounds.
+
+### Restablecer todo el tráfico de clientes
+- **Método**: `DELETE`
+- **Endpoint**: `/inbounds/traffic`
+- **Descripción**: Restablecer el tráfico de todos los clientes.
+
+---
+
+## Inbound
+
+### Agregar Inbound
+- **Método**: `POST`
+- **Endpoint**: `/inbounds/`
+- **Descripción**: Agregar un nuevo inbound.
+
+### Obtener Inbound por ID
+- **Método**: `GET`
+- **Endpoint**: `/inbounds/:id`
+- **Descripción**: Obtener información sobre un inbound específico por su ID.
+
+### Eliminar Inbound por ID
+- **Método**: `DELETE`
+- **Endpoint**: `/inbounds/:id`
+- **Descripción**: Eliminar un inbound por su ID.
+
+### Actualizar Inbound por ID
+- **Método**: `PUT`
+- **Endpoint**: `/inbounds/:id`
+- **Descripción**: Actualizar la información de un inbound por su ID.
+
+### Eliminar tráfico de Inbound
+- **Método**: `DELETE`
+- **Endpoint**: `/inbounds/:id/traffic`
+- **Descripción**: Eliminar el tráfico de un inbound por su ID.
+
+### Eliminar clientes con tráfico agotado
+- **Método**: `DELETE`
+- **Endpoint**: `/inbounds/:id/depleted-clients`
+- **Descripción**: Eliminar clientes con tráfico agotado para un inbound específico.
+
+---
+
+## Clientes de Inbound
+
+### Obtener clientes de Inbound
+- **Método**: `GET`
+- **Endpoint**: `/inbounds/:id/clients/`
+- **Descripción**: Obtener una lista de clientes para un inbound específico.
+
+---
+
+## Cliente de Inbound
+
+### Agregar cliente de Inbound
+- **Método**: `POST`
+- **Endpoint**: `/inbounds/:id/clients`
+- **Descripción**: Agregar un nuevo cliente a un inbound.
+
+### Obtener cliente por ID
+- **Método**: `GET`
+- **Endpoint**: `/inbounds/:id/clients/:clientId`
+- **Descripción**: Obtener información sobre un cliente por su ID.
+
+### Actualizar cliente de Inbound
+- **Método**: `PUT`
+- **Endpoint**: `/inbounds/:id/clients/:clientId`
+- **Descripción**: Actualizar la información del cliente por su ID.
+
+### Eliminar cliente de Inbound
+- **Método**: `DELETE`
+- **Endpoint**: `/inbounds/:id/clients/:clientId`
+- **Descripción**: Eliminar un cliente por su ID.
+
+### Obtener tráfico del cliente por ID
+- **Método**: `GET`
+- **Endpoint**: `/inbounds/:id/clients/:clientId/traffic`
+- **Descripción**: Obtener estadísticas de tráfico para un cliente por su ID.
+
+---
+
+## Cliente de Inbound por correo electrónico
+
+### Obtener cliente por correo electrónico
+- **Método**: `GET`
+- **Endpoint**: `/inbounds/:id/clients/email/:email`
+- **Descripción**: Obtener información del cliente por correo electrónico.
+
+### Obtener IPs del cliente
+- **Método**: `GET`
+- **Endpoint**: `/inbounds/:id/clients/email/:email/ips`
+- **Descripción**: Obtener una lista de direcciones IP del cliente por correo electrónico.
+
+### Limpiar IPs del cliente
+- **Método**: `DELETE`
+- **Endpoint**: `/inbounds/:id/clients/email/:email/ips`
+- **Descripción**: Limpiar la lista de direcciones IP del cliente por correo electrónico.
+
+### Obtener tráfico del cliente por correo electrónico
+- **Método**: `GET`
+- **Endpoint**: `/inbounds/:id/clients/email/:email/traffic`
+- **Descripción**: Obtener estadísticas de tráfico para un cliente por correo electrónico.
+
+### Restablecer tráfico del cliente por correo electrónico
+- **Método**: `DELETE`
+- **Endpoint**: `/inbounds/:id/clients/email/:email/traffic`
+- **Descripción**: Restablecer el tráfico de un cliente por correo electrónico.
+
+---
+
+## Otros
+
+### Crear copia de seguridad
+- **Método**: `GET`
+- **Endpoint**: `/inbounds/create-backup`
+- **Descripción**: Crear una copia de seguridad de los datos.
+
+### Obtener clientes en línea
+- **Método**: `GET`
+- **Endpoint**: `/inbounds/online`
+- **Descripción**: Obtener una lista de clientes en línea.
+
+---
+
+## Servidor
+
+### Obtener estado del servidor
+- **Método**: `GET`
+- **Endpoint**: `/server/status`
+- **Descripción**: Obtener el estado del servidor.
\ No newline at end of file
diff --git a/docs/API.fa.md b/docs/API.fa.md
new file mode 100644
index 0000000000..755755b6cd
--- /dev/null
+++ b/docs/API.fa.md
@@ -0,0 +1,143 @@
+# مستندات API
+
+برای انجام درخواستها به REST API v2، در هر درخواست باید هدر Authorization با نوع Bearer و توکن ارسال شود.
+
+```
+Authorization: Bearer {token}
+```
+
+## ورودیها (Inbounds)
+
+### دریافت تمام ورودیها
+- **متد**: `GET`
+- **اندپوینت**: `/inbounds/`
+- **توضیحات**: دریافت لیستی از تمام ورودیها.
+
+### بازنشانی ترافیک تمام کاربران
+- **متد**: `DELETE`
+- **اندپوینت**: `/inbounds/traffic`
+- **توضیحات**: بازنشانی ترافیک تمام کاربران.
+
+---
+
+## ورودی (Inbound)
+
+### افزودن ورودی
+- **متد**: `POST`
+- **اندپوینت**: `/inbounds/`
+- **توضیحات**: افزودن یک ورودی جدید.
+
+### دریافت ورودی با شناسه (ID)
+- **متد**: `GET`
+- **اندپوینت**: `/inbounds/:id`
+- **توضیحات**: دریافت اطلاعات یک ورودی خاص بر اساس شناسه آن.
+
+### حذف ورودی با شناسه (ID)
+- **متد**: `DELETE`
+- **اندپوینت**: `/inbounds/:id`
+- **توضیحات**: حذف ورودی بر اساس شناسه آن.
+
+### بهروزرسانی ورودی با شناسه (ID)
+- **متد**: `PUT`
+- **اندپوینت**: `/inbounds/:id`
+- **توضیحات**: بهروزرسانی اطلاعات یک ورودی بر اساس شناسه آن.
+
+### حذف ترافیک ورودی
+- **متد**: `DELETE`
+- **اندپوینت**: `/inbounds/:id/traffic`
+- **توضیحات**: حذف ترافیک ورودی بر اساس شناسه آن.
+
+### حذف کاربران با ترافیک تمامشده
+- **متد**: `DELETE`
+- **اندپوینت**: `/inbounds/:id/depleted-clients`
+- **توضیحات**: حذف کاربران با ترافیک مصرفشده برای یک ورودی خاص.
+
+---
+
+## کاربران ورودی
+
+### دریافت کاربران ورودی
+- **متد**: `GET`
+- **اندپوینت**: `/inbounds/:id/clients/`
+- **توضیحات**: دریافت لیست کاربران برای یک ورودی خاص.
+
+---
+
+## کاربر ورودی
+
+### افزودن کاربر ورودی
+- **متد**: `POST`
+- **اندپوینت**: `/inbounds/:id/clients`
+- **توضیحات**: افزودن یک کاربر جدید به ورودی.
+
+### دریافت کاربر با شناسه (ID)
+- **متد**: `GET`
+- **اندپوینت**: `/inbounds/:id/clients/:clientId`
+- **توضیحات**: دریافت اطلاعات کاربر بر اساس شناسه آن.
+
+### بهروزرسانی کاربر ورودی
+- **متد**: `PUT`
+- **اندپوینت**: `/inbounds/:id/clients/:clientId`
+- **توضیحات**: بهروزرسانی اطلاعات کاربر بر اساس شناسه آن.
+
+### حذف کاربر ورودی
+- **متد**: `DELETE`
+- **اندپوینت**: `/inbounds/:id/clients/:clientId`
+- **توضیحات**: حذف کاربر بر اساس شناسه آن.
+
+### دریافت ترافیک کاربر با شناسه (ID)
+- **متد**: `GET`
+- **اندپوینت**: `/inbounds/:id/clients/:clientId/traffic`
+- **توضیحات**: دریافت آمار ترافیک کاربر بر اساس شناسه آن.
+
+---
+
+## کاربر ورودی با ایمیل
+
+### دریافت کاربر با ایمیل
+- **متد**: `GET`
+- **اندپوینت**: `/inbounds/:id/clients/email/:email`
+- **توضیحات**: دریافت اطلاعات کاربر بر اساس ایمیل.
+
+### دریافت IPهای کاربر
+- **متد**: `GET`
+- **اندپوینت**: `/inbounds/:id/clients/email/:email/ips`
+- **توضیحات**: دریافت لیست آدرسهای IP کاربر بر اساس ایمیل.
+
+### پاک کردن IPهای کاربر
+- **متد**: `DELETE`
+- **اندپوینت**: `/inbounds/:id/clients/email/:email/ips`
+- **توضیحات**: پاک کردن لیست آدرسهای IP کاربر بر اساس ایمیل.
+
+### دریافت ترافیک کاربر با ایمیل
+- **متد**: `GET`
+- **اندپوینت**: `/inbounds/:id/clients/email/:email/traffic`
+- **توضیحات**: دریافت آمار ترافیک کاربر بر اساس ایمیل.
+
+### بازنشانی ترافیک کاربر با ایمیل
+- **متد**: `DELETE`
+- **اندپوینت**: `/inbounds/:id/clients/email/:email/traffic`
+- **توضیحات**: بازنشانی ترافیک کاربر بر اساس ایمیل.
+
+---
+
+## سایر موارد
+
+### ایجاد نسخه پشتیبان
+- **متد**: `GET`
+- **اندپوینت**: `/inbounds/create-backup`
+- **توضیحات**: ایجاد یک نسخه پشتیبان از دادهها.
+
+### دریافت کاربران آنلاین
+- **متد**: `GET`
+- **اندپوینت**: `/inbounds/online`
+- **توضیحات**: دریافت لیست کاربران آنلاین.
+
+---
+
+## سرور
+
+### دریافت وضعیت سرور
+- **متد**: `GET`
+- **اندپوینت**: `/server/status`
+- **توضیحات**: دریافت وضعیت سرور.
\ No newline at end of file
diff --git a/docs/API.ru.md b/docs/API.ru.md
new file mode 100644
index 0000000000..9ba2e3d76b
--- /dev/null
+++ b/docs/API.ru.md
@@ -0,0 +1,143 @@
+# API Documentation
+
+Для выполнения запросов к REST API v2 в каждом запросе необходимо передавать заголовок Authorization с типом Bearer и указанием токена.
+
+```
+Authorization: Bearer {token}
+```
+
+## Inbounds
+
+### Get All Inbounds
+- **Method**: `GET`
+- **Endpoint**: `/inbounds/`
+- **Description**: Получить список всех inbounds.
+
+### Reset All Client Traffics
+- **Method**: `DELETE`
+- **Endpoint**: `/inbounds/traffic`
+- **Description**: Сбросить трафик всех клиентов.
+
+---
+
+## Inbound
+
+### Add Inbound
+- **Method**: `POST`
+- **Endpoint**: `/inbounds/`
+- **Description**: Добавить новый inbound.
+
+### Get Inbound by ID
+- **Method**: `GET`
+- **Endpoint**: `/inbounds/:id`
+- **Description**: Получить информацию о конкретном inbound по его ID.
+
+### Delete Inbound by ID
+- **Method**: `DELETE`
+- **Endpoint**: `/inbounds/:id`
+- **Description**: Удалить inbound по его ID.
+
+### Update Inbound by ID
+- **Method**: `PUT`
+- **Endpoint**: `/inbounds/:id`
+- **Description**: Обновить информацию о inbound по его ID.
+
+### Delete Inbound Traffic
+- **Method**: `DELETE`
+- **Endpoint**: `/inbounds/:id/traffic`
+- **Description**: Удалить трафик inbound по его ID.
+
+### Delete Depleted Clients
+- **Method**: `DELETE`
+- **Endpoint**: `/inbounds/:id/depleted-clients`
+- **Description**: Удалить клиентов с исчерпанным трафиком для конкретного inbound.
+
+---
+
+## Inbound Clients
+
+### Get Inbound Clients
+- **Method**: `GET`
+- **Endpoint**: `/inbounds/:id/clients/`
+- **Description**: Получить список клиентов для конкретного inbound.
+
+---
+
+## Inbound Client
+
+### Add Inbound Client
+- **Method**: `POST`
+- **Endpoint**: `/inbounds/:id/clients`
+- **Description**: Добавить нового клиента к inbound.
+
+### Get Client by ID
+- **Method**: `GET`
+- **Endpoint**: `/inbounds/:id/clients/:clientId`
+- **Description**: Получить информацию о клиенте по его ID.
+
+### Update Inbound Client
+- **Method**: `PUT`
+- **Endpoint**: `/inbounds/:id/clients/:clientId`
+- **Description**: Обновить информацию о клиенте по его ID.
+
+### Delete Inbound Client
+- **Method**: `DELETE`
+- **Endpoint**: `/inbounds/:id/clients/:clientId`
+- **Description**: Удалить клиента по его ID.
+
+### Get Client Traffics by ID
+- **Method**: `GET`
+- **Endpoint**: `/inbounds/:id/clients/:clientId/traffic`
+- **Description**: Получить статистику трафика клиента по его ID.
+
+---
+
+## Inbound Client by Email
+
+### Get Client by Email
+- **Method**: `GET`
+- **Endpoint**: `/inbounds/:id/clients/email/:email`
+- **Description**: Получить информацию о клиенте по его email.
+
+### Get Client IPs
+- **Method**: `GET`
+- **Endpoint**: `/inbounds/:id/clients/email/:email/ips`
+- **Description**: Получить список IP-адресов клиента по его email.
+
+### Clear Client IPs
+- **Method**: `DELETE`
+- **Endpoint**: `/inbounds/:id/clients/email/:email/ips`
+- **Description**: Очистить список IP-адресов клиента по его email.
+
+### Get Client Traffics by Email
+- **Method**: `GET`
+- **Endpoint**: `/inbounds/:id/clients/email/:email/traffic`
+- **Description**: Получить статистику трафика клиента по его email.
+
+### Reset Client Traffic by Email
+- **Method**: `DELETE`
+- **Endpoint**: `/inbounds/:id/clients/email/:email/traffic`
+- **Description**: Сбросить трафик клиента по его email.
+
+---
+
+## Other
+
+### Create Backup
+- **Method**: `GET`
+- **Endpoint**: `/inbounds/create-backup`
+- **Description**: Создать резервную копию данных.
+
+### Get Online Clients
+- **Method**: `GET`
+- **Endpoint**: `/inbounds/online`
+- **Description**: Получить список онлайн-клиентов.
+
+---
+
+## Server
+
+### Get Server Status
+- **Method**: `GET`
+- **Endpoint**: `/server/status`
+- **Description**: Получить статус сервера.
\ No newline at end of file
diff --git a/web/assets/js/util/utils.js b/web/assets/js/util/utils.js
index 30f1f6a2eb..941a676e56 100644
--- a/web/assets/js/util/utils.js
+++ b/web/assets/js/util/utils.js
@@ -57,6 +57,20 @@ class HttpUtil {
}
}
+ static async delete(url, params, options = {}) {
+ try {
+ const resp = await axios.delete(url, { params, ...options });
+ const msg = this._respToMsg(resp);
+ this._handleMsg(msg);
+ return msg;
+ } catch (error) {
+ console.error('DELETE request failed:', error);
+ const errorMsg = new Msg(false, error.response?.data?.message || error.message || 'Request failed');
+ this._handleMsg(errorMsg);
+ return errorMsg;
+ }
+ }
+
static async postWithModal(url, data, modal) {
if (modal) {
modal.loading(true);
diff --git a/web/controller/api.go b/web/controller/api.go
index 9944e2a3d3..0a078a84d5 100644
--- a/web/controller/api.go
+++ b/web/controller/api.go
@@ -8,52 +8,126 @@ import (
type APIController struct {
BaseController
- inboundController *InboundController
- Tgbot service.Tgbot
+ inbounds *InboundController
+ Tgbot service.Tgbot
+ server *ServerController
}
func NewAPIController(g *gin.RouterGroup) *APIController {
a := &APIController{}
a.initRouter(g)
+ a.initApiV2Router(g)
return a
}
-func (a *APIController) initRouter(g *gin.RouterGroup) {
- g = g.Group("/panel/api/inbounds")
- g.Use(a.checkLogin)
+func (controller *APIController) initRouter(router *gin.RouterGroup) {
+ apiV1 := router.Group("/panel/api")
+ apiV1.Use(controller.checkLogin)
- a.inboundController = NewInboundController(g)
+ inboundsApiGroup := apiV1.Group("/inbounds")
+ controller.inbounds = NewInboundController(inboundsApiGroup)
inboundRoutes := []struct {
Method string
Path string
Handler gin.HandlerFunc
}{
- {"GET", "/createbackup", a.createBackup},
- {"GET", "/list", a.inboundController.getInbounds},
- {"GET", "/get/:id", a.inboundController.getInbound},
- {"GET", "/getClientTraffics/:email", a.inboundController.getClientTraffics},
- {"GET", "/getClientTrafficsById/:id", a.inboundController.getClientTrafficsById},
- {"POST", "/add", a.inboundController.addInbound},
- {"POST", "/del/:id", a.inboundController.delInbound},
- {"POST", "/update/:id", a.inboundController.updateInbound},
- {"POST", "/clientIps/:email", a.inboundController.getClientIps},
- {"POST", "/clearClientIps/:email", a.inboundController.clearClientIps},
- {"POST", "/addClient", a.inboundController.addInboundClient},
- {"POST", "/:id/delClient/:clientId", a.inboundController.delInboundClient},
- {"POST", "/updateClient/:clientId", a.inboundController.updateInboundClient},
- {"POST", "/:id/resetClientTraffic/:email", a.inboundController.resetClientTraffic},
- {"POST", "/resetAllTraffics", a.inboundController.resetAllTraffics},
- {"POST", "/resetAllClientTraffics/:id", a.inboundController.resetAllClientTraffics},
- {"POST", "/delDepletedClients/:id", a.inboundController.delDepletedClients},
- {"POST", "/onlines", a.inboundController.onlines},
+ {"GET", "/createbackup", controller.createBackup},
+ {"GET", "/list", controller.inbounds.getInbounds},
+ {"GET", "/get/:id", controller.inbounds.getInbound},
+ {"GET", "/getClientTraffics/:email", controller.inbounds.getClientTraffics},
+ {"GET", "/getClientTrafficsById/:id", controller.inbounds.getClientTrafficsById},
+ {"POST", "/add", controller.inbounds.addInbound},
+ {"POST", "/del/:id", controller.inbounds.delInbound},
+ {"POST", "/update/:id", controller.inbounds.updateInbound},
+ {"POST", "/clientIps/:email", controller.inbounds.getClientIps},
+ {"POST", "/clearClientIps/:email", controller.inbounds.clearClientIps},
+ {"POST", "/addClient", controller.inbounds.addInboundClient},
+ {"POST", "/:id/delClient/:clientId", controller.inbounds.delInboundClient},
+ {"POST", "/updateClient/:clientId", controller.inbounds.updateInboundClient},
+ {"POST", "/:id/resetClientTraffic/:email", controller.inbounds.resetClientTraffic},
+ {"POST", "/resetAllTraffics", controller.inbounds.resetAllTraffics},
+ {"POST", "/resetAllClientTraffics/:id", controller.inbounds.resetAllClientTraffics},
+ {"POST", "/delDepletedClients/:id", controller.inbounds.delDepletedClients},
+ {"POST", "/onlines", controller.inbounds.onlines},
}
for _, route := range inboundRoutes {
- g.Handle(route.Method, route.Path, route.Handler)
+ inboundsApiGroup.Handle(route.Method, route.Path, route.Handler)
}
}
func (a *APIController) createBackup(c *gin.Context) {
a.Tgbot.SendBackupToAdmins()
}
+
+
+func (controller *APIController) initApiV2Router(router *gin.RouterGroup) {
+ apiV2 := router.Group("/api/v2")
+ apiV2.Use(controller.apiTokenGuard)
+
+ serverApiGroup := apiV2.Group("/server")
+ inboundsApiGroup := apiV2.Group("/inbounds")
+
+ controller.inbounds = NewInboundController(inboundsApiGroup)
+ controller.server = NewServerController(serverApiGroup)
+
+ /**
+ * Inbounds
+ */
+ inboundsApiGroup.GET("/", controller.inbounds.getInbounds)
+ inboundsApiGroup.DELETE("/traffic", controller.inbounds.resetAllClientTraffics)
+
+ /**
+ * Inbound
+ */
+ inboundsApiGroup.POST("/", controller.inbounds.addInbound)
+ inboundsApiGroup.GET("/:id", controller.inbounds.getInbound)
+ inboundsApiGroup.DELETE("/:id", controller.inbounds.delInbound)
+ inboundsApiGroup.PUT("/:id", controller.inbounds.updateInbound)
+
+ inboundsApiGroup.DELETE("/:id/traffic", controller.inbounds.delInbound)
+ inboundsApiGroup.DELETE("/:id/depleted-clients", controller.inbounds.delDepletedClients)
+
+ /**
+ * Inbound clients
+ */
+ inboundsApiGroup.GET("/:id/clients/", controller.inbounds.getInboundClients)
+
+ /**
+ * Inbound client
+ */
+ inboundsApiGroup.POST("/:id/clients", controller.inbounds.addInboundClient)
+ inboundsApiGroup.GET("/:id/clients/:clientId", controller.inbounds.getClientById)
+ inboundsApiGroup.PUT("/:id/clients/:clientId", controller.inbounds.updateInboundClient)
+ inboundsApiGroup.DELETE("/:id/clients/:clientId", controller.inbounds.delInboundClient)
+
+ inboundsApiGroup.GET("/:id/clients/:clientId/traffic", controller.inbounds.getClientTrafficsById)
+ // TODO: get client ips by ID
+ // TODO: clear client ips by ID
+ // TODO: reset client traffic by ID
+
+ /**
+ * Inbound client by email
+ */
+ inboundsApiGroup.GET("/:id/clients/email/:email", controller.inbounds.getClientByEmail)
+ // TODO: update client by Email
+ // TODO: delete client by Email
+
+ inboundsApiGroup.GET("/:id/clients/email/:email/ips", controller.inbounds.getClientIps)
+ inboundsApiGroup.DELETE("/:id/clients/email/:email/ips", controller.inbounds.clearClientIps)
+
+ inboundsApiGroup.GET("/:id/clients/email/:email/traffic", controller.inbounds.getClientTraffics)
+ inboundsApiGroup.DELETE("/:id/clients/email/:email/traffic", controller.inbounds.resetClientTraffic)
+
+ /**
+ * Other
+ */
+ inboundsApiGroup.GET("/create-backup", controller.createBackup)
+ inboundsApiGroup.GET("/online", controller.inbounds.onlines)
+
+ /**
+ * Server
+ */
+ serverApiGroup.GET("/status", controller.server.status)
+}
\ No newline at end of file
diff --git a/web/controller/base.go b/web/controller/base.go
index 492fc2dc86..c2975c7cfc 100644
--- a/web/controller/base.go
+++ b/web/controller/base.go
@@ -1,16 +1,21 @@
package controller
import (
+ "fmt"
"net/http"
+ "strings"
"x-ui/logger"
"x-ui/web/locale"
"x-ui/web/session"
"github.com/gin-gonic/gin"
+ "x-ui/web/service"
)
-type BaseController struct{}
+type BaseController struct{
+ settingService service.SettingService
+}
func (a *BaseController) checkLogin(c *gin.Context) {
if !session.IsLogin(c) {
@@ -35,3 +40,39 @@ func I18nWeb(c *gin.Context, name string, params ...string) string {
msg := i18nFunc(locale.Web, name, params...)
return msg
}
+
+func (a *BaseController) apiTokenGuard(c *gin.Context) {
+ bearerToken := c.Request.Header.Get("Authorization")
+ tokenParts := strings.Split(bearerToken, " ")
+ if len(tokenParts) != 2 {
+ pureJsonMsg(c, http.StatusUnauthorized, false, "Invalid token format")
+ c.Abort()
+ return
+ }
+ reqToken := tokenParts[1]
+ token, err := a.settingService.GetApiToken()
+
+ if err != nil {
+ pureJsonMsg(c, http.StatusUnauthorized, false, err.Error())
+ c.Abort()
+ return
+ }
+
+ if reqToken != token {
+ pureJsonMsg(c, http.StatusUnauthorized, false, "Auth failed")
+ c.Abort()
+ return
+ }
+
+ userService := service.UserService{}
+ user, err := userService.GetFirstUser()
+ if err != nil {
+ fmt.Println("get current user info failed, error info:", err)
+ }
+
+ session.SetSessionUser(c, user)
+
+ c.Next()
+
+ session.ClearSession(c)
+}
\ No newline at end of file
diff --git a/web/controller/inbound.go b/web/controller/inbound.go
index c22ce1924e..a923f782d5 100644
--- a/web/controller/inbound.go
+++ b/web/controller/inbound.go
@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"strconv"
+ "errors"
"x-ui/database/model"
"x-ui/web/service"
@@ -44,7 +45,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
}
func (a *InboundController) getInbounds(c *gin.Context) {
- user := session.GetLoginUser(c)
+ user := session.GetSessionUser(c)
inbounds, err := a.inboundService.GetInbounds(user.Id)
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
@@ -53,10 +54,19 @@ func (a *InboundController) getInbounds(c *gin.Context) {
jsonObj(c, inbounds, nil)
}
+func (a *InboundController) getAllInbounds(c *gin.Context) {
+ inbounds, err := a.inboundService.GetAllInbounds()
+ if err != nil {
+ jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
+ return
+ }
+ jsonObj(c, inbounds, nil)
+}
+
func (a *InboundController) getInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
- jsonMsg(c, I18nWeb(c, "get"), err)
+ jsonMsg(c, I18nWeb(c, "get"), errors.New("Invalid inbound id"))
return
}
inbound, err := a.inboundService.GetInbound(id)
@@ -94,7 +104,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.inbounds.create"), err)
return
}
- user := session.GetLoginUser(c)
+ user := session.GetSessionUser(c)
inbound.UserId = user.Id
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
@@ -146,6 +156,60 @@ func (a *InboundController) updateInbound(c *gin.Context) {
}
}
+func (a *InboundController) getInboundClients(c *gin.Context) {
+ id, err := strconv.Atoi(c.Param("id"))
+ if err != nil {
+ jsonMsg(c, "GetInboundClients", errors.New("Incorrect inbound id"))
+ return
+ }
+
+ client, err := a.inboundService.GetInboundClients(id)
+ if err != nil {
+ jsonMsg(c, "GetInboundClientById", err)
+ return
+ }
+
+ jsonObj(c, client, nil)
+}
+
+func (a *InboundController) getClientById(c *gin.Context) {
+ id, err := strconv.Atoi(c.Param("id"))
+ if err != nil {
+ jsonMsg(c, "GetInboundClientById", errors.New("Incorrect inbound id"))
+ return
+ }
+
+ client, err := a.inboundService.GetInboundClientById(id, c.Param("clientId"))
+ if err != nil {
+ jsonMsg(c, "GetInboundClientById", err)
+ return
+ }
+ if client == nil {
+ jsonMsg(c, "GetInboundClientById", errors.New("Client not found"))
+ return
+ }
+ jsonObj(c, client, nil)
+}
+
+func (a *InboundController) getClientByEmail(c *gin.Context) {
+ id, err := strconv.Atoi(c.Param("id"))
+ if err != nil {
+ jsonMsg(c, "GetInboundClientByEmail", errors.New("Incorrect inbound id"))
+ return
+ }
+
+ client, err := a.inboundService.GetInboundClientByEmail(id, c.Param("email"))
+ if err != nil {
+ jsonMsg(c, "GetInboundClientByEmail", err)
+ return
+ }
+ if client == nil {
+ jsonMsg(c, "GetInboundClientByEmail", errors.New("Client not found"))
+ return
+ }
+ jsonObj(c, client, nil)
+}
+
func (a *InboundController) getClientIps(c *gin.Context) {
email := c.Param("email")
@@ -288,7 +352,7 @@ func (a *InboundController) importInbound(c *gin.Context) {
jsonMsg(c, "Something went wrong!", err)
return
}
- user := session.GetLoginUser(c)
+ user := session.GetSessionUser(c)
inbound.Id = 0
inbound.UserId = user.Id
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
diff --git a/web/controller/index.go b/web/controller/index.go
index 9af4ed7fa2..e30175059c 100644
--- a/web/controller/index.go
+++ b/web/controller/index.go
@@ -86,7 +86,7 @@ func (a *IndexController) login(c *gin.Context) {
}
session.SetMaxAge(c, sessionMaxAge*60)
- session.SetLoginUser(c, user)
+ session.SetSessionUser(c, user)
if err := sessions.Default(c).Save(); err != nil {
logger.Warning("Unable to save session: ", err)
return
@@ -97,7 +97,7 @@ func (a *IndexController) login(c *gin.Context) {
}
func (a *IndexController) logout(c *gin.Context) {
- user := session.GetLoginUser(c)
+ user := session.GetSessionUser(c)
if user != nil {
logger.Infof("%s logged out successfully", user.Username)
}
diff --git a/web/controller/setting.go b/web/controller/setting.go
index d04969dcae..f488a16c06 100644
--- a/web/controller/setting.go
+++ b/web/controller/setting.go
@@ -3,6 +3,9 @@ package controller
import (
"errors"
"time"
+ "crypto/rand"
+ "crypto/sha512"
+ "encoding/hex"
"x-ui/web/entity"
"x-ui/web/service"
@@ -28,6 +31,10 @@ type SettingController struct {
panelService service.PanelService
}
+type ApiTokenResponse struct {
+ Token string `json:"token"`
+}
+
func NewSettingController(g *gin.RouterGroup) *SettingController {
a := &SettingController{}
a.initRouter(g)
@@ -45,6 +52,10 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
g.POST("/updateUserSecret", a.updateSecret)
g.POST("/getUserSecret", a.getUserSecret)
+
+ g.GET("/apiToken", a.getApiToken)
+ g.POST("/apiToken", a.generateApiToken)
+ g.DELETE("/apiToken", a.removeApiToken)
}
func (a *SettingController) getAllSetting(c *gin.Context) {
@@ -83,7 +94,7 @@ func (a *SettingController) updateUser(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
return
}
- user := session.GetLoginUser(c)
+ user := session.GetSessionUser(c)
if user.Username != form.OldUsername || user.Password != form.OldPassword {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), errors.New(I18nWeb(c, "pages.settings.toasts.originalUserPassIncorrect")))
return
@@ -96,7 +107,7 @@ func (a *SettingController) updateUser(c *gin.Context) {
if err == nil {
user.Username = form.NewUsername
user.Password = form.NewPassword
- session.SetLoginUser(c, user)
+ session.SetSessionUser(c, user)
}
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
}
@@ -112,17 +123,17 @@ func (a *SettingController) updateSecret(c *gin.Context) {
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
}
- user := session.GetLoginUser(c)
+ user := session.GetSessionUser(c)
err = a.userService.UpdateUserSecret(user.Id, form.LoginSecret)
if err == nil {
user.LoginSecret = form.LoginSecret
- session.SetLoginUser(c, user)
+ session.SetSessionUser(c, user)
}
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
}
func (a *SettingController) getUserSecret(c *gin.Context) {
- loginUser := session.GetLoginUser(c)
+ loginUser := session.GetSessionUser(c)
user := a.userService.GetUserSecret(loginUser.Id)
if user != nil {
jsonObj(c, user, nil)
@@ -137,3 +148,50 @@ func (a *SettingController) getDefaultXrayConfig(c *gin.Context) {
}
jsonObj(c, defaultJsonConfig, nil)
}
+
+func (a *SettingController) getApiToken(c *gin.Context) {
+ response := &ApiTokenResponse{}
+ token, err := a.settingService.GetApiToken()
+ if err != nil {
+ jsonObj(c, response , err)
+ return
+ }
+
+ response.Token = token
+
+ jsonObj(c, response , nil)
+}
+
+func (a *SettingController) generateApiToken(c *gin.Context) {
+ response := &ApiTokenResponse{}
+ randomBytes := make([]byte, 32)
+
+ _, err := rand.Read(randomBytes)
+ if err != nil {
+ jsonObj(c, nil, err)
+ return
+ }
+
+ hash := sha512.Sum512(randomBytes)
+ response.Token = hex.EncodeToString(hash[:])
+
+ saveErr := a.settingService.SaveApiToken(response.Token)
+
+ if saveErr != nil {
+ jsonObj(c, nil, saveErr)
+ return
+ }
+
+ jsonMsgObj(c, I18nWeb(c, "pages.settings.security.apiTokenGeneratedSuccess"), response, nil)
+}
+
+func (a *SettingController) removeApiToken(c *gin.Context) {
+ err := a.settingService.RemoveApiToken()
+
+ if err != nil {
+ jsonObj(c, nil, err)
+ return
+ }
+
+ jsonMsg(c, "Removed", nil)
+}
diff --git a/web/entity/entity.go b/web/entity/entity.go
index 1220634087..b875bdc304 100644
--- a/web/entity/entity.go
+++ b/web/entity/entity.go
@@ -57,6 +57,7 @@ type AllSetting struct {
SubJsonMux string `json:"subJsonMux" form:"subJsonMux"`
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"`
Datepicker string `json:"datepicker" form:"datepicker"`
+ ApiToken string `json:"apiToken" form:"apiToken"`
}
func (s *AllSetting) CheckValid() error {
diff --git a/web/html/xui/settings.html b/web/html/xui/settings.html
index 0c70ca1c39..4c6c46cfbe 100644
--- a/web/html/xui/settings.html
+++ b/web/html/xui/settings.html
@@ -235,6 +235,28 @@
{{ i18n "confirm" }}
+
+ {{ i18n "pages.settings.security.apiTitle"}}
+
+
+
+
+
+
+
+ [[ apiToken ]]
+
+
+
+
+
{{ i18n "pages.settings.security.apiGenerateToken" }}
+
+
+
+
+
+
+
@@ -401,6 +423,7 @@
+
{{template "js" .}}
{{template "component/themeSwitcher" .}}
@@ -522,132 +545,166 @@
sample = []
this.remarkModel.forEach(r => sample.push(this.remarkModels[r]));
this.remarkSample = sample.length == 0 ? '' : sample.join(this.remarkSeparator);
- }
- },
- methods: {
- loading(spinning = true) {
- this.spinning = spinning;
- },
- async getAllSetting() {
- this.loading(true);
- const msg = await HttpUtil.post("/panel/setting/all");
- this.loading(false);
- if (msg.success) {
- this.oldAllSetting = new AllSetting(msg.obj);
- this.allSetting = new AllSetting(msg.obj);
- app.changeRemarkSample();
- this.saveBtnDisable = true;
- }
- await this.fetchUserSecret();
- },
- async updateAllSetting() {
- this.loading(true);
- const msg = await HttpUtil.post("/panel/setting/update", this.allSetting);
- this.loading(false);
- if (msg.success) {
- await this.getAllSetting();
- }
- },
- async updateUser() {
- this.loading(true);
- const msg = await HttpUtil.post("/panel/setting/updateUser", this.user);
- this.loading(false);
- if (msg.success) {
- this.user = {};
- window.location.replace(basePath + "logout");
- }
- },
- async restartPanel() {
- await new Promise(resolve => {
- this.$confirm({
- title: '{{ i18n "pages.settings.restartPanel" }}',
- content: '{{ i18n "pages.settings.restartPanelDesc" }}',
- class: themeSwitcher.currentTheme,
- okText: '{{ i18n "sure" }}',
- cancelText: '{{ i18n "cancel" }}',
- onOk: () => resolve(),
- });
- });
- this.loading(true);
- const msg = await HttpUtil.post("/panel/setting/restartPanel");
- this.loading(false);
- if (msg.success) {
- this.loading(true);
- await PromiseUtil.sleep(5000);
- var { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting;
- if (host == this.oldAllSetting.webDomain) host = null;
- if (port == this.oldAllSetting.webPort) port = null;
- const isTLS = webCertFile !== "" || webKeyFile !== "";
- const url = buildURL({ host, port, isTLS, base, path: "panel/settings" });
- window.location.replace(url);
- }
- },
- async fetchUserSecret() {
- this.loading(true);
- const userMessage = await HttpUtil.post("/panel/setting/getUserSecret", this.user);
- if (userMessage.success) {
- this.user = userMessage.obj;
- }
- this.loading(false);
- },
- async updateSecret() {
- this.loading(true);
- const msg = await HttpUtil.post("/panel/setting/updateUserSecret", this.user);
- if (msg && msg.obj) {
- this.user = msg.obj;
- }
- this.loading(false);
- await this.updateAllSetting();
- },
- generateRandomString(length) {
- var chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
- let randomString = "";
- for (let i = 0; i < length; i++) {
- randomString += chars[Math.floor(Math.random() * chars.length)];
- }
- return randomString;
- },
- async getNewSecret() {
- if (!this.changeSecret) {
- this.changeSecret = true;
- this.user.loginSecret = '';
- const newSecret = this.generateRandomString(64);
- await PromiseUtil.sleep(1000);
- this.user.loginSecret = newSecret;
- this.changeSecret = false;
- }
- },
- async toggleToken(value) {
- if (value) {
- await this.getNewSecret();
- } else {
- this.user.loginSecret = "";
- }
- },
- addNoise() {
- const newNoise = { type: "rand", packet: "10-20", delay: "10-16" };
- this.noisesArray = [...this.noisesArray, newNoise];
- },
- removeNoise(index) {
- const newNoises = [...this.noisesArray];
- newNoises.splice(index, 1);
- this.noisesArray = newNoises;
- },
- updateNoiseType(index, value) {
- const updatedNoises = [...this.noisesArray];
- updatedNoises[index] = { ...updatedNoises[index], type: value };
- this.noisesArray = updatedNoises;
- },
- updateNoisePacket(index, value) {
- const updatedNoises = [...this.noisesArray];
- updatedNoises[index] = { ...updatedNoises[index], packet: value };
- this.noisesArray = updatedNoises;
- },
- updateNoiseDelay(index, value) {
- const updatedNoises = [...this.noisesArray];
- updatedNoises[index] = { ...updatedNoises[index], delay: value };
- this.noisesArray = updatedNoises;
},
+ apiToken: null,
+ },
+ methods: {
+ loading(spinning = true) {
+ this.spinning = spinning;
+ },
+ async getAllSetting() {
+ this.loading(true);
+ const msg = await HttpUtil.post("/panel/setting/all");
+ this.loading(false);
+ if (msg.success) {
+ this.oldAllSetting = new AllSetting(msg.obj);
+ this.allSetting = new AllSetting(msg.obj);
+ this.apiToken = msg.obj.apiToken;
+ app.changeRemarkSample();
+ this.saveBtnDisable = true;
+ }
+ await this.fetchUserSecret();
+ },
+ async updateAllSetting() {
+ this.loading(true);
+ const msg = await HttpUtil.post("/panel/setting/update", this.allSetting);
+ this.loading(false);
+ if (msg.success) {
+ await this.getAllSetting();
+ }
+ },
+ async updateUser() {
+ this.loading(true);
+ const msg = await HttpUtil.post("/panel/setting/updateUser", this.user);
+ this.loading(false);
+ if (msg.success) {
+ this.user = {};
+ window.location.replace(basePath + "logout");
+ }
+ },
+ async restartPanel() {
+ await new Promise(resolve => {
+ this.$confirm({
+ title: '{{ i18n "pages.settings.restartPanel" }}',
+ content: '{{ i18n "pages.settings.restartPanelDesc" }}',
+ class: themeSwitcher.currentTheme,
+ okText: '{{ i18n "sure" }}',
+ cancelText: '{{ i18n "cancel" }}',
+ onOk: () => resolve(),
+ });
+ });
+ this.loading(true);
+ const msg = await HttpUtil.post("/panel/setting/restartPanel");
+ this.loading(false);
+ if (msg.success) {
+ this.loading(true);
+ await PromiseUtil.sleep(5000);
+ var { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting;
+ if (host == this.oldAllSetting.webDomain) host = null;
+ if (port == this.oldAllSetting.webPort) port = null;
+ const isTLS = webCertFile !== "" || webKeyFile !== "";
+ const url = buildURL({ host, port, isTLS, base, path: "panel/settings" });
+ window.location.replace(url);
+ }
+ },
+ async fetchUserSecret() {
+ this.loading(true);
+ const userMessage = await HttpUtil.post("/panel/setting/getUserSecret", this.user);
+ if (userMessage.success) {
+ this.user = userMessage.obj;
+ }
+ this.loading(false);
+ },
+ async updateSecret() {
+ this.loading(true);
+ const msg = await HttpUtil.post("/panel/setting/updateUserSecret", this.user);
+ if (msg && msg.obj) {
+ this.user = msg.obj;
+ }
+ this.loading(false);
+ await this.updateAllSetting();
+ },
+ generateRandomString(length) {
+ var chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
+ let randomString = "";
+ for (let i = 0; i < length; i++) {
+ randomString += chars[Math.floor(Math.random() * chars.length)];
+ }
+ return randomString;
+ },
+ async getNewSecret() {
+ if (!this.changeSecret) {
+ this.changeSecret = true;
+ this.user.loginSecret = '';
+ const newSecret = this.generateRandomString(64);
+ await PromiseUtil.sleep(1000);
+ this.user.loginSecret = newSecret;
+ this.changeSecret = false;
+ }
+ },
+ async toggleToken(value) {
+ if (value) {
+ await this.getNewSecret();
+ } else {
+ this.user.loginSecret = "";
+ }
+ },
+ addNoise() {
+ const newNoise = { type: "rand", packet: "10-20", delay: "10-16" };
+ this.noisesArray = [...this.noisesArray, newNoise];
+ },
+ removeNoise(index) {
+ const newNoises = [...this.noisesArray];
+ newNoises.splice(index, 1);
+ this.noisesArray = newNoises;
+ },
+ updateNoiseType(index, value) {
+ const updatedNoises = [...this.noisesArray];
+ updatedNoises[index] = { ...updatedNoises[index], type: value };
+ this.noisesArray = updatedNoises;
+ },
+ updateNoisePacket(index, value) {
+ const updatedNoises = [...this.noisesArray];
+ updatedNoises[index] = { ...updatedNoises[index], packet: value };
+ this.noisesArray = updatedNoises;
+ },
+ updateNoiseDelay(index, value) {
+ const updatedNoises = [...this.noisesArray];
+ updatedNoises[index] = { ...updatedNoises[index], delay: value };
+ this.noisesArray = updatedNoises;
+ },
+ async generateApiToken() {
+ this.loading(true);
+ const msg = await HttpUtil.post("/panel/setting/apiToken");
+ if (msg && msg.obj) {
+ this.apiToken = msg.obj.token;
+ }
+ this.loading(false);
+ },
+ copyApiToken() {
+ ClipboardJS.copy(this.apiToken);
+ app.$message.success('{{ i18n "copied" }}')
+ },
+ async removeApiToken() {
+ await new Promise(() => {
+ this.$confirm({
+ title: '{{ i18n "pages.settings.security.apiConfirmRemoveTokenTitle" }}',
+ content: '{{ i18n "pages.settings.security.apiConfirmRemoveTokenText" }}',
+ class: themeSwitcher.currentTheme,
+ okText: '{{ i18n "delete"}}',
+ cancelText: '{{ i18n "cancel" }}',
+ onOk: async () => {
+ this.loading(true);
+ const msg = await HttpUtil.delete("/panel/setting/apiToken");
+ if (msg && msg.success) {
+ app.$message.success('{{ i18n "deleted" }}')
+ this.apiToken = null;
+ }
+ this.loading(false);
+ },
+ });
+ });
+ },
},
computed: {
fragment: {
diff --git a/web/service/inbound.go b/web/service/inbound.go
index 4f28af21a8..15e98054a3 100644
--- a/web/service/inbound.go
+++ b/web/service/inbound.go
@@ -1729,6 +1729,56 @@ func (s *InboundService) DelDepletedClients(id int) (err error) {
return nil
}
+func (s *InboundService) GetInboundClients(inboundId int) (client []model.Client, err error) {
+ inbound, err := s.GetInbound(inboundId)
+ if err != nil {
+ return nil, err
+ }
+ clients, err := s.GetClients(inbound)
+ if err != nil {
+ return nil, err
+ }
+ if (clients == nil) {
+ return make([]model.Client, 0), nil
+ }
+ return clients, nil
+}
+
+func (s *InboundService) GetInboundClientById(inboundId int, clientId string) (client *model.Client, err error) {
+ inbound, err := s.GetInbound(inboundId)
+ if err != nil {
+ return nil, err
+ }
+
+ clients, err := s.GetClients(inbound)
+ if err != nil {
+ return nil, err
+ }
+ for _, client := range clients {
+ if client.ID == clientId {
+ return &client, nil
+ }
+ }
+ return nil, nil
+}
+
+func (s *InboundService) GetInboundClientByEmail(inboundId int, email string) (client *model.Client, err error) {
+ inbound, err := s.GetInbound(inboundId)
+ if err != nil {
+ return nil, err
+ }
+ clients, err := s.GetClients(inbound)
+ if err != nil {
+ return nil, err
+ }
+ for _, client := range clients {
+ if client.Email == email {
+ return &client, nil
+ }
+ }
+ return nil, nil
+}
+
func (s *InboundService) GetClientTrafficTgBot(tgId int64) ([]*xray.ClientTraffic, error) {
db := database.GetDB()
var inbounds []*model.Inbound
diff --git a/web/service/setting.go b/web/service/setting.go
index e3ea3eced8..7531d38963 100644
--- a/web/service/setting.go
+++ b/web/service/setting.go
@@ -596,3 +596,46 @@ func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
return result, nil
}
+
+
+func (s *SettingService) GetApiToken() (token string, err error) {
+ db := database.GetDB()
+ setting := &model.Setting{}
+ err = db.Model(model.Setting{}).Where("key = 'apiToken'").Find(setting).Error
+ if err != nil {
+ return "", err
+ }
+ return setting.Value, nil
+}
+
+func (s *SettingService) SaveApiToken(token string) error {
+ db := database.GetDB()
+ setting := &model.Setting{}
+ err := db.Model(model.Setting{}).Where("key = 'apiToken'").Find(setting).Error
+
+ if err != nil {
+ return err
+ }
+
+ if setting.Value == "" {
+ newSetting := model.Setting{
+ Key: "apiToken",
+ Value: token,
+ }
+ fmt.Println("New setting created")
+ return db.Model(model.Setting{}).Create(&newSetting).Error
+ }
+ return db.Model(model.Setting{}).
+ Where("key = 'apiToken'").
+ Update("value", token).Error
+}
+
+func (s *SettingService) RemoveApiToken() error {
+ db := database.GetDB()
+ setting := &model.Setting{}
+ err := db.Model(model.Setting{}).Where("key = 'apiToken'").Find(setting).Error
+ if err != nil {
+ return err
+ }
+ return db.Model(model.Setting{}).Delete(setting, setting.Id).Error
+}
\ No newline at end of file
diff --git a/web/session/session.go b/web/session/session.go
index 13aedad811..f52382bf57 100644
--- a/web/session/session.go
+++ b/web/session/session.go
@@ -18,7 +18,7 @@ func init() {
gob.Register(model.User{})
}
-func SetLoginUser(c *gin.Context, user *model.User) {
+func SetSessionUser(c *gin.Context, user *model.User) {
if user == nil {
return
}
@@ -35,7 +35,7 @@ func SetMaxAge(c *gin.Context, maxAge int) {
})
}
-func GetLoginUser(c *gin.Context) *model.User {
+func GetSessionUser(c *gin.Context) *model.User {
s := sessions.Default(c)
obj := s.Get(loginUserKey)
if obj == nil {
@@ -51,7 +51,7 @@ func GetLoginUser(c *gin.Context) *model.User {
}
func IsLogin(c *gin.Context) bool {
- return GetLoginUser(c) != nil
+ return GetSessionUser(c) != nil
}
func ClearSession(c *gin.Context) {
diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml
index ceceabd54f..c22c182167 100644
--- a/web/translation/translate.en_US.toml
+++ b/web/translation/translate.en_US.toml
@@ -446,6 +446,11 @@
"loginSecurityDesc" = "Adds an additional layer of authentication to provide more security."
"secretToken" = "Secret Token"
"secretTokenDesc" = "Please securely store this token in a safe place. This token is required for login and cannot be recovered."
+"apiDescription" = "To make requests to REST API v2, you need to include the Authorization header with the Bearer type and the token in each request.\nExample: Authorization: Bearer {token}"
+"apiGenerateToken" = "Generate token"
+"apiTokenGeneratedSuccess" = "Token generated"
+"apiConfirmRemoveTokenTitle" = "Confirm token deletion"
+"apiConfirmRemoveTokenText" = "After deleting the token, access to the API will be unavailable, and making requests will no longer be possible."
[pages.settings.toasts]
"modifySettings" = "Modify Settings"
diff --git a/web/translation/translate.es_ES.toml b/web/translation/translate.es_ES.toml
index b9af32725e..3d236e6a45 100644
--- a/web/translation/translate.es_ES.toml
+++ b/web/translation/translate.es_ES.toml
@@ -446,6 +446,11 @@
"loginSecurityDesc" = "Habilitar un paso adicional de seguridad para el inicio de sesión de usuarios."
"secretToken" = "Token Secreto"
"secretTokenDesc" = "Por favor, copia y guarda este token de forma segura en un lugar seguro. Este token es necesario para iniciar sesión y no se puede recuperar con la herramienta de comando x-ui."
+"apiDescription" = "Para realizar solicitudes al REST API v2, es necesario incluir en cada solicitud el encabezado Authorization con el tipo Bearer y el token.\nEjemplo: Authorization: Bearer {token}"
+"apiGenerateToken" = "Generar token"
+"apiTokenGeneratedSuccess" = "Token generado con éxito"
+"apiConfirmRemoveTokenTitle" = "Confirmar eliminación del token"
+"apiConfirmRemoveTokenText" = "Después de eliminar el token, el acceso al API no estará disponible y no se podrán realizar solicitudes."
[pages.settings.toasts]
"modifySettings" = "Modificar Configuraciones "
diff --git a/web/translation/translate.fa_IR.toml b/web/translation/translate.fa_IR.toml
index c7fad84e34..eab28a6402 100644
--- a/web/translation/translate.fa_IR.toml
+++ b/web/translation/translate.fa_IR.toml
@@ -446,6 +446,11 @@
"loginSecurityDesc" = "یک لایه اضافی از احراز هویت برای ایجاد امنیت بیشتر اضافه می کند"
"secretToken" = "توکن مخفی"
"secretTokenDesc" = "لطفاً این توکن را در مکانی امن ذخیره کنید. این توکن برای ورود به سیستم مورد نیاز است و قابل بازیابی نیست"
+"apiDescription" = "برای انجام درخواستها به REST API v2، در هر درخواست باید هدر Authorization با نوع Bearer و توکن ارسال شود.\nمثال: Authorization: Bearer {token}"
+"apiGenerateToken" = "توکن ایجاد کنید"
+"apiTokenGeneratedSuccessful" = "توکن با موفقیت ایجاد شد"
+"apiConfirmRemoveTokenTitle" = "تایید حذف توکن"
+"apiConfirmRemoveTokenText" = "پس از حذف توکن، دسترسی به API غیرفعال میشود و انجام درخواستها امکانپذیر نخواهد بود."
[pages.settings.toasts]
"modifySettings" = "ویرایش تنظیمات"
diff --git a/web/translation/translate.id_ID.toml b/web/translation/translate.id_ID.toml
index 85f8f967ca..524d12b1cd 100644
--- a/web/translation/translate.id_ID.toml
+++ b/web/translation/translate.id_ID.toml
@@ -446,6 +446,11 @@
"loginSecurityDesc" = "Menambahkan lapisan otentikasi tambahan untuk memberikan keamanan lebih."
"secretToken" = "Token Rahasia"
"secretTokenDesc" = "Simpan token ini dengan aman di tempat yang aman. Token ini diperlukan untuk login dan tidak dapat dipulihkan."
+"apiDescription" = "Untuk melakukan permintaan ke REST API v2, Anda perlu menyertakan header Authorization dengan tipe Bearer dan token di setiap permintaan.\nContoh: Authorization: Bearer {token}"
+"apiGenerateToken" = "Buat token"
+"apiTokenGeneratedSuccessful" = "Token berhasil dibuat"
+"apiConfirmRemoveTokenTitle" = "Konfirmasi penghapusan token"
+"apiConfirmRemoveTokenText" = "Setelah menghapus token, akses ke API akan tidak tersedia, dan permintaan tidak dapat dilakukan."
[pages.settings.toasts]
"modifySettings" = "Ubah Pengaturan"
diff --git a/web/translation/translate.ja_JP.toml b/web/translation/translate.ja_JP.toml
index 0141317162..37551413be 100644
--- a/web/translation/translate.ja_JP.toml
+++ b/web/translation/translate.ja_JP.toml
@@ -446,6 +446,11 @@
"loginSecurityDesc" = "追加の認証を追加してセキュリティを向上させる"
"secretToken" = "セキュリティトークン"
"secretTokenDesc" = "このトークンを安全な場所に保管してください。このトークンはログインに使用され、紛失すると回復できません。"
+"apiDescription" = "REST API v2 にリクエストを送信するには、Authorization ヘッダーに Bearer タイプのトークンを含める必要があります。\n例: Authorization: Bearer {token}"
+"apiGenerateToken" = "トークンを生成"
+"apiTokenGeneratedSuccessful" = "トークンが正常に生成されました"
+"apiConfirmRemoveTokenTitle" = "トークン削除の確認"
+"apiConfirmRemoveTokenText" = "トークンを削除すると、API へのアクセスができなくなり、リクエストを送信できなくなります。"
[pages.settings.toasts]
"modifySettings" = "設定を変更"
diff --git a/web/translation/translate.pt_BR.toml b/web/translation/translate.pt_BR.toml
index 5effdecbd7..1a08a7a2ee 100644
--- a/web/translation/translate.pt_BR.toml
+++ b/web/translation/translate.pt_BR.toml
@@ -446,6 +446,11 @@
"loginSecurityDesc" = "Adiciona uma camada extra de autenticação para fornecer mais segurança."
"secretToken" = "Token Secreto"
"secretTokenDesc" = "Por favor, armazene este token em um local seguro. Este token é necessário para o login e não pode ser recuperado."
+"apiDescription" = "Para fazer solicitações à REST API v2, você precisa incluir o cabeçalho Authorization com o tipo Bearer e o token em cada solicitação.\nExemplo: Authorization: Bearer {token}"
+"apiGenerateToken" = "Gerar token"
+"apiTokenGeneratedSuccessful" = "Token gerado com sucesso"
+"apiConfirmRemoveTokenTitle" = "Confirmar exclusão do token"
+"apiConfirmRemoveTokenText" = "Após excluir o token, o acesso à API não estará mais disponível e não será possível fazer solicitações."
[pages.settings.toasts]
"modifySettings" = "Modificar Configurações"
diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml
index a995f29cf2..cb75bc401b 100644
--- a/web/translation/translate.ru_RU.toml
+++ b/web/translation/translate.ru_RU.toml
@@ -446,6 +446,12 @@
"loginSecurityDesc" = "Включить дополнительные меры безопасности входа пользователя"
"secretToken" = "Секретный токен"
"secretTokenDesc" = "Пожалуйста, скопируйте и сохраните этот токен в безопасном месте. Этот токен необходим для входа в систему и не может быть восстановлен с помощью инструмента x-ui"
+"apiTitle" = "REST API"
+"apiDescription" = "Для выполнения запросов к REST API v2 в каждом запросе необходимо передавать заголовок Authorization с типом Bearer и указанием токена.\nПример: Authorization: Bearer {token}"
+"apiGenerateToken" = "Сгенерировать токен"
+"apiTokenGeneratedSuccess" = "Токен сгенерирован"
+"apiConfirmRemoveTokenTitle" = "Подтвердите удаление токена"
+"apiConfirmRemoveTokenText" = "После удаления токена доступ к API станет недоступным, и выполнение запросов будет невозможно."
[pages.settings.toasts]
"modifySettings" = "Изменение настроек"
@@ -454,6 +460,8 @@
"originalUserPassIncorrect" = "Неверное имя пользователя или пароль"
"userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены"
+
+
[tgbot]
"keyboardClosed" = "❌ Закрыта настраиваемая клавиатура!"
"noResult" = "❗ Нет результатов!"
@@ -592,4 +600,4 @@
"disableSuccess" = "✅ {{ .Email }}: Отключено успешно."
"askToAddUserId" = "Ваша конфигурация не найдена!\r\nПожалуйста, попросите администратора использовать ваш идентификатор пользователя Telegram в ваших конфигурациях.\r\n\r\nВаш идентификатор пользователя: {{ .TgUserID }}
"
"chooseClient" = "Выберите пользователя для подключения {{ .Inbound }}"
-"chooseInbound" = "Выберите подключение"
+"chooseInbound" = "Выберите подключение"
\ No newline at end of file
diff --git a/web/translation/translate.tr_TR.toml b/web/translation/translate.tr_TR.toml
index bee64b0f13..1c264e99ae 100644
--- a/web/translation/translate.tr_TR.toml
+++ b/web/translation/translate.tr_TR.toml
@@ -446,6 +446,11 @@
"loginSecurityDesc" = "Daha fazla güvenlik sağlamak için ek bir kimlik doğrulama katmanı ekler."
"secretToken" = "Gizli Anahtar"
"secretTokenDesc" = "Bu anahtarı güvenli bir yerde saklayın. Bu anahtar giriş için gereklidir ve geri alınamaz."
+"apiDescription" = "REST API v2'ye istek gönderebilmek için, her isteğe Bearer türünde bir yetkilendirme (Authorization) başlığı ve token eklemeniz gerekir.\nÖrnek: Authorization: Bearer {token}"
+"apiGenerateToken" = "Token oluştur"
+"apiTokenGeneratedSuccessful" = "Token başarıyla oluşturuldu"
+"apiConfirmRemoveTokenTitle" = "Token silme onayı"
+"apiConfirmRemoveTokenText" = "Token silindikten sonra API erişimi mümkün olmayacak ve istek gönderilemeyecektir."
[pages.settings.toasts]
"modifySettings" = "Ayarları Değiştir"
diff --git a/web/translation/translate.uk_UA.toml b/web/translation/translate.uk_UA.toml
index 791d2154e7..96a81507ef 100644
--- a/web/translation/translate.uk_UA.toml
+++ b/web/translation/translate.uk_UA.toml
@@ -446,6 +446,11 @@
"loginSecurityDesc" = "Додає додатковий рівень автентифікації для забезпечення більшої безпеки."
"secretToken" = "Секретний маркер"
"secretTokenDesc" = "Будь ласка, надійно зберігайте цей маркер у безпечному місці. Цей маркер потрібен для входу, і його неможливо відновити."
+"apiDescription" = "Щоб надсилати запити до REST API v2, вам потрібно додати заголовок Authorization із типом Bearer і токен у кожен запит.\nПриклад: Authorization: Bearer {token}"
+"apiGenerateToken" = "Створити токен"
+"apiTokenGeneratedSuccessful" = "Токен успішно створено"
+"apiConfirmRemoveTokenTitle" = "Підтвердження видалення токена"
+"apiConfirmRemoveTokenText" = "Після видалення токена доступ до API буде неможливим, і надсилати запити більше не вдасться."
[pages.settings.toasts]
"modifySettings" = "Змінити налаштування"
diff --git a/web/translation/translate.vi_VN.toml b/web/translation/translate.vi_VN.toml
index cd750891ab..c26e1d0676 100644
--- a/web/translation/translate.vi_VN.toml
+++ b/web/translation/translate.vi_VN.toml
@@ -446,6 +446,11 @@
"loginSecurityDesc" = "Bật bước bảo mật đăng nhập bổ sung cho người dùng"
"secretToken" = "Mã bí mật"
"secretTokenDesc" = "Vui lòng sao chép và lưu trữ mã này một cách an toàn ở nơi an toàn. Mã này cần thiết để đăng nhập và không thể phục hồi từ công cụ lệnh x-ui."
+"apiDescription" = "Để gửi yêu cầu đến REST API v2, bạn cần bao gồm tiêu đề Authorization với loại Bearer và token trong mỗi yêu cầu.\nVí dụ: Authorization: Bearer {token}"
+"apiGenerateToken" = "Tạo token"
+"apiTokenGeneratedSuccessful" = "Token đã được tạo thành công"
+"apiConfirmRemoveTokenTitle" = "Xác nhận xóa token"
+"apiConfirmRemoveTokenText" = "Sau khi xóa token, quyền truy cập API sẽ không còn khả dụng và bạn sẽ không thể gửi yêu cầu nữa."
[pages.settings.toasts]
"modifySettings" = "Chỉnh sửa cài đặt "
diff --git a/web/translation/translate.zh_CN.toml b/web/translation/translate.zh_CN.toml
index b5f5cce1ad..b9a267dc76 100644
--- a/web/translation/translate.zh_CN.toml
+++ b/web/translation/translate.zh_CN.toml
@@ -446,6 +446,11 @@
"loginSecurityDesc" = "添加额外的身份验证以提高安全性"
"secretToken" = "安全令牌"
"secretTokenDesc" = "请将此令牌存储在安全的地方。此令牌用于登录,丢失无法恢复。"
+"apiDescription" = "要向 REST API v2 发送请求,您需要在每个请求中包含带有 Bearer 类型的 Authorization 头和令牌。\n示例: Authorization: Bearer {token}"
+"apiGenerateToken" = "生成令牌"
+"apiTokenGeneratedSuccessful" = "令牌生成成功"
+"apiConfirmRemoveTokenTitle" = "确认删除令牌"
+"apiConfirmRemoveTokenText" = "删除令牌后,将无法访问 API,并且无法再发送请求。"
[pages.settings.toasts]
"modifySettings" = "修改设置"
diff --git a/web/translation/translate.zh_TW.toml b/web/translation/translate.zh_TW.toml
index 466b2b82b0..9d08d8f0df 100644
--- a/web/translation/translate.zh_TW.toml
+++ b/web/translation/translate.zh_TW.toml
@@ -446,6 +446,11 @@
"loginSecurityDesc" = "新增額外的身份驗證以提高安全性"
"secretToken" = "安全令牌"
"secretTokenDesc" = "請將此令牌儲存在安全的地方。此令牌用於登入,丟失無法恢復。"
+"apiDescription" = "要向 REST API v2 發送請求,您需要在每個請求中包含帶有 Bearer 類型的 Authorization 標頭和權杖。\n示例: Authorization: Bearer {token}"
+"apiGenerateToken" = "生成權杖"
+"apiTokenGeneratedSuccessful" = "權杖生成成功"
+"apiConfirmRemoveTokenTitle" = "確認刪除權杖"
+"apiConfirmRemoveTokenText" = "刪除權杖後,將無法存取 API,並且無法再發送請求。"
[pages.settings.toasts]
"modifySettings" = "修改設定"