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" = "修改設定"