diff --git a/Wavelet/docs/docs.go b/Wavelet/docs/docs.go index 535f4401..475418be 100644 --- a/Wavelet/docs/docs.go +++ b/Wavelet/docs/docs.go @@ -10,8 +10,8 @@ const docTemplate = `{ "description": "{{escape .Description}}", "title": "{{.Title}}", "contact": { - "name": "Wavelet", - "url": "https://github.com/Rain-kl/Wavelet" + "name": "OpenFlare", + "url": "https://github.com/Rain-kl/OpenFlare" }, "license": { "name": "Apache 2.0", @@ -285,6 +285,7 @@ const docTemplate = `{ "parameters": [ { "type": "integer", + "format": "int64", "description": "认证源 ID 或名称", "name": "id", "in": "path", @@ -362,6 +363,7 @@ const docTemplate = `{ "parameters": [ { "type": "integer", + "format": "int64", "description": "认证源 ID 或名称", "name": "id", "in": "path", @@ -429,6 +431,7 @@ const docTemplate = `{ "parameters": [ { "type": "integer", + "format": "int64", "description": "认证源 ID 或名称", "name": "id", "in": "path", @@ -1339,6 +1342,7 @@ const docTemplate = `{ "parameters": [ { "type": "integer", + "format": "int64", "description": "通道ID", "name": "id", "in": "path", @@ -1392,6 +1396,7 @@ const docTemplate = `{ "parameters": [ { "type": "integer", + "format": "int64", "description": "通道ID", "name": "id", "in": "path", @@ -3344,6 +3349,7 @@ const docTemplate = `{ }, { "type": "integer", + "format": "int64", "description": "上传用户 ID", "name": "user_id", "in": "query" @@ -4103,6 +4109,7028 @@ const docTemplate = `{ } } }, + "/api/v1/custom/openflare/about": { + "get": { + "description": "返回 OpenFlare 关于页面文本,无需登录", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-option" + ], + "summary": "获取关于信息", + "responses": { + "200": { + "description": "关于信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/access-logs": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "分页返回 OpenFlare 访问日志,支持按节点、IP、主机与路径筛选,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-observability" + ], + "summary": "列出访问日志", + "parameters": [ + { + "type": "string", + "description": "节点 ID", + "name": "node_id", + "in": "query" + }, + { + "type": "string", + "description": "客户端 IP", + "name": "remote_addr", + "in": "query" + }, + { + "type": "string", + "description": "请求 Host", + "name": "host", + "in": "query" + }, + { + "type": "string", + "description": "请求路径", + "name": "path", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "p", + "in": "query" + }, + { + "type": "integer", + "description": "每页条数", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "排序字段", + "name": "sort_by", + "in": "query" + }, + { + "type": "string", + "description": "排序方向", + "name": "sort_order", + "in": "query" + } + ], + "responses": { + "200": { + "description": "访问日志列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/observability.AccessLogList" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/access-logs/cleanup": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按保留天数清理过期访问日志记录,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-observability" + ], + "summary": "清理访问日志", + "parameters": [ + { + "description": "清理参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/observability.AccessLogCleanupInput" + } + } + ], + "responses": { + "200": { + "description": "清理结果", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/observability.AccessLogCleanupResult" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/access-logs/folds": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按时间桶聚合访问日志并分页返回,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-observability" + ], + "summary": "列出折叠访问日志", + "parameters": [ + { + "type": "string", + "description": "节点 ID", + "name": "node_id", + "in": "query" + }, + { + "type": "string", + "description": "客户端 IP", + "name": "remote_addr", + "in": "query" + }, + { + "type": "string", + "description": "请求 Host", + "name": "host", + "in": "query" + }, + { + "type": "string", + "description": "请求路径", + "name": "path", + "in": "query" + }, + { + "type": "integer", + "description": "折叠时间窗口(分钟)", + "name": "fold_minutes", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "p", + "in": "query" + }, + { + "type": "integer", + "description": "每页条数", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "排序字段", + "name": "sort_by", + "in": "query" + }, + { + "type": "string", + "description": "排序方向", + "name": "sort_order", + "in": "query" + } + ], + "responses": { + "200": { + "description": "折叠访问日志列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/observability.FoldedAccessLogList" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/access-logs/folds/ip-summary": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "在指定时间桶内按 IP 聚合访问统计,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-observability" + ], + "summary": "列出折叠访问日志 IP 汇总", + "parameters": [ + { + "type": "string", + "description": "节点 ID", + "name": "node_id", + "in": "query" + }, + { + "type": "string", + "description": "客户端 IP", + "name": "remote_addr", + "in": "query" + }, + { + "type": "string", + "description": "请求 Host", + "name": "host", + "in": "query" + }, + { + "type": "string", + "description": "请求路径", + "name": "path", + "in": "query" + }, + { + "type": "string", + "description": "时间桶起始时间", + "name": "bucket_started_at", + "in": "query" + }, + { + "type": "integer", + "description": "折叠时间窗口(分钟)", + "name": "fold_minutes", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "p", + "in": "query" + }, + { + "type": "integer", + "description": "每页条数", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "排序字段", + "name": "sort_by", + "in": "query" + }, + { + "type": "string", + "description": "排序方向", + "name": "sort_order", + "in": "query" + } + ], + "responses": { + "200": { + "description": "折叠 IP 汇总列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/observability.FoldedAccessLogIPList" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/access-logs/ip-summary": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 IP 聚合访问日志统计并分页返回,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-observability" + ], + "summary": "列出访问日志 IP 汇总", + "parameters": [ + { + "type": "string", + "description": "节点 ID", + "name": "node_id", + "in": "query" + }, + { + "type": "string", + "description": "客户端 IP", + "name": "remote_addr", + "in": "query" + }, + { + "type": "string", + "description": "请求 Host", + "name": "host", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "p", + "in": "query" + }, + { + "type": "integer", + "description": "每页条数", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "排序字段", + "name": "sort_by", + "in": "query" + }, + { + "type": "string", + "description": "排序方向", + "name": "sort_order", + "in": "query" + } + ], + "responses": { + "200": { + "description": "IP 汇总列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/observability.AccessLogIPSummaryList" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/access-logs/ip-summary/trend": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回指定 IP 在时间范围内的访问趋势数据,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-observability" + ], + "summary": "获取访问日志 IP 趋势", + "parameters": [ + { + "type": "string", + "description": "节点 ID", + "name": "node_id", + "in": "query" + }, + { + "type": "string", + "description": "客户端 IP", + "name": "remote_addr", + "in": "query" + }, + { + "type": "string", + "description": "请求 Host", + "name": "host", + "in": "query" + }, + { + "type": "integer", + "description": "统计时间范围(小时)", + "name": "hours", + "in": "query" + }, + { + "type": "integer", + "description": "时间桶粒度(分钟)", + "name": "bucket_minutes", + "in": "query" + } + ], + "responses": { + "200": { + "description": "IP 访问趋势", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/observability.AccessLogIPTrendView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/acme-accounts/default": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回系统默认 ACME 账号配置,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "获取默认 ACME 账号", + "responses": { + "200": { + "description": "默认 ACME 账号", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.AcmeAccount" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/apply-logs": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "分页返回节点配置下发记录,支持按节点 ID 筛选,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-apply-log" + ], + "summary": "获取配置下发日志", + "parameters": [ + { + "type": "string", + "description": "节点 ID 筛选", + "name": "node_id", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "pageNo", + "in": "query" + }, + { + "type": "integer", + "description": "页码(别名)", + "name": "page_no", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "pageSize", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量(别名)", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "下发日志列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/apply_log.ListResult" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/apply-logs/cleanup": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按保留天数清理历史下发记录,或删除全部记录,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-apply-log" + ], + "summary": "清理配置下发日志", + "parameters": [ + { + "description": "清理参数", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/apply_log.CleanupInput" + } + } + ], + "responses": { + "200": { + "description": "清理结果", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/apply_log.CleanupResult" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/config-versions": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回所有已发布的 OpenResty 配置版本摘要,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-config-version" + ], + "summary": "获取配置版本列表", + "responses": { + "200": { + "description": "配置版本列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/model.ConfigVersionSummary" + } + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/config-versions/active": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回当前正在使用的配置版本,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-config-version" + ], + "summary": "获取当前活跃配置版本", + "responses": { + "200": { + "description": "活跃配置版本", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.ConfigVersion" + } + } + } + ] + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限、不存在或无活跃版本", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/config-versions/cleanup": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "删除超出保留数量的非活跃配置版本,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-config-version" + ], + "summary": "清理历史配置版本", + "parameters": [ + { + "description": "清理参数", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/config_version.CleanupInput" + } + } + ], + "responses": { + "200": { + "description": "清理结果", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/config_version.CleanupResult" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/config-versions/diff": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "对比当前草稿配置与活跃版本之间的差异,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-config-version" + ], + "summary": "对比草稿与活跃配置", + "responses": { + "200": { + "description": "配置差异", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/config_version.ConfigDiffResult" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/config-versions/preview": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "渲染并返回当前草稿配置的预览结果,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-config-version" + ], + "summary": "预览当前草稿配置", + "responses": { + "200": { + "description": "配置预览", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/config_version.ConfigPreviewResult" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/config-versions/publish": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "将当前草稿配置发布为新版本,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-config-version" + ], + "summary": "发布配置版本", + "parameters": [ + { + "type": "boolean", + "description": "是否强制发布", + "name": "force", + "in": "query" + } + ], + "responses": { + "200": { + "description": "发布成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.ConfigVersion" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/config-versions/{id}": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回指定配置版本的完整快照与渲染内容,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-config-version" + ], + "summary": "获取配置版本详情", + "parameters": [ + { + "type": "integer", + "description": "配置版本 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "配置版本详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.ConfigVersion" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或版本不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/config-versions/{id}/activate": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "将指定历史版本设为当前活跃配置,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-config-version" + ], + "summary": "激活配置版本", + "parameters": [ + { + "type": "integer", + "description": "配置版本 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "激活成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.ConfigVersion" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或版本不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/dashboard/overview": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "聚合节点与可观测性数据,返回 OpenFlare 控制台仪表盘概览,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-dashboard" + ], + "summary": "获取仪表盘概览", + "responses": { + "200": { + "description": "仪表盘概览", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dashboard.OverviewPayload" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/dns-accounts": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回全部 DNS 提供商账号,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "列出 DNS 账号", + "responses": { + "200": { + "description": "DNS 账号列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/model.DNSAccount" + } + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + }, + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "创建新的 DNS 提供商账号,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "创建 DNS 账号", + "parameters": [ + { + "description": "DNS 账号参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tls.DNSAccountInput" + } + } + ], + "responses": { + "200": { + "description": "创建成功的 DNS 账号", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.DNSAccount" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/dns-accounts/{id}/delete": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 删除 DNS 提供商账号,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "删除 DNS 账号", + "parameters": [ + { + "type": "integer", + "description": "DNS 账号 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/dns-accounts/{id}/update": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 更新 DNS 提供商账号,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "更新 DNS 账号", + "parameters": [ + { + "type": "integer", + "description": "DNS 账号 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "DNS 账号参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tls.DNSAccountInput" + } + } + ], + "responses": { + "200": { + "description": "更新后的 DNS 账号", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.DNSAccount" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/managed-domains": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回全部托管域名及关联证书,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "列出托管域名", + "responses": { + "200": { + "description": "托管域名列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/model.ManagedDomain" + } + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + }, + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "创建新的托管域名记录,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "创建托管域名", + "parameters": [ + { + "description": "托管域名参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tls.ManagedDomainInput" + } + } + ], + "responses": { + "200": { + "description": "创建成功的托管域名", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.ManagedDomain" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/managed-domains/match": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按域名查询可用的证书匹配候选,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "匹配托管域名证书", + "parameters": [ + { + "type": "string", + "description": "域名", + "name": "domain", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "证书匹配结果", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/tls.ManagedDomainMatchResult" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/managed-domains/{id}/delete": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 删除托管域名,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "删除托管域名", + "parameters": [ + { + "type": "integer", + "description": "托管域名 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/managed-domains/{id}/update": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 更新托管域名,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "更新托管域名", + "parameters": [ + { + "type": "integer", + "description": "托管域名 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "托管域名参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tls.ManagedDomainInput" + } + } + ], + "responses": { + "200": { + "description": "更新后的托管域名", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.ManagedDomain" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/nodes": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回所有节点及最新配置下发记录,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-node" + ], + "summary": "获取节点列表", + "responses": { + "200": { + "description": "节点列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/node.View" + } + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + }, + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "创建新的边缘节点记录,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-node" + ], + "summary": "创建节点", + "parameters": [ + { + "description": "节点参数", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/node.Input" + } + } + ], + "responses": { + "200": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/node.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/nodes/bootstrap-token": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回全局节点发现引导令牌,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-node" + ], + "summary": "获取引导令牌", + "responses": { + "200": { + "description": "引导令牌", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/node.BootstrapView" + } + } + } + ] + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/nodes/bootstrap-token/rotate": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "重新生成全局节点发现引导令牌,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-node" + ], + "summary": "轮换引导令牌", + "responses": { + "200": { + "description": "新引导令牌", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/node.BootstrapView" + } + } + } + ] + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/nodes/{id}/agent-release": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回指定节点可用的最新 Agent 版本信息,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-node" + ], + "summary": "获取 Agent 发布信息", + "parameters": [ + { + "type": "integer", + "description": "节点 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "发布渠道", + "name": "channel", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Agent 发布信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/node.AgentReleaseInfo" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或节点不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/nodes/{id}/agent-update": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "向指定节点下发 Agent 自更新指令,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-node" + ], + "summary": "请求 Agent 更新", + "parameters": [ + { + "type": "integer", + "description": "节点 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新参数(可选)", + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/node.AgentUpdateInput" + } + } + ], + "responses": { + "200": { + "description": "更新请求已下发", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/node.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或节点不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/nodes/{id}/delete": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "删除指定节点记录,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-node" + ], + "summary": "删除节点", + "parameters": [ + { + "type": "integer", + "description": "节点 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或节点不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/nodes/{id}/force-sync": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "向指定节点下发强制同步当前活跃配置的指令,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-node" + ], + "summary": "请求强制同步配置", + "parameters": [ + { + "type": "integer", + "description": "节点 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "同步请求已下发", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/node.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或节点不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/nodes/{id}/observability": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回指定节点的指标、健康事件与流量分析数据,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-node" + ], + "summary": "获取节点可观测性数据", + "parameters": [ + { + "type": "integer", + "description": "节点 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "统计时间范围(小时)", + "name": "hours", + "in": "query" + }, + { + "type": "integer", + "description": "返回记录数量上限", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "可观测性数据", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/node.ObservabilityView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或节点不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/nodes/{id}/observability/cleanup": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "清理指定节点的历史健康事件记录,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-node" + ], + "summary": "清理节点健康事件", + "parameters": [ + { + "type": "integer", + "description": "节点 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "清理结果", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/node.HealthEventCleanupResult" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或节点不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/nodes/{id}/openresty-restart": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "向指定节点下发 OpenResty 重启指令,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-node" + ], + "summary": "请求重启 OpenResty", + "parameters": [ + { + "type": "integer", + "description": "节点 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "重启请求已下发", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/node.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或节点不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/nodes/{id}/update": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "更新指定节点的配置信息,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-node" + ], + "summary": "更新节点", + "parameters": [ + { + "type": "integer", + "description": "节点 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "节点参数", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/node.Input" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/node.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或节点不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/notice": { + "get": { + "description": "返回 OpenFlare 控制台公告文本,无需登录", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-option" + ], + "summary": "获取系统公告", + "responses": { + "200": { + "description": "系统公告", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/option": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回全部非敏感 OpenFlare 配置项,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-option" + ], + "summary": "列出 OpenFlare 配置项", + "responses": { + "200": { + "description": "配置项列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/model.OpenFlareOption" + } + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/option/database/cleanup": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按目标与保留天数清理可观测性相关数据表,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-option" + ], + "summary": "清理可观测性数据库", + "parameters": [ + { + "description": "清理参数", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/option.databaseCleanupInput" + } + } + ], + "responses": { + "200": { + "description": "清理结果", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/option.databaseCleanupResult" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/option/geoip/lookup": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按提供商与 IP 查询地理位置信息,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-option" + ], + "summary": "GeoIP 地址查询", + "parameters": [ + { + "description": "查询参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/option.geoIPLookupRequest" + } + } + ], + "responses": { + "200": { + "description": "GeoIP 查询结果", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/option.geoIPLookupView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/option/update": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "更新单个 OpenFlare 配置项,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-option" + ], + "summary": "更新 OpenFlare 配置项", + "parameters": [ + { + "description": "配置项", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/model.OpenFlareOption" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/option/update-batch": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "批量更新多个 OpenFlare 配置项,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-option" + ], + "summary": "批量更新 OpenFlare 配置项", + "parameters": [ + { + "description": "批量配置项", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/option.optionBatchPayload" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/origins": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回所有源站及关联代理规则数量,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-origin" + ], + "summary": "获取源站列表", + "responses": { + "200": { + "description": "源站列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/origin.View" + } + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + }, + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "创建新的上游源站记录,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-origin" + ], + "summary": "创建源站", + "parameters": [ + { + "description": "源站参数", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/origin.Input" + } + } + ], + "responses": { + "200": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/origin.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/origins/{id}": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回指定源站信息及关联代理规则摘要,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-origin" + ], + "summary": "获取源站详情", + "parameters": [ + { + "type": "integer", + "description": "源站 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "源站详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/origin.DetailView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或源站不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/origins/{id}/delete": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "删除指定源站记录,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-origin" + ], + "summary": "删除源站", + "parameters": [ + { + "type": "integer", + "description": "源站 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或源站不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/origins/{id}/update": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "更新指定源站的配置信息,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-origin" + ], + "summary": "更新源站", + "parameters": [ + { + "type": "integer", + "description": "源站 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "源站参数", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/origin.Input" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/origin.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或源站不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/pages": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回全部 OpenFlare Pages 项目,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-pages" + ], + "summary": "列出 Pages 项目", + "responses": { + "200": { + "description": "Pages 项目列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/pages.View" + } + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + }, + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "创建新的 OpenFlare Pages 项目,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-pages" + ], + "summary": "创建 Pages 项目", + "parameters": [ + { + "description": "项目参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/pages.Input" + } + } + ], + "responses": { + "200": { + "description": "创建成功的项目", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/pages.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/pages/deployments/{deployment_id}/files": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回指定部署包含的文件清单,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-pages" + ], + "summary": "列出 Pages 部署文件", + "parameters": [ + { + "type": "integer", + "description": "部署 ID", + "name": "deployment_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "部署文件列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/pages.DeploymentFileView" + } + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "部署不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/pages/{id}": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 返回 Pages 项目详情,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-pages" + ], + "summary": "获取 Pages 项目详情", + "parameters": [ + { + "type": "integer", + "description": "项目 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Pages 项目详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/pages.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "项目不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/pages/{id}/delete": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 删除 OpenFlare Pages 项目,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-pages" + ], + "summary": "删除 Pages 项目", + "parameters": [ + { + "type": "integer", + "description": "项目 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "项目不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/pages/{id}/deployments": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回指定项目的全部部署记录,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-pages" + ], + "summary": "列出 Pages 部署", + "parameters": [ + { + "type": "integer", + "description": "项目 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "部署列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/pages.DeploymentView" + } + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "项目不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/pages/{id}/deployments/upload": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "为指定项目上传 ZIP 部署包,需要管理员权限", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-pages" + ], + "summary": "上传 Pages 部署包", + "parameters": [ + { + "type": "integer", + "description": "项目 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "file", + "description": "部署包 ZIP 文件", + "name": "package", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "部署记录", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/pages.DeploymentView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "项目不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/pages/{id}/deployments/{deployment_id}/activate": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "将指定部署设为项目当前生效版本,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-pages" + ], + "summary": "激活 Pages 部署", + "parameters": [ + { + "type": "integer", + "description": "项目 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "部署 ID", + "name": "deployment_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "激活后的项目", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/pages.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "项目或部署不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/pages/{id}/deployments/{deployment_id}/delete": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "删除指定项目的部署记录,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-pages" + ], + "summary": "删除 Pages 部署", + "parameters": [ + { + "type": "integer", + "description": "项目 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "部署 ID", + "name": "deployment_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "项目或部署不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/pages/{id}/update": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 更新 OpenFlare Pages 项目,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-pages" + ], + "summary": "更新 Pages 项目", + "parameters": [ + { + "type": "integer", + "description": "项目 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "项目参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/pages.Input" + } + } + ], + "responses": { + "200": { + "description": "更新后的项目", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/pages.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "项目不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/proxy-routes": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回所有代理规则配置,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-proxy-route" + ], + "summary": "获取代理规则列表", + "responses": { + "200": { + "description": "代理规则列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/proxy_route.View" + } + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + }, + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "创建新的反向代理规则,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-proxy-route" + ], + "summary": "创建代理规则", + "parameters": [ + { + "description": "代理规则参数", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/proxy_route.Input" + } + } + ], + "responses": { + "200": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/proxy_route.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/proxy-routes/{id}": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回指定代理规则的完整配置,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-proxy-route" + ], + "summary": "获取代理规则详情", + "parameters": [ + { + "type": "integer", + "description": "代理规则 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "代理规则详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/proxy_route.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或规则不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/proxy-routes/{id}/delete": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "删除指定代理规则,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-proxy-route" + ], + "summary": "删除代理规则", + "parameters": [ + { + "type": "integer", + "description": "代理规则 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或规则不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/proxy-routes/{id}/update": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "更新指定代理规则的配置,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-proxy-route" + ], + "summary": "更新代理规则", + "parameters": [ + { + "type": "integer", + "description": "代理规则 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "代理规则参数", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/proxy_route.Input" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/proxy_route.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或规则不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/status": { + "get": { + "description": "返回版本、认证源与系统公开配置,无需登录", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-option" + ], + "summary": "获取 OpenFlare 公开状态", + "responses": { + "200": { + "description": "公开状态", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/option.statusView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/tls-certificates": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回全部 TLS 证书(不含 PEM),需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "列出 TLS 证书", + "responses": { + "200": { + "description": "证书列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/model.TLSCertificate" + } + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + }, + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "从 PEM 文本创建 TLS 证书,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "创建 TLS 证书", + "parameters": [ + { + "description": "证书参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tls.CertificateInput" + } + } + ], + "responses": { + "200": { + "description": "创建成功的证书", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.TLSCertificate" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/tls-certificates/apply": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "通过 ACME 申请新的 TLS 证书,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "申请 ACME 证书", + "parameters": [ + { + "description": "ACME 申请参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tls.ApplyInput" + } + } + ], + "responses": { + "200": { + "description": "申请中的证书", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.TLSCertificate" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/tls-certificates/import-file": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "上传证书与私钥文件创建 TLS 证书,需要管理员权限", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "从文件导入 TLS 证书", + "parameters": [ + { + "type": "string", + "description": "证书名称", + "name": "name", + "in": "formData" + }, + { + "type": "string", + "description": "备注", + "name": "remark", + "in": "formData" + }, + { + "type": "file", + "description": "证书文件", + "name": "cert_file", + "in": "formData", + "required": true + }, + { + "type": "file", + "description": "私钥文件", + "name": "key_file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "导入成功的证书", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.TLSCertificate" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/tls-certificates/{id}": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 返回 TLS 证书详情(不含 PEM),需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "获取 TLS 证书详情", + "parameters": [ + { + "type": "integer", + "description": "证书 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "证书详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.TLSCertificate" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/tls-certificates/{id}/content": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 返回证书与私钥 PEM 内容,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "获取 TLS 证书 PEM 内容", + "parameters": [ + { + "type": "integer", + "description": "证书 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "证书 PEM 内容", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/tls.CertificateContent" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/tls-certificates/{id}/convert-acme": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "将已上传证书转换为 ACME 自动续期模式,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "将证书转为 ACME 管理", + "parameters": [ + { + "type": "integer", + "description": "证书 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "ACME 申请参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tls.ApplyInput" + } + } + ], + "responses": { + "200": { + "description": "转换后的证书", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.TLSCertificate" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/tls-certificates/{id}/delete": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 删除 TLS 证书,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "删除 TLS 证书", + "parameters": [ + { + "type": "integer", + "description": "证书 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/tls-certificates/{id}/renew": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "手动触发 ACME 证书续期,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "续期 ACME 证书", + "parameters": [ + { + "type": "integer", + "description": "证书 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "续期后的证书", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.TLSCertificate" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/tls-certificates/{id}/update": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 更新 TLS 证书 PEM 信息,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "更新 TLS 证书", + "parameters": [ + { + "type": "integer", + "description": "证书 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "证书参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tls.CertificateInput" + } + } + ], + "responses": { + "200": { + "description": "更新后的证书", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.TLSCertificate" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/tls-certificates/{id}/update-acme": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 更新 ACME 证书申请配置,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "更新 ACME 证书配置", + "parameters": [ + { + "type": "integer", + "description": "证书 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "ACME 申请参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tls.ApplyInput" + } + } + ], + "responses": { + "200": { + "description": "更新后的证书", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.TLSCertificate" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/update/latest-release": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "查询 OpenFlare 服务端最新 GitHub Release 及升级状态,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-update" + ], + "summary": "获取最新服务端发布版本", + "parameters": [ + { + "type": "string", + "description": "发布渠道(stable/preview)", + "name": "channel", + "in": "query" + } + ], + "responses": { + "200": { + "description": "最新发布信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/update.LatestReleaseView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/update/logs/ws": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "通过 WebSocket 推送升级进度快照,需要管理员权限", + "tags": [ + "openflare-update" + ], + "summary": "流式获取服务端升级日志", + "responses": { + "101": { + "description": "WebSocket 升级日志流", + "schema": { + "$ref": "#/definitions/update.StreamSnapshot" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/update/manual-upgrade": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "确认并执行手动上传的服务端升级(当前功能已禁用),需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-update" + ], + "summary": "确认手动服务端升级", + "responses": { + "400": { + "description": "功能已禁用或参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/update/manual-upload": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "上传服务端二进制以进行手动升级(当前功能已禁用),需要管理员权限", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-update" + ], + "summary": "上传手动升级二进制", + "parameters": [ + { + "type": "file", + "description": "服务端二进制文件", + "name": "binary", + "in": "formData", + "required": true + } + ], + "responses": { + "400": { + "description": "功能已禁用或参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/update/upgrade": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "从最新 Release 调度 OpenFlare 服务端自动升级,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-update" + ], + "summary": "触发服务端自动升级", + "parameters": [ + { + "description": "升级参数", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/update.upgradeRequest" + } + } + ], + "responses": { + "200": { + "description": "升级任务已调度", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/update.LatestReleaseView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/uptimekuma/sync": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "将 OpenFlare 节点同步到 Uptime Kuma,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-option" + ], + "summary": "同步 Uptime Kuma", + "responses": { + "200": { + "description": "同步成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/waf/ip-groups": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回全部 WAF IP 组,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "列出 WAF IP 组", + "responses": { + "200": { + "description": "IP 组列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/waf.IPGroupView" + } + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + }, + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "创建新的 WAF IP 组,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "创建 WAF IP 组", + "parameters": [ + { + "description": "IP 组参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/waf.IPGroupInput" + } + } + ], + "responses": { + "200": { + "description": "创建成功的 IP 组", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/waf.IPGroupView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/waf/ip-groups/test": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "根据自动配置规则测试 IP 匹配结果(桩实现),需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "测试 WAF IP 组自动配置", + "parameters": [ + { + "description": "自动配置参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/waf.IPGroupAutoTestInput" + } + } + ], + "responses": { + "200": { + "description": "测试结果", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/waf.IPGroupAutoTestResult" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/waf/ip-groups/{id}": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 返回 WAF IP 组详情,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "获取 WAF IP 组详情", + "parameters": [ + { + "type": "integer", + "description": "IP 组 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "IP 组详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/waf.IPGroupView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/waf/ip-groups/{id}/delete": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 删除 WAF IP 组,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "删除 WAF IP 组", + "parameters": [ + { + "type": "integer", + "description": "IP 组 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/waf/ip-groups/{id}/sync": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "手动触发 WAF IP 组外部 IP 同步,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "同步 WAF IP 组", + "parameters": [ + { + "type": "integer", + "description": "IP 组 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "同步结果", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/waf.IPGroupSyncResult" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/waf/ip-groups/{id}/update": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 更新 WAF IP 组,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "更新 WAF IP 组", + "parameters": [ + { + "type": "integer", + "description": "IP 组 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "IP 组参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/waf.IPGroupInput" + } + } + ], + "responses": { + "200": { + "description": "更新后的 IP 组", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/waf.IPGroupView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/waf/rule-groups": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回全部 WAF 规则组,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "列出 WAF 规则组", + "responses": { + "200": { + "description": "规则组列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/waf.RuleGroupView" + } + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + }, + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "创建新的 WAF 规则组,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "创建 WAF 规则组", + "parameters": [ + { + "description": "规则组参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/waf.RuleGroupInput" + } + } + ], + "responses": { + "200": { + "description": "创建成功的规则组", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/waf.RuleGroupView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/waf/rule-groups/{id}": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 返回 WAF 规则组详情,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "获取 WAF 规则组详情", + "parameters": [ + { + "type": "integer", + "description": "规则组 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "规则组详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/waf.RuleGroupView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/waf/rule-groups/{id}/delete": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 删除 WAF 规则组,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "删除 WAF 规则组", + "parameters": [ + { + "type": "integer", + "description": "规则组 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/waf/rule-groups/{id}/sites": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "替换 WAF 规则组关联的代理站点列表,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "替换规则组站点绑定", + "parameters": [ + { + "type": "integer", + "description": "规则组 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "站点 ID 列表", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/waf.IDsRequest" + } + } + ], + "responses": { + "200": { + "description": "更新后的规则组", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/waf.RuleGroupView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/waf/rule-groups/{id}/update": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 更新 WAF 规则组,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "更新 WAF 规则组", + "parameters": [ + { + "type": "integer", + "description": "规则组 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "规则组参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/waf.RuleGroupInput" + } + } + ], + "responses": { + "200": { + "description": "更新后的规则组", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/waf.RuleGroupView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/waf/sites/{route_id}/rule-groups": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回代理站点关联的 WAF 规则组绑定,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "获取站点 WAF 规则组", + "parameters": [ + { + "type": "integer", + "description": "代理路由 ID", + "name": "route_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "站点规则组绑定", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/waf.SiteRuleGroupsView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + }, + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "替换代理站点关联的 WAF 规则组列表,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "替换站点 WAF 规则组", + "parameters": [ + { + "type": "integer", + "description": "代理路由 ID", + "name": "route_id", + "in": "path", + "required": true + }, + { + "description": "规则组 ID 列表", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/waf.IDsRequest" + } + } + ], + "responses": { + "200": { + "description": "更新后的站点规则组绑定", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/waf.SiteRuleGroupsView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, "/api/v1/oauth/callback": { "post": { "description": "接收前端传回的 state 和 code,完成 OAuth/OIDC 认证并建立会话。支持登录(login)和账号绑定(bind)两种场景。", @@ -4237,6 +11265,7 @@ const docTemplate = `{ "parameters": [ { "type": "integer", + "format": "int64", "description": "外部帐号绑定记录 ID", "name": "id", "in": "path", @@ -5464,6 +12493,54 @@ const docTemplate = `{ } }, "definitions": { + "apply_log.CleanupInput": { + "type": "object", + "properties": { + "delete_all": { + "type": "boolean" + }, + "retention_days": { + "type": "integer" + } + } + }, + "apply_log.CleanupResult": { + "type": "object", + "properties": { + "cutoff": { + "type": "string" + }, + "delete_all": { + "type": "boolean" + }, + "deleted_count": { + "type": "integer" + }, + "retention_days": { + "type": "integer" + } + } + }, + "apply_log.ListResult": { + "type": "object", + "properties": { + "current": { + "type": "integer" + }, + "rows": { + "type": "array", + "items": { + "$ref": "#/definitions/model.OpenFlareApplyLog" + } + }, + "total": { + "type": "integer" + }, + "totalPage": { + "type": "integer" + } + } + }, "auth_source.AuthSourceRequest": { "type": "object", "properties": { @@ -5579,6 +12656,297 @@ const docTemplate = `{ } } }, + "config_version.CleanupInput": { + "type": "object", + "properties": { + "keep_count": { + "type": "integer" + } + } + }, + "config_version.CleanupResult": { + "type": "object", + "properties": { + "deleted_count": { + "type": "integer" + }, + "message": { + "type": "string" + } + } + }, + "config_version.ConfigDiffResult": { + "type": "object", + "properties": { + "active_version": { + "type": "string" + }, + "active_website_count": { + "type": "integer" + }, + "added_domains": { + "type": "array", + "items": { + "type": "string" + } + }, + "added_sites": { + "type": "array", + "items": { + "type": "string" + } + }, + "changed_option_details": { + "type": "array", + "items": { + "$ref": "#/definitions/config_version.ConfigOptionDiffItem" + } + }, + "changed_option_keys": { + "type": "array", + "items": { + "type": "string" + } + }, + "current_website_count": { + "type": "integer" + }, + "main_config_changed": { + "type": "boolean" + }, + "modified_domains": { + "type": "array", + "items": { + "type": "string" + } + }, + "modified_sites": { + "type": "array", + "items": { + "type": "string" + } + }, + "removed_domains": { + "type": "array", + "items": { + "type": "string" + } + }, + "removed_sites": { + "type": "array", + "items": { + "type": "string" + } + }, + "waf_config_changed": { + "type": "boolean" + } + } + }, + "config_version.ConfigOptionDiffItem": { + "type": "object", + "properties": { + "current_value": { + "type": "string" + }, + "key": { + "type": "string" + }, + "previous_value": { + "type": "string" + } + } + }, + "config_version.ConfigPreviewResult": { + "type": "object", + "properties": { + "checksum": { + "type": "string" + }, + "main_config": { + "type": "string" + }, + "rendered_config": { + "type": "string" + }, + "route_config": { + "type": "string" + }, + "route_count": { + "type": "integer" + }, + "snapshot_json": { + "type": "string" + }, + "support_files": { + "type": "array", + "items": { + "$ref": "#/definitions/config_version.SupportFile" + } + }, + "website_count": { + "type": "integer" + } + } + }, + "config_version.SupportFile": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "path": { + "type": "string" + } + } + }, + "dashboard.Capacity": { + "type": "object", + "properties": { + "average_cpu_usage_percent": { + "type": "number" + }, + "average_memory_usage_percent": { + "type": "number" + }, + "high_cpu_nodes": { + "type": "integer" + }, + "high_memory_nodes": { + "type": "integer" + }, + "high_storage_nodes": { + "type": "integer" + } + } + }, + "dashboard.OverviewPayload": { + "type": "object", + "properties": { + "capacity": { + "$ref": "#/definitions/dashboard.Capacity" + }, + "distributions": { + "$ref": "#/definitions/dashboard.distributionsPayload" + }, + "generated_at": {}, + "nodes": { + "type": "array", + "items": { + "type": "array", + "items": {} + } + }, + "summary": { + "$ref": "#/definitions/dashboard.Summary" + }, + "traffic": { + "$ref": "#/definitions/dashboard.Traffic" + }, + "trends": { + "$ref": "#/definitions/dashboard.trendsPayload" + } + } + }, + "dashboard.Summary": { + "type": "object", + "properties": { + "offline_nodes": { + "type": "integer" + }, + "online_nodes": { + "type": "integer" + }, + "pending_nodes": { + "type": "integer" + }, + "total_nodes": { + "type": "integer" + }, + "unhealthy_nodes": { + "type": "integer" + } + } + }, + "dashboard.Traffic": { + "type": "object", + "properties": { + "error_count": { + "type": "integer" + }, + "estimated_qps": { + "type": "number" + }, + "reported_nodes": { + "type": "integer" + }, + "request_count": { + "type": "integer" + }, + "unique_visitors": { + "type": "integer" + } + } + }, + "dashboard.distributionsPayload": { + "type": "object", + "properties": { + "source_countries": { + "type": "array", + "items": { + "type": "array", + "items": {} + } + }, + "status_codes": { + "type": "array", + "items": { + "type": "array", + "items": {} + } + }, + "top_domains": { + "type": "array", + "items": { + "type": "array", + "items": {} + } + } + } + }, + "dashboard.trendsPayload": { + "type": "object", + "properties": { + "capacity_24h": { + "type": "array", + "items": { + "type": "array", + "items": {} + } + }, + "disk_io_24h": { + "type": "array", + "items": { + "type": "array", + "items": {} + } + }, + "network_24h": { + "type": "array", + "items": { + "type": "array", + "items": {} + } + }, + "traffic_24h": { + "type": "array", + "items": { + "type": "array", + "items": {} + } + } + } + }, "db_manage.DBOverviewResponse": { "type": "object", "properties": { @@ -5986,6 +13354,26 @@ const docTemplate = `{ } } }, + "model.AcmeAccount": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, "model.AuthSource": { "type": "object", "properties": { @@ -6027,6 +13415,84 @@ const docTemplate = `{ } } }, + "model.ConfigVersion": { + "type": "object", + "properties": { + "checksum": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "created_by": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + }, + "main_config": { + "type": "string" + }, + "rendered_config": { + "type": "string" + }, + "snapshot_json": { + "type": "string" + }, + "support_files_json": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "model.ConfigVersionSummary": { + "type": "object", + "properties": { + "checksum": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "created_by": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + }, + "version": { + "type": "string" + } + } + }, + "model.DNSAccount": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "model.ExternalAccountView": { "type": "object", "properties": { @@ -6056,6 +13522,254 @@ const docTemplate = `{ } } }, + "model.ManagedDomain": { + "type": "object", + "properties": { + "cert_id": { + "type": "integer" + }, + "created_at": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "remark": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "model.OpenFlareApplyLog": { + "type": "object", + "properties": { + "checksum": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "main_config_checksum": { + "type": "string" + }, + "message": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "result": { + "type": "string" + }, + "route_config_checksum": { + "type": "string" + }, + "support_file_count": { + "type": "integer" + }, + "version": { + "type": "string" + } + } + }, + "model.OpenFlareHealthEvent": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "event_type": { + "type": "string" + }, + "first_triggered_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_triggered_at": { + "type": "string" + }, + "message": { + "type": "string" + }, + "metadata_json": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "reported_at": { + "type": "string" + }, + "resolved_at": { + "type": "string" + }, + "severity": { + "type": "string" + }, + "status": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "model.OpenFlareMetricSnapshot": { + "type": "object", + "properties": { + "captured_at": { + "type": "string" + }, + "cpu_usage_percent": { + "type": "number" + }, + "created_at": { + "type": "string" + }, + "disk_read_bytes": { + "type": "integer" + }, + "disk_write_bytes": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "memory_total_bytes": { + "type": "integer" + }, + "memory_used_bytes": { + "type": "integer" + }, + "network_rx_bytes": { + "type": "integer" + }, + "network_tx_bytes": { + "type": "integer" + }, + "node_id": { + "type": "string" + }, + "storage_total_bytes": { + "type": "integer" + }, + "storage_used_bytes": { + "type": "integer" + } + } + }, + "model.OpenFlareNodeSystemProfile": { + "type": "object", + "properties": { + "architecture": { + "type": "string" + }, + "cpu_cores": { + "type": "integer" + }, + "cpu_model": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "hostname": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "kernel_version": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "os_name": { + "type": "string" + }, + "os_version": { + "type": "string" + }, + "reported_at": { + "type": "string" + }, + "total_disk_bytes": { + "type": "integer" + }, + "total_memory_bytes": { + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "uptime_seconds": { + "type": "integer" + } + } + }, + "model.OpenFlareOption": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "model.OpenFlareRequestReport": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "error_count": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "node_id": { + "type": "string" + }, + "request_count": { + "type": "integer" + }, + "source_countries_json": { + "type": "string" + }, + "status_codes_json": { + "type": "string" + }, + "top_domains_json": { + "type": "string" + }, + "unique_visitor_count": { + "type": "integer" + }, + "window_ended_at": { + "type": "string" + }, + "window_started_at": { + "type": "string" + } + } + }, "model.PushChannel": { "type": "object", "properties": { @@ -6238,6 +13952,71 @@ const docTemplate = `{ } } }, + "model.TLSCertificate": { + "type": "object", + "properties": { + "acme_account_id": { + "type": "integer" + }, + "apply_message": { + "type": "string" + }, + "apply_status": { + "type": "string" + }, + "auto_renew": { + "type": "boolean" + }, + "created_at": { + "type": "string" + }, + "disable_cname": { + "type": "boolean" + }, + "dns1": { + "type": "string" + }, + "dns2": { + "type": "string" + }, + "dns_account_id": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "key_algorithm": { + "type": "string" + }, + "name": { + "type": "string" + }, + "not_after": { + "type": "string" + }, + "not_before": { + "type": "string" + }, + "other_domains": { + "type": "string" + }, + "primary_domain": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "skip_dns": { + "type": "boolean" + }, + "updated_at": { + "type": "string" + } + } + }, "model.TaskExecution": { "type": "object", "properties": { @@ -6464,12 +14243,296 @@ const docTemplate = `{ "UploadStatusPending": "待使用", "UploadStatusUsed": "已使用" }, + "x-enum-descriptions": [ + "待使用", + "已使用", + "已删除" + ], "x-enum-varnames": [ "UploadStatusPending", "UploadStatusUsed", "UploadStatusDeleted" ] }, + "node.AgentReleaseInfo": { + "type": "object", + "properties": { + "body": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "current_version": { + "type": "string" + }, + "has_update": { + "type": "boolean" + }, + "html_url": { + "type": "string" + }, + "prerelease": { + "type": "boolean" + }, + "published_at": { + "type": "string" + }, + "requested_channel": { + "type": "string" + }, + "requested_tag": { + "type": "string" + }, + "tag_name": { + "type": "string" + }, + "update_requested": { + "type": "boolean" + } + } + }, + "node.AgentUpdateInput": { + "type": "object", + "properties": { + "channel": { + "type": "string" + }, + "tag_name": { + "type": "string" + } + } + }, + "node.BootstrapView": { + "type": "object", + "properties": { + "discovery_token": { + "type": "string" + } + } + }, + "node.HealthEventCleanupResult": { + "type": "object", + "properties": { + "deleted_count": { + "type": "integer" + }, + "node_id": { + "type": "string" + } + } + }, + "node.Input": { + "type": "object", + "properties": { + "auto_update_enabled": { + "type": "boolean" + }, + "geo_latitude": { + "type": "number" + }, + "geo_longitude": { + "type": "number" + }, + "geo_manual_override": { + "type": "boolean" + }, + "geo_name": { + "type": "string" + }, + "ip": { + "type": "string" + }, + "ip_manual_override": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "node_type": { + "type": "string" + }, + "relay_agent_access_addr": { + "type": "string" + }, + "relay_bind_port": { + "type": "integer" + }, + "relay_client_access_addr": { + "type": "string" + }, + "relay_client_proxy_url": { + "type": "string" + }, + "relay_vhost_http_port": { + "type": "integer" + }, + "relay_web_server_enabled": { + "type": "boolean" + } + } + }, + "node.ObservabilityView": { + "type": "object", + "properties": { + "analytics": { + "$ref": "#/definitions/observability.NodeAnalytics" + }, + "health_events": { + "type": "array", + "items": { + "$ref": "#/definitions/model.OpenFlareHealthEvent" + } + }, + "metric_snapshots": { + "type": "array", + "items": { + "$ref": "#/definitions/model.OpenFlareMetricSnapshot" + } + }, + "node_id": { + "type": "string" + }, + "profile": { + "$ref": "#/definitions/model.OpenFlareNodeSystemProfile" + }, + "relay_dashboard": { + "$ref": "#/definitions/observability.RelayDashboardSnapshot" + }, + "traffic_reports": { + "type": "array", + "items": { + "$ref": "#/definitions/model.OpenFlareRequestReport" + } + }, + "trends": { + "$ref": "#/definitions/observability.NodeTrends" + } + } + }, + "node.View": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "auto_update_enabled": { + "type": "boolean" + }, + "created_at": { + "type": "string" + }, + "current_version": { + "type": "string" + }, + "ext_version": { + "type": "string" + }, + "geo_latitude": { + "type": "number" + }, + "geo_longitude": { + "type": "number" + }, + "geo_manual_override": { + "type": "boolean" + }, + "geo_name": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "ip": { + "type": "string" + }, + "ip_manual_override": { + "type": "boolean" + }, + "last_error": { + "type": "string" + }, + "last_seen_at": {}, + "latest_apply_at": { + "type": "string" + }, + "latest_apply_checksum": { + "type": "string" + }, + "latest_apply_message": { + "type": "string" + }, + "latest_apply_result": { + "type": "string" + }, + "latest_main_config_checksum": { + "type": "string" + }, + "latest_route_config_checksum": { + "type": "string" + }, + "latest_support_file_count": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "node_type": { + "type": "string" + }, + "openresty_message": { + "type": "string" + }, + "openresty_status": { + "type": "string" + }, + "relay_agent_access_addr": { + "type": "string" + }, + "relay_bind_port": { + "type": "integer" + }, + "relay_client_access_addr": { + "type": "string" + }, + "relay_client_proxy_url": { + "type": "string" + }, + "relay_status": { + "type": "string" + }, + "relay_vhost_http_port": { + "type": "integer" + }, + "relay_web_server_enabled": { + "type": "boolean" + }, + "restart_openresty_requested": { + "type": "boolean" + }, + "status": { + "type": "string" + }, + "update_channel": { + "type": "string" + }, + "update_requested": { + "type": "boolean" + }, + "update_tag": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, "oauth.AuthSourceView": { "type": "object", "properties": { @@ -6571,6 +14634,1209 @@ const docTemplate = `{ } } }, + "observability.AccessLogCleanupInput": { + "type": "object", + "properties": { + "retention_days": { + "type": "integer" + } + } + }, + "observability.AccessLogCleanupResult": { + "type": "object", + "properties": { + "cutoff": { + "type": "string" + }, + "deleted_count": { + "type": "integer" + }, + "retention_days": { + "type": "integer" + } + } + }, + "observability.AccessLogIPSummaryList": { + "type": "object", + "properties": { + "has_more": { + "type": "boolean" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.AccessLogIPSummaryView" + } + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "sort_by": { + "type": "string" + }, + "sort_order": { + "type": "string" + }, + "total_ip": { + "type": "integer" + } + } + }, + "observability.AccessLogIPSummaryView": { + "type": "object", + "properties": { + "last_seen_at": { + "type": "string" + }, + "recent_requests": { + "type": "integer" + }, + "remote_addr": { + "type": "string" + }, + "total_requests": { + "type": "integer" + } + } + }, + "observability.AccessLogIPTrendPoint": { + "type": "object", + "properties": { + "bucket_started_at": { + "type": "string" + }, + "request_count": { + "type": "integer" + } + } + }, + "observability.AccessLogIPTrendView": { + "type": "object", + "properties": { + "bucket_minutes": { + "type": "integer" + }, + "hours": { + "type": "integer" + }, + "points": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.AccessLogIPTrendPoint" + } + }, + "remote_addr": { + "type": "string" + } + } + }, + "observability.AccessLogList": { + "type": "object", + "properties": { + "has_more": { + "type": "boolean" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.AccessLogView" + } + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total_ip": { + "type": "integer" + }, + "total_record": { + "type": "integer" + } + } + }, + "observability.AccessLogView": { + "type": "object", + "properties": { + "host": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "logged_at": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "node_name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "region": { + "type": "string" + }, + "remote_addr": { + "type": "string" + }, + "status_code": { + "type": "integer" + } + } + }, + "observability.CapacityTrendPoint": { + "type": "object", + "properties": { + "average_cpu_usage_percent": { + "type": "number" + }, + "average_memory_usage_percent": { + "type": "number" + }, + "bucket_started_at": { + "type": "string" + }, + "reported_nodes": { + "type": "integer" + } + } + }, + "observability.DiskIOTrendPoint": { + "type": "object", + "properties": { + "bucket_started_at": { + "type": "string" + }, + "disk_read_bytes": { + "type": "integer" + }, + "disk_write_bytes": { + "type": "integer" + }, + "reported_nodes": { + "type": "integer" + } + } + }, + "observability.DistributionItem": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "integer" + } + } + }, + "observability.FoldedAccessLogIPList": { + "type": "object", + "properties": { + "bucket_started_at": { + "type": "string" + }, + "fold_minutes": { + "type": "integer" + }, + "has_more": { + "type": "boolean" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.FoldedAccessLogIPView" + } + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "sort_by": { + "type": "string" + }, + "sort_order": { + "type": "string" + }, + "total_ip": { + "type": "integer" + } + } + }, + "observability.FoldedAccessLogIPView": { + "type": "object", + "properties": { + "client_error_count": { + "type": "integer" + }, + "last_seen_at": { + "type": "string" + }, + "remote_addr": { + "type": "string" + }, + "request_count": { + "type": "integer" + }, + "server_error_count": { + "type": "integer" + }, + "success_count": { + "type": "integer" + } + } + }, + "observability.FoldedAccessLogList": { + "type": "object", + "properties": { + "fold_minutes": { + "type": "integer" + }, + "has_more": { + "type": "boolean" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.FoldedAccessLogView" + } + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total_bucket": { + "type": "integer" + }, + "total_ip": { + "type": "integer" + }, + "total_record": { + "type": "integer" + } + } + }, + "observability.FoldedAccessLogView": { + "type": "object", + "properties": { + "bucket_started_at": { + "type": "string" + }, + "client_error_count": { + "type": "integer" + }, + "request_count": { + "type": "integer" + }, + "server_error_count": { + "type": "integer" + }, + "success_count": { + "type": "integer" + }, + "unique_host_count": { + "type": "integer" + }, + "unique_ip_count": { + "type": "integer" + } + } + }, + "observability.HealthSummary": { + "type": "object", + "properties": { + "active_alerts": { + "type": "integer" + }, + "critical_alerts": { + "type": "integer" + }, + "has_capacity_risk": { + "type": "boolean" + }, + "has_runtime_risk": { + "type": "boolean" + }, + "has_traffic_risk": { + "type": "boolean" + }, + "info_alerts": { + "type": "integer" + }, + "resolved_alerts": { + "type": "integer" + }, + "warning_alerts": { + "type": "integer" + } + } + }, + "observability.NetworkTrendPoint": { + "type": "object", + "properties": { + "bucket_started_at": { + "type": "string" + }, + "network_rx_bytes": { + "type": "integer" + }, + "network_tx_bytes": { + "type": "integer" + }, + "openresty_rx_bytes": { + "type": "integer" + }, + "openresty_tx_bytes": { + "type": "integer" + }, + "reported_nodes": { + "type": "integer" + } + } + }, + "observability.NodeAnalytics": { + "type": "object", + "properties": { + "distributions": { + "$ref": "#/definitions/observability.TrafficDistributions" + }, + "health": { + "$ref": "#/definitions/observability.HealthSummary" + }, + "traffic": { + "$ref": "#/definitions/observability.TrafficWindowSummary" + } + } + }, + "observability.NodeTrends": { + "type": "object", + "properties": { + "capacity_24h": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.CapacityTrendPoint" + } + }, + "disk_io_24h": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.DiskIOTrendPoint" + } + }, + "network_24h": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.NetworkTrendPoint" + } + }, + "traffic_24h": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.TrafficTrendPoint" + } + } + } + }, + "observability.RelayDashboardSnapshot": { + "type": "object", + "properties": { + "client_counts": { + "type": "integer" + }, + "offline_proxies": { + "type": "integer" + }, + "online_proxies": { + "type": "integer" + }, + "proxies": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.RelayProxyStat" + } + }, + "total_connections": { + "type": "integer" + }, + "total_proxies": { + "type": "integer" + } + } + }, + "observability.RelayProxyStat": { + "type": "object", + "properties": { + "client_addr": { + "type": "string" + }, + "client_version": { + "type": "string" + }, + "last_close_time": { + "type": "string" + }, + "last_start_time": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "observability.TrafficDistributions": { + "type": "object", + "properties": { + "source_countries": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.DistributionItem" + } + }, + "status_codes": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.DistributionItem" + } + }, + "top_domains": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.DistributionItem" + } + } + } + }, + "observability.TrafficTrendPoint": { + "type": "object", + "properties": { + "bucket_started_at": { + "type": "string" + }, + "error_count": { + "type": "integer" + }, + "request_count": { + "type": "integer" + }, + "unique_visitor_count": { + "type": "integer" + } + } + }, + "observability.TrafficWindowSummary": { + "type": "object", + "properties": { + "error_count": { + "type": "integer" + }, + "error_rate_percent": { + "type": "number" + }, + "estimated_qps": { + "type": "number" + }, + "request_count": { + "type": "integer" + }, + "unique_visitor_count": { + "type": "integer" + }, + "window_ended_at": { + "type": "string" + }, + "window_started_at": { + "type": "string" + } + } + }, + "option.databaseCleanupInput": { + "type": "object", + "properties": { + "retention_days": { + "type": "integer" + }, + "target": { + "type": "string" + } + } + }, + "option.databaseCleanupResult": { + "type": "object", + "properties": { + "delete_all": { + "type": "boolean" + }, + "deleted_count": { + "type": "integer" + }, + "retention_days": { + "type": "integer" + }, + "target": { + "type": "string" + }, + "target_label": { + "type": "string" + } + } + }, + "option.geoIPLookupRequest": { + "type": "object", + "properties": { + "ip": { + "type": "string" + }, + "provider": { + "type": "string" + } + } + }, + "option.geoIPLookupView": { + "type": "object", + "properties": { + "ip": { + "type": "string" + }, + "iso_code": { + "type": "string" + }, + "latitude": { + "type": "number" + }, + "longitude": { + "type": "number" + }, + "name": { + "type": "string" + }, + "provider": { + "type": "string" + } + } + }, + "option.optionBatchPayload": { + "type": "object", + "properties": { + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/model.OpenFlareOption" + } + } + } + }, + "option.publicAuthSourceView": { + "type": "object", + "properties": { + "authorize_url": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "option.statusView": { + "type": "object", + "properties": { + "auth_sources": { + "type": "array", + "items": { + "$ref": "#/definitions/option.publicAuthSourceView" + } + }, + "cap_login_enabled": { + "type": "boolean" + }, + "email_verification": { + "type": "boolean" + }, + "footer_html": { + "type": "string" + }, + "github_client_id": { + "type": "string" + }, + "github_oauth": { + "type": "boolean" + }, + "home_page_link": { + "type": "string" + }, + "password_register_enabled": { + "type": "boolean" + }, + "server_address": { + "type": "string" + }, + "start_time": { + "type": "integer" + }, + "system_name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "wechat_login": { + "type": "boolean" + }, + "wechat_qrcode": { + "type": "string" + } + } + }, + "origin.DetailView": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "route_count": { + "type": "integer" + }, + "routes": { + "type": "array", + "items": { + "$ref": "#/definitions/origin.RouteSummary" + } + }, + "updated_at": { + "type": "string" + } + } + }, + "origin.Input": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "name": { + "type": "string" + }, + "remark": { + "type": "string" + } + } + }, + "origin.RouteSummary": { + "type": "object", + "properties": { + "domain": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "origin_url": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "origin.View": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "route_count": { + "type": "integer" + }, + "updated_at": { + "type": "string" + } + } + }, + "pages.DeploymentFileView": { + "type": "object", + "properties": { + "checksum": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "deployment_id": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "path": { + "type": "string" + }, + "size": { + "type": "integer" + } + } + }, + "pages.DeploymentView": { + "type": "object", + "properties": { + "activated_at": { + "type": "string" + }, + "checksum": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "created_by": { + "type": "string" + }, + "deployment_number": { + "type": "integer" + }, + "file_count": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "project_id": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "total_size": { + "type": "integer" + } + } + }, + "pages.Input": { + "type": "object", + "properties": { + "api_proxy_enabled": { + "type": "boolean" + }, + "api_proxy_pass": { + "type": "string" + }, + "api_proxy_path": { + "type": "string" + }, + "api_proxy_rewrite": { + "type": "string" + }, + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "entry_file": { + "type": "string" + }, + "name": { + "type": "string" + }, + "root_dir": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "spa_fallback_enabled": { + "type": "boolean" + }, + "spa_fallback_path": { + "type": "string" + } + } + }, + "pages.View": { + "type": "object", + "properties": { + "active_deployment": { + "$ref": "#/definitions/pages.DeploymentView" + }, + "active_deployment_id": { + "type": "integer" + }, + "api_proxy_enabled": { + "type": "boolean" + }, + "api_proxy_pass": { + "type": "string" + }, + "api_proxy_path": { + "type": "string" + }, + "api_proxy_rewrite": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "deployment_count": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "entry_file": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "root_dir": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "spa_fallback_enabled": { + "type": "boolean" + }, + "spa_fallback_path": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "proxy_route.CustomHeaderInput": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "proxy_route.Input": { + "type": "object", + "properties": { + "basic_auth_enabled": { + "type": "boolean" + }, + "basic_auth_password": { + "type": "string" + }, + "basic_auth_username": { + "type": "string" + }, + "cache_enabled": { + "type": "boolean" + }, + "cache_policy": { + "type": "string" + }, + "cache_rules": { + "type": "array", + "items": { + "type": "string" + } + }, + "cert_id": { + "type": "integer" + }, + "cert_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "custom_headers": { + "type": "array", + "items": { + "$ref": "#/definitions/proxy_route.CustomHeaderInput" + } + }, + "domain": { + "type": "string" + }, + "domain_cert_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "domains": { + "type": "array", + "items": { + "type": "string" + } + }, + "enable_https": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "limit_conn_per_ip": { + "type": "integer" + }, + "limit_conn_per_server": { + "type": "integer" + }, + "limit_rate": { + "type": "string" + }, + "origin_address": { + "type": "string" + }, + "origin_host": { + "type": "string" + }, + "origin_id": { + "type": "integer" + }, + "origin_port": { + "type": "string" + }, + "origin_scheme": { + "type": "string" + }, + "origin_uri": { + "type": "string" + }, + "origin_url": { + "type": "string" + }, + "pages_project_id": { + "type": "integer" + }, + "redirect_http": { + "type": "boolean" + }, + "remark": { + "type": "string" + }, + "site_name": { + "type": "string" + }, + "tunnel_id": { + "type": "integer" + }, + "tunnel_node_id": { + "type": "integer" + }, + "tunnel_target_addr": { + "type": "string" + }, + "tunnel_target_protocol": { + "type": "string" + }, + "upstream_type": { + "type": "string" + }, + "upstreams": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "proxy_route.View": { + "type": "object", + "properties": { + "basic_auth_enabled": { + "type": "boolean" + }, + "basic_auth_password": { + "type": "string" + }, + "basic_auth_username": { + "type": "string" + }, + "cache_enabled": { + "type": "boolean" + }, + "cache_policy": { + "type": "string" + }, + "cache_rule_list": { + "type": "array", + "items": { + "type": "string" + } + }, + "cache_rules": { + "type": "string" + }, + "cert_id": { + "type": "integer" + }, + "cert_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "created_at": { + "type": "string" + }, + "custom_header_list": { + "type": "array", + "items": { + "$ref": "#/definitions/proxy_route.CustomHeaderInput" + } + }, + "custom_headers": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "domain_cert_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "domain_count": { + "type": "integer" + }, + "domains": { + "type": "array", + "items": { + "type": "string" + } + }, + "enable_https": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "limit_conn_per_ip": { + "type": "integer" + }, + "limit_conn_per_server": { + "type": "integer" + }, + "limit_rate": { + "type": "string" + }, + "origin_host": { + "type": "string" + }, + "origin_id": { + "type": "integer" + }, + "origin_url": { + "type": "string" + }, + "pages_project_id": { + "type": "integer" + }, + "primary_domain": { + "type": "string" + }, + "redirect_http": { + "type": "boolean" + }, + "remark": { + "type": "string" + }, + "site_name": { + "type": "string" + }, + "tunnel_id": { + "type": "integer" + }, + "tunnel_node_id": { + "type": "integer" + }, + "tunnel_target_addr": { + "type": "string" + }, + "tunnel_target_protocol": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "upstream_list": { + "type": "array", + "items": { + "type": "string" + } + }, + "upstream_type": { + "type": "string" + }, + "upstreams": { + "type": "string" + } + } + }, "push.Config": { "type": "object", "properties": { @@ -7265,6 +16531,277 @@ const docTemplate = `{ } } }, + "tls.ApplyInput": { + "type": "object", + "properties": { + "acme_account_id": { + "type": "integer" + }, + "auto_renew": { + "type": "boolean" + }, + "disable_cname": { + "type": "boolean" + }, + "dns1": { + "type": "string" + }, + "dns2": { + "type": "string" + }, + "dns_account_id": { + "type": "integer" + }, + "key_algorithm": { + "type": "string" + }, + "name": { + "type": "string" + }, + "other_domains": { + "type": "string" + }, + "primary_domain": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "skip_dns": { + "type": "boolean" + } + } + }, + "tls.CertificateContent": { + "type": "object", + "properties": { + "acme_account_id": { + "type": "integer" + }, + "apply_message": { + "type": "string" + }, + "apply_status": { + "type": "string" + }, + "auto_renew": { + "type": "boolean" + }, + "cert_pem": { + "type": "string" + }, + "disable_cname": { + "type": "boolean" + }, + "dns1": { + "type": "string" + }, + "dns2": { + "type": "string" + }, + "dns_account_id": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "key_algorithm": { + "type": "string" + }, + "key_pem": { + "type": "string" + }, + "name": { + "type": "string" + }, + "other_domains": { + "type": "string" + }, + "primary_domain": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "skip_dns": { + "type": "boolean" + } + } + }, + "tls.CertificateInput": { + "type": "object", + "properties": { + "cert_pem": { + "type": "string" + }, + "key_pem": { + "type": "string" + }, + "name": { + "type": "string" + }, + "remark": { + "type": "string" + } + } + }, + "tls.DNSAccountInput": { + "type": "object", + "properties": { + "authorization": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "tls.ManagedDomainInput": { + "type": "object", + "properties": { + "cert_id": { + "type": "integer" + }, + "domain": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "remark": { + "type": "string" + } + } + }, + "tls.ManagedDomainMatchCandidate": { + "type": "object", + "properties": { + "certificate_id": { + "type": "integer" + }, + "certificate_name": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "managed_domain_id": { + "type": "integer" + }, + "match_type": { + "type": "string" + } + } + }, + "tls.ManagedDomainMatchResult": { + "type": "object", + "properties": { + "candidate": { + "$ref": "#/definitions/tls.ManagedDomainMatchCandidate" + }, + "candidates": { + "type": "array", + "items": { + "$ref": "#/definitions/tls.ManagedDomainMatchCandidate" + } + }, + "domain": { + "type": "string" + }, + "matched": { + "type": "boolean" + } + } + }, + "update.LatestReleaseView": { + "type": "object", + "properties": { + "body": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "current_version": { + "type": "string" + }, + "has_update": { + "type": "boolean" + }, + "html_url": { + "type": "string" + }, + "in_progress": { + "type": "boolean" + }, + "prerelease": { + "type": "boolean" + }, + "published_at": { + "type": "string" + }, + "tag_name": { + "type": "string" + }, + "upgrade_logs": { + "type": "array", + "items": { + "$ref": "#/definitions/update.UpgradeLogRecord" + } + }, + "upgrade_status": { + "type": "string" + }, + "upgrade_supported": { + "type": "boolean" + } + } + }, + "update.StreamSnapshot": { + "type": "object", + "properties": { + "in_progress": { + "type": "boolean" + }, + "upgrade_logs": { + "type": "array", + "items": { + "$ref": "#/definitions/update.UpgradeLogRecord" + } + }, + "upgrade_status": { + "type": "string" + } + } + }, + "update.UpgradeLogRecord": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "level": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "update.upgradeRequest": { + "type": "object", + "properties": { + "channel": { + "type": "string" + } + } + }, "updater.Status": { "type": "object", "properties": { @@ -7529,6 +17066,456 @@ const docTemplate = `{ "type": "string" } } + }, + "waf.IDsRequest": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "waf.IPGroupAutoTestInput": { + "type": "object", + "properties": { + "auto_config": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "waf.IPGroupAutoTestResult": { + "type": "object", + "properties": { + "lookback_minutes": { + "type": "integer" + }, + "matched_count": { + "type": "integer" + }, + "matched_ips": { + "type": "array", + "items": { + "type": "string" + } + }, + "rule_count": { + "type": "integer" + }, + "tested_at": { + "type": "string" + } + } + }, + "waf.IPGroupExtIPView": { + "type": "object", + "properties": { + "captured_at": { + "type": "string" + }, + "ip": { + "type": "string" + } + } + }, + "waf.IPGroupInput": { + "type": "object", + "properties": { + "auto_config": { + "type": "array", + "items": { + "type": "integer" + } + }, + "enabled": { + "type": "boolean" + }, + "ip_list": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "subscription_format": { + "type": "string" + }, + "subscription_mapping_rule": { + "type": "string" + }, + "subscription_url": { + "type": "string" + }, + "sync_interval_minutes": { + "type": "integer" + }, + "type": { + "type": "string" + } + } + }, + "waf.IPGroupSyncResult": { + "type": "object", + "properties": { + "group": { + "$ref": "#/definitions/waf.IPGroupView" + }, + "ip_count": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "next_sync_at": { + "type": "string" + }, + "status": { + "type": "string" + }, + "synced_at": { + "type": "string" + } + } + }, + "waf.IPGroupView": { + "type": "object", + "properties": { + "auto_config": { + "type": "array", + "items": { + "type": "integer" + } + }, + "created_at": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "ext_ips": { + "type": "array", + "items": { + "$ref": "#/definitions/waf.IPGroupExtIPView" + } + }, + "id": { + "type": "integer" + }, + "ip_list": { + "type": "array", + "items": { + "type": "string" + } + }, + "last_sync_message": { + "type": "string" + }, + "last_sync_status": { + "type": "string" + }, + "last_synced_at": { + "type": "string" + }, + "name": { + "type": "string" + }, + "next_sync_at": { + "type": "string" + }, + "referenced_by_rule_count": { + "type": "integer" + }, + "remark": { + "type": "string" + }, + "subscription_format": { + "type": "string" + }, + "subscription_mapping_rule": { + "type": "string" + }, + "subscription_url": { + "type": "string" + }, + "sync_interval_minutes": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "waf.PoWConfig": { + "type": "object", + "properties": { + "algorithm": { + "type": "string" + }, + "blacklist": { + "$ref": "#/definitions/waf.PoWListConfig" + }, + "challenge_ttl": { + "type": "integer" + }, + "difficulty": { + "type": "integer" + }, + "session_ttl": { + "type": "integer" + }, + "whitelist": { + "$ref": "#/definitions/waf.PoWListConfig" + } + } + }, + "waf.PoWListConfig": { + "type": "object", + "properties": { + "ip_cidrs": { + "type": "array", + "items": { + "type": "string" + } + }, + "ips": { + "type": "array", + "items": { + "type": "string" + } + }, + "path_regexes": { + "type": "array", + "items": { + "type": "string" + } + }, + "paths": { + "type": "array", + "items": { + "type": "string" + } + }, + "user_agents": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "waf.RuleGroupInput": { + "type": "object", + "properties": { + "block_response_body": { + "type": "string" + }, + "block_status_code": { + "type": "integer" + }, + "country_blacklist": { + "type": "array", + "items": { + "type": "string" + } + }, + "country_whitelist": { + "type": "array", + "items": { + "type": "string" + } + }, + "enabled": { + "type": "boolean" + }, + "ip_blacklist": { + "type": "array", + "items": { + "type": "string" + } + }, + "ip_blacklist_group_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "ip_whitelist": { + "type": "array", + "items": { + "type": "string" + } + }, + "ip_whitelist_group_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "name": { + "type": "string" + }, + "pow_config": { + "type": "array", + "items": { + "type": "integer" + } + }, + "pow_enabled": { + "type": "boolean" + }, + "region_blacklist": { + "type": "array", + "items": { + "type": "string" + } + }, + "region_whitelist": { + "type": "array", + "items": { + "type": "string" + } + }, + "remark": { + "type": "string" + } + } + }, + "waf.RuleGroupView": { + "type": "object", + "properties": { + "applied_site_count": { + "type": "integer" + }, + "applied_site_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "block_response_body": { + "type": "string" + }, + "block_status_code": { + "type": "integer" + }, + "country_blacklist": { + "type": "array", + "items": { + "type": "string" + } + }, + "country_whitelist": { + "type": "array", + "items": { + "type": "string" + } + }, + "created_at": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "ip_blacklist": { + "type": "array", + "items": { + "type": "string" + } + }, + "ip_blacklist_group_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "ip_whitelist": { + "type": "array", + "items": { + "type": "string" + } + }, + "ip_whitelist_group_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "is_global": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "pow_config": { + "$ref": "#/definitions/waf.PoWConfig" + }, + "pow_enabled": { + "type": "boolean" + }, + "region_blacklist": { + "type": "array", + "items": { + "type": "string" + } + }, + "region_whitelist": { + "type": "array", + "items": { + "type": "string" + } + }, + "remark": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "waf.SiteRuleGroupsView": { + "type": "object", + "properties": { + "applied_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "applied_rule_groups": { + "type": "array", + "items": { + "$ref": "#/definitions/waf.RuleGroupView" + } + }, + "global_rule_group": { + "$ref": "#/definitions/waf.RuleGroupView" + }, + "route_id": { + "type": "integer" + }, + "rule_groups": { + "type": "array", + "items": { + "$ref": "#/definitions/waf.RuleGroupView" + } + } + } } }, "securityDefinitions": { @@ -7546,8 +17533,8 @@ var SwaggerInfo = &swag.Spec{ Host: "", BasePath: "/", Schemes: []string{}, - Title: "Wavelet API", - Description: "Wavelet 平台后端 API,提供用户认证、系统配置、任务调度等通用功能。", + Title: "OpenFlare API", + Description: "OpenFlare 平台后端 API,提供用户认证、系统配置、任务调度与边缘节点管理能力。", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, LeftDelim: "{{", diff --git a/Wavelet/docs/swagger.json b/Wavelet/docs/swagger.json index 68b5835a..7d7620d9 100644 --- a/Wavelet/docs/swagger.json +++ b/Wavelet/docs/swagger.json @@ -1,11 +1,11 @@ { "swagger": "2.0", "info": { - "description": "Wavelet 平台后端 API,提供用户认证、系统配置、任务调度等通用功能。", - "title": "Wavelet API", + "description": "OpenFlare 平台后端 API,提供用户认证、系统配置、任务调度与边缘节点管理能力。", + "title": "OpenFlare API", "contact": { - "name": "Wavelet", - "url": "https://github.com/Rain-kl/Wavelet" + "name": "OpenFlare", + "url": "https://github.com/Rain-kl/OpenFlare" }, "license": { "name": "Apache 2.0", @@ -278,6 +278,7 @@ "parameters": [ { "type": "integer", + "format": "int64", "description": "认证源 ID 或名称", "name": "id", "in": "path", @@ -355,6 +356,7 @@ "parameters": [ { "type": "integer", + "format": "int64", "description": "认证源 ID 或名称", "name": "id", "in": "path", @@ -422,6 +424,7 @@ "parameters": [ { "type": "integer", + "format": "int64", "description": "认证源 ID 或名称", "name": "id", "in": "path", @@ -1332,6 +1335,7 @@ "parameters": [ { "type": "integer", + "format": "int64", "description": "通道ID", "name": "id", "in": "path", @@ -1385,6 +1389,7 @@ "parameters": [ { "type": "integer", + "format": "int64", "description": "通道ID", "name": "id", "in": "path", @@ -3337,6 +3342,7 @@ }, { "type": "integer", + "format": "int64", "description": "上传用户 ID", "name": "user_id", "in": "query" @@ -4096,6 +4102,7028 @@ } } }, + "/api/v1/custom/openflare/about": { + "get": { + "description": "返回 OpenFlare 关于页面文本,无需登录", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-option" + ], + "summary": "获取关于信息", + "responses": { + "200": { + "description": "关于信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/access-logs": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "分页返回 OpenFlare 访问日志,支持按节点、IP、主机与路径筛选,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-observability" + ], + "summary": "列出访问日志", + "parameters": [ + { + "type": "string", + "description": "节点 ID", + "name": "node_id", + "in": "query" + }, + { + "type": "string", + "description": "客户端 IP", + "name": "remote_addr", + "in": "query" + }, + { + "type": "string", + "description": "请求 Host", + "name": "host", + "in": "query" + }, + { + "type": "string", + "description": "请求路径", + "name": "path", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "p", + "in": "query" + }, + { + "type": "integer", + "description": "每页条数", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "排序字段", + "name": "sort_by", + "in": "query" + }, + { + "type": "string", + "description": "排序方向", + "name": "sort_order", + "in": "query" + } + ], + "responses": { + "200": { + "description": "访问日志列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/observability.AccessLogList" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/access-logs/cleanup": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按保留天数清理过期访问日志记录,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-observability" + ], + "summary": "清理访问日志", + "parameters": [ + { + "description": "清理参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/observability.AccessLogCleanupInput" + } + } + ], + "responses": { + "200": { + "description": "清理结果", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/observability.AccessLogCleanupResult" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/access-logs/folds": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按时间桶聚合访问日志并分页返回,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-observability" + ], + "summary": "列出折叠访问日志", + "parameters": [ + { + "type": "string", + "description": "节点 ID", + "name": "node_id", + "in": "query" + }, + { + "type": "string", + "description": "客户端 IP", + "name": "remote_addr", + "in": "query" + }, + { + "type": "string", + "description": "请求 Host", + "name": "host", + "in": "query" + }, + { + "type": "string", + "description": "请求路径", + "name": "path", + "in": "query" + }, + { + "type": "integer", + "description": "折叠时间窗口(分钟)", + "name": "fold_minutes", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "p", + "in": "query" + }, + { + "type": "integer", + "description": "每页条数", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "排序字段", + "name": "sort_by", + "in": "query" + }, + { + "type": "string", + "description": "排序方向", + "name": "sort_order", + "in": "query" + } + ], + "responses": { + "200": { + "description": "折叠访问日志列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/observability.FoldedAccessLogList" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/access-logs/folds/ip-summary": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "在指定时间桶内按 IP 聚合访问统计,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-observability" + ], + "summary": "列出折叠访问日志 IP 汇总", + "parameters": [ + { + "type": "string", + "description": "节点 ID", + "name": "node_id", + "in": "query" + }, + { + "type": "string", + "description": "客户端 IP", + "name": "remote_addr", + "in": "query" + }, + { + "type": "string", + "description": "请求 Host", + "name": "host", + "in": "query" + }, + { + "type": "string", + "description": "请求路径", + "name": "path", + "in": "query" + }, + { + "type": "string", + "description": "时间桶起始时间", + "name": "bucket_started_at", + "in": "query" + }, + { + "type": "integer", + "description": "折叠时间窗口(分钟)", + "name": "fold_minutes", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "p", + "in": "query" + }, + { + "type": "integer", + "description": "每页条数", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "排序字段", + "name": "sort_by", + "in": "query" + }, + { + "type": "string", + "description": "排序方向", + "name": "sort_order", + "in": "query" + } + ], + "responses": { + "200": { + "description": "折叠 IP 汇总列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/observability.FoldedAccessLogIPList" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/access-logs/ip-summary": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 IP 聚合访问日志统计并分页返回,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-observability" + ], + "summary": "列出访问日志 IP 汇总", + "parameters": [ + { + "type": "string", + "description": "节点 ID", + "name": "node_id", + "in": "query" + }, + { + "type": "string", + "description": "客户端 IP", + "name": "remote_addr", + "in": "query" + }, + { + "type": "string", + "description": "请求 Host", + "name": "host", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "p", + "in": "query" + }, + { + "type": "integer", + "description": "每页条数", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "排序字段", + "name": "sort_by", + "in": "query" + }, + { + "type": "string", + "description": "排序方向", + "name": "sort_order", + "in": "query" + } + ], + "responses": { + "200": { + "description": "IP 汇总列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/observability.AccessLogIPSummaryList" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/access-logs/ip-summary/trend": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回指定 IP 在时间范围内的访问趋势数据,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-observability" + ], + "summary": "获取访问日志 IP 趋势", + "parameters": [ + { + "type": "string", + "description": "节点 ID", + "name": "node_id", + "in": "query" + }, + { + "type": "string", + "description": "客户端 IP", + "name": "remote_addr", + "in": "query" + }, + { + "type": "string", + "description": "请求 Host", + "name": "host", + "in": "query" + }, + { + "type": "integer", + "description": "统计时间范围(小时)", + "name": "hours", + "in": "query" + }, + { + "type": "integer", + "description": "时间桶粒度(分钟)", + "name": "bucket_minutes", + "in": "query" + } + ], + "responses": { + "200": { + "description": "IP 访问趋势", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/observability.AccessLogIPTrendView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/acme-accounts/default": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回系统默认 ACME 账号配置,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "获取默认 ACME 账号", + "responses": { + "200": { + "description": "默认 ACME 账号", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.AcmeAccount" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/apply-logs": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "分页返回节点配置下发记录,支持按节点 ID 筛选,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-apply-log" + ], + "summary": "获取配置下发日志", + "parameters": [ + { + "type": "string", + "description": "节点 ID 筛选", + "name": "node_id", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "pageNo", + "in": "query" + }, + { + "type": "integer", + "description": "页码(别名)", + "name": "page_no", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "pageSize", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量(别名)", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "下发日志列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/apply_log.ListResult" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/apply-logs/cleanup": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按保留天数清理历史下发记录,或删除全部记录,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-apply-log" + ], + "summary": "清理配置下发日志", + "parameters": [ + { + "description": "清理参数", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/apply_log.CleanupInput" + } + } + ], + "responses": { + "200": { + "description": "清理结果", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/apply_log.CleanupResult" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/config-versions": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回所有已发布的 OpenResty 配置版本摘要,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-config-version" + ], + "summary": "获取配置版本列表", + "responses": { + "200": { + "description": "配置版本列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/model.ConfigVersionSummary" + } + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/config-versions/active": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回当前正在使用的配置版本,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-config-version" + ], + "summary": "获取当前活跃配置版本", + "responses": { + "200": { + "description": "活跃配置版本", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.ConfigVersion" + } + } + } + ] + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限、不存在或无活跃版本", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/config-versions/cleanup": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "删除超出保留数量的非活跃配置版本,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-config-version" + ], + "summary": "清理历史配置版本", + "parameters": [ + { + "description": "清理参数", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/config_version.CleanupInput" + } + } + ], + "responses": { + "200": { + "description": "清理结果", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/config_version.CleanupResult" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/config-versions/diff": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "对比当前草稿配置与活跃版本之间的差异,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-config-version" + ], + "summary": "对比草稿与活跃配置", + "responses": { + "200": { + "description": "配置差异", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/config_version.ConfigDiffResult" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/config-versions/preview": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "渲染并返回当前草稿配置的预览结果,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-config-version" + ], + "summary": "预览当前草稿配置", + "responses": { + "200": { + "description": "配置预览", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/config_version.ConfigPreviewResult" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/config-versions/publish": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "将当前草稿配置发布为新版本,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-config-version" + ], + "summary": "发布配置版本", + "parameters": [ + { + "type": "boolean", + "description": "是否强制发布", + "name": "force", + "in": "query" + } + ], + "responses": { + "200": { + "description": "发布成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.ConfigVersion" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/config-versions/{id}": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回指定配置版本的完整快照与渲染内容,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-config-version" + ], + "summary": "获取配置版本详情", + "parameters": [ + { + "type": "integer", + "description": "配置版本 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "配置版本详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.ConfigVersion" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或版本不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/config-versions/{id}/activate": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "将指定历史版本设为当前活跃配置,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-config-version" + ], + "summary": "激活配置版本", + "parameters": [ + { + "type": "integer", + "description": "配置版本 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "激活成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.ConfigVersion" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或版本不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/dashboard/overview": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "聚合节点与可观测性数据,返回 OpenFlare 控制台仪表盘概览,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-dashboard" + ], + "summary": "获取仪表盘概览", + "responses": { + "200": { + "description": "仪表盘概览", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dashboard.OverviewPayload" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/dns-accounts": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回全部 DNS 提供商账号,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "列出 DNS 账号", + "responses": { + "200": { + "description": "DNS 账号列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/model.DNSAccount" + } + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + }, + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "创建新的 DNS 提供商账号,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "创建 DNS 账号", + "parameters": [ + { + "description": "DNS 账号参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tls.DNSAccountInput" + } + } + ], + "responses": { + "200": { + "description": "创建成功的 DNS 账号", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.DNSAccount" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/dns-accounts/{id}/delete": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 删除 DNS 提供商账号,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "删除 DNS 账号", + "parameters": [ + { + "type": "integer", + "description": "DNS 账号 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/dns-accounts/{id}/update": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 更新 DNS 提供商账号,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "更新 DNS 账号", + "parameters": [ + { + "type": "integer", + "description": "DNS 账号 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "DNS 账号参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tls.DNSAccountInput" + } + } + ], + "responses": { + "200": { + "description": "更新后的 DNS 账号", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.DNSAccount" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/managed-domains": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回全部托管域名及关联证书,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "列出托管域名", + "responses": { + "200": { + "description": "托管域名列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/model.ManagedDomain" + } + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + }, + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "创建新的托管域名记录,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "创建托管域名", + "parameters": [ + { + "description": "托管域名参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tls.ManagedDomainInput" + } + } + ], + "responses": { + "200": { + "description": "创建成功的托管域名", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.ManagedDomain" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/managed-domains/match": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按域名查询可用的证书匹配候选,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "匹配托管域名证书", + "parameters": [ + { + "type": "string", + "description": "域名", + "name": "domain", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "证书匹配结果", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/tls.ManagedDomainMatchResult" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/managed-domains/{id}/delete": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 删除托管域名,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "删除托管域名", + "parameters": [ + { + "type": "integer", + "description": "托管域名 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/managed-domains/{id}/update": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 更新托管域名,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "更新托管域名", + "parameters": [ + { + "type": "integer", + "description": "托管域名 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "托管域名参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tls.ManagedDomainInput" + } + } + ], + "responses": { + "200": { + "description": "更新后的托管域名", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.ManagedDomain" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/nodes": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回所有节点及最新配置下发记录,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-node" + ], + "summary": "获取节点列表", + "responses": { + "200": { + "description": "节点列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/node.View" + } + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + }, + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "创建新的边缘节点记录,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-node" + ], + "summary": "创建节点", + "parameters": [ + { + "description": "节点参数", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/node.Input" + } + } + ], + "responses": { + "200": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/node.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/nodes/bootstrap-token": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回全局节点发现引导令牌,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-node" + ], + "summary": "获取引导令牌", + "responses": { + "200": { + "description": "引导令牌", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/node.BootstrapView" + } + } + } + ] + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/nodes/bootstrap-token/rotate": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "重新生成全局节点发现引导令牌,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-node" + ], + "summary": "轮换引导令牌", + "responses": { + "200": { + "description": "新引导令牌", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/node.BootstrapView" + } + } + } + ] + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/nodes/{id}/agent-release": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回指定节点可用的最新 Agent 版本信息,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-node" + ], + "summary": "获取 Agent 发布信息", + "parameters": [ + { + "type": "integer", + "description": "节点 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "发布渠道", + "name": "channel", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Agent 发布信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/node.AgentReleaseInfo" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或节点不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/nodes/{id}/agent-update": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "向指定节点下发 Agent 自更新指令,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-node" + ], + "summary": "请求 Agent 更新", + "parameters": [ + { + "type": "integer", + "description": "节点 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新参数(可选)", + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/node.AgentUpdateInput" + } + } + ], + "responses": { + "200": { + "description": "更新请求已下发", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/node.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或节点不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/nodes/{id}/delete": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "删除指定节点记录,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-node" + ], + "summary": "删除节点", + "parameters": [ + { + "type": "integer", + "description": "节点 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或节点不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/nodes/{id}/force-sync": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "向指定节点下发强制同步当前活跃配置的指令,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-node" + ], + "summary": "请求强制同步配置", + "parameters": [ + { + "type": "integer", + "description": "节点 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "同步请求已下发", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/node.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或节点不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/nodes/{id}/observability": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回指定节点的指标、健康事件与流量分析数据,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-node" + ], + "summary": "获取节点可观测性数据", + "parameters": [ + { + "type": "integer", + "description": "节点 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "统计时间范围(小时)", + "name": "hours", + "in": "query" + }, + { + "type": "integer", + "description": "返回记录数量上限", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "可观测性数据", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/node.ObservabilityView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或节点不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/nodes/{id}/observability/cleanup": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "清理指定节点的历史健康事件记录,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-node" + ], + "summary": "清理节点健康事件", + "parameters": [ + { + "type": "integer", + "description": "节点 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "清理结果", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/node.HealthEventCleanupResult" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或节点不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/nodes/{id}/openresty-restart": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "向指定节点下发 OpenResty 重启指令,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-node" + ], + "summary": "请求重启 OpenResty", + "parameters": [ + { + "type": "integer", + "description": "节点 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "重启请求已下发", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/node.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或节点不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/nodes/{id}/update": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "更新指定节点的配置信息,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-node" + ], + "summary": "更新节点", + "parameters": [ + { + "type": "integer", + "description": "节点 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "节点参数", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/node.Input" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/node.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或节点不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/notice": { + "get": { + "description": "返回 OpenFlare 控制台公告文本,无需登录", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-option" + ], + "summary": "获取系统公告", + "responses": { + "200": { + "description": "系统公告", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/option": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回全部非敏感 OpenFlare 配置项,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-option" + ], + "summary": "列出 OpenFlare 配置项", + "responses": { + "200": { + "description": "配置项列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/model.OpenFlareOption" + } + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/option/database/cleanup": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按目标与保留天数清理可观测性相关数据表,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-option" + ], + "summary": "清理可观测性数据库", + "parameters": [ + { + "description": "清理参数", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/option.databaseCleanupInput" + } + } + ], + "responses": { + "200": { + "description": "清理结果", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/option.databaseCleanupResult" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/option/geoip/lookup": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按提供商与 IP 查询地理位置信息,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-option" + ], + "summary": "GeoIP 地址查询", + "parameters": [ + { + "description": "查询参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/option.geoIPLookupRequest" + } + } + ], + "responses": { + "200": { + "description": "GeoIP 查询结果", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/option.geoIPLookupView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/option/update": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "更新单个 OpenFlare 配置项,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-option" + ], + "summary": "更新 OpenFlare 配置项", + "parameters": [ + { + "description": "配置项", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/model.OpenFlareOption" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/option/update-batch": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "批量更新多个 OpenFlare 配置项,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-option" + ], + "summary": "批量更新 OpenFlare 配置项", + "parameters": [ + { + "description": "批量配置项", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/option.optionBatchPayload" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/origins": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回所有源站及关联代理规则数量,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-origin" + ], + "summary": "获取源站列表", + "responses": { + "200": { + "description": "源站列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/origin.View" + } + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + }, + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "创建新的上游源站记录,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-origin" + ], + "summary": "创建源站", + "parameters": [ + { + "description": "源站参数", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/origin.Input" + } + } + ], + "responses": { + "200": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/origin.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/origins/{id}": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回指定源站信息及关联代理规则摘要,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-origin" + ], + "summary": "获取源站详情", + "parameters": [ + { + "type": "integer", + "description": "源站 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "源站详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/origin.DetailView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或源站不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/origins/{id}/delete": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "删除指定源站记录,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-origin" + ], + "summary": "删除源站", + "parameters": [ + { + "type": "integer", + "description": "源站 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或源站不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/origins/{id}/update": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "更新指定源站的配置信息,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-origin" + ], + "summary": "更新源站", + "parameters": [ + { + "type": "integer", + "description": "源站 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "源站参数", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/origin.Input" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/origin.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或源站不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/pages": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回全部 OpenFlare Pages 项目,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-pages" + ], + "summary": "列出 Pages 项目", + "responses": { + "200": { + "description": "Pages 项目列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/pages.View" + } + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + }, + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "创建新的 OpenFlare Pages 项目,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-pages" + ], + "summary": "创建 Pages 项目", + "parameters": [ + { + "description": "项目参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/pages.Input" + } + } + ], + "responses": { + "200": { + "description": "创建成功的项目", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/pages.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/pages/deployments/{deployment_id}/files": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回指定部署包含的文件清单,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-pages" + ], + "summary": "列出 Pages 部署文件", + "parameters": [ + { + "type": "integer", + "description": "部署 ID", + "name": "deployment_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "部署文件列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/pages.DeploymentFileView" + } + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "部署不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/pages/{id}": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 返回 Pages 项目详情,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-pages" + ], + "summary": "获取 Pages 项目详情", + "parameters": [ + { + "type": "integer", + "description": "项目 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Pages 项目详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/pages.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "项目不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/pages/{id}/delete": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 删除 OpenFlare Pages 项目,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-pages" + ], + "summary": "删除 Pages 项目", + "parameters": [ + { + "type": "integer", + "description": "项目 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "项目不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/pages/{id}/deployments": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回指定项目的全部部署记录,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-pages" + ], + "summary": "列出 Pages 部署", + "parameters": [ + { + "type": "integer", + "description": "项目 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "部署列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/pages.DeploymentView" + } + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "项目不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/pages/{id}/deployments/upload": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "为指定项目上传 ZIP 部署包,需要管理员权限", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-pages" + ], + "summary": "上传 Pages 部署包", + "parameters": [ + { + "type": "integer", + "description": "项目 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "file", + "description": "部署包 ZIP 文件", + "name": "package", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "部署记录", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/pages.DeploymentView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "项目不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/pages/{id}/deployments/{deployment_id}/activate": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "将指定部署设为项目当前生效版本,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-pages" + ], + "summary": "激活 Pages 部署", + "parameters": [ + { + "type": "integer", + "description": "项目 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "部署 ID", + "name": "deployment_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "激活后的项目", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/pages.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "项目或部署不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/pages/{id}/deployments/{deployment_id}/delete": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "删除指定项目的部署记录,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-pages" + ], + "summary": "删除 Pages 部署", + "parameters": [ + { + "type": "integer", + "description": "项目 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "部署 ID", + "name": "deployment_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "项目或部署不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/pages/{id}/update": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 更新 OpenFlare Pages 项目,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-pages" + ], + "summary": "更新 Pages 项目", + "parameters": [ + { + "type": "integer", + "description": "项目 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "项目参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/pages.Input" + } + } + ], + "responses": { + "200": { + "description": "更新后的项目", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/pages.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "项目不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/proxy-routes": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回所有代理规则配置,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-proxy-route" + ], + "summary": "获取代理规则列表", + "responses": { + "200": { + "description": "代理规则列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/proxy_route.View" + } + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + }, + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "创建新的反向代理规则,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-proxy-route" + ], + "summary": "创建代理规则", + "parameters": [ + { + "description": "代理规则参数", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/proxy_route.Input" + } + } + ], + "responses": { + "200": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/proxy_route.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/proxy-routes/{id}": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回指定代理规则的完整配置,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-proxy-route" + ], + "summary": "获取代理规则详情", + "parameters": [ + { + "type": "integer", + "description": "代理规则 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "代理规则详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/proxy_route.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或规则不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/proxy-routes/{id}/delete": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "删除指定代理规则,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-proxy-route" + ], + "summary": "删除代理规则", + "parameters": [ + { + "type": "integer", + "description": "代理规则 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或规则不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/proxy-routes/{id}/update": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "更新指定代理规则的配置,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-proxy-route" + ], + "summary": "更新代理规则", + "parameters": [ + { + "type": "integer", + "description": "代理规则 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "代理规则参数", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/proxy_route.Input" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/proxy_route.View" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "无权限或规则不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/status": { + "get": { + "description": "返回版本、认证源与系统公开配置,无需登录", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-option" + ], + "summary": "获取 OpenFlare 公开状态", + "responses": { + "200": { + "description": "公开状态", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/option.statusView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/tls-certificates": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回全部 TLS 证书(不含 PEM),需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "列出 TLS 证书", + "responses": { + "200": { + "description": "证书列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/model.TLSCertificate" + } + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + }, + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "从 PEM 文本创建 TLS 证书,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "创建 TLS 证书", + "parameters": [ + { + "description": "证书参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tls.CertificateInput" + } + } + ], + "responses": { + "200": { + "description": "创建成功的证书", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.TLSCertificate" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/tls-certificates/apply": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "通过 ACME 申请新的 TLS 证书,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "申请 ACME 证书", + "parameters": [ + { + "description": "ACME 申请参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tls.ApplyInput" + } + } + ], + "responses": { + "200": { + "description": "申请中的证书", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.TLSCertificate" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/tls-certificates/import-file": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "上传证书与私钥文件创建 TLS 证书,需要管理员权限", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "从文件导入 TLS 证书", + "parameters": [ + { + "type": "string", + "description": "证书名称", + "name": "name", + "in": "formData" + }, + { + "type": "string", + "description": "备注", + "name": "remark", + "in": "formData" + }, + { + "type": "file", + "description": "证书文件", + "name": "cert_file", + "in": "formData", + "required": true + }, + { + "type": "file", + "description": "私钥文件", + "name": "key_file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "导入成功的证书", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.TLSCertificate" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/tls-certificates/{id}": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 返回 TLS 证书详情(不含 PEM),需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "获取 TLS 证书详情", + "parameters": [ + { + "type": "integer", + "description": "证书 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "证书详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.TLSCertificate" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/tls-certificates/{id}/content": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 返回证书与私钥 PEM 内容,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "获取 TLS 证书 PEM 内容", + "parameters": [ + { + "type": "integer", + "description": "证书 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "证书 PEM 内容", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/tls.CertificateContent" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/tls-certificates/{id}/convert-acme": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "将已上传证书转换为 ACME 自动续期模式,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "将证书转为 ACME 管理", + "parameters": [ + { + "type": "integer", + "description": "证书 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "ACME 申请参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tls.ApplyInput" + } + } + ], + "responses": { + "200": { + "description": "转换后的证书", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.TLSCertificate" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/tls-certificates/{id}/delete": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 删除 TLS 证书,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "删除 TLS 证书", + "parameters": [ + { + "type": "integer", + "description": "证书 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/tls-certificates/{id}/renew": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "手动触发 ACME 证书续期,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "续期 ACME 证书", + "parameters": [ + { + "type": "integer", + "description": "证书 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "续期后的证书", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.TLSCertificate" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/tls-certificates/{id}/update": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 更新 TLS 证书 PEM 信息,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "更新 TLS 证书", + "parameters": [ + { + "type": "integer", + "description": "证书 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "证书参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tls.CertificateInput" + } + } + ], + "responses": { + "200": { + "description": "更新后的证书", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.TLSCertificate" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/tls-certificates/{id}/update-acme": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 更新 ACME 证书申请配置,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-tls" + ], + "summary": "更新 ACME 证书配置", + "parameters": [ + { + "type": "integer", + "description": "证书 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "ACME 申请参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tls.ApplyInput" + } + } + ], + "responses": { + "200": { + "description": "更新后的证书", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.TLSCertificate" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/update/latest-release": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "查询 OpenFlare 服务端最新 GitHub Release 及升级状态,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-update" + ], + "summary": "获取最新服务端发布版本", + "parameters": [ + { + "type": "string", + "description": "发布渠道(stable/preview)", + "name": "channel", + "in": "query" + } + ], + "responses": { + "200": { + "description": "最新发布信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/update.LatestReleaseView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/update/logs/ws": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "通过 WebSocket 推送升级进度快照,需要管理员权限", + "tags": [ + "openflare-update" + ], + "summary": "流式获取服务端升级日志", + "responses": { + "101": { + "description": "WebSocket 升级日志流", + "schema": { + "$ref": "#/definitions/update.StreamSnapshot" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/update/manual-upgrade": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "确认并执行手动上传的服务端升级(当前功能已禁用),需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-update" + ], + "summary": "确认手动服务端升级", + "responses": { + "400": { + "description": "功能已禁用或参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/update/manual-upload": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "上传服务端二进制以进行手动升级(当前功能已禁用),需要管理员权限", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-update" + ], + "summary": "上传手动升级二进制", + "parameters": [ + { + "type": "file", + "description": "服务端二进制文件", + "name": "binary", + "in": "formData", + "required": true + } + ], + "responses": { + "400": { + "description": "功能已禁用或参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/update/upgrade": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "从最新 Release 调度 OpenFlare 服务端自动升级,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-update" + ], + "summary": "触发服务端自动升级", + "parameters": [ + { + "description": "升级参数", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/update.upgradeRequest" + } + } + ], + "responses": { + "200": { + "description": "升级任务已调度", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/update.LatestReleaseView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/uptimekuma/sync": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "将 OpenFlare 节点同步到 Uptime Kuma,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-option" + ], + "summary": "同步 Uptime Kuma", + "responses": { + "200": { + "description": "同步成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/waf/ip-groups": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回全部 WAF IP 组,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "列出 WAF IP 组", + "responses": { + "200": { + "description": "IP 组列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/waf.IPGroupView" + } + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + }, + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "创建新的 WAF IP 组,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "创建 WAF IP 组", + "parameters": [ + { + "description": "IP 组参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/waf.IPGroupInput" + } + } + ], + "responses": { + "200": { + "description": "创建成功的 IP 组", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/waf.IPGroupView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/waf/ip-groups/test": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "根据自动配置规则测试 IP 匹配结果(桩实现),需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "测试 WAF IP 组自动配置", + "parameters": [ + { + "description": "自动配置参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/waf.IPGroupAutoTestInput" + } + } + ], + "responses": { + "200": { + "description": "测试结果", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/waf.IPGroupAutoTestResult" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/waf/ip-groups/{id}": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 返回 WAF IP 组详情,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "获取 WAF IP 组详情", + "parameters": [ + { + "type": "integer", + "description": "IP 组 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "IP 组详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/waf.IPGroupView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/waf/ip-groups/{id}/delete": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 删除 WAF IP 组,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "删除 WAF IP 组", + "parameters": [ + { + "type": "integer", + "description": "IP 组 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/waf/ip-groups/{id}/sync": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "手动触发 WAF IP 组外部 IP 同步,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "同步 WAF IP 组", + "parameters": [ + { + "type": "integer", + "description": "IP 组 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "同步结果", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/waf.IPGroupSyncResult" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/waf/ip-groups/{id}/update": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 更新 WAF IP 组,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "更新 WAF IP 组", + "parameters": [ + { + "type": "integer", + "description": "IP 组 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "IP 组参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/waf.IPGroupInput" + } + } + ], + "responses": { + "200": { + "description": "更新后的 IP 组", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/waf.IPGroupView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/waf/rule-groups": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回全部 WAF 规则组,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "列出 WAF 规则组", + "responses": { + "200": { + "description": "规则组列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/waf.RuleGroupView" + } + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + }, + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "创建新的 WAF 规则组,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "创建 WAF 规则组", + "parameters": [ + { + "description": "规则组参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/waf.RuleGroupInput" + } + } + ], + "responses": { + "200": { + "description": "创建成功的规则组", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/waf.RuleGroupView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/waf/rule-groups/{id}": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 返回 WAF 规则组详情,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "获取 WAF 规则组详情", + "parameters": [ + { + "type": "integer", + "description": "规则组 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "规则组详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/waf.RuleGroupView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/waf/rule-groups/{id}/delete": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 删除 WAF 规则组,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "删除 WAF 规则组", + "parameters": [ + { + "type": "integer", + "description": "规则组 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/waf/rule-groups/{id}/sites": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "替换 WAF 规则组关联的代理站点列表,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "替换规则组站点绑定", + "parameters": [ + { + "type": "integer", + "description": "规则组 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "站点 ID 列表", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/waf.IDsRequest" + } + } + ], + "responses": { + "200": { + "description": "更新后的规则组", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/waf.RuleGroupView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/waf/rule-groups/{id}/update": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "按 ID 更新 WAF 规则组,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "更新 WAF 规则组", + "parameters": [ + { + "type": "integer", + "description": "规则组 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "规则组参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/waf.RuleGroupInput" + } + } + ], + "responses": { + "200": { + "description": "更新后的规则组", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/waf.RuleGroupView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/custom/openflare/waf/sites/{route_id}/rule-groups": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回代理站点关联的 WAF 规则组绑定,需要管理员权限", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "获取站点 WAF 规则组", + "parameters": [ + { + "type": "integer", + "description": "代理路由 ID", + "name": "route_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "站点规则组绑定", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/waf.SiteRuleGroupsView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + }, + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "替换代理站点关联的 WAF 规则组列表,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-waf" + ], + "summary": "替换站点 WAF 规则组", + "parameters": [ + { + "type": "integer", + "description": "代理路由 ID", + "name": "route_id", + "in": "path", + "required": true + }, + { + "description": "规则组 ID 列表", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/waf.IDsRequest" + } + } + ], + "responses": { + "200": { + "description": "更新后的站点规则组绑定", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/waf.SiteRuleGroupsView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "记录不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, "/api/v1/oauth/callback": { "post": { "description": "接收前端传回的 state 和 code,完成 OAuth/OIDC 认证并建立会话。支持登录(login)和账号绑定(bind)两种场景。", @@ -4230,6 +11258,7 @@ "parameters": [ { "type": "integer", + "format": "int64", "description": "外部帐号绑定记录 ID", "name": "id", "in": "path", @@ -5457,6 +12486,54 @@ } }, "definitions": { + "apply_log.CleanupInput": { + "type": "object", + "properties": { + "delete_all": { + "type": "boolean" + }, + "retention_days": { + "type": "integer" + } + } + }, + "apply_log.CleanupResult": { + "type": "object", + "properties": { + "cutoff": { + "type": "string" + }, + "delete_all": { + "type": "boolean" + }, + "deleted_count": { + "type": "integer" + }, + "retention_days": { + "type": "integer" + } + } + }, + "apply_log.ListResult": { + "type": "object", + "properties": { + "current": { + "type": "integer" + }, + "rows": { + "type": "array", + "items": { + "$ref": "#/definitions/model.OpenFlareApplyLog" + } + }, + "total": { + "type": "integer" + }, + "totalPage": { + "type": "integer" + } + } + }, "auth_source.AuthSourceRequest": { "type": "object", "properties": { @@ -5572,6 +12649,297 @@ } } }, + "config_version.CleanupInput": { + "type": "object", + "properties": { + "keep_count": { + "type": "integer" + } + } + }, + "config_version.CleanupResult": { + "type": "object", + "properties": { + "deleted_count": { + "type": "integer" + }, + "message": { + "type": "string" + } + } + }, + "config_version.ConfigDiffResult": { + "type": "object", + "properties": { + "active_version": { + "type": "string" + }, + "active_website_count": { + "type": "integer" + }, + "added_domains": { + "type": "array", + "items": { + "type": "string" + } + }, + "added_sites": { + "type": "array", + "items": { + "type": "string" + } + }, + "changed_option_details": { + "type": "array", + "items": { + "$ref": "#/definitions/config_version.ConfigOptionDiffItem" + } + }, + "changed_option_keys": { + "type": "array", + "items": { + "type": "string" + } + }, + "current_website_count": { + "type": "integer" + }, + "main_config_changed": { + "type": "boolean" + }, + "modified_domains": { + "type": "array", + "items": { + "type": "string" + } + }, + "modified_sites": { + "type": "array", + "items": { + "type": "string" + } + }, + "removed_domains": { + "type": "array", + "items": { + "type": "string" + } + }, + "removed_sites": { + "type": "array", + "items": { + "type": "string" + } + }, + "waf_config_changed": { + "type": "boolean" + } + } + }, + "config_version.ConfigOptionDiffItem": { + "type": "object", + "properties": { + "current_value": { + "type": "string" + }, + "key": { + "type": "string" + }, + "previous_value": { + "type": "string" + } + } + }, + "config_version.ConfigPreviewResult": { + "type": "object", + "properties": { + "checksum": { + "type": "string" + }, + "main_config": { + "type": "string" + }, + "rendered_config": { + "type": "string" + }, + "route_config": { + "type": "string" + }, + "route_count": { + "type": "integer" + }, + "snapshot_json": { + "type": "string" + }, + "support_files": { + "type": "array", + "items": { + "$ref": "#/definitions/config_version.SupportFile" + } + }, + "website_count": { + "type": "integer" + } + } + }, + "config_version.SupportFile": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "path": { + "type": "string" + } + } + }, + "dashboard.Capacity": { + "type": "object", + "properties": { + "average_cpu_usage_percent": { + "type": "number" + }, + "average_memory_usage_percent": { + "type": "number" + }, + "high_cpu_nodes": { + "type": "integer" + }, + "high_memory_nodes": { + "type": "integer" + }, + "high_storage_nodes": { + "type": "integer" + } + } + }, + "dashboard.OverviewPayload": { + "type": "object", + "properties": { + "capacity": { + "$ref": "#/definitions/dashboard.Capacity" + }, + "distributions": { + "$ref": "#/definitions/dashboard.distributionsPayload" + }, + "generated_at": {}, + "nodes": { + "type": "array", + "items": { + "type": "array", + "items": {} + } + }, + "summary": { + "$ref": "#/definitions/dashboard.Summary" + }, + "traffic": { + "$ref": "#/definitions/dashboard.Traffic" + }, + "trends": { + "$ref": "#/definitions/dashboard.trendsPayload" + } + } + }, + "dashboard.Summary": { + "type": "object", + "properties": { + "offline_nodes": { + "type": "integer" + }, + "online_nodes": { + "type": "integer" + }, + "pending_nodes": { + "type": "integer" + }, + "total_nodes": { + "type": "integer" + }, + "unhealthy_nodes": { + "type": "integer" + } + } + }, + "dashboard.Traffic": { + "type": "object", + "properties": { + "error_count": { + "type": "integer" + }, + "estimated_qps": { + "type": "number" + }, + "reported_nodes": { + "type": "integer" + }, + "request_count": { + "type": "integer" + }, + "unique_visitors": { + "type": "integer" + } + } + }, + "dashboard.distributionsPayload": { + "type": "object", + "properties": { + "source_countries": { + "type": "array", + "items": { + "type": "array", + "items": {} + } + }, + "status_codes": { + "type": "array", + "items": { + "type": "array", + "items": {} + } + }, + "top_domains": { + "type": "array", + "items": { + "type": "array", + "items": {} + } + } + } + }, + "dashboard.trendsPayload": { + "type": "object", + "properties": { + "capacity_24h": { + "type": "array", + "items": { + "type": "array", + "items": {} + } + }, + "disk_io_24h": { + "type": "array", + "items": { + "type": "array", + "items": {} + } + }, + "network_24h": { + "type": "array", + "items": { + "type": "array", + "items": {} + } + }, + "traffic_24h": { + "type": "array", + "items": { + "type": "array", + "items": {} + } + } + } + }, "db_manage.DBOverviewResponse": { "type": "object", "properties": { @@ -5979,6 +13347,26 @@ } } }, + "model.AcmeAccount": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, "model.AuthSource": { "type": "object", "properties": { @@ -6020,6 +13408,84 @@ } } }, + "model.ConfigVersion": { + "type": "object", + "properties": { + "checksum": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "created_by": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + }, + "main_config": { + "type": "string" + }, + "rendered_config": { + "type": "string" + }, + "snapshot_json": { + "type": "string" + }, + "support_files_json": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "model.ConfigVersionSummary": { + "type": "object", + "properties": { + "checksum": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "created_by": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + }, + "version": { + "type": "string" + } + } + }, + "model.DNSAccount": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "model.ExternalAccountView": { "type": "object", "properties": { @@ -6049,6 +13515,254 @@ } } }, + "model.ManagedDomain": { + "type": "object", + "properties": { + "cert_id": { + "type": "integer" + }, + "created_at": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "remark": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "model.OpenFlareApplyLog": { + "type": "object", + "properties": { + "checksum": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "main_config_checksum": { + "type": "string" + }, + "message": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "result": { + "type": "string" + }, + "route_config_checksum": { + "type": "string" + }, + "support_file_count": { + "type": "integer" + }, + "version": { + "type": "string" + } + } + }, + "model.OpenFlareHealthEvent": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "event_type": { + "type": "string" + }, + "first_triggered_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_triggered_at": { + "type": "string" + }, + "message": { + "type": "string" + }, + "metadata_json": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "reported_at": { + "type": "string" + }, + "resolved_at": { + "type": "string" + }, + "severity": { + "type": "string" + }, + "status": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "model.OpenFlareMetricSnapshot": { + "type": "object", + "properties": { + "captured_at": { + "type": "string" + }, + "cpu_usage_percent": { + "type": "number" + }, + "created_at": { + "type": "string" + }, + "disk_read_bytes": { + "type": "integer" + }, + "disk_write_bytes": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "memory_total_bytes": { + "type": "integer" + }, + "memory_used_bytes": { + "type": "integer" + }, + "network_rx_bytes": { + "type": "integer" + }, + "network_tx_bytes": { + "type": "integer" + }, + "node_id": { + "type": "string" + }, + "storage_total_bytes": { + "type": "integer" + }, + "storage_used_bytes": { + "type": "integer" + } + } + }, + "model.OpenFlareNodeSystemProfile": { + "type": "object", + "properties": { + "architecture": { + "type": "string" + }, + "cpu_cores": { + "type": "integer" + }, + "cpu_model": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "hostname": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "kernel_version": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "os_name": { + "type": "string" + }, + "os_version": { + "type": "string" + }, + "reported_at": { + "type": "string" + }, + "total_disk_bytes": { + "type": "integer" + }, + "total_memory_bytes": { + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "uptime_seconds": { + "type": "integer" + } + } + }, + "model.OpenFlareOption": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "model.OpenFlareRequestReport": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "error_count": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "node_id": { + "type": "string" + }, + "request_count": { + "type": "integer" + }, + "source_countries_json": { + "type": "string" + }, + "status_codes_json": { + "type": "string" + }, + "top_domains_json": { + "type": "string" + }, + "unique_visitor_count": { + "type": "integer" + }, + "window_ended_at": { + "type": "string" + }, + "window_started_at": { + "type": "string" + } + } + }, "model.PushChannel": { "type": "object", "properties": { @@ -6231,6 +13945,71 @@ } } }, + "model.TLSCertificate": { + "type": "object", + "properties": { + "acme_account_id": { + "type": "integer" + }, + "apply_message": { + "type": "string" + }, + "apply_status": { + "type": "string" + }, + "auto_renew": { + "type": "boolean" + }, + "created_at": { + "type": "string" + }, + "disable_cname": { + "type": "boolean" + }, + "dns1": { + "type": "string" + }, + "dns2": { + "type": "string" + }, + "dns_account_id": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "key_algorithm": { + "type": "string" + }, + "name": { + "type": "string" + }, + "not_after": { + "type": "string" + }, + "not_before": { + "type": "string" + }, + "other_domains": { + "type": "string" + }, + "primary_domain": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "skip_dns": { + "type": "boolean" + }, + "updated_at": { + "type": "string" + } + } + }, "model.TaskExecution": { "type": "object", "properties": { @@ -6457,12 +14236,296 @@ "UploadStatusPending": "待使用", "UploadStatusUsed": "已使用" }, + "x-enum-descriptions": [ + "待使用", + "已使用", + "已删除" + ], "x-enum-varnames": [ "UploadStatusPending", "UploadStatusUsed", "UploadStatusDeleted" ] }, + "node.AgentReleaseInfo": { + "type": "object", + "properties": { + "body": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "current_version": { + "type": "string" + }, + "has_update": { + "type": "boolean" + }, + "html_url": { + "type": "string" + }, + "prerelease": { + "type": "boolean" + }, + "published_at": { + "type": "string" + }, + "requested_channel": { + "type": "string" + }, + "requested_tag": { + "type": "string" + }, + "tag_name": { + "type": "string" + }, + "update_requested": { + "type": "boolean" + } + } + }, + "node.AgentUpdateInput": { + "type": "object", + "properties": { + "channel": { + "type": "string" + }, + "tag_name": { + "type": "string" + } + } + }, + "node.BootstrapView": { + "type": "object", + "properties": { + "discovery_token": { + "type": "string" + } + } + }, + "node.HealthEventCleanupResult": { + "type": "object", + "properties": { + "deleted_count": { + "type": "integer" + }, + "node_id": { + "type": "string" + } + } + }, + "node.Input": { + "type": "object", + "properties": { + "auto_update_enabled": { + "type": "boolean" + }, + "geo_latitude": { + "type": "number" + }, + "geo_longitude": { + "type": "number" + }, + "geo_manual_override": { + "type": "boolean" + }, + "geo_name": { + "type": "string" + }, + "ip": { + "type": "string" + }, + "ip_manual_override": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "node_type": { + "type": "string" + }, + "relay_agent_access_addr": { + "type": "string" + }, + "relay_bind_port": { + "type": "integer" + }, + "relay_client_access_addr": { + "type": "string" + }, + "relay_client_proxy_url": { + "type": "string" + }, + "relay_vhost_http_port": { + "type": "integer" + }, + "relay_web_server_enabled": { + "type": "boolean" + } + } + }, + "node.ObservabilityView": { + "type": "object", + "properties": { + "analytics": { + "$ref": "#/definitions/observability.NodeAnalytics" + }, + "health_events": { + "type": "array", + "items": { + "$ref": "#/definitions/model.OpenFlareHealthEvent" + } + }, + "metric_snapshots": { + "type": "array", + "items": { + "$ref": "#/definitions/model.OpenFlareMetricSnapshot" + } + }, + "node_id": { + "type": "string" + }, + "profile": { + "$ref": "#/definitions/model.OpenFlareNodeSystemProfile" + }, + "relay_dashboard": { + "$ref": "#/definitions/observability.RelayDashboardSnapshot" + }, + "traffic_reports": { + "type": "array", + "items": { + "$ref": "#/definitions/model.OpenFlareRequestReport" + } + }, + "trends": { + "$ref": "#/definitions/observability.NodeTrends" + } + } + }, + "node.View": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "auto_update_enabled": { + "type": "boolean" + }, + "created_at": { + "type": "string" + }, + "current_version": { + "type": "string" + }, + "ext_version": { + "type": "string" + }, + "geo_latitude": { + "type": "number" + }, + "geo_longitude": { + "type": "number" + }, + "geo_manual_override": { + "type": "boolean" + }, + "geo_name": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "ip": { + "type": "string" + }, + "ip_manual_override": { + "type": "boolean" + }, + "last_error": { + "type": "string" + }, + "last_seen_at": {}, + "latest_apply_at": { + "type": "string" + }, + "latest_apply_checksum": { + "type": "string" + }, + "latest_apply_message": { + "type": "string" + }, + "latest_apply_result": { + "type": "string" + }, + "latest_main_config_checksum": { + "type": "string" + }, + "latest_route_config_checksum": { + "type": "string" + }, + "latest_support_file_count": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "node_type": { + "type": "string" + }, + "openresty_message": { + "type": "string" + }, + "openresty_status": { + "type": "string" + }, + "relay_agent_access_addr": { + "type": "string" + }, + "relay_bind_port": { + "type": "integer" + }, + "relay_client_access_addr": { + "type": "string" + }, + "relay_client_proxy_url": { + "type": "string" + }, + "relay_status": { + "type": "string" + }, + "relay_vhost_http_port": { + "type": "integer" + }, + "relay_web_server_enabled": { + "type": "boolean" + }, + "restart_openresty_requested": { + "type": "boolean" + }, + "status": { + "type": "string" + }, + "update_channel": { + "type": "string" + }, + "update_requested": { + "type": "boolean" + }, + "update_tag": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, "oauth.AuthSourceView": { "type": "object", "properties": { @@ -6564,6 +14627,1209 @@ } } }, + "observability.AccessLogCleanupInput": { + "type": "object", + "properties": { + "retention_days": { + "type": "integer" + } + } + }, + "observability.AccessLogCleanupResult": { + "type": "object", + "properties": { + "cutoff": { + "type": "string" + }, + "deleted_count": { + "type": "integer" + }, + "retention_days": { + "type": "integer" + } + } + }, + "observability.AccessLogIPSummaryList": { + "type": "object", + "properties": { + "has_more": { + "type": "boolean" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.AccessLogIPSummaryView" + } + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "sort_by": { + "type": "string" + }, + "sort_order": { + "type": "string" + }, + "total_ip": { + "type": "integer" + } + } + }, + "observability.AccessLogIPSummaryView": { + "type": "object", + "properties": { + "last_seen_at": { + "type": "string" + }, + "recent_requests": { + "type": "integer" + }, + "remote_addr": { + "type": "string" + }, + "total_requests": { + "type": "integer" + } + } + }, + "observability.AccessLogIPTrendPoint": { + "type": "object", + "properties": { + "bucket_started_at": { + "type": "string" + }, + "request_count": { + "type": "integer" + } + } + }, + "observability.AccessLogIPTrendView": { + "type": "object", + "properties": { + "bucket_minutes": { + "type": "integer" + }, + "hours": { + "type": "integer" + }, + "points": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.AccessLogIPTrendPoint" + } + }, + "remote_addr": { + "type": "string" + } + } + }, + "observability.AccessLogList": { + "type": "object", + "properties": { + "has_more": { + "type": "boolean" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.AccessLogView" + } + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total_ip": { + "type": "integer" + }, + "total_record": { + "type": "integer" + } + } + }, + "observability.AccessLogView": { + "type": "object", + "properties": { + "host": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "logged_at": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "node_name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "region": { + "type": "string" + }, + "remote_addr": { + "type": "string" + }, + "status_code": { + "type": "integer" + } + } + }, + "observability.CapacityTrendPoint": { + "type": "object", + "properties": { + "average_cpu_usage_percent": { + "type": "number" + }, + "average_memory_usage_percent": { + "type": "number" + }, + "bucket_started_at": { + "type": "string" + }, + "reported_nodes": { + "type": "integer" + } + } + }, + "observability.DiskIOTrendPoint": { + "type": "object", + "properties": { + "bucket_started_at": { + "type": "string" + }, + "disk_read_bytes": { + "type": "integer" + }, + "disk_write_bytes": { + "type": "integer" + }, + "reported_nodes": { + "type": "integer" + } + } + }, + "observability.DistributionItem": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "integer" + } + } + }, + "observability.FoldedAccessLogIPList": { + "type": "object", + "properties": { + "bucket_started_at": { + "type": "string" + }, + "fold_minutes": { + "type": "integer" + }, + "has_more": { + "type": "boolean" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.FoldedAccessLogIPView" + } + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "sort_by": { + "type": "string" + }, + "sort_order": { + "type": "string" + }, + "total_ip": { + "type": "integer" + } + } + }, + "observability.FoldedAccessLogIPView": { + "type": "object", + "properties": { + "client_error_count": { + "type": "integer" + }, + "last_seen_at": { + "type": "string" + }, + "remote_addr": { + "type": "string" + }, + "request_count": { + "type": "integer" + }, + "server_error_count": { + "type": "integer" + }, + "success_count": { + "type": "integer" + } + } + }, + "observability.FoldedAccessLogList": { + "type": "object", + "properties": { + "fold_minutes": { + "type": "integer" + }, + "has_more": { + "type": "boolean" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.FoldedAccessLogView" + } + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total_bucket": { + "type": "integer" + }, + "total_ip": { + "type": "integer" + }, + "total_record": { + "type": "integer" + } + } + }, + "observability.FoldedAccessLogView": { + "type": "object", + "properties": { + "bucket_started_at": { + "type": "string" + }, + "client_error_count": { + "type": "integer" + }, + "request_count": { + "type": "integer" + }, + "server_error_count": { + "type": "integer" + }, + "success_count": { + "type": "integer" + }, + "unique_host_count": { + "type": "integer" + }, + "unique_ip_count": { + "type": "integer" + } + } + }, + "observability.HealthSummary": { + "type": "object", + "properties": { + "active_alerts": { + "type": "integer" + }, + "critical_alerts": { + "type": "integer" + }, + "has_capacity_risk": { + "type": "boolean" + }, + "has_runtime_risk": { + "type": "boolean" + }, + "has_traffic_risk": { + "type": "boolean" + }, + "info_alerts": { + "type": "integer" + }, + "resolved_alerts": { + "type": "integer" + }, + "warning_alerts": { + "type": "integer" + } + } + }, + "observability.NetworkTrendPoint": { + "type": "object", + "properties": { + "bucket_started_at": { + "type": "string" + }, + "network_rx_bytes": { + "type": "integer" + }, + "network_tx_bytes": { + "type": "integer" + }, + "openresty_rx_bytes": { + "type": "integer" + }, + "openresty_tx_bytes": { + "type": "integer" + }, + "reported_nodes": { + "type": "integer" + } + } + }, + "observability.NodeAnalytics": { + "type": "object", + "properties": { + "distributions": { + "$ref": "#/definitions/observability.TrafficDistributions" + }, + "health": { + "$ref": "#/definitions/observability.HealthSummary" + }, + "traffic": { + "$ref": "#/definitions/observability.TrafficWindowSummary" + } + } + }, + "observability.NodeTrends": { + "type": "object", + "properties": { + "capacity_24h": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.CapacityTrendPoint" + } + }, + "disk_io_24h": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.DiskIOTrendPoint" + } + }, + "network_24h": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.NetworkTrendPoint" + } + }, + "traffic_24h": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.TrafficTrendPoint" + } + } + } + }, + "observability.RelayDashboardSnapshot": { + "type": "object", + "properties": { + "client_counts": { + "type": "integer" + }, + "offline_proxies": { + "type": "integer" + }, + "online_proxies": { + "type": "integer" + }, + "proxies": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.RelayProxyStat" + } + }, + "total_connections": { + "type": "integer" + }, + "total_proxies": { + "type": "integer" + } + } + }, + "observability.RelayProxyStat": { + "type": "object", + "properties": { + "client_addr": { + "type": "string" + }, + "client_version": { + "type": "string" + }, + "last_close_time": { + "type": "string" + }, + "last_start_time": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "observability.TrafficDistributions": { + "type": "object", + "properties": { + "source_countries": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.DistributionItem" + } + }, + "status_codes": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.DistributionItem" + } + }, + "top_domains": { + "type": "array", + "items": { + "$ref": "#/definitions/observability.DistributionItem" + } + } + } + }, + "observability.TrafficTrendPoint": { + "type": "object", + "properties": { + "bucket_started_at": { + "type": "string" + }, + "error_count": { + "type": "integer" + }, + "request_count": { + "type": "integer" + }, + "unique_visitor_count": { + "type": "integer" + } + } + }, + "observability.TrafficWindowSummary": { + "type": "object", + "properties": { + "error_count": { + "type": "integer" + }, + "error_rate_percent": { + "type": "number" + }, + "estimated_qps": { + "type": "number" + }, + "request_count": { + "type": "integer" + }, + "unique_visitor_count": { + "type": "integer" + }, + "window_ended_at": { + "type": "string" + }, + "window_started_at": { + "type": "string" + } + } + }, + "option.databaseCleanupInput": { + "type": "object", + "properties": { + "retention_days": { + "type": "integer" + }, + "target": { + "type": "string" + } + } + }, + "option.databaseCleanupResult": { + "type": "object", + "properties": { + "delete_all": { + "type": "boolean" + }, + "deleted_count": { + "type": "integer" + }, + "retention_days": { + "type": "integer" + }, + "target": { + "type": "string" + }, + "target_label": { + "type": "string" + } + } + }, + "option.geoIPLookupRequest": { + "type": "object", + "properties": { + "ip": { + "type": "string" + }, + "provider": { + "type": "string" + } + } + }, + "option.geoIPLookupView": { + "type": "object", + "properties": { + "ip": { + "type": "string" + }, + "iso_code": { + "type": "string" + }, + "latitude": { + "type": "number" + }, + "longitude": { + "type": "number" + }, + "name": { + "type": "string" + }, + "provider": { + "type": "string" + } + } + }, + "option.optionBatchPayload": { + "type": "object", + "properties": { + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/model.OpenFlareOption" + } + } + } + }, + "option.publicAuthSourceView": { + "type": "object", + "properties": { + "authorize_url": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "option.statusView": { + "type": "object", + "properties": { + "auth_sources": { + "type": "array", + "items": { + "$ref": "#/definitions/option.publicAuthSourceView" + } + }, + "cap_login_enabled": { + "type": "boolean" + }, + "email_verification": { + "type": "boolean" + }, + "footer_html": { + "type": "string" + }, + "github_client_id": { + "type": "string" + }, + "github_oauth": { + "type": "boolean" + }, + "home_page_link": { + "type": "string" + }, + "password_register_enabled": { + "type": "boolean" + }, + "server_address": { + "type": "string" + }, + "start_time": { + "type": "integer" + }, + "system_name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "wechat_login": { + "type": "boolean" + }, + "wechat_qrcode": { + "type": "string" + } + } + }, + "origin.DetailView": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "route_count": { + "type": "integer" + }, + "routes": { + "type": "array", + "items": { + "$ref": "#/definitions/origin.RouteSummary" + } + }, + "updated_at": { + "type": "string" + } + } + }, + "origin.Input": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "name": { + "type": "string" + }, + "remark": { + "type": "string" + } + } + }, + "origin.RouteSummary": { + "type": "object", + "properties": { + "domain": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "origin_url": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "origin.View": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "route_count": { + "type": "integer" + }, + "updated_at": { + "type": "string" + } + } + }, + "pages.DeploymentFileView": { + "type": "object", + "properties": { + "checksum": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "deployment_id": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "path": { + "type": "string" + }, + "size": { + "type": "integer" + } + } + }, + "pages.DeploymentView": { + "type": "object", + "properties": { + "activated_at": { + "type": "string" + }, + "checksum": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "created_by": { + "type": "string" + }, + "deployment_number": { + "type": "integer" + }, + "file_count": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "project_id": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "total_size": { + "type": "integer" + } + } + }, + "pages.Input": { + "type": "object", + "properties": { + "api_proxy_enabled": { + "type": "boolean" + }, + "api_proxy_pass": { + "type": "string" + }, + "api_proxy_path": { + "type": "string" + }, + "api_proxy_rewrite": { + "type": "string" + }, + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "entry_file": { + "type": "string" + }, + "name": { + "type": "string" + }, + "root_dir": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "spa_fallback_enabled": { + "type": "boolean" + }, + "spa_fallback_path": { + "type": "string" + } + } + }, + "pages.View": { + "type": "object", + "properties": { + "active_deployment": { + "$ref": "#/definitions/pages.DeploymentView" + }, + "active_deployment_id": { + "type": "integer" + }, + "api_proxy_enabled": { + "type": "boolean" + }, + "api_proxy_pass": { + "type": "string" + }, + "api_proxy_path": { + "type": "string" + }, + "api_proxy_rewrite": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "deployment_count": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "entry_file": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "root_dir": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "spa_fallback_enabled": { + "type": "boolean" + }, + "spa_fallback_path": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "proxy_route.CustomHeaderInput": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "proxy_route.Input": { + "type": "object", + "properties": { + "basic_auth_enabled": { + "type": "boolean" + }, + "basic_auth_password": { + "type": "string" + }, + "basic_auth_username": { + "type": "string" + }, + "cache_enabled": { + "type": "boolean" + }, + "cache_policy": { + "type": "string" + }, + "cache_rules": { + "type": "array", + "items": { + "type": "string" + } + }, + "cert_id": { + "type": "integer" + }, + "cert_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "custom_headers": { + "type": "array", + "items": { + "$ref": "#/definitions/proxy_route.CustomHeaderInput" + } + }, + "domain": { + "type": "string" + }, + "domain_cert_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "domains": { + "type": "array", + "items": { + "type": "string" + } + }, + "enable_https": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "limit_conn_per_ip": { + "type": "integer" + }, + "limit_conn_per_server": { + "type": "integer" + }, + "limit_rate": { + "type": "string" + }, + "origin_address": { + "type": "string" + }, + "origin_host": { + "type": "string" + }, + "origin_id": { + "type": "integer" + }, + "origin_port": { + "type": "string" + }, + "origin_scheme": { + "type": "string" + }, + "origin_uri": { + "type": "string" + }, + "origin_url": { + "type": "string" + }, + "pages_project_id": { + "type": "integer" + }, + "redirect_http": { + "type": "boolean" + }, + "remark": { + "type": "string" + }, + "site_name": { + "type": "string" + }, + "tunnel_id": { + "type": "integer" + }, + "tunnel_node_id": { + "type": "integer" + }, + "tunnel_target_addr": { + "type": "string" + }, + "tunnel_target_protocol": { + "type": "string" + }, + "upstream_type": { + "type": "string" + }, + "upstreams": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "proxy_route.View": { + "type": "object", + "properties": { + "basic_auth_enabled": { + "type": "boolean" + }, + "basic_auth_password": { + "type": "string" + }, + "basic_auth_username": { + "type": "string" + }, + "cache_enabled": { + "type": "boolean" + }, + "cache_policy": { + "type": "string" + }, + "cache_rule_list": { + "type": "array", + "items": { + "type": "string" + } + }, + "cache_rules": { + "type": "string" + }, + "cert_id": { + "type": "integer" + }, + "cert_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "created_at": { + "type": "string" + }, + "custom_header_list": { + "type": "array", + "items": { + "$ref": "#/definitions/proxy_route.CustomHeaderInput" + } + }, + "custom_headers": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "domain_cert_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "domain_count": { + "type": "integer" + }, + "domains": { + "type": "array", + "items": { + "type": "string" + } + }, + "enable_https": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "limit_conn_per_ip": { + "type": "integer" + }, + "limit_conn_per_server": { + "type": "integer" + }, + "limit_rate": { + "type": "string" + }, + "origin_host": { + "type": "string" + }, + "origin_id": { + "type": "integer" + }, + "origin_url": { + "type": "string" + }, + "pages_project_id": { + "type": "integer" + }, + "primary_domain": { + "type": "string" + }, + "redirect_http": { + "type": "boolean" + }, + "remark": { + "type": "string" + }, + "site_name": { + "type": "string" + }, + "tunnel_id": { + "type": "integer" + }, + "tunnel_node_id": { + "type": "integer" + }, + "tunnel_target_addr": { + "type": "string" + }, + "tunnel_target_protocol": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "upstream_list": { + "type": "array", + "items": { + "type": "string" + } + }, + "upstream_type": { + "type": "string" + }, + "upstreams": { + "type": "string" + } + } + }, "push.Config": { "type": "object", "properties": { @@ -7258,6 +16524,277 @@ } } }, + "tls.ApplyInput": { + "type": "object", + "properties": { + "acme_account_id": { + "type": "integer" + }, + "auto_renew": { + "type": "boolean" + }, + "disable_cname": { + "type": "boolean" + }, + "dns1": { + "type": "string" + }, + "dns2": { + "type": "string" + }, + "dns_account_id": { + "type": "integer" + }, + "key_algorithm": { + "type": "string" + }, + "name": { + "type": "string" + }, + "other_domains": { + "type": "string" + }, + "primary_domain": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "skip_dns": { + "type": "boolean" + } + } + }, + "tls.CertificateContent": { + "type": "object", + "properties": { + "acme_account_id": { + "type": "integer" + }, + "apply_message": { + "type": "string" + }, + "apply_status": { + "type": "string" + }, + "auto_renew": { + "type": "boolean" + }, + "cert_pem": { + "type": "string" + }, + "disable_cname": { + "type": "boolean" + }, + "dns1": { + "type": "string" + }, + "dns2": { + "type": "string" + }, + "dns_account_id": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "key_algorithm": { + "type": "string" + }, + "key_pem": { + "type": "string" + }, + "name": { + "type": "string" + }, + "other_domains": { + "type": "string" + }, + "primary_domain": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "skip_dns": { + "type": "boolean" + } + } + }, + "tls.CertificateInput": { + "type": "object", + "properties": { + "cert_pem": { + "type": "string" + }, + "key_pem": { + "type": "string" + }, + "name": { + "type": "string" + }, + "remark": { + "type": "string" + } + } + }, + "tls.DNSAccountInput": { + "type": "object", + "properties": { + "authorization": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "tls.ManagedDomainInput": { + "type": "object", + "properties": { + "cert_id": { + "type": "integer" + }, + "domain": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "remark": { + "type": "string" + } + } + }, + "tls.ManagedDomainMatchCandidate": { + "type": "object", + "properties": { + "certificate_id": { + "type": "integer" + }, + "certificate_name": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "managed_domain_id": { + "type": "integer" + }, + "match_type": { + "type": "string" + } + } + }, + "tls.ManagedDomainMatchResult": { + "type": "object", + "properties": { + "candidate": { + "$ref": "#/definitions/tls.ManagedDomainMatchCandidate" + }, + "candidates": { + "type": "array", + "items": { + "$ref": "#/definitions/tls.ManagedDomainMatchCandidate" + } + }, + "domain": { + "type": "string" + }, + "matched": { + "type": "boolean" + } + } + }, + "update.LatestReleaseView": { + "type": "object", + "properties": { + "body": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "current_version": { + "type": "string" + }, + "has_update": { + "type": "boolean" + }, + "html_url": { + "type": "string" + }, + "in_progress": { + "type": "boolean" + }, + "prerelease": { + "type": "boolean" + }, + "published_at": { + "type": "string" + }, + "tag_name": { + "type": "string" + }, + "upgrade_logs": { + "type": "array", + "items": { + "$ref": "#/definitions/update.UpgradeLogRecord" + } + }, + "upgrade_status": { + "type": "string" + }, + "upgrade_supported": { + "type": "boolean" + } + } + }, + "update.StreamSnapshot": { + "type": "object", + "properties": { + "in_progress": { + "type": "boolean" + }, + "upgrade_logs": { + "type": "array", + "items": { + "$ref": "#/definitions/update.UpgradeLogRecord" + } + }, + "upgrade_status": { + "type": "string" + } + } + }, + "update.UpgradeLogRecord": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "level": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "update.upgradeRequest": { + "type": "object", + "properties": { + "channel": { + "type": "string" + } + } + }, "updater.Status": { "type": "object", "properties": { @@ -7522,6 +17059,456 @@ "type": "string" } } + }, + "waf.IDsRequest": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "waf.IPGroupAutoTestInput": { + "type": "object", + "properties": { + "auto_config": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "waf.IPGroupAutoTestResult": { + "type": "object", + "properties": { + "lookback_minutes": { + "type": "integer" + }, + "matched_count": { + "type": "integer" + }, + "matched_ips": { + "type": "array", + "items": { + "type": "string" + } + }, + "rule_count": { + "type": "integer" + }, + "tested_at": { + "type": "string" + } + } + }, + "waf.IPGroupExtIPView": { + "type": "object", + "properties": { + "captured_at": { + "type": "string" + }, + "ip": { + "type": "string" + } + } + }, + "waf.IPGroupInput": { + "type": "object", + "properties": { + "auto_config": { + "type": "array", + "items": { + "type": "integer" + } + }, + "enabled": { + "type": "boolean" + }, + "ip_list": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "subscription_format": { + "type": "string" + }, + "subscription_mapping_rule": { + "type": "string" + }, + "subscription_url": { + "type": "string" + }, + "sync_interval_minutes": { + "type": "integer" + }, + "type": { + "type": "string" + } + } + }, + "waf.IPGroupSyncResult": { + "type": "object", + "properties": { + "group": { + "$ref": "#/definitions/waf.IPGroupView" + }, + "ip_count": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "next_sync_at": { + "type": "string" + }, + "status": { + "type": "string" + }, + "synced_at": { + "type": "string" + } + } + }, + "waf.IPGroupView": { + "type": "object", + "properties": { + "auto_config": { + "type": "array", + "items": { + "type": "integer" + } + }, + "created_at": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "ext_ips": { + "type": "array", + "items": { + "$ref": "#/definitions/waf.IPGroupExtIPView" + } + }, + "id": { + "type": "integer" + }, + "ip_list": { + "type": "array", + "items": { + "type": "string" + } + }, + "last_sync_message": { + "type": "string" + }, + "last_sync_status": { + "type": "string" + }, + "last_synced_at": { + "type": "string" + }, + "name": { + "type": "string" + }, + "next_sync_at": { + "type": "string" + }, + "referenced_by_rule_count": { + "type": "integer" + }, + "remark": { + "type": "string" + }, + "subscription_format": { + "type": "string" + }, + "subscription_mapping_rule": { + "type": "string" + }, + "subscription_url": { + "type": "string" + }, + "sync_interval_minutes": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "waf.PoWConfig": { + "type": "object", + "properties": { + "algorithm": { + "type": "string" + }, + "blacklist": { + "$ref": "#/definitions/waf.PoWListConfig" + }, + "challenge_ttl": { + "type": "integer" + }, + "difficulty": { + "type": "integer" + }, + "session_ttl": { + "type": "integer" + }, + "whitelist": { + "$ref": "#/definitions/waf.PoWListConfig" + } + } + }, + "waf.PoWListConfig": { + "type": "object", + "properties": { + "ip_cidrs": { + "type": "array", + "items": { + "type": "string" + } + }, + "ips": { + "type": "array", + "items": { + "type": "string" + } + }, + "path_regexes": { + "type": "array", + "items": { + "type": "string" + } + }, + "paths": { + "type": "array", + "items": { + "type": "string" + } + }, + "user_agents": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "waf.RuleGroupInput": { + "type": "object", + "properties": { + "block_response_body": { + "type": "string" + }, + "block_status_code": { + "type": "integer" + }, + "country_blacklist": { + "type": "array", + "items": { + "type": "string" + } + }, + "country_whitelist": { + "type": "array", + "items": { + "type": "string" + } + }, + "enabled": { + "type": "boolean" + }, + "ip_blacklist": { + "type": "array", + "items": { + "type": "string" + } + }, + "ip_blacklist_group_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "ip_whitelist": { + "type": "array", + "items": { + "type": "string" + } + }, + "ip_whitelist_group_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "name": { + "type": "string" + }, + "pow_config": { + "type": "array", + "items": { + "type": "integer" + } + }, + "pow_enabled": { + "type": "boolean" + }, + "region_blacklist": { + "type": "array", + "items": { + "type": "string" + } + }, + "region_whitelist": { + "type": "array", + "items": { + "type": "string" + } + }, + "remark": { + "type": "string" + } + } + }, + "waf.RuleGroupView": { + "type": "object", + "properties": { + "applied_site_count": { + "type": "integer" + }, + "applied_site_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "block_response_body": { + "type": "string" + }, + "block_status_code": { + "type": "integer" + }, + "country_blacklist": { + "type": "array", + "items": { + "type": "string" + } + }, + "country_whitelist": { + "type": "array", + "items": { + "type": "string" + } + }, + "created_at": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "ip_blacklist": { + "type": "array", + "items": { + "type": "string" + } + }, + "ip_blacklist_group_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "ip_whitelist": { + "type": "array", + "items": { + "type": "string" + } + }, + "ip_whitelist_group_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "is_global": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "pow_config": { + "$ref": "#/definitions/waf.PoWConfig" + }, + "pow_enabled": { + "type": "boolean" + }, + "region_blacklist": { + "type": "array", + "items": { + "type": "string" + } + }, + "region_whitelist": { + "type": "array", + "items": { + "type": "string" + } + }, + "remark": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "waf.SiteRuleGroupsView": { + "type": "object", + "properties": { + "applied_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "applied_rule_groups": { + "type": "array", + "items": { + "$ref": "#/definitions/waf.RuleGroupView" + } + }, + "global_rule_group": { + "$ref": "#/definitions/waf.RuleGroupView" + }, + "route_id": { + "type": "integer" + }, + "rule_groups": { + "type": "array", + "items": { + "$ref": "#/definitions/waf.RuleGroupView" + } + } + } } }, "securityDefinitions": { diff --git a/Wavelet/docs/swagger.yaml b/Wavelet/docs/swagger.yaml index 68da5de3..c39f6185 100644 --- a/Wavelet/docs/swagger.yaml +++ b/Wavelet/docs/swagger.yaml @@ -1,5 +1,36 @@ basePath: / definitions: + apply_log.CleanupInput: + properties: + delete_all: + type: boolean + retention_days: + type: integer + type: object + apply_log.CleanupResult: + properties: + cutoff: + type: string + delete_all: + type: boolean + deleted_count: + type: integer + retention_days: + type: integer + type: object + apply_log.ListResult: + properties: + current: + type: integer + rows: + items: + $ref: '#/definitions/model.OpenFlareApplyLog' + type: array + total: + type: integer + totalPage: + type: integer + type: object auth_source.AuthSourceRequest: properties: client_id: @@ -76,6 +107,199 @@ definitions: - solutions - token type: object + config_version.CleanupInput: + properties: + keep_count: + type: integer + type: object + config_version.CleanupResult: + properties: + deleted_count: + type: integer + message: + type: string + type: object + config_version.ConfigDiffResult: + properties: + active_version: + type: string + active_website_count: + type: integer + added_domains: + items: + type: string + type: array + added_sites: + items: + type: string + type: array + changed_option_details: + items: + $ref: '#/definitions/config_version.ConfigOptionDiffItem' + type: array + changed_option_keys: + items: + type: string + type: array + current_website_count: + type: integer + main_config_changed: + type: boolean + modified_domains: + items: + type: string + type: array + modified_sites: + items: + type: string + type: array + removed_domains: + items: + type: string + type: array + removed_sites: + items: + type: string + type: array + waf_config_changed: + type: boolean + type: object + config_version.ConfigOptionDiffItem: + properties: + current_value: + type: string + key: + type: string + previous_value: + type: string + type: object + config_version.ConfigPreviewResult: + properties: + checksum: + type: string + main_config: + type: string + rendered_config: + type: string + route_config: + type: string + route_count: + type: integer + snapshot_json: + type: string + support_files: + items: + $ref: '#/definitions/config_version.SupportFile' + type: array + website_count: + type: integer + type: object + config_version.SupportFile: + properties: + content: + type: string + path: + type: string + type: object + dashboard.Capacity: + properties: + average_cpu_usage_percent: + type: number + average_memory_usage_percent: + type: number + high_cpu_nodes: + type: integer + high_memory_nodes: + type: integer + high_storage_nodes: + type: integer + type: object + dashboard.OverviewPayload: + properties: + capacity: + $ref: '#/definitions/dashboard.Capacity' + distributions: + $ref: '#/definitions/dashboard.distributionsPayload' + generated_at: {} + nodes: + items: + items: {} + type: array + type: array + summary: + $ref: '#/definitions/dashboard.Summary' + traffic: + $ref: '#/definitions/dashboard.Traffic' + trends: + $ref: '#/definitions/dashboard.trendsPayload' + type: object + dashboard.Summary: + properties: + offline_nodes: + type: integer + online_nodes: + type: integer + pending_nodes: + type: integer + total_nodes: + type: integer + unhealthy_nodes: + type: integer + type: object + dashboard.Traffic: + properties: + error_count: + type: integer + estimated_qps: + type: number + reported_nodes: + type: integer + request_count: + type: integer + unique_visitors: + type: integer + type: object + dashboard.distributionsPayload: + properties: + source_countries: + items: + items: {} + type: array + type: array + status_codes: + items: + items: {} + type: array + type: array + top_domains: + items: + items: {} + type: array + type: array + type: object + dashboard.trendsPayload: + properties: + capacity_24h: + items: + items: {} + type: array + type: array + disk_io_24h: + items: + items: {} + type: array + type: array + network_24h: + items: + items: {} + type: array + type: array + traffic_24h: + items: + items: {} + type: array + type: array + type: object db_manage.DBOverviewResponse: properties: connections: @@ -344,6 +568,19 @@ definitions: user_id: type: integer type: object + model.AcmeAccount: + properties: + created_at: + type: string + email: + type: string + id: + type: integer + updated_at: + type: string + url: + type: string + type: object model.AuthSource: properties: client_id: @@ -371,6 +608,57 @@ definitions: updated_at: type: string type: object + model.ConfigVersion: + properties: + checksum: + type: string + created_at: + type: string + created_by: + type: string + id: + type: integer + is_active: + type: boolean + main_config: + type: string + rendered_config: + type: string + snapshot_json: + type: string + support_files_json: + type: string + version: + type: string + type: object + model.ConfigVersionSummary: + properties: + checksum: + type: string + created_at: + type: string + created_by: + type: string + id: + type: integer + is_active: + type: boolean + version: + type: string + type: object + model.DNSAccount: + properties: + created_at: + type: string + id: + type: integer + name: + type: string + type: + type: string + updated_at: + type: string + type: object model.ExternalAccountView: properties: auth_source_id: @@ -390,6 +678,169 @@ definitions: id: type: integer type: object + model.ManagedDomain: + properties: + cert_id: + type: integer + created_at: + type: string + domain: + type: string + enabled: + type: boolean + id: + type: integer + remark: + type: string + updated_at: + type: string + type: object + model.OpenFlareApplyLog: + properties: + checksum: + type: string + created_at: + type: string + id: + type: integer + main_config_checksum: + type: string + message: + type: string + node_id: + type: string + result: + type: string + route_config_checksum: + type: string + support_file_count: + type: integer + version: + type: string + type: object + model.OpenFlareHealthEvent: + properties: + created_at: + type: string + event_type: + type: string + first_triggered_at: + type: string + id: + type: integer + last_triggered_at: + type: string + message: + type: string + metadata_json: + type: string + node_id: + type: string + reported_at: + type: string + resolved_at: + type: string + severity: + type: string + status: + type: string + updated_at: + type: string + type: object + model.OpenFlareMetricSnapshot: + properties: + captured_at: + type: string + cpu_usage_percent: + type: number + created_at: + type: string + disk_read_bytes: + type: integer + disk_write_bytes: + type: integer + id: + type: integer + memory_total_bytes: + type: integer + memory_used_bytes: + type: integer + network_rx_bytes: + type: integer + network_tx_bytes: + type: integer + node_id: + type: string + storage_total_bytes: + type: integer + storage_used_bytes: + type: integer + type: object + model.OpenFlareNodeSystemProfile: + properties: + architecture: + type: string + cpu_cores: + type: integer + cpu_model: + type: string + created_at: + type: string + hostname: + type: string + id: + type: integer + kernel_version: + type: string + node_id: + type: string + os_name: + type: string + os_version: + type: string + reported_at: + type: string + total_disk_bytes: + type: integer + total_memory_bytes: + type: integer + updated_at: + type: string + uptime_seconds: + type: integer + type: object + model.OpenFlareOption: + properties: + key: + type: string + value: + type: string + type: object + model.OpenFlareRequestReport: + properties: + created_at: + type: string + error_count: + type: integer + id: + type: integer + node_id: + type: string + request_count: + type: integer + source_countries_json: + type: string + status_codes_json: + type: string + top_domains_json: + type: string + unique_visitor_count: + type: integer + window_ended_at: + type: string + window_started_at: + type: string + type: object model.PushChannel: properties: created_at: @@ -515,6 +966,49 @@ definitions: visibility: type: integer type: object + model.TLSCertificate: + properties: + acme_account_id: + type: integer + apply_message: + type: string + apply_status: + type: string + auto_renew: + type: boolean + created_at: + type: string + disable_cname: + type: boolean + dns_account_id: + type: integer + dns1: + type: string + dns2: + type: string + id: + type: integer + key_algorithm: + type: string + name: + type: string + not_after: + type: string + not_before: + type: string + other_domains: + type: string + primary_domain: + type: string + provider: + type: string + remark: + type: string + skip_dns: + type: boolean + updated_at: + type: string + type: object model.TaskExecution: properties: created_at: @@ -672,10 +1166,198 @@ definitions: UploadStatusDeleted: 已删除 UploadStatusPending: 待使用 UploadStatusUsed: 已使用 + x-enum-descriptions: + - 待使用 + - 已使用 + - 已删除 x-enum-varnames: - UploadStatusPending - UploadStatusUsed - UploadStatusDeleted + node.AgentReleaseInfo: + properties: + body: + type: string + channel: + type: string + current_version: + type: string + has_update: + type: boolean + html_url: + type: string + prerelease: + type: boolean + published_at: + type: string + requested_channel: + type: string + requested_tag: + type: string + tag_name: + type: string + update_requested: + type: boolean + type: object + node.AgentUpdateInput: + properties: + channel: + type: string + tag_name: + type: string + type: object + node.BootstrapView: + properties: + discovery_token: + type: string + type: object + node.HealthEventCleanupResult: + properties: + deleted_count: + type: integer + node_id: + type: string + type: object + node.Input: + properties: + auto_update_enabled: + type: boolean + geo_latitude: + type: number + geo_longitude: + type: number + geo_manual_override: + type: boolean + geo_name: + type: string + ip: + type: string + ip_manual_override: + type: boolean + name: + type: string + node_type: + type: string + relay_agent_access_addr: + type: string + relay_bind_port: + type: integer + relay_client_access_addr: + type: string + relay_client_proxy_url: + type: string + relay_vhost_http_port: + type: integer + relay_web_server_enabled: + type: boolean + type: object + node.ObservabilityView: + properties: + analytics: + $ref: '#/definitions/observability.NodeAnalytics' + health_events: + items: + $ref: '#/definitions/model.OpenFlareHealthEvent' + type: array + metric_snapshots: + items: + $ref: '#/definitions/model.OpenFlareMetricSnapshot' + type: array + node_id: + type: string + profile: + $ref: '#/definitions/model.OpenFlareNodeSystemProfile' + relay_dashboard: + $ref: '#/definitions/observability.RelayDashboardSnapshot' + traffic_reports: + items: + $ref: '#/definitions/model.OpenFlareRequestReport' + type: array + trends: + $ref: '#/definitions/observability.NodeTrends' + type: object + node.View: + properties: + access_token: + type: string + auto_update_enabled: + type: boolean + created_at: + type: string + current_version: + type: string + ext_version: + type: string + geo_latitude: + type: number + geo_longitude: + type: number + geo_manual_override: + type: boolean + geo_name: + type: string + id: + type: integer + ip: + type: string + ip_manual_override: + type: boolean + last_error: + type: string + last_seen_at: {} + latest_apply_at: + type: string + latest_apply_checksum: + type: string + latest_apply_message: + type: string + latest_apply_result: + type: string + latest_main_config_checksum: + type: string + latest_route_config_checksum: + type: string + latest_support_file_count: + type: integer + name: + type: string + node_id: + type: string + node_type: + type: string + openresty_message: + type: string + openresty_status: + type: string + relay_agent_access_addr: + type: string + relay_bind_port: + type: integer + relay_client_access_addr: + type: string + relay_client_proxy_url: + type: string + relay_status: + type: string + relay_vhost_http_port: + type: integer + relay_web_server_enabled: + type: boolean + restart_openresty_requested: + type: boolean + status: + type: string + update_channel: + type: string + update_requested: + type: boolean + update_tag: + type: string + updated_at: + type: string + version: + type: string + type: object oauth.AuthSourceView: properties: client_secret_configured: @@ -742,6 +1424,794 @@ definitions: user: $ref: '#/definitions/oauth.BasicUserInfo' type: object + observability.AccessLogCleanupInput: + properties: + retention_days: + type: integer + type: object + observability.AccessLogCleanupResult: + properties: + cutoff: + type: string + deleted_count: + type: integer + retention_days: + type: integer + type: object + observability.AccessLogIPSummaryList: + properties: + has_more: + type: boolean + items: + items: + $ref: '#/definitions/observability.AccessLogIPSummaryView' + type: array + page: + type: integer + page_size: + type: integer + sort_by: + type: string + sort_order: + type: string + total_ip: + type: integer + type: object + observability.AccessLogIPSummaryView: + properties: + last_seen_at: + type: string + recent_requests: + type: integer + remote_addr: + type: string + total_requests: + type: integer + type: object + observability.AccessLogIPTrendPoint: + properties: + bucket_started_at: + type: string + request_count: + type: integer + type: object + observability.AccessLogIPTrendView: + properties: + bucket_minutes: + type: integer + hours: + type: integer + points: + items: + $ref: '#/definitions/observability.AccessLogIPTrendPoint' + type: array + remote_addr: + type: string + type: object + observability.AccessLogList: + properties: + has_more: + type: boolean + items: + items: + $ref: '#/definitions/observability.AccessLogView' + type: array + page: + type: integer + page_size: + type: integer + total_ip: + type: integer + total_record: + type: integer + type: object + observability.AccessLogView: + properties: + host: + type: string + id: + type: integer + logged_at: + type: string + node_id: + type: string + node_name: + type: string + path: + type: string + region: + type: string + remote_addr: + type: string + status_code: + type: integer + type: object + observability.CapacityTrendPoint: + properties: + average_cpu_usage_percent: + type: number + average_memory_usage_percent: + type: number + bucket_started_at: + type: string + reported_nodes: + type: integer + type: object + observability.DiskIOTrendPoint: + properties: + bucket_started_at: + type: string + disk_read_bytes: + type: integer + disk_write_bytes: + type: integer + reported_nodes: + type: integer + type: object + observability.DistributionItem: + properties: + key: + type: string + value: + type: integer + type: object + observability.FoldedAccessLogIPList: + properties: + bucket_started_at: + type: string + fold_minutes: + type: integer + has_more: + type: boolean + items: + items: + $ref: '#/definitions/observability.FoldedAccessLogIPView' + type: array + page: + type: integer + page_size: + type: integer + sort_by: + type: string + sort_order: + type: string + total_ip: + type: integer + type: object + observability.FoldedAccessLogIPView: + properties: + client_error_count: + type: integer + last_seen_at: + type: string + remote_addr: + type: string + request_count: + type: integer + server_error_count: + type: integer + success_count: + type: integer + type: object + observability.FoldedAccessLogList: + properties: + fold_minutes: + type: integer + has_more: + type: boolean + items: + items: + $ref: '#/definitions/observability.FoldedAccessLogView' + type: array + page: + type: integer + page_size: + type: integer + total_bucket: + type: integer + total_ip: + type: integer + total_record: + type: integer + type: object + observability.FoldedAccessLogView: + properties: + bucket_started_at: + type: string + client_error_count: + type: integer + request_count: + type: integer + server_error_count: + type: integer + success_count: + type: integer + unique_host_count: + type: integer + unique_ip_count: + type: integer + type: object + observability.HealthSummary: + properties: + active_alerts: + type: integer + critical_alerts: + type: integer + has_capacity_risk: + type: boolean + has_runtime_risk: + type: boolean + has_traffic_risk: + type: boolean + info_alerts: + type: integer + resolved_alerts: + type: integer + warning_alerts: + type: integer + type: object + observability.NetworkTrendPoint: + properties: + bucket_started_at: + type: string + network_rx_bytes: + type: integer + network_tx_bytes: + type: integer + openresty_rx_bytes: + type: integer + openresty_tx_bytes: + type: integer + reported_nodes: + type: integer + type: object + observability.NodeAnalytics: + properties: + distributions: + $ref: '#/definitions/observability.TrafficDistributions' + health: + $ref: '#/definitions/observability.HealthSummary' + traffic: + $ref: '#/definitions/observability.TrafficWindowSummary' + type: object + observability.NodeTrends: + properties: + capacity_24h: + items: + $ref: '#/definitions/observability.CapacityTrendPoint' + type: array + disk_io_24h: + items: + $ref: '#/definitions/observability.DiskIOTrendPoint' + type: array + network_24h: + items: + $ref: '#/definitions/observability.NetworkTrendPoint' + type: array + traffic_24h: + items: + $ref: '#/definitions/observability.TrafficTrendPoint' + type: array + type: object + observability.RelayDashboardSnapshot: + properties: + client_counts: + type: integer + offline_proxies: + type: integer + online_proxies: + type: integer + proxies: + items: + $ref: '#/definitions/observability.RelayProxyStat' + type: array + total_connections: + type: integer + total_proxies: + type: integer + type: object + observability.RelayProxyStat: + properties: + client_addr: + type: string + client_version: + type: string + last_close_time: + type: string + last_start_time: + type: string + name: + type: string + status: + type: string + type: + type: string + type: object + observability.TrafficDistributions: + properties: + source_countries: + items: + $ref: '#/definitions/observability.DistributionItem' + type: array + status_codes: + items: + $ref: '#/definitions/observability.DistributionItem' + type: array + top_domains: + items: + $ref: '#/definitions/observability.DistributionItem' + type: array + type: object + observability.TrafficTrendPoint: + properties: + bucket_started_at: + type: string + error_count: + type: integer + request_count: + type: integer + unique_visitor_count: + type: integer + type: object + observability.TrafficWindowSummary: + properties: + error_count: + type: integer + error_rate_percent: + type: number + estimated_qps: + type: number + request_count: + type: integer + unique_visitor_count: + type: integer + window_ended_at: + type: string + window_started_at: + type: string + type: object + option.databaseCleanupInput: + properties: + retention_days: + type: integer + target: + type: string + type: object + option.databaseCleanupResult: + properties: + delete_all: + type: boolean + deleted_count: + type: integer + retention_days: + type: integer + target: + type: string + target_label: + type: string + type: object + option.geoIPLookupRequest: + properties: + ip: + type: string + provider: + type: string + type: object + option.geoIPLookupView: + properties: + ip: + type: string + iso_code: + type: string + latitude: + type: number + longitude: + type: number + name: + type: string + provider: + type: string + type: object + option.optionBatchPayload: + properties: + options: + items: + $ref: '#/definitions/model.OpenFlareOption' + type: array + type: object + option.publicAuthSourceView: + properties: + authorize_url: + type: string + display_name: + type: string + icon_url: + type: string + id: + type: integer + name: + type: string + type: + type: string + type: object + option.statusView: + properties: + auth_sources: + items: + $ref: '#/definitions/option.publicAuthSourceView' + type: array + cap_login_enabled: + type: boolean + email_verification: + type: boolean + footer_html: + type: string + github_client_id: + type: string + github_oauth: + type: boolean + home_page_link: + type: string + password_register_enabled: + type: boolean + server_address: + type: string + start_time: + type: integer + system_name: + type: string + version: + type: string + wechat_login: + type: boolean + wechat_qrcode: + type: string + type: object + origin.DetailView: + properties: + address: + type: string + created_at: + type: string + id: + type: integer + name: + type: string + remark: + type: string + route_count: + type: integer + routes: + items: + $ref: '#/definitions/origin.RouteSummary' + type: array + updated_at: + type: string + type: object + origin.Input: + properties: + address: + type: string + name: + type: string + remark: + type: string + type: object + origin.RouteSummary: + properties: + domain: + type: string + enabled: + type: boolean + id: + type: integer + origin_url: + type: string + updated_at: + type: string + type: object + origin.View: + properties: + address: + type: string + created_at: + type: string + id: + type: integer + name: + type: string + remark: + type: string + route_count: + type: integer + updated_at: + type: string + type: object + pages.DeploymentFileView: + properties: + checksum: + type: string + created_at: + type: string + deployment_id: + type: integer + id: + type: integer + path: + type: string + size: + type: integer + type: object + pages.DeploymentView: + properties: + activated_at: + type: string + checksum: + type: string + created_at: + type: string + created_by: + type: string + deployment_number: + type: integer + file_count: + type: integer + id: + type: integer + project_id: + type: integer + status: + type: string + total_size: + type: integer + type: object + pages.Input: + properties: + api_proxy_enabled: + type: boolean + api_proxy_pass: + type: string + api_proxy_path: + type: string + api_proxy_rewrite: + type: string + description: + type: string + enabled: + type: boolean + entry_file: + type: string + name: + type: string + root_dir: + type: string + slug: + type: string + spa_fallback_enabled: + type: boolean + spa_fallback_path: + type: string + type: object + pages.View: + properties: + active_deployment: + $ref: '#/definitions/pages.DeploymentView' + active_deployment_id: + type: integer + api_proxy_enabled: + type: boolean + api_proxy_pass: + type: string + api_proxy_path: + type: string + api_proxy_rewrite: + type: string + created_at: + type: string + deployment_count: + type: integer + description: + type: string + enabled: + type: boolean + entry_file: + type: string + id: + type: integer + name: + type: string + root_dir: + type: string + slug: + type: string + spa_fallback_enabled: + type: boolean + spa_fallback_path: + type: string + updated_at: + type: string + type: object + proxy_route.CustomHeaderInput: + properties: + key: + type: string + value: + type: string + type: object + proxy_route.Input: + properties: + basic_auth_enabled: + type: boolean + basic_auth_password: + type: string + basic_auth_username: + type: string + cache_enabled: + type: boolean + cache_policy: + type: string + cache_rules: + items: + type: string + type: array + cert_id: + type: integer + cert_ids: + items: + type: integer + type: array + custom_headers: + items: + $ref: '#/definitions/proxy_route.CustomHeaderInput' + type: array + domain: + type: string + domain_cert_ids: + items: + type: integer + type: array + domains: + items: + type: string + type: array + enable_https: + type: boolean + enabled: + type: boolean + limit_conn_per_ip: + type: integer + limit_conn_per_server: + type: integer + limit_rate: + type: string + origin_address: + type: string + origin_host: + type: string + origin_id: + type: integer + origin_port: + type: string + origin_scheme: + type: string + origin_uri: + type: string + origin_url: + type: string + pages_project_id: + type: integer + redirect_http: + type: boolean + remark: + type: string + site_name: + type: string + tunnel_id: + type: integer + tunnel_node_id: + type: integer + tunnel_target_addr: + type: string + tunnel_target_protocol: + type: string + upstream_type: + type: string + upstreams: + items: + type: string + type: array + type: object + proxy_route.View: + properties: + basic_auth_enabled: + type: boolean + basic_auth_password: + type: string + basic_auth_username: + type: string + cache_enabled: + type: boolean + cache_policy: + type: string + cache_rule_list: + items: + type: string + type: array + cache_rules: + type: string + cert_id: + type: integer + cert_ids: + items: + type: integer + type: array + created_at: + type: string + custom_header_list: + items: + $ref: '#/definitions/proxy_route.CustomHeaderInput' + type: array + custom_headers: + type: string + domain: + type: string + domain_cert_ids: + items: + type: integer + type: array + domain_count: + type: integer + domains: + items: + type: string + type: array + enable_https: + type: boolean + enabled: + type: boolean + id: + type: integer + limit_conn_per_ip: + type: integer + limit_conn_per_server: + type: integer + limit_rate: + type: string + origin_host: + type: string + origin_id: + type: integer + origin_url: + type: string + pages_project_id: + type: integer + primary_domain: + type: string + redirect_http: + type: boolean + remark: + type: string + site_name: + type: string + tunnel_id: + type: integer + tunnel_node_id: + type: integer + tunnel_target_addr: + type: string + tunnel_target_protocol: + type: string + updated_at: + type: string + upstream_list: + items: + type: string + type: array + upstream_type: + type: string + upstreams: + type: string + type: object push.Config: properties: channel: @@ -1217,6 +2687,183 @@ definitions: - name - type type: object + tls.ApplyInput: + properties: + acme_account_id: + type: integer + auto_renew: + type: boolean + disable_cname: + type: boolean + dns_account_id: + type: integer + dns1: + type: string + dns2: + type: string + key_algorithm: + type: string + name: + type: string + other_domains: + type: string + primary_domain: + type: string + remark: + type: string + skip_dns: + type: boolean + type: object + tls.CertificateContent: + properties: + acme_account_id: + type: integer + apply_message: + type: string + apply_status: + type: string + auto_renew: + type: boolean + cert_pem: + type: string + disable_cname: + type: boolean + dns_account_id: + type: integer + dns1: + type: string + dns2: + type: string + id: + type: integer + key_algorithm: + type: string + key_pem: + type: string + name: + type: string + other_domains: + type: string + primary_domain: + type: string + provider: + type: string + remark: + type: string + skip_dns: + type: boolean + type: object + tls.CertificateInput: + properties: + cert_pem: + type: string + key_pem: + type: string + name: + type: string + remark: + type: string + type: object + tls.DNSAccountInput: + properties: + authorization: + type: string + name: + type: string + type: + type: string + type: object + tls.ManagedDomainInput: + properties: + cert_id: + type: integer + domain: + type: string + enabled: + type: boolean + remark: + type: string + type: object + tls.ManagedDomainMatchCandidate: + properties: + certificate_id: + type: integer + certificate_name: + type: string + domain: + type: string + managed_domain_id: + type: integer + match_type: + type: string + type: object + tls.ManagedDomainMatchResult: + properties: + candidate: + $ref: '#/definitions/tls.ManagedDomainMatchCandidate' + candidates: + items: + $ref: '#/definitions/tls.ManagedDomainMatchCandidate' + type: array + domain: + type: string + matched: + type: boolean + type: object + update.LatestReleaseView: + properties: + body: + type: string + channel: + type: string + current_version: + type: string + has_update: + type: boolean + html_url: + type: string + in_progress: + type: boolean + prerelease: + type: boolean + published_at: + type: string + tag_name: + type: string + upgrade_logs: + items: + $ref: '#/definitions/update.UpgradeLogRecord' + type: array + upgrade_status: + type: string + upgrade_supported: + type: boolean + type: object + update.StreamSnapshot: + properties: + in_progress: + type: boolean + upgrade_logs: + items: + $ref: '#/definitions/update.UpgradeLogRecord' + type: array + upgrade_status: + type: string + type: object + update.UpgradeLogRecord: + properties: + created_at: + type: string + level: + type: string + message: + type: string + type: object + update.upgradeRequest: + properties: + channel: + type: string + type: object updater.Status: properties: asset_name: @@ -1393,15 +3040,311 @@ definitions: website: type: string type: object + waf.IDsRequest: + properties: + ids: + items: + type: integer + type: array + type: object + waf.IPGroupAutoTestInput: + properties: + auto_config: + items: + type: integer + type: array + type: object + waf.IPGroupAutoTestResult: + properties: + lookback_minutes: + type: integer + matched_count: + type: integer + matched_ips: + items: + type: string + type: array + rule_count: + type: integer + tested_at: + type: string + type: object + waf.IPGroupExtIPView: + properties: + captured_at: + type: string + ip: + type: string + type: object + waf.IPGroupInput: + properties: + auto_config: + items: + type: integer + type: array + enabled: + type: boolean + ip_list: + items: + type: string + type: array + name: + type: string + remark: + type: string + subscription_format: + type: string + subscription_mapping_rule: + type: string + subscription_url: + type: string + sync_interval_minutes: + type: integer + type: + type: string + type: object + waf.IPGroupSyncResult: + properties: + group: + $ref: '#/definitions/waf.IPGroupView' + ip_count: + type: integer + message: + type: string + next_sync_at: + type: string + status: + type: string + synced_at: + type: string + type: object + waf.IPGroupView: + properties: + auto_config: + items: + type: integer + type: array + created_at: + type: string + enabled: + type: boolean + ext_ips: + items: + $ref: '#/definitions/waf.IPGroupExtIPView' + type: array + id: + type: integer + ip_list: + items: + type: string + type: array + last_sync_message: + type: string + last_sync_status: + type: string + last_synced_at: + type: string + name: + type: string + next_sync_at: + type: string + referenced_by_rule_count: + type: integer + remark: + type: string + subscription_format: + type: string + subscription_mapping_rule: + type: string + subscription_url: + type: string + sync_interval_minutes: + type: integer + type: + type: string + updated_at: + type: string + type: object + waf.PoWConfig: + properties: + algorithm: + type: string + blacklist: + $ref: '#/definitions/waf.PoWListConfig' + challenge_ttl: + type: integer + difficulty: + type: integer + session_ttl: + type: integer + whitelist: + $ref: '#/definitions/waf.PoWListConfig' + type: object + waf.PoWListConfig: + properties: + ip_cidrs: + items: + type: string + type: array + ips: + items: + type: string + type: array + path_regexes: + items: + type: string + type: array + paths: + items: + type: string + type: array + user_agents: + items: + type: string + type: array + type: object + waf.RuleGroupInput: + properties: + block_response_body: + type: string + block_status_code: + type: integer + country_blacklist: + items: + type: string + type: array + country_whitelist: + items: + type: string + type: array + enabled: + type: boolean + ip_blacklist: + items: + type: string + type: array + ip_blacklist_group_ids: + items: + type: integer + type: array + ip_whitelist: + items: + type: string + type: array + ip_whitelist_group_ids: + items: + type: integer + type: array + name: + type: string + pow_config: + items: + type: integer + type: array + pow_enabled: + type: boolean + region_blacklist: + items: + type: string + type: array + region_whitelist: + items: + type: string + type: array + remark: + type: string + type: object + waf.RuleGroupView: + properties: + applied_site_count: + type: integer + applied_site_ids: + items: + type: integer + type: array + block_response_body: + type: string + block_status_code: + type: integer + country_blacklist: + items: + type: string + type: array + country_whitelist: + items: + type: string + type: array + created_at: + type: string + enabled: + type: boolean + id: + type: integer + ip_blacklist: + items: + type: string + type: array + ip_blacklist_group_ids: + items: + type: integer + type: array + ip_whitelist: + items: + type: string + type: array + ip_whitelist_group_ids: + items: + type: integer + type: array + is_global: + type: boolean + name: + type: string + pow_config: + $ref: '#/definitions/waf.PoWConfig' + pow_enabled: + type: boolean + region_blacklist: + items: + type: string + type: array + region_whitelist: + items: + type: string + type: array + remark: + type: string + updated_at: + type: string + type: object + waf.SiteRuleGroupsView: + properties: + applied_ids: + items: + type: integer + type: array + applied_rule_groups: + items: + $ref: '#/definitions/waf.RuleGroupView' + type: array + global_rule_group: + $ref: '#/definitions/waf.RuleGroupView' + route_id: + type: integer + rule_groups: + items: + $ref: '#/definitions/waf.RuleGroupView' + type: array + type: object info: contact: - name: Wavelet - url: https://github.com/Rain-kl/Wavelet - description: Wavelet 平台后端 API,提供用户认证、系统配置、任务调度等通用功能。 + name: OpenFlare + url: https://github.com/Rain-kl/OpenFlare + description: OpenFlare 平台后端 API,提供用户认证、系统配置、任务调度与边缘节点管理能力。 license: name: Apache 2.0 url: http://www.apache.org/licenses/LICENSE-2.0.html - title: Wavelet API + title: OpenFlare API version: 1.0.0 paths: /api/cap/challenge: @@ -1556,6 +3499,7 @@ paths: description: 删除指定认证源及其关联的所有外部帐号绑定记录,警告:删除后相关用户将无法通过该源登录,需要管理员权限 parameters: - description: 认证源 ID 或名称 + format: int64 in: path name: id required: true @@ -1595,6 +3539,7 @@ paths: description: 更新指定 ID 的认证源配置。若 client_secret 字段为空,则保留原有密钥不变,需要管理员权限 parameters: - description: 认证源 ID 或名称 + format: int64 in: path name: id required: true @@ -1645,6 +3590,7 @@ paths: description: 启用或禁用指定认证源。尝试启用时将验证 Client ID 和 Client Secret 是否已配置,需要管理员权限 parameters: - description: 认证源 ID 或名称 + format: int64 in: path name: id required: true @@ -2141,6 +4087,7 @@ paths: description: 根据ID删除消息通道,需要管理员权限 parameters: - description: 通道ID + format: int64 in: path name: id required: true @@ -2163,6 +4110,7 @@ paths: description: 修改消息通道配置,需要管理员权限 parameters: - description: 通道ID + format: int64 in: path name: id required: true @@ -3383,6 +5331,7 @@ paths: name: extension type: string - description: 上传用户 ID + format: int64 in: query name: user_id type: integer @@ -3849,6 +5798,4260 @@ paths: summary: Sample Hello API tags: - custom + /api/v1/custom/openflare/about: + get: + description: 返回 OpenFlare 关于页面文本,无需登录 + produces: + - application/json + responses: + "200": + description: 关于信息 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + type: string + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + summary: 获取关于信息 + tags: + - openflare-option + /api/v1/custom/openflare/access-logs: + get: + description: 分页返回 OpenFlare 访问日志,支持按节点、IP、主机与路径筛选,需要管理员权限 + parameters: + - description: 节点 ID + in: query + name: node_id + type: string + - description: 客户端 IP + in: query + name: remote_addr + type: string + - description: 请求 Host + in: query + name: host + type: string + - description: 请求路径 + in: query + name: path + type: string + - description: 页码 + in: query + name: p + type: integer + - description: 每页条数 + in: query + name: page_size + type: integer + - description: 排序字段 + in: query + name: sort_by + type: string + - description: 排序方向 + in: query + name: sort_order + type: string + produces: + - application/json + responses: + "200": + description: 访问日志列表 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/observability.AccessLogList' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 列出访问日志 + tags: + - openflare-observability + /api/v1/custom/openflare/access-logs/cleanup: + post: + consumes: + - application/json + description: 按保留天数清理过期访问日志记录,需要管理员权限 + parameters: + - description: 清理参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/observability.AccessLogCleanupInput' + produces: + - application/json + responses: + "200": + description: 清理结果 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/observability.AccessLogCleanupResult' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 清理访问日志 + tags: + - openflare-observability + /api/v1/custom/openflare/access-logs/folds: + get: + description: 按时间桶聚合访问日志并分页返回,需要管理员权限 + parameters: + - description: 节点 ID + in: query + name: node_id + type: string + - description: 客户端 IP + in: query + name: remote_addr + type: string + - description: 请求 Host + in: query + name: host + type: string + - description: 请求路径 + in: query + name: path + type: string + - description: 折叠时间窗口(分钟) + in: query + name: fold_minutes + type: integer + - description: 页码 + in: query + name: p + type: integer + - description: 每页条数 + in: query + name: page_size + type: integer + - description: 排序字段 + in: query + name: sort_by + type: string + - description: 排序方向 + in: query + name: sort_order + type: string + produces: + - application/json + responses: + "200": + description: 折叠访问日志列表 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/observability.FoldedAccessLogList' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 列出折叠访问日志 + tags: + - openflare-observability + /api/v1/custom/openflare/access-logs/folds/ip-summary: + get: + description: 在指定时间桶内按 IP 聚合访问统计,需要管理员权限 + parameters: + - description: 节点 ID + in: query + name: node_id + type: string + - description: 客户端 IP + in: query + name: remote_addr + type: string + - description: 请求 Host + in: query + name: host + type: string + - description: 请求路径 + in: query + name: path + type: string + - description: 时间桶起始时间 + in: query + name: bucket_started_at + type: string + - description: 折叠时间窗口(分钟) + in: query + name: fold_minutes + type: integer + - description: 页码 + in: query + name: p + type: integer + - description: 每页条数 + in: query + name: page_size + type: integer + - description: 排序字段 + in: query + name: sort_by + type: string + - description: 排序方向 + in: query + name: sort_order + type: string + produces: + - application/json + responses: + "200": + description: 折叠 IP 汇总列表 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/observability.FoldedAccessLogIPList' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 列出折叠访问日志 IP 汇总 + tags: + - openflare-observability + /api/v1/custom/openflare/access-logs/ip-summary: + get: + description: 按 IP 聚合访问日志统计并分页返回,需要管理员权限 + parameters: + - description: 节点 ID + in: query + name: node_id + type: string + - description: 客户端 IP + in: query + name: remote_addr + type: string + - description: 请求 Host + in: query + name: host + type: string + - description: 页码 + in: query + name: p + type: integer + - description: 每页条数 + in: query + name: page_size + type: integer + - description: 排序字段 + in: query + name: sort_by + type: string + - description: 排序方向 + in: query + name: sort_order + type: string + produces: + - application/json + responses: + "200": + description: IP 汇总列表 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/observability.AccessLogIPSummaryList' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 列出访问日志 IP 汇总 + tags: + - openflare-observability + /api/v1/custom/openflare/access-logs/ip-summary/trend: + get: + description: 返回指定 IP 在时间范围内的访问趋势数据,需要管理员权限 + parameters: + - description: 节点 ID + in: query + name: node_id + type: string + - description: 客户端 IP + in: query + name: remote_addr + type: string + - description: 请求 Host + in: query + name: host + type: string + - description: 统计时间范围(小时) + in: query + name: hours + type: integer + - description: 时间桶粒度(分钟) + in: query + name: bucket_minutes + type: integer + produces: + - application/json + responses: + "200": + description: IP 访问趋势 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/observability.AccessLogIPTrendView' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取访问日志 IP 趋势 + tags: + - openflare-observability + /api/v1/custom/openflare/acme-accounts/default: + get: + description: 返回系统默认 ACME 账号配置,需要管理员权限 + produces: + - application/json + responses: + "200": + description: 默认 ACME 账号 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/model.AcmeAccount' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 记录不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取默认 ACME 账号 + tags: + - openflare-tls + /api/v1/custom/openflare/apply-logs: + get: + description: 分页返回节点配置下发记录,支持按节点 ID 筛选,需要管理员权限 + parameters: + - description: 节点 ID 筛选 + in: query + name: node_id + type: string + - description: 页码 + in: query + name: pageNo + type: integer + - description: 页码(别名) + in: query + name: page_no + type: integer + - description: 每页数量 + in: query + name: pageSize + type: integer + - description: 每页数量(别名) + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: 下发日志列表 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/apply_log.ListResult' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取配置下发日志 + tags: + - openflare-apply-log + /api/v1/custom/openflare/apply-logs/cleanup: + post: + consumes: + - application/json + description: 按保留天数清理历史下发记录,或删除全部记录,需要管理员权限 + parameters: + - description: 清理参数 + in: body + name: body + required: true + schema: + $ref: '#/definitions/apply_log.CleanupInput' + produces: + - application/json + responses: + "200": + description: 清理结果 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/apply_log.CleanupResult' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 清理配置下发日志 + tags: + - openflare-apply-log + /api/v1/custom/openflare/config-versions: + get: + description: 返回所有已发布的 OpenResty 配置版本摘要,需要管理员权限 + produces: + - application/json + responses: + "200": + description: 配置版本列表 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + items: + $ref: '#/definitions/model.ConfigVersionSummary' + type: array + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取配置版本列表 + tags: + - openflare-config-version + /api/v1/custom/openflare/config-versions/{id}: + get: + description: 返回指定配置版本的完整快照与渲染内容,需要管理员权限 + parameters: + - description: 配置版本 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 配置版本详情 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/model.ConfigVersion' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或版本不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取配置版本详情 + tags: + - openflare-config-version + /api/v1/custom/openflare/config-versions/{id}/activate: + post: + description: 将指定历史版本设为当前活跃配置,需要管理员权限 + parameters: + - description: 配置版本 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 激活成功 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/model.ConfigVersion' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或版本不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 激活配置版本 + tags: + - openflare-config-version + /api/v1/custom/openflare/config-versions/active: + get: + description: 返回当前正在使用的配置版本,需要管理员权限 + produces: + - application/json + responses: + "200": + description: 活跃配置版本 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/model.ConfigVersion' + type: object + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限、不存在或无活跃版本 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取当前活跃配置版本 + tags: + - openflare-config-version + /api/v1/custom/openflare/config-versions/cleanup: + post: + consumes: + - application/json + description: 删除超出保留数量的非活跃配置版本,需要管理员权限 + parameters: + - description: 清理参数 + in: body + name: body + required: true + schema: + $ref: '#/definitions/config_version.CleanupInput' + produces: + - application/json + responses: + "200": + description: 清理结果 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/config_version.CleanupResult' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 清理历史配置版本 + tags: + - openflare-config-version + /api/v1/custom/openflare/config-versions/diff: + get: + description: 对比当前草稿配置与活跃版本之间的差异,需要管理员权限 + produces: + - application/json + responses: + "200": + description: 配置差异 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/config_version.ConfigDiffResult' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 对比草稿与活跃配置 + tags: + - openflare-config-version + /api/v1/custom/openflare/config-versions/preview: + get: + description: 渲染并返回当前草稿配置的预览结果,需要管理员权限 + produces: + - application/json + responses: + "200": + description: 配置预览 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/config_version.ConfigPreviewResult' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 预览当前草稿配置 + tags: + - openflare-config-version + /api/v1/custom/openflare/config-versions/publish: + post: + description: 将当前草稿配置发布为新版本,需要管理员权限 + parameters: + - description: 是否强制发布 + in: query + name: force + type: boolean + produces: + - application/json + responses: + "200": + description: 发布成功 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/model.ConfigVersion' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 发布配置版本 + tags: + - openflare-config-version + /api/v1/custom/openflare/dashboard/overview: + get: + description: 聚合节点与可观测性数据,返回 OpenFlare 控制台仪表盘概览,需要管理员权限 + produces: + - application/json + responses: + "200": + description: 仪表盘概览 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/dashboard.OverviewPayload' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取仪表盘概览 + tags: + - openflare-dashboard + /api/v1/custom/openflare/dns-accounts: + get: + description: 返回全部 DNS 提供商账号,需要管理员权限 + produces: + - application/json + responses: + "200": + description: DNS 账号列表 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + items: + $ref: '#/definitions/model.DNSAccount' + type: array + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 列出 DNS 账号 + tags: + - openflare-tls + post: + consumes: + - application/json + description: 创建新的 DNS 提供商账号,需要管理员权限 + parameters: + - description: DNS 账号参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/tls.DNSAccountInput' + produces: + - application/json + responses: + "200": + description: 创建成功的 DNS 账号 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/model.DNSAccount' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 创建 DNS 账号 + tags: + - openflare-tls + /api/v1/custom/openflare/dns-accounts/{id}/delete: + post: + description: 按 ID 删除 DNS 提供商账号,需要管理员权限 + parameters: + - description: DNS 账号 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + $ref: '#/definitions/response.Any' + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 记录不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 删除 DNS 账号 + tags: + - openflare-tls + /api/v1/custom/openflare/dns-accounts/{id}/update: + post: + consumes: + - application/json + description: 按 ID 更新 DNS 提供商账号,需要管理员权限 + parameters: + - description: DNS 账号 ID + in: path + name: id + required: true + type: integer + - description: DNS 账号参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/tls.DNSAccountInput' + produces: + - application/json + responses: + "200": + description: 更新后的 DNS 账号 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/model.DNSAccount' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 记录不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 更新 DNS 账号 + tags: + - openflare-tls + /api/v1/custom/openflare/managed-domains: + get: + description: 返回全部托管域名及关联证书,需要管理员权限 + produces: + - application/json + responses: + "200": + description: 托管域名列表 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + items: + $ref: '#/definitions/model.ManagedDomain' + type: array + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 列出托管域名 + tags: + - openflare-tls + post: + consumes: + - application/json + description: 创建新的托管域名记录,需要管理员权限 + parameters: + - description: 托管域名参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/tls.ManagedDomainInput' + produces: + - application/json + responses: + "200": + description: 创建成功的托管域名 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/model.ManagedDomain' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 创建托管域名 + tags: + - openflare-tls + /api/v1/custom/openflare/managed-domains/{id}/delete: + post: + description: 按 ID 删除托管域名,需要管理员权限 + parameters: + - description: 托管域名 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + $ref: '#/definitions/response.Any' + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 记录不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 删除托管域名 + tags: + - openflare-tls + /api/v1/custom/openflare/managed-domains/{id}/update: + post: + consumes: + - application/json + description: 按 ID 更新托管域名,需要管理员权限 + parameters: + - description: 托管域名 ID + in: path + name: id + required: true + type: integer + - description: 托管域名参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/tls.ManagedDomainInput' + produces: + - application/json + responses: + "200": + description: 更新后的托管域名 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/model.ManagedDomain' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 记录不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 更新托管域名 + tags: + - openflare-tls + /api/v1/custom/openflare/managed-domains/match: + get: + description: 按域名查询可用的证书匹配候选,需要管理员权限 + parameters: + - description: 域名 + in: query + name: domain + required: true + type: string + produces: + - application/json + responses: + "200": + description: 证书匹配结果 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/tls.ManagedDomainMatchResult' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 匹配托管域名证书 + tags: + - openflare-tls + /api/v1/custom/openflare/nodes: + get: + description: 返回所有节点及最新配置下发记录,需要管理员权限 + produces: + - application/json + responses: + "200": + description: 节点列表 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + items: + $ref: '#/definitions/node.View' + type: array + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取节点列表 + tags: + - openflare-node + post: + consumes: + - application/json + description: 创建新的边缘节点记录,需要管理员权限 + parameters: + - description: 节点参数 + in: body + name: body + required: true + schema: + $ref: '#/definitions/node.Input' + produces: + - application/json + responses: + "200": + description: 创建成功 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/node.View' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 创建节点 + tags: + - openflare-node + /api/v1/custom/openflare/nodes/{id}/agent-release: + get: + description: 返回指定节点可用的最新 Agent 版本信息,需要管理员权限 + parameters: + - description: 节点 ID + in: path + name: id + required: true + type: integer + - description: 发布渠道 + in: query + name: channel + type: string + produces: + - application/json + responses: + "200": + description: Agent 发布信息 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/node.AgentReleaseInfo' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或节点不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取 Agent 发布信息 + tags: + - openflare-node + /api/v1/custom/openflare/nodes/{id}/agent-update: + post: + consumes: + - application/json + description: 向指定节点下发 Agent 自更新指令,需要管理员权限 + parameters: + - description: 节点 ID + in: path + name: id + required: true + type: integer + - description: 更新参数(可选) + in: body + name: body + schema: + $ref: '#/definitions/node.AgentUpdateInput' + produces: + - application/json + responses: + "200": + description: 更新请求已下发 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/node.View' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或节点不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 请求 Agent 更新 + tags: + - openflare-node + /api/v1/custom/openflare/nodes/{id}/delete: + post: + description: 删除指定节点记录,需要管理员权限 + parameters: + - description: 节点 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + type: string + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或节点不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 删除节点 + tags: + - openflare-node + /api/v1/custom/openflare/nodes/{id}/force-sync: + post: + description: 向指定节点下发强制同步当前活跃配置的指令,需要管理员权限 + parameters: + - description: 节点 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 同步请求已下发 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/node.View' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或节点不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 请求强制同步配置 + tags: + - openflare-node + /api/v1/custom/openflare/nodes/{id}/observability: + get: + description: 返回指定节点的指标、健康事件与流量分析数据,需要管理员权限 + parameters: + - description: 节点 ID + in: path + name: id + required: true + type: integer + - description: 统计时间范围(小时) + in: query + name: hours + type: integer + - description: 返回记录数量上限 + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: 可观测性数据 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/node.ObservabilityView' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或节点不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取节点可观测性数据 + tags: + - openflare-node + /api/v1/custom/openflare/nodes/{id}/observability/cleanup: + post: + description: 清理指定节点的历史健康事件记录,需要管理员权限 + parameters: + - description: 节点 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 清理结果 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/node.HealthEventCleanupResult' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或节点不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 清理节点健康事件 + tags: + - openflare-node + /api/v1/custom/openflare/nodes/{id}/openresty-restart: + post: + description: 向指定节点下发 OpenResty 重启指令,需要管理员权限 + parameters: + - description: 节点 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 重启请求已下发 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/node.View' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或节点不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 请求重启 OpenResty + tags: + - openflare-node + /api/v1/custom/openflare/nodes/{id}/update: + post: + consumes: + - application/json + description: 更新指定节点的配置信息,需要管理员权限 + parameters: + - description: 节点 ID + in: path + name: id + required: true + type: integer + - description: 节点参数 + in: body + name: body + required: true + schema: + $ref: '#/definitions/node.Input' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/node.View' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或节点不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 更新节点 + tags: + - openflare-node + /api/v1/custom/openflare/nodes/bootstrap-token: + get: + description: 返回全局节点发现引导令牌,需要管理员权限 + produces: + - application/json + responses: + "200": + description: 引导令牌 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/node.BootstrapView' + type: object + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取引导令牌 + tags: + - openflare-node + /api/v1/custom/openflare/nodes/bootstrap-token/rotate: + post: + description: 重新生成全局节点发现引导令牌,需要管理员权限 + produces: + - application/json + responses: + "200": + description: 新引导令牌 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/node.BootstrapView' + type: object + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 轮换引导令牌 + tags: + - openflare-node + /api/v1/custom/openflare/notice: + get: + description: 返回 OpenFlare 控制台公告文本,无需登录 + produces: + - application/json + responses: + "200": + description: 系统公告 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + type: string + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + summary: 获取系统公告 + tags: + - openflare-option + /api/v1/custom/openflare/option: + get: + description: 返回全部非敏感 OpenFlare 配置项,需要管理员权限 + produces: + - application/json + responses: + "200": + description: 配置项列表 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + items: + $ref: '#/definitions/model.OpenFlareOption' + type: array + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 列出 OpenFlare 配置项 + tags: + - openflare-option + /api/v1/custom/openflare/option/database/cleanup: + post: + consumes: + - application/json + description: 按目标与保留天数清理可观测性相关数据表,需要管理员权限 + parameters: + - description: 清理参数 + in: body + name: request + schema: + $ref: '#/definitions/option.databaseCleanupInput' + produces: + - application/json + responses: + "200": + description: 清理结果 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/option.databaseCleanupResult' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 清理可观测性数据库 + tags: + - openflare-option + /api/v1/custom/openflare/option/geoip/lookup: + post: + consumes: + - application/json + description: 按提供商与 IP 查询地理位置信息,需要管理员权限 + parameters: + - description: 查询参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/option.geoIPLookupRequest' + produces: + - application/json + responses: + "200": + description: GeoIP 查询结果 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/option.geoIPLookupView' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: GeoIP 地址查询 + tags: + - openflare-option + /api/v1/custom/openflare/option/update: + post: + consumes: + - application/json + description: 更新单个 OpenFlare 配置项,需要管理员权限 + parameters: + - description: 配置项 + in: body + name: request + required: true + schema: + $ref: '#/definitions/model.OpenFlareOption' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + $ref: '#/definitions/response.Any' + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 更新 OpenFlare 配置项 + tags: + - openflare-option + /api/v1/custom/openflare/option/update-batch: + post: + consumes: + - application/json + description: 批量更新多个 OpenFlare 配置项,需要管理员权限 + parameters: + - description: 批量配置项 + in: body + name: request + required: true + schema: + $ref: '#/definitions/option.optionBatchPayload' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + $ref: '#/definitions/response.Any' + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 批量更新 OpenFlare 配置项 + tags: + - openflare-option + /api/v1/custom/openflare/origins: + get: + description: 返回所有源站及关联代理规则数量,需要管理员权限 + produces: + - application/json + responses: + "200": + description: 源站列表 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + items: + $ref: '#/definitions/origin.View' + type: array + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取源站列表 + tags: + - openflare-origin + post: + consumes: + - application/json + description: 创建新的上游源站记录,需要管理员权限 + parameters: + - description: 源站参数 + in: body + name: body + required: true + schema: + $ref: '#/definitions/origin.Input' + produces: + - application/json + responses: + "200": + description: 创建成功 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/origin.View' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 创建源站 + tags: + - openflare-origin + /api/v1/custom/openflare/origins/{id}: + get: + description: 返回指定源站信息及关联代理规则摘要,需要管理员权限 + parameters: + - description: 源站 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 源站详情 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/origin.DetailView' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或源站不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取源站详情 + tags: + - openflare-origin + /api/v1/custom/openflare/origins/{id}/delete: + post: + description: 删除指定源站记录,需要管理员权限 + parameters: + - description: 源站 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + type: string + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或源站不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 删除源站 + tags: + - openflare-origin + /api/v1/custom/openflare/origins/{id}/update: + post: + consumes: + - application/json + description: 更新指定源站的配置信息,需要管理员权限 + parameters: + - description: 源站 ID + in: path + name: id + required: true + type: integer + - description: 源站参数 + in: body + name: body + required: true + schema: + $ref: '#/definitions/origin.Input' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/origin.View' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或源站不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 更新源站 + tags: + - openflare-origin + /api/v1/custom/openflare/pages: + get: + description: 返回全部 OpenFlare Pages 项目,需要管理员权限 + produces: + - application/json + responses: + "200": + description: Pages 项目列表 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + items: + $ref: '#/definitions/pages.View' + type: array + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 列出 Pages 项目 + tags: + - openflare-pages + post: + consumes: + - application/json + description: 创建新的 OpenFlare Pages 项目,需要管理员权限 + parameters: + - description: 项目参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/pages.Input' + produces: + - application/json + responses: + "200": + description: 创建成功的项目 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/pages.View' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 创建 Pages 项目 + tags: + - openflare-pages + /api/v1/custom/openflare/pages/{id}: + get: + description: 按 ID 返回 Pages 项目详情,需要管理员权限 + parameters: + - description: 项目 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Pages 项目详情 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/pages.View' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 项目不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取 Pages 项目详情 + tags: + - openflare-pages + /api/v1/custom/openflare/pages/{id}/delete: + post: + description: 按 ID 删除 OpenFlare Pages 项目,需要管理员权限 + parameters: + - description: 项目 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + $ref: '#/definitions/response.Any' + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 项目不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 删除 Pages 项目 + tags: + - openflare-pages + /api/v1/custom/openflare/pages/{id}/deployments: + get: + description: 返回指定项目的全部部署记录,需要管理员权限 + parameters: + - description: 项目 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 部署列表 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + items: + $ref: '#/definitions/pages.DeploymentView' + type: array + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 项目不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 列出 Pages 部署 + tags: + - openflare-pages + /api/v1/custom/openflare/pages/{id}/deployments/{deployment_id}/activate: + post: + description: 将指定部署设为项目当前生效版本,需要管理员权限 + parameters: + - description: 项目 ID + in: path + name: id + required: true + type: integer + - description: 部署 ID + in: path + name: deployment_id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 激活后的项目 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/pages.View' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 项目或部署不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 激活 Pages 部署 + tags: + - openflare-pages + /api/v1/custom/openflare/pages/{id}/deployments/{deployment_id}/delete: + post: + description: 删除指定项目的部署记录,需要管理员权限 + parameters: + - description: 项目 ID + in: path + name: id + required: true + type: integer + - description: 部署 ID + in: path + name: deployment_id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + $ref: '#/definitions/response.Any' + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 项目或部署不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 删除 Pages 部署 + tags: + - openflare-pages + /api/v1/custom/openflare/pages/{id}/deployments/upload: + post: + consumes: + - multipart/form-data + description: 为指定项目上传 ZIP 部署包,需要管理员权限 + parameters: + - description: 项目 ID + in: path + name: id + required: true + type: integer + - description: 部署包 ZIP 文件 + in: formData + name: package + required: true + type: file + produces: + - application/json + responses: + "200": + description: 部署记录 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/pages.DeploymentView' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 项目不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 上传 Pages 部署包 + tags: + - openflare-pages + /api/v1/custom/openflare/pages/{id}/update: + post: + consumes: + - application/json + description: 按 ID 更新 OpenFlare Pages 项目,需要管理员权限 + parameters: + - description: 项目 ID + in: path + name: id + required: true + type: integer + - description: 项目参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/pages.Input' + produces: + - application/json + responses: + "200": + description: 更新后的项目 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/pages.View' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 项目不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 更新 Pages 项目 + tags: + - openflare-pages + /api/v1/custom/openflare/pages/deployments/{deployment_id}/files: + get: + description: 返回指定部署包含的文件清单,需要管理员权限 + parameters: + - description: 部署 ID + in: path + name: deployment_id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 部署文件列表 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + items: + $ref: '#/definitions/pages.DeploymentFileView' + type: array + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 部署不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 列出 Pages 部署文件 + tags: + - openflare-pages + /api/v1/custom/openflare/proxy-routes: + get: + description: 返回所有代理规则配置,需要管理员权限 + produces: + - application/json + responses: + "200": + description: 代理规则列表 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + items: + $ref: '#/definitions/proxy_route.View' + type: array + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取代理规则列表 + tags: + - openflare-proxy-route + post: + consumes: + - application/json + description: 创建新的反向代理规则,需要管理员权限 + parameters: + - description: 代理规则参数 + in: body + name: body + required: true + schema: + $ref: '#/definitions/proxy_route.Input' + produces: + - application/json + responses: + "200": + description: 创建成功 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/proxy_route.View' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 创建代理规则 + tags: + - openflare-proxy-route + /api/v1/custom/openflare/proxy-routes/{id}: + get: + description: 返回指定代理规则的完整配置,需要管理员权限 + parameters: + - description: 代理规则 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 代理规则详情 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/proxy_route.View' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或规则不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取代理规则详情 + tags: + - openflare-proxy-route + /api/v1/custom/openflare/proxy-routes/{id}/delete: + post: + description: 删除指定代理规则,需要管理员权限 + parameters: + - description: 代理规则 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + type: string + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或规则不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 删除代理规则 + tags: + - openflare-proxy-route + /api/v1/custom/openflare/proxy-routes/{id}/update: + post: + consumes: + - application/json + description: 更新指定代理规则的配置,需要管理员权限 + parameters: + - description: 代理规则 ID + in: path + name: id + required: true + type: integer + - description: 代理规则参数 + in: body + name: body + required: true + schema: + $ref: '#/definitions/proxy_route.Input' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/proxy_route.View' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 无权限或规则不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 更新代理规则 + tags: + - openflare-proxy-route + /api/v1/custom/openflare/status: + get: + description: 返回版本、认证源与系统公开配置,无需登录 + produces: + - application/json + responses: + "200": + description: 公开状态 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/option.statusView' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + summary: 获取 OpenFlare 公开状态 + tags: + - openflare-option + /api/v1/custom/openflare/tls-certificates: + get: + description: 返回全部 TLS 证书(不含 PEM),需要管理员权限 + produces: + - application/json + responses: + "200": + description: 证书列表 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + items: + $ref: '#/definitions/model.TLSCertificate' + type: array + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 列出 TLS 证书 + tags: + - openflare-tls + post: + consumes: + - application/json + description: 从 PEM 文本创建 TLS 证书,需要管理员权限 + parameters: + - description: 证书参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/tls.CertificateInput' + produces: + - application/json + responses: + "200": + description: 创建成功的证书 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/model.TLSCertificate' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 创建 TLS 证书 + tags: + - openflare-tls + /api/v1/custom/openflare/tls-certificates/{id}: + get: + description: 按 ID 返回 TLS 证书详情(不含 PEM),需要管理员权限 + parameters: + - description: 证书 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 证书详情 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/model.TLSCertificate' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 记录不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取 TLS 证书详情 + tags: + - openflare-tls + /api/v1/custom/openflare/tls-certificates/{id}/content: + get: + description: 按 ID 返回证书与私钥 PEM 内容,需要管理员权限 + parameters: + - description: 证书 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 证书 PEM 内容 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/tls.CertificateContent' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 记录不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取 TLS 证书 PEM 内容 + tags: + - openflare-tls + /api/v1/custom/openflare/tls-certificates/{id}/convert-acme: + post: + consumes: + - application/json + description: 将已上传证书转换为 ACME 自动续期模式,需要管理员权限 + parameters: + - description: 证书 ID + in: path + name: id + required: true + type: integer + - description: ACME 申请参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/tls.ApplyInput' + produces: + - application/json + responses: + "200": + description: 转换后的证书 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/model.TLSCertificate' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 记录不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 将证书转为 ACME 管理 + tags: + - openflare-tls + /api/v1/custom/openflare/tls-certificates/{id}/delete: + post: + description: 按 ID 删除 TLS 证书,需要管理员权限 + parameters: + - description: 证书 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + $ref: '#/definitions/response.Any' + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 记录不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 删除 TLS 证书 + tags: + - openflare-tls + /api/v1/custom/openflare/tls-certificates/{id}/renew: + post: + description: 手动触发 ACME 证书续期,需要管理员权限 + parameters: + - description: 证书 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 续期后的证书 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/model.TLSCertificate' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 记录不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 续期 ACME 证书 + tags: + - openflare-tls + /api/v1/custom/openflare/tls-certificates/{id}/update: + post: + consumes: + - application/json + description: 按 ID 更新 TLS 证书 PEM 信息,需要管理员权限 + parameters: + - description: 证书 ID + in: path + name: id + required: true + type: integer + - description: 证书参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/tls.CertificateInput' + produces: + - application/json + responses: + "200": + description: 更新后的证书 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/model.TLSCertificate' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 记录不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 更新 TLS 证书 + tags: + - openflare-tls + /api/v1/custom/openflare/tls-certificates/{id}/update-acme: + post: + consumes: + - application/json + description: 按 ID 更新 ACME 证书申请配置,需要管理员权限 + parameters: + - description: 证书 ID + in: path + name: id + required: true + type: integer + - description: ACME 申请参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/tls.ApplyInput' + produces: + - application/json + responses: + "200": + description: 更新后的证书 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/model.TLSCertificate' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 记录不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 更新 ACME 证书配置 + tags: + - openflare-tls + /api/v1/custom/openflare/tls-certificates/apply: + post: + consumes: + - application/json + description: 通过 ACME 申请新的 TLS 证书,需要管理员权限 + parameters: + - description: ACME 申请参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/tls.ApplyInput' + produces: + - application/json + responses: + "200": + description: 申请中的证书 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/model.TLSCertificate' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 申请 ACME 证书 + tags: + - openflare-tls + /api/v1/custom/openflare/tls-certificates/import-file: + post: + consumes: + - multipart/form-data + description: 上传证书与私钥文件创建 TLS 证书,需要管理员权限 + parameters: + - description: 证书名称 + in: formData + name: name + type: string + - description: 备注 + in: formData + name: remark + type: string + - description: 证书文件 + in: formData + name: cert_file + required: true + type: file + - description: 私钥文件 + in: formData + name: key_file + required: true + type: file + produces: + - application/json + responses: + "200": + description: 导入成功的证书 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/model.TLSCertificate' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 从文件导入 TLS 证书 + tags: + - openflare-tls + /api/v1/custom/openflare/update/latest-release: + get: + description: 查询 OpenFlare 服务端最新 GitHub Release 及升级状态,需要管理员权限 + parameters: + - description: 发布渠道(stable/preview) + in: query + name: channel + type: string + produces: + - application/json + responses: + "200": + description: 最新发布信息 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/update.LatestReleaseView' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取最新服务端发布版本 + tags: + - openflare-update + /api/v1/custom/openflare/update/logs/ws: + get: + description: 通过 WebSocket 推送升级进度快照,需要管理员权限 + responses: + "101": + description: WebSocket 升级日志流 + schema: + $ref: '#/definitions/update.StreamSnapshot' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 流式获取服务端升级日志 + tags: + - openflare-update + /api/v1/custom/openflare/update/manual-upgrade: + post: + consumes: + - application/json + description: 确认并执行手动上传的服务端升级(当前功能已禁用),需要管理员权限 + produces: + - application/json + responses: + "400": + description: 功能已禁用或参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 确认手动服务端升级 + tags: + - openflare-update + /api/v1/custom/openflare/update/manual-upload: + post: + consumes: + - multipart/form-data + description: 上传服务端二进制以进行手动升级(当前功能已禁用),需要管理员权限 + parameters: + - description: 服务端二进制文件 + in: formData + name: binary + required: true + type: file + produces: + - application/json + responses: + "400": + description: 功能已禁用或参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 上传手动升级二进制 + tags: + - openflare-update + /api/v1/custom/openflare/update/upgrade: + post: + consumes: + - application/json + description: 从最新 Release 调度 OpenFlare 服务端自动升级,需要管理员权限 + parameters: + - description: 升级参数 + in: body + name: request + schema: + $ref: '#/definitions/update.upgradeRequest' + produces: + - application/json + responses: + "200": + description: 升级任务已调度 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/update.LatestReleaseView' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 触发服务端自动升级 + tags: + - openflare-update + /api/v1/custom/openflare/uptimekuma/sync: + post: + consumes: + - application/json + description: 将 OpenFlare 节点同步到 Uptime Kuma,需要管理员权限 + produces: + - application/json + responses: + "200": + description: 同步成功 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + type: string + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 同步 Uptime Kuma + tags: + - openflare-option + /api/v1/custom/openflare/waf/ip-groups: + get: + description: 返回全部 WAF IP 组,需要管理员权限 + produces: + - application/json + responses: + "200": + description: IP 组列表 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + items: + $ref: '#/definitions/waf.IPGroupView' + type: array + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 列出 WAF IP 组 + tags: + - openflare-waf + post: + consumes: + - application/json + description: 创建新的 WAF IP 组,需要管理员权限 + parameters: + - description: IP 组参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/waf.IPGroupInput' + produces: + - application/json + responses: + "200": + description: 创建成功的 IP 组 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/waf.IPGroupView' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 创建 WAF IP 组 + tags: + - openflare-waf + /api/v1/custom/openflare/waf/ip-groups/{id}: + get: + description: 按 ID 返回 WAF IP 组详情,需要管理员权限 + parameters: + - description: IP 组 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: IP 组详情 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/waf.IPGroupView' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 记录不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取 WAF IP 组详情 + tags: + - openflare-waf + /api/v1/custom/openflare/waf/ip-groups/{id}/delete: + post: + description: 按 ID 删除 WAF IP 组,需要管理员权限 + parameters: + - description: IP 组 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + $ref: '#/definitions/response.Any' + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 记录不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 删除 WAF IP 组 + tags: + - openflare-waf + /api/v1/custom/openflare/waf/ip-groups/{id}/sync: + post: + description: 手动触发 WAF IP 组外部 IP 同步,需要管理员权限 + parameters: + - description: IP 组 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 同步结果 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/waf.IPGroupSyncResult' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 记录不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 同步 WAF IP 组 + tags: + - openflare-waf + /api/v1/custom/openflare/waf/ip-groups/{id}/update: + post: + consumes: + - application/json + description: 按 ID 更新 WAF IP 组,需要管理员权限 + parameters: + - description: IP 组 ID + in: path + name: id + required: true + type: integer + - description: IP 组参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/waf.IPGroupInput' + produces: + - application/json + responses: + "200": + description: 更新后的 IP 组 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/waf.IPGroupView' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 记录不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 更新 WAF IP 组 + tags: + - openflare-waf + /api/v1/custom/openflare/waf/ip-groups/test: + post: + consumes: + - application/json + description: 根据自动配置规则测试 IP 匹配结果(桩实现),需要管理员权限 + parameters: + - description: 自动配置参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/waf.IPGroupAutoTestInput' + produces: + - application/json + responses: + "200": + description: 测试结果 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/waf.IPGroupAutoTestResult' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 测试 WAF IP 组自动配置 + tags: + - openflare-waf + /api/v1/custom/openflare/waf/rule-groups: + get: + description: 返回全部 WAF 规则组,需要管理员权限 + produces: + - application/json + responses: + "200": + description: 规则组列表 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + items: + $ref: '#/definitions/waf.RuleGroupView' + type: array + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 列出 WAF 规则组 + tags: + - openflare-waf + post: + consumes: + - application/json + description: 创建新的 WAF 规则组,需要管理员权限 + parameters: + - description: 规则组参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/waf.RuleGroupInput' + produces: + - application/json + responses: + "200": + description: 创建成功的规则组 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/waf.RuleGroupView' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 创建 WAF 规则组 + tags: + - openflare-waf + /api/v1/custom/openflare/waf/rule-groups/{id}: + get: + description: 按 ID 返回 WAF 规则组详情,需要管理员权限 + parameters: + - description: 规则组 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 规则组详情 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/waf.RuleGroupView' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 记录不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取 WAF 规则组详情 + tags: + - openflare-waf + /api/v1/custom/openflare/waf/rule-groups/{id}/delete: + post: + description: 按 ID 删除 WAF 规则组,需要管理员权限 + parameters: + - description: 规则组 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + $ref: '#/definitions/response.Any' + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 记录不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 删除 WAF 规则组 + tags: + - openflare-waf + /api/v1/custom/openflare/waf/rule-groups/{id}/sites: + post: + consumes: + - application/json + description: 替换 WAF 规则组关联的代理站点列表,需要管理员权限 + parameters: + - description: 规则组 ID + in: path + name: id + required: true + type: integer + - description: 站点 ID 列表 + in: body + name: request + required: true + schema: + $ref: '#/definitions/waf.IDsRequest' + produces: + - application/json + responses: + "200": + description: 更新后的规则组 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/waf.RuleGroupView' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 记录不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 替换规则组站点绑定 + tags: + - openflare-waf + /api/v1/custom/openflare/waf/rule-groups/{id}/update: + post: + consumes: + - application/json + description: 按 ID 更新 WAF 规则组,需要管理员权限 + parameters: + - description: 规则组 ID + in: path + name: id + required: true + type: integer + - description: 规则组参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/waf.RuleGroupInput' + produces: + - application/json + responses: + "200": + description: 更新后的规则组 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/waf.RuleGroupView' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 记录不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 更新 WAF 规则组 + tags: + - openflare-waf + /api/v1/custom/openflare/waf/sites/{route_id}/rule-groups: + get: + description: 返回代理站点关联的 WAF 规则组绑定,需要管理员权限 + parameters: + - description: 代理路由 ID + in: path + name: route_id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 站点规则组绑定 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/waf.SiteRuleGroupsView' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 记录不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取站点 WAF 规则组 + tags: + - openflare-waf + post: + consumes: + - application/json + description: 替换代理站点关联的 WAF 规则组列表,需要管理员权限 + parameters: + - description: 代理路由 ID + in: path + name: route_id + required: true + type: integer + - description: 规则组 ID 列表 + in: body + name: request + required: true + schema: + $ref: '#/definitions/waf.IDsRequest' + produces: + - application/json + responses: + "200": + description: 更新后的站点规则组绑定 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/waf.SiteRuleGroupsView' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 记录不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 替换站点 WAF 规则组 + tags: + - openflare-waf /api/v1/oauth/{source}/authorize: get: description: 根据指定认证源名称发起 OAuth 授权,支持 purpose 参数用于区分登录和账号绑定场景。认证源必须已启用。 @@ -3959,6 +10162,7 @@ paths: description: 解除当前登录用户与指定外部帐号的绑定关系,需要登录 parameters: - description: 外部帐号绑定记录 ID + format: int64 in: path name: id required: true diff --git a/Wavelet/frontend/app/(main)/origins/detail/page-client.tsx b/Wavelet/frontend/app/(main)/origins/detail/page-client.tsx index eae72904..435a7d91 100644 --- a/Wavelet/frontend/app/(main)/origins/detail/page-client.tsx +++ b/Wavelet/frontend/app/(main)/origins/detail/page-client.tsx @@ -40,12 +40,12 @@ export function OriginDetailPageClient() { const originQuery = useQuery({ queryKey: ["openflare", "origins", originId], - queryFn: () => OriginService.get(parsedId), + queryFn: () => OriginService.getById(parsedId), enabled, }) const deleteMutation = useMutation({ - mutationFn: () => OriginService.delete(parsedId), + mutationFn: () => OriginService.deleteById(parsedId), onSuccess: async () => { toast.success("源站已删除") await queryClient.invalidateQueries({ queryKey: ["openflare", "origins"] }) diff --git a/Wavelet/frontend/app/(main)/origins/page.tsx b/Wavelet/frontend/app/(main)/origins/page.tsx index 304717fa..c4a21878 100644 --- a/Wavelet/frontend/app/(main)/origins/page.tsx +++ b/Wavelet/frontend/app/(main)/origins/page.tsx @@ -42,7 +42,7 @@ export default function OriginsPage() { const origins = useMemo(() => originsQuery.data ?? [], [originsQuery.data]) const deleteMutation = useMutation({ - mutationFn: (id: number) => OriginService.delete(id), + mutationFn: (id: number) => OriginService.deleteById(id), onSuccess: async () => { toast.success("源站已删除") setDeleteTarget(null) diff --git a/Wavelet/frontend/app/(main)/proxy-routes/page-client.tsx b/Wavelet/frontend/app/(main)/proxy-routes/page-client.tsx index 9560d241..64f0af6a 100644 --- a/Wavelet/frontend/app/(main)/proxy-routes/page-client.tsx +++ b/Wavelet/frontend/app/(main)/proxy-routes/page-client.tsx @@ -83,7 +83,7 @@ export function ProxyRoutesPageClient() { setDeleting(true); try { - await ProxyRouteService.delete(deleteTarget.id); + await ProxyRouteService.deleteById(deleteTarget.id); toast.success('网站已删除'); setDeleteTarget(null); await fetchRoutes(); diff --git a/Wavelet/frontend/app/(main)/websites/certificates/page.tsx b/Wavelet/frontend/app/(main)/websites/certificates/page.tsx index 124bc13a..d380500e 100644 --- a/Wavelet/frontend/app/(main)/websites/certificates/page.tsx +++ b/Wavelet/frontend/app/(main)/websites/certificates/page.tsx @@ -52,7 +52,7 @@ export default function CertificatesPage() { }); const deleteMutation = useMutation({ - mutationFn: (id: number) => TlsCertificateService.delete(id), + mutationFn: (id: number) => TlsCertificateService.deleteById(id), onSuccess: async () => { toast.success('证书已删除'); setDeleteTarget(null); diff --git a/Wavelet/frontend/app/(main)/websites/detail/page-client.tsx b/Wavelet/frontend/app/(main)/websites/detail/page-client.tsx index f6a66624..4f2a3661 100644 --- a/Wavelet/frontend/app/(main)/websites/detail/page-client.tsx +++ b/Wavelet/frontend/app/(main)/websites/detail/page-client.tsx @@ -102,7 +102,7 @@ export function WebsiteDetailPageClient() { }, [routesQuery.data, website]); const deleteDomainMutation = useMutation({ - mutationFn: (id: number) => WebsiteService.delete(id), + mutationFn: (id: number) => WebsiteService.deleteById(id), onSuccess: async () => { await queryClient.invalidateQueries({queryKey: domainsQueryKey}); router.push('/websites'); @@ -111,7 +111,7 @@ export function WebsiteDetailPageClient() { }); const deleteCertificateMutation = useMutation({ - mutationFn: (id: number) => TlsCertificateService.delete(id), + mutationFn: (id: number) => TlsCertificateService.deleteById(id), onSuccess: async () => { toast.success('证书已删除'); setDeleteCertOpen(false); diff --git a/Wavelet/frontend/app/(main)/websites/dns-accounts/page.tsx b/Wavelet/frontend/app/(main)/websites/dns-accounts/page.tsx index 7457b24a..2df023f1 100644 --- a/Wavelet/frontend/app/(main)/websites/dns-accounts/page.tsx +++ b/Wavelet/frontend/app/(main)/websites/dns-accounts/page.tsx @@ -40,7 +40,7 @@ export default function DnsAccountsPage() { }); const deleteMutation = useMutation({ - mutationFn: (id: number) => DnsAccountService.delete(id), + mutationFn: (id: number) => DnsAccountService.deleteById(id), onSuccess: async () => { toast.success('DNS 账号已删除'); setDeleteTarget(null); diff --git a/Wavelet/frontend/app/(main)/websites/page.tsx b/Wavelet/frontend/app/(main)/websites/page.tsx index 038895fd..d6d6de78 100644 --- a/Wavelet/frontend/app/(main)/websites/page.tsx +++ b/Wavelet/frontend/app/(main)/websites/page.tsx @@ -50,7 +50,7 @@ export default function WebsitesPage() { }); const deleteMutation = useMutation({ - mutationFn: (id: number) => WebsiteService.delete(id), + mutationFn: (id: number) => WebsiteService.deleteById(id), onSuccess: async () => { toast.success('网站已删除'); setDeleteTarget(null); diff --git a/Wavelet/frontend/components/auth/login-form.tsx b/Wavelet/frontend/components/auth/login-form.tsx index 97784f2a..50d456b7 100644 --- a/Wavelet/frontend/components/auth/login-form.tsx +++ b/Wavelet/frontend/components/auth/login-form.tsx @@ -73,7 +73,7 @@ export function LoginForm({ onOTPStateChange }: { onOTPStateChange?: (show: bool queryFn: () => AuthService.getAuthSources(), }) - const capEnabled = configBool(publicConfigQuery.data?.cap_login_enabled, false) + const capEnabled = configBool(publicConfigQuery.data?.cap_login_enabled, true) const capAutoSolve = configBool(publicConfigQuery.data?.cap_auto_solve, true) const loginMutation = useMutation({ @@ -190,8 +190,8 @@ export function LoginForm({ onOTPStateChange }: { onOTPStateChange?: (show: bool } const registrationEnabled = - configBool(publicConfigQuery.data?.registration_enabled, true) && - configBool(publicConfigQuery.data?.password_register_enabled, true) + configBool(publicConfigQuery.data?.registration_enabled, false) && + configBool(publicConfigQuery.data?.password_register_enabled, false) const passwordLoginEnabled = configBool(publicConfigQuery.data?.password_login_enabled, true) const oidcLoginEnabled = configBool(publicConfigQuery.data?.oidc_login_enabled, true) diff --git a/Wavelet/frontend/components/auth/register-form.tsx b/Wavelet/frontend/components/auth/register-form.tsx index 10389a0f..2af38076 100644 --- a/Wavelet/frontend/components/auth/register-form.tsx +++ b/Wavelet/frontend/components/auth/register-form.tsx @@ -63,12 +63,12 @@ export function RegisterForm() { ) const registrationEnabled = - configBool(publicConfigQuery.data?.registration_enabled, true) && - configBool(publicConfigQuery.data?.password_register_enabled, true) + configBool(publicConfigQuery.data?.registration_enabled, false) && + configBool(publicConfigQuery.data?.password_register_enabled, false) const emailRegisterEnabled = configBool(publicConfigQuery.data?.email_register_verification_enabled, false) - const capEnabled = configBool(publicConfigQuery.data?.cap_login_enabled, false) + const capEnabled = configBool(publicConfigQuery.data?.cap_login_enabled, true) const capAutoSolve = configBool(publicConfigQuery.data?.cap_auto_solve, true) const [capScope, setCapScope] = useState<'send_email_code' | 'register'>('send_email_code') diff --git a/Wavelet/frontend/components/common/docs/api.tsx b/Wavelet/frontend/components/common/docs/api.tsx index 1e2c3e2d..919932a5 100644 --- a/Wavelet/frontend/components/common/docs/api.tsx +++ b/Wavelet/frontend/components/common/docs/api.tsx @@ -242,9 +242,10 @@ export const apiSections: PolicySection[] = [ "error_msg": "", "data": { "site_name": "OpenFlare", - "registration_enabled": "true", + "registration_enabled": "false", "password_login_enabled": "true", - "password_register_enabled": "true", + "password_register_enabled": "false", + "cap_login_enabled": "true", "oidc_login_enabled": "true" } }`} diff --git a/Wavelet/frontend/lib/services/index.ts b/Wavelet/frontend/lib/services/index.ts index 851d0c5a..1bc67f3e 100644 --- a/Wavelet/frontend/lib/services/index.ts +++ b/Wavelet/frontend/lib/services/index.ts @@ -184,7 +184,7 @@ export type { PushEvent, PushHistory, PushChannelConfig, ListPushHistoriesReques export { AboutService, - LegacyOpenFlareBaseService, + OpenFlareBaseService, NodeService, ProxyRouteService, ConfigVersionService, @@ -204,7 +204,6 @@ export { } from './openflare'; export type { - LegacyApiResponse, NodeItem, ProxyRouteItem, ProxyRouteConfigSection, diff --git a/Wavelet/frontend/lib/services/openflare/about.service.ts b/Wavelet/frontend/lib/services/openflare/about.service.ts index d927b043..3c11eeac 100644 --- a/Wavelet/frontend/lib/services/openflare/about.service.ts +++ b/Wavelet/frontend/lib/services/openflare/about.service.ts @@ -1,9 +1,7 @@ -import {LegacyOpenFlareBaseService} from './legacy-base.service'; - -export class AboutService extends LegacyOpenFlareBaseService { - protected static override readonly basePath = '/api'; +import {OpenFlareBaseService} from './base.service'; +export class AboutService extends OpenFlareBaseService { static getAboutContent(): Promise { - return this.legacyGet('/about'); + return this.get('/about'); } } \ No newline at end of file diff --git a/Wavelet/frontend/lib/services/openflare/access-log.service.ts b/Wavelet/frontend/lib/services/openflare/access-log.service.ts index d4951f2a..c444f52a 100644 --- a/Wavelet/frontend/lib/services/openflare/access-log.service.ts +++ b/Wavelet/frontend/lib/services/openflare/access-log.service.ts @@ -1,4 +1,4 @@ -import {LegacyOpenFlareBaseService} from './legacy-base.service'; +import {OpenFlareBaseService} from './base.service'; import type { AccessLogCleanupPayload, AccessLogCleanupResult, @@ -25,17 +25,17 @@ function buildSearchParams(filters: object): Record { return params; } -export class AccessLogService extends LegacyOpenFlareBaseService { - protected static override readonly basePath = '/api/access-logs'; +export class AccessLogService extends OpenFlareBaseService { + protected static override readonly basePath: string = '/api/v1/custom/openflare/access-logs'; static list(filters: AccessLogFilters = {}): Promise { - return this.legacyGet('/', buildSearchParams(filters)); + return this.get('/', buildSearchParams(filters)); } static listFolds( filters: FoldedAccessLogFilters, ): Promise { - return this.legacyGet( + return this.get( '/folds', buildSearchParams(filters), ); @@ -44,7 +44,7 @@ export class AccessLogService extends LegacyOpenFlareBaseService { static listFoldIPs( filters: FoldedAccessLogIPFilters, ): Promise { - return this.legacyGet( + return this.get( '/folds/ip-summary', buildSearchParams(filters), ); @@ -53,7 +53,7 @@ export class AccessLogService extends LegacyOpenFlareBaseService { static listIPSummaries( filters: AccessLogIPSummaryFilters = {}, ): Promise { - return this.legacyGet( + return this.get( '/ip-summary', buildSearchParams(filters), ); @@ -62,7 +62,7 @@ export class AccessLogService extends LegacyOpenFlareBaseService { static getIPTrend( filters: AccessLogIPTrendFilters, ): Promise { - return this.legacyGet( + return this.get( '/ip-summary/trend', buildSearchParams(filters), ); @@ -71,6 +71,6 @@ export class AccessLogService extends LegacyOpenFlareBaseService { static cleanup( payload: AccessLogCleanupPayload, ): Promise { - return this.legacyPost('/cleanup', payload); + return this.post('/cleanup', payload); } -} +} \ No newline at end of file diff --git a/Wavelet/frontend/lib/services/openflare/apply-log.service.ts b/Wavelet/frontend/lib/services/openflare/apply-log.service.ts index d4c00e36..8502a045 100644 --- a/Wavelet/frontend/lib/services/openflare/apply-log.service.ts +++ b/Wavelet/frontend/lib/services/openflare/apply-log.service.ts @@ -1,8 +1,13 @@ -import {LegacyOpenFlareBaseService} from './legacy-base.service'; -import type {ApplyLogCleanupPayload, ApplyLogCleanupResult, ApplyLogList, ApplyLogListQuery,} from './types'; +import {OpenFlareBaseService} from './base.service'; +import type { + ApplyLogCleanupPayload, + ApplyLogCleanupResult, + ApplyLogList, + ApplyLogListQuery, +} from './types'; -export class ApplyLogService extends LegacyOpenFlareBaseService { - protected static override readonly basePath = '/api/apply-logs'; +export class ApplyLogService extends OpenFlareBaseService { + protected static override readonly basePath: string = '/api/v1/custom/openflare/apply-logs'; static list(query: ApplyLogListQuery = {}): Promise { const params: Record = {}; @@ -18,10 +23,10 @@ export class ApplyLogService extends LegacyOpenFlareBaseService { params.pageSize = query.pageSize; } - return this.legacyGet('/', params); + return this.get('/', params); } static cleanup(payload: ApplyLogCleanupPayload): Promise { - return this.legacyPost('/cleanup', payload); + return this.post('/cleanup', payload); } -} +} \ No newline at end of file diff --git a/Wavelet/frontend/lib/services/openflare/base.service.ts b/Wavelet/frontend/lib/services/openflare/base.service.ts new file mode 100644 index 00000000..50ac648c --- /dev/null +++ b/Wavelet/frontend/lib/services/openflare/base.service.ts @@ -0,0 +1,5 @@ +import {BaseService} from '@/lib/services/core'; + +export class OpenFlareBaseService extends BaseService { + protected static override readonly basePath: string = '/api/v1/custom/openflare'; +} \ No newline at end of file diff --git a/Wavelet/frontend/lib/services/openflare/config-version.service.ts b/Wavelet/frontend/lib/services/openflare/config-version.service.ts index 89bf3922..a870dec0 100644 --- a/Wavelet/frontend/lib/services/openflare/config-version.service.ts +++ b/Wavelet/frontend/lib/services/openflare/config-version.service.ts @@ -1,6 +1,6 @@ import type {InternalAxiosRequestConfig} from 'axios'; -import {LegacyOpenFlareBaseService} from './legacy-base.service'; +import {OpenFlareBaseService} from './base.service'; import type { ConfigDiffResult, ConfigPreviewResult, @@ -10,31 +10,31 @@ import type { ConfigVersionSummary, } from './types'; -export class ConfigVersionService extends LegacyOpenFlareBaseService { - protected static override readonly basePath = '/api/config-versions'; +export class ConfigVersionService extends OpenFlareBaseService { + protected static override readonly basePath: string = '/api/v1/custom/openflare/config-versions'; static list(): Promise { - return this.legacyGet('/'); + return this.get('/'); } static getActive(): Promise { - return this.legacyGet('/active'); + return this.get('/active'); } static preview(): Promise { - return this.legacyGet('/preview'); + return this.get('/preview'); } static diff(): Promise { - return this.legacyGet('/diff'); + return this.get('/diff'); } static getById(id: number): Promise { - return this.legacyGet(`/${id}`); + return this.get(`/${id}`); } static publish(force?: boolean): Promise { - return this.legacyPost( + return this.post( '/publish', undefined, force @@ -44,12 +44,12 @@ export class ConfigVersionService extends LegacyOpenFlareBaseService { } static activate(id: number): Promise { - return this.legacyPost(`/${id}/activate`); + return this.post(`/${id}/activate`); } static cleanup( payload: ConfigVersionCleanupPayload, ): Promise { - return this.legacyPost('/cleanup', payload); + return this.post('/cleanup', payload); } -} +} \ No newline at end of file diff --git a/Wavelet/frontend/lib/services/openflare/dashboard.service.ts b/Wavelet/frontend/lib/services/openflare/dashboard.service.ts index a40ceb73..92399354 100644 --- a/Wavelet/frontend/lib/services/openflare/dashboard.service.ts +++ b/Wavelet/frontend/lib/services/openflare/dashboard.service.ts @@ -1,4 +1,4 @@ -import {LegacyOpenFlareBaseService} from './legacy-base.service'; +import {OpenFlareBaseService} from './base.service'; import type { CompactCapacityTrendPoint, CompactDashboardNodeHealth, @@ -230,11 +230,11 @@ function normalizeDashboardOverview( }; } -export class DashboardService extends LegacyOpenFlareBaseService { - protected static readonly basePath = '/api/dashboard'; +export class DashboardService extends OpenFlareBaseService { + protected static override readonly basePath: string = '/api/v1/custom/openflare/dashboard'; static async getOverview(): Promise { - const overview = await this.legacyGet< + const overview = await this.get< DashboardOverview | DashboardOverviewCompact >('/overview'); return normalizeDashboardOverview(overview); diff --git a/Wavelet/frontend/lib/services/openflare/dns-account.service.ts b/Wavelet/frontend/lib/services/openflare/dns-account.service.ts index d1c4a70d..b1c607aa 100644 --- a/Wavelet/frontend/lib/services/openflare/dns-account.service.ts +++ b/Wavelet/frontend/lib/services/openflare/dns-account.service.ts @@ -1,27 +1,27 @@ -import {LegacyOpenFlareBaseService} from './legacy-base.service'; +import {OpenFlareBaseService} from './base.service'; import type {DnsAccountItem, DnsAccountMutationPayload} from './types'; -export class DnsAccountService extends LegacyOpenFlareBaseService { - protected static readonly basePath = '/api/dns-accounts'; +export class DnsAccountService extends OpenFlareBaseService { + protected static override readonly basePath: string = '/api/v1/custom/openflare/dns-accounts'; static async list(): Promise { - return this.legacyGet('/'); + return this.get('/'); } static async create( payload: DnsAccountMutationPayload, ): Promise { - return this.legacyPost('/', payload); + return this.post('/', payload); } static async update( id: number, payload: DnsAccountMutationPayload, ): Promise { - return this.legacyPost(`/${id}/update`, payload); + return this.post(`/${id}/update`, payload); } - static async delete(id: number): Promise { - return this.legacyPost(`/${id}/delete`); + static async deleteById(id: number): Promise { + return this.post(`/${id}/delete`); } } \ No newline at end of file diff --git a/Wavelet/frontend/lib/services/openflare/index.ts b/Wavelet/frontend/lib/services/openflare/index.ts index f54f2f86..71e8bef5 100644 --- a/Wavelet/frontend/lib/services/openflare/index.ts +++ b/Wavelet/frontend/lib/services/openflare/index.ts @@ -1,6 +1,5 @@ export { AboutService } from './about.service'; -export { LegacyOpenFlareBaseService } from './legacy-base.service'; -export type { LegacyApiResponse } from './legacy-base.service'; +export { OpenFlareBaseService } from './base.service'; export { NodeService } from './node.service'; export { ProxyRouteService } from './proxy-route.service'; diff --git a/Wavelet/frontend/lib/services/openflare/legacy-base.service.ts b/Wavelet/frontend/lib/services/openflare/legacy-base.service.ts deleted file mode 100644 index 8b48cacc..00000000 --- a/Wavelet/frontend/lib/services/openflare/legacy-base.service.ts +++ /dev/null @@ -1,58 +0,0 @@ -import apiClient from '@/lib/services/core/api-client'; -import {ApiErrorBase} from '@/lib/services/core/errors'; -import type {InternalAxiosRequestConfig} from 'axios'; - -export interface LegacyApiResponse { - success: boolean; - message: string; - data: T; -} - -/** - * OpenFlare legacy 业务 API 基类 - * 解析 { success, message, data } 响应格式 - */ -export class LegacyOpenFlareBaseService { - protected static readonly basePath: string = ''; - - protected static getFullPath(path: string): string { - const full = `${this.basePath}${path}`; - // Avoid Next.js 308 (strip slash) <-> Gin 301 (add slash) redirect loops in dev proxy. - if (full.length > 1 && full.endsWith('/')) { - return full.slice(0, -1); - } - return full; - } - - protected static parseLegacyResponse(body: LegacyApiResponse): T { - if (!body.success) { - throw new ApiErrorBase(body.message || '请求失败'); - } - return body.data; - } - - protected static async legacyGet( - path: string, - params?: Record, - config?: InternalAxiosRequestConfig, - ): Promise { - const response = await apiClient.get>( - this.getFullPath(path), - { ...config, params } as InternalAxiosRequestConfig, - ); - return this.parseLegacyResponse(response.data); - } - - protected static async legacyPost( - path: string, - data?: unknown, - config?: InternalAxiosRequestConfig, - ): Promise { - const response = await apiClient.post>( - this.getFullPath(path), - data, - config, - ); - return this.parseLegacyResponse(response.data); - } -} diff --git a/Wavelet/frontend/lib/services/openflare/node.service.ts b/Wavelet/frontend/lib/services/openflare/node.service.ts index 1d4966de..77707f18 100644 --- a/Wavelet/frontend/lib/services/openflare/node.service.ts +++ b/Wavelet/frontend/lib/services/openflare/node.service.ts @@ -1,4 +1,4 @@ -import {LegacyOpenFlareBaseService} from './legacy-base.service'; +import {OpenFlareBaseService} from './base.service'; import type { NodeAgentReleaseInfo, NodeAgentUpdatePayload, @@ -9,60 +9,60 @@ import type { ReleaseChannel, } from './types'; -export class NodeService extends LegacyOpenFlareBaseService { - protected static readonly basePath = '/api/nodes'; +export class NodeService extends OpenFlareBaseService { + protected static override readonly basePath: string = '/api/v1/custom/openflare/nodes'; static async listNodes(): Promise { - return this.legacyGet('/'); + return this.get('/'); } static async createNode(payload: NodeMutationPayload): Promise { - return this.legacyPost('/', payload); + return this.post('/', payload); } static async updateNode(id: number, payload: NodeMutationPayload): Promise { - return this.legacyPost(`/${id}/update`, payload); + return this.post(`/${id}/update`, payload); } static async deleteNode(id: number): Promise { - return this.legacyPost(`/${id}/delete`); + return this.post(`/${id}/delete`); } static async getBootstrapToken(): Promise { - return this.legacyGet('/bootstrap-token'); + return this.get('/bootstrap-token'); } static async rotateBootstrapToken(): Promise { - return this.legacyPost('/bootstrap-token/rotate'); + return this.post('/bootstrap-token/rotate'); } static async requestAgentUpdate( id: number, payload?: NodeAgentUpdatePayload, ): Promise { - return this.legacyPost(`/${id}/agent-update`, payload ?? {}); + return this.post(`/${id}/agent-update`, payload ?? {}); } static async requestForceSync(id: number): Promise { - return this.legacyPost(`/${id}/force-sync`); + return this.post(`/${id}/force-sync`); } static async requestOpenrestyRestart(id: number): Promise { - return this.legacyPost(`/${id}/openresty-restart`); + return this.post(`/${id}/openresty-restart`); } static async getAgentRelease( id: number, channel: ReleaseChannel = 'stable', ): Promise { - return this.legacyGet(`/${id}/agent-release`, { channel }); + return this.get(`/${id}/agent-release`, { channel }); } static async getObservability( id: number, options?: { hours?: number; limit?: number }, ): Promise { - return this.legacyGet(`/${id}/observability`, { + return this.get(`/${id}/observability`, { hours: options?.hours, limit: options?.limit, }); @@ -71,8 +71,8 @@ export class NodeService extends LegacyOpenFlareBaseService { static async cleanupHealthEvents( id: number, ): Promise<{ node_id: string; deleted_count: number }> { - return this.legacyPost<{ node_id: string; deleted_count: number }>( + return this.post<{ node_id: string; deleted_count: number }>( `/${id}/observability/cleanup`, ); } -} +} \ No newline at end of file diff --git a/Wavelet/frontend/lib/services/openflare/option.service.ts b/Wavelet/frontend/lib/services/openflare/option.service.ts index 9a4a2002..c10fa006 100644 --- a/Wavelet/frontend/lib/services/openflare/option.service.ts +++ b/Wavelet/frontend/lib/services/openflare/option.service.ts @@ -1,4 +1,4 @@ -import {LegacyOpenFlareBaseService} from './legacy-base.service'; +import {OpenFlareBaseService} from './base.service'; import type { DatabaseCleanupPayload, DatabaseCleanupResult, @@ -7,29 +7,29 @@ import type { OptionItem, } from './types'; -export class OptionService extends LegacyOpenFlareBaseService { - protected static override readonly basePath = '/api/option'; +export class OptionService extends OpenFlareBaseService { + protected static override readonly basePath: string = '/api/v1/custom/openflare/option'; static list(): Promise { - return this.legacyGet('/'); + return this.get('/'); } static update(key: string, value: string): Promise { - return this.legacyPost('/update', { key, value }); + return this.post('/update', { key, value }); } static updateBatch(options: OptionItem[]): Promise { const payload: OptionBatchPayload = { options }; - return this.legacyPost('/update-batch', payload); + return this.post('/update-batch', payload); } static lookupGeoIP(provider: string, ip: string): Promise { - return this.legacyPost('/geoip/lookup', { provider, ip }); + return this.post('/geoip/lookup', { provider, ip }); } static cleanupDatabase( payload: DatabaseCleanupPayload, ): Promise { - return this.legacyPost('/database/cleanup', payload); + return this.post('/database/cleanup', payload); } -} +} \ No newline at end of file diff --git a/Wavelet/frontend/lib/services/openflare/origin.service.ts b/Wavelet/frontend/lib/services/openflare/origin.service.ts index 612c6e92..f460b89a 100644 --- a/Wavelet/frontend/lib/services/openflare/origin.service.ts +++ b/Wavelet/frontend/lib/services/openflare/origin.service.ts @@ -1,29 +1,29 @@ -import {LegacyOpenFlareBaseService} from './legacy-base.service'; -import type {OriginDetail, OriginItem, OriginMutationPayload,} from './types'; +import {OpenFlareBaseService} from './base.service'; +import type {OriginDetail, OriginItem, OriginMutationPayload} from './types'; -export class OriginService extends LegacyOpenFlareBaseService { - protected static override readonly basePath = '/api/origins'; +export class OriginService extends OpenFlareBaseService { + protected static override readonly basePath: string = '/api/v1/custom/openflare/origins'; static list(): Promise { - return this.legacyGet('/'); + return this.get('/'); } - static get(id: number): Promise { - return this.legacyGet(`/${id}`); + static getById(id: number): Promise { + return this.get(`/${id}`); } static create(payload: OriginMutationPayload): Promise { - return this.legacyPost('/', payload); + return this.post('/', payload); } static update( id: number, payload: OriginMutationPayload, ): Promise { - return this.legacyPost(`/${id}/update`, payload); + return this.post(`/${id}/update`, payload); } - static delete(id: number): Promise { - return this.legacyPost(`/${id}/delete`); + static deleteById(id: number): Promise { + return this.post(`/${id}/delete`); } -} +} \ No newline at end of file diff --git a/Wavelet/frontend/lib/services/openflare/pages.service.ts b/Wavelet/frontend/lib/services/openflare/pages.service.ts index f95371f7..99ea1da7 100644 --- a/Wavelet/frontend/lib/services/openflare/pages.service.ts +++ b/Wavelet/frontend/lib/services/openflare/pages.service.ts @@ -1,11 +1,10 @@ import type {AxiosProgressEvent, InternalAxiosRequestConfig} from 'axios'; import apiClient from '@/lib/services/core/api-client'; -import {ApiErrorBase} from '@/lib/services/core/errors'; +import type {ApiResponse} from '@/lib/services/core'; -import {LegacyOpenFlareBaseService} from './legacy-base.service'; +import {OpenFlareBaseService} from './base.service'; import type { - LegacyApiResponse, PagesDeployment, PagesDeploymentFile, PagesDeploymentUploadPayload, @@ -13,41 +12,41 @@ import type { PagesProjectPayload, } from './types'; -export class PagesService extends LegacyOpenFlareBaseService { - protected static override readonly basePath = '/api/pages'; +export class PagesService extends OpenFlareBaseService { + protected static override readonly basePath: string = '/api/v1/custom/openflare/pages'; static listProjects(): Promise { - return this.legacyGet('/'); + return this.get('/'); } static getProject(id: number): Promise { - return this.legacyGet(`/${id}`); + return this.get(`/${id}`); } static createProject(payload: PagesProjectPayload): Promise { - return this.legacyPost('/', payload); + return this.post('/', payload); } static updateProject( id: number, payload: PagesProjectPayload, ): Promise { - return this.legacyPost(`/${id}/update`, payload); + return this.post(`/${id}/update`, payload); } static deleteProject(id: number): Promise { - return this.legacyPost(`/${id}/delete`); + return this.post(`/${id}/delete`); } static listDeployments(projectId: number): Promise { - return this.legacyGet(`/${projectId}/deployments`); + return this.get(`/${projectId}/deployments`); } static listDeploymentFiles( projectId: number, deploymentId: number, ): Promise { - return this.legacyGet( + return this.get( `/${projectId}/deployments/${deploymentId}/files`, ); } @@ -72,7 +71,7 @@ export class PagesService extends LegacyOpenFlareBaseService { projectId: number, deploymentId: number, ): Promise { - return this.legacyPost( + return this.post( `/${projectId}/deployments/${deploymentId}/activate`, ); } @@ -81,7 +80,7 @@ export class PagesService extends LegacyOpenFlareBaseService { projectId: number, deploymentId: number, ): Promise { - return this.legacyPost( + return this.post( `/${projectId}/deployments/${deploymentId}/delete`, ); } @@ -91,7 +90,7 @@ export class PagesService extends LegacyOpenFlareBaseService { formData: FormData, onProgress?: (percent: number) => void, ): Promise { - const response = await apiClient.post>( + const response = await apiClient.post>( this.getFullPath(path), formData, { @@ -104,10 +103,6 @@ export class PagesService extends LegacyOpenFlareBaseService { } as InternalAxiosRequestConfig, ); - if (!response.data.success) { - throw new ApiErrorBase(response.data.message || '请求失败'); - } - return response.data.data; } -} +} \ No newline at end of file diff --git a/Wavelet/frontend/lib/services/openflare/proxy-route.service.ts b/Wavelet/frontend/lib/services/openflare/proxy-route.service.ts index b97ee023..9898992d 100644 --- a/Wavelet/frontend/lib/services/openflare/proxy-route.service.ts +++ b/Wavelet/frontend/lib/services/openflare/proxy-route.service.ts @@ -1,29 +1,29 @@ -import {LegacyOpenFlareBaseService} from './legacy-base.service'; +import {OpenFlareBaseService} from './base.service'; import type {ProxyRouteItem, ProxyRouteMutationPayload} from './types'; -export class ProxyRouteService extends LegacyOpenFlareBaseService { - protected static readonly basePath = '/api/proxy-routes'; +export class ProxyRouteService extends OpenFlareBaseService { + protected static override readonly basePath: string = '/api/v1/custom/openflare/proxy-routes'; static async list(): Promise { - return this.legacyGet('/'); + return this.get('/'); } static async getById(id: number): Promise { - return this.legacyGet(`/${id}`); + return this.get(`/${id}`); } static async create(payload: ProxyRouteMutationPayload): Promise { - return this.legacyPost('/', payload); + return this.post('/', payload); } static async update( id: number, payload: ProxyRouteMutationPayload, ): Promise { - return this.legacyPost(`/${id}/update`, payload); + return this.post(`/${id}/update`, payload); } - static async delete(id: number): Promise { - return this.legacyPost(`/${id}/delete`); + static async deleteById(id: number): Promise { + return this.post(`/${id}/delete`); } -} +} \ No newline at end of file diff --git a/Wavelet/frontend/lib/services/openflare/status.service.ts b/Wavelet/frontend/lib/services/openflare/status.service.ts index 75cd894a..815855a5 100644 --- a/Wavelet/frontend/lib/services/openflare/status.service.ts +++ b/Wavelet/frontend/lib/services/openflare/status.service.ts @@ -1,10 +1,8 @@ -import {LegacyOpenFlareBaseService} from './legacy-base.service'; +import {OpenFlareBaseService} from './base.service'; import type {OpenFlarePublicStatus} from './types'; -export class StatusService extends LegacyOpenFlareBaseService { - protected static override readonly basePath = '/api'; - +export class StatusService extends OpenFlareBaseService { static getPublicStatus(): Promise { - return this.legacyGet('/status'); + return this.get('/status'); } -} +} \ No newline at end of file diff --git a/Wavelet/frontend/lib/services/openflare/tls-certificate.service.ts b/Wavelet/frontend/lib/services/openflare/tls-certificate.service.ts index fc2649e0..60646a5e 100644 --- a/Wavelet/frontend/lib/services/openflare/tls-certificate.service.ts +++ b/Wavelet/frontend/lib/services/openflare/tls-certificate.service.ts @@ -1,4 +1,4 @@ -import {LegacyOpenFlareBaseService} from './legacy-base.service'; +import {OpenFlareBaseService} from './base.service'; import type { AcmeAccountItem, TlsCertificateApplyPayload, @@ -9,68 +9,68 @@ import type { TlsCertificateMutationPayload, } from './types'; -class AcmeAccountApi extends LegacyOpenFlareBaseService { - protected static readonly basePath = '/api/acme-accounts'; +class AcmeAccountApi extends OpenFlareBaseService { + protected static override readonly basePath: string = '/api/v1/custom/openflare/acme-accounts'; static getDefault(): Promise { - return this.legacyGet('/default'); + return this.get('/default'); } } -export class TlsCertificateService extends LegacyOpenFlareBaseService { - protected static readonly basePath = '/api/tls-certificates'; +export class TlsCertificateService extends OpenFlareBaseService { + protected static override readonly basePath: string = '/api/v1/custom/openflare/tls-certificates'; static async list(): Promise { - return this.legacyGet('/'); + return this.get('/'); } static async getById(id: number): Promise { - return this.legacyGet(`/${id}`); + return this.get(`/${id}`); } static async getContent(id: number): Promise { - return this.legacyGet(`/${id}/content`); + return this.get(`/${id}/content`); } static async create( payload: TlsCertificateMutationPayload, ): Promise { - return this.legacyPost('/', payload); + return this.post('/', payload); } static async update( id: number, payload: TlsCertificateMutationPayload, ): Promise { - return this.legacyPost(`/${id}/update`, payload); + return this.post(`/${id}/update`, payload); } - static async delete(id: number): Promise { - return this.legacyPost(`/${id}/delete`); + static async deleteById(id: number): Promise { + return this.post(`/${id}/delete`); } static async apply( payload: TlsCertificateApplyPayload, ): Promise { - return this.legacyPost('/apply', payload); + return this.post('/apply', payload); } static async renew(id: number): Promise { - return this.legacyPost(`/${id}/renew`); + return this.post(`/${id}/renew`); } static async updateAcme( id: number, payload: TlsCertificateApplyPayload, ): Promise { - return this.legacyPost(`/${id}/update-acme`, payload); + return this.post(`/${id}/update-acme`, payload); } static async convertToAcme( id: number, payload: TlsCertificateApplyPayload, ): Promise { - return this.legacyPost(`/${id}/convert-acme`, payload); + return this.post(`/${id}/convert-acme`, payload); } static async importFile( @@ -82,7 +82,7 @@ export class TlsCertificateService extends LegacyOpenFlareBaseService { formData.append('cert_file', payload.certFile); formData.append('key_file', payload.keyFile); - return this.legacyPost('/import-file', formData); + return this.post('/import-file', formData); } static getDefaultAcmeAccount(): Promise { diff --git a/Wavelet/frontend/lib/services/openflare/types.ts b/Wavelet/frontend/lib/services/openflare/types.ts index f2abfc6f..b287fc09 100644 --- a/Wavelet/frontend/lib/services/openflare/types.ts +++ b/Wavelet/frontend/lib/services/openflare/types.ts @@ -1,13 +1,3 @@ -/** - * OpenFlare 遗留业务 API 响应信封 - * 阶段一 `/api/*` 端点使用此格式,与 Wavelet `/api/v1/*` 的 `{error_msg,data}` 不同 - */ -export interface LegacyApiResponse { - success: boolean; - message: string; - data: T; -} - export type ReleaseChannel = 'stable' | 'preview'; export type NodeType = 'edge_node' | 'tunnel_relay' | 'tunnel_client'; diff --git a/Wavelet/frontend/lib/services/openflare/update.service.ts b/Wavelet/frontend/lib/services/openflare/update.service.ts index cfa8dfb2..c2c570b9 100644 --- a/Wavelet/frontend/lib/services/openflare/update.service.ts +++ b/Wavelet/frontend/lib/services/openflare/update.service.ts @@ -2,18 +2,18 @@ import type {InternalAxiosRequestConfig} from 'axios'; import {getApiBaseUrl} from '@/lib/services/core/config'; import {ApiErrorBase} from '@/lib/services/core/errors'; +import type {ApiResponse} from '@/lib/services/core'; -import type {LegacyApiResponse} from './legacy-base.service'; -import {LegacyOpenFlareBaseService} from './legacy-base.service'; -import type {LatestReleaseInfo, ReleaseChannel, UpgradeStreamSnapshot, UploadedServerBinaryInfo,} from './types'; +import {OpenFlareBaseService} from './base.service'; +import type {LatestReleaseInfo, ReleaseChannel, UpgradeStreamSnapshot, UploadedServerBinaryInfo} from './types'; /** - * OpenFlare 服务端升级 API(遗留 `/api/update/*`)。 + * OpenFlare 服务端升级 API(`/api/v1/custom/openflare/update/*`)。 */ -export class UpdateService extends LegacyOpenFlareBaseService { - protected static override readonly basePath = '/api/update'; +export class UpdateService extends OpenFlareBaseService { + protected static override readonly basePath: string = '/api/v1/custom/openflare/update'; - private static getLegacyApiUrl(path: string): string { + private static getApiUrl(path: string): string { const baseURL = getApiBaseUrl(); const fullPath = this.getFullPath(path); if (!baseURL) { @@ -23,11 +23,11 @@ export class UpdateService extends LegacyOpenFlareBaseService { } static getLatestRelease(channel: ReleaseChannel = 'stable'): Promise { - return this.legacyGet('/latest-release', { channel }); + return this.get('/latest-release', { channel }); } static upgradeServer(channel: ReleaseChannel = 'stable'): Promise { - return this.legacyPost('/upgrade', { channel }); + return this.post('/upgrade', { channel }); } static uploadServerBinary( @@ -38,14 +38,14 @@ export class UpdateService extends LegacyOpenFlareBaseService { formData.append('binary', binary); if (!onProgress) { - return this.legacyPost('/manual-upload', formData, { + return this.post('/manual-upload', formData, { headers: { 'Content-Type': 'multipart/form-data' }, } as InternalAxiosRequestConfig); } return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); - xhr.open('POST', this.getLegacyApiUrl('/manual-upload')); + xhr.open('POST', this.getApiUrl('/manual-upload')); xhr.withCredentials = true; xhr.upload.addEventListener('progress', (event) => { @@ -55,11 +55,11 @@ export class UpdateService extends LegacyOpenFlareBaseService { }); xhr.addEventListener('load', () => { - let payload: LegacyApiResponse | null = null; + let payload: ApiResponse | null = null; try { payload = JSON.parse( xhr.responseText, - ) as LegacyApiResponse; + ) as ApiResponse; } catch { payload = null; } @@ -77,10 +77,6 @@ export class UpdateService extends LegacyOpenFlareBaseService { reject(new ApiErrorBase('响应格式无效', undefined, xhr.status)); return; } - if (!payload.success) { - reject(new ApiErrorBase(payload.message || '请求失败', undefined, xhr.status)); - return; - } resolve(payload.data); }); @@ -93,7 +89,7 @@ export class UpdateService extends LegacyOpenFlareBaseService { } static confirmManualServerUpgrade(uploadToken: string): Promise { - return this.legacyPost('/manual-upgrade', { + return this.post('/manual-upgrade', { upload_token: uploadToken, }); } @@ -103,7 +99,7 @@ export class UpdateService extends LegacyOpenFlareBaseService { return null; } - const apiUrl = this.getLegacyApiUrl('/logs/ws'); + const apiUrl = this.getApiUrl('/logs/ws'); const resolvedUrl = apiUrl.startsWith('http://') ? `ws://${apiUrl.slice('http://'.length)}` : apiUrl.startsWith('https://') @@ -132,4 +128,4 @@ export class UpdateService extends LegacyOpenFlareBaseService { return null; } } -} +} \ No newline at end of file diff --git a/Wavelet/frontend/lib/services/openflare/uptimekuma.service.ts b/Wavelet/frontend/lib/services/openflare/uptimekuma.service.ts index 3b61a55b..6e9f6ee8 100644 --- a/Wavelet/frontend/lib/services/openflare/uptimekuma.service.ts +++ b/Wavelet/frontend/lib/services/openflare/uptimekuma.service.ts @@ -1,9 +1,9 @@ -import {LegacyOpenFlareBaseService} from './legacy-base.service'; +import {OpenFlareBaseService} from './base.service'; -export class UptimeKumaService extends LegacyOpenFlareBaseService { - protected static override readonly basePath = '/api/uptimekuma'; +export class UptimeKumaService extends OpenFlareBaseService { + protected static override readonly basePath: string = '/api/v1/custom/openflare/uptimekuma'; static sync(): Promise { - return this.legacyPost('/sync'); + return this.post('/sync'); } -} +} \ No newline at end of file diff --git a/Wavelet/frontend/lib/services/openflare/waf.service.ts b/Wavelet/frontend/lib/services/openflare/waf.service.ts index 399b9149..d338790f 100644 --- a/Wavelet/frontend/lib/services/openflare/waf.service.ts +++ b/Wavelet/frontend/lib/services/openflare/waf.service.ts @@ -1,4 +1,4 @@ -import {LegacyOpenFlareBaseService} from './legacy-base.service'; +import {OpenFlareBaseService} from './base.service'; import type { WAFIPGroup, WAFIPGroupAutoTestPayload, @@ -10,76 +10,76 @@ import type { WAFSiteRuleGroups, } from './types'; -export class WafService extends LegacyOpenFlareBaseService { - protected static readonly basePath = '/api/waf'; +export class WafService extends OpenFlareBaseService { + protected static override readonly basePath: string = '/api/v1/custom/openflare/waf'; static async listRuleGroups(): Promise { - return this.legacyGet('/rule-groups'); + return this.get('/rule-groups'); } static async getRuleGroup(id: number): Promise { - return this.legacyGet(`/rule-groups/${id}`); + return this.get(`/rule-groups/${id}`); } static async createRuleGroup(payload: WAFRuleGroupPayload): Promise { - return this.legacyPost('/rule-groups', payload); + return this.post('/rule-groups', payload); } static async updateRuleGroup( id: number, payload: WAFRuleGroupPayload, ): Promise { - return this.legacyPost(`/rule-groups/${id}/update`, payload); + return this.post(`/rule-groups/${id}/update`, payload); } static async deleteRuleGroup(id: number): Promise { - return this.legacyPost(`/rule-groups/${id}/delete`); + return this.post(`/rule-groups/${id}/delete`); } static async updateRuleGroupSites(id: number, ids: number[]): Promise { - return this.legacyPost(`/rule-groups/${id}/sites`, { ids }); + return this.post(`/rule-groups/${id}/sites`, { ids }); } static async listSiteRuleGroups(routeId: number): Promise { - return this.legacyGet(`/sites/${routeId}/rule-groups`); + return this.get(`/sites/${routeId}/rule-groups`); } static async updateSiteRuleGroups( routeId: number, ids: number[], ): Promise { - return this.legacyPost(`/sites/${routeId}/rule-groups`, { + return this.post(`/sites/${routeId}/rule-groups`, { ids, }); } static async listIPGroups(): Promise { - return this.legacyGet('/ip-groups'); + return this.get('/ip-groups'); } static async getIPGroup(id: number): Promise { - return this.legacyGet(`/ip-groups/${id}`); + return this.get(`/ip-groups/${id}`); } static async createIPGroup(payload: WAFIPGroupPayload): Promise { - return this.legacyPost('/ip-groups', payload); + return this.post('/ip-groups', payload); } static async updateIPGroup(id: number, payload: WAFIPGroupPayload): Promise { - return this.legacyPost(`/ip-groups/${id}/update`, payload); + return this.post(`/ip-groups/${id}/update`, payload); } static async deleteIPGroup(id: number): Promise { - return this.legacyPost(`/ip-groups/${id}/delete`); + return this.post(`/ip-groups/${id}/delete`); } static async testIPGroup( payload: WAFIPGroupAutoTestPayload, ): Promise { - return this.legacyPost('/ip-groups/test', payload); + return this.post('/ip-groups/test', payload); } static async syncIPGroup(id: number): Promise { - return this.legacyPost(`/ip-groups/${id}/sync`); + return this.post(`/ip-groups/${id}/sync`); } -} +} \ No newline at end of file diff --git a/Wavelet/frontend/lib/services/openflare/website.service.ts b/Wavelet/frontend/lib/services/openflare/website.service.ts index bbe5ace7..33896b56 100644 --- a/Wavelet/frontend/lib/services/openflare/website.service.ts +++ b/Wavelet/frontend/lib/services/openflare/website.service.ts @@ -1,31 +1,35 @@ -import {LegacyOpenFlareBaseService} from './legacy-base.service'; -import type {ManagedDomainItem, ManagedDomainMatchResult, ManagedDomainMutationPayload,} from './types'; +import {OpenFlareBaseService} from './base.service'; +import type { + ManagedDomainItem, + ManagedDomainMatchResult, + ManagedDomainMutationPayload, +} from './types'; -export class WebsiteService extends LegacyOpenFlareBaseService { - protected static readonly basePath = '/api/managed-domains'; +export class WebsiteService extends OpenFlareBaseService { + protected static override readonly basePath: string = '/api/v1/custom/openflare/managed-domains'; static async list(): Promise { - return this.legacyGet('/'); + return this.get('/'); } static async create( payload: ManagedDomainMutationPayload, ): Promise { - return this.legacyPost('/', payload); + return this.post('/', payload); } static async update( id: number, payload: ManagedDomainMutationPayload, ): Promise { - return this.legacyPost(`/${id}/update`, payload); + return this.post(`/${id}/update`, payload); } - static async delete(id: number): Promise { - return this.legacyPost(`/${id}/delete`); + static async deleteById(id: number): Promise { + return this.post(`/${id}/delete`); } static async match(domain: string): Promise { - return this.legacyGet('/match', {domain}); + return this.get('/match', {domain}); } -} +} \ No newline at end of file diff --git a/Wavelet/internal/apps/cap/runtime_settings_test.go b/Wavelet/internal/apps/cap/runtime_settings_test.go index f04ca5ee..c62b19ee 100644 --- a/Wavelet/internal/apps/cap/runtime_settings_test.go +++ b/Wavelet/internal/apps/cap/runtime_settings_test.go @@ -56,13 +56,13 @@ func TestProtectionEnabledReflectsLoginSwitch(t *testing.T) { ResetRuntimeSettingsForTest() - if ProtectionEnabled(ctx) { - t.Fatal("ProtectionEnabled() = true, want false from seed defaults") + if !ProtectionEnabled(ctx) { + t.Fatal("ProtectionEnabled() = false, want true from seed defaults") } if err := db.DB(ctx).Model(&model.SystemConfig{}). Where("key = ?", model.ConfigKeyCapLoginEnabled). - Update("value", "true").Error; err != nil { + Update("value", "false").Error; err != nil { t.Fatalf("Update(cap_login_enabled) error = %v", err) } if err := repository.InvalidateSystemConfigCache(ctx, model.ConfigKeyCapLoginEnabled); err != nil { @@ -70,8 +70,8 @@ func TestProtectionEnabledReflectsLoginSwitch(t *testing.T) { } InvalidateRuntimeSettings() - if !ProtectionEnabled(ctx) { - t.Fatal("ProtectionEnabled() = false, want true after config update") + if ProtectionEnabled(ctx) { + t.Fatal("ProtectionEnabled() = true, want false after config update") } } diff --git a/Wavelet/internal/apps/openflare/agent/routers.go b/Wavelet/internal/apps/openflare/agent/routers.go index e44498e4..761fbea1 100644 --- a/Wavelet/internal/apps/openflare/agent/routers.go +++ b/Wavelet/internal/apps/openflare/agent/routers.go @@ -13,28 +13,6 @@ import ( "github.com/gin-gonic/gin" ) -// RegisterRoutes mounts agent API routes under /agent. -func RegisterRoutes(apiGroup *gin.RouterGroup) { - agentRoute := apiGroup.Group("/agent") - { - discoveryRoute := agentRoute.Group("/") - discoveryRoute.Use(AgentRegisterAuth()) - { - discoveryRoute.POST("/nodes/register", RegisterHandler) - } - - authorizedRoute := agentRoute.Group("/") - authorizedRoute.Use(AgentAuth()) - { - authorizedRoute.GET("/ws", AgentWebSocketHandler) - authorizedRoute.POST("/nodes/heartbeat", HeartbeatHandler) - authorizedRoute.GET("/config-versions/active", GetActiveConfigHandler) - authorizedRoute.GET("/pages/deployments/:deployment_id/package", DownloadPagesPackageHandler) - authorizedRoute.POST("/waf/ip-groups/sync", SyncWAFIPGroupsHandler) - authorizedRoute.POST("/apply-logs", ReportApplyLogHandler) - } - } -} // RegisterHandler registers or discovers an agent node. func RegisterHandler(c *gin.Context) { diff --git a/Wavelet/internal/apps/openflare/apiutil/bind.go b/Wavelet/internal/apps/openflare/apiutil/bind.go new file mode 100644 index 00000000..554ef4c8 --- /dev/null +++ b/Wavelet/internal/apps/openflare/apiutil/bind.go @@ -0,0 +1,39 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +// Package apiutil provides HTTP helpers for OpenFlare v1 custom API handlers. +package apiutil + +import ( + "strconv" + + "github.com/Rain-kl/Wavelet/internal/common/response" + "github.com/gin-gonic/gin" +) + +const errInvalidParams = "参数错误" +const errInvalidID = "无效的 ID" + +// BindJSON binds JSON body; returns false after aborting with 400. +func BindJSON(c *gin.Context, dst any) bool { + if err := c.ShouldBindJSON(dst); err != nil { + response.AbortBadRequest(c, errInvalidParams) + return false + } + return true +} + +// IDParam parses :id from the URL path. +func IDParam(c *gin.Context) (uint, bool) { + raw := c.Param("id") + if raw == "" { + response.AbortBadRequest(c, errInvalidID) + return 0, false + } + id64, err := strconv.ParseUint(raw, 10, 64) + if err != nil || id64 == 0 { + response.AbortBadRequest(c, errInvalidID) + return 0, false + } + return uint(id64), true +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/apiutil/errors.go b/Wavelet/internal/apps/openflare/apiutil/errors.go new file mode 100644 index 00000000..a260da5c --- /dev/null +++ b/Wavelet/internal/apps/openflare/apiutil/errors.go @@ -0,0 +1,34 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +package apiutil + +import ( + "errors" + + "github.com/Rain-kl/Wavelet/internal/common/response" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// AbortNotFoundIfMissing maps gorm.ErrRecordNotFound to 404; other errors to 400. +func AbortNotFoundIfMissing(c *gin.Context, err error, notFoundMsg string) bool { + if err == nil { + return false + } + if errors.Is(err, gorm.ErrRecordNotFound) { + response.AbortNotFound(c, notFoundMsg) + return true + } + response.AbortBadRequest(c, err.Error()) + return true +} + +// AbortBadRequestOnError writes a 400 for any non-nil error. +func AbortBadRequestOnError(c *gin.Context, err error) bool { + if err == nil { + return false + } + response.AbortBadRequest(c, err.Error()) + return true +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/apiutil/middleware.go b/Wavelet/internal/apps/openflare/apiutil/middleware.go new file mode 100644 index 00000000..62e92d12 --- /dev/null +++ b/Wavelet/internal/apps/openflare/apiutil/middleware.go @@ -0,0 +1,37 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +package apiutil + +import ( + "github.com/Rain-kl/Wavelet/internal/apps/admin" + "github.com/Rain-kl/Wavelet/internal/apps/oauth" + "github.com/Rain-kl/Wavelet/internal/common" + "github.com/Rain-kl/Wavelet/internal/common/response" + "github.com/gin-gonic/gin" +) + +// AdminRequired ensures the caller is logged in as a Wavelet administrator. +func AdminRequired() gin.HandlerFunc { + return func(c *gin.Context) { + user, err := oauth.GetUserFromRequest(c) + if err != nil { + response.AbortUnauthorized(c, common.UnAuthorized) + return + } + oauth.SetToContext(c, oauth.UserObjKey, user) + + if tokenAuth, _ := oauth.GetFromContext[bool](c, oauth.TokenAuthKey); tokenAuth { + tokenAdmin, _ := oauth.GetFromContext[bool](c, oauth.TokenAdminKey) + if !tokenAdmin { + response.AbortNotFound(c, admin.TokenAdminRequired) + return + } + } + if !user.IsAdmin { + response.AbortNotFound(c, admin.AdminRequired) + return + } + c.Next() + } +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/compat/routes.go b/Wavelet/internal/apps/openflare/apiutil/routes.go similarity index 58% rename from Wavelet/internal/apps/openflare/compat/routes.go rename to Wavelet/internal/apps/openflare/apiutil/routes.go index 9a5aa3d5..47d3a011 100644 --- a/Wavelet/internal/apps/openflare/compat/routes.go +++ b/Wavelet/internal/apps/openflare/apiutil/routes.go @@ -1,7 +1,7 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -package compat +package apiutil import ( "strings" @@ -10,14 +10,10 @@ import ( ) // RegisterCollection registers a collection endpoint on both "" and "/" so requests -// work with or without a trailing slash. This avoids 301/308 redirect loops between -// Next.js dev proxy and Gin when paths disagree on trailing slashes. -// -// When the group base already ends with "/" (e.g. userGroup.Group("/")), only "/" is -// registered to avoid duplicate route panic. +// work with or without a trailing slash. func RegisterCollection(route *gin.RouterGroup, method string, handlers ...gin.HandlerFunc) { route.Handle(method, "/", handlers...) if !strings.HasSuffix(route.BasePath(), "/") { route.Handle(method, "", handlers...) } -} +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/apply_log/routers.go b/Wavelet/internal/apps/openflare/apply_log/routers.go index bca22f90..c0c88810 100644 --- a/Wavelet/internal/apps/openflare/apply_log/routers.go +++ b/Wavelet/internal/apps/openflare/apply_log/routers.go @@ -4,39 +4,67 @@ package apply_log import ( + "net/http" "strconv" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" + "github.com/Rain-kl/Wavelet/internal/common/response" "github.com/gin-gonic/gin" ) + // GetApplyLogs lists apply logs with pagination and optional node_id filter. +// @Summary 获取配置下发日志 +// @Description 分页返回节点配置下发记录,支持按节点 ID 筛选,需要管理员权限 +// @Tags openflare-apply-log +// @Produce json +// @Security SessionCookie +// @Param node_id query string false "节点 ID 筛选" +// @Param pageNo query int false "页码" +// @Param page_no query int false "页码(别名)" +// @Param pageSize query int false "每页数量" +// @Param page_size query int false "每页数量(别名)" +// @Success 200 {object} response.Any{data=apply_log.ListResult} "下发日志列表" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或不存在" +// @Router /api/v1/custom/openflare/apply-logs [get] func GetApplyLogs(c *gin.Context) { result, err := ListPage(c.Request.Context(), ListQuery{ NodeID: c.Query("node_id"), PageNo: readIntQuery(c, "pageNo", "page_no"), PageSize: readIntQuery(c, "pageSize", "page_size"), }) - if err != nil { - compat.Fail(c, err.Error()) + if apiutil.AbortBadRequestOnError(c, err) { return } - compat.OK(c, result) + c.JSON(http.StatusOK, response.OK(result)) } // CleanupApplyLogs removes old apply logs or deletes all records. +// @Summary 清理配置下发日志 +// @Description 按保留天数清理历史下发记录,或删除全部记录,需要管理员权限 +// @Tags openflare-apply-log +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param body body apply_log.CleanupInput true "清理参数" +// @Success 200 {object} response.Any{data=apply_log.CleanupResult} "清理结果" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或不存在" +// @Router /api/v1/custom/openflare/apply-logs/cleanup [post] func CleanupApplyLogs(c *gin.Context) { var input CleanupInput - if !compat.BindJSON(c, &input) { + if !apiutil.BindJSON(c, &input) { return } result, err := Cleanup(c.Request.Context(), input) - if err != nil { - compat.Fail(c, err.Error()) + if apiutil.AbortBadRequestOnError(c, err) { return } - compat.OK(c, result) + c.JSON(http.StatusOK, response.OK(result)) } func readIntQuery(c *gin.Context, primary, secondary string) int { @@ -46,4 +74,4 @@ func readIntQuery(c *gin.Context, primary, secondary string) int { } parsed, _ := strconv.Atoi(value) return parsed -} +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/auth/errs.go b/Wavelet/internal/apps/openflare/auth/errs.go deleted file mode 100644 index 5f431144..00000000 --- a/Wavelet/internal/apps/openflare/auth/errs.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package auth - -const ( - errInvalidParams = "无效的参数" - errUnauthorized = "无权进行此操作,未登录或 token 无效" - errPasswordLoginDisabled = "管理员关闭了密码登录" - errUsernameOrPasswordWrong = "用户名或密码错误" - errBannedAccount = "用户已被封禁" - errSaveSessionFailed = "无法保存会话信息,请重试" - errRegistrationDisabled = "管理员关闭了注册" - errPasswordTooShort = "密码长度不能少于 8 位" - errEmailRequired = "邮箱地址不能为空" - errEmailAlreadyRegistered = "邮箱地址已被占用" - errEmailNotRegistered = "该邮箱地址未注册" - errEmailCodeInvalid = "验证码错误或已过期" - errResetLinkInvalid = "重置链接非法或已过期" - errUserNotFound = "用户不存在" - errGenerateTokenFailed = "生成 Token 失败" - errInsufficientPermission = "无权进行此操作,权限不足" - errCannotDisableRoot = "无法禁用超级管理员用户" - errCannotDeleteRoot = "无法删除超级管理员用户" - errCannotPromoteAdmin = "普通管理员用户无法提升其他用户为管理员" - errAlreadyAdmin = "该用户已经是管理员" - errAlreadyCommonUser = "该用户已经是普通用户" - errAuthSourceDisabled = "认证源未启用" - errInvalidAuthSourceID = "认证源 ID 无效" - errPendingOAuthExpired = "待绑定第三方账号已失效,请重新登录" - errPendingOAuthInvalid = "待绑定第三方账号无效,请重新登录" - errCapTokenMissing = "缺少人机验证凭证" - errCapTokenInvalid = "人机验证凭证无效或已过期" -) diff --git a/Wavelet/internal/apps/openflare/auth/legacy_user.go b/Wavelet/internal/apps/openflare/auth/legacy_user.go deleted file mode 100644 index cb092fce..00000000 --- a/Wavelet/internal/apps/openflare/auth/legacy_user.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -// Package auth provides OpenFlare legacy auth business logic. -package auth - -import ( - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" - "github.com/Rain-kl/Wavelet/internal/model" -) - -// LegacyUser mirrors the old OpenFlare frontend user shape. -type LegacyUser struct { - ID int `json:"id"` - Username string `json:"username"` - DisplayName string `json:"display_name"` - Role int `json:"role"` - Status int `json:"status"` - Token string `json:"token,omitempty"` - Email string `json:"email,omitempty"` -} - -const ( - legacyUserStatusEnabled = 1 - legacyUserStatusDisabled = 2 -) - -// RoleFromUser maps a Wavelet user to the legacy role value. -func RoleFromUser(user *model.User) int { - if user == nil { - return 0 - } - if user.IsAdmin { - return compat.RoleRootUser - } - return compat.RoleCommonUser -} - -// StatusFromUser maps is_active to legacy status. -func StatusFromUser(user *model.User) int { - if user == nil || !user.IsActive { - return legacyUserStatusDisabled - } - return legacyUserStatusEnabled -} - -// ToLegacyUser converts a Wavelet user to the legacy response shape. -func ToLegacyUser(user *model.User, token string) LegacyUser { - if user == nil { - return LegacyUser{} - } - return LegacyUser{ - ID: int(user.ID), - Username: user.Username, - DisplayName: displayName(user), - Role: RoleFromUser(user), - Status: StatusFromUser(user), - Token: token, - Email: user.Email, - } -} - -// ToLegacyUsers converts a slice of users. -func ToLegacyUsers(users []model.User) []LegacyUser { - result := make([]LegacyUser, 0, len(users)) - for i := range users { - result = append(result, ToLegacyUser(&users[i], "")) - } - return result -} - -func displayName(user *model.User) string { - if user.Nickname != "" { - return user.Nickname - } - return user.Username -} - -// IsAdminRole reports whether a legacy role has admin privileges. -func IsAdminRole(role int) bool { - return role >= compat.RoleAdminUser -} diff --git a/Wavelet/internal/apps/openflare/auth/logics.go b/Wavelet/internal/apps/openflare/auth/logics.go deleted file mode 100644 index 2c49df3e..00000000 --- a/Wavelet/internal/apps/openflare/auth/logics.go +++ /dev/null @@ -1,801 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package auth - -import ( - "context" - "crypto/rand" - "encoding/json" - "errors" - "fmt" - "math/big" - "strings" - "time" - - "github.com/Rain-kl/Wavelet/internal/apps/oauth" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" - "github.com/Rain-kl/Wavelet/internal/buildinfo" - "github.com/Rain-kl/Wavelet/internal/config" - "github.com/Rain-kl/Wavelet/internal/db" - "github.com/Rain-kl/Wavelet/internal/db/idgen" - "github.com/Rain-kl/Wavelet/internal/listener" - "github.com/Rain-kl/Wavelet/internal/model" - "github.com/Rain-kl/Wavelet/internal/repository" - "github.com/Rain-kl/Wavelet/internal/task" - "github.com/Rain-kl/Wavelet/pkg/logger" - "github.com/gin-contrib/sessions" - "github.com/gin-gonic/gin" - "gorm.io/gorm" -) - -const ( - legacyTokenName = "openflare-legacy" - minPasswordLength = 8 - legacyItemsPerPage = 10 - passwordResetKeyFmt = "of_password_reset:%s" - passwordResetExpiry = 15 * time.Minute - verificationCodeRange = 900000 - verificationOffset = 100000 -) - -// LoginInput holds legacy login credentials. -type LoginInput struct { - Username string - Password string - Code string -} - -// RegisterInput holds legacy registration fields. -type RegisterInput struct { - Username string - Password string - Nickname string - DisplayName string - Email string - Code string -} - -// ManageUserInput holds legacy user management actions. -type ManageUserInput struct { - Username string - Action string -} - -// UpdateUserInput holds legacy admin user update fields. -type UpdateUserInput struct { - ID int - Username string - Password string - DisplayName string - Role int - Email string -} - -// UpdateSelfInput holds legacy self-update fields. -type UpdateSelfInput struct { - Username string - Password string - DisplayName string - Email string -} - -// CreateUserInput holds legacy admin create-user fields. -type CreateUserInput struct { - Username string - Password string - DisplayName string - Role int - Email string -} - -// PasswordResetInput holds password reset confirmation. -type PasswordResetInput struct { - Email string - Token string -} - -// LinkExistingInput binds a pending OAuth account to an existing user. -type LinkExistingInput struct { - Username string - Password string -} - -// Login authenticates a user and returns the legacy user shape with an access token. -func Login(ctx context.Context, c *gin.Context, input LoginInput) (LegacyUser, error) { - if !isPasswordLoginEnabled(ctx) { - return LegacyUser{}, errors.New(errPasswordLoginDisabled) - } - - input.Username = strings.TrimSpace(input.Username) - if input.Username == "" || input.Password == "" { - return LegacyUser{}, errors.New(errInvalidParams) - } - - var user model.User - if err := db.DB(ctx).Where("username = ? OR email = ?", input.Username, input.Username).First(&user).Error; err != nil { - logger.WarnF(ctx, "[LoginAudit] failed login attempt (username not found) for input: %s, IP: %s", input.Username, c.ClientIP()) - return LegacyUser{}, errors.New(errUsernameOrPasswordWrong) - } - if !user.IsActive { - return LegacyUser{}, errors.New(errBannedAccount) - } - if !user.CheckPassword(input.Password) { - logger.WarnF(ctx, "[LoginAudit] failed login attempt (incorrect password) for username: %s, ID: %d, IP: %s", user.Username, user.ID, c.ClientIP()) - return LegacyUser{}, errors.New(errUsernameOrPasswordWrong) - } - - if isEmailLoginVerificationEnabled(ctx) { - if err := verifyLoginEmailCode(ctx, user.Email, input.Code); err != nil { - return LegacyUser{}, err - } - } - - user.LastLoginAt = time.Now() - if err := db.DB(ctx).Model(&user).Update("last_login_at", user.LastLoginAt).Error; err != nil { - return LegacyUser{}, err - } - if err := setLoginSession(ctx, c, &user); err != nil { - return LegacyUser{}, errors.New(errSaveSessionFailed) - } - - token, err := issueLegacyAccessToken(ctx, &user) - if err != nil { - return LegacyUser{}, err - } - - logger.InfoF(ctx, "[LoginAudit] successful legacy login for user: %s, ID: %d, IP: %s", user.Username, user.ID, c.ClientIP()) - listener.EmitAdminLoggedIn(ctx, &user, c.ClientIP()) - - return ToLegacyUser(&user, token), nil -} - -// Register creates a new user and logs them in. -func Register(ctx context.Context, c *gin.Context, input RegisterInput) (LegacyUser, error) { - if !isRegistrationEnabled(ctx) || !isPasswordRegisterEnabled(ctx) { - return LegacyUser{}, errors.New(errRegistrationDisabled) - } - - input.Username = strings.TrimSpace(input.Username) - input.Password = strings.TrimSpace(input.Password) - input.Nickname = strings.TrimSpace(input.Nickname) - input.DisplayName = strings.TrimSpace(input.DisplayName) - input.Email = strings.TrimSpace(input.Email) - input.Code = strings.TrimSpace(input.Code) - - if input.Username == "" || input.Password == "" { - return LegacyUser{}, errors.New(errInvalidParams) - } - if len(input.Password) < minPasswordLength { - return LegacyUser{}, errors.New(errPasswordTooShort) - } - if input.Email == "" { - return LegacyUser{}, errors.New(errEmailRequired) - } - if isEmailRegisterVerificationEnabled(ctx) { - if input.Code == "" || !verifyEmailCode(ctx, input.Email, "register", input.Code) { - return LegacyUser{}, errors.New(errEmailCodeInvalid) - } - } - - user := model.User{ - ID: idgen.NextUint64ID(), - Username: input.Username, - Nickname: input.Nickname, - Email: input.Email, - IsActive: true, - IsAdmin: false, - LastLoginAt: time.Now(), - } - if user.Nickname == "" { - user.Nickname = input.DisplayName - } - if user.Nickname == "" { - user.Nickname = input.Username - } - if err := user.SetEncryptedPassword(input.Password); err != nil { - return LegacyUser{}, err - } - if err := user.RegisterUser(ctx, db.DB(ctx)); err != nil { - return LegacyUser{}, err - } - if err := setLoginSession(ctx, c, &user); err != nil { - return LegacyUser{}, errors.New(errSaveSessionFailed) - } - - token, err := issueLegacyAccessToken(ctx, &user) - if err != nil { - return LegacyUser{}, err - } - return ToLegacyUser(&user, token), nil -} - -// Logout clears session and revokes the legacy access token when provided. -func Logout(ctx context.Context, c *gin.Context) error { - token := strings.TrimSpace(c.GetHeader(compat.OpenFlareTokenHeader())) - if token == "" { - token = strings.TrimSpace(c.GetHeader("X-Access-Token")) - } - if token != "" { - tokenHash := model.HashToken(token) - _ = db.DB(ctx).Where("token_hash = ? AND name = ?", tokenHash, legacyTokenName).Delete(&model.AccessToken{}).Error - } - - session := sessions.Default(c) - session.Options(oauth.GetSessionOptions(-1)) - session.Clear() - return session.Save() -} - -// GetSelf returns the current user's legacy profile. -func GetSelf(ctx context.Context, userID uint64) (LegacyUser, error) { - user, err := repository.GetUserByID(ctx, userID) - if err != nil { - return LegacyUser{}, errors.New(errUserNotFound) - } - return ToLegacyUser(&user, ""), nil -} - -// GenerateUserToken issues a fresh legacy access token for the user. -func GenerateUserToken(ctx context.Context, userID uint64) (string, error) { - user, err := repository.GetUserByID(ctx, userID) - if err != nil { - return "", errors.New(errUserNotFound) - } - return issueLegacyAccessToken(ctx, &user) -} - -// UpdateSelf updates the logged-in user's profile. -func UpdateSelf(ctx context.Context, userID uint64, input UpdateSelfInput) error { - user, err := repository.GetUserByID(ctx, userID) - if err != nil { - return errors.New(errUserNotFound) - } - - if input.DisplayName != "" { - user.Nickname = strings.TrimSpace(input.DisplayName) - } - if input.Username != "" { - user.Username = strings.TrimSpace(input.Username) - } - if input.Email != "" { - user.Email = strings.TrimSpace(input.Email) - } - if input.Password != "" { - if len(input.Password) < minPasswordLength { - return errors.New(errPasswordTooShort) - } - if err := user.SetEncryptedPassword(input.Password); err != nil { - return err - } - } - return db.DB(ctx).Save(&user).Error -} - -// DeleteSelf removes the logged-in user. -func DeleteSelf(ctx context.Context, userID uint64) error { - return repository.DeleteUserWithRelations(ctx, userID) -} - -// ListUsers returns a paginated legacy user list. -func ListUsers(ctx context.Context, page int) ([]LegacyUser, error) { - if page < 0 { - page = 0 - } - _, users, err := repository.ListAdminUsers(ctx, repository.AdminUserListFilter{ - Page: page + 1, - PageSize: legacyItemsPerPage, - }) - if err != nil { - return nil, err - } - return ToLegacyUsers(users), nil -} - -// SearchUsers searches users by keyword. -func SearchUsers(ctx context.Context, keyword string) ([]LegacyUser, error) { - keyword = strings.TrimSpace(keyword) - var users []model.User - query := db.DB(ctx).Model(&model.User{}). - Select("id, username, nickname, email, is_active, is_admin") - if keyword != "" { - like := keyword + "%" - query = query.Where( - "CAST(id AS TEXT) = ? OR username LIKE ? OR email LIKE ? OR nickname LIKE ?", - keyword, like, like, like, - ) - } - if err := query.Order("id DESC").Find(&users).Error; err != nil { - return nil, err - } - return ToLegacyUsers(users), nil -} - -// GetUserByID returns a legacy user if the caller has sufficient role. -func GetUserByID(ctx context.Context, callerRole int, id uint64) (LegacyUser, error) { - user, err := repository.GetAdminUserDetail(ctx, id) - if err != nil { - return LegacyUser{}, errors.New(errUserNotFound) - } - targetRole := RoleFromUser(&user) - if callerRole <= targetRole { - return LegacyUser{}, errors.New(errInsufficientPermission) - } - return ToLegacyUser(&user, ""), nil -} - -// CreateUser creates a user from legacy admin input. -func CreateUser(ctx context.Context, callerRole int, input CreateUserInput) error { - input.Username = strings.TrimSpace(input.Username) - input.Password = strings.TrimSpace(input.Password) - input.DisplayName = strings.TrimSpace(input.DisplayName) - input.Email = strings.TrimSpace(input.Email) - - if input.Username == "" || input.Password == "" { - return errors.New(errInvalidParams) - } - if input.Role >= callerRole { - return errors.New(errInsufficientPermission) - } - if input.Email == "" { - input.Email = input.Username + "@openflare.local" - } - - newUser := model.User{ - ID: idgen.NextUint64ID(), - Username: input.Username, - Nickname: input.DisplayName, - Email: input.Email, - IsActive: true, - IsAdmin: input.Role >= compat.RoleAdminUser, - } - if newUser.Nickname == "" { - newUser.Nickname = input.Username - } - if err := newUser.SetEncryptedPassword(input.Password); err != nil { - return err - } - return repository.CreateUser(ctx, &newUser) -} - -// UpdateUser updates another user with legacy role checks. -func UpdateUser(ctx context.Context, callerRole int, input UpdateUserInput) error { - if input.ID == 0 { - return errors.New(errInvalidParams) - } - - origin, err := repository.GetAdminUserDetail(ctx, uint64(input.ID)) - if err != nil { - return errors.New(errUserNotFound) - } - originRole := RoleFromUser(&origin) - if callerRole <= originRole { - return errors.New(errInsufficientPermission) - } - if input.Role > 0 && callerRole <= input.Role { - return errors.New(errInsufficientPermission) - } - - if trimmed := strings.TrimSpace(input.Username); trimmed != "" { - origin.Username = trimmed - } - if input.DisplayName != "" { - origin.Nickname = strings.TrimSpace(input.DisplayName) - } - if input.Email != "" { - origin.Email = strings.TrimSpace(input.Email) - } - if input.Role > 0 { - origin.IsAdmin = input.Role >= compat.RoleAdminUser - } - if input.Password != "" { - if len(input.Password) < minPasswordLength { - return errors.New(errPasswordTooShort) - } - if err := origin.SetEncryptedPassword(input.Password); err != nil { - return err - } - } - return db.DB(ctx).Save(&origin).Error -} - -// DeleteUserByID deletes a user when the caller has sufficient role. -func DeleteUserByID(ctx context.Context, callerRole int, id uint64) error { - origin, err := repository.GetAdminUserDetail(ctx, id) - if err != nil { - return errors.New(errUserNotFound) - } - if callerRole <= RoleFromUser(&origin) { - return errors.New(errInsufficientPermission) - } - if RoleFromUser(&origin) >= compat.RoleRootUser { - return errors.New(errCannotDeleteRoot) - } - return repository.DeleteUserWithRelations(ctx, id) -} - -// ManageUser performs enable/disable/delete/promote/demote actions. -func ManageUser(ctx context.Context, callerRole int, input ManageUserInput) (LegacyUser, error) { - input.Username = strings.TrimSpace(input.Username) - input.Action = strings.TrimSpace(input.Action) - if input.Username == "" || input.Action == "" { - return LegacyUser{}, errors.New(errInvalidParams) - } - - user, err := repository.GetUserByUsername(ctx, input.Username) - if err != nil { - return LegacyUser{}, errors.New(errUserNotFound) - } - targetRole := RoleFromUser(&user) - if callerRole <= targetRole && callerRole != compat.RoleRootUser { - return LegacyUser{}, errors.New(errInsufficientPermission) - } - - switch input.Action { - case "disable": - if targetRole >= compat.RoleRootUser { - return LegacyUser{}, errors.New(errCannotDisableRoot) - } - user.IsActive = false - case "enable": - user.IsActive = true - case "delete": - if targetRole >= compat.RoleRootUser { - return LegacyUser{}, errors.New(errCannotDeleteRoot) - } - if err := repository.DeleteUserWithRelations(ctx, user.ID); err != nil { - return LegacyUser{}, err - } - return LegacyUser{Role: compat.RoleCommonUser, Status: legacyUserStatusDisabled}, nil - case "promote": - if callerRole != compat.RoleRootUser { - return LegacyUser{}, errors.New(errCannotPromoteAdmin) - } - if user.IsAdmin { - return LegacyUser{}, errors.New(errAlreadyAdmin) - } - user.IsAdmin = true - case "demote": - if targetRole >= compat.RoleRootUser { - return LegacyUser{}, errors.New(errCannotDisableRoot) - } - if !user.IsAdmin { - return LegacyUser{}, errors.New(errAlreadyCommonUser) - } - user.IsAdmin = false - default: - return LegacyUser{}, errors.New(errInvalidParams) - } - - if err := db.DB(ctx).Save(&user).Error; err != nil { - return LegacyUser{}, err - } - return LegacyUser{ - Role: RoleFromUser(&user), - Status: StatusFromUser(&user), - }, nil -} - -// SendRegisterVerificationEmail sends a registration verification code. -func SendRegisterVerificationEmail(ctx context.Context, email string) error { - email = strings.TrimSpace(email) - if email == "" || !strings.Contains(email, "@") { - return errors.New(errInvalidParams) - } - var count int64 - if err := db.DB(ctx).Model(&model.User{}).Where("email = ?", email).Count(&count).Error; err != nil { - return err - } - if count > 0 { - return errors.New(errEmailAlreadyRegistered) - } - return sendEmailVerificationCode(ctx, email, "register", "register_email") -} - -// SendPasswordResetEmail stores a reset token and emails the user. -func SendPasswordResetEmail(ctx context.Context, email string) error { - email = strings.TrimSpace(email) - if email == "" || !strings.Contains(email, "@") { - return errors.New(errInvalidParams) - } - var user model.User - if err := db.DB(ctx).Where("email = ?", email).First(&user).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return errors.New(errEmailNotRegistered) - } - return err - } - - token, err := generateResetToken() - if err != nil { - return err - } - key := fmt.Sprintf(passwordResetKeyFmt, email) - if err := db.SetJSON(ctx, key, token, passwordResetExpiry); err != nil { - return err - } - - serverAddr, _ := repository.GetSystemConfigByKey(ctx, model.ConfigKeyServerAddress) - base := strings.TrimRight(serverAddr.Value, "/") - link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", base, email, token) - body := fmt.Sprintf("

您好,你正在进行密码重置。

点击此处进行密码重置。

", link) - return dispatchEmail(ctx, email, "密码重置", body) -} - -// ResetPassword validates a reset token and returns a new random password. -func ResetPassword(ctx context.Context, input PasswordResetInput) (string, error) { - input.Email = strings.TrimSpace(input.Email) - input.Token = strings.TrimSpace(input.Token) - if input.Email == "" || input.Token == "" { - return "", errors.New(errInvalidParams) - } - - key := fmt.Sprintf(passwordResetKeyFmt, input.Email) - var stored string - if err := db.GetJSON(ctx, key, &stored); err != nil || stored != input.Token { - return "", errors.New(errResetLinkInvalid) - } - - password, err := generateResetToken() - if err != nil { - return "", err - } - if len(password) < minPasswordLength { - password = password + "Aa1!" - } - - var user model.User - if err := db.DB(ctx).Where("email = ?", input.Email).First(&user).Error; err != nil { - return "", errors.New(errEmailNotRegistered) - } - if err := user.SetEncryptedPassword(password); err != nil { - return "", err - } - if err := db.DB(ctx).Model(&user).Update("password", user.Password).Error; err != nil { - return "", err - } - _ = db.Redis.Del(ctx, db.PrefixedKey(key)).Err() - return password, nil -} - -// BuildPublicStatus assembles the legacy /api/status payload. -func BuildPublicStatus(ctx context.Context) (map[string]any, error) { - authSources, err := publicAuthSources(ctx, "/api") - if err != nil { - authSources = []map[string]any{} - } - - siteName, _ := repository.GetSystemConfigByKey(ctx, model.ConfigKeySiteName) - serverAddr, _ := repository.GetSystemConfigByKey(ctx, model.ConfigKeyServerAddress) - - return map[string]any{ - "version": buildVersion(), - "start_time": appStartUnix(), - "email_verification": isEmailRegisterVerificationEnabled(ctx), - "github_oauth": false, - "github_client_id": "", - "system_name": siteName.Value, - "home_page_link": "", - "footer_html": "", - "wechat_qrcode": "", - "wechat_login": false, - "server_address": serverAddr.Value, - "password_register_enabled": isPasswordRegisterEnabled(ctx), - "cap_login_enabled": capLoginEnabled(ctx), - "auth_sources": authSources, - }, nil -} - -// GetNotice returns the legacy notice option value. -func GetNotice(ctx context.Context) string { - return getOptionValue(ctx, "notice") -} - -// GetAbout returns the legacy about option value. -func GetAbout(ctx context.Context) string { - return getOptionValue(ctx, "about") -} - -func issueLegacyAccessToken(ctx context.Context, user *model.User) (string, error) { - tokenStr, err := model.GenerateTokenString() - if err != nil { - return "", errors.New(errGenerateTokenFailed) - } - record := model.AccessToken{ - UserID: user.ID, - Name: legacyTokenName, - TokenHash: model.HashToken(tokenStr), - MaskedToken: model.MaskTokenString(tokenStr), - IsAdmin: user.IsAdmin, - } - if err := db.DB(ctx).Create(&record).Error; err != nil { - return "", err - } - return tokenStr, nil -} - -func setLoginSession(ctx context.Context, c *gin.Context, user *model.User) error { - session := sessions.Default(c) - session.Set(oauth.UserIDKey, user.ID) - session.Set(oauth.UserNameKey, user.Username) - session.Set(oauth.PasswordHashKey, user.Password) - - maxAge := config.Config.App.SessionAge - isSessionCookie := false - ttlHours, err := repository.GetIntByKey(ctx, model.ConfigKeyLoginSessionTTLHours) - if err == nil { - switch { - case ttlHours == -1: - maxAge = 10 * 365 * 24 * 3600 - case ttlHours > 0: - maxAge = ttlHours * 3600 - case ttlHours == 0: - isSessionCookie = true - } - } - session.Options(oauth.GetSessionOptions(maxAge)) - if err := session.Save(); err != nil { - return err - } - if isSessionCookie { - oauth.StripCookieMaxAgeAndExpires(c.Writer.Header(), config.Config.App.SessionCookieName) - } - return nil -} - -func isPasswordLoginEnabled(ctx context.Context) bool { - enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyPasswordLoginEnabled) - return err != nil || enabled -} - -func isPasswordRegisterEnabled(ctx context.Context) bool { - enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyPasswordRegisterEnabled) - return err != nil || enabled -} - -func isRegistrationEnabled(ctx context.Context) bool { - enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyRegistrationEnabled) - return err != nil || enabled -} - -func isEmailLoginVerificationEnabled(ctx context.Context) bool { - enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyEmailLoginVerificationEnabled) - return err == nil && enabled -} - -func isEmailRegisterVerificationEnabled(ctx context.Context) bool { - enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyEmailRegisterVerificationEnabled) - return err == nil && enabled -} - -func capLoginEnabled(ctx context.Context) bool { - enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyCapLoginEnabled) - return err == nil && enabled -} - -func verifyLoginEmailCode(ctx context.Context, email, code string) error { - if code == "" { - return errors.New("need_email_code:" + email) - } - if !verifyEmailCode(ctx, email, "login", code) { - return errors.New(errEmailCodeInvalid) - } - return nil -} - -func verifyEmailCode(ctx context.Context, email, scene, code string) bool { - key := fmt.Sprintf("email_code:%s:%s", scene, email) - var stored string - if err := db.GetJSON(ctx, key, &stored); err != nil { - return false - } - if stored != code { - return false - } - _ = db.Redis.Del(ctx, db.PrefixedKey(key)).Err() - return true -} - -func sendEmailVerificationCode(ctx context.Context, email, scene, templateName string) error { - scHost, errHost := repository.GetSystemConfigByKey(ctx, model.ConfigKeySMTPHost) - scPort, errPort := repository.GetSystemConfigByKey(ctx, model.ConfigKeySMTPPort) - scUser, errUser := repository.GetSystemConfigByKey(ctx, model.ConfigKeySMTPUsername) - scPass, errPass := repository.GetSystemConfigByKey(ctx, model.ConfigKeySMTPPassword) - if errHost != nil || errPort != nil || errUser != nil || errPass != nil || - scHost.Value == "" || scPort.Value == "" || scUser.Value == "" || scPass.Value == "" { - return errors.New("系统 SMTP 邮件服务配置不完整") - } - - code, err := generateVerificationCode() - if err != nil { - return err - } - codeKey := fmt.Sprintf("email_code:%s:%s", scene, email) - if err := db.SetJSON(ctx, codeKey, code, 5*time.Minute); err != nil { - return err - } - - tmpl, err := repository.GetTemplateByKey(ctx, templateName) - if err != nil { - body := fmt.Sprintf("

您的验证码为: %s

", code) - return dispatchEmail(ctx, email, "邮箱验证", body) - } - subject, body, err := tmpl.Render(map[string]any{"Code": code}) - if err != nil { - return err - } - return dispatchEmail(ctx, email, subject, body) -} - -type sendEmailPayload struct { - To string `json:"to"` - Subject string `json:"subject"` - Body string `json:"body"` -} - -func dispatchEmail(ctx context.Context, to, subject, body string) error { - payload := sendEmailPayload{To: to, Subject: subject, Body: body} - payloadBytes, err := json.Marshal(payload) - if err != nil { - return err - } - _, err = task.DispatchTask(ctx, "mail:send", payloadBytes, "system") - return err -} - -func generateVerificationCode() (string, error) { - n, err := rand.Int(rand.Reader, big.NewInt(verificationCodeRange)) - if err != nil { - return "", err - } - return fmt.Sprintf("%06d", n.Int64()+verificationOffset), nil -} - -func generateResetToken() (string, error) { - n, err := rand.Int(rand.Reader, big.NewInt(1<<62)) - if err != nil { - return "", err - } - return fmt.Sprintf("%x", n.Int64()), nil -} - -func publicAuthSources(ctx context.Context, baseAPIPath string) ([]map[string]any, error) { - sources, err := model.GetActiveAuthSources(ctx) - if err != nil { - return nil, err - } - result := make([]map[string]any, 0, len(sources)) - base := strings.TrimRight(baseAPIPath, "/") - for _, source := range sources { - result = append(result, map[string]any{ - "id": source.ID, - "name": source.Name, - "type": source.Type, - "display_name": source.DisplayName, - "authorize_url": fmt.Sprintf("%s/oauth/%s/authorize", base, source.Name), - "icon_url": source.IconURL, - }) - } - return result, nil -} - -func getOptionValue(ctx context.Context, key string) string { - sc, err := repository.GetSystemConfigByKey(ctx, key) - if err != nil { - return "" - } - return sc.Value -} - -var appStart = time.Now() - -func appStartUnix() int64 { - return appStart.Unix() -} - -func buildVersion() string { - if buildinfo.Version != "" { - return buildinfo.Version - } - return "dev" -} diff --git a/Wavelet/internal/apps/openflare/auth/oauth.go b/Wavelet/internal/apps/openflare/auth/oauth.go deleted file mode 100644 index 700d1aeb..00000000 --- a/Wavelet/internal/apps/openflare/auth/oauth.go +++ /dev/null @@ -1,509 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package auth - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net/url" - "strings" - "time" - - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" - "github.com/Rain-kl/Wavelet/internal/db" - "github.com/Rain-kl/Wavelet/internal/listener" - "github.com/Rain-kl/Wavelet/internal/model" - "github.com/Rain-kl/Wavelet/internal/repository" - "github.com/Rain-kl/Wavelet/pkg/logger" - "github.com/gin-contrib/sessions" - "github.com/gin-gonic/gin" - "github.com/google/uuid" - "gorm.io/gorm" -) - -const pendingExternalAccountSessionKey = "pending_external_account" - -// OAuthCallbackResult is the legacy OAuth callback payload. -type OAuthCallbackResult struct { - Status string `json:"status"` - User *LegacyUser `json:"user,omitempty"` -} - -// PendingExternalAccount stores OAuth bind-pending state in session. -type PendingExternalAccount struct { - AuthSourceID uint64 `json:"auth_source_id"` - ExternalID string `json:"external_id"` - ExternalUsername string `json:"external_username"` - DisplayName string `json:"display_name"` - Email string `json:"email"` -} - -// OAuthAuthorize builds an authorize URL for a legacy auth source route param. -func OAuthAuthorize(ctx context.Context, c *gin.Context, sourceKey string) (string, error) { - source, err := resolveAuthSourceByRoute(ctx, sourceKey) - if err != nil { - return "", err - } - if !source.IsActive { - return "", errors.New(errAuthSourceDisabled) - } - if err := source.Validate(); err != nil { - return "", err - } - - state := uuid.NewString() - session := sessions.Default(c) - session.Set(oauthStateSessionKey(source.ID), state) - if err := session.Save(); err != nil { - return "", errors.New(errSaveSessionFailed) - } - - redirectURL := legacyOAuthCallbackURL(c, source) - return buildLegacyAuthorizeURL(ctx, source, redirectURL, state) -} - -// OAuthCallback handles GET /oauth/:source/callback for the legacy frontend. -func OAuthCallback(ctx context.Context, c *gin.Context, sourceKey string) (OAuthCallbackResult, error) { - source, err := resolveAuthSourceByRoute(ctx, sourceKey) - if err != nil { - return OAuthCallbackResult{}, err - } - if !source.IsActive { - return OAuthCallbackResult{}, errors.New(errAuthSourceDisabled) - } - - session := sessions.Default(c) - expectedState, _ := session.Get(oauthStateSessionKey(source.ID)).(string) - state := c.Query("state") - if expectedState == "" || state == "" || state != expectedState { - return OAuthCallbackResult{}, errors.New("授权状态无效,请重新登录") - } - session.Delete(oauthStateSessionKey(source.ID)) - if err := session.Save(); err != nil { - return OAuthCallbackResult{}, errors.New(errSaveSessionFailed) - } - if oauthError := c.Query("error"); oauthError != "" { - description := c.Query("error_description") - if description == "" { - description = oauthError - } - return OAuthCallbackResult{}, errors.New(description) - } - - redirectURL := legacyOAuthCallbackURL(c, source) - userInfo, err := exchangeLegacyOAuthProfile(ctx, source, c.Query("code"), state, redirectURL) - if err != nil { - return OAuthCallbackResult{}, err - } - - var currentUserID *uint64 - if current := currentUserFromLegacyToken(ctx, c); current != nil { - currentUserID = ¤t.ID - } - - result, pending, err := completeLegacyOAuthLogin(ctx, source, userInfo, currentUserID) - if err != nil { - return OAuthCallbackResult{}, err - } - if pending != nil { - raw, marshalErr := json.Marshal(pending) - if marshalErr != nil { - return OAuthCallbackResult{}, marshalErr - } - session.Set(pendingExternalAccountSessionKey, string(raw)) - if err := session.Save(); err != nil { - return OAuthCallbackResult{}, errors.New(errSaveSessionFailed) - } - return result, nil - } - if result.User != nil { - var dbUser model.User - if err := db.DB(ctx).Where("id = ?", result.User.ID).First(&dbUser).Error; err != nil { - return OAuthCallbackResult{}, err - } - if err := setLoginSession(ctx, c, &dbUser); err != nil { - return OAuthCallbackResult{}, errors.New(errSaveSessionFailed) - } - token, tokenErr := issueLegacyAccessToken(ctx, &dbUser) - if tokenErr != nil { - return OAuthCallbackResult{}, tokenErr - } - legacy := ToLegacyUser(&dbUser, token) - result.User = &legacy - listener.EmitAdminLoggedIn(ctx, &dbUser, c.ClientIP()) - } - return result, nil -} - -// LinkExistingOAuthAccount binds a pending external account to an existing user. -func LinkExistingOAuthAccount(ctx context.Context, c *gin.Context, input LinkExistingInput) (OAuthCallbackResult, error) { - session := sessions.Default(c) - raw, _ := session.Get(pendingExternalAccountSessionKey).(string) - if raw == "" { - return OAuthCallbackResult{}, errors.New(errPendingOAuthExpired) - } - var pending PendingExternalAccount - if err := json.Unmarshal([]byte(raw), &pending); err != nil { - return OAuthCallbackResult{}, errors.New(errPendingOAuthInvalid) - } - - user, err := linkPendingExternalAccount(ctx, &pending, input) - if err != nil { - return OAuthCallbackResult{}, err - } - - session.Delete(pendingExternalAccountSessionKey) - if err := session.Save(); err != nil { - return OAuthCallbackResult{}, errors.New(errSaveSessionFailed) - } - if err := setLoginSession(ctx, c, user); err != nil { - return OAuthCallbackResult{}, errors.New(errSaveSessionFailed) - } - token, err := issueLegacyAccessToken(ctx, user) - if err != nil { - return OAuthCallbackResult{}, err - } - legacy := ToLegacyUser(user, token) - return OAuthCallbackResult{Status: "linked", User: &legacy}, nil -} - -func resolveAuthSourceByRoute(ctx context.Context, raw string) (*model.AuthSource, error) { - raw = strings.TrimSpace(raw) - if raw == "" { - return nil, errors.New("认证源不能为空") - } - if parsed, err := parseUint64(raw); err == nil && parsed > 0 { - return model.GetAuthSourceByID(ctx, parsed) - } - return model.GetAuthSourceByName(ctx, raw) -} - -func oauthStateSessionKey(sourceID uint64) string { - return fmt.Sprintf("oauth_state_%d", sourceID) -} - -func legacyOAuthCallbackURL(c *gin.Context, source *model.AuthSource) string { - ctx := c.Request.Context() - base := "" - if sc, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeyServerAddress); err == nil { - base = strings.TrimRight(sc.Value, "/") - } - if base == "" { - scheme := "http" - if c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https" { - scheme = "https" - } - host := c.Request.Host - if forwardedHost := c.GetHeader("X-Forwarded-Host"); forwardedHost != "" { - host = forwardedHost - } - base = scheme + "://" + host - } - sourceName := source.Name - if sourceName == "" { - sourceName = fmt.Sprintf("%d", source.ID) - } - callback, _ := url.JoinPath(base, "oauth", sourceName) - return callback -} - -func buildLegacyAuthorizeURL(ctx context.Context, source *model.AuthSource, redirectURL, state string) (string, error) { - payloadValue, err := encodeLegacyOAuthState(source.Name, state) - if err != nil { - return "", err - } - stateKey := fmt.Sprintf("of_oauth_state:%s", state) - if err := db.Redis.Set(ctx, db.PrefixedKey(stateKey), payloadValue, 10*time.Minute).Err(); err != nil { - return "", err - } - return oauthBuildAuthorizeURL(ctx, source, redirectURL, state) -} - -func encodeLegacyOAuthState(sourceName, state string) (string, error) { - payload := map[string]string{ - "source_name": sourceName, - "state": state, - } - raw, err := json.Marshal(payload) - if err != nil { - return "", err - } - return string(raw), nil -} - -func legacyFrontendLoginRedirectURL(ctx context.Context, source *model.AuthSource) (string, error) { - sc, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeyServerAddress) - if err != nil || strings.TrimSpace(sc.Value) == "" { - return "", errors.New("server_address 未配置") - } - base := strings.TrimRight(sc.Value, "/") - name := source.Name - if name == "" { - name = fmt.Sprintf("%d", source.ID) - } - return base + "/oauth/" + url.PathEscape(name), nil -} - -func exchangeLegacyOAuthProfile(ctx context.Context, source *model.AuthSource, code, state, redirectURL string) (*model.OAuthUserInfo, error) { - if strings.TrimSpace(code) == "" { - return nil, errors.New("授权 code 不能为空") - } - // Validate state from Redis cache written during authorize. - stateKey := fmt.Sprintf("of_oauth_state:%s", state) - payloadRaw, err := db.Redis.Get(ctx, db.PrefixedKey(stateKey)).Result() - if err != nil { - return nil, errors.New("授权状态无效,请重新登录") - } - _ = db.Redis.Del(ctx, db.PrefixedKey(stateKey)).Err() - - var payload map[string]string - if err := json.Unmarshal([]byte(payloadRaw), &payload); err != nil { - return nil, err - } - if payload["source_name"] != source.Name { - return nil, errors.New("授权状态无效,请重新登录") - } - - userInfo, err := buildOAuthUserInfo(ctx, source, code, state, redirectURL) - if err != nil { - return nil, err - } - if err := normalizeOAuthUserInfo(userInfo); err != nil { - return nil, err - } - if userInfo.Sub == "" { - userInfo.Sub = userInfo.Username - } - return userInfo, nil -} - -func completeLegacyOAuthLogin(ctx context.Context, source *model.AuthSource, profile *model.OAuthUserInfo, currentUserID *uint64) (OAuthCallbackResult, *PendingExternalAccount, error) { - if source == nil || profile == nil || strings.TrimSpace(profile.Sub) == "" { - return OAuthCallbackResult{}, nil, errors.New("第三方账号资料不完整") - } - - account, err := model.FindExternalAccount(ctx, source.ID, profile.Sub) - if err == nil { - var user model.User - if err := db.DB(ctx).Where("id = ?", account.UserID).First(&user).Error; err != nil { - return OAuthCallbackResult{}, nil, err - } - if !user.IsActive { - return OAuthCallbackResult{}, nil, errors.New(errBannedAccount) - } - legacy := ToLegacyUser(&user, "") - return OAuthCallbackResult{Status: "logged_in", User: &legacy}, nil, nil - } - if !errors.Is(err, gorm.ErrRecordNotFound) { - return OAuthCallbackResult{}, nil, err - } - - if currentUserID != nil && *currentUserID > 0 { - var user model.User - if err := db.DB(ctx).Where("id = ?", *currentUserID).First(&user).Error; err != nil { - return OAuthCallbackResult{}, nil, err - } - if !user.IsActive { - return OAuthCallbackResult{}, nil, errors.New(errBannedAccount) - } - if err := model.BindExternalAccount(ctx, &model.ExternalAccount{ - AuthSourceID: source.ID, - UserID: user.ID, - ExternalID: profile.Sub, - ExternalUsername: profile.Username, - Email: profile.Email, - }); err != nil { - return OAuthCallbackResult{}, nil, err - } - legacy := ToLegacyUser(&user, "") - return OAuthCallbackResult{Status: "linked", User: &legacy}, nil, nil - } - - registrationEnabled, regErr := repository.GetBoolByKey(ctx, model.ConfigKeyRegistrationEnabled) - if regErr != nil { - registrationEnabled = true - } - if !registrationEnabled { - pending := &PendingExternalAccount{ - AuthSourceID: source.ID, - ExternalID: profile.Sub, - ExternalUsername: profile.Username, - DisplayName: profile.Name, - Email: profile.Email, - } - return OAuthCallbackResult{Status: "link_required"}, pending, nil - } - - user, err := createUserFromOAuthProfile(ctx, source, profile) - if err != nil { - return OAuthCallbackResult{}, nil, err - } - legacy := ToLegacyUser(&user, "") - return OAuthCallbackResult{Status: "logged_in", User: &legacy}, nil, nil -} - -func createUserFromOAuthProfile(ctx context.Context, source *model.AuthSource, profile *model.OAuthUserInfo) (model.User, error) { - username, err := uniqueLegacyUsername(ctx, profile.Username) - if err != nil { - return model.User{}, err - } - profile.Username = username - - var user model.User - if err := user.CreateUser(ctx, db.DB(ctx), profile); err != nil { - return model.User{}, err - } - if err := model.BindExternalAccount(ctx, &model.ExternalAccount{ - AuthSourceID: source.ID, - UserID: user.ID, - ExternalID: profile.Sub, - ExternalUsername: profile.Username, - Email: profile.Email, - }); err != nil { - return model.User{}, err - } - logger.InfoF(ctx, "[LoginAudit] successful legacy OAuth registration via source: %s, user: %s, ID: %d", source.Name, user.Username, user.ID) - return user, nil -} - -func linkPendingExternalAccount(ctx context.Context, pending *PendingExternalAccount, input LinkExistingInput) (*model.User, error) { - if pending == nil || pending.AuthSourceID == 0 || pending.ExternalID == "" { - return nil, errors.New(errPendingOAuthExpired) - } - input.Username = strings.TrimSpace(input.Username) - if input.Username == "" || input.Password == "" { - return nil, errors.New(errInvalidParams) - } - - var user model.User - if err := db.DB(ctx).Where("username = ? OR email = ?", input.Username, input.Username).First(&user).Error; err != nil { - return nil, errors.New(errUsernameOrPasswordWrong) - } - if !user.IsActive { - return nil, errors.New(errBannedAccount) - } - if !user.CheckPassword(input.Password) { - return nil, errors.New(errUsernameOrPasswordWrong) - } - - if existing, err := model.FindExternalAccount(ctx, pending.AuthSourceID, pending.ExternalID); err == nil { - if existing.UserID != user.ID { - return nil, errors.New("该第三方账号已绑定其他用户") - } - return &user, nil - } else if !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, err - } - - if err := model.BindExternalAccount(ctx, &model.ExternalAccount{ - AuthSourceID: pending.AuthSourceID, - UserID: user.ID, - ExternalID: pending.ExternalID, - ExternalUsername: pending.ExternalUsername, - Email: pending.Email, - }); err != nil { - return nil, err - } - return &user, nil -} - -func currentUserFromLegacyToken(ctx context.Context, c *gin.Context) *model.User { - token := strings.TrimSpace(c.GetHeader(compat.OpenFlareTokenHeader())) - if token == "" { - return nil - } - tokenHash := model.HashToken(token) - var record model.AccessToken - if err := db.DB(ctx).Where("token_hash = ?", tokenHash).First(&record).Error; err != nil { - return nil - } - var user model.User - if err := db.DB(ctx).Where("id = ? AND is_active = ?", record.UserID, true).First(&user).Error; err != nil { - return nil - } - return &user -} - -func uniqueLegacyUsername(ctx context.Context, base string) (string, error) { - base = strings.TrimSpace(base) - if base == "" { - base = "user" - } - candidate := base - for i := 0; i <= 1000; i++ { - if i > 0 { - candidate = fmt.Sprintf("%s-%d", base, i) - } - count, err := repository.CountUsersByUsername(ctx, candidate) - if err != nil { - return "", err - } - if count == 0 { - return candidate, nil - } - } - return "", errors.New("无法生成唯一用户名") -} - -func parseUint64(raw string) (uint64, error) { - var id uint64 - _, err := fmt.Sscanf(raw, "%d", &id) - return id, err -} - -func isOIDCLoginEnabled(ctx context.Context) bool { - enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyOIDCLoginEnabled) - return err != nil || enabled -} - -// The following functions mirror oauth package internals for legacy GET callback support. -// They intentionally duplicate minimal logic to avoid modifying the core oauth module. - -func buildOAuthUserInfo(ctx context.Context, source *model.AuthSource, code, nonce, redirectURL string) (*model.OAuthUserInfo, error) { - authConfig, verifier, err := buildOAuthConfig(ctx, source, redirectURL) - if err != nil { - return nil, err - } - token, err := authConfig.Exchange(ctx, code) - if err != nil { - return nil, err - } - userInfo := &model.OAuthUserInfo{Active: true} - if verifier != nil { - if verifyErr := verifyIDToken(ctx, verifier, token, nonce, userInfo); verifyErr != nil { - return nil, verifyErr - } - } - if userInfo.Username == "" && userInfo.PreferredUsername != "" { - userInfo.Username = userInfo.PreferredUsername - } - if userInfo.Username == "" && userInfo.Email != "" { - userInfo.Username = strings.Split(userInfo.Email, "@")[0] - } - if userInfo.Username == "" && userInfo.Sub != "" { - userInfo.Username = userInfo.Sub - } - if userInfo.Name == "" { - userInfo.Name = userInfo.Username - } - return userInfo, nil -} - -func normalizeOAuthUserInfo(userInfo *model.OAuthUserInfo) error { - userInfo.Username = strings.TrimSpace(userInfo.Username) - userInfo.Email = strings.TrimSpace(userInfo.Email) - userInfo.Name = strings.TrimSpace(userInfo.Name) - if userInfo.Username == "" { - return errors.New("无法从认证源获取用户名") - } - if userInfo.Name == "" { - userInfo.Name = userInfo.Username - } - if !userInfo.Active { - userInfo.Active = true - } - return nil -} diff --git a/Wavelet/internal/apps/openflare/auth/oauth_compat.go b/Wavelet/internal/apps/openflare/auth/oauth_compat.go deleted file mode 100644 index 8048a5b0..00000000 --- a/Wavelet/internal/apps/openflare/auth/oauth_compat.go +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package auth - -import ( - "context" - "errors" - "fmt" - "net/http" - "strings" - "sync" - - "github.com/Rain-kl/Wavelet/internal/model" - "github.com/coreos/go-oidc/v3/oidc" - "golang.org/x/oauth2" - "golang.org/x/sync/singleflight" -) - -type legacyOIDCProviderCacheType struct { - mu sync.RWMutex - entries map[string]*oidc.Provider - sfGroup singleflight.Group -} - -var legacyOIDCProviderCache = &legacyOIDCProviderCacheType{ - entries: make(map[string]*oidc.Provider), -} - -func (c *legacyOIDCProviderCacheType) get(ctx context.Context, issuer string) (*oidc.Provider, error) { - c.mu.RLock() - if p, ok := c.entries[issuer]; ok { - c.mu.RUnlock() - return p, nil - } - c.mu.RUnlock() - - bg := context.Background() - if client, ok := ctx.Value(oauth2.HTTPClient).(*http.Client); ok && client != nil { - bg = oidc.ClientContext(bg, client) - } - - v, err, _ := c.sfGroup.Do(issuer, func() (any, error) { - c.mu.RLock() - if p, ok := c.entries[issuer]; ok { - c.mu.RUnlock() - return p, nil - } - c.mu.RUnlock() - - p, err := oidc.NewProvider(bg, issuer) - if err != nil { - return nil, err - } - c.mu.Lock() - c.entries[issuer] = p - c.mu.Unlock() - return p, nil - }) - if err != nil { - return nil, err - } - return v.(*oidc.Provider), nil //nolint:forcetypeassert // singleflight value type is fixed -} - -// oauthBuildAuthorizeURL builds an OAuth authorize URL using the same rules as apps/oauth. -func oauthBuildAuthorizeURL(ctx context.Context, source *model.AuthSource, redirectURL, state string) (string, error) { - authConfig, verifier, err := buildOAuthConfig(ctx, source, redirectURL) - if err != nil { - return "", err - } - if verifier != nil { - return authConfig.AuthCodeURL(state, oidc.Nonce(state)), nil - } - return authConfig.AuthCodeURL(state), nil -} - -func buildOAuthConfig(ctx context.Context, source *model.AuthSource, redirectURL string) (*oauth2.Config, *oidc.IDTokenVerifier, error) { - if source == nil { - return nil, nil, errors.New("认证源不能为空") - } - if source.OpenIDDiscoveryURL == "" { - return nil, nil, errors.New("认证源未配置 OpenID Discovery URL") - } - - issuer := strings.TrimSuffix(strings.TrimSpace(source.OpenIDDiscoveryURL), "/") - issuer = strings.TrimSuffix(issuer, "/.well-known/openid-configuration") - issuer = strings.TrimSuffix(issuer, "/.well-known/oauth-authorization-server") - - provider, err := legacyOIDCProviderCache.get(ctx, issuer) - if err != nil { - return nil, nil, err - } - verifier := provider.Verifier(&oidc.Config{ClientID: source.ClientID}) - scopes := strings.Fields(source.Scopes) - if len(scopes) == 0 { - scopes = []string{oidc.ScopeOpenID, "profile", "email"} - } - if !containsScope(scopes, oidc.ScopeOpenID) { - scopes = append([]string{oidc.ScopeOpenID}, scopes...) - } - - return &oauth2.Config{ - ClientID: source.ClientID, - ClientSecret: source.ClientSecret, - RedirectURL: redirectURL, - Scopes: scopes, - Endpoint: provider.Endpoint(), - }, verifier, nil -} - -func containsScope(scopes []string, scope string) bool { - for _, item := range scopes { - if item == scope { - return true - } - } - return false -} - -func verifyIDToken(ctx context.Context, verifier *oidc.IDTokenVerifier, token *oauth2.Token, nonce string, userInfo *model.OAuthUserInfo) error { - rawIDToken, ok := token.Extra("id_token").(string) - if !ok { - return nil - } - idToken, verifyErr := verifier.Verify(ctx, rawIDToken) - if verifyErr != nil { - return fmt.Errorf("ID Token 验证失败: %w", verifyErr) - } - if nonce != "" && idToken.Nonce != nonce { - return errors.New("nonce 不匹配") - } - return idToken.Claims(userInfo) -} diff --git a/Wavelet/internal/apps/openflare/auth/oauth_shortcuts.go b/Wavelet/internal/apps/openflare/auth/oauth_shortcuts.go deleted file mode 100644 index b92e29ed..00000000 --- a/Wavelet/internal/apps/openflare/auth/oauth_shortcuts.go +++ /dev/null @@ -1,353 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package auth - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "log/slog" - "net/http" - "strings" - "time" - - "github.com/Rain-kl/Wavelet/internal/db" - "github.com/Rain-kl/Wavelet/internal/listener" - "github.com/Rain-kl/Wavelet/internal/model" - "github.com/Rain-kl/Wavelet/pkg/logger" - "github.com/gin-gonic/gin" - "gorm.io/gorm" -) - -const ( - errGitHubOAuthDisabled = "管理员未开启通过 GitHub 登录以及注册" - errWeChatOAuthDisabled = "管理员未开启通过微信登录以及注册" - errRegistrationClosed = "管理员关闭了新用户注册" - errGitHubAlreadyBound = "该 GitHub 账户已被绑定" - errWeChatAlreadyBound = "该微信账号已被绑定" -) - -type githubOAuthResponse struct { - AccessToken string `json:"access_token"` -} - -type githubUser struct { - Login string `json:"login"` - Name string `json:"name"` - Email string `json:"email"` -} - -type wechatLoginResponse struct { - Success bool `json:"success"` - Message string `json:"message"` - Data string `json:"data"` -} - -// GitHubOAuth handles the legacy GET /oauth/github shortcut. -func GitHubOAuth(ctx context.Context, c *gin.Context, code string) (LegacyUser, error) { - if current := currentUserFromLegacyToken(ctx, c); current != nil { - if err := GitHubBind(ctx, c, current, code); err != nil { - return LegacyUser{}, err - } - return LegacyUser{}, nil - } - - if !model.GitHubOAuthEnabled { - return LegacyUser{}, errors.New(errGitHubOAuthDisabled) - } - - githubUser, err := getGitHubUserInfoByCode(code) - if err != nil { - return LegacyUser{}, err - } - - user, err := findUserByShortcutBinding(ctx, githubUser.Login, "github", "GitHub") - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return LegacyUser{}, errors.New(errRegistrationClosed) - } - return LegacyUser{}, err - } - if !user.IsActive { - return LegacyUser{}, errors.New(errBannedAccount) - } - return finishLegacyLogin(ctx, c, user) -} - -// GitHubBind binds a GitHub account to the current user. -func GitHubBind(ctx context.Context, c *gin.Context, current *model.User, code string) error { - if current == nil { - return errors.New(errUnauthorized) - } - if !model.GitHubOAuthEnabled { - return errors.New(errGitHubOAuthDisabled) - } - - githubUser, err := getGitHubUserInfoByCode(code) - if err != nil { - return err - } - if _, err := findUserByShortcutBinding(ctx, githubUser.Login, "github", "GitHub"); err == nil { - return errors.New(errGitHubAlreadyBound) - } else if !errors.Is(err, gorm.ErrRecordNotFound) { - return err - } - - return bindShortcutExternalAccount(ctx, current.ID, githubUser.Login, githubUser.Login, githubUser.Email, "github", "GitHub") -} - -// WeChatOAuth handles the legacy GET /oauth/wechat shortcut. -func WeChatOAuth(ctx context.Context, c *gin.Context, code string) (LegacyUser, error) { - if !model.WeChatAuthEnabled { - return LegacyUser{}, errors.New(errWeChatOAuthDisabled) - } - - wechatID, err := getWeChatIDByCode(code) - if err != nil { - return LegacyUser{}, err - } - - user, err := findUserByShortcutBinding(ctx, wechatID, "wechat", "WeChat") - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return LegacyUser{}, errors.New(errRegistrationClosed) - } - return LegacyUser{}, err - } - if !user.IsActive { - return LegacyUser{}, errors.New(errBannedAccount) - } - return finishLegacyLogin(ctx, c, user) -} - -// WeChatBind binds a WeChat account to the current user. -func WeChatBind(ctx context.Context, userID uint64, code string) error { - if userID == 0 { - return errors.New(errUnauthorized) - } - if !model.WeChatAuthEnabled { - return errors.New(errWeChatOAuthDisabled) - } - - wechatID, err := getWeChatIDByCode(code) - if err != nil { - return err - } - if _, err := findUserByShortcutBinding(ctx, wechatID, "wechat", "WeChat"); err == nil { - return errors.New(errWeChatAlreadyBound) - } else if !errors.Is(err, gorm.ErrRecordNotFound) { - return err - } - - return bindShortcutExternalAccount(ctx, userID, wechatID, wechatID, "", "wechat", "WeChat") -} - -// EmailBind binds a verified email address to the current user. -func EmailBind(ctx context.Context, userID uint64, email, code string) error { - email = strings.TrimSpace(email) - code = strings.TrimSpace(code) - if userID == 0 { - return errors.New(errUnauthorized) - } - if email == "" || code == "" { - return errors.New(errInvalidParams) - } - if !verifyEmailCode(ctx, email, "register", code) { - return errors.New(errEmailCodeInvalid) - } - - var user model.User - if err := db.DB(ctx).Where("id = ?", userID).First(&user).Error; err != nil { - return errors.New(errUserNotFound) - } - user.Email = email - return db.DB(ctx).Model(&user).Update("email", email).Error -} - -func finishLegacyLogin(ctx context.Context, c *gin.Context, user *model.User) (LegacyUser, error) { - if user == nil { - return LegacyUser{}, errors.New(errUserNotFound) - } - user.LastLoginAt = time.Now() - if err := db.DB(ctx).Model(user).Update("last_login_at", user.LastLoginAt).Error; err != nil { - return LegacyUser{}, err - } - if err := setLoginSession(ctx, c, user); err != nil { - return LegacyUser{}, errors.New(errSaveSessionFailed) - } - token, err := issueLegacyAccessToken(ctx, user) - if err != nil { - return LegacyUser{}, err - } - logger.InfoF(ctx, "[LoginAudit] successful legacy shortcut login for user: %s, ID: %d, IP: %s", user.Username, user.ID, c.ClientIP()) - listener.EmitAdminLoggedIn(ctx, user, c.ClientIP()) - return ToLegacyUser(user, token), nil -} - -func getGitHubUserInfoByCode(code string) (*githubUser, error) { - code = strings.TrimSpace(code) - if code == "" { - return nil, errors.New(errInvalidParams) - } - values := map[string]string{ - "client_id": model.GitHubClientId, - "client_secret": model.GitHubClientSecret, - "code": code, - } - jsonData, err := json.Marshal(values) - if err != nil { - return nil, err - } - - client := http.Client{Timeout: 5 * time.Second} - req, err := http.NewRequest(http.MethodPost, "https://github.com/login/oauth/access_token", bytes.NewBuffer(jsonData)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "application/json") - - res, err := client.Do(req) - if err != nil { - slog.Error("github oauth access token request failed", "error", err) - return nil, errors.New("无法连接至 GitHub 服务器,请稍后重试!") - } - defer res.Body.Close() - - var oauthResponse githubOAuthResponse - if err := json.NewDecoder(res.Body).Decode(&oauthResponse); err != nil { - return nil, err - } - if strings.TrimSpace(oauthResponse.AccessToken) == "" { - return nil, errors.New("无法连接至 GitHub 服务器,请稍后重试!") - } - - req, err = http.NewRequest(http.MethodGet, "https://api.github.com/user", nil) - if err != nil { - return nil, err - } - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", oauthResponse.AccessToken)) - - res2, err := client.Do(req) - if err != nil { - slog.Error("github user info request failed", "error", err) - return nil, errors.New("无法连接至 GitHub 服务器,请稍后重试!") - } - defer res2.Body.Close() - - var ghUser githubUser - if err := json.NewDecoder(res2.Body).Decode(&ghUser); err != nil { - return nil, err - } - if strings.TrimSpace(ghUser.Login) == "" { - return nil, errors.New("返回值非法,用户字段为空,请稍后重试!") - } - return &ghUser, nil -} - -func getWeChatIDByCode(code string) (string, error) { - code = strings.TrimSpace(code) - if code == "" { - return "", errors.New(errInvalidParams) - } - serverAddress := strings.TrimRight(strings.TrimSpace(model.WeChatServerAddress), "/") - if serverAddress == "" { - return "", errors.New(errWeChatOAuthDisabled) - } - - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/wechat/user?code=%s", serverAddress, code), nil) - if err != nil { - return "", err - } - req.Header.Set("Authorization", model.WeChatServerToken) - - client := http.Client{Timeout: 5 * time.Second} - httpResponse, err := client.Do(req) - if err != nil { - return "", err - } - defer func(body io.ReadCloser) { - if closeErr := body.Close(); closeErr != nil { - slog.Error("failed to close wechat response body", "error", closeErr) - } - }(httpResponse.Body) - - var res wechatLoginResponse - if err := json.NewDecoder(httpResponse.Body).Decode(&res); err != nil { - return "", err - } - if !res.Success { - if strings.TrimSpace(res.Message) == "" { - return "", errors.New(errInvalidParams) - } - return "", errors.New(res.Message) - } - if strings.TrimSpace(res.Data) == "" { - return "", errors.New(errEmailCodeInvalid) - } - return strings.TrimSpace(res.Data), nil -} - -func findUserByShortcutBinding(ctx context.Context, externalID string, sourceNames ...string) (*model.User, error) { - externalID = strings.TrimSpace(externalID) - if externalID == "" { - return nil, gorm.ErrRecordNotFound - } - - query := db.DB(ctx). - Table("w_external_accounts AS ea"). - Select("u.*"). - Joins("JOIN w_users u ON u.id = ea.user_id"). - Where("ea.external_id = ?", externalID) - if len(sourceNames) > 0 { - lowered := make([]string, 0, len(sourceNames)) - for _, name := range sourceNames { - trimmed := strings.ToLower(strings.TrimSpace(name)) - if trimmed != "" { - lowered = append(lowered, trimmed) - } - } - if len(lowered) > 0 { - query = query. - Joins("JOIN w_auth_sources s ON s.id = ea.auth_source_id"). - Where("LOWER(s.name) IN ?", lowered) - } - } - - var user model.User - if err := query.First(&user).Error; err != nil { - return nil, err - } - return &user, nil -} - -func bindShortcutExternalAccount(ctx context.Context, userID uint64, externalID, externalUsername, email string, sourceNames ...string) error { - source, err := resolveShortcutAuthSource(ctx, sourceNames...) - if err != nil { - return err - } - return model.BindExternalAccount(ctx, &model.ExternalAccount{ - AuthSourceID: source.ID, - UserID: userID, - ExternalID: strings.TrimSpace(externalID), - ExternalUsername: strings.TrimSpace(externalUsername), - Email: strings.TrimSpace(email), - }) -} - -func resolveShortcutAuthSource(ctx context.Context, sourceNames ...string) (*model.AuthSource, error) { - for _, name := range sourceNames { - source, err := model.GetAuthSourceByName(ctx, name) - if err == nil { - return source, nil - } - if !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, err - } - } - return nil, errors.New("认证源不存在") -} diff --git a/Wavelet/internal/apps/openflare/compat/auth.go b/Wavelet/internal/apps/openflare/compat/auth.go deleted file mode 100644 index 1660664c..00000000 --- a/Wavelet/internal/apps/openflare/compat/auth.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package compat - -import ( - "github.com/Rain-kl/Wavelet/internal/apps/oauth" - "github.com/Rain-kl/Wavelet/internal/model" - "github.com/gin-gonic/gin" -) - -const openFlareTokenHeader = "OpenFlare-Token" - -// Role constants mirror legacy OpenFlare role values. -const ( - RoleCommonUser = 1 - RoleAdminUser = 10 - RoleRootUser = 100 -) - -// RequireRole ensures the caller is authenticated with at least minRole. -// Phase 1: supports Wavelet session/access-token via oauth.GetUserFromRequest. -func RequireRole(minRole int) gin.HandlerFunc { - return func(c *gin.Context) { - user, err := oauth.GetUserFromRequest(c) - if err != nil || user == nil { - Unauthorized(c, "无权进行此操作,未登录或 token 无效") - c.Abort() - return - } - role := resolveRole(c, user) - if role < minRole { - Fail(c, "无权进行此操作,权限不足") - c.Abort() - return - } - c.Set("of_user_id", user.ID) - c.Set("of_role", role) - c.Set("of_is_admin", user.IsAdmin) - c.Next() - } -} - -// UserAuth requires role >= CommonUser. -func UserAuth() gin.HandlerFunc { return RequireRole(RoleCommonUser) } - -// AdminAuth requires role >= AdminUser. -func AdminAuth() gin.HandlerFunc { return RequireRole(RoleAdminUser) } - -// RootAuth requires role >= RootUser. -func RootAuth() gin.HandlerFunc { return RequireRole(RoleRootUser) } - -func resolveRole(c *gin.Context, user *model.User) int { - if user == nil { - return 0 - } - if tokenAdmin, ok := oauth.GetFromContext[bool](c, oauth.TokenAdminKey); ok && tokenAdmin { - return RoleRootUser - } - if user.IsAdmin { - return RoleRootUser - } - return RoleCommonUser -} - -// OpenFlareTokenHeader returns the legacy auth header name. -func OpenFlareTokenHeader() string { - return openFlareTokenHeader -} - -// BridgeOpenFlareToken maps OpenFlare-Token to X-Access-Token for legacy clients. -func BridgeOpenFlareToken() gin.HandlerFunc { - return func(c *gin.Context) { - if c.GetHeader("X-Access-Token") == "" { - if token := c.GetHeader(openFlareTokenHeader); token != "" { - c.Request.Header.Set("X-Access-Token", token) - } - } - c.Next() - } -} diff --git a/Wavelet/internal/apps/openflare/config_version/routers.go b/Wavelet/internal/apps/openflare/config_version/routers.go index b9684b64..e5ea4e62 100644 --- a/Wavelet/internal/apps/openflare/config_version/routers.go +++ b/Wavelet/internal/apps/openflare/config_version/routers.go @@ -4,37 +4,54 @@ package config_version import ( - "errors" + "net/http" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" + "github.com/Rain-kl/Wavelet/internal/common/response" "github.com/gin-gonic/gin" - "gorm.io/gorm" ) + func handleLogicError(c *gin.Context, err error) bool { if err == nil { return false } - if errors.Is(err, gorm.ErrRecordNotFound) { - compat.Fail(c, "记录不存在") - return true - } - compat.Fail(c, err.Error()) - return true + return apiutil.AbortNotFoundIfMissing(c, err, "记录不存在") } // ListConfigVersionsHandler lists config versions. +// @Summary 获取配置版本列表 +// @Description 返回所有已发布的 OpenResty 配置版本摘要,需要管理员权限 +// @Tags openflare-config-version +// @Produce json +// @Security SessionCookie +// @Success 200 {object} response.Any{data=[]model.ConfigVersionSummary} "配置版本列表" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或不存在" +// @Router /api/v1/custom/openflare/config-versions [get] func ListConfigVersionsHandler(c *gin.Context) { versions, err := ListConfigVersions(c.Request.Context()) if handleLogicError(c, err) { return } - compat.OK(c, versions) + c.JSON(http.StatusOK, response.OK(versions)) } // GetConfigVersionHandler returns a config version by id. +// @Summary 获取配置版本详情 +// @Description 返回指定配置版本的完整快照与渲染内容,需要管理员权限 +// @Tags openflare-config-version +// @Produce json +// @Security SessionCookie +// @Param id path int true "配置版本 ID" +// @Success 200 {object} response.Any{data=model.ConfigVersion} "配置版本详情" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或版本不存在" +// @Router /api/v1/custom/openflare/config-versions/{id} [get] func GetConfigVersionHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } @@ -42,42 +59,77 @@ func GetConfigVersionHandler(c *gin.Context) { if handleLogicError(c, err) { return } - compat.OK(c, version) + c.JSON(http.StatusOK, response.OK(version)) } // GetActiveConfigVersionHandler returns the active config version. +// @Summary 获取当前活跃配置版本 +// @Description 返回当前正在使用的配置版本,需要管理员权限 +// @Tags openflare-config-version +// @Produce json +// @Security SessionCookie +// @Success 200 {object} response.Any{data=model.ConfigVersion} "活跃配置版本" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限、不存在或无活跃版本" +// @Router /api/v1/custom/openflare/config-versions/active [get] func GetActiveConfigVersionHandler(c *gin.Context) { version, err := GetActiveConfigVersion(c.Request.Context()) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - compat.Fail(c, errNoActiveVersion) - return - } - handleLogicError(c, err) + if apiutil.AbortNotFoundIfMissing(c, err, errNoActiveVersion) { return } - compat.OK(c, version) + c.JSON(http.StatusOK, response.OK(version)) } // PreviewConfigVersionHandler previews the current draft configuration. +// @Summary 预览当前草稿配置 +// @Description 渲染并返回当前草稿配置的预览结果,需要管理员权限 +// @Tags openflare-config-version +// @Produce json +// @Security SessionCookie +// @Success 200 {object} response.Any{data=config_version.ConfigPreviewResult} "配置预览" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或不存在" +// @Router /api/v1/custom/openflare/config-versions/preview [get] func PreviewConfigVersionHandler(c *gin.Context) { preview, err := PreviewConfigVersion(c.Request.Context()) if handleLogicError(c, err) { return } - compat.OK(c, preview) + c.JSON(http.StatusOK, response.OK(preview)) } // DiffConfigVersionHandler diffs the current draft against the active version. +// @Summary 对比草稿与活跃配置 +// @Description 对比当前草稿配置与活跃版本之间的差异,需要管理员权限 +// @Tags openflare-config-version +// @Produce json +// @Security SessionCookie +// @Success 200 {object} response.Any{data=config_version.ConfigDiffResult} "配置差异" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或不存在" +// @Router /api/v1/custom/openflare/config-versions/diff [get] func DiffConfigVersionHandler(c *gin.Context) { diff, err := DiffConfigVersion(c.Request.Context()) if handleLogicError(c, err) { return } - compat.OK(c, diff) + c.JSON(http.StatusOK, response.OK(diff)) } // PublishConfigVersionHandler publishes a new config version. +// @Summary 发布配置版本 +// @Description 将当前草稿配置发布为新版本,需要管理员权限 +// @Tags openflare-config-version +// @Produce json +// @Security SessionCookie +// @Param force query bool false "是否强制发布" +// @Success 200 {object} response.Any{data=model.ConfigVersion} "发布成功" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或不存在" +// @Router /api/v1/custom/openflare/config-versions/publish [post] func PublishConfigVersionHandler(c *gin.Context) { username := c.GetString("username") force := c.Query("force") == "true" @@ -85,12 +137,23 @@ func PublishConfigVersionHandler(c *gin.Context) { if handleLogicError(c, err) { return } - compat.OK(c, version) + c.JSON(http.StatusOK, response.OK(version)) } // ActivateConfigVersionHandler activates an existing config version. +// @Summary 激活配置版本 +// @Description 将指定历史版本设为当前活跃配置,需要管理员权限 +// @Tags openflare-config-version +// @Produce json +// @Security SessionCookie +// @Param id path int true "配置版本 ID" +// @Success 200 {object} response.Any{data=model.ConfigVersion} "激活成功" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或版本不存在" +// @Router /api/v1/custom/openflare/config-versions/{id}/activate [post] func ActivateConfigVersionHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } @@ -98,18 +161,30 @@ func ActivateConfigVersionHandler(c *gin.Context) { if handleLogicError(c, err) { return } - compat.OK(c, version) + c.JSON(http.StatusOK, response.OK(version)) } // CleanupConfigVersionsHandler removes old inactive config versions. +// @Summary 清理历史配置版本 +// @Description 删除超出保留数量的非活跃配置版本,需要管理员权限 +// @Tags openflare-config-version +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param body body config_version.CleanupInput true "清理参数" +// @Success 200 {object} response.Any{data=config_version.CleanupResult} "清理结果" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或不存在" +// @Router /api/v1/custom/openflare/config-versions/cleanup [post] func CleanupConfigVersionsHandler(c *gin.Context) { var input CleanupInput - if !compat.BindJSON(c, &input) { + if !apiutil.BindJSON(c, &input) { return } result, err := CleanupConfigVersions(c.Request.Context(), input.KeepCount) if handleLogicError(c, err) { return } - compat.OK(c, result) -} + c.JSON(http.StatusOK, response.OK(result)) +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/dashboard/routers.go b/Wavelet/internal/apps/openflare/dashboard/routers.go index 30f7cf65..9263b2b8 100644 --- a/Wavelet/internal/apps/openflare/dashboard/routers.go +++ b/Wavelet/internal/apps/openflare/dashboard/routers.go @@ -4,24 +4,29 @@ package dashboard import ( - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "net/http" + + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" + "github.com/Rain-kl/Wavelet/internal/common/response" "github.com/gin-gonic/gin" ) -// RegisterRoutes mounts legacy OpenFlare dashboard routes. -func RegisterRoutes(apiGroup *gin.RouterGroup) { - dashboardRoute := apiGroup.Group("/dashboard") - dashboardRoute.Use(compat.AdminAuth()) - { - dashboardRoute.GET("/overview", getOverviewHandler) - } -} - -func getOverviewHandler(c *gin.Context) { +// GetOverviewHandler 获取仪表盘概览数据。 +// @Summary 获取仪表盘概览 +// @Description 聚合节点与可观测性数据,返回 OpenFlare 控制台仪表盘概览,需要管理员权限 +// @Tags openflare-dashboard +// @Produce json +// @Security SessionCookie +// @Success 200 {object} response.Any{data=dashboard.OverviewPayload} "仪表盘概览" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/dashboard/overview [get] +func GetOverviewHandler(c *gin.Context) { overview, err := GetOverview(c.Request.Context()) - if err != nil { - compat.Fail(c, err.Error()) + if apiutil.AbortBadRequestOnError(c, err) { return } - compat.OK(c, overview) -} + c.JSON(http.StatusOK, response.OK(overview)) +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/flared/routers.go b/Wavelet/internal/apps/openflare/flared/routers.go index a3816837..b5dd4f90 100644 --- a/Wavelet/internal/apps/openflare/flared/routers.go +++ b/Wavelet/internal/apps/openflare/flared/routers.go @@ -10,6 +10,7 @@ import ( "github.com/gin-gonic/gin" ) + // PostHeartbeat handles POST /flared/heartbeat. func PostHeartbeat(c *gin.Context) { var payload HeartbeatPayload diff --git a/Wavelet/internal/apps/openflare/integration/agent_protocol_test.go b/Wavelet/internal/apps/openflare/integration/agent_protocol_test.go index c17bf2e8..aa9a19ff 100644 --- a/Wavelet/internal/apps/openflare/integration/agent_protocol_test.go +++ b/Wavelet/internal/apps/openflare/integration/agent_protocol_test.go @@ -10,11 +10,11 @@ import ( "testing" "github.com/Rain-kl/Wavelet/internal/apps/openflare/agent" - oflegacy "github.com/Rain-kl/Wavelet/internal/apps/openflare/legacy" ofnode "github.com/Rain-kl/Wavelet/internal/apps/openflare/node" "github.com/Rain-kl/Wavelet/internal/apps/openflare/option" "github.com/Rain-kl/Wavelet/internal/db" "github.com/Rain-kl/Wavelet/internal/model" + "github.com/Rain-kl/Wavelet/internal/testhelper" "github.com/gin-gonic/gin" "github.com/glebarez/sqlite" "github.com/stretchr/testify/assert" @@ -58,10 +58,8 @@ func setupProtocolTestEnv(t *testing.T) (*gin.Engine, func()) { option.ResetInitializationForTest() agent.ResetAuthCacheForTest() - gin.SetMode(gin.TestMode) - engine := gin.New() - apiGroup := engine.Group("/api") - oflegacy.RegisterRoutes(apiGroup) + engine := testhelper.NewTestGinEngine() + mountOpenFlareTestRoutes(engine) cleanup := func() { db.SetDB(nil) diff --git a/Wavelet/internal/apps/openflare/integration/auth_option_test.go b/Wavelet/internal/apps/openflare/integration/auth_option_test.go index 60f9202a..08b170c8 100644 --- a/Wavelet/internal/apps/openflare/integration/auth_option_test.go +++ b/Wavelet/internal/apps/openflare/integration/auth_option_test.go @@ -8,13 +8,14 @@ import ( "net/http" "testing" + "github.com/Rain-kl/Wavelet/internal/apps/admin" + "github.com/Rain-kl/Wavelet/internal/apps/cap" "github.com/Rain-kl/Wavelet/internal/apps/oauth" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" - oflegacy "github.com/Rain-kl/Wavelet/internal/apps/openflare/legacy" "github.com/Rain-kl/Wavelet/internal/apps/openflare/option" "github.com/Rain-kl/Wavelet/internal/config" "github.com/Rain-kl/Wavelet/internal/db/idgen" "github.com/Rain-kl/Wavelet/internal/model" + "github.com/Rain-kl/Wavelet/internal/repository" "github.com/Rain-kl/Wavelet/internal/testhelper" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" @@ -28,11 +29,6 @@ type statusPayload struct { SystemName string `json:"system_name"` } -type legacyUserPayload struct { - Username string `json:"username"` - Token string `json:"token"` -} - func setupAuthOptionIntegration(t *testing.T) (*gorm.DB, *gin.Engine) { t.Helper() @@ -43,6 +39,12 @@ func setupAuthOptionIntegration(t *testing.T) (*gorm.DB, *gin.Engine) { option.ResetInitializationForTest() t.Cleanup(option.ResetInitializationForTest) + require.NoError(t, dbConn.Model(&model.SystemConfig{}). + Where("key = ?", model.ConfigKeyCapLoginEnabled). + Update("value", "false").Error) + require.NoError(t, repository.InvalidateSystemConfigCache(context.Background(), model.ConfigKeyCapLoginEnabled)) + cap.InvalidateRuntimeSettings() + oldCookieName := config.Config.App.SessionCookieName oldSecret := config.Config.App.SessionSecret oldDomain := config.Config.App.SessionDomain @@ -65,9 +67,7 @@ func setupAuthOptionIntegration(t *testing.T) (*gorm.DB, *gin.Engine) { store := cookie.NewStore([]byte(config.Config.App.SessionSecret)) store.Options(oauth.GetSessionOptions(3600)) r := testhelper.NewTestGinEngine(sessions.Sessions(config.Config.App.SessionCookieName, store)) - - api := r.Group("/api") - oflegacy.RegisterRoutes(api) + mountOpenFlareTestRoutes(r) return dbConn, r } @@ -88,134 +88,88 @@ func seedUser(t *testing.T, dbConn *gorm.DB, username, password string, isAdmin return user } +func seedUserWithAccessToken(t *testing.T, dbConn *gorm.DB, username, password string, isAdmin bool) string { + t.Helper() + + user := seedUser(t, dbConn, username, password, isAdmin) + + token, err := model.GenerateTokenString() + require.NoError(t, err) + + tokenRecord := model.AccessToken{ + UserID: user.ID, + Name: username + "-integration-token", + TokenHash: model.HashToken(token), + MaskedToken: model.MaskTokenString(token), + IsAdmin: isAdmin, + } + require.NoError(t, dbConn.Create(&tokenRecord).Error) + return token +} + func TestGETStatusReturnsSuccessEnvelope(t *testing.T) { _, r := setupAuthOptionIntegration(t) - w := performJSONRequest(t, r, http.MethodGet, "/api/status", nil, nil) + w := performJSONRequest(t, r, http.MethodGet, apiPath("/status"), nil, nil) assert.Equal(t, http.StatusOK, w.Code) - env := decodeEnvelope(t, w) - assert.True(t, env.Success, "message=%s", env.Message) + resp := requireAPIOK(t, w) var status statusPayload - unmarshalEnvelopeData(t, env.Data, &status) + unmarshalAPIData(t, resp.Data, &status) assert.NotEmpty(t, status.SystemName) } -func TestPOSTUserLoginWithSeededUser(t *testing.T) { - dbConn, r := setupAuthOptionIntegration(t) - seedUser(t, dbConn, "testuser", "password123", false) - - w := performJSONRequest(t, r, http.MethodPost, "/api/user/login", map[string]string{ - "username": "testuser", - "password": "password123", - }, nil) - - assert.Equal(t, http.StatusOK, w.Code) - env := decodeEnvelope(t, w) - assert.True(t, env.Success, "message=%s", env.Message) - - var user legacyUserPayload - unmarshalEnvelopeData(t, env.Data, &user) - assert.Equal(t, "testuser", user.Username) - assert.NotEmpty(t, user.Token) -} - -func TestGETUserSelfWithToken(t *testing.T) { - dbConn, r := setupAuthOptionIntegration(t) - seedUser(t, dbConn, "selfuser", "password123", false) - - loginResp := performJSONRequest(t, r, http.MethodPost, "/api/user/login", map[string]string{ - "username": "selfuser", - "password": "password123", - }, nil) - loginEnv := decodeEnvelope(t, loginResp) - require.True(t, loginEnv.Success, "login failed: %s", loginEnv.Message) - - var loginUser legacyUserPayload - unmarshalEnvelopeData(t, loginEnv.Data, &loginUser) - require.NotEmpty(t, loginUser.Token) - - w := performJSONRequest(t, r, http.MethodGet, "/api/user/self", nil, map[string]string{ - compat.OpenFlareTokenHeader(): loginUser.Token, - }) - - assert.Equal(t, http.StatusOK, w.Code) - env := decodeEnvelope(t, w) - assert.True(t, env.Success, "message=%s", env.Message) - - var self legacyUserPayload - unmarshalEnvelopeData(t, env.Data, &self) - assert.Equal(t, "selfuser", self.Username) -} - func TestGETOptionRequiresRootAuth(t *testing.T) { dbConn, r := setupAuthOptionIntegration(t) - seedUser(t, dbConn, "commonuser", "password123", false) - seedUser(t, dbConn, "rootuser", "password123", true) - - commonToken := loginAndGetToken(t, r, "commonuser", "password123") - rootToken := loginAndGetToken(t, r, "rootuser", "password123") + commonToken := seedUserWithAccessToken(t, dbConn, "commonuser", "password123", false) + rootToken := seedUserWithAccessToken(t, dbConn, "rootuser", "password123", true) t.Run("unauthenticated", func(t *testing.T) { - w := performJSONRequest(t, r, http.MethodGet, "/api/option/", nil, nil) + w := performJSONRequest(t, r, http.MethodGet, apiPath("/option/"), nil, nil) assert.Equal(t, http.StatusUnauthorized, w.Code) - env := decodeEnvelope(t, w) - assert.False(t, env.Success) + resp := decodeAPIResponse(t, w) + assert.NotEmpty(t, resp.ErrorMsg) }) t.Run("common user forbidden", func(t *testing.T) { - w := performJSONRequest(t, r, http.MethodGet, "/api/option/", nil, map[string]string{ - compat.OpenFlareTokenHeader(): commonToken, - }) - assert.Equal(t, http.StatusOK, w.Code) - env := decodeEnvelope(t, w) - assert.False(t, env.Success) - assert.Contains(t, env.Message, "权限不足") + w := performJSONRequest(t, r, http.MethodGet, apiPath("/option/"), nil, adminAuthHeaders(commonToken)) + assert.Equal(t, http.StatusNotFound, w.Code) + resp := decodeAPIResponse(t, w) + assert.Equal(t, admin.TokenAdminRequired, resp.ErrorMsg) }) t.Run("root user allowed", func(t *testing.T) { - w := performJSONRequest(t, r, http.MethodGet, "/api/option/", nil, map[string]string{ - compat.OpenFlareTokenHeader(): rootToken, - }) + w := performJSONRequest(t, r, http.MethodGet, apiPath("/option/"), nil, adminAuthHeaders(rootToken)) assert.Equal(t, http.StatusOK, w.Code) - env := decodeEnvelope(t, w) - assert.True(t, env.Success, "message=%s", env.Message) + requireAPIOK(t, w) }) } -func TestGETNodesWithOpenFlareToken(t *testing.T) { +func TestGETNodesWithAccessToken(t *testing.T) { dbConn, r := setupAuthOptionIntegration(t) require.NoError(t, dbConn.AutoMigrate(&model.OpenFlareNode{})) - seedUser(t, dbConn, "admin", "password123", true) - rootToken := loginAndGetToken(t, r, "admin", "password123") + rootToken := seedUserWithAccessToken(t, dbConn, "admin", "password123", true) - w := performJSONRequest(t, r, http.MethodGet, "/api/nodes/", nil, map[string]string{ - compat.OpenFlareTokenHeader(): rootToken, - }) + w := performJSONRequest(t, r, http.MethodGet, apiPath("/nodes/"), nil, adminAuthHeaders(rootToken)) assert.Equal(t, http.StatusOK, w.Code) - env := decodeEnvelope(t, w) - assert.True(t, env.Success, "message=%s", env.Message) + requireAPIOK(t, w) } func TestOptionHotReloadAfterUpdate(t *testing.T) { dbConn, r := setupAuthOptionIntegration(t) - seedUser(t, dbConn, "admin", "password123", true) - rootToken := loginAndGetToken(t, r, "admin", "password123") + rootToken := seedUserWithAccessToken(t, dbConn, "admin", "password123", true) statusBefore := getStatusSystemName(t, r, nil) assert.NotEmpty(t, statusBefore) - updateResp := performJSONRequest(t, r, http.MethodPost, "/api/option/update", map[string]string{ + updateResp := performJSONRequest(t, r, http.MethodPost, apiPath("/option/update"), map[string]string{ "key": "SystemName", "value": "HotReloadIntegration", - }, map[string]string{ - compat.OpenFlareTokenHeader(): rootToken, - }) + }, adminAuthHeaders(rootToken)) assert.Equal(t, http.StatusOK, updateResp.Code) - updateEnv := decodeEnvelope(t, updateResp) - assert.True(t, updateEnv.Success, "message=%s", updateEnv.Message) + requireAPIOK(t, updateResp) statusAfter := getStatusSystemName(t, r, nil) assert.Equal(t, "HotReloadIntegration", statusAfter) @@ -226,31 +180,14 @@ func TestOptionHotReloadAfterUpdate(t *testing.T) { assert.Equal(t, "HotReloadIntegration", model.OptionValue("SystemName")) } -func loginAndGetToken(t *testing.T, r http.Handler, username, password string) string { - t.Helper() - - w := performJSONRequest(t, r, http.MethodPost, "/api/user/login", map[string]string{ - "username": username, - "password": password, - }, nil) - env := decodeEnvelope(t, w) - require.True(t, env.Success, "login failed: %s", env.Message) - - var user legacyUserPayload - unmarshalEnvelopeData(t, env.Data, &user) - require.NotEmpty(t, user.Token) - return user.Token -} - func getStatusSystemName(t *testing.T, r http.Handler, headers map[string]string) string { t.Helper() - w := performJSONRequest(t, r, http.MethodGet, "/api/status", nil, headers) + w := performJSONRequest(t, r, http.MethodGet, apiPath("/status"), nil, headers) require.Equal(t, http.StatusOK, w.Code) - env := decodeEnvelope(t, w) - require.True(t, env.Success, "message=%s", env.Message) + resp := requireAPIOK(t, w) var status statusPayload - unmarshalEnvelopeData(t, env.Data, &status) + unmarshalAPIData(t, resp.Data, &status) return status.SystemName -} +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/integration/core_chain_test.go b/Wavelet/internal/apps/openflare/integration/core_chain_test.go index d37c5f85..ca5c8627 100644 --- a/Wavelet/internal/apps/openflare/integration/core_chain_test.go +++ b/Wavelet/internal/apps/openflare/integration/core_chain_test.go @@ -9,10 +9,10 @@ import ( "time" "github.com/Rain-kl/Wavelet/internal/apps/openflare/agent" - oflegacy "github.com/Rain-kl/Wavelet/internal/apps/openflare/legacy" "github.com/Rain-kl/Wavelet/internal/apps/openflare/option" "github.com/Rain-kl/Wavelet/internal/db" "github.com/Rain-kl/Wavelet/internal/model" + "github.com/Rain-kl/Wavelet/internal/testhelper" "github.com/gin-gonic/gin" "github.com/glebarez/sqlite" "github.com/stretchr/testify/assert" @@ -59,10 +59,8 @@ func setupCoreChainTest(t *testing.T) (*gin.Engine, adminSeed, func()) { seed, err := seedAdminWithAccessToken(sqliteDB) require.NoError(t, err) - gin.SetMode(gin.TestMode) - engine := gin.New() - apiGroup := engine.Group("/api") - oflegacy.RegisterRoutes(apiGroup) + engine := testhelper.NewTestGinEngine() + mountOpenFlareTestRoutes(engine) cleanup := func() { db.SetDB(nil) @@ -125,7 +123,7 @@ func TestCoreChainMigrationFlow(t *testing.T) { ) t.Run("create origin", func(t *testing.T) { - rec := performJSONRequest(t, engine, http.MethodPost, "/api/origins/", map[string]any{ + rec := performJSONRequest(t, engine, http.MethodPost, apiPath("/origins/"), map[string]any{ "name": "Primary Origin", "address": "origin.core-chain.internal", "remark": "integration upstream", @@ -134,10 +132,8 @@ func TestCoreChainMigrationFlow(t *testing.T) { }) require.Equal(t, http.StatusOK, rec.Code) - envelope := decodeEnvelope(t, rec) - require.True(t, envelope.Success, envelope.Message) - - data := unmarshalEnvelopeMap(t, envelope.Data) + resp := requireAPIOK(t, rec) + data := unmarshalAPIMap(t, resp.Data) originID = uint(data["id"].(float64)) assert.NotZero(t, originID) assert.Equal(t, "Primary Origin", data["name"]) @@ -145,7 +141,7 @@ func TestCoreChainMigrationFlow(t *testing.T) { }) t.Run("create proxy route linked to origin", func(t *testing.T) { - rec := performJSONRequest(t, engine, http.MethodPost, "/api/proxy-routes/", map[string]any{ + rec := performJSONRequest(t, engine, http.MethodPost, apiPath("/proxy-routes/"), map[string]any{ "site_name": "core-chain-site", "domain": "core-chain.example.com", "origin_id": originID, @@ -157,10 +153,8 @@ func TestCoreChainMigrationFlow(t *testing.T) { }) require.Equal(t, http.StatusOK, rec.Code) - envelope := decodeEnvelope(t, rec) - require.True(t, envelope.Success, envelope.Message) - - data := unmarshalEnvelopeMap(t, envelope.Data) + resp := requireAPIOK(t, rec) + data := unmarshalAPIMap(t, resp.Data) proxyRouteID = uint(data["id"].(float64)) assert.NotZero(t, proxyRouteID) assert.Equal(t, "core-chain-site", data["site_name"]) @@ -170,35 +164,32 @@ func TestCoreChainMigrationFlow(t *testing.T) { }) t.Run("publish config version", func(t *testing.T) { - rec := performJSONRequest(t, engine, http.MethodPost, "/api/config-versions/publish", nil, map[string]string{ + rec := performJSONRequest(t, engine, http.MethodPost, apiPath("/config-versions/publish"), nil, map[string]string{ "X-Access-Token": seed.Token, }) require.Equal(t, http.StatusOK, rec.Code) - envelope := decodeEnvelope(t, rec) - require.True(t, envelope.Success, envelope.Message) - - data := unmarshalEnvelopeMap(t, envelope.Data) + resp := requireAPIOK(t, rec) + data := unmarshalAPIMap(t, resp.Data) configVersion, _ = data["version"].(string) configChecksum, _ = data["checksum"].(string) assert.NotEmpty(t, configVersion) assert.NotEmpty(t, configChecksum) assert.Equal(t, true, data["is_active"]) - activeRec := performJSONRequest(t, engine, http.MethodGet, "/api/config-versions/active", nil, map[string]string{ + activeRec := performJSONRequest(t, engine, http.MethodGet, apiPath("/config-versions/active"), nil, map[string]string{ "X-Access-Token": seed.Token, }) require.Equal(t, http.StatusOK, activeRec.Code) - activeEnvelope := decodeEnvelope(t, activeRec) - require.True(t, activeEnvelope.Success, activeEnvelope.Message) + activeResp := requireAPIOK(t, activeRec) - activeData := unmarshalEnvelopeMap(t, activeEnvelope.Data) + activeData := unmarshalAPIMap(t, activeResp.Data) assert.Equal(t, configVersion, activeData["version"]) assert.Equal(t, configChecksum, activeData["checksum"]) }) t.Run("create node", func(t *testing.T) { - rec := performJSONRequest(t, engine, http.MethodPost, "/api/nodes/", map[string]any{ + rec := performJSONRequest(t, engine, http.MethodPost, apiPath("/nodes/"), map[string]any{ "name": "edge-core-chain", "ip": "10.10.0.1", "auto_update_enabled": true, @@ -207,10 +198,8 @@ func TestCoreChainMigrationFlow(t *testing.T) { }) require.Equal(t, http.StatusOK, rec.Code) - envelope := decodeEnvelope(t, rec) - require.True(t, envelope.Success, envelope.Message) - - data := unmarshalEnvelopeMap(t, envelope.Data) + resp := requireAPIOK(t, rec) + data := unmarshalAPIMap(t, resp.Data) nodeID = uint(data["id"].(float64)) nodePublicID, _ = data["node_id"].(string) agentToken, _ = data["access_token"].(string) @@ -248,7 +237,7 @@ func TestCoreChainMigrationFlow(t *testing.T) { t, engine, http.MethodGet, - "/api/apply-logs/?node_id="+nodePublicID+"&pageNo=1&pageSize=10", + apiPath("/apply-logs/?node_id="+nodePublicID+"&pageNo=1&pageSize=10"), nil, map[string]string{ "X-Access-Token": seed.Token, @@ -256,10 +245,8 @@ func TestCoreChainMigrationFlow(t *testing.T) { ) require.Equal(t, http.StatusOK, listRec.Code) - listEnvelope := decodeEnvelope(t, listRec) - require.True(t, listEnvelope.Success, listEnvelope.Message) - - listData := unmarshalEnvelopeMap(t, listEnvelope.Data) + listResp := requireAPIOK(t, listRec) + listData := unmarshalAPIMap(t, listResp.Data) assert.Equal(t, float64(1), listData["total"]) rows, ok := listData["rows"].([]any) @@ -271,14 +258,13 @@ func TestCoreChainMigrationFlow(t *testing.T) { assert.Equal(t, configVersion, row["version"]) assert.Equal(t, "success", row["result"]) - nodeRec := performJSONRequest(t, engine, http.MethodGet, "/api/nodes/", nil, map[string]string{ + nodeRec := performJSONRequest(t, engine, http.MethodGet, apiPath("/nodes/"), nil, map[string]string{ "X-Access-Token": seed.Token, }) require.Equal(t, http.StatusOK, nodeRec.Code) - nodeEnvelope := decodeEnvelope(t, nodeRec) - require.True(t, nodeEnvelope.Success, nodeEnvelope.Message) + nodeResp := requireAPIOK(t, nodeRec) - nodes := unmarshalEnvelopeSlice(t, nodeEnvelope.Data) + nodes := unmarshalAPISlice(t, nodeResp.Data) require.Len(t, nodes, 1) nodeView, ok := nodes[0].(map[string]any) require.True(t, ok) @@ -288,4 +274,4 @@ func TestCoreChainMigrationFlow(t *testing.T) { assert.Equal(t, configChecksum, nodeView["latest_apply_checksum"]) assert.Equal(t, float64(2), nodeView["latest_support_file_count"]) }) -} +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/integration/helpers_test.go b/Wavelet/internal/apps/openflare/integration/helpers_test.go index 775c9fce..cfa5f4ba 100644 --- a/Wavelet/internal/apps/openflare/integration/helpers_test.go +++ b/Wavelet/internal/apps/openflare/integration/helpers_test.go @@ -11,9 +11,29 @@ import ( "testing" "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "github.com/Rain-kl/Wavelet/internal/common/response" + v1 "github.com/Rain-kl/Wavelet/internal/router/v1" + ofrouter "github.com/Rain-kl/Wavelet/internal/router/v1/openflare" + "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" ) +func decodeAPIResponse(t *testing.T, rec *httptest.ResponseRecorder) response.Any { + t.Helper() + + var resp response.Any + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp)) + return resp +} + +func requireAPIOK(t *testing.T, rec *httptest.ResponseRecorder) response.Any { + t.Helper() + + resp := decodeAPIResponse(t, rec) + require.Empty(t, resp.ErrorMsg, "unexpected API error: %s", resp.ErrorMsg) + return resp +} + func decodeEnvelope(t *testing.T, rec *httptest.ResponseRecorder) compat.Envelope { t.Helper() @@ -22,7 +42,7 @@ func decodeEnvelope(t *testing.T, rec *httptest.ResponseRecorder) compat.Envelop return envelope } -func unmarshalEnvelopeData(t *testing.T, data any, target any) { +func unmarshalAPIData(t *testing.T, data any, target any) { t.Helper() payload, err := json.Marshal(data) @@ -30,20 +50,47 @@ func unmarshalEnvelopeData(t *testing.T, data any, target any) { require.NoError(t, json.Unmarshal(payload, target)) } -func unmarshalEnvelopeMap(t *testing.T, data any) map[string]any { +func unmarshalEnvelopeData(t *testing.T, data any, target any) { + t.Helper() + unmarshalAPIData(t, data, target) +} + +func unmarshalAPIMap(t *testing.T, data any) map[string]any { t.Helper() var result map[string]any - unmarshalEnvelopeData(t, data, &result) + unmarshalAPIData(t, data, &result) + return result +} + +func unmarshalEnvelopeMap(t *testing.T, data any) map[string]any { + t.Helper() + return unmarshalAPIMap(t, data) +} + +func unmarshalAPISlice(t *testing.T, data any) []any { + t.Helper() + + var result []any + unmarshalAPIData(t, data, &result) return result } func unmarshalEnvelopeSlice(t *testing.T, data any) []any { t.Helper() + return unmarshalAPISlice(t, data) +} - var result []any - unmarshalEnvelopeData(t, data, &result) - return result +func mountOpenFlareTestRoutes(engine *gin.Engine) { + api := engine.Group("/api") + ofrouter.RegisterRoutes(api) + + apiV1 := api.Group("/v1") + v1.RegisterV1Routes(apiV1, api) +} + +func apiPath(subpath string) string { + return ofrouter.V1BasePath + subpath } func performJSONRequest( diff --git a/Wavelet/internal/apps/openflare/integration/security_test.go b/Wavelet/internal/apps/openflare/integration/security_test.go index 069fd584..97bc16df 100644 --- a/Wavelet/internal/apps/openflare/integration/security_test.go +++ b/Wavelet/internal/apps/openflare/integration/security_test.go @@ -15,7 +15,6 @@ import ( "testing" "time" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/legacy" "github.com/Rain-kl/Wavelet/internal/apps/openflare/option" "github.com/Rain-kl/Wavelet/internal/config" "github.com/Rain-kl/Wavelet/internal/db" @@ -59,8 +58,7 @@ func setupSecurityTest(t *testing.T) (*gin.Engine, adminSeed, func()) { config.Config.App.SessionSecret = "test_session_secret_for_security_integration" engine := testhelper.NewTestGinEngine() - apiGroup := engine.Group("/api") - legacy.RegisterRoutes(apiGroup) + mountOpenFlareTestRoutes(engine) cleanup := func() { config.Config.App.SessionSecret = oldSecret @@ -110,7 +108,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { ) t.Run("WAF rule group create", func(t *testing.T) { - rec := performLegacyRequest(t, engine, http.MethodPost, "/api/waf/rule-groups", map[string]any{ + rec := performLegacyRequest(t, engine, http.MethodPost, apiPath("/waf/rule-groups"), map[string]any{ "name": "edge-security", "enabled": true, "block_status_code": 403, @@ -121,10 +119,8 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { }, adminAuthHeaders(seed.Token)) require.Equal(t, http.StatusOK, rec.Code) - envelope := decodeEnvelope(t, rec) - require.True(t, envelope.Success, envelope.Message) - - data := unmarshalEnvelopeMap(t, envelope.Data) + resp := requireAPIOK(t, rec) + data := unmarshalAPIMap(t, resp.Data) ruleGroupID = uint(data["id"].(float64)) assert.NotZero(t, ruleGroupID) assert.Equal(t, "edge-security", data["name"]) @@ -133,13 +129,11 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { }) t.Run("WAF rule group list includes global and custom groups", func(t *testing.T) { - rec := performLegacyRequest(t, engine, http.MethodGet, "/api/waf/rule-groups", nil, adminAuthHeaders(seed.Token)) + rec := performLegacyRequest(t, engine, http.MethodGet, apiPath("/waf/rule-groups"), nil, adminAuthHeaders(seed.Token)) require.Equal(t, http.StatusOK, rec.Code) - envelope := decodeEnvelope(t, rec) - require.True(t, envelope.Success, envelope.Message) - - groups := unmarshalEnvelopeSlice(t, envelope.Data) + resp := requireAPIOK(t, rec) + groups := unmarshalAPISlice(t, resp.Data) require.GreaterOrEqual(t, len(groups), 2) foundCustom := false @@ -164,16 +158,14 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { t, engine, http.MethodGet, - fmt.Sprintf("/api/waf/rule-groups/%d", ruleGroupID), + fmt.Sprintf("%s/waf/rule-groups/%d", apiPath(""), ruleGroupID), nil, adminAuthHeaders(seed.Token), ) require.Equal(t, http.StatusOK, rec.Code) - envelope := decodeEnvelope(t, rec) - require.True(t, envelope.Success, envelope.Message) - - data := unmarshalEnvelopeMap(t, envelope.Data) + resp := requireAPIOK(t, rec) + data := unmarshalAPIMap(t, resp.Data) assert.Equal(t, float64(ruleGroupID), data["id"]) assert.Equal(t, "edge-security", data["name"]) }) @@ -183,7 +175,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { t, engine, http.MethodPost, - fmt.Sprintf("/api/waf/rule-groups/%d/update", ruleGroupID), + fmt.Sprintf("%s/waf/rule-groups/%d/update", apiPath(""), ruleGroupID), map[string]any{ "name": "edge-security-updated", "enabled": true, @@ -194,16 +186,14 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { ) require.Equal(t, http.StatusOK, rec.Code) - envelope := decodeEnvelope(t, rec) - require.True(t, envelope.Success, envelope.Message) - - data := unmarshalEnvelopeMap(t, envelope.Data) + resp := requireAPIOK(t, rec) + data := unmarshalAPIMap(t, resp.Data) assert.Equal(t, "edge-security-updated", data["name"]) assert.Equal(t, float64(451), data["block_status_code"]) }) t.Run("WAF IP group create", func(t *testing.T) { - rec := performLegacyRequest(t, engine, http.MethodPost, "/api/waf/ip-groups", map[string]any{ + rec := performLegacyRequest(t, engine, http.MethodPost, apiPath("/waf/ip-groups"), map[string]any{ "name": "blocked-ips", "type": "manual", "enabled": true, @@ -212,10 +202,8 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { }, adminAuthHeaders(seed.Token)) require.Equal(t, http.StatusOK, rec.Code) - envelope := decodeEnvelope(t, rec) - require.True(t, envelope.Success, envelope.Message) - - data := unmarshalEnvelopeMap(t, envelope.Data) + resp := requireAPIOK(t, rec) + data := unmarshalAPIMap(t, resp.Data) ipGroupID = uint(data["id"].(float64)) assert.NotZero(t, ipGroupID) assert.Equal(t, "blocked-ips", data["name"]) @@ -223,7 +211,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { }) t.Run("create proxy route for WAF binding", func(t *testing.T) { - rec := performLegacyRequest(t, engine, http.MethodPost, "/api/proxy-routes/", map[string]any{ + rec := performLegacyRequest(t, engine, http.MethodPost, apiPath("/proxy-routes/"), map[string]any{ "site_name": "security-site", "domain": "security.example.com", "origin_url": "http://origin.security.internal:8080", @@ -231,10 +219,8 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { }, adminAuthHeaders(seed.Token)) require.Equal(t, http.StatusOK, rec.Code) - envelope := decodeEnvelope(t, rec) - require.True(t, envelope.Success, envelope.Message) - - data := unmarshalEnvelopeMap(t, envelope.Data) + resp := requireAPIOK(t, rec) + data := unmarshalAPIMap(t, resp.Data) proxyRouteID = uint(data["id"].(float64)) assert.NotZero(t, proxyRouteID) assert.Equal(t, "security.example.com", data["domain"]) @@ -245,7 +231,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { t, engine, http.MethodPost, - fmt.Sprintf("/api/waf/sites/%d/rule-groups", proxyRouteID), + fmt.Sprintf("%s/waf/sites/%d/rule-groups", apiPath(""), proxyRouteID), map[string]any{ "ids": []uint{ruleGroupID}, }, @@ -253,10 +239,8 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { ) require.Equal(t, http.StatusOK, rec.Code) - envelope := decodeEnvelope(t, rec) - require.True(t, envelope.Success, envelope.Message) - - data := unmarshalEnvelopeMap(t, envelope.Data) + resp := requireAPIOK(t, rec) + data := unmarshalAPIMap(t, resp.Data) assert.Equal(t, float64(proxyRouteID), data["route_id"]) appliedIDs, ok := data["applied_ids"].([]any) @@ -270,16 +254,14 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { t, engine, http.MethodGet, - fmt.Sprintf("/api/waf/sites/%d/rule-groups", proxyRouteID), + fmt.Sprintf("%s/waf/sites/%d/rule-groups", apiPath(""), proxyRouteID), nil, adminAuthHeaders(seed.Token), ) require.Equal(t, http.StatusOK, rec.Code) - envelope := decodeEnvelope(t, rec) - require.True(t, envelope.Success, envelope.Message) - - data := unmarshalEnvelopeMap(t, envelope.Data) + resp := requireAPIOK(t, rec) + data := unmarshalAPIMap(t, resp.Data) assert.NotNil(t, data["global_rule_group"]) appliedGroups, ok := data["applied_rule_groups"].([]any) @@ -293,7 +275,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { t.Run("create TLS certificate with PEM", func(t *testing.T) { certPEM, keyPEM := generateSelfSignedCertificatePair(t, []string{"security.example.com"}) - rec := performLegacyRequest(t, engine, http.MethodPost, "/api/tls-certificates/", map[string]any{ + rec := performLegacyRequest(t, engine, http.MethodPost, apiPath("/tls-certificates/"), map[string]any{ "name": "security-cert", "cert_pem": certPEM, "key_pem": keyPEM, @@ -301,10 +283,8 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { }, adminAuthHeaders(seed.Token)) require.Equal(t, http.StatusOK, rec.Code) - envelope := decodeEnvelope(t, rec) - require.True(t, envelope.Success, envelope.Message) - - data := unmarshalEnvelopeMap(t, envelope.Data) + resp := requireAPIOK(t, rec) + data := unmarshalAPIMap(t, resp.Data) certID = uint(data["id"].(float64)) assert.NotZero(t, certID) assert.Equal(t, "security-cert", data["name"]) @@ -312,7 +292,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { }) t.Run("create managed domain", func(t *testing.T) { - rec := performLegacyRequest(t, engine, http.MethodPost, "/api/managed-domains/", map[string]any{ + rec := performLegacyRequest(t, engine, http.MethodPost, apiPath("/managed-domains/"), map[string]any{ "domain": "security.example.com", "cert_id": certID, "enabled": true, @@ -320,10 +300,8 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { }, adminAuthHeaders(seed.Token)) require.Equal(t, http.StatusOK, rec.Code) - envelope := decodeEnvelope(t, rec) - require.True(t, envelope.Success, envelope.Message) - - data := unmarshalEnvelopeMap(t, envelope.Data) + resp := requireAPIOK(t, rec) + data := unmarshalAPIMap(t, resp.Data) domainID = uint(data["id"].(float64)) assert.NotZero(t, domainID) assert.Equal(t, "security.example.com", data["domain"]) @@ -332,17 +310,15 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { }) t.Run("create DNS account", func(t *testing.T) { - rec := performLegacyRequest(t, engine, http.MethodPost, "/api/dns-accounts/", map[string]any{ + rec := performLegacyRequest(t, engine, http.MethodPost, apiPath("/dns-accounts/"), map[string]any{ "name": "cloudflare-dns", "type": "cloudflare", "authorization": "test-api-token-value", }, adminAuthHeaders(seed.Token)) require.Equal(t, http.StatusOK, rec.Code) - envelope := decodeEnvelope(t, rec) - require.True(t, envelope.Success, envelope.Message) - - data := unmarshalEnvelopeMap(t, envelope.Data) + resp := requireAPIOK(t, rec) + data := unmarshalAPIMap(t, resp.Data) dnsAccountID = uint(data["id"].(float64)) assert.NotZero(t, dnsAccountID) assert.Equal(t, "cloudflare-dns", data["name"]) @@ -358,29 +334,27 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { t, engine, http.MethodPost, - fmt.Sprintf("/api/waf/rule-groups/%d/delete", ruleGroupID), + fmt.Sprintf("%s/waf/rule-groups/%d/delete", apiPath(""), ruleGroupID), nil, adminAuthHeaders(seed.Token), ) require.Equal(t, http.StatusOK, rec.Code) - - envelope := decodeEnvelope(t, rec) - require.True(t, envelope.Success, envelope.Message) + requireAPIOK(t, rec) detailRec := performLegacyRequest( t, engine, http.MethodGet, - fmt.Sprintf("/api/waf/rule-groups/%d", ruleGroupID), + fmt.Sprintf("%s/waf/rule-groups/%d", apiPath(""), ruleGroupID), nil, adminAuthHeaders(seed.Token), ) - require.Equal(t, http.StatusOK, detailRec.Code) - detailEnvelope := decodeEnvelope(t, detailRec) - assert.False(t, detailEnvelope.Success) + require.Equal(t, http.StatusNotFound, detailRec.Code) + detailResp := decodeAPIResponse(t, detailRec) + assert.NotEmpty(t, detailResp.ErrorMsg) }) _ = ipGroupID _ = domainID _ = dnsAccountID -} +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/legacy/auth_admin.go b/Wavelet/internal/apps/openflare/legacy/auth_admin.go deleted file mode 100644 index c7c3fa9d..00000000 --- a/Wavelet/internal/apps/openflare/legacy/auth_admin.go +++ /dev/null @@ -1,251 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package legacy - -import ( - "fmt" - "strconv" - "strings" - - ofauth "github.com/Rain-kl/Wavelet/internal/apps/openflare/auth" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" - "github.com/Rain-kl/Wavelet/internal/model" - "github.com/gin-gonic/gin" -) - -type legacyUserPayload struct { - ID int `json:"id"` - Username string `json:"username"` - Password string `json:"password"` - DisplayName string `json:"display_name"` - Role int `json:"role"` - Email string `json:"email"` -} - -type manageUserRequest struct { - Username string `json:"username"` - Action string `json:"action"` -} - -type authSourcePayload struct { - Name string `json:"name"` - Type string `json:"type"` - DisplayName string `json:"display_name"` - IsActive bool `json:"is_active"` - ClientID string `json:"client_id"` - ClientSecret string `json:"client_secret"` - OpenIDDiscoveryURL string `json:"openid_discovery_url"` - Scopes string `json:"scopes"` - IconURL string `json:"icon_url"` -} - -type authSourceTogglePayload struct { - IsActive bool `json:"is_active"` -} - -func GetAllUsers(c *gin.Context) { - page, _ := strconv.Atoi(c.Query("p")) - users, err := ofauth.ListUsers(c.Request.Context(), page) - if err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OK(c, users) -} - -func SearchUsers(c *gin.Context) { - users, err := ofauth.SearchUsers(c.Request.Context(), c.Query("keyword")) - if err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OK(c, users) -} - -func GetUser(c *gin.Context) { - id, ok := compat.IDParam(c) - if !ok { - return - } - user, err := ofauth.GetUserByID(c.Request.Context(), callerRole(c), uint64(id)) - if err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OK(c, user) -} - -func CreateUser(c *gin.Context) { - var req legacyUserPayload - if !compat.BindJSON(c, &req) { - return - } - if err := ofauth.CreateUser(c.Request.Context(), callerRole(c), ofauth.CreateUserInput{ - Username: req.Username, - Password: req.Password, - DisplayName: req.DisplayName, - Role: req.Role, - Email: req.Email, - }); err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OKMessage(c, "") -} - -func UpdateUser(c *gin.Context) { - var req legacyUserPayload - if !compat.BindJSON(c, &req) { - return - } - if err := ofauth.UpdateUser(c.Request.Context(), callerRole(c), ofauth.UpdateUserInput{ - ID: req.ID, - Username: req.Username, - Password: req.Password, - DisplayName: req.DisplayName, - Role: req.Role, - Email: req.Email, - }); err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OKMessage(c, "") -} - -func DeleteUser(c *gin.Context) { - id, ok := compat.IDParam(c) - if !ok { - return - } - if err := ofauth.DeleteUserByID(c.Request.Context(), callerRole(c), uint64(id)); err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OKMessage(c, "") -} - -func ManageUser(c *gin.Context) { - var req manageUserRequest - if !compat.BindJSON(c, &req) { - return - } - user, err := ofauth.ManageUser(c.Request.Context(), callerRole(c), ofauth.ManageUserInput{ - Username: req.Username, - Action: req.Action, - }) - if err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OK(c, user) -} - -func ListAuthSources(c *gin.Context) { - sources, err := model.GetAuthSources(c.Request.Context()) - if err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OK(c, sources) -} - -func CreateAuthSource(c *gin.Context) { - var payload authSourcePayload - if !compat.BindJSON(c, &payload) { - return - } - source := payload.toModel() - if err := model.CreateAuthSource(c.Request.Context(), &source); err != nil { - compat.Fail(c, err.Error()) - return - } - source.Sanitize() - compat.OK(c, source) -} - -func UpdateAuthSource(c *gin.Context) { - id, err := parseAuthSourceID(c) - if err != nil { - compat.Fail(c, err.Error()) - return - } - var payload authSourcePayload - if !compat.BindJSON(c, &payload) { - return - } - source := payload.toModel() - source.ID = id - keepSecret := strings.TrimSpace(source.ClientSecret) == "" - if err := model.UpdateAuthSource(c.Request.Context(), &source, keepSecret); err != nil { - compat.Fail(c, err.Error()) - return - } - updated, err := model.GetAuthSourceByID(c.Request.Context(), id) - if err != nil { - compat.Fail(c, err.Error()) - return - } - updated.Sanitize() - compat.OK(c, updated) -} - -func DeleteAuthSource(c *gin.Context) { - id, err := parseAuthSourceID(c) - if err != nil { - compat.Fail(c, err.Error()) - return - } - if err := model.DeleteAuthSource(c.Request.Context(), id); err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OKMessage(c, "") -} - -func ToggleAuthSource(c *gin.Context) { - id, err := parseAuthSourceID(c) - if err != nil { - compat.Fail(c, err.Error()) - return - } - var payload authSourceTogglePayload - if !compat.BindJSON(c, &payload) { - return - } - if err := model.ToggleAuthSource(c.Request.Context(), id, payload.IsActive); err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OKMessage(c, "") -} - -func (payload authSourcePayload) toModel() model.AuthSource { - return model.AuthSource{ - Name: payload.Name, - Type: payload.Type, - DisplayName: payload.DisplayName, - IsActive: payload.IsActive, - ClientID: payload.ClientID, - ClientSecret: payload.ClientSecret, - OpenIDDiscoveryURL: payload.OpenIDDiscoveryURL, - Scopes: payload.Scopes, - IconURL: payload.IconURL, - } -} - -func parseAuthSourceID(c *gin.Context) (uint64, error) { - raw := strings.TrimSpace(c.Param("id")) - if raw == "" { - return 0, fmt.Errorf("认证源 ID 无效") - } - source, err := model.GetAuthSourceByName(c.Request.Context(), raw) - if err == nil { - return source.ID, nil - } - parsed, err := strconv.ParseUint(raw, 10, 64) - if err != nil || parsed == 0 { - return 0, fmt.Errorf("认证源 ID 无效") - } - return parsed, nil -} diff --git a/Wavelet/internal/apps/openflare/legacy/auth_oauth.go b/Wavelet/internal/apps/openflare/legacy/auth_oauth.go deleted file mode 100644 index 7151e9c7..00000000 --- a/Wavelet/internal/apps/openflare/legacy/auth_oauth.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package legacy - -import ( - "strconv" - "strings" - - ofauth "github.com/Rain-kl/Wavelet/internal/apps/openflare/auth" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" - "github.com/Rain-kl/Wavelet/internal/model" - "github.com/gin-gonic/gin" -) - -type linkExistingRequest struct { - Username string `json:"username"` - Password string `json:"password"` -} - -// OAuthAuthorize starts OAuth authorization for a legacy auth source. -func OAuthAuthorize(c *gin.Context) { - url, err := ofauth.OAuthAuthorize(c.Request.Context(), c, c.Param("source")) - if err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OK(c, gin.H{"authorize_url": url}) -} - -// OAuthCallback handles the legacy GET OAuth callback. -func OAuthCallback(c *gin.Context) { - result, err := ofauth.OAuthCallback(c.Request.Context(), c, c.Param("source")) - if err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OK(c, result) -} - -// LinkExistingOAuthAccount binds a pending OAuth account to an existing user. -func LinkExistingOAuthAccount(c *gin.Context) { - var req linkExistingRequest - if !compat.BindJSON(c, &req) { - return - } - result, err := ofauth.LinkExistingOAuthAccount(c.Request.Context(), c, ofauth.LinkExistingInput{ - Username: req.Username, - Password: req.Password, - }) - if err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OK(c, result) -} - -// ListExternalAccounts returns external account bindings for the current user. -func ListExternalAccounts(c *gin.Context) { - userID := callerUserID(c) - accounts, err := model.ListExternalAccountsByUserID(c.Request.Context(), userID) - if err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OK(c, accounts) -} - -// DeleteExternalAccount removes an external account binding. -func DeleteExternalAccount(c *gin.Context) { - userID := callerUserID(c) - if userID == 0 { - compat.Unauthorized(c, "无权进行此操作,未登录或 token 无效") - return - } - rawID := strings.TrimSpace(c.Param("id")) - id, err := strconv.ParseUint(rawID, 10, 64) - if err != nil || id == 0 { - compat.Fail(c, "绑定记录 ID 无效") - return - } - if err := model.DeleteExternalAccountForUser(c.Request.Context(), id, userID); err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OKMessage(c, "") -} diff --git a/Wavelet/internal/apps/openflare/legacy/auth_shortcuts.go b/Wavelet/internal/apps/openflare/legacy/auth_shortcuts.go deleted file mode 100644 index e9bd7a82..00000000 --- a/Wavelet/internal/apps/openflare/legacy/auth_shortcuts.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package legacy - -import ( - ofauth "github.com/Rain-kl/Wavelet/internal/apps/openflare/auth" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" - "github.com/gin-gonic/gin" -) - -// GitHubOAuth handles GET /oauth/github for legacy GitHub login or bind. -func GitHubOAuth(c *gin.Context) { - user, err := ofauth.GitHubOAuth(c.Request.Context(), c, c.Query("code")) - if err != nil { - compat.Fail(c, err.Error()) - return - } - if user.ID == 0 { - compat.OKMessage(c, "bind") - return - } - compat.OK(c, user) -} - -// WeChatOAuth handles GET /oauth/wechat for legacy WeChat login. -func WeChatOAuth(c *gin.Context) { - user, err := ofauth.WeChatOAuth(c.Request.Context(), c, c.Query("code")) - if err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OK(c, user) -} - -// WeChatBind handles GET /oauth/wechat/bind for legacy WeChat account binding. -func WeChatBind(c *gin.Context) { - if err := ofauth.WeChatBind(c.Request.Context(), callerUserID(c), c.Query("code")); err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OKMessage(c, "") -} - -// EmailBind handles GET /oauth/email/bind for legacy email binding. -func EmailBind(c *gin.Context) { - if err := ofauth.EmailBind(c.Request.Context(), callerUserID(c), c.Query("email"), c.Query("code")); err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OKMessage(c, "") -} diff --git a/Wavelet/internal/apps/openflare/legacy/auth_user.go b/Wavelet/internal/apps/openflare/legacy/auth_user.go deleted file mode 100644 index f478ce66..00000000 --- a/Wavelet/internal/apps/openflare/legacy/auth_user.go +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package legacy - -import ( - ofauth "github.com/Rain-kl/Wavelet/internal/apps/openflare/auth" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" - "github.com/gin-gonic/gin" -) - -type loginRequest struct { - Username string `json:"username"` - Password string `json:"password"` - Code string `json:"code"` -} - -type registerRequest struct { - Username string `json:"username"` - Password string `json:"password"` - Nickname string `json:"nickname"` - DisplayName string `json:"display_name"` - Email string `json:"email"` - Code string `json:"code"` -} - -type updateSelfRequest struct { - Username string `json:"username"` - Password string `json:"password"` - DisplayName string `json:"display_name"` - Email string `json:"email"` -} - -type passwordResetRequest struct { - Email string `json:"email"` - Token string `json:"token"` -} - -func Login(c *gin.Context) { - var req loginRequest - if !compat.BindJSON(c, &req) { - return - } - user, err := ofauth.Login(c.Request.Context(), c, ofauth.LoginInput{ - Username: req.Username, - Password: req.Password, - Code: req.Code, - }) - if err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OK(c, user) -} - -func Logout(c *gin.Context) { - if err := ofauth.Logout(c.Request.Context(), c); err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OKMessage(c, "") -} - -func GetSelf(c *gin.Context) { - user, err := ofauth.GetSelf(c.Request.Context(), callerUserID(c)) - if err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OK(c, user) -} - -func UpdateSelf(c *gin.Context) { - var req updateSelfRequest - if !compat.BindJSON(c, &req) { - return - } - if err := ofauth.UpdateSelf(c.Request.Context(), callerUserID(c), ofauth.UpdateSelfInput{ - Username: req.Username, - Password: req.Password, - DisplayName: req.DisplayName, - Email: req.Email, - }); err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OKMessage(c, "") -} - -func DeleteSelf(c *gin.Context) { - if err := ofauth.DeleteSelf(c.Request.Context(), callerUserID(c)); err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OKMessage(c, "") -} - -func GenerateToken(c *gin.Context) { - token, err := ofauth.GenerateUserToken(c.Request.Context(), callerUserID(c)) - if err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OK(c, token) -} - -func Register(c *gin.Context) { - var req registerRequest - if !compat.BindJSON(c, &req) { - return - } - user, err := ofauth.Register(c.Request.Context(), c, ofauth.RegisterInput{ - Username: req.Username, - Password: req.Password, - Nickname: req.Nickname, - DisplayName: req.DisplayName, - Email: req.Email, - Code: req.Code, - }) - if err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OK(c, user) -} - -func SendEmailVerification(c *gin.Context) { - email := c.Query("email") - if err := ofauth.SendRegisterVerificationEmail(c.Request.Context(), email); err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OKMessage(c, "") -} - -func ResetPassword(c *gin.Context) { - var req passwordResetRequest - if !compat.BindJSON(c, &req) { - return - } - password, err := ofauth.ResetPassword(c.Request.Context(), ofauth.PasswordResetInput{ - Email: req.Email, - Token: req.Token, - }) - if err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OK(c, password) -} diff --git a/Wavelet/internal/apps/openflare/legacy/cap.go b/Wavelet/internal/apps/openflare/legacy/cap.go deleted file mode 100644 index 7e61d2aa..00000000 --- a/Wavelet/internal/apps/openflare/legacy/cap.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package legacy - -import ( - "net/http" - - "github.com/Rain-kl/Wavelet/internal/apps/cap" - "github.com/gin-gonic/gin" -) - -type capRedeemRequest struct { - Token string `json:"token" binding:"required"` - Solutions []int `json:"solutions" binding:"required"` -} - -// GetCapChallenge generates a CAP challenge for the legacy frontend. -func GetCapChallenge(c *gin.Context) { - scope := c.Param("scope") - if scope == "" { - scope = c.Query("scope") - } - if scope == "" { - scope = "login" - } - - mgr := cap.GetDefaultManager() - resp, err := mgr.Generate(c.Request.Context(), scope) - if err != nil { - c.JSON(http.StatusInternalServerError, cap.RedeemResponse{ - Success: false, - Error: err.Error(), - }) - return - } - c.JSON(http.StatusOK, resp) -} - -// RedeemCapChallenge redeems a CAP challenge for the legacy frontend. -func RedeemCapChallenge(c *gin.Context) { - scope := c.Param("scope") - if scope == "" { - scope = c.Query("scope") - } - if scope == "" { - scope = "login" - } - - var req capRedeemRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, cap.RedeemResponse{ - Success: false, - Error: "无效的参数", - }) - return - } - - mgr := cap.GetDefaultManager() - resp, err := mgr.Redeem(c.Request.Context(), req.Token, req.Solutions, scope) - if err != nil { - c.JSON(http.StatusInternalServerError, cap.RedeemResponse{ - Success: false, - Error: err.Error(), - }) - return - } - if !resp.Success { - c.JSON(http.StatusBadRequest, resp) - return - } - c.JSON(http.StatusOK, resp) -} diff --git a/Wavelet/internal/apps/openflare/legacy/middleware.go b/Wavelet/internal/apps/openflare/legacy/middleware.go deleted file mode 100644 index 20c9d173..00000000 --- a/Wavelet/internal/apps/openflare/legacy/middleware.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package legacy - -import ( - "github.com/Rain-kl/Wavelet/internal/apps/cap" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" - "github.com/gin-gonic/gin" -) - -// legacyCapAuth verifies PoW CAPTCHA for legacy login using OpenFlare response format. -func legacyCapAuth(scope string) gin.HandlerFunc { - mgr := cap.GetDefaultManager() - return func(c *gin.Context) { - if !cap.ProtectionEnabled(c.Request.Context()) { - c.Next() - return - } - token := c.GetHeader("X-Cap-Token") - if token == "" { - compat.Fail(c, "缺少人机验证凭证") - c.Abort() - return - } - valid, err := mgr.VerifyToken(c.Request.Context(), token, scope) - if err != nil || !valid { - compat.Fail(c, "人机验证凭证无效或已过期") - c.Abort() - return - } - c.Next() - } -} - -func callerRole(c *gin.Context) int { - if role, ok := c.Get("of_role"); ok { - if v, ok := role.(int); ok { - return v - } - } - return 0 -} - -func callerUserID(c *gin.Context) uint64 { - if id, ok := c.Get("of_user_id"); ok { - if v, ok := id.(uint64); ok { - return v - } - } - return 0 -} diff --git a/Wavelet/internal/apps/openflare/legacy/password_reset.go b/Wavelet/internal/apps/openflare/legacy/password_reset.go deleted file mode 100644 index a0d7fc7e..00000000 --- a/Wavelet/internal/apps/openflare/legacy/password_reset.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package legacy - -import ( - ofauth "github.com/Rain-kl/Wavelet/internal/apps/openflare/auth" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" - "github.com/gin-gonic/gin" -) - -// SendPasswordResetEmail sends a password reset email for the legacy frontend. -func SendPasswordResetEmail(c *gin.Context) { - email := c.Query("email") - if err := ofauth.SendPasswordResetEmail(c.Request.Context(), email); err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OKMessage(c, "") -} diff --git a/Wavelet/internal/apps/openflare/legacy/register.go b/Wavelet/internal/apps/openflare/legacy/register.go deleted file mode 100644 index e88de2dd..00000000 --- a/Wavelet/internal/apps/openflare/legacy/register.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -// Package legacy registers OpenFlare /api/* compatibility routes for the old frontend. -package legacy - -import ( - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" - "github.com/gin-gonic/gin" -) - -// RegisterRoutes mounts all OpenFlare legacy API routes under the /api group. -func RegisterRoutes(apiGroup *gin.RouterGroup) { - apiGroup.Use(compat.BridgeOpenFlareToken()) - registerAuthRoutes(apiGroup) - registerOptionRoutes(apiGroup) - registerOriginRoutes(apiGroup) - registerApplyLogRoutes(apiGroup) - registerProxyRouteRoutes(apiGroup) - registerNodeRoutes(apiGroup) - registerWAFRoutes(apiGroup) - registerTLSRoutes(apiGroup) - registerConfigVersionRoutes(apiGroup) - registerAgentRoutes(apiGroup) - registerPagesRoutes(apiGroup) - registerRelayFlaredRoutes(apiGroup) - registerDashboardObsRoutes(apiGroup) - registerMiscRoutes(apiGroup) -} diff --git a/Wavelet/internal/apps/openflare/legacy/register_agent.go b/Wavelet/internal/apps/openflare/legacy/register_agent.go deleted file mode 100644 index 7743cb90..00000000 --- a/Wavelet/internal/apps/openflare/legacy/register_agent.go +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package legacy - -import ( - "github.com/Rain-kl/Wavelet/internal/apps/openflare/agent" - "github.com/gin-gonic/gin" -) - -func registerAgentRoutes(apiGroup *gin.RouterGroup) { - agent.RegisterRoutes(apiGroup) -} diff --git a/Wavelet/internal/apps/openflare/legacy/register_auth.go b/Wavelet/internal/apps/openflare/legacy/register_auth.go deleted file mode 100644 index 95b626f6..00000000 --- a/Wavelet/internal/apps/openflare/legacy/register_auth.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package legacy - -import ( - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" - "github.com/gin-gonic/gin" -) - -func registerAuthRoutes(apiGroup *gin.RouterGroup) { - // /status, /notice, /about are registered by T-OPTION (option.RegisterRoutes). - apiGroup.GET("/verification", SendEmailVerification) - apiGroup.GET("/reset_password", SendPasswordResetEmail) - apiGroup.POST("/user/reset", ResetPassword) - - oauthGroup := apiGroup.Group("/oauth") - { - oauthGroup.GET("/github", GitHubOAuth) - oauthGroup.GET("/wechat", WeChatOAuth) - oauthGroup.GET("/wechat/bind", compat.BridgeOpenFlareToken(), compat.UserAuth(), WeChatBind) - oauthGroup.GET("/email/bind", compat.BridgeOpenFlareToken(), compat.UserAuth(), EmailBind) - oauthGroup.GET("/:source/authorize", OAuthAuthorize) - oauthGroup.GET("/:source/callback", OAuthCallback) - oauthGroup.POST("/link-existing", LinkExistingOAuthAccount) - - externalAccounts := oauthGroup.Group("/external-accounts") - externalAccounts.Use(compat.UserAuth()) - { - compat.RegisterCollection(externalAccounts, "GET", ListExternalAccounts) - externalAccounts.POST("/:id/delete", DeleteExternalAccount) - } - } - - capGroup := apiGroup.Group("/cap") - { - capGroup.POST("/:scope/challenge", GetCapChallenge) - capGroup.POST("/:scope/redeem", RedeemCapChallenge) - } - - userGroup := apiGroup.Group("/user") - { - userGroup.POST("/register", Register) - userGroup.POST("/login", legacyCapAuth("login"), Login) - userGroup.GET("/logout", Logout) - - selfGroup := userGroup.Group("/") - selfGroup.Use(compat.UserAuth()) - { - selfGroup.GET("/self", GetSelf) - selfGroup.POST("/self/update", UpdateSelf) - selfGroup.POST("/self/delete", DeleteSelf) - selfGroup.GET("/token", GenerateToken) - } - - adminGroup := userGroup.Group("/") - adminGroup.Use(compat.AdminAuth()) - { - compat.RegisterCollection(adminGroup, "GET", GetAllUsers) - adminGroup.GET("/search", SearchUsers) - adminGroup.GET("/:id", GetUser) - compat.RegisterCollection(adminGroup, "POST", CreateUser) - adminGroup.POST("/manage", ManageUser) - adminGroup.POST("/update", UpdateUser) - adminGroup.POST("/:id/delete", DeleteUser) - } - } - - authSourceGroup := apiGroup.Group("/auth-sources") - authSourceGroup.Use(compat.RootAuth()) - { - compat.RegisterCollection(authSourceGroup, "GET", ListAuthSources) - compat.RegisterCollection(authSourceGroup, "POST", CreateAuthSource) - authSourceGroup.POST("/:id/update", UpdateAuthSource) - authSourceGroup.POST("/:id/delete", DeleteAuthSource) - authSourceGroup.POST("/:id/toggle", ToggleAuthSource) - } -} diff --git a/Wavelet/internal/apps/openflare/legacy/register_dashboard_obs.go b/Wavelet/internal/apps/openflare/legacy/register_dashboard_obs.go deleted file mode 100644 index 7a610672..00000000 --- a/Wavelet/internal/apps/openflare/legacy/register_dashboard_obs.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package legacy - -import ( - "github.com/Rain-kl/Wavelet/internal/apps/openflare/dashboard" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/observability" - "github.com/gin-gonic/gin" -) - -func registerDashboardObsRoutes(apiGroup *gin.RouterGroup) { - dashboard.RegisterRoutes(apiGroup) - observability.RegisterRoutes(apiGroup) -} diff --git a/Wavelet/internal/apps/openflare/legacy/register_option.go b/Wavelet/internal/apps/openflare/legacy/register_option.go deleted file mode 100644 index a972ad45..00000000 --- a/Wavelet/internal/apps/openflare/legacy/register_option.go +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package legacy - -import ( - "github.com/Rain-kl/Wavelet/internal/apps/openflare/option" - "github.com/gin-gonic/gin" -) - -func registerOptionRoutes(apiGroup *gin.RouterGroup) { - option.RegisterRoutes(apiGroup) -} diff --git a/Wavelet/internal/apps/openflare/legacy/status_public.go b/Wavelet/internal/apps/openflare/legacy/status_public.go deleted file mode 100644 index d4f35c51..00000000 --- a/Wavelet/internal/apps/openflare/legacy/status_public.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package legacy - -import ( - ofauth "github.com/Rain-kl/Wavelet/internal/apps/openflare/auth" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" - "github.com/gin-gonic/gin" -) - -// GetStatus returns public server status for the legacy frontend. -func GetStatus(c *gin.Context) { - data, err := ofauth.BuildPublicStatus(c.Request.Context()) - if err != nil { - compat.Fail(c, err.Error()) - return - } - compat.OK(c, data) -} - -// GetNotice returns the legacy notice content. -func GetNotice(c *gin.Context) { - compat.OK(c, ofauth.GetNotice(c.Request.Context())) -} - -// GetAbout returns the legacy about content. -func GetAbout(c *gin.Context) { - compat.OK(c, ofauth.GetAbout(c.Request.Context())) -} diff --git a/Wavelet/internal/apps/openflare/node/routers.go b/Wavelet/internal/apps/openflare/node/routers.go index de4fe555..66ff3808 100644 --- a/Wavelet/internal/apps/openflare/node/routers.go +++ b/Wavelet/internal/apps/openflare/node/routers.go @@ -7,96 +7,169 @@ import ( "encoding/json" "errors" "io" + "net/http" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" + "github.com/Rain-kl/Wavelet/internal/common/response" "github.com/gin-gonic/gin" - "gorm.io/gorm" ) + func handleLogicError(c *gin.Context, err error) bool { if err == nil { return false } - if errors.Is(err, gorm.ErrRecordNotFound) { - compat.Fail(c, errNodeNotFound) - return true - } - compat.Fail(c, err.Error()) - return true + return apiutil.AbortNotFoundIfMissing(c, err, errNodeNotFound) } // ListNodesHandler lists all nodes. +// @Summary 获取节点列表 +// @Description 返回所有节点及最新配置下发记录,需要管理员权限 +// @Tags openflare-node +// @Produce json +// @Security SessionCookie +// @Success 200 {object} response.Any{data=[]node.View} "节点列表" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或不存在" +// @Router /api/v1/custom/openflare/nodes [get] func ListNodesHandler(c *gin.Context) { nodes, err := ListNodes(c.Request.Context()) if handleLogicError(c, err) { return } - compat.OK(c, nodes) + c.JSON(http.StatusOK, response.OK(nodes)) } // CreateNodeHandler creates a node. +// @Summary 创建节点 +// @Description 创建新的边缘节点记录,需要管理员权限 +// @Tags openflare-node +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param body body node.Input true "节点参数" +// @Success 200 {object} response.Any{data=node.View} "创建成功" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或不存在" +// @Router /api/v1/custom/openflare/nodes [post] func CreateNodeHandler(c *gin.Context) { var input Input - if !compat.BindJSON(c, &input) { + if !apiutil.BindJSON(c, &input) { return } view, err := CreateNode(c.Request.Context(), input) if handleLogicError(c, err) { return } - compat.OK(c, view) + c.JSON(http.StatusOK, response.OK(view)) } // UpdateNodeHandler updates a node. +// @Summary 更新节点 +// @Description 更新指定节点的配置信息,需要管理员权限 +// @Tags openflare-node +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param id path int true "节点 ID" +// @Param body body node.Input true "节点参数" +// @Success 200 {object} response.Any{data=node.View} "更新成功" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或节点不存在" +// @Router /api/v1/custom/openflare/nodes/{id}/update [post] func UpdateNodeHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } var input Input - if !compat.BindJSON(c, &input) { + if !apiutil.BindJSON(c, &input) { return } view, err := UpdateNode(c.Request.Context(), id, input) if handleLogicError(c, err) { return } - compat.OK(c, view) + c.JSON(http.StatusOK, response.OK(view)) } // DeleteNodeHandler deletes a node. +// @Summary 删除节点 +// @Description 删除指定节点记录,需要管理员权限 +// @Tags openflare-node +// @Produce json +// @Security SessionCookie +// @Param id path int true "节点 ID" +// @Success 200 {object} response.Any{data=string} "删除成功" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或节点不存在" +// @Router /api/v1/custom/openflare/nodes/{id}/delete [post] func DeleteNodeHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } if err := DeleteNode(c.Request.Context(), id); handleLogicError(c, err) { return } - compat.OKMessage(c, "") + c.JSON(http.StatusOK, response.OKNil()) } // GetBootstrapTokenHandler returns the global discovery token. +// @Summary 获取引导令牌 +// @Description 返回全局节点发现引导令牌,需要管理员权限 +// @Tags openflare-node +// @Produce json +// @Security SessionCookie +// @Success 200 {object} response.Any{data=node.BootstrapView} "引导令牌" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或不存在" +// @Router /api/v1/custom/openflare/nodes/bootstrap-token [get] func GetBootstrapTokenHandler(c *gin.Context) { view, err := GetBootstrapToken(c.Request.Context()) if handleLogicError(c, err) { return } - compat.OK(c, view) + c.JSON(http.StatusOK, response.OK(view)) } // RotateBootstrapTokenHandler rotates the global discovery token. +// @Summary 轮换引导令牌 +// @Description 重新生成全局节点发现引导令牌,需要管理员权限 +// @Tags openflare-node +// @Produce json +// @Security SessionCookie +// @Success 200 {object} response.Any{data=node.BootstrapView} "新引导令牌" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或不存在" +// @Router /api/v1/custom/openflare/nodes/bootstrap-token/rotate [post] func RotateBootstrapTokenHandler(c *gin.Context) { view, err := RotateBootstrapToken(c.Request.Context()) if handleLogicError(c, err) { return } - compat.OK(c, view) + c.JSON(http.StatusOK, response.OK(view)) } // GetAgentReleaseHandler returns the latest agent release for a node. +// @Summary 获取 Agent 发布信息 +// @Description 返回指定节点可用的最新 Agent 版本信息,需要管理员权限 +// @Tags openflare-node +// @Produce json +// @Security SessionCookie +// @Param id path int true "节点 ID" +// @Param channel query string false "发布渠道" +// @Success 200 {object} response.Any{data=node.AgentReleaseInfo} "Agent 发布信息" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或节点不存在" +// @Router /api/v1/custom/openflare/nodes/{id}/agent-release [get] func GetAgentReleaseHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } @@ -104,19 +177,32 @@ func GetAgentReleaseHandler(c *gin.Context) { if handleLogicError(c, err) { return } - compat.OK(c, release) + c.JSON(http.StatusOK, response.OK(release)) } // RequestAgentUpdateHandler requests agent self-update on a node. +// @Summary 请求 Agent 更新 +// @Description 向指定节点下发 Agent 自更新指令,需要管理员权限 +// @Tags openflare-node +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param id path int true "节点 ID" +// @Param body body node.AgentUpdateInput false "更新参数(可选)" +// @Success 200 {object} response.Any{data=node.View} "更新请求已下发" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或节点不存在" +// @Router /api/v1/custom/openflare/nodes/{id}/agent-update [post] func RequestAgentUpdateHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } var request AgentUpdateInput if c.Request.ContentLength > 0 { if err := bindOptionalJSON(c.Request.Body, &request); err != nil { - compat.Fail(c, "参数错误") + response.AbortBadRequest(c, "参数错误") return } } @@ -124,12 +210,23 @@ func RequestAgentUpdateHandler(c *gin.Context) { if handleLogicError(c, err) { return } - compat.OK(c, view) + c.JSON(http.StatusOK, response.OK(view)) } // RequestOpenrestyRestartHandler requests openresty restart on a node. +// @Summary 请求重启 OpenResty +// @Description 向指定节点下发 OpenResty 重启指令,需要管理员权限 +// @Tags openflare-node +// @Produce json +// @Security SessionCookie +// @Param id path int true "节点 ID" +// @Success 200 {object} response.Any{data=node.View} "重启请求已下发" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或节点不存在" +// @Router /api/v1/custom/openflare/nodes/{id}/openresty-restart [post] func RequestOpenrestyRestartHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } @@ -137,12 +234,23 @@ func RequestOpenrestyRestartHandler(c *gin.Context) { if handleLogicError(c, err) { return } - compat.OK(c, view) + c.JSON(http.StatusOK, response.OK(view)) } // RequestForceSyncHandler requests force sync on a node. +// @Summary 请求强制同步配置 +// @Description 向指定节点下发强制同步当前活跃配置的指令,需要管理员权限 +// @Tags openflare-node +// @Produce json +// @Security SessionCookie +// @Param id path int true "节点 ID" +// @Success 200 {object} response.Any{data=node.View} "同步请求已下发" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或节点不存在" +// @Router /api/v1/custom/openflare/nodes/{id}/force-sync [post] func RequestForceSyncHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } @@ -150,30 +258,54 @@ func RequestForceSyncHandler(c *gin.Context) { if handleLogicError(c, err) { return } - compat.OK(c, view) + c.JSON(http.StatusOK, response.OK(view)) } // GetObservabilityHandler returns node observability details. +// @Summary 获取节点可观测性数据 +// @Description 返回指定节点的指标、健康事件与流量分析数据,需要管理员权限 +// @Tags openflare-node +// @Produce json +// @Security SessionCookie +// @Param id path int true "节点 ID" +// @Param hours query int false "统计时间范围(小时)" +// @Param limit query int false "返回记录数量上限" +// @Success 200 {object} response.Any{data=node.ObservabilityView} "可观测性数据" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或节点不存在" +// @Router /api/v1/custom/openflare/nodes/{id}/observability [get] func GetObservabilityHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } var query ObservabilityQuery if err := c.ShouldBindQuery(&query); err != nil { - compat.Fail(c, "参数错误") + response.AbortBadRequest(c, "参数错误") return } view, err := GetObservability(c.Request.Context(), id, query) if handleLogicError(c, err) { return } - compat.OK(c, view) + c.JSON(http.StatusOK, response.OK(view)) } // CleanupHealthEventsHandler cleans up node health events. +// @Summary 清理节点健康事件 +// @Description 清理指定节点的历史健康事件记录,需要管理员权限 +// @Tags openflare-node +// @Produce json +// @Security SessionCookie +// @Param id path int true "节点 ID" +// @Success 200 {object} response.Any{data=node.HealthEventCleanupResult} "清理结果" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或节点不存在" +// @Router /api/v1/custom/openflare/nodes/{id}/observability/cleanup [post] func CleanupHealthEventsHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } @@ -181,7 +313,7 @@ func CleanupHealthEventsHandler(c *gin.Context) { if handleLogicError(c, err) { return } - compat.OK(c, result) + c.JSON(http.StatusOK, response.OK(result)) } func bindOptionalJSON(body io.Reader, target any) error { @@ -189,4 +321,4 @@ func bindOptionalJSON(body io.Reader, target any) error { return err } return nil -} +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/observability/routers.go b/Wavelet/internal/apps/openflare/observability/routers.go index 9abb6612..c3e4be46 100644 --- a/Wavelet/internal/apps/openflare/observability/routers.go +++ b/Wavelet/internal/apps/openflare/observability/routers.go @@ -4,47 +4,96 @@ package observability import ( + "net/http" "strconv" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" + "github.com/Rain-kl/Wavelet/internal/common/response" "github.com/gin-gonic/gin" ) -// RegisterRoutes mounts legacy OpenFlare access log routes. -func RegisterRoutes(apiGroup *gin.RouterGroup) { - accessLogRoute := apiGroup.Group("/access-logs") - accessLogRoute.Use(compat.AdminAuth()) - { - compat.RegisterCollection(accessLogRoute, "GET", getAccessLogsHandler) - accessLogRoute.GET("/folds", getFoldedAccessLogsHandler) - accessLogRoute.GET("/folds/ip-summary", getFoldedAccessLogIPsHandler) - accessLogRoute.GET("/ip-summary", getAccessLogIPSummariesHandler) - accessLogRoute.GET("/ip-summary/trend", getAccessLogIPTrendHandler) - accessLogRoute.POST("/cleanup", cleanupAccessLogsHandler) - } -} - -func getAccessLogsHandler(c *gin.Context) { +// GetAccessLogsHandler 分页列出访问日志。 +// @Summary 列出访问日志 +// @Description 分页返回 OpenFlare 访问日志,支持按节点、IP、主机与路径筛选,需要管理员权限 +// @Tags openflare-observability +// @Produce json +// @Security SessionCookie +// @Param node_id query string false "节点 ID" +// @Param remote_addr query string false "客户端 IP" +// @Param host query string false "请求 Host" +// @Param path query string false "请求路径" +// @Param p query int false "页码" +// @Param page_size query int false "每页条数" +// @Param sort_by query string false "排序字段" +// @Param sort_order query string false "排序方向" +// @Success 200 {object} response.Any{data=observability.AccessLogList} "访问日志列表" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/access-logs [get] +func GetAccessLogsHandler(c *gin.Context) { logs, err := ListAccessLogs(c.Request.Context(), readAccessLogQuery(c)) - if err != nil { - compat.Fail(c, err.Error()) + if apiutil.AbortBadRequestOnError(c, err) { return } - compat.OK(c, logs) + c.JSON(http.StatusOK, response.OK(logs)) } -func getFoldedAccessLogsHandler(c *gin.Context) { +// getFoldedAccessLogsHandler 分页列出折叠访问日志。 +// @Summary 列出折叠访问日志 +// @Description 按时间桶聚合访问日志并分页返回,需要管理员权限 +// @Tags openflare-observability +// @Produce json +// @Security SessionCookie +// @Param node_id query string false "节点 ID" +// @Param remote_addr query string false "客户端 IP" +// @Param host query string false "请求 Host" +// @Param path query string false "请求路径" +// @Param fold_minutes query int false "折叠时间窗口(分钟)" +// @Param p query int false "页码" +// @Param page_size query int false "每页条数" +// @Param sort_by query string false "排序字段" +// @Param sort_order query string false "排序方向" +// @Success 200 {object} response.Any{data=observability.FoldedAccessLogList} "折叠访问日志列表" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/access-logs/folds [get] +func GetFoldedAccessLogsHandler(c *gin.Context) { query := readAccessLogQuery(c) query.FoldMinutes = readQueryInt(c, "fold_minutes") logs, err := ListFoldedAccessLogs(c.Request.Context(), query) - if err != nil { - compat.Fail(c, err.Error()) + if apiutil.AbortBadRequestOnError(c, err) { return } - compat.OK(c, logs) + c.JSON(http.StatusOK, response.OK(logs)) } -func getFoldedAccessLogIPsHandler(c *gin.Context) { +// getFoldedAccessLogIPsHandler 列出折叠桶内的 IP 汇总。 +// @Summary 列出折叠访问日志 IP 汇总 +// @Description 在指定时间桶内按 IP 聚合访问统计,需要管理员权限 +// @Tags openflare-observability +// @Produce json +// @Security SessionCookie +// @Param node_id query string false "节点 ID" +// @Param remote_addr query string false "客户端 IP" +// @Param host query string false "请求 Host" +// @Param path query string false "请求路径" +// @Param bucket_started_at query string false "时间桶起始时间" +// @Param fold_minutes query int false "折叠时间窗口(分钟)" +// @Param p query int false "页码" +// @Param page_size query int false "每页条数" +// @Param sort_by query string false "排序字段" +// @Param sort_order query string false "排序方向" +// @Success 200 {object} response.Any{data=observability.FoldedAccessLogIPList} "折叠 IP 汇总列表" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/access-logs/folds/ip-summary [get] +func GetFoldedAccessLogIPsHandler(c *gin.Context) { result, err := ListFoldedAccessLogIPs(c.Request.Context(), FoldedAccessLogIPQuery{ NodeID: c.Query("node_id"), RemoteAddr: c.Query("remote_addr"), @@ -57,14 +106,32 @@ func getFoldedAccessLogIPsHandler(c *gin.Context) { SortBy: c.Query("sort_by"), SortOrder: c.Query("sort_order"), }) - if err != nil { - compat.Fail(c, err.Error()) + if apiutil.AbortBadRequestOnError(c, err) { return } - compat.OK(c, result) + c.JSON(http.StatusOK, response.OK(result)) } -func getAccessLogIPSummariesHandler(c *gin.Context) { +// getAccessLogIPSummariesHandler 列出访问日志 IP 汇总。 +// @Summary 列出访问日志 IP 汇总 +// @Description 按 IP 聚合访问日志统计并分页返回,需要管理员权限 +// @Tags openflare-observability +// @Produce json +// @Security SessionCookie +// @Param node_id query string false "节点 ID" +// @Param remote_addr query string false "客户端 IP" +// @Param host query string false "请求 Host" +// @Param p query int false "页码" +// @Param page_size query int false "每页条数" +// @Param sort_by query string false "排序字段" +// @Param sort_order query string false "排序方向" +// @Success 200 {object} response.Any{data=observability.AccessLogIPSummaryList} "IP 汇总列表" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/access-logs/ip-summary [get] +func GetAccessLogIPSummariesHandler(c *gin.Context) { result, err := ListAccessLogIPSummaries(c.Request.Context(), AccessLogIPSummaryQuery{ NodeID: c.Query("node_id"), RemoteAddr: c.Query("remote_addr"), @@ -74,14 +141,30 @@ func getAccessLogIPSummariesHandler(c *gin.Context) { SortBy: c.Query("sort_by"), SortOrder: c.Query("sort_order"), }) - if err != nil { - compat.Fail(c, err.Error()) + if apiutil.AbortBadRequestOnError(c, err) { return } - compat.OK(c, result) + c.JSON(http.StatusOK, response.OK(result)) } -func getAccessLogIPTrendHandler(c *gin.Context) { +// getAccessLogIPTrendHandler 获取 IP 访问趋势。 +// @Summary 获取访问日志 IP 趋势 +// @Description 返回指定 IP 在时间范围内的访问趋势数据,需要管理员权限 +// @Tags openflare-observability +// @Produce json +// @Security SessionCookie +// @Param node_id query string false "节点 ID" +// @Param remote_addr query string false "客户端 IP" +// @Param host query string false "请求 Host" +// @Param hours query int false "统计时间范围(小时)" +// @Param bucket_minutes query int false "时间桶粒度(分钟)" +// @Success 200 {object} response.Any{data=observability.AccessLogIPTrendView} "IP 访问趋势" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/access-logs/ip-summary/trend [get] +func GetAccessLogIPTrendHandler(c *gin.Context) { result, err := GetAccessLogIPTrend(c.Request.Context(), AccessLogIPTrendQuery{ NodeID: c.Query("node_id"), RemoteAddr: c.Query("remote_addr"), @@ -89,24 +172,36 @@ func getAccessLogIPTrendHandler(c *gin.Context) { Hours: readQueryInt(c, "hours"), BucketMinutes: readQueryInt(c, "bucket_minutes"), }) - if err != nil { - compat.Fail(c, err.Error()) + if apiutil.AbortBadRequestOnError(c, err) { return } - compat.OK(c, result) + c.JSON(http.StatusOK, response.OK(result)) } -func cleanupAccessLogsHandler(c *gin.Context) { +// cleanupAccessLogsHandler 清理过期访问日志。 +// @Summary 清理访问日志 +// @Description 按保留天数清理过期访问日志记录,需要管理员权限 +// @Tags openflare-observability +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param request body observability.AccessLogCleanupInput true "清理参数" +// @Success 200 {object} response.Any{data=observability.AccessLogCleanupResult} "清理结果" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/access-logs/cleanup [post] +func CleanupAccessLogsHandler(c *gin.Context) { var input AccessLogCleanupInput - if !compat.BindJSON(c, &input) { + if !apiutil.BindJSON(c, &input) { return } result, err := CleanupAccessLogs(c.Request.Context(), input) - if err != nil { - compat.Fail(c, err.Error()) + if apiutil.AbortBadRequestOnError(c, err) { return } - compat.OK(c, result) + c.JSON(http.StatusOK, response.OK(result)) } func readAccessLogQuery(c *gin.Context) AccessLogQuery { @@ -125,4 +220,4 @@ func readAccessLogQuery(c *gin.Context) AccessLogQuery { func readQueryInt(c *gin.Context, key string) int { value, _ := strconv.Atoi(c.DefaultQuery(key, "0")) return value -} +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/option/routers.go b/Wavelet/internal/apps/openflare/option/routers.go index e5fefc7f..a7827b69 100644 --- a/Wavelet/internal/apps/openflare/option/routers.go +++ b/Wavelet/internal/apps/openflare/option/routers.go @@ -7,128 +7,214 @@ import ( "encoding/json" "errors" "io" + "net/http" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" + "github.com/Rain-kl/Wavelet/internal/common/response" "github.com/Rain-kl/Wavelet/internal/model" "github.com/gin-gonic/gin" ) -// RegisterRoutes mounts legacy OpenFlare option and public status routes. -func RegisterRoutes(apiGroup *gin.RouterGroup) { - apiGroup.GET("/status", getStatusHandler) - apiGroup.GET("/notice", getNoticeHandler) - apiGroup.GET("/about", getAboutHandler) - - optionRoute := apiGroup.Group("/option") - optionRoute.Use(compat.RootAuth()) - { - compat.RegisterCollection(optionRoute, "GET", listOptionsHandler) - optionRoute.POST("/update", updateOptionHandler) - optionRoute.POST("/update-batch", updateOptionsBatchHandler) - optionRoute.POST("/geoip/lookup", lookupGeoIPHandler) - optionRoute.POST("/database/cleanup", cleanupDatabaseHandler) - } - - uptimeKumaRoute := apiGroup.Group("/uptimekuma") - uptimeKumaRoute.Use(compat.RootAuth()) - { - uptimeKumaRoute.POST("/sync", syncUptimeKumaHandler) - } -} - -func getStatusHandler(c *gin.Context) { - view, err := getStatus(c.Request.Context(), "/api") - if err != nil { - compat.Fail(c, errOptionInitFailed) +// GetStatusHandler 获取公开运行状态。 +// @Summary 获取 OpenFlare 公开状态 +// @Description 返回版本、认证源与系统公开配置,无需登录 +// @Tags openflare-option +// @Produce json +// @Success 200 {object} response.Any{data=option.statusView} "公开状态" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/status [get] +func GetStatusHandler(c *gin.Context) { + view, err := getStatus(c.Request.Context(), "/api/v1/custom/openflare") + if apiutil.AbortBadRequestOnError(c, err) { return } - compat.OK(c, view) + c.JSON(http.StatusOK, response.OK(view)) } -func getNoticeHandler(c *gin.Context) { +// getNoticeHandler 获取系统公告。 +// @Summary 获取系统公告 +// @Description 返回 OpenFlare 控制台公告文本,无需登录 +// @Tags openflare-option +// @Produce json +// @Success 200 {object} response.Any{data=string} "系统公告" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/notice [get] +// GetNoticeHandler returns the notice content. +func GetNoticeHandler(c *gin.Context) { notice, err := getNotice(c.Request.Context()) - if err != nil { - compat.Fail(c, errOptionInitFailed) + if apiutil.AbortBadRequestOnError(c, err) { return } - compat.OK(c, notice) + c.JSON(http.StatusOK, response.OK(notice)) } -func getAboutHandler(c *gin.Context) { +// getAboutHandler 获取关于信息。 +// @Summary 获取关于信息 +// @Description 返回 OpenFlare 关于页面文本,无需登录 +// @Tags openflare-option +// @Produce json +// @Success 200 {object} response.Any{data=string} "关于信息" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/about [get] +// GetAboutHandler returns the about content. +func GetAboutHandler(c *gin.Context) { about, err := getAbout(c.Request.Context()) - if err != nil { - compat.Fail(c, errOptionInitFailed) + if apiutil.AbortBadRequestOnError(c, err) { return } - compat.OK(c, about) + c.JSON(http.StatusOK, response.OK(about)) } -func listOptionsHandler(c *gin.Context) { +// listOptionsHandler 列出全部配置项。 +// @Summary 列出 OpenFlare 配置项 +// @Description 返回全部非敏感 OpenFlare 配置项,需要管理员权限 +// @Tags openflare-option +// @Produce json +// @Security SessionCookie +// @Success 200 {object} response.Any{data=[]model.OpenFlareOption} "配置项列表" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/option [get] +// ListOptionsHandler lists OpenFlare options. +func ListOptionsHandler(c *gin.Context) { options, err := listOptions(c.Request.Context()) - if err != nil { - compat.Fail(c, errOptionInitFailed) + if apiutil.AbortBadRequestOnError(c, err) { return } - compat.OK(c, options) + c.JSON(http.StatusOK, response.OK(options)) } -func updateOptionHandler(c *gin.Context) { +// updateOptionHandler 更新单个配置项。 +// @Summary 更新 OpenFlare 配置项 +// @Description 更新单个 OpenFlare 配置项,需要管理员权限 +// @Tags openflare-option +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param request body model.OpenFlareOption true "配置项" +// @Success 200 {object} response.Any "更新成功" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/option/update [post] +// UpdateOptionHandler updates a single option. +func UpdateOptionHandler(c *gin.Context) { var option model.OpenFlareOption - if !compat.BindJSON(c, &option) { + if !apiutil.BindJSON(c, &option) { return } - if err := updateOption(c.Request.Context(), option); err != nil { - compat.Fail(c, err.Error()) + if apiutil.AbortBadRequestOnError(c, updateOption(c.Request.Context(), option)) { return } - compat.OKMessage(c, "") + c.JSON(http.StatusOK, response.OKNil()) } -func updateOptionsBatchHandler(c *gin.Context) { +// updateOptionsBatchHandler 批量更新配置项。 +// @Summary 批量更新 OpenFlare 配置项 +// @Description 批量更新多个 OpenFlare 配置项,需要管理员权限 +// @Tags openflare-option +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param request body option.optionBatchPayload true "批量配置项" +// @Success 200 {object} response.Any "更新成功" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/option/update-batch [post] +// UpdateOptionsBatchHandler updates options in batch. +func UpdateOptionsBatchHandler(c *gin.Context) { var payload optionBatchPayload - if !compat.BindJSON(c, &payload) { + if !apiutil.BindJSON(c, &payload) { return } - if err := updateOptionsBatch(c.Request.Context(), payload); err != nil { - compat.Fail(c, err.Error()) + if apiutil.AbortBadRequestOnError(c, updateOptionsBatch(c.Request.Context(), payload)) { return } - compat.OKMessage(c, "") + c.JSON(http.StatusOK, response.OKNil()) } -func lookupGeoIPHandler(c *gin.Context) { +// lookupGeoIPHandler 查询 GeoIP 信息。 +// @Summary GeoIP 地址查询 +// @Description 按提供商与 IP 查询地理位置信息,需要管理员权限 +// @Tags openflare-option +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param request body option.geoIPLookupRequest true "查询参数" +// @Success 200 {object} response.Any{data=option.geoIPLookupView} "GeoIP 查询结果" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/option/geoip/lookup [post] +// LookupGeoIPHandler performs a GeoIP lookup. +func LookupGeoIPHandler(c *gin.Context) { var request geoIPLookupRequest - if !compat.BindJSON(c, &request) { + if !apiutil.BindJSON(c, &request) { return } view, err := lookupGeoIP(c.Request.Context(), request.Provider, request.IP) - if err != nil { - compat.Fail(c, err.Error()) + if apiutil.AbortBadRequestOnError(c, err) { return } - compat.OK(c, view) + c.JSON(http.StatusOK, response.OK(view)) } -func cleanupDatabaseHandler(c *gin.Context) { +// cleanupDatabaseHandler 清理可观测性数据库数据。 +// @Summary 清理可观测性数据库 +// @Description 按目标与保留天数清理可观测性相关数据表,需要管理员权限 +// @Tags openflare-option +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param request body option.databaseCleanupInput false "清理参数" +// @Success 200 {object} response.Any{data=option.databaseCleanupResult} "清理结果" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/option/database/cleanup [post] +// CleanupDatabaseHandler cleans up observability data. +func CleanupDatabaseHandler(c *gin.Context) { var input databaseCleanupInput if err := bindOptionalJSON(c.Request.Body, &input); err != nil { - compat.Fail(c, errInvalidParams) + response.AbortBadRequest(c, errInvalidParams) return } result, err := cleanupDatabaseObservability(c.Request.Context(), input) - if err != nil { - compat.Fail(c, err.Error()) + if apiutil.AbortBadRequestOnError(c, err) { return } - compat.OK(c, result) + c.JSON(http.StatusOK, response.OK(result)) } -func syncUptimeKumaHandler(c *gin.Context) { - if err := syncUptimeKuma(c.Request.Context()); err != nil { - compat.Fail(c, err.Error()) +// syncUptimeKumaHandler 同步 Uptime Kuma 监控。 +// @Summary 同步 Uptime Kuma +// @Description 将 OpenFlare 节点同步到 Uptime Kuma,需要管理员权限 +// @Tags openflare-option +// @Accept json +// @Produce json +// @Security SessionCookie +// @Success 200 {object} response.Any{data=string} "同步成功" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/uptimekuma/sync [post] +// SyncUptimeKumaHandler triggers UptimeKuma sync. +func SyncUptimeKumaHandler(c *gin.Context) { + if apiutil.AbortBadRequestOnError(c, syncUptimeKuma(c.Request.Context())) { return } - compat.OKMessage(c, "同步成功") + c.JSON(http.StatusOK, response.OK("同步成功")) } func bindOptionalJSON(body io.Reader, target any) error { @@ -136,4 +222,4 @@ func bindOptionalJSON(body io.Reader, target any) error { return err } return nil -} +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/origin/routers.go b/Wavelet/internal/apps/openflare/origin/routers.go index b064fe9d..d375117b 100644 --- a/Wavelet/internal/apps/openflare/origin/routers.go +++ b/Wavelet/internal/apps/openflare/origin/routers.go @@ -4,37 +4,54 @@ package origin import ( - "errors" + "net/http" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" + "github.com/Rain-kl/Wavelet/internal/common/response" "github.com/gin-gonic/gin" - "gorm.io/gorm" ) + func handleLogicError(c *gin.Context, err error) bool { if err == nil { return false } - if errors.Is(err, gorm.ErrRecordNotFound) { - compat.Fail(c, errOriginNotFound) - return true - } - compat.Fail(c, err.Error()) - return true + return apiutil.AbortNotFoundIfMissing(c, err, errOriginNotFound) } // GetOrigins 列出全部源站。 +// @Summary 获取源站列表 +// @Description 返回所有源站及关联代理规则数量,需要管理员权限 +// @Tags openflare-origin +// @Produce json +// @Security SessionCookie +// @Success 200 {object} response.Any{data=[]origin.View} "源站列表" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或不存在" +// @Router /api/v1/custom/openflare/origins [get] func GetOrigins(c *gin.Context) { origins, err := ListOrigins(c.Request.Context()) if handleLogicError(c, err) { return } - compat.OK(c, origins) + c.JSON(http.StatusOK, response.OK(origins)) } // GetOrigin 获取源站详情。 +// @Summary 获取源站详情 +// @Description 返回指定源站信息及关联代理规则摘要,需要管理员权限 +// @Tags openflare-origin +// @Produce json +// @Security SessionCookie +// @Param id path int true "源站 ID" +// @Success 200 {object} response.Any{data=origin.DetailView} "源站详情" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或源站不存在" +// @Router /api/v1/custom/openflare/origins/{id} [get] func GetOrigin(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } @@ -42,47 +59,83 @@ func GetOrigin(c *gin.Context) { if handleLogicError(c, err) { return } - compat.OK(c, detail) + c.JSON(http.StatusOK, response.OK(detail)) } // CreateOriginHandler 创建源站。 +// @Summary 创建源站 +// @Description 创建新的上游源站记录,需要管理员权限 +// @Tags openflare-origin +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param body body origin.Input true "源站参数" +// @Success 200 {object} response.Any{data=origin.View} "创建成功" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或不存在" +// @Router /api/v1/custom/openflare/origins [post] func CreateOriginHandler(c *gin.Context) { var input Input - if !compat.BindJSON(c, &input) { + if !apiutil.BindJSON(c, &input) { return } origin, err := CreateOrigin(c.Request.Context(), input) if handleLogicError(c, err) { return } - compat.OK(c, origin) + c.JSON(http.StatusOK, response.OK(origin)) } // UpdateOriginHandler 更新源站。 +// @Summary 更新源站 +// @Description 更新指定源站的配置信息,需要管理员权限 +// @Tags openflare-origin +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param id path int true "源站 ID" +// @Param body body origin.Input true "源站参数" +// @Success 200 {object} response.Any{data=origin.View} "更新成功" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或源站不存在" +// @Router /api/v1/custom/openflare/origins/{id}/update [post] func UpdateOriginHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } var input Input - if !compat.BindJSON(c, &input) { + if !apiutil.BindJSON(c, &input) { return } origin, err := UpdateOrigin(c.Request.Context(), id, input) if handleLogicError(c, err) { return } - compat.OK(c, origin) + c.JSON(http.StatusOK, response.OK(origin)) } // DeleteOriginHandler 删除源站。 +// @Summary 删除源站 +// @Description 删除指定源站记录,需要管理员权限 +// @Tags openflare-origin +// @Produce json +// @Security SessionCookie +// @Param id path int true "源站 ID" +// @Success 200 {object} response.Any{data=string} "删除成功" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或源站不存在" +// @Router /api/v1/custom/openflare/origins/{id}/delete [post] func DeleteOriginHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } if err := DeleteOrigin(c.Request.Context(), id); handleLogicError(c, err) { return } - compat.OK(c, nil) -} + c.JSON(http.StatusOK, response.OKNil()) +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/pages/routers.go b/Wavelet/internal/apps/openflare/pages/routers.go index f0e527a8..ad084a59 100644 --- a/Wavelet/internal/apps/openflare/pages/routers.go +++ b/Wavelet/internal/apps/openflare/pages/routers.go @@ -4,52 +4,72 @@ package pages import ( - "errors" + "net/http" "strconv" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" + "github.com/Rain-kl/Wavelet/internal/common/response" "github.com/gin-gonic/gin" - "gorm.io/gorm" ) + func handleLogicError(c *gin.Context, err error) bool { if err == nil { return false } - if errors.Is(err, gorm.ErrRecordNotFound) { - compat.Fail(c, errPagesProjectNotFound) - return true - } - compat.Fail(c, err.Error()) - return true + return apiutil.AbortNotFoundIfMissing(c, err, errPagesProjectNotFound) } func deploymentIDParam(c *gin.Context) (uint, bool) { raw := c.Param("deployment_id") if raw == "" { - compat.Fail(c, "无效的 ID") + response.AbortBadRequest(c, "无效的 ID") return 0, false } id64, err := strconv.ParseUint(raw, 10, 64) if err != nil || id64 == 0 { - compat.Fail(c, "无效的 ID") + response.AbortBadRequest(c, "无效的 ID") return 0, false } return uint(id64), true } // ListProjectsHandler 列出全部 Pages 项目。 +// @Summary 列出 Pages 项目 +// @Description 返回全部 OpenFlare Pages 项目,需要管理员权限 +// @Tags openflare-pages +// @Produce json +// @Security SessionCookie +// @Success 200 {object} response.Any{data=[]pages.View} "Pages 项目列表" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/pages [get] func ListProjectsHandler(c *gin.Context) { projects, err := ListProjects(c.Request.Context()) if handleLogicError(c, err) { return } - compat.OK(c, projects) + c.JSON(http.StatusOK, response.OK(projects)) } // GetProjectHandler 获取 Pages 项目详情。 +// @Summary 获取 Pages 项目详情 +// @Description 按 ID 返回 Pages 项目详情,需要管理员权限 +// @Tags openflare-pages +// @Produce json +// @Security SessionCookie +// @Param id path int true "项目 ID" +// @Success 200 {object} response.Any{data=pages.View} "Pages 项目详情" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "项目不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/pages/{id} [get] func GetProjectHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } @@ -57,54 +77,108 @@ func GetProjectHandler(c *gin.Context) { if handleLogicError(c, err) { return } - compat.OK(c, project) + c.JSON(http.StatusOK, response.OK(project)) } // CreateProjectHandler 创建 Pages 项目。 +// @Summary 创建 Pages 项目 +// @Description 创建新的 OpenFlare Pages 项目,需要管理员权限 +// @Tags openflare-pages +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param request body pages.Input true "项目参数" +// @Success 200 {object} response.Any{data=pages.View} "创建成功的项目" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/pages [post] func CreateProjectHandler(c *gin.Context) { var input Input - if !compat.BindJSON(c, &input) { + if !apiutil.BindJSON(c, &input) { return } project, err := CreateProject(c.Request.Context(), input) if handleLogicError(c, err) { return } - compat.OK(c, project) + c.JSON(http.StatusOK, response.OK(project)) } // UpdateProjectHandler 更新 Pages 项目。 +// @Summary 更新 Pages 项目 +// @Description 按 ID 更新 OpenFlare Pages 项目,需要管理员权限 +// @Tags openflare-pages +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param id path int true "项目 ID" +// @Param request body pages.Input true "项目参数" +// @Success 200 {object} response.Any{data=pages.View} "更新后的项目" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "项目不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/pages/{id}/update [post] func UpdateProjectHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } var input Input - if !compat.BindJSON(c, &input) { + if !apiutil.BindJSON(c, &input) { return } project, err := UpdateProject(c.Request.Context(), id, input) if handleLogicError(c, err) { return } - compat.OK(c, project) + c.JSON(http.StatusOK, response.OK(project)) } // DeleteProjectHandler 删除 Pages 项目。 +// @Summary 删除 Pages 项目 +// @Description 按 ID 删除 OpenFlare Pages 项目,需要管理员权限 +// @Tags openflare-pages +// @Produce json +// @Security SessionCookie +// @Param id path int true "项目 ID" +// @Success 200 {object} response.Any "删除成功" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "项目不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/pages/{id}/delete [post] func DeleteProjectHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } if err := DeleteProject(c.Request.Context(), id); handleLogicError(c, err) { return } - compat.OK(c, nil) + c.JSON(http.StatusOK, response.OKNil()) } // ListDeploymentsHandler 列出项目的全部部署。 +// @Summary 列出 Pages 部署 +// @Description 返回指定项目的全部部署记录,需要管理员权限 +// @Tags openflare-pages +// @Produce json +// @Security SessionCookie +// @Param id path int true "项目 ID" +// @Success 200 {object} response.Any{data=[]pages.DeploymentView} "部署列表" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "项目不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/pages/{id}/deployments [get] func ListDeploymentsHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } @@ -112,30 +186,59 @@ func ListDeploymentsHandler(c *gin.Context) { if handleLogicError(c, err) { return } - compat.OK(c, deployments) + c.JSON(http.StatusOK, response.OK(deployments)) } // UploadDeploymentHandler 上传 Pages 部署包。 +// @Summary 上传 Pages 部署包 +// @Description 为指定项目上传 ZIP 部署包,需要管理员权限 +// @Tags openflare-pages +// @Accept multipart/form-data +// @Produce json +// @Security SessionCookie +// @Param id path int true "项目 ID" +// @Param package formData file true "部署包 ZIP 文件" +// @Success 200 {object} response.Any{data=pages.DeploymentView} "部署记录" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "项目不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/pages/{id}/deployments/upload [post] func UploadDeploymentHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } file, err := c.FormFile("package") if err != nil { - compat.Fail(c, errPagesPackageMissing) + response.AbortBadRequest(c, errPagesPackageMissing) return } deployment, err := UploadDeployment(c.Request.Context(), id, file, "") if handleLogicError(c, err) { return } - compat.OK(c, deployment) + c.JSON(http.StatusOK, response.OK(deployment)) } // ActivateDeploymentHandler 激活 Pages 部署。 +// @Summary 激活 Pages 部署 +// @Description 将指定部署设为项目当前生效版本,需要管理员权限 +// @Tags openflare-pages +// @Produce json +// @Security SessionCookie +// @Param id path int true "项目 ID" +// @Param deployment_id path int true "部署 ID" +// @Success 200 {object} response.Any{data=pages.View} "激活后的项目" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "项目或部署不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/pages/{id}/deployments/{deployment_id}/activate [post] func ActivateDeploymentHandler(c *gin.Context) { - projectID, ok := compat.IDParam(c) + projectID, ok := apiutil.IDParam(c) if !ok { return } @@ -147,12 +250,26 @@ func ActivateDeploymentHandler(c *gin.Context) { if handleLogicError(c, err) { return } - compat.OK(c, project) + c.JSON(http.StatusOK, response.OK(project)) } // DeleteDeploymentHandler 删除 Pages 部署。 +// @Summary 删除 Pages 部署 +// @Description 删除指定项目的部署记录,需要管理员权限 +// @Tags openflare-pages +// @Produce json +// @Security SessionCookie +// @Param id path int true "项目 ID" +// @Param deployment_id path int true "部署 ID" +// @Success 200 {object} response.Any "删除成功" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "项目或部署不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/pages/{id}/deployments/{deployment_id}/delete [post] func DeleteDeploymentHandler(c *gin.Context) { - projectID, ok := compat.IDParam(c) + projectID, ok := apiutil.IDParam(c) if !ok { return } @@ -163,10 +280,23 @@ func DeleteDeploymentHandler(c *gin.Context) { if err := DeleteDeployment(c.Request.Context(), projectID, deploymentID); handleLogicError(c, err) { return } - compat.OK(c, nil) + c.JSON(http.StatusOK, response.OKNil()) } // ListDeploymentFilesHandler 列出部署文件清单。 +// @Summary 列出 Pages 部署文件 +// @Description 返回指定部署包含的文件清单,需要管理员权限 +// @Tags openflare-pages +// @Produce json +// @Security SessionCookie +// @Param deployment_id path int true "部署 ID" +// @Success 200 {object} response.Any{data=[]pages.DeploymentFileView} "部署文件列表" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "部署不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/pages/deployments/{deployment_id}/files [get] func ListDeploymentFilesHandler(c *gin.Context) { deploymentID, ok := deploymentIDParam(c) if !ok { @@ -176,5 +306,5 @@ func ListDeploymentFilesHandler(c *gin.Context) { if handleLogicError(c, err) { return } - compat.OK(c, files) -} + c.JSON(http.StatusOK, response.OK(files)) +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/proxy_route/routers.go b/Wavelet/internal/apps/openflare/proxy_route/routers.go index 9c7cff00..728b687b 100644 --- a/Wavelet/internal/apps/openflare/proxy_route/routers.go +++ b/Wavelet/internal/apps/openflare/proxy_route/routers.go @@ -4,37 +4,54 @@ package proxy_route import ( - "errors" + "net/http" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" + "github.com/Rain-kl/Wavelet/internal/common/response" "github.com/gin-gonic/gin" - "gorm.io/gorm" ) + func handleLogicError(c *gin.Context, err error) bool { if err == nil { return false } - if errors.Is(err, gorm.ErrRecordNotFound) { - compat.Fail(c, errProxyRouteNotFound) - return true - } - compat.Fail(c, err.Error()) - return true + return apiutil.AbortNotFoundIfMissing(c, err, errProxyRouteNotFound) } // GetProxyRoutes 列出全部代理规则。 +// @Summary 获取代理规则列表 +// @Description 返回所有代理规则配置,需要管理员权限 +// @Tags openflare-proxy-route +// @Produce json +// @Security SessionCookie +// @Success 200 {object} response.Any{data=[]proxy_route.View} "代理规则列表" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或不存在" +// @Router /api/v1/custom/openflare/proxy-routes [get] func GetProxyRoutes(c *gin.Context) { routes, err := ListProxyRoutes(c.Request.Context()) if handleLogicError(c, err) { return } - compat.OK(c, routes) + c.JSON(http.StatusOK, response.OK(routes)) } // GetProxyRouteHandler 获取代理规则详情。 +// @Summary 获取代理规则详情 +// @Description 返回指定代理规则的完整配置,需要管理员权限 +// @Tags openflare-proxy-route +// @Produce json +// @Security SessionCookie +// @Param id path int true "代理规则 ID" +// @Success 200 {object} response.Any{data=proxy_route.View} "代理规则详情" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或规则不存在" +// @Router /api/v1/custom/openflare/proxy-routes/{id} [get] func GetProxyRouteHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } @@ -42,47 +59,83 @@ func GetProxyRouteHandler(c *gin.Context) { if handleLogicError(c, err) { return } - compat.OK(c, route) + c.JSON(http.StatusOK, response.OK(route)) } // CreateProxyRouteHandler 创建代理规则。 +// @Summary 创建代理规则 +// @Description 创建新的反向代理规则,需要管理员权限 +// @Tags openflare-proxy-route +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param body body proxy_route.Input true "代理规则参数" +// @Success 200 {object} response.Any{data=proxy_route.View} "创建成功" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或不存在" +// @Router /api/v1/custom/openflare/proxy-routes [post] func CreateProxyRouteHandler(c *gin.Context) { var input Input - if !compat.BindJSON(c, &input) { + if !apiutil.BindJSON(c, &input) { return } route, err := CreateProxyRoute(c.Request.Context(), input) if handleLogicError(c, err) { return } - compat.OK(c, route) + c.JSON(http.StatusOK, response.OK(route)) } // UpdateProxyRouteHandler 更新代理规则。 +// @Summary 更新代理规则 +// @Description 更新指定代理规则的配置,需要管理员权限 +// @Tags openflare-proxy-route +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param id path int true "代理规则 ID" +// @Param body body proxy_route.Input true "代理规则参数" +// @Success 200 {object} response.Any{data=proxy_route.View} "更新成功" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或规则不存在" +// @Router /api/v1/custom/openflare/proxy-routes/{id}/update [post] func UpdateProxyRouteHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } var input Input - if !compat.BindJSON(c, &input) { + if !apiutil.BindJSON(c, &input) { return } route, err := UpdateProxyRoute(c.Request.Context(), id, input) if handleLogicError(c, err) { return } - compat.OK(c, route) + c.JSON(http.StatusOK, response.OK(route)) } // DeleteProxyRouteHandler 删除代理规则。 +// @Summary 删除代理规则 +// @Description 删除指定代理规则,需要管理员权限 +// @Tags openflare-proxy-route +// @Produce json +// @Security SessionCookie +// @Param id path int true "代理规则 ID" +// @Success 200 {object} response.Any{data=string} "删除成功" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 404 {object} response.Any "无权限或规则不存在" +// @Router /api/v1/custom/openflare/proxy-routes/{id}/delete [post] func DeleteProxyRouteHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } if err := DeleteProxyRoute(c.Request.Context(), id); handleLogicError(c, err) { return } - compat.OK(c, nil) -} + c.JSON(http.StatusOK, response.OKNil()) +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/relay/routers.go b/Wavelet/internal/apps/openflare/relay/routers.go index f1b3f13a..64a184e7 100644 --- a/Wavelet/internal/apps/openflare/relay/routers.go +++ b/Wavelet/internal/apps/openflare/relay/routers.go @@ -10,6 +10,7 @@ import ( "github.com/gin-gonic/gin" ) + // PostHeartbeat handles POST /relay/heartbeat. func PostHeartbeat(c *gin.Context) { var payload HeartbeatPayload diff --git a/Wavelet/internal/apps/openflare/tls/routers.go b/Wavelet/internal/apps/openflare/tls/routers.go index 103dadae..42b23fae 100644 --- a/Wavelet/internal/apps/openflare/tls/routers.go +++ b/Wavelet/internal/apps/openflare/tls/routers.go @@ -4,38 +4,58 @@ package tls import ( - "errors" + "net/http" "strings" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" + "github.com/Rain-kl/Wavelet/internal/common/response" "github.com/gin-gonic/gin" - "gorm.io/gorm" ) + func handleLogicError(c *gin.Context, err error) bool { if err == nil { return false } - if errors.Is(err, gorm.ErrRecordNotFound) { - compat.Fail(c, "记录不存在") - return true - } - compat.Fail(c, err.Error()) - return true + return apiutil.AbortNotFoundIfMissing(c, err, "记录不存在") } // GetCertificates 列出 TLS 证书。 +// @Summary 列出 TLS 证书 +// @Description 返回全部 TLS 证书(不含 PEM),需要管理员权限 +// @Tags openflare-tls +// @Produce json +// @Security SessionCookie +// @Success 200 {object} response.Any{data=[]model.TLSCertificate} "证书列表" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/tls-certificates [get] func GetCertificates(c *gin.Context) { certificates, err := ListCertificates(c.Request.Context()) if handleLogicError(c, err) { return } - compat.OK(c, certificates) + c.JSON(http.StatusOK, response.OK(certificates)) } // GetCertificateDetail 获取 TLS 证书详情。 +// @Summary 获取 TLS 证书详情 +// @Description 按 ID 返回 TLS 证书详情(不含 PEM),需要管理员权限 +// @Tags openflare-tls +// @Produce json +// @Security SessionCookie +// @Param id path int true "证书 ID" +// @Success 200 {object} response.Any{data=model.TLSCertificate} "证书详情" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "记录不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/tls-certificates/{id} [get] func GetCertificateDetail(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } @@ -43,12 +63,25 @@ func GetCertificateDetail(c *gin.Context) { if handleLogicError(c, err) { return } - compat.OK(c, certificate) + c.JSON(http.StatusOK, response.OK(certificate)) } // GetCertificateContentHandler 获取 TLS 证书 PEM 内容。 +// @Summary 获取 TLS 证书 PEM 内容 +// @Description 按 ID 返回证书与私钥 PEM 内容,需要管理员权限 +// @Tags openflare-tls +// @Produce json +// @Security SessionCookie +// @Param id path int true "证书 ID" +// @Success 200 {object} response.Any{data=tls.CertificateContent} "证书 PEM 内容" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "记录不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/tls-certificates/{id}/content [get] func GetCertificateContentHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } @@ -56,122 +89,235 @@ func GetCertificateContentHandler(c *gin.Context) { if handleLogicError(c, err) { return } - compat.OK(c, content) + c.JSON(http.StatusOK, response.OK(content)) } // CreateCertificateHandler 从 PEM 创建证书。 +// @Summary 创建 TLS 证书 +// @Description 从 PEM 文本创建 TLS 证书,需要管理员权限 +// @Tags openflare-tls +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param request body tls.CertificateInput true "证书参数" +// @Success 200 {object} response.Any{data=model.TLSCertificate} "创建成功的证书" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/tls-certificates [post] func CreateCertificateHandler(c *gin.Context) { var input CertificateInput - if !compat.BindJSON(c, &input) { + if !apiutil.BindJSON(c, &input) { return } certificate, err := CreateCertificate(c.Request.Context(), input) if handleLogicError(c, err) { return } - compat.OK(c, certificate) + c.JSON(http.StatusOK, response.OK(certificate)) } // UpdateCertificateHandler 更新证书。 +// @Summary 更新 TLS 证书 +// @Description 按 ID 更新 TLS 证书 PEM 信息,需要管理员权限 +// @Tags openflare-tls +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param id path int true "证书 ID" +// @Param request body tls.CertificateInput true "证书参数" +// @Success 200 {object} response.Any{data=model.TLSCertificate} "更新后的证书" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "记录不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/tls-certificates/{id}/update [post] func UpdateCertificateHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } var input CertificateInput - if !compat.BindJSON(c, &input) { + if !apiutil.BindJSON(c, &input) { return } certificate, err := UpdateCertificate(c.Request.Context(), id, input) if handleLogicError(c, err) { return } - compat.OK(c, certificate) + c.JSON(http.StatusOK, response.OK(certificate)) } // ImportCertificateFile 从文件导入证书。 +// @Summary 从文件导入 TLS 证书 +// @Description 上传证书与私钥文件创建 TLS 证书,需要管理员权限 +// @Tags openflare-tls +// @Accept multipart/form-data +// @Produce json +// @Security SessionCookie +// @Param name formData string false "证书名称" +// @Param remark formData string false "备注" +// @Param cert_file formData file true "证书文件" +// @Param key_file formData file true "私钥文件" +// @Success 200 {object} response.Any{data=model.TLSCertificate} "导入成功的证书" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/tls-certificates/import-file [post] func ImportCertificateFile(c *gin.Context) { name := c.PostForm("name") remark := c.PostForm("remark") certFile, err := c.FormFile("cert_file") if err != nil { - compat.Fail(c, "缺少证书文件") + response.AbortBadRequest(c, "缺少证书文件") return } keyFile, err := c.FormFile("key_file") if err != nil { - compat.Fail(c, "缺少私钥文件") + response.AbortBadRequest(c, "缺少私钥文件") return } certificate, err := CreateCertificateFromFiles(c.Request.Context(), name, certFile, keyFile, remark) if handleLogicError(c, err) { return } - compat.OK(c, certificate) + c.JSON(http.StatusOK, response.OK(certificate)) } // DeleteCertificateHandler 删除证书。 +// @Summary 删除 TLS 证书 +// @Description 按 ID 删除 TLS 证书,需要管理员权限 +// @Tags openflare-tls +// @Produce json +// @Security SessionCookie +// @Param id path int true "证书 ID" +// @Success 200 {object} response.Any "删除成功" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "记录不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/tls-certificates/{id}/delete [post] func DeleteCertificateHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } if err := DeleteCertificate(c.Request.Context(), id); handleLogicError(c, err) { return } - compat.OK(c, nil) + c.JSON(http.StatusOK, response.OKNil()) } // ApplyCertificateHandler 申请 ACME 证书。 +// @Summary 申请 ACME 证书 +// @Description 通过 ACME 申请新的 TLS 证书,需要管理员权限 +// @Tags openflare-tls +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param request body tls.ApplyInput true "ACME 申请参数" +// @Success 200 {object} response.Any{data=model.TLSCertificate} "申请中的证书" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/tls-certificates/apply [post] func ApplyCertificateHandler(c *gin.Context) { var input ApplyInput - if !compat.BindJSON(c, &input) { + if !apiutil.BindJSON(c, &input) { return } certificate, err := ApplyCertificate(c.Request.Context(), input) if handleLogicError(c, err) { return } - compat.OK(c, certificate) + c.JSON(http.StatusOK, response.OK(certificate)) } // UpdateACMECertificateHandler 更新 ACME 证书配置。 +// @Summary 更新 ACME 证书配置 +// @Description 按 ID 更新 ACME 证书申请配置,需要管理员权限 +// @Tags openflare-tls +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param id path int true "证书 ID" +// @Param request body tls.ApplyInput true "ACME 申请参数" +// @Success 200 {object} response.Any{data=model.TLSCertificate} "更新后的证书" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "记录不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/tls-certificates/{id}/update-acme [post] func UpdateACMECertificateHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } var input ApplyInput - if !compat.BindJSON(c, &input) { + if !apiutil.BindJSON(c, &input) { return } certificate, err := UpdateACMECertificate(c.Request.Context(), id, input) if handleLogicError(c, err) { return } - compat.OK(c, certificate) + c.JSON(http.StatusOK, response.OK(certificate)) } // ConvertCertificateToACMEHandler 将上传证书转为 ACME。 +// @Summary 将证书转为 ACME 管理 +// @Description 将已上传证书转换为 ACME 自动续期模式,需要管理员权限 +// @Tags openflare-tls +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param id path int true "证书 ID" +// @Param request body tls.ApplyInput true "ACME 申请参数" +// @Success 200 {object} response.Any{data=model.TLSCertificate} "转换后的证书" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "记录不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/tls-certificates/{id}/convert-acme [post] func ConvertCertificateToACMEHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } var input ApplyInput - if !compat.BindJSON(c, &input) { + if !apiutil.BindJSON(c, &input) { return } certificate, err := ConvertCertificateToACME(c.Request.Context(), id, input) if handleLogicError(c, err) { return } - compat.OK(c, certificate) + c.JSON(http.StatusOK, response.OK(certificate)) } // RenewCertificateHandler 续期 ACME 证书。 +// @Summary 续期 ACME 证书 +// @Description 手动触发 ACME 证书续期,需要管理员权限 +// @Tags openflare-tls +// @Produce json +// @Security SessionCookie +// @Param id path int true "证书 ID" +// @Success 200 {object} response.Any{data=model.TLSCertificate} "续期后的证书" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "记录不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/tls-certificates/{id}/renew [post] func RenewCertificateHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } @@ -179,126 +325,254 @@ func RenewCertificateHandler(c *gin.Context) { if handleLogicError(c, err) { return } - compat.OK(c, certificate) + c.JSON(http.StatusOK, response.OK(certificate)) } // GetManagedDomains 列出托管域名。 +// @Summary 列出托管域名 +// @Description 返回全部托管域名及关联证书,需要管理员权限 +// @Tags openflare-tls +// @Produce json +// @Security SessionCookie +// @Success 200 {object} response.Any{data=[]model.ManagedDomain} "托管域名列表" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/managed-domains [get] func GetManagedDomains(c *gin.Context) { domains, err := ListManagedDomains(c.Request.Context()) if handleLogicError(c, err) { return } - compat.OK(c, domains) + c.JSON(http.StatusOK, response.OK(domains)) } // CreateManagedDomainHandler 创建托管域名。 +// @Summary 创建托管域名 +// @Description 创建新的托管域名记录,需要管理员权限 +// @Tags openflare-tls +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param request body tls.ManagedDomainInput true "托管域名参数" +// @Success 200 {object} response.Any{data=model.ManagedDomain} "创建成功的托管域名" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/managed-domains [post] func CreateManagedDomainHandler(c *gin.Context) { var input ManagedDomainInput - if !compat.BindJSON(c, &input) { + if !apiutil.BindJSON(c, &input) { return } domain, err := CreateManagedDomain(c.Request.Context(), input) if handleLogicError(c, err) { return } - compat.OK(c, domain) + c.JSON(http.StatusOK, response.OK(domain)) } // UpdateManagedDomainHandler 更新托管域名。 +// @Summary 更新托管域名 +// @Description 按 ID 更新托管域名,需要管理员权限 +// @Tags openflare-tls +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param id path int true "托管域名 ID" +// @Param request body tls.ManagedDomainInput true "托管域名参数" +// @Success 200 {object} response.Any{data=model.ManagedDomain} "更新后的托管域名" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "记录不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/managed-domains/{id}/update [post] func UpdateManagedDomainHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } var input ManagedDomainInput - if !compat.BindJSON(c, &input) { + if !apiutil.BindJSON(c, &input) { return } domain, err := UpdateManagedDomain(c.Request.Context(), id, input) if handleLogicError(c, err) { return } - compat.OK(c, domain) + c.JSON(http.StatusOK, response.OK(domain)) } // DeleteManagedDomainHandler 删除托管域名。 +// @Summary 删除托管域名 +// @Description 按 ID 删除托管域名,需要管理员权限 +// @Tags openflare-tls +// @Produce json +// @Security SessionCookie +// @Param id path int true "托管域名 ID" +// @Success 200 {object} response.Any "删除成功" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "记录不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/managed-domains/{id}/delete [post] func DeleteManagedDomainHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } if err := DeleteManagedDomain(c.Request.Context(), id); handleLogicError(c, err) { return } - compat.OK(c, nil) + c.JSON(http.StatusOK, response.OKNil()) } // MatchManagedDomainCertificateHandler 匹配域名证书。 +// @Summary 匹配托管域名证书 +// @Description 按域名查询可用的证书匹配候选,需要管理员权限 +// @Tags openflare-tls +// @Produce json +// @Security SessionCookie +// @Param domain query string true "域名" +// @Success 200 {object} response.Any{data=tls.ManagedDomainMatchResult} "证书匹配结果" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/managed-domains/match [get] func MatchManagedDomainCertificateHandler(c *gin.Context) { domain := strings.TrimSpace(c.Query("domain")) result, err := MatchManagedDomainCertificate(c.Request.Context(), domain) if handleLogicError(c, err) { return } - compat.OK(c, result) + c.JSON(http.StatusOK, response.OK(result)) } // GetDNSAccounts 列出 DNS 账号。 +// @Summary 列出 DNS 账号 +// @Description 返回全部 DNS 提供商账号,需要管理员权限 +// @Tags openflare-tls +// @Produce json +// @Security SessionCookie +// @Success 200 {object} response.Any{data=[]model.DNSAccount} "DNS 账号列表" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/dns-accounts [get] func GetDNSAccounts(c *gin.Context) { accounts, err := ListDNSAccounts(c.Request.Context()) if handleLogicError(c, err) { return } - compat.OK(c, accounts) + c.JSON(http.StatusOK, response.OK(accounts)) } // CreateDNSAccountHandler 创建 DNS 账号。 +// @Summary 创建 DNS 账号 +// @Description 创建新的 DNS 提供商账号,需要管理员权限 +// @Tags openflare-tls +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param request body tls.DNSAccountInput true "DNS 账号参数" +// @Success 200 {object} response.Any{data=model.DNSAccount} "创建成功的 DNS 账号" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/dns-accounts [post] func CreateDNSAccountHandler(c *gin.Context) { var input DNSAccountInput - if !compat.BindJSON(c, &input) { + if !apiutil.BindJSON(c, &input) { return } account, err := CreateDNSAccount(c.Request.Context(), input) if handleLogicError(c, err) { return } - compat.OK(c, account) + c.JSON(http.StatusOK, response.OK(account)) } // UpdateDNSAccountHandler 更新 DNS 账号。 +// @Summary 更新 DNS 账号 +// @Description 按 ID 更新 DNS 提供商账号,需要管理员权限 +// @Tags openflare-tls +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param id path int true "DNS 账号 ID" +// @Param request body tls.DNSAccountInput true "DNS 账号参数" +// @Success 200 {object} response.Any{data=model.DNSAccount} "更新后的 DNS 账号" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "记录不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/dns-accounts/{id}/update [post] func UpdateDNSAccountHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } var input DNSAccountInput - if !compat.BindJSON(c, &input) { + if !apiutil.BindJSON(c, &input) { return } account, err := UpdateDNSAccount(c.Request.Context(), id, input) if handleLogicError(c, err) { return } - compat.OK(c, account) + c.JSON(http.StatusOK, response.OK(account)) } // DeleteDNSAccountHandler 删除 DNS 账号。 +// @Summary 删除 DNS 账号 +// @Description 按 ID 删除 DNS 提供商账号,需要管理员权限 +// @Tags openflare-tls +// @Produce json +// @Security SessionCookie +// @Param id path int true "DNS 账号 ID" +// @Success 200 {object} response.Any "删除成功" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "记录不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/dns-accounts/{id}/delete [post] func DeleteDNSAccountHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } if err := DeleteDNSAccount(c.Request.Context(), id); handleLogicError(c, err) { return } - compat.OK(c, nil) + c.JSON(http.StatusOK, response.OKNil()) } // GetDefaultAcmeAccountHandler 获取默认 ACME 账号。 +// @Summary 获取默认 ACME 账号 +// @Description 返回系统默认 ACME 账号配置,需要管理员权限 +// @Tags openflare-tls +// @Produce json +// @Security SessionCookie +// @Success 200 {object} response.Any{data=model.AcmeAccount} "默认 ACME 账号" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "记录不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/acme-accounts/default [get] func GetDefaultAcmeAccountHandler(c *gin.Context) { account, err := GetDefaultAcmeAccount(c.Request.Context()) if handleLogicError(c, err) { return } - compat.OK(c, account) -} + c.JSON(http.StatusOK, response.OK(account)) +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/update/routers.go b/Wavelet/internal/apps/openflare/update/routers.go index cf9546c2..ef133273 100644 --- a/Wavelet/internal/apps/openflare/update/routers.go +++ b/Wavelet/internal/apps/openflare/update/routers.go @@ -10,7 +10,8 @@ import ( "net/http" "time" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" + "github.com/Rain-kl/Wavelet/internal/common/response" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" ) @@ -19,50 +20,101 @@ var upgradeLogsUpgrader = websocket.Upgrader{ CheckOrigin: func(_ *http.Request) bool { return true }, } -// GetLatestReleaseHandler returns the newest GitHub release for the legacy update UI. + +// GetLatestReleaseHandler 获取最新 GitHub 发布版本。 +// @Summary 获取最新服务端发布版本 +// @Description 查询 OpenFlare 服务端最新 GitHub Release 及升级状态,需要管理员权限 +// @Tags openflare-update +// @Produce json +// @Security SessionCookie +// @Param channel query string false "发布渠道(stable/preview)" +// @Success 200 {object} response.Any{data=update.LatestReleaseView} "最新发布信息" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/update/latest-release [get] func GetLatestReleaseHandler(c *gin.Context) { release, err := GetLatestRelease(c.Request.Context(), c.Query("channel")) - if err != nil { - compat.Fail(c, err.Error()) + if apiutil.AbortBadRequestOnError(c, err) { return } - compat.OK(c, release) + c.JSON(http.StatusOK, response.OK(release)) } -// UpgradeServerHandler schedules an automatic upgrade from the latest release. +// UpgradeServerHandler 调度自动升级任务。 +// @Summary 触发服务端自动升级 +// @Description 从最新 Release 调度 OpenFlare 服务端自动升级,需要管理员权限 +// @Tags openflare-update +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param request body update.upgradeRequest false "升级参数" +// @Success 200 {object} response.Any{data=update.LatestReleaseView} "升级任务已调度" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/update/upgrade [post] func UpgradeServerHandler(c *gin.Context) { var request upgradeRequest if err := bindOptionalJSON(c.Request.Body, &request); err != nil { - compat.Fail(c, "无效的参数") + response.AbortBadRequest(c, "无效的参数") return } release, err := ScheduleUpgrade(c.Request.Context(), request.Channel) - if err != nil { - compat.Fail(c, err.Error()) + if apiutil.AbortBadRequestOnError(c, err) { return } - okWithMessage(c, release, "服务升级任务已启动,下载完成后将自动重启。") + c.JSON(http.StatusOK, response.OK(release)) } -// UploadManualServerBinaryHandler rejects manual uploads (feature disabled upstream). +// UploadManualServerBinaryHandler 上传手动升级二进制(已禁用)。 +// @Summary 上传手动升级二进制 +// @Description 上传服务端二进制以进行手动升级(当前功能已禁用),需要管理员权限 +// @Tags openflare-update +// @Accept multipart/form-data +// @Produce json +// @Security SessionCookie +// @Param binary formData file true "服务端二进制文件" +// @Failure 400 {object} response.Any "功能已禁用或参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Router /api/v1/custom/openflare/update/manual-upload [post] func UploadManualServerBinaryHandler(c *gin.Context) { - if err := UploadManualBinary(); err != nil { - compat.Fail(c, err.Error()) + if apiutil.AbortBadRequestOnError(c, UploadManualBinary()) { return } } -// ConfirmManualServerUpgradeHandler rejects manual upgrades (feature disabled upstream). +// ConfirmManualServerUpgradeHandler 确认手动升级(已禁用)。 +// @Summary 确认手动服务端升级 +// @Description 确认并执行手动上传的服务端升级(当前功能已禁用),需要管理员权限 +// @Tags openflare-update +// @Accept json +// @Produce json +// @Security SessionCookie +// @Failure 400 {object} response.Any "功能已禁用或参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Router /api/v1/custom/openflare/update/manual-upgrade [post] func ConfirmManualServerUpgradeHandler(c *gin.Context) { - if err := ConfirmManualUpgrade(); err != nil { - compat.Fail(c, err.Error()) + if apiutil.AbortBadRequestOnError(c, ConfirmManualUpgrade()) { return } } -// StreamServerUpgradeLogsHandler streams upgrade progress snapshots over WebSocket. +// StreamServerUpgradeLogsHandler 通过 WebSocket 推送升级日志。 +// @Summary 流式获取服务端升级日志 +// @Description 通过 WebSocket 推送升级进度快照,需要管理员权限 +// @Tags openflare-update +// @Security SessionCookie +// @Success 101 {object} update.StreamSnapshot "WebSocket 升级日志流" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Router /api/v1/custom/openflare/update/logs/ws [get] func StreamServerUpgradeLogsHandler(c *gin.Context) { conn, err := upgradeLogsUpgrader.Upgrade(c.Writer, c.Request, nil) if err != nil { @@ -97,17 +149,9 @@ func StreamServerUpgradeLogsHandler(c *gin.Context) { } } -func okWithMessage(c *gin.Context, data any, message string) { - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": message, - "data": data, - }) -} - func bindOptionalJSON(body io.Reader, target any) error { if err := json.NewDecoder(body).Decode(target); err != nil && !errors.Is(err, io.EOF) { return err } return nil -} +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/waf/routers.go b/Wavelet/internal/apps/openflare/waf/routers.go index bc43e5af..67af78b3 100644 --- a/Wavelet/internal/apps/openflare/waf/routers.go +++ b/Wavelet/internal/apps/openflare/waf/routers.go @@ -4,52 +4,72 @@ package waf import ( - "errors" + "net/http" "strconv" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" + "github.com/Rain-kl/Wavelet/internal/common/response" "github.com/gin-gonic/gin" - "gorm.io/gorm" ) + func handleLogicError(c *gin.Context, err error) bool { if err == nil { return false } - if errors.Is(err, gorm.ErrRecordNotFound) { - compat.Fail(c, "记录不存在") - return true - } - compat.Fail(c, err.Error()) - return true + return apiutil.AbortNotFoundIfMissing(c, err, "记录不存在") } func routeIDParam(c *gin.Context) (uint, bool) { raw := c.Param("route_id") if raw == "" { - compat.Fail(c, "invalid id") + response.AbortBadRequest(c, "invalid id") return 0, false } id64, err := strconv.ParseUint(raw, 10, 64) if err != nil || id64 == 0 { - compat.Fail(c, "invalid id") + response.AbortBadRequest(c, "invalid id") return 0, false } return uint(id64), true } -// ListRuleGroupsHandler lists all WAF rule groups. +// ListRuleGroupsHandler 列出全部 WAF 规则组。 +// @Summary 列出 WAF 规则组 +// @Description 返回全部 WAF 规则组,需要管理员权限 +// @Tags openflare-waf +// @Produce json +// @Security SessionCookie +// @Success 200 {object} response.Any{data=[]waf.RuleGroupView} "规则组列表" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/waf/rule-groups [get] func ListRuleGroupsHandler(c *gin.Context) { groups, err := ListRuleGroups(c.Request.Context()) if handleLogicError(c, err) { return } - compat.OK(c, groups) + c.JSON(http.StatusOK, response.OK(groups)) } -// GetRuleGroupHandler returns a WAF rule group by id. +// GetRuleGroupHandler 获取 WAF 规则组详情。 +// @Summary 获取 WAF 规则组详情 +// @Description 按 ID 返回 WAF 规则组详情,需要管理员权限 +// @Tags openflare-waf +// @Produce json +// @Security SessionCookie +// @Param id path int true "规则组 ID" +// @Success 200 {object} response.Any{data=waf.RuleGroupView} "规则组详情" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "记录不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/waf/rule-groups/{id} [get] func GetRuleGroupHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } @@ -57,69 +77,138 @@ func GetRuleGroupHandler(c *gin.Context) { if handleLogicError(c, err) { return } - compat.OK(c, group) + c.JSON(http.StatusOK, response.OK(group)) } -// CreateRuleGroupHandler creates a WAF rule group. +// CreateRuleGroupHandler 创建 WAF 规则组。 +// @Summary 创建 WAF 规则组 +// @Description 创建新的 WAF 规则组,需要管理员权限 +// @Tags openflare-waf +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param request body waf.RuleGroupInput true "规则组参数" +// @Success 200 {object} response.Any{data=waf.RuleGroupView} "创建成功的规则组" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/waf/rule-groups [post] func CreateRuleGroupHandler(c *gin.Context) { var input RuleGroupInput - if !compat.BindJSON(c, &input) { + if !apiutil.BindJSON(c, &input) { return } group, err := CreateRuleGroup(c.Request.Context(), input) if handleLogicError(c, err) { return } - compat.OK(c, group) + c.JSON(http.StatusOK, response.OK(group)) } -// UpdateRuleGroupHandler updates a WAF rule group. +// UpdateRuleGroupHandler 更新 WAF 规则组。 +// @Summary 更新 WAF 规则组 +// @Description 按 ID 更新 WAF 规则组,需要管理员权限 +// @Tags openflare-waf +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param id path int true "规则组 ID" +// @Param request body waf.RuleGroupInput true "规则组参数" +// @Success 200 {object} response.Any{data=waf.RuleGroupView} "更新后的规则组" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "记录不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/waf/rule-groups/{id}/update [post] func UpdateRuleGroupHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } var input RuleGroupInput - if !compat.BindJSON(c, &input) { + if !apiutil.BindJSON(c, &input) { return } group, err := UpdateRuleGroup(c.Request.Context(), id, input) if handleLogicError(c, err) { return } - compat.OK(c, group) + c.JSON(http.StatusOK, response.OK(group)) } -// DeleteRuleGroupHandler deletes a WAF rule group. +// DeleteRuleGroupHandler 删除 WAF 规则组。 +// @Summary 删除 WAF 规则组 +// @Description 按 ID 删除 WAF 规则组,需要管理员权限 +// @Tags openflare-waf +// @Produce json +// @Security SessionCookie +// @Param id path int true "规则组 ID" +// @Success 200 {object} response.Any "删除成功" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "记录不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/waf/rule-groups/{id}/delete [post] func DeleteRuleGroupHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } if err := DeleteRuleGroup(c.Request.Context(), id); handleLogicError(c, err) { return } - compat.OKMessage(c, "") + c.JSON(http.StatusOK, response.OKNil()) } -// ReplaceRuleGroupSitesHandler replaces site bindings for a rule group. +// ReplaceRuleGroupSitesHandler 替换规则组绑定的站点。 +// @Summary 替换规则组站点绑定 +// @Description 替换 WAF 规则组关联的代理站点列表,需要管理员权限 +// @Tags openflare-waf +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param id path int true "规则组 ID" +// @Param request body waf.IDsRequest true "站点 ID 列表" +// @Success 200 {object} response.Any{data=waf.RuleGroupView} "更新后的规则组" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "记录不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/waf/rule-groups/{id}/sites [post] func ReplaceRuleGroupSitesHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } var request IDsRequest - if !compat.BindJSON(c, &request) { + if !apiutil.BindJSON(c, &request) { return } group, err := ReplaceRuleGroupSites(c.Request.Context(), id, request.IDs) if handleLogicError(c, err) { return } - compat.OK(c, group) + c.JSON(http.StatusOK, response.OK(group)) } -// GetSiteRuleGroupsHandler returns WAF rule groups for a proxy route. +// GetSiteRuleGroupsHandler 获取站点的 WAF 规则组绑定。 +// @Summary 获取站点 WAF 规则组 +// @Description 返回代理站点关联的 WAF 规则组绑定,需要管理员权限 +// @Tags openflare-waf +// @Produce json +// @Security SessionCookie +// @Param route_id path int true "代理路由 ID" +// @Success 200 {object} response.Any{data=waf.SiteRuleGroupsView} "站点规则组绑定" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "记录不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/waf/sites/{route_id}/rule-groups [get] func GetSiteRuleGroupsHandler(c *gin.Context) { routeID, ok := routeIDParam(c) if !ok { @@ -129,38 +218,77 @@ func GetSiteRuleGroupsHandler(c *gin.Context) { if handleLogicError(c, err) { return } - compat.OK(c, view) + c.JSON(http.StatusOK, response.OK(view)) } -// ReplaceSiteRuleGroupsHandler replaces rule group bindings for a proxy route. +// ReplaceSiteRuleGroupsHandler 替换站点的 WAF 规则组绑定。 +// @Summary 替换站点 WAF 规则组 +// @Description 替换代理站点关联的 WAF 规则组列表,需要管理员权限 +// @Tags openflare-waf +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param route_id path int true "代理路由 ID" +// @Param request body waf.IDsRequest true "规则组 ID 列表" +// @Success 200 {object} response.Any{data=waf.SiteRuleGroupsView} "更新后的站点规则组绑定" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "记录不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/waf/sites/{route_id}/rule-groups [post] func ReplaceSiteRuleGroupsHandler(c *gin.Context) { routeID, ok := routeIDParam(c) if !ok { return } var request IDsRequest - if !compat.BindJSON(c, &request) { + if !apiutil.BindJSON(c, &request) { return } view, err := ReplaceSiteRuleGroups(c.Request.Context(), routeID, request.IDs) if handleLogicError(c, err) { return } - compat.OK(c, view) + c.JSON(http.StatusOK, response.OK(view)) } -// ListIPGroupsHandler lists all WAF IP groups. +// ListIPGroupsHandler 列出全部 WAF IP 组。 +// @Summary 列出 WAF IP 组 +// @Description 返回全部 WAF IP 组,需要管理员权限 +// @Tags openflare-waf +// @Produce json +// @Security SessionCookie +// @Success 200 {object} response.Any{data=[]waf.IPGroupView} "IP 组列表" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/waf/ip-groups [get] func ListIPGroupsHandler(c *gin.Context) { groups, err := ListIPGroups(c.Request.Context()) if handleLogicError(c, err) { return } - compat.OK(c, groups) + c.JSON(http.StatusOK, response.OK(groups)) } -// GetIPGroupHandler returns a WAF IP group by id. +// GetIPGroupHandler 获取 WAF IP 组详情。 +// @Summary 获取 WAF IP 组详情 +// @Description 按 ID 返回 WAF IP 组详情,需要管理员权限 +// @Tags openflare-waf +// @Produce json +// @Security SessionCookie +// @Param id path int true "IP 组 ID" +// @Success 200 {object} response.Any{data=waf.IPGroupView} "IP 组详情" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "记录不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/waf/ip-groups/{id} [get] func GetIPGroupHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } @@ -168,54 +296,108 @@ func GetIPGroupHandler(c *gin.Context) { if handleLogicError(c, err) { return } - compat.OK(c, group) + c.JSON(http.StatusOK, response.OK(group)) } -// CreateIPGroupHandler creates a WAF IP group. +// CreateIPGroupHandler 创建 WAF IP 组。 +// @Summary 创建 WAF IP 组 +// @Description 创建新的 WAF IP 组,需要管理员权限 +// @Tags openflare-waf +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param request body waf.IPGroupInput true "IP 组参数" +// @Success 200 {object} response.Any{data=waf.IPGroupView} "创建成功的 IP 组" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/waf/ip-groups [post] func CreateIPGroupHandler(c *gin.Context) { var input IPGroupInput - if !compat.BindJSON(c, &input) { + if !apiutil.BindJSON(c, &input) { return } group, err := CreateIPGroup(c.Request.Context(), input) if handleLogicError(c, err) { return } - compat.OK(c, group) + c.JSON(http.StatusOK, response.OK(group)) } -// UpdateIPGroupHandler updates a WAF IP group. +// UpdateIPGroupHandler 更新 WAF IP 组。 +// @Summary 更新 WAF IP 组 +// @Description 按 ID 更新 WAF IP 组,需要管理员权限 +// @Tags openflare-waf +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param id path int true "IP 组 ID" +// @Param request body waf.IPGroupInput true "IP 组参数" +// @Success 200 {object} response.Any{data=waf.IPGroupView} "更新后的 IP 组" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "记录不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/waf/ip-groups/{id}/update [post] func UpdateIPGroupHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } var input IPGroupInput - if !compat.BindJSON(c, &input) { + if !apiutil.BindJSON(c, &input) { return } group, err := UpdateIPGroup(c.Request.Context(), id, input) if handleLogicError(c, err) { return } - compat.OK(c, group) + c.JSON(http.StatusOK, response.OK(group)) } -// DeleteIPGroupHandler deletes a WAF IP group. +// DeleteIPGroupHandler 删除 WAF IP 组。 +// @Summary 删除 WAF IP 组 +// @Description 按 ID 删除 WAF IP 组,需要管理员权限 +// @Tags openflare-waf +// @Produce json +// @Security SessionCookie +// @Param id path int true "IP 组 ID" +// @Success 200 {object} response.Any "删除成功" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "记录不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/waf/ip-groups/{id}/delete [post] func DeleteIPGroupHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } if err := DeleteIPGroup(c.Request.Context(), id); handleLogicError(c, err) { return } - compat.OKMessage(c, "") + c.JSON(http.StatusOK, response.OKNil()) } -// SyncIPGroupHandler triggers a stub sync for a WAF IP group. +// SyncIPGroupHandler 触发 WAF IP 组同步。 +// @Summary 同步 WAF IP 组 +// @Description 手动触发 WAF IP 组外部 IP 同步,需要管理员权限 +// @Tags openflare-waf +// @Produce json +// @Security SessionCookie +// @Param id path int true "IP 组 ID" +// @Success 200 {object} response.Any{data=waf.IPGroupSyncResult} "同步结果" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 404 {object} response.Any "记录不存在" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/waf/ip-groups/{id}/sync [post] func SyncIPGroupHandler(c *gin.Context) { - id, ok := compat.IDParam(c) + id, ok := apiutil.IDParam(c) if !ok { return } @@ -223,18 +405,31 @@ func SyncIPGroupHandler(c *gin.Context) { if handleLogicError(c, err) { return } - compat.OK(c, result) + c.JSON(http.StatusOK, response.OK(result)) } -// TestIPGroupAutoConfigHandler tests automatic IP group configuration (stub). +// TestIPGroupAutoConfigHandler 测试 WAF IP 组自动配置。 +// @Summary 测试 WAF IP 组自动配置 +// @Description 根据自动配置规则测试 IP 匹配结果(桩实现),需要管理员权限 +// @Tags openflare-waf +// @Accept json +// @Produce json +// @Security SessionCookie +// @Param request body waf.IPGroupAutoTestInput true "自动配置参数" +// @Success 200 {object} response.Any{data=waf.IPGroupAutoTestResult} "测试结果" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Failure 403 {object} response.Any "无管理员权限" +// @Failure 500 {object} response.Any "内部错误" +// @Router /api/v1/custom/openflare/waf/ip-groups/test [post] func TestIPGroupAutoConfigHandler(c *gin.Context) { var input IPGroupAutoTestInput - if !compat.BindJSON(c, &input) { + if !apiutil.BindJSON(c, &input) { return } result, err := TestIPGroupAutoConfig(c.Request.Context(), input) if handleLogicError(c, err) { return } - compat.OK(c, result) -} + c.JSON(http.StatusOK, response.OK(result)) +} \ No newline at end of file diff --git a/Wavelet/internal/db/migrator/goose/postgres/202606090001_initial_schema.sql b/Wavelet/internal/db/migrator/goose/postgres/202606090001_initial_schema.sql index 32191999..c53cc515 100644 --- a/Wavelet/internal/db/migrator/goose/postgres/202606090001_initial_schema.sql +++ b/Wavelet/internal/db/migrator/goose/postgres/202606090001_initial_schema.sql @@ -137,7 +137,7 @@ CREATE INDEX IF NOT EXISTS idx_templates_created_at ON templates (created_at); CREATE INDEX IF NOT EXISTS idx_templates_updated_at ON templates (updated_at); INSERT INTO system_configs (key, value, type, visibility, description, created_at, updated_at) VALUES - ('cap_login_enabled', 'false', 'system', 1, '是否启用登录人机验证(true/false)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + ('cap_login_enabled', 'true', 'system', 1, '是否启用登录人机验证(true/false)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), ('cap_auto_solve', 'true', 'system', 1, '打开页面后是否自动开始计算,关闭则需用户手动点击触发', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), ('cap_challenge_count', '1', 'system', 0, '客户端需求解的 PoW 难题总数,默认 1,推荐 1~5', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), ('cap_challenge_size', '32', 'system', 0, '人机验证盐值长度', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), @@ -152,8 +152,8 @@ INSERT INTO system_configs (key, value, type, visibility, description, created_a ('upload_allowed_extensions', 'jpg,png,webp', 'system', 1, '允许上传的图片扩展名(逗号分隔)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), ('site_name', 'OpenFlare', 'system', 1, '系统平台的展示名称', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), ('password_login_enabled', 'true', 'system', 1, '是否允许使用账号密码登录', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), - ('registration_enabled', 'true', 'system', 1, '控制普通用户是否可以自主注册(true/false)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), - ('password_register_enabled', 'true', 'system', 1, '是否允许通过密码创建本地账号', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + ('registration_enabled', 'false', 'system', 1, '控制普通用户是否可以自主注册(true/false)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + ('password_register_enabled', 'false', 'system', 1, '是否允许通过密码创建本地账号', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), ('oidc_login_enabled', 'true', 'system', 1, '是否允许使用第三方 OIDC 认证源登录', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), ('max_api_keys_per_user', '5', 'business', 1, '限制每个普通用户可以创建的 API Key 最大数量', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), ('email_login_verification_enabled', 'false', 'system', 1, '是否开启邮箱登录验证(true/false)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), diff --git a/Wavelet/internal/db/migrator/goose/postgres/202606200002_update_security_defaults.sql b/Wavelet/internal/db/migrator/goose/postgres/202606200002_update_security_defaults.sql new file mode 100644 index 00000000..e1790f7d --- /dev/null +++ b/Wavelet/internal/db/migrator/goose/postgres/202606200002_update_security_defaults.sql @@ -0,0 +1,25 @@ +-- +goose Up +UPDATE w_system_configs +SET value = 'false', updated_at = CURRENT_TIMESTAMP +WHERE key = 'registration_enabled' AND value = 'true'; + +UPDATE w_system_configs +SET value = 'false', updated_at = CURRENT_TIMESTAMP +WHERE key = 'password_register_enabled' AND value = 'true'; + +UPDATE w_system_configs +SET value = 'true', updated_at = CURRENT_TIMESTAMP +WHERE key = 'cap_login_enabled' AND value = 'false'; + +-- +goose Down +UPDATE w_system_configs +SET value = 'true', updated_at = CURRENT_TIMESTAMP +WHERE key = 'registration_enabled' AND value = 'false'; + +UPDATE w_system_configs +SET value = 'true', updated_at = CURRENT_TIMESTAMP +WHERE key = 'password_register_enabled' AND value = 'false'; + +UPDATE w_system_configs +SET value = 'false', updated_at = CURRENT_TIMESTAMP +WHERE key = 'cap_login_enabled' AND value = 'true'; \ No newline at end of file diff --git a/Wavelet/internal/db/migrator/goose/sqlite/202606090001_initial_schema.sql b/Wavelet/internal/db/migrator/goose/sqlite/202606090001_initial_schema.sql index 8e77091a..73eb79b3 100644 --- a/Wavelet/internal/db/migrator/goose/sqlite/202606090001_initial_schema.sql +++ b/Wavelet/internal/db/migrator/goose/sqlite/202606090001_initial_schema.sql @@ -137,7 +137,7 @@ CREATE INDEX IF NOT EXISTS idx_templates_created_at ON templates (created_at); CREATE INDEX IF NOT EXISTS idx_templates_updated_at ON templates (updated_at); INSERT INTO system_configs (key, value, type, visibility, description, created_at, updated_at) VALUES - ('cap_login_enabled', 'false', 'system', 1, '是否启用登录人机验证(true/false)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + ('cap_login_enabled', 'true', 'system', 1, '是否启用登录人机验证(true/false)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), ('cap_auto_solve', 'true', 'system', 1, '打开页面后是否自动开始计算,关闭则需用户手动点击触发', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), ('cap_challenge_count', '1', 'system', 0, '客户端需求解的 PoW 难题总数,默认 1,推荐 1~5', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), ('cap_challenge_size', '32', 'system', 0, '人机验证盐值长度', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), @@ -152,8 +152,8 @@ INSERT INTO system_configs (key, value, type, visibility, description, created_a ('upload_allowed_extensions', 'jpg,png,webp', 'system', 1, '允许上传的图片扩展名(逗号分隔)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), ('site_name', 'OpenFlare', 'system', 1, '系统平台的展示名称', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), ('password_login_enabled', 'true', 'system', 1, '是否允许使用账号密码登录', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), - ('registration_enabled', 'true', 'system', 1, '控制普通用户是否可以自主注册(true/false)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), - ('password_register_enabled', 'true', 'system', 1, '是否允许通过密码创建本地账号', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + ('registration_enabled', 'false', 'system', 1, '控制普通用户是否可以自主注册(true/false)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + ('password_register_enabled', 'false', 'system', 1, '是否允许通过密码创建本地账号', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), ('oidc_login_enabled', 'true', 'system', 1, '是否允许使用第三方 OIDC 认证源登录', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), ('max_api_keys_per_user', '5', 'business', 1, '限制每个普通用户可以创建的 API Key 最大数量', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), ('email_login_verification_enabled', 'false', 'system', 1, '是否开启邮箱登录验证(true/false)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), diff --git a/Wavelet/internal/db/migrator/goose/sqlite/202606200002_update_security_defaults.sql b/Wavelet/internal/db/migrator/goose/sqlite/202606200002_update_security_defaults.sql new file mode 100644 index 00000000..e1790f7d --- /dev/null +++ b/Wavelet/internal/db/migrator/goose/sqlite/202606200002_update_security_defaults.sql @@ -0,0 +1,25 @@ +-- +goose Up +UPDATE w_system_configs +SET value = 'false', updated_at = CURRENT_TIMESTAMP +WHERE key = 'registration_enabled' AND value = 'true'; + +UPDATE w_system_configs +SET value = 'false', updated_at = CURRENT_TIMESTAMP +WHERE key = 'password_register_enabled' AND value = 'true'; + +UPDATE w_system_configs +SET value = 'true', updated_at = CURRENT_TIMESTAMP +WHERE key = 'cap_login_enabled' AND value = 'false'; + +-- +goose Down +UPDATE w_system_configs +SET value = 'true', updated_at = CURRENT_TIMESTAMP +WHERE key = 'registration_enabled' AND value = 'false'; + +UPDATE w_system_configs +SET value = 'true', updated_at = CURRENT_TIMESTAMP +WHERE key = 'password_register_enabled' AND value = 'false'; + +UPDATE w_system_configs +SET value = 'false', updated_at = CURRENT_TIMESTAMP +WHERE key = 'cap_login_enabled' AND value = 'true'; \ No newline at end of file diff --git a/Wavelet/internal/router/router.go b/Wavelet/internal/router/router.go index dfe4535c..8f891d7a 100644 --- a/Wavelet/internal/router/router.go +++ b/Wavelet/internal/router/router.go @@ -15,10 +15,10 @@ import ( "syscall" "time" - oflegacy "github.com/Rain-kl/Wavelet/internal/apps/openflare/legacy" "github.com/Rain-kl/Wavelet/internal/apps/risk_control" router_root "github.com/Rain-kl/Wavelet/internal/router/root" v1 "github.com/Rain-kl/Wavelet/internal/router/v1" + ofrouter "github.com/Rain-kl/Wavelet/internal/router/v1/openflare" "github.com/Rain-kl/Wavelet/internal/apps/oauth" "github.com/Rain-kl/Wavelet/internal/config" @@ -116,8 +116,8 @@ func registerRoutes(r *gin.Engine) { apiGroup := r.Group(config.Config.App.APIPrefix) { - // OpenFlare legacy /api/* routes (old frontend compatibility) - oflegacy.RegisterRoutes(apiGroup) + // OpenFlare Agent/Relay/Flared protocol routes under /api/* + ofrouter.RegisterRoutes(apiGroup) // API V1 apiV1Router := apiGroup.Group("/v1") diff --git a/Wavelet/internal/router/v1/custom.go b/Wavelet/internal/router/v1/custom.go index ba88641d..80bb4eb1 100644 --- a/Wavelet/internal/router/v1/custom.go +++ b/Wavelet/internal/router/v1/custom.go @@ -6,6 +6,7 @@ package v1 import ( "github.com/Rain-kl/Wavelet/internal/apps/custom" + ofrouter "github.com/Rain-kl/Wavelet/internal/router/v1/openflare" "github.com/gin-gonic/gin" ) @@ -14,5 +15,6 @@ func RegisterCustomRoutes(apiV1Router *gin.RouterGroup) { customRouter := apiV1Router.Group("/custom") { customRouter.GET("/hello", custom.Hello) + ofrouter.RegisterV1Routes(customRouter) } } diff --git a/Wavelet/internal/router/v1/openflare/openflare.go b/Wavelet/internal/router/v1/openflare/openflare.go new file mode 100644 index 00000000..2b4268fc --- /dev/null +++ b/Wavelet/internal/router/v1/openflare/openflare.go @@ -0,0 +1,16 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +// Package openflare registers OpenFlare HTTP routes. +// Management console APIs are mounted via RegisterV1Routes under /api/v1/custom/openflare. +// Agent/Relay/Flared protocol routes are mounted via RegisterRoutes under /api. +package openflare + +import "github.com/gin-gonic/gin" + +// RegisterRoutes mounts Agent/Relay/Flared protocol routes under the /api group. +func RegisterRoutes(apiGroup *gin.RouterGroup) { + registerAgentRoutes(apiGroup) + registerRelayRoutes(apiGroup) + registerFlaredRoutes(apiGroup) +} diff --git a/Wavelet/internal/router/v1/openflare/register_agent.go b/Wavelet/internal/router/v1/openflare/register_agent.go new file mode 100644 index 00000000..973ff12c --- /dev/null +++ b/Wavelet/internal/router/v1/openflare/register_agent.go @@ -0,0 +1,31 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +package openflare + +import ( + "github.com/Rain-kl/Wavelet/internal/apps/openflare/agent" + "github.com/gin-gonic/gin" +) + +func registerAgentRoutes(apiGroup *gin.RouterGroup) { + agentRoute := apiGroup.Group("/agent") + { + discoveryRoute := agentRoute.Group("/") + discoveryRoute.Use(agent.AgentRegisterAuth()) + { + discoveryRoute.POST("/nodes/register", agent.RegisterHandler) + } + + authorizedRoute := agentRoute.Group("/") + authorizedRoute.Use(agent.AgentAuth()) + { + authorizedRoute.GET("/ws", agent.AgentWebSocketHandler) + authorizedRoute.POST("/nodes/heartbeat", agent.HeartbeatHandler) + authorizedRoute.GET("/config-versions/active", agent.GetActiveConfigHandler) + authorizedRoute.GET("/pages/deployments/:deployment_id/package", agent.DownloadPagesPackageHandler) + authorizedRoute.POST("/waf/ip-groups/sync", agent.SyncWAFIPGroupsHandler) + authorizedRoute.POST("/apply-logs", agent.ReportApplyLogHandler) + } + } +} diff --git a/Wavelet/internal/apps/openflare/legacy/register_apply_log.go b/Wavelet/internal/router/v1/openflare/register_apply_log.go similarity index 63% rename from Wavelet/internal/apps/openflare/legacy/register_apply_log.go rename to Wavelet/internal/router/v1/openflare/register_apply_log.go index 8c1cc80b..43182da8 100644 --- a/Wavelet/internal/apps/openflare/legacy/register_apply_log.go +++ b/Wavelet/internal/router/v1/openflare/register_apply_log.go @@ -1,19 +1,19 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -package legacy +package openflare import ( + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" "github.com/Rain-kl/Wavelet/internal/apps/openflare/apply_log" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" "github.com/gin-gonic/gin" ) func registerApplyLogRoutes(apiGroup *gin.RouterGroup) { applyLogRoute := apiGroup.Group("/apply-logs") - applyLogRoute.Use(compat.AdminAuth()) + applyLogRoute.Use(apiutil.AdminRequired()) { - compat.RegisterCollection(applyLogRoute, "GET", apply_log.GetApplyLogs) + apiutil.RegisterCollection(applyLogRoute, "GET", apply_log.GetApplyLogs) applyLogRoute.POST("/cleanup", apply_log.CleanupApplyLogs) } } diff --git a/Wavelet/internal/apps/openflare/legacy/register_config_version.go b/Wavelet/internal/router/v1/openflare/register_config_version.go similarity index 79% rename from Wavelet/internal/apps/openflare/legacy/register_config_version.go rename to Wavelet/internal/router/v1/openflare/register_config_version.go index e74474eb..a7daebf6 100644 --- a/Wavelet/internal/apps/openflare/legacy/register_config_version.go +++ b/Wavelet/internal/router/v1/openflare/register_config_version.go @@ -1,19 +1,19 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -package legacy +package openflare import ( - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" "github.com/Rain-kl/Wavelet/internal/apps/openflare/config_version" "github.com/gin-gonic/gin" ) func registerConfigVersionRoutes(apiGroup *gin.RouterGroup) { configVersionGroup := apiGroup.Group("/config-versions") - configVersionGroup.Use(compat.AdminAuth()) + configVersionGroup.Use(apiutil.AdminRequired()) { - compat.RegisterCollection(configVersionGroup, "GET", config_version.ListConfigVersionsHandler) + apiutil.RegisterCollection(configVersionGroup, "GET", config_version.ListConfigVersionsHandler) configVersionGroup.GET("/active", config_version.GetActiveConfigVersionHandler) configVersionGroup.GET("/preview", config_version.PreviewConfigVersionHandler) configVersionGroup.GET("/diff", config_version.DiffConfigVersionHandler) diff --git a/Wavelet/internal/router/v1/openflare/register_dashboard.go b/Wavelet/internal/router/v1/openflare/register_dashboard.go new file mode 100644 index 00000000..21bb7e7d --- /dev/null +++ b/Wavelet/internal/router/v1/openflare/register_dashboard.go @@ -0,0 +1,18 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +package openflare + +import ( + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/dashboard" + "github.com/gin-gonic/gin" +) + +func registerDashboardRoutes(apiGroup *gin.RouterGroup) { + dashboardRoute := apiGroup.Group("/dashboard") + dashboardRoute.Use(apiutil.AdminRequired()) + { + dashboardRoute.GET("/overview", dashboard.GetOverviewHandler) + } +} diff --git a/Wavelet/internal/apps/openflare/legacy/register_node.go b/Wavelet/internal/router/v1/openflare/register_node.go similarity index 78% rename from Wavelet/internal/apps/openflare/legacy/register_node.go rename to Wavelet/internal/router/v1/openflare/register_node.go index d3b147ac..f8e6ffe2 100644 --- a/Wavelet/internal/apps/openflare/legacy/register_node.go +++ b/Wavelet/internal/router/v1/openflare/register_node.go @@ -1,22 +1,22 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -package legacy +package openflare import ( - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" "github.com/Rain-kl/Wavelet/internal/apps/openflare/node" "github.com/gin-gonic/gin" ) func registerNodeRoutes(apiGroup *gin.RouterGroup) { nodeRoute := apiGroup.Group("/nodes") - nodeRoute.Use(compat.AdminAuth()) + nodeRoute.Use(apiutil.AdminRequired()) { nodeRoute.GET("/bootstrap-token", node.GetBootstrapTokenHandler) nodeRoute.POST("/bootstrap-token/rotate", node.RotateBootstrapTokenHandler) - compat.RegisterCollection(nodeRoute, "GET", node.ListNodesHandler) - compat.RegisterCollection(nodeRoute, "POST", node.CreateNodeHandler) + apiutil.RegisterCollection(nodeRoute, "GET", node.ListNodesHandler) + apiutil.RegisterCollection(nodeRoute, "POST", node.CreateNodeHandler) nodeRoute.GET("/:id/agent-release", node.GetAgentReleaseHandler) nodeRoute.POST("/:id/update", node.UpdateNodeHandler) nodeRoute.POST("/:id/delete", node.DeleteNodeHandler) diff --git a/Wavelet/internal/router/v1/openflare/register_observability.go b/Wavelet/internal/router/v1/openflare/register_observability.go new file mode 100644 index 00000000..1d38d227 --- /dev/null +++ b/Wavelet/internal/router/v1/openflare/register_observability.go @@ -0,0 +1,23 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +package openflare + +import ( + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/observability" + "github.com/gin-gonic/gin" +) + +func registerObservabilityRoutes(apiGroup *gin.RouterGroup) { + accessLogRoute := apiGroup.Group("/access-logs") + accessLogRoute.Use(apiutil.AdminRequired()) + { + apiutil.RegisterCollection(accessLogRoute, "GET", observability.GetAccessLogsHandler) + accessLogRoute.GET("/folds", observability.GetFoldedAccessLogsHandler) + accessLogRoute.GET("/folds/ip-summary", observability.GetFoldedAccessLogIPsHandler) + accessLogRoute.GET("/ip-summary", observability.GetAccessLogIPSummariesHandler) + accessLogRoute.GET("/ip-summary/trend", observability.GetAccessLogIPTrendHandler) + accessLogRoute.POST("/cleanup", observability.CleanupAccessLogsHandler) + } +} diff --git a/Wavelet/internal/router/v1/openflare/register_option.go b/Wavelet/internal/router/v1/openflare/register_option.go new file mode 100644 index 00000000..68754be4 --- /dev/null +++ b/Wavelet/internal/router/v1/openflare/register_option.go @@ -0,0 +1,32 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +package openflare + +import ( + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/option" + "github.com/gin-gonic/gin" +) + +func registerOptionRoutes(apiGroup *gin.RouterGroup) { + apiGroup.GET("/status", option.GetStatusHandler) + apiGroup.GET("/notice", option.GetNoticeHandler) + apiGroup.GET("/about", option.GetAboutHandler) + + optionRoute := apiGroup.Group("/option") + optionRoute.Use(apiutil.AdminRequired()) + { + apiutil.RegisterCollection(optionRoute, "GET", option.ListOptionsHandler) + optionRoute.POST("/update", option.UpdateOptionHandler) + optionRoute.POST("/update-batch", option.UpdateOptionsBatchHandler) + optionRoute.POST("/geoip/lookup", option.LookupGeoIPHandler) + optionRoute.POST("/database/cleanup", option.CleanupDatabaseHandler) + } + + uptimeKumaRoute := apiGroup.Group("/uptimekuma") + uptimeKumaRoute.Use(apiutil.AdminRequired()) + { + uptimeKumaRoute.POST("/sync", option.SyncUptimeKumaHandler) + } +} diff --git a/Wavelet/internal/apps/openflare/legacy/register_origin.go b/Wavelet/internal/router/v1/openflare/register_origin.go similarity index 62% rename from Wavelet/internal/apps/openflare/legacy/register_origin.go rename to Wavelet/internal/router/v1/openflare/register_origin.go index aab13e02..ca26b496 100644 --- a/Wavelet/internal/apps/openflare/legacy/register_origin.go +++ b/Wavelet/internal/router/v1/openflare/register_origin.go @@ -1,21 +1,21 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -package legacy +package openflare import ( - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" "github.com/Rain-kl/Wavelet/internal/apps/openflare/origin" "github.com/gin-gonic/gin" ) func registerOriginRoutes(apiGroup *gin.RouterGroup) { originRoute := apiGroup.Group("/origins") - originRoute.Use(compat.AdminAuth()) + originRoute.Use(apiutil.AdminRequired()) { - compat.RegisterCollection(originRoute, "GET", origin.GetOrigins) + apiutil.RegisterCollection(originRoute, "GET", origin.GetOrigins) originRoute.GET("/:id", origin.GetOrigin) - compat.RegisterCollection(originRoute, "POST", origin.CreateOriginHandler) + apiutil.RegisterCollection(originRoute, "POST", origin.CreateOriginHandler) originRoute.POST("/:id/update", origin.UpdateOriginHandler) originRoute.POST("/:id/delete", origin.DeleteOriginHandler) } diff --git a/Wavelet/internal/apps/openflare/legacy/register_pages.go b/Wavelet/internal/router/v1/openflare/register_pages.go similarity index 75% rename from Wavelet/internal/apps/openflare/legacy/register_pages.go rename to Wavelet/internal/router/v1/openflare/register_pages.go index 5b2156ab..aa7e64a5 100644 --- a/Wavelet/internal/apps/openflare/legacy/register_pages.go +++ b/Wavelet/internal/router/v1/openflare/register_pages.go @@ -1,21 +1,21 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -package legacy +package openflare import ( - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" "github.com/Rain-kl/Wavelet/internal/apps/openflare/pages" "github.com/gin-gonic/gin" ) func registerPagesRoutes(apiGroup *gin.RouterGroup) { pagesRoute := apiGroup.Group("/pages") - pagesRoute.Use(compat.AdminAuth()) + pagesRoute.Use(apiutil.AdminRequired()) { - compat.RegisterCollection(pagesRoute, "GET", pages.ListProjectsHandler) + apiutil.RegisterCollection(pagesRoute, "GET", pages.ListProjectsHandler) pagesRoute.GET("/:id", pages.GetProjectHandler) - compat.RegisterCollection(pagesRoute, "POST", pages.CreateProjectHandler) + apiutil.RegisterCollection(pagesRoute, "POST", pages.CreateProjectHandler) pagesRoute.POST("/:id/update", pages.UpdateProjectHandler) pagesRoute.POST("/:id/delete", pages.DeleteProjectHandler) pagesRoute.GET("/:id/deployments", pages.ListDeploymentsHandler) diff --git a/Wavelet/internal/apps/openflare/legacy/register_proxy_route.go b/Wavelet/internal/router/v1/openflare/register_proxy_route.go similarity index 63% rename from Wavelet/internal/apps/openflare/legacy/register_proxy_route.go rename to Wavelet/internal/router/v1/openflare/register_proxy_route.go index de56fe35..ea10e0b5 100644 --- a/Wavelet/internal/apps/openflare/legacy/register_proxy_route.go +++ b/Wavelet/internal/router/v1/openflare/register_proxy_route.go @@ -1,21 +1,21 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -package legacy +package openflare import ( - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" "github.com/Rain-kl/Wavelet/internal/apps/openflare/proxy_route" "github.com/gin-gonic/gin" ) func registerProxyRouteRoutes(apiGroup *gin.RouterGroup) { proxyRouteGroup := apiGroup.Group("/proxy-routes") - proxyRouteGroup.Use(compat.AdminAuth()) + proxyRouteGroup.Use(apiutil.AdminRequired()) { - compat.RegisterCollection(proxyRouteGroup, "GET", proxy_route.GetProxyRoutes) + apiutil.RegisterCollection(proxyRouteGroup, "GET", proxy_route.GetProxyRoutes) proxyRouteGroup.GET("/:id", proxy_route.GetProxyRouteHandler) - compat.RegisterCollection(proxyRouteGroup, "POST", proxy_route.CreateProxyRouteHandler) + apiutil.RegisterCollection(proxyRouteGroup, "POST", proxy_route.CreateProxyRouteHandler) proxyRouteGroup.POST("/:id/update", proxy_route.UpdateProxyRouteHandler) proxyRouteGroup.POST("/:id/delete", proxy_route.DeleteProxyRouteHandler) } diff --git a/Wavelet/internal/apps/openflare/legacy/register_relay_flared.go b/Wavelet/internal/router/v1/openflare/register_relay_flared.go similarity index 84% rename from Wavelet/internal/apps/openflare/legacy/register_relay_flared.go rename to Wavelet/internal/router/v1/openflare/register_relay_flared.go index a54f847b..55e98f9e 100644 --- a/Wavelet/internal/apps/openflare/legacy/register_relay_flared.go +++ b/Wavelet/internal/router/v1/openflare/register_relay_flared.go @@ -1,7 +1,7 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -package legacy +package openflare import ( "github.com/Rain-kl/Wavelet/internal/apps/openflare/flared" @@ -9,14 +9,16 @@ import ( "github.com/gin-gonic/gin" ) -func registerRelayFlaredRoutes(apiGroup *gin.RouterGroup) { +func registerRelayRoutes(apiGroup *gin.RouterGroup) { relayRoute := apiGroup.Group("/relay") relayRoute.Use(relay.RelayAuth()) { relayRoute.POST("/heartbeat", relay.PostHeartbeat) relayRoute.GET("/ws", relay.GetWebSocket) } +} +func registerFlaredRoutes(apiGroup *gin.RouterGroup) { flaredRoute := apiGroup.Group("/flared") flaredRoute.Use(flared.TunnelAuth()) { diff --git a/Wavelet/internal/apps/openflare/legacy/register_tls.go b/Wavelet/internal/router/v1/openflare/register_tls.go similarity index 67% rename from Wavelet/internal/apps/openflare/legacy/register_tls.go rename to Wavelet/internal/router/v1/openflare/register_tls.go index ac701a77..62176378 100644 --- a/Wavelet/internal/apps/openflare/legacy/register_tls.go +++ b/Wavelet/internal/router/v1/openflare/register_tls.go @@ -1,32 +1,32 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -package legacy +package openflare import ( - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" "github.com/Rain-kl/Wavelet/internal/apps/openflare/tls" "github.com/gin-gonic/gin" ) func registerTLSRoutes(apiGroup *gin.RouterGroup) { managedDomainRoute := apiGroup.Group("/managed-domains") - managedDomainRoute.Use(compat.AdminAuth()) + managedDomainRoute.Use(apiutil.AdminRequired()) { - compat.RegisterCollection(managedDomainRoute, "GET", tls.GetManagedDomains) + apiutil.RegisterCollection(managedDomainRoute, "GET", tls.GetManagedDomains) managedDomainRoute.GET("/match", tls.MatchManagedDomainCertificateHandler) - compat.RegisterCollection(managedDomainRoute, "POST", tls.CreateManagedDomainHandler) + apiutil.RegisterCollection(managedDomainRoute, "POST", tls.CreateManagedDomainHandler) managedDomainRoute.POST("/:id/update", tls.UpdateManagedDomainHandler) managedDomainRoute.POST("/:id/delete", tls.DeleteManagedDomainHandler) } tlsCertificateRoute := apiGroup.Group("/tls-certificates") - tlsCertificateRoute.Use(compat.AdminAuth()) + tlsCertificateRoute.Use(apiutil.AdminRequired()) { - compat.RegisterCollection(tlsCertificateRoute, "GET", tls.GetCertificates) + apiutil.RegisterCollection(tlsCertificateRoute, "GET", tls.GetCertificates) tlsCertificateRoute.GET("/:id", tls.GetCertificateDetail) tlsCertificateRoute.GET("/:id/content", tls.GetCertificateContentHandler) - compat.RegisterCollection(tlsCertificateRoute, "POST", tls.CreateCertificateHandler) + apiutil.RegisterCollection(tlsCertificateRoute, "POST", tls.CreateCertificateHandler) tlsCertificateRoute.POST("/:id/update", tls.UpdateCertificateHandler) tlsCertificateRoute.POST("/:id/update-acme", tls.UpdateACMECertificateHandler) tlsCertificateRoute.POST("/:id/convert-acme", tls.ConvertCertificateToACMEHandler) @@ -37,16 +37,16 @@ func registerTLSRoutes(apiGroup *gin.RouterGroup) { } acmeAccountRoute := apiGroup.Group("/acme-accounts") - acmeAccountRoute.Use(compat.AdminAuth()) + acmeAccountRoute.Use(apiutil.AdminRequired()) { acmeAccountRoute.GET("/default", tls.GetDefaultAcmeAccountHandler) } dnsAccountRoute := apiGroup.Group("/dns-accounts") - dnsAccountRoute.Use(compat.AdminAuth()) + dnsAccountRoute.Use(apiutil.AdminRequired()) { - compat.RegisterCollection(dnsAccountRoute, "GET", tls.GetDNSAccounts) - compat.RegisterCollection(dnsAccountRoute, "POST", tls.CreateDNSAccountHandler) + apiutil.RegisterCollection(dnsAccountRoute, "GET", tls.GetDNSAccounts) + apiutil.RegisterCollection(dnsAccountRoute, "POST", tls.CreateDNSAccountHandler) dnsAccountRoute.POST("/:id/update", tls.UpdateDNSAccountHandler) dnsAccountRoute.POST("/:id/delete", tls.DeleteDNSAccountHandler) } diff --git a/Wavelet/internal/apps/openflare/legacy/register_misc.go b/Wavelet/internal/router/v1/openflare/register_update.go similarity index 76% rename from Wavelet/internal/apps/openflare/legacy/register_misc.go rename to Wavelet/internal/router/v1/openflare/register_update.go index 1d8d5fdc..a5370d66 100644 --- a/Wavelet/internal/apps/openflare/legacy/register_misc.go +++ b/Wavelet/internal/router/v1/openflare/register_update.go @@ -1,17 +1,17 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -package legacy +package openflare import ( - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" "github.com/Rain-kl/Wavelet/internal/apps/openflare/update" "github.com/gin-gonic/gin" ) -func registerMiscRoutes(apiGroup *gin.RouterGroup) { +func registerUpdateRoutes(apiGroup *gin.RouterGroup) { updateRoute := apiGroup.Group("/update") - updateRoute.Use(compat.RootAuth()) + updateRoute.Use(apiutil.AdminRequired()) { updateRoute.GET("/latest-release", update.GetLatestReleaseHandler) updateRoute.GET("/logs/ws", update.StreamServerUpgradeLogsHandler) diff --git a/Wavelet/internal/apps/openflare/legacy/register_waf.go b/Wavelet/internal/router/v1/openflare/register_waf.go similarity index 91% rename from Wavelet/internal/apps/openflare/legacy/register_waf.go rename to Wavelet/internal/router/v1/openflare/register_waf.go index f10f505e..8bcaf47f 100644 --- a/Wavelet/internal/apps/openflare/legacy/register_waf.go +++ b/Wavelet/internal/router/v1/openflare/register_waf.go @@ -1,17 +1,17 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -package legacy +package openflare import ( - "github.com/Rain-kl/Wavelet/internal/apps/openflare/compat" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" "github.com/Rain-kl/Wavelet/internal/apps/openflare/waf" "github.com/gin-gonic/gin" ) func registerWAFRoutes(apiGroup *gin.RouterGroup) { wafRoute := apiGroup.Group("/waf") - wafRoute.Use(compat.AdminAuth()) + wafRoute.Use(apiutil.AdminRequired()) { wafRoute.GET("/ip-groups", waf.ListIPGroupsHandler) wafRoute.GET("/ip-groups/:id", waf.GetIPGroupHandler) diff --git a/Wavelet/internal/router/v1/openflare/v1.go b/Wavelet/internal/router/v1/openflare/v1.go new file mode 100644 index 00000000..5ea94768 --- /dev/null +++ b/Wavelet/internal/router/v1/openflare/v1.go @@ -0,0 +1,27 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +package openflare + +import "github.com/gin-gonic/gin" + +// V1BasePath is the OpenFlare console API prefix under /api/v1/custom. +const V1BasePath = "/api/v1/custom/openflare" + +// RegisterV1Routes mounts OpenFlare management console APIs under /custom/openflare. +// The parent group must already be the /custom router group from v1/custom.go. +func RegisterV1Routes(customGroup *gin.RouterGroup) { + group := customGroup.Group("/openflare") + registerOptionRoutes(group) + registerOriginRoutes(group) + registerApplyLogRoutes(group) + registerProxyRouteRoutes(group) + registerNodeRoutes(group) + registerWAFRoutes(group) + registerTLSRoutes(group) + registerConfigVersionRoutes(group) + registerPagesRoutes(group) + registerDashboardRoutes(group) + registerObservabilityRoutes(group) + registerUpdateRoutes(group) +} diff --git a/Wavelet/internal/testhelper/test_helper.go b/Wavelet/internal/testhelper/test_helper.go index 62530bba..3aeda7d3 100644 --- a/Wavelet/internal/testhelper/test_helper.go +++ b/Wavelet/internal/testhelper/test_helper.go @@ -110,13 +110,13 @@ func getSeedConfigsPart1() []model.SystemConfig { }, { Key: model.ConfigKeyRegistrationEnabled, - Value: configValueTrue, + Value: configValueFalse, Type: configTypeSystem, Description: "控制普通用户是否可以自主注册(true/false)", }, { Key: model.ConfigKeyPasswordRegisterEnabled, - Value: configValueTrue, + Value: configValueFalse, Type: configTypeSystem, Description: "是否允许通过密码创建本地账号", }, @@ -134,7 +134,7 @@ func getSeedConfigsPart1() []model.SystemConfig { }, { Key: model.ConfigKeyCapLoginEnabled, - Value: configValueFalse, + Value: configValueTrue, Type: configTypeSystem, Description: "是否启用登录人机验证(true/false)", }, diff --git a/docs/changelog/index.md b/docs/changelog/index.md index 7f5e9ef3..0f7c3687 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -68,6 +68,16 @@ sidebar: false - 移除 Wavelet 顶栏 OpenFlare 服务端版本入口按钮;版本升级能力保留在 Admin 设置 OpenFlare 运维 Tab。 - 将 Wavelet 默认上游仓库调整为 `Rain-kl/OpenFlare`,站点名称、邮件模板、前端默认标题与页脚品牌统一初始化为 OpenFlare;新增 goose 迁移回填既有环境的旧 Wavelet 默认值。 - Wavelet WAF IP 组列表新增「查看」操作,支持在查看弹窗中浏览当前 IP 并移除单条 IP;自动类型同步裁剪 `ext_ips` 以与 `ip_list` 保持一致。 +- 系统默认关闭用户自主注册(`registration_enabled` / `password_register_enabled`),默认开启登录 Cap 人机验证(`cap_login_enabled`)。 +- 将 OpenFlare 业务 API 路由注册从 `internal/apps/openflare/legacy` 迁移至 `internal/router/openflare`,与 Wavelet 路由分层规范对齐。 +- 移除 `internal/router/openflare` 中与 Wavelet `/api/v1` 重复的 legacy 认证路由(用户登录/注册、OAuth、Cap、认证源等),统一由 Wavelet 内置用户/OAuth/Cap 模块提供。 +- OpenFlare 路由注册统一收敛至 `internal/router/v1/openflare/`(`register_*.go` + `RegisterV1Routes`);`internal/apps/openflare/*/routers.go` 仅保留 Handler 与业务逻辑。 +- 将 OpenFlare 路由注册包从 `internal/router/openflare/` 迁移至 `internal/router/v1/openflare/`,与 v1 路由分层目录结构对齐。 +- OpenFlare 管理控制台 API 统一迁移至 `/api/v1/custom/openflare/*`,响应格式对齐 Wavelet `{error_msg, data}` + `response.Abort*`。 +- `/api/*` 仅保留 Agent/Relay/Flared 节点协议路由(legacy `{success, message, data}` 信封);控制台鉴权改为 `apiutil.AdminRequired()`。 +- 前端 OpenFlare Service 层由 `LegacyOpenFlareBaseService` 切换为 `OpenFlareBaseService`(`BaseService` + v1 路径)。 +- 为 OpenFlare v1 管理端 API 补充 Swagger 注解(`/api/v1/custom/openflare/*`,约 99 个端点)。 +- 移除已无引用的 `internal/apps/openflare/auth/`、`compat/auth.go`、`compat/routes.go` 及前端 `legacy-base.service.ts`。 ## [v2.3.4] - 2026-06-17