@@ -15,6 +15,12 @@ var (
|
||||
)
|
||||
|
||||
// about 返回构建与运行环境信息,「设置 · 关于」页展示。
|
||||
//
|
||||
// @Summary 返回构建与运行环境信息,「设置 · 关于」页展示
|
||||
// @Tags 设置
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/about [get]
|
||||
func about(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"version": buildVersion,
|
||||
|
||||
@@ -16,6 +16,11 @@ type aiAdminHandler struct {
|
||||
|
||||
// ---- 密钥 ----
|
||||
|
||||
// @Summary ---- 密钥 ----
|
||||
// @Tags AI 管理
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/ai-keys [get]
|
||||
func (h *aiAdminHandler) listKeys(c *gin.Context) {
|
||||
keys, err := h.gw.Keys(c.Request.Context())
|
||||
if err != nil {
|
||||
@@ -26,6 +31,13 @@ func (h *aiAdminHandler) listKeys(c *gin.Context) {
|
||||
}
|
||||
|
||||
// createKey 生成密钥;明文仅在本响应返回一次。
|
||||
//
|
||||
// @Summary 生成密钥
|
||||
// @Tags AI 管理
|
||||
// @Param body body object true "请求体(见接口说明)"
|
||||
// @Success 201 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/ai-keys [post]
|
||||
func (h *aiAdminHandler) createKey(c *gin.Context) {
|
||||
var req struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
@@ -44,6 +56,13 @@ func (h *aiAdminHandler) createKey(c *gin.Context) {
|
||||
c.JSON(http.StatusCreated, gin.H{"key": plaintext, "item": key})
|
||||
}
|
||||
|
||||
// @Summary 更新 AI 网关密钥
|
||||
// @Tags AI 管理
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param body body object true "请求体(见接口说明)"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/ai-keys/{id} [put]
|
||||
func (h *aiAdminHandler) updateKey(c *gin.Context) {
|
||||
id, ok := aiPathID(c)
|
||||
if !ok {
|
||||
@@ -65,6 +84,12 @@ func (h *aiAdminHandler) updateKey(c *gin.Context) {
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// @Summary 删除 AI 网关密钥
|
||||
// @Tags AI 管理
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/ai-keys/{id} [delete]
|
||||
func (h *aiAdminHandler) deleteKey(c *gin.Context) {
|
||||
id, ok := aiPathID(c)
|
||||
if !ok {
|
||||
@@ -78,6 +103,14 @@ func (h *aiAdminHandler) deleteKey(c *gin.Context) {
|
||||
}
|
||||
|
||||
// updateKeyContentLog 设置密钥内容日志窗口(红线例外:显式限时开启,0 关闭)。
|
||||
//
|
||||
// @Summary 设置密钥内容日志窗口
|
||||
// @Tags AI 管理
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param body body object true "请求体(见接口说明)"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/ai-keys/{id}/content-log [put]
|
||||
func (h *aiAdminHandler) updateKeyContentLog(c *gin.Context) {
|
||||
id, ok := aiPathID(c)
|
||||
if !ok {
|
||||
@@ -99,6 +132,12 @@ func (h *aiAdminHandler) updateKeyContentLog(c *gin.Context) {
|
||||
}
|
||||
|
||||
// listContentLogs 分页查询内容日志(可选 keyId / callLogId 过滤)。
|
||||
//
|
||||
// @Summary 分页查询内容日志
|
||||
// @Tags AI 管理
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/ai-content-logs [get]
|
||||
func (h *aiAdminHandler) listContentLogs(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
|
||||
@@ -114,6 +153,11 @@ func (h *aiAdminHandler) listContentLogs(c *gin.Context) {
|
||||
|
||||
// ---- 渠道 ----
|
||||
|
||||
// @Summary ---- 渠道 ----
|
||||
// @Tags AI 管理
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/ai-channels [get]
|
||||
func (h *aiAdminHandler) listChannels(c *gin.Context) {
|
||||
chs, err := h.gw.Channels(c.Request.Context())
|
||||
if err != nil {
|
||||
@@ -123,6 +167,12 @@ func (h *aiAdminHandler) listChannels(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"items": chs})
|
||||
}
|
||||
|
||||
// @Summary 创建 AI 渠道
|
||||
// @Tags AI 管理
|
||||
// @Param body body service.ChannelInput true "请求体"
|
||||
// @Success 201 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/ai-channels [post]
|
||||
func (h *aiAdminHandler) createChannel(c *gin.Context) {
|
||||
var req service.ChannelInput
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -137,6 +187,13 @@ func (h *aiAdminHandler) createChannel(c *gin.Context) {
|
||||
c.JSON(http.StatusCreated, ch)
|
||||
}
|
||||
|
||||
// @Summary 更新 AI 渠道
|
||||
// @Tags AI 管理
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param body body service.ChannelInput true "请求体"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/ai-channels/{id} [put]
|
||||
func (h *aiAdminHandler) updateChannel(c *gin.Context) {
|
||||
id, ok := aiPathID(c)
|
||||
if !ok {
|
||||
@@ -154,6 +211,12 @@ func (h *aiAdminHandler) updateChannel(c *gin.Context) {
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// @Summary 删除 AI 渠道
|
||||
// @Tags AI 管理
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/ai-channels/{id} [delete]
|
||||
func (h *aiAdminHandler) deleteChannel(c *gin.Context) {
|
||||
id, ok := aiPathID(c)
|
||||
if !ok {
|
||||
@@ -167,6 +230,13 @@ func (h *aiAdminHandler) deleteChannel(c *gin.Context) {
|
||||
}
|
||||
|
||||
// probeChannel 触发探测:服务可见性 + 模型同步 + 配额试调,返回更新后的渠道。
|
||||
//
|
||||
// @Summary 触发探测:服务可见性 + 模型同步 + 配额试调,返回更新后的渠道
|
||||
// @Tags AI 管理
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/ai-channels/{id}/probe [post]
|
||||
func (h *aiAdminHandler) probeChannel(c *gin.Context) {
|
||||
id, ok := aiPathID(c)
|
||||
if !ok {
|
||||
@@ -180,6 +250,12 @@ func (h *aiAdminHandler) probeChannel(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, ch)
|
||||
}
|
||||
|
||||
// @Summary 同步渠道模型缓存
|
||||
// @Tags AI 管理
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/ai-channels/{id}/sync-models [post]
|
||||
func (h *aiAdminHandler) syncChannelModels(c *gin.Context) {
|
||||
id, ok := aiPathID(c)
|
||||
if !ok {
|
||||
@@ -195,6 +271,11 @@ func (h *aiAdminHandler) syncChannelModels(c *gin.Context) {
|
||||
|
||||
// ---- 聚合模型与调用日志 ----
|
||||
|
||||
// @Summary ---- 聚合模型与调用日志 ----
|
||||
// @Tags AI 管理
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/ai-models [get]
|
||||
func (h *aiAdminHandler) gatewayModels(c *gin.Context) {
|
||||
list, err := h.gw.GatewayModels(c.Request.Context(), "")
|
||||
if err != nil {
|
||||
@@ -204,6 +285,11 @@ func (h *aiAdminHandler) gatewayModels(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"items": list.Data})
|
||||
}
|
||||
|
||||
// @Summary AI 调用日志列表
|
||||
// @Tags AI 管理
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/ai-logs [get]
|
||||
func (h *aiAdminHandler) listLogs(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("size", "50"))
|
||||
|
||||
@@ -148,6 +148,12 @@ func (h *aiGatewayHandler) logFailure(c *gin.Context, entry model.AiCallLog, req
|
||||
}
|
||||
|
||||
// chatCompletions 是 OpenAI /ai/v1/chat/completions 端点。
|
||||
//
|
||||
// @Summary OpenAI 兼容对话补全
|
||||
// @Tags AI 网关
|
||||
// @Param body body object true "OpenAI chat/completions 请求体(支持 stream)"
|
||||
// @Success 200 {object} map[string]any "OpenAI 兼容响应(流式为 SSE)"
|
||||
// @Router /ai/v1/chat/completions [post]
|
||||
func (h *aiGatewayHandler) chatCompletions(c *gin.Context) {
|
||||
var ir aiwire.ChatRequest
|
||||
if err := c.ShouldBindJSON(&ir); err != nil {
|
||||
@@ -191,6 +197,12 @@ func fillUsage(entry *model.AiCallLog, u *aiwire.Usage) {
|
||||
}
|
||||
|
||||
// embeddings 是 OpenAI /ai/v1/embeddings 端点(非流式)。
|
||||
//
|
||||
// @Summary OpenAI 兼容向量嵌入
|
||||
// @Tags AI 网关
|
||||
// @Param body body object true "OpenAI embeddings 请求体"
|
||||
// @Success 200 {object} map[string]any "OpenAI 兼容响应"
|
||||
// @Router /ai/v1/embeddings [post]
|
||||
func (h *aiGatewayHandler) embeddings(c *gin.Context) {
|
||||
var req aiwire.EmbeddingsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -280,6 +292,12 @@ func writeSSEData(c *gin.Context, v any) {
|
||||
}
|
||||
|
||||
// messages 是 Anthropic /ai/v1/messages 端点。
|
||||
//
|
||||
// @Summary Anthropic Messages 兼容端点
|
||||
// @Tags AI 网关
|
||||
// @Param body body object true "Anthropic messages 请求体(支持 stream)"
|
||||
// @Success 200 {object} map[string]any "Anthropic 兼容响应(流式为 SSE)"
|
||||
// @Router /ai/v1/messages [post]
|
||||
func (h *aiGatewayHandler) messages(c *gin.Context) {
|
||||
var req aiwire.MessagesRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -369,6 +387,11 @@ func writeAnthEvents(c *gin.Context, events []service.AnthEvent) {
|
||||
}
|
||||
|
||||
// listModels 是 /ai/v1/models 端点(OpenAI 格式,从启用渠道的模型缓存聚合)。
|
||||
//
|
||||
// @Summary 可用模型列表
|
||||
// @Tags AI 网关
|
||||
// @Success 200 {object} map[string]any "OpenAI 兼容 models 列表"
|
||||
// @Router /ai/v1/models [get]
|
||||
func (h *aiGatewayHandler) listModels(c *gin.Context) {
|
||||
list, err := h.gw.GatewayModels(c.Request.Context(), keyGroup(c))
|
||||
if err != nil {
|
||||
@@ -379,6 +402,12 @@ func (h *aiGatewayHandler) listModels(c *gin.Context) {
|
||||
}
|
||||
|
||||
// responses 是 OpenAI /ai/v1/responses 端点(无状态子集)。
|
||||
//
|
||||
// @Summary OpenAI Responses 兼容端点
|
||||
// @Tags AI 网关
|
||||
// @Param body body object true "OpenAI responses 请求体(支持 stream)"
|
||||
// @Success 200 {object} map[string]any "OpenAI 兼容响应(流式为 SSE)"
|
||||
// @Router /ai/v1/responses [post]
|
||||
func (h *aiGatewayHandler) responses(c *gin.Context) {
|
||||
var req aiwire.RespRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
|
||||
@@ -10,6 +10,14 @@ import (
|
||||
|
||||
// ---- 实例 IP ----
|
||||
|
||||
// @Summary ---- 实例 IP ----
|
||||
// @Tags 计算
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param instanceId path string true "instanceId"
|
||||
// @Param body body object true "请求体(见接口说明)"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/change-public-ip [post]
|
||||
func (h *ociConfigHandler) changePublicIP(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -27,6 +35,14 @@ func (h *ociConfigHandler) changePublicIP(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"publicIp": ip})
|
||||
}
|
||||
|
||||
// @Summary 实例添加 IPv6 地址
|
||||
// @Tags 计算
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param instanceId path string true "instanceId"
|
||||
// @Param body body object true "请求体(见接口说明)"
|
||||
// @Success 201 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/ipv6-addresses [post]
|
||||
func (h *ociConfigHandler) addInstanceIpv6(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -45,6 +61,13 @@ func (h *ociConfigHandler) addInstanceIpv6(c *gin.Context) {
|
||||
c.JSON(http.StatusCreated, gin.H{"ipv6Address": address})
|
||||
}
|
||||
|
||||
// @Summary 移除实例 IPv6 地址
|
||||
// @Tags 计算
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param instanceId path string true "instanceId"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/ipv6-addresses [delete]
|
||||
func (h *ociConfigHandler) deleteInstanceIpv6(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -65,6 +88,13 @@ type attachVnicRequest struct {
|
||||
oci.AttachVnicInput
|
||||
}
|
||||
|
||||
// @Summary 实例 VNIC 列表
|
||||
// @Tags 计算
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param instanceId path string true "instanceId"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/vnics [get]
|
||||
func (h *ociConfigHandler) listInstanceVnics(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -78,6 +108,14 @@ func (h *ociConfigHandler) listInstanceVnics(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, vnics)
|
||||
}
|
||||
|
||||
// @Summary 附加 VNIC
|
||||
// @Tags 计算
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param instanceId path string true "instanceId"
|
||||
// @Param body body attachVnicRequest true "请求体"
|
||||
// @Success 201 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/vnics [post]
|
||||
func (h *ociConfigHandler) attachVnic(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -96,6 +134,13 @@ func (h *ociConfigHandler) attachVnic(c *gin.Context) {
|
||||
c.JSON(http.StatusCreated, vnic)
|
||||
}
|
||||
|
||||
// @Summary 分离 VNIC
|
||||
// @Tags 计算
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param attachmentId path string true "attachmentId"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/vnic-attachments/{attachmentId} [delete]
|
||||
func (h *ociConfigHandler) detachVnic(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -109,6 +154,15 @@ func (h *ociConfigHandler) detachVnic(c *gin.Context) {
|
||||
}
|
||||
|
||||
// addVnicIpv6 为指定 VNIC 添加 IPv6;body.address 留空自动分配。
|
||||
//
|
||||
// @Summary 为指定 VNIC 添加 IPv6
|
||||
// @Tags 计算
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param vnicId path string true "vnicId"
|
||||
// @Param body body object true "请求体(见接口说明)"
|
||||
// @Success 201 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/vnics/{vnicId}/ipv6-addresses [post]
|
||||
func (h *ociConfigHandler) addVnicIpv6(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -137,6 +191,13 @@ type attachBootVolumeRequest struct {
|
||||
BootVolumeID string `json:"bootVolumeId" binding:"required"`
|
||||
}
|
||||
|
||||
// @Summary 引导卷附件列表
|
||||
// @Tags 存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param instanceId path string true "instanceId"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/boot-volume-attachments [get]
|
||||
func (h *ociConfigHandler) listBootVolumeAttachments(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -150,6 +211,14 @@ func (h *ociConfigHandler) listBootVolumeAttachments(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, atts)
|
||||
}
|
||||
|
||||
// @Summary 附加引导卷
|
||||
// @Tags 存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param instanceId path string true "instanceId"
|
||||
// @Param body body attachBootVolumeRequest true "请求体"
|
||||
// @Success 201 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/boot-volume-attachments [post]
|
||||
func (h *ociConfigHandler) attachBootVolume(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -168,6 +237,14 @@ func (h *ociConfigHandler) attachBootVolume(c *gin.Context) {
|
||||
c.JSON(http.StatusCreated, att)
|
||||
}
|
||||
|
||||
// @Summary 更换实例引导卷
|
||||
// @Tags 存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param instanceId path string true "instanceId"
|
||||
// @Param body body attachBootVolumeRequest true "请求体"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/replace-boot-volume [post]
|
||||
func (h *ociConfigHandler) replaceBootVolume(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -186,6 +263,13 @@ func (h *ociConfigHandler) replaceBootVolume(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, att)
|
||||
}
|
||||
|
||||
// @Summary 分离引导卷
|
||||
// @Tags 存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param attachmentId path string true "attachmentId"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/boot-volume-attachments/{attachmentId} [delete]
|
||||
func (h *ociConfigHandler) detachBootVolume(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -206,6 +290,12 @@ type attachVolumeRequest struct {
|
||||
ReadOnly bool `json:"readOnly"`
|
||||
}
|
||||
|
||||
// @Summary 块存储卷列表
|
||||
// @Tags 存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/volumes [get]
|
||||
func (h *ociConfigHandler) listBlockVolumes(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -219,6 +309,13 @@ func (h *ociConfigHandler) listBlockVolumes(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, volumes)
|
||||
}
|
||||
|
||||
// @Summary 卷附件列表
|
||||
// @Tags 存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param instanceId path string true "instanceId"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/volume-attachments [get]
|
||||
func (h *ociConfigHandler) listVolumeAttachments(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -232,6 +329,14 @@ func (h *ociConfigHandler) listVolumeAttachments(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, atts)
|
||||
}
|
||||
|
||||
// @Summary 附加块存储卷
|
||||
// @Tags 存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param instanceId path string true "instanceId"
|
||||
// @Param body body attachVolumeRequest true "请求体"
|
||||
// @Success 201 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/volume-attachments [post]
|
||||
func (h *ociConfigHandler) attachVolume(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -250,6 +355,13 @@ func (h *ociConfigHandler) attachVolume(c *gin.Context) {
|
||||
c.JSON(http.StatusCreated, att)
|
||||
}
|
||||
|
||||
// @Summary 分离块存储卷
|
||||
// @Tags 存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param attachmentId path string true "attachmentId"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/volume-attachments/{attachmentId} [delete]
|
||||
func (h *ociConfigHandler) detachVolume(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
|
||||
@@ -24,6 +24,17 @@ type loginRequest struct {
|
||||
TotpCode string `json:"totpCode"`
|
||||
}
|
||||
|
||||
// login 校验用户名密码(与可选 TOTP)后签发 JWT。
|
||||
//
|
||||
// @Summary 登录
|
||||
// @Tags 认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body loginRequest true "登录凭据"
|
||||
// @Success 200 {object} map[string]any "token 与 expiresAt"
|
||||
// @Failure 401 {object} map[string]string "凭据错误"
|
||||
// @Failure 428 {object} map[string]any "需要两步验证码(totpRequired=true)"
|
||||
// @Router /api/v1/auth/login [post]
|
||||
func (h *authHandler) login(c *gin.Context) {
|
||||
var req loginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -61,6 +72,12 @@ func (h *authHandler) login(c *gin.Context) {
|
||||
}
|
||||
|
||||
// logout 把当前令牌拉黑至自然过期(secured 组内,写请求由系统日志中间件留痕)。
|
||||
//
|
||||
// @Summary 登出
|
||||
// @Tags 认证
|
||||
// @Success 204 "令牌已吊销"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/auth/logout [post]
|
||||
func (h *authHandler) logout(c *gin.Context) {
|
||||
token, ok := strings.CutPrefix(c.GetHeader("Authorization"), "Bearer ")
|
||||
if ok && token != "" {
|
||||
|
||||
@@ -20,6 +20,12 @@ type authxHandler struct {
|
||||
// ---- TOTP(JWT 组内) ----
|
||||
|
||||
// totpStatus 返回当前账号两步验证启用状态。
|
||||
//
|
||||
// @Summary 两步验证状态
|
||||
// @Tags 认证
|
||||
// @Success 200 {object} map[string]bool "enabled"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/auth/totp [get]
|
||||
func (h *authxHandler) totpStatus(c *gin.Context) {
|
||||
enabled, err := h.auth.TotpStatus(c.Request.Context(), c.GetString(usernameKey))
|
||||
if err != nil {
|
||||
@@ -30,6 +36,13 @@ func (h *authxHandler) totpStatus(c *gin.Context) {
|
||||
}
|
||||
|
||||
// totpSetup 生成待激活密钥,返回手动输入码与 otpauth URI(前端渲染二维码)。
|
||||
//
|
||||
// @Summary 发起两步验证设置
|
||||
// @Tags 认证
|
||||
// @Success 200 {object} map[string]string "secret 与 otpauthUri"
|
||||
// @Failure 409 {object} map[string]string "已启用"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/auth/totp/setup [post]
|
||||
func (h *authxHandler) totpSetup(c *gin.Context) {
|
||||
secret, uri, err := h.auth.SetupTotp(c.Request.Context(), c.GetString(usernameKey))
|
||||
if err != nil {
|
||||
@@ -44,6 +57,13 @@ func (h *authxHandler) totpSetup(c *gin.Context) {
|
||||
}
|
||||
|
||||
// totpActivate 校验验证码并正式启用两步验证。
|
||||
//
|
||||
// @Summary 激活两步验证
|
||||
// @Tags 认证
|
||||
// @Param body body object true "{code: 6 位验证码}"
|
||||
// @Success 204 "已启用"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/auth/totp/activate [post]
|
||||
func (h *authxHandler) totpActivate(c *gin.Context) {
|
||||
var req struct {
|
||||
Code string `json:"code" binding:"required"`
|
||||
@@ -65,6 +85,13 @@ func (h *authxHandler) totpActivate(c *gin.Context) {
|
||||
}
|
||||
|
||||
// totpDisable 停用两步验证;需当前验证码或登录密码任一确认。
|
||||
//
|
||||
// @Summary 停用两步验证
|
||||
// @Tags 认证
|
||||
// @Param body body object true "{password 或 code 任一确认}"
|
||||
// @Success 204 "已停用"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/auth/totp/disable [post]
|
||||
func (h *authxHandler) totpDisable(c *gin.Context) {
|
||||
var req struct {
|
||||
Password string `json:"password"`
|
||||
@@ -89,6 +116,12 @@ func (h *authxHandler) totpDisable(c *gin.Context) {
|
||||
// ---- 登录凭据(JWT 组内) ----
|
||||
|
||||
// getCredentials 返回登录凭据摘要:当前用户名与密码登录禁用态。
|
||||
//
|
||||
// @Summary 登录凭据摘要
|
||||
// @Tags 认证
|
||||
// @Success 200 {object} map[string]any "username 与 passwordLoginDisabled"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/auth/credentials [get]
|
||||
func (h *authxHandler) getCredentials(c *gin.Context) {
|
||||
disabled, err := h.auth.PasswordLoginDisabled(c.Request.Context())
|
||||
if err != nil {
|
||||
@@ -102,6 +135,14 @@ func (h *authxHandler) getCredentials(c *gin.Context) {
|
||||
}
|
||||
|
||||
// updateCredentials 修改用户名 / 密码;当前密码必验,改名后旧 JWT 随即失效(前端应重新登录)。
|
||||
//
|
||||
// @Summary 修改登录凭据
|
||||
// @Tags 认证
|
||||
// @Param body body service.UpdateCredentialsInput true "新凭据(当前密码必验)"
|
||||
// @Success 204 "已更新,请重新登录"
|
||||
// @Failure 401 {object} map[string]string "当前密码错误"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/auth/credentials [put]
|
||||
func (h *authxHandler) updateCredentials(c *gin.Context) {
|
||||
var req service.UpdateCredentialsInput
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -125,6 +166,14 @@ func (h *authxHandler) updateCredentials(c *gin.Context) {
|
||||
}
|
||||
|
||||
// updatePasswordLogin 保存密码登录禁用开关;开启需至少绑定一个外部身份。
|
||||
//
|
||||
// @Summary 密码登录开关
|
||||
// @Tags 认证
|
||||
// @Param body body object true "{disabled: bool}"
|
||||
// @Success 204 "已保存"
|
||||
// @Failure 409 {object} map[string]string "未绑定外部身份"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/auth/password-login [put]
|
||||
func (h *authxHandler) updatePasswordLogin(c *gin.Context) {
|
||||
var req struct {
|
||||
Disabled *bool `json:"disabled" binding:"required"`
|
||||
@@ -150,6 +199,11 @@ func (h *authxHandler) updatePasswordLogin(c *gin.Context) {
|
||||
// oauthProviders 返回已配置的 provider 列表(公开,登录页展示按钮),
|
||||
// 并附带密码登录禁用开关:开启且存在可用外部身份时,登录页隐藏密码表单;
|
||||
// provider 清空时不下发禁用,保证界面始终留有登录入口(后端 Login 仍会拒绝)。
|
||||
//
|
||||
// @Summary 外部登录 provider 列表
|
||||
// @Tags 认证
|
||||
// @Success 200 {object} map[string]any "providers 与 passwordLoginDisabled"
|
||||
// @Router /api/v1/auth/oauth/providers [get]
|
||||
func (h *authxHandler) oauthProviders(c *gin.Context) {
|
||||
providers := h.oauth.Providers(c.Request.Context())
|
||||
disabled := false
|
||||
@@ -160,6 +214,13 @@ func (h *authxHandler) oauthProviders(c *gin.Context) {
|
||||
}
|
||||
|
||||
// oauthAuthorize 返回授权跳转 URL;mode=bind 需有效 JWT(绑定到当前账号),login 公开。
|
||||
//
|
||||
// @Summary 外部登录授权跳转
|
||||
// @Tags 认证
|
||||
// @Param provider path string true "oidc / github"
|
||||
// @Param mode query string false "bind=绑定当前账号(需 Bearer),缺省登录"
|
||||
// @Success 200 {object} map[string]string "url"
|
||||
// @Router /api/v1/auth/oauth/{provider}/authorize [get]
|
||||
func (h *authxHandler) oauthAuthorize(c *gin.Context) {
|
||||
provider := c.Param("provider")
|
||||
if provider != "oidc" && provider != "github" {
|
||||
@@ -201,6 +262,14 @@ func (h *authxHandler) bearerUser(c *gin.Context) (string, bool) {
|
||||
}
|
||||
|
||||
// oauthCallback 是 provider 回调落点(浏览器直达):完成登录或绑定后 302 回前端。
|
||||
//
|
||||
// @Summary 外部登录回调
|
||||
// @Tags 认证
|
||||
// @Param provider path string true "oidc / github"
|
||||
// @Param state query string true "授权流程 state"
|
||||
// @Param code query string true "授权码"
|
||||
// @Success 302 "登录 token 经 fragment 回前端,绑定回设置页"
|
||||
// @Router /api/v1/auth/oauth/{provider}/callback [get]
|
||||
func (h *authxHandler) oauthCallback(c *gin.Context) {
|
||||
provider := c.Param("provider")
|
||||
token, _, mode, err := h.oauth.HandleCallback(
|
||||
@@ -233,6 +302,12 @@ func oauthErrText(err error) string {
|
||||
}
|
||||
|
||||
// identities 列出当前账号绑定的外部身份。
|
||||
//
|
||||
// @Summary 已绑定外部身份
|
||||
// @Tags 认证
|
||||
// @Success 200 {object} map[string]any "items"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/auth/identities [get]
|
||||
func (h *authxHandler) identities(c *gin.Context) {
|
||||
items, err := h.oauth.Identities(c.Request.Context(), c.GetString(usernameKey))
|
||||
if err != nil {
|
||||
@@ -243,6 +318,14 @@ func (h *authxHandler) identities(c *gin.Context) {
|
||||
}
|
||||
|
||||
// unbindIdentity 解绑外部身份;密码登录禁用期间不允许解绑最后一个身份。
|
||||
//
|
||||
// @Summary 解绑外部身份
|
||||
// @Tags 认证
|
||||
// @Param id path int true "身份 ID"
|
||||
// @Success 204 "已解绑"
|
||||
// @Failure 409 {object} map[string]string "最后一个身份不可解绑"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/auth/identities/{id} [delete]
|
||||
func (h *authxHandler) unbindIdentity(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
@@ -263,6 +346,12 @@ func (h *authxHandler) unbindIdentity(c *gin.Context) {
|
||||
// ---- OAuth provider 设置(JWT 组内) ----
|
||||
|
||||
// getOAuthSettings 返回 provider 配置(secret 只回设置态)。
|
||||
//
|
||||
// @Summary OAuth provider 配置
|
||||
// @Tags 设置
|
||||
// @Success 200 {object} service.OAuthProvidersView
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/settings/oauth [get]
|
||||
func (h *authxHandler) getOAuthSettings(c *gin.Context, settings *service.SettingService) {
|
||||
view, err := settings.OAuthView(c.Request.Context())
|
||||
if err != nil {
|
||||
@@ -273,6 +362,13 @@ func (h *authxHandler) getOAuthSettings(c *gin.Context, settings *service.Settin
|
||||
}
|
||||
|
||||
// updateOAuthSettings 保存 provider 配置;secret 缺省沿用、空串清除。
|
||||
//
|
||||
// @Summary 保存 OAuth provider 配置
|
||||
// @Tags 设置
|
||||
// @Param body body service.UpdateOAuthInput true "provider 配置"
|
||||
// @Success 200 {object} service.OAuthProvidersView
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/settings/oauth [put]
|
||||
func (h *authxHandler) updateOAuthSettings(c *gin.Context, settings *service.SettingService) {
|
||||
var req service.UpdateOAuthInput
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
|
||||
@@ -13,6 +13,14 @@ type createConsoleConnectionRequest struct {
|
||||
SSHPublicKey string `json:"sshPublicKey" binding:"required"`
|
||||
}
|
||||
|
||||
// @Summary 创建实例控制台连接
|
||||
// @Tags 计算
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param instanceId path string true "instanceId"
|
||||
// @Param body body createConsoleConnectionRequest true "请求体"
|
||||
// @Success 201 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/console-connections [post]
|
||||
func (h *ociConfigHandler) createConsoleConnection(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -31,6 +39,13 @@ func (h *ociConfigHandler) createConsoleConnection(c *gin.Context) {
|
||||
c.JSON(http.StatusCreated, conn)
|
||||
}
|
||||
|
||||
// @Summary 实例控制台连接列表
|
||||
// @Tags 计算
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param instanceId path string true "instanceId"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/console-connections [get]
|
||||
func (h *ociConfigHandler) listConsoleConnections(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -44,6 +59,13 @@ func (h *ociConfigHandler) listConsoleConnections(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, connections)
|
||||
}
|
||||
|
||||
// @Summary 删除实例控制台连接
|
||||
// @Tags 计算
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param connectionId path string true "connectionId"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/console-connections/{connectionId} [delete]
|
||||
func (h *ociConfigHandler) deleteConsoleConnection(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
|
||||
@@ -37,6 +37,12 @@ func boolOr(v *bool, def bool) bool {
|
||||
return *v
|
||||
}
|
||||
|
||||
// @Summary 身份提供商列表
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/identity-providers [get]
|
||||
func (h *ociConfigHandler) listIdentityProviders(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -50,6 +56,13 @@ func (h *ociConfigHandler) listIdentityProviders(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, idps)
|
||||
}
|
||||
|
||||
// @Summary 创建身份提供商(SAML)
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param body body createIdpRequest true "请求体"
|
||||
// @Success 201 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/identity-providers [post]
|
||||
func (h *ociConfigHandler) createIdentityProvider(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -81,6 +94,14 @@ func (h *ociConfigHandler) createIdentityProvider(c *gin.Context) {
|
||||
c.JSON(http.StatusCreated, idp)
|
||||
}
|
||||
|
||||
// @Summary 激活身份提供商
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param idpId path string true "idpId"
|
||||
// @Param body body object true "请求体(见接口说明)"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/identity-providers/{idpId}/activate [post]
|
||||
func (h *ociConfigHandler) activateIdentityProvider(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -101,6 +122,13 @@ func (h *ociConfigHandler) activateIdentityProvider(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, idp)
|
||||
}
|
||||
|
||||
// @Summary 删除身份提供商
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param idpId path string true "idpId"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/identity-providers/{idpId} [delete]
|
||||
func (h *ociConfigHandler) deleteIdentityProvider(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -113,6 +141,12 @@ func (h *ociConfigHandler) deleteIdentityProvider(c *gin.Context) {
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// @Summary 下载 SAML 元数据
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/saml-metadata [get]
|
||||
func (h *ociConfigHandler) downloadSamlMetadata(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -127,6 +161,12 @@ func (h *ociConfigHandler) downloadSamlMetadata(c *gin.Context) {
|
||||
c.Data(http.StatusOK, "application/xml", metadata)
|
||||
}
|
||||
|
||||
// @Summary 单点登录规则列表
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/sign-on-rules [get]
|
||||
func (h *ociConfigHandler) listSignOnRules(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -140,6 +180,13 @@ func (h *ociConfigHandler) listSignOnRules(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, rules)
|
||||
}
|
||||
|
||||
// @Summary 创建 MFA 豁免规则
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param body body object true "请求体(见接口说明)"
|
||||
// @Success 201 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/sign-on-exemptions [post]
|
||||
func (h *ociConfigHandler) createMfaExemption(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -160,6 +207,13 @@ func (h *ociConfigHandler) createMfaExemption(c *gin.Context) {
|
||||
c.JSON(http.StatusCreated, rule)
|
||||
}
|
||||
|
||||
// @Summary 删除 MFA 豁免规则
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param ruleId path string true "ruleId"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/sign-on-exemptions/{ruleId} [delete]
|
||||
func (h *ociConfigHandler) deleteMfaExemption(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
|
||||
@@ -8,6 +8,12 @@ import (
|
||||
"oci-portal/internal/oci"
|
||||
)
|
||||
|
||||
// @Summary 可用域列表
|
||||
// @Tags 租户配置
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/availability-domains [get]
|
||||
func (h *ociConfigHandler) availabilityDomains(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -58,6 +64,12 @@ type instanceActionRequest struct {
|
||||
Action string `json:"action" binding:"required"`
|
||||
}
|
||||
|
||||
// @Summary 实例列表
|
||||
// @Tags 计算
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/instances [get]
|
||||
func (h *ociConfigHandler) listInstances(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -71,6 +83,13 @@ func (h *ociConfigHandler) listInstances(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, instances)
|
||||
}
|
||||
|
||||
// @Summary 创建实例
|
||||
// @Tags 计算
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param body body createInstanceRequest true "请求体"
|
||||
// @Success 201 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/instances [post]
|
||||
func (h *ociConfigHandler) createInstance(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -124,6 +143,13 @@ func (h *ociConfigHandler) createInstance(c *gin.Context) {
|
||||
c.JSON(status, gin.H{"instances": instances, "errors": failures})
|
||||
}
|
||||
|
||||
// @Summary 实例详情
|
||||
// @Tags 计算
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param instanceId path string true "instanceId"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/instances/{instanceId} [get]
|
||||
func (h *ociConfigHandler) getInstance(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -137,6 +163,14 @@ func (h *ociConfigHandler) getInstance(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, instance)
|
||||
}
|
||||
|
||||
// @Summary 更新实例
|
||||
// @Tags 计算
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param instanceId path string true "instanceId"
|
||||
// @Param body body updateInstanceRequest true "请求体"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/instances/{instanceId} [put]
|
||||
func (h *ociConfigHandler) updateInstance(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -161,6 +195,13 @@ func (h *ociConfigHandler) updateInstance(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, instance)
|
||||
}
|
||||
|
||||
// @Summary 终止实例
|
||||
// @Tags 计算
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param instanceId path string true "instanceId"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/instances/{instanceId} [delete]
|
||||
func (h *ociConfigHandler) terminateInstance(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -174,6 +215,14 @@ func (h *ociConfigHandler) terminateInstance(c *gin.Context) {
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// @Summary 实例电源操作(启停/重启)
|
||||
// @Tags 计算
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param instanceId path string true "instanceId"
|
||||
// @Param body body instanceActionRequest true "请求体"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/action [post]
|
||||
func (h *ociConfigHandler) instanceAction(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -201,6 +250,12 @@ type updateBootVolumeRequest struct {
|
||||
VpusPerGB int64 `json:"vpusPerGB"`
|
||||
}
|
||||
|
||||
// @Summary 引导卷列表
|
||||
// @Tags 存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/boot-volumes [get]
|
||||
func (h *ociConfigHandler) listBootVolumes(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -214,6 +269,13 @@ func (h *ociConfigHandler) listBootVolumes(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, volumes)
|
||||
}
|
||||
|
||||
// @Summary 引导卷详情
|
||||
// @Tags 存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param bootVolumeId path string true "bootVolumeId"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/boot-volumes/{bootVolumeId} [get]
|
||||
func (h *ociConfigHandler) getBootVolume(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -227,6 +289,14 @@ func (h *ociConfigHandler) getBootVolume(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, volume)
|
||||
}
|
||||
|
||||
// @Summary 更新引导卷
|
||||
// @Tags 存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param bootVolumeId path string true "bootVolumeId"
|
||||
// @Param body body updateBootVolumeRequest true "请求体"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/boot-volumes/{bootVolumeId} [put]
|
||||
func (h *ociConfigHandler) updateBootVolume(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -250,6 +320,13 @@ func (h *ociConfigHandler) updateBootVolume(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, volume)
|
||||
}
|
||||
|
||||
// @Summary 删除引导卷
|
||||
// @Tags 存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param bootVolumeId path string true "bootVolumeId"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/boot-volumes/{bootVolumeId} [delete]
|
||||
func (h *ociConfigHandler) deleteBootVolume(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
|
||||
@@ -16,6 +16,13 @@ type logEventHandler struct {
|
||||
}
|
||||
|
||||
// getWebhook 查询配置的回调地址;未生成时 exists 为 false 且不含 secret。
|
||||
//
|
||||
// @Summary 查询配置的回调地址
|
||||
// @Tags 任务与日志回传
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/log-webhook [get]
|
||||
func (h *logEventHandler) getWebhook(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -34,6 +41,13 @@ func (h *logEventHandler) getWebhook(c *gin.Context) {
|
||||
}
|
||||
|
||||
// ensureWebhook 生成(或幂等返回)配置的回传 secret 与回调路径。
|
||||
//
|
||||
// @Summary 生成
|
||||
// @Tags 任务与日志回传
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/log-webhook [post]
|
||||
func (h *logEventHandler) ensureWebhook(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -48,6 +62,13 @@ func (h *logEventHandler) ensureWebhook(c *gin.Context) {
|
||||
}
|
||||
|
||||
// revokeWebhook 撤销配置的回传 secret,旧回调地址随即失效。
|
||||
//
|
||||
// @Summary 撤销配置的回传 secret,旧回调地址随即失效
|
||||
// @Tags 任务与日志回传
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/log-webhook [delete]
|
||||
func (h *logEventHandler) revokeWebhook(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -61,6 +82,14 @@ func (h *logEventHandler) revokeWebhook(c *gin.Context) {
|
||||
}
|
||||
|
||||
// list 分页查询回传事件;cfgId 缺省为全部租户。
|
||||
//
|
||||
// @Summary 回传日志事件列表
|
||||
// @Tags 任务与日志回传
|
||||
// @Param configId query int false "按配置过滤"
|
||||
// @Param q query string false "关键字"
|
||||
// @Success 200 {object} map[string]any "items 与 total"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/log-events [get]
|
||||
func (h *logEventHandler) list(c *gin.Context) {
|
||||
cfgID, _ := strconv.ParseUint(c.Query("cfgId"), 10, 64)
|
||||
page, _ := strconv.Atoi(c.Query("page"))
|
||||
@@ -78,6 +107,13 @@ func (h *logEventHandler) list(c *gin.Context) {
|
||||
}
|
||||
|
||||
// getRelay 查询 OCI 侧链路状态与关键事件清单。
|
||||
//
|
||||
// @Summary 查询 OCI 侧链路状态与关键事件清单
|
||||
// @Tags 任务与日志回传
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/log-relay [get]
|
||||
func (h *logEventHandler) getRelay(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -92,6 +128,13 @@ func (h *logEventHandler) getRelay(c *gin.Context) {
|
||||
}
|
||||
|
||||
// setupRelay 一键建立回传链路(P1 引导创建);同步执行,前端以长超时等待。
|
||||
//
|
||||
// @Summary 一键建立回传链路
|
||||
// @Tags 任务与日志回传
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/log-relay [post]
|
||||
func (h *logEventHandler) setupRelay(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -106,6 +149,13 @@ func (h *logEventHandler) setupRelay(c *gin.Context) {
|
||||
}
|
||||
|
||||
// teardownRelay 逆序销毁链路并撤销 secret。
|
||||
//
|
||||
// @Summary 逆序销毁链路并撤销 secret
|
||||
// @Tags 任务与日志回传
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/log-relay [delete]
|
||||
func (h *logEventHandler) teardownRelay(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
|
||||
@@ -8,6 +8,12 @@ import (
|
||||
"oci-portal/internal/oci"
|
||||
)
|
||||
|
||||
// @Summary 实例形状列表
|
||||
// @Tags 租户配置
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/shapes [get]
|
||||
func (h *ociConfigHandler) shapes(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -21,6 +27,12 @@ func (h *ociConfigHandler) shapes(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, shapes)
|
||||
}
|
||||
|
||||
// @Summary 镜像列表
|
||||
// @Tags 租户配置
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/images [get]
|
||||
func (h *ociConfigHandler) images(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -38,6 +50,13 @@ func (h *ociConfigHandler) images(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, images)
|
||||
}
|
||||
|
||||
// @Summary 镜像详情
|
||||
// @Tags 租户配置
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param imageId path string true "imageId"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/images/{imageId} [get]
|
||||
func (h *ociConfigHandler) getImage(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -67,6 +86,12 @@ type renameRequest struct {
|
||||
DisplayName string `json:"displayName" binding:"required"`
|
||||
}
|
||||
|
||||
// @Summary VCN 列表
|
||||
// @Tags 网络
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/vcns [get]
|
||||
func (h *ociConfigHandler) listVCNs(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -80,6 +105,13 @@ func (h *ociConfigHandler) listVCNs(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, vcns)
|
||||
}
|
||||
|
||||
// @Summary 创建 VCN
|
||||
// @Tags 网络
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param body body createVCNRequest true "请求体"
|
||||
// @Success 201 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/vcns [post]
|
||||
func (h *ociConfigHandler) createVCN(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -105,6 +137,13 @@ func (h *ociConfigHandler) createVCN(c *gin.Context) {
|
||||
c.JSON(http.StatusCreated, vcn)
|
||||
}
|
||||
|
||||
// @Summary VCN 详情
|
||||
// @Tags 网络
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param vcnId path string true "vcnId"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/vcns/{vcnId} [get]
|
||||
func (h *ociConfigHandler) getVCN(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -118,6 +157,14 @@ func (h *ociConfigHandler) getVCN(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, vcn)
|
||||
}
|
||||
|
||||
// @Summary 更新 VCN
|
||||
// @Tags 网络
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param vcnId path string true "vcnId"
|
||||
// @Param body body renameRequest true "请求体"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/vcns/{vcnId} [put]
|
||||
func (h *ociConfigHandler) updateVCN(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -136,6 +183,13 @@ func (h *ociConfigHandler) updateVCN(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, vcn)
|
||||
}
|
||||
|
||||
// @Summary 删除 VCN
|
||||
// @Tags 网络
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param vcnId path string true "vcnId"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/vcns/{vcnId} [delete]
|
||||
func (h *ociConfigHandler) deleteVCN(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -152,6 +206,14 @@ type enableIPv6Request struct {
|
||||
Region string `json:"region"`
|
||||
}
|
||||
|
||||
// @Summary VCN 启用 IPv6
|
||||
// @Tags 网络
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param vcnId path string true "vcnId"
|
||||
// @Param body body enableIPv6Request true "请求体"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/vcns/{vcnId}/enable-ipv6 [post]
|
||||
func (h *ociConfigHandler) enableVCNIPv6(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -182,6 +244,12 @@ type createSubnetRequest struct {
|
||||
ProhibitPublicIP bool `json:"prohibitPublicIp"`
|
||||
}
|
||||
|
||||
// @Summary 子网列表
|
||||
// @Tags 网络
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/subnets [get]
|
||||
func (h *ociConfigHandler) listSubnets(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -195,6 +263,13 @@ func (h *ociConfigHandler) listSubnets(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, subnets)
|
||||
}
|
||||
|
||||
// @Summary 创建子网
|
||||
// @Tags 网络
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param body body createSubnetRequest true "请求体"
|
||||
// @Success 201 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/subnets [post]
|
||||
func (h *ociConfigHandler) createSubnet(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -221,6 +296,13 @@ func (h *ociConfigHandler) createSubnet(c *gin.Context) {
|
||||
c.JSON(http.StatusCreated, subnet)
|
||||
}
|
||||
|
||||
// @Summary 子网详情
|
||||
// @Tags 网络
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param subnetId path string true "subnetId"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/subnets/{subnetId} [get]
|
||||
func (h *ociConfigHandler) getSubnet(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -234,6 +316,14 @@ func (h *ociConfigHandler) getSubnet(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, subnet)
|
||||
}
|
||||
|
||||
// @Summary 更新子网
|
||||
// @Tags 网络
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param subnetId path string true "subnetId"
|
||||
// @Param body body renameRequest true "请求体"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/subnets/{subnetId} [put]
|
||||
func (h *ociConfigHandler) updateSubnet(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -252,6 +342,13 @@ func (h *ociConfigHandler) updateSubnet(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, subnet)
|
||||
}
|
||||
|
||||
// @Summary 删除子网
|
||||
// @Tags 网络
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param subnetId path string true "subnetId"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/subnets/{subnetId} [delete]
|
||||
func (h *ociConfigHandler) deleteSubnet(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -281,6 +378,12 @@ type updateSecurityListRequest struct {
|
||||
EgressRules *[]oci.SecurityRule `json:"egressRules"`
|
||||
}
|
||||
|
||||
// @Summary 安全列表清单
|
||||
// @Tags 网络
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/security-lists [get]
|
||||
func (h *ociConfigHandler) listSecurityLists(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -294,6 +397,13 @@ func (h *ociConfigHandler) listSecurityLists(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, lists)
|
||||
}
|
||||
|
||||
// @Summary 创建安全列表
|
||||
// @Tags 网络
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param body body createSecurityListRequest true "请求体"
|
||||
// @Success 201 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/security-lists [post]
|
||||
func (h *ociConfigHandler) createSecurityList(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -318,6 +428,13 @@ func (h *ociConfigHandler) createSecurityList(c *gin.Context) {
|
||||
c.JSON(http.StatusCreated, list)
|
||||
}
|
||||
|
||||
// @Summary 安全列表详情
|
||||
// @Tags 网络
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param securityListId path string true "securityListId"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/security-lists/{securityListId} [get]
|
||||
func (h *ociConfigHandler) getSecurityList(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -331,6 +448,14 @@ func (h *ociConfigHandler) getSecurityList(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, list)
|
||||
}
|
||||
|
||||
// @Summary 更新安全列表
|
||||
// @Tags 网络
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param securityListId path string true "securityListId"
|
||||
// @Param body body updateSecurityListRequest true "请求体"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/security-lists/{securityListId} [put]
|
||||
func (h *ociConfigHandler) updateSecurityList(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -354,6 +479,13 @@ func (h *ociConfigHandler) updateSecurityList(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, list)
|
||||
}
|
||||
|
||||
// @Summary 删除安全列表
|
||||
// @Tags 网络
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param securityListId path string true "securityListId"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/security-lists/{securityListId} [delete]
|
||||
func (h *ociConfigHandler) deleteSecurityList(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
|
||||
@@ -33,6 +33,12 @@ type importRequest struct {
|
||||
ProxyID *uint `json:"proxyId"` // 关联出站代理,null 直连
|
||||
}
|
||||
|
||||
// @Summary 导入租户配置
|
||||
// @Tags 租户配置
|
||||
// @Param body body importRequest true "API Key 配置(私钥密文落库)"
|
||||
// @Success 201 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs [post]
|
||||
func (h *ociConfigHandler) create(c *gin.Context) {
|
||||
var req importRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -60,6 +66,11 @@ func (h *ociConfigHandler) create(c *gin.Context) {
|
||||
c.JSON(http.StatusCreated, cfg)
|
||||
}
|
||||
|
||||
// @Summary 租户配置列表
|
||||
// @Tags 租户配置
|
||||
// @Success 200 {object} map[string]any "items"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs [get]
|
||||
func (h *ociConfigHandler) list(c *gin.Context) {
|
||||
items, err := h.svc.List(c.Request.Context())
|
||||
if err != nil {
|
||||
@@ -69,6 +80,12 @@ func (h *ociConfigHandler) list(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// @Summary 租户配置详情
|
||||
// @Tags 租户配置
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id} [get]
|
||||
func (h *ociConfigHandler) get(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -82,6 +99,12 @@ func (h *ociConfigHandler) get(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, cfg)
|
||||
}
|
||||
|
||||
// @Summary 验证配置连通性并刷新画像
|
||||
// @Tags 租户配置
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/verify [post]
|
||||
func (h *ociConfigHandler) verify(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -109,6 +132,13 @@ type updateConfigRequest struct {
|
||||
ProxyID *uint `json:"proxyId"` // 非 null 切换代理:0 解除,>0 关联
|
||||
}
|
||||
|
||||
// @Summary 更新租户配置
|
||||
// @Tags 租户配置
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param body body object true "可更新字段(别名/分组/代理/多区域开关等)"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id} [put]
|
||||
func (h *ociConfigHandler) update(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -137,6 +167,12 @@ func (h *ociConfigHandler) update(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"config": cfg, "changes": changes})
|
||||
}
|
||||
|
||||
// @Summary 删除租户配置
|
||||
// @Tags 租户配置
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id} [delete]
|
||||
func (h *ociConfigHandler) remove(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -150,6 +186,13 @@ func (h *ociConfigHandler) remove(c *gin.Context) {
|
||||
}
|
||||
|
||||
// compartments 列出租户下全部 ACTIVE compartment(不含租户根)。
|
||||
//
|
||||
// @Summary 列出租户下全部 ACTIVE compartment
|
||||
// @Tags 租户配置
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/compartments [get]
|
||||
func (h *ociConfigHandler) compartments(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -165,6 +208,13 @@ func (h *ociConfigHandler) compartments(c *gin.Context) {
|
||||
|
||||
// cachedRegions 返回筛选器用区域列表:未开多区域支持只含默认区域;
|
||||
// 缓存存在非 READY 时实时刷新。
|
||||
//
|
||||
// @Summary 返回筛选器用区域列表:未开多区域支持只含默认区域
|
||||
// @Tags 租户配置
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/cached-regions [get]
|
||||
func (h *ociConfigHandler) cachedRegions(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -179,6 +229,13 @@ func (h *ociConfigHandler) cachedRegions(c *gin.Context) {
|
||||
}
|
||||
|
||||
// cachedCompartments 返回筛选器用区间列表:未开多区间支持返回空数组。
|
||||
//
|
||||
// @Summary 返回筛选器用区间列表:未开多区间支持返回空数组
|
||||
// @Tags 租户配置
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/cached-compartments [get]
|
||||
func (h *ociConfigHandler) cachedCompartments(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
|
||||
@@ -7,6 +7,12 @@ import (
|
||||
)
|
||||
|
||||
// overview 返回总览页聚合数据(本地快照,不发云端请求)。
|
||||
//
|
||||
// @Summary 返回总览页聚合数据
|
||||
// @Tags 租户配置
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/overview [get]
|
||||
func (h *ociConfigHandler) overview(c *gin.Context) {
|
||||
out, err := h.svc.Overview(c.Request.Context())
|
||||
if err != nil {
|
||||
|
||||
@@ -15,6 +15,12 @@ type proxyHandler struct {
|
||||
}
|
||||
|
||||
// list 返回全部代理(脱敏视图,含引用计数)。
|
||||
//
|
||||
// @Summary 代理列表
|
||||
// @Tags 设置
|
||||
// @Success 200 {object} map[string]any "items"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/proxies [get]
|
||||
func (h *proxyHandler) list(c *gin.Context) {
|
||||
items, err := h.svc.List(c.Request.Context())
|
||||
if err != nil {
|
||||
@@ -25,6 +31,13 @@ func (h *proxyHandler) list(c *gin.Context) {
|
||||
}
|
||||
|
||||
// create 新建代理。
|
||||
//
|
||||
// @Summary 创建代理
|
||||
// @Tags 设置
|
||||
// @Param body body service.ProxyInput true "代理配置(密码只写不回)"
|
||||
// @Success 201 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/proxies [post]
|
||||
func (h *proxyHandler) create(c *gin.Context) {
|
||||
var req service.ProxyInput
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -40,6 +53,14 @@ func (h *proxyHandler) create(c *gin.Context) {
|
||||
}
|
||||
|
||||
// update 更新代理;密码缺省沿用、空串清除。
|
||||
//
|
||||
// @Summary 更新代理
|
||||
// @Tags 设置
|
||||
// @Param id path int true "代理 ID"
|
||||
// @Param body body service.ProxyInput true "代理配置(密码缺省沿用)"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/proxies/{id} [put]
|
||||
func (h *proxyHandler) update(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -59,6 +80,13 @@ func (h *proxyHandler) update(c *gin.Context) {
|
||||
}
|
||||
|
||||
// remove 删除代理;仍被租户引用时返回 409。
|
||||
//
|
||||
// @Summary 删除代理
|
||||
// @Tags 设置
|
||||
// @Param id path int true "代理 ID"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/proxies/{id} [delete]
|
||||
func (h *proxyHandler) remove(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -72,6 +100,13 @@ func (h *proxyHandler) remove(c *gin.Context) {
|
||||
}
|
||||
|
||||
// importBatch 批量导入代理;逐行解析,返回成功与脱敏后的失败明细。
|
||||
//
|
||||
// @Summary 批量导入代理
|
||||
// @Tags 设置
|
||||
// @Param body body object true "请求体(见接口说明)"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/proxies/import [post]
|
||||
func (h *proxyHandler) importBatch(c *gin.Context) {
|
||||
var req struct {
|
||||
Text string `json:"text" binding:"required"`
|
||||
@@ -89,6 +124,13 @@ func (h *proxyHandler) importBatch(c *gin.Context) {
|
||||
}
|
||||
|
||||
// probe 手动重测代理出口地区,同步返回最新视图。
|
||||
//
|
||||
// @Summary 手动重测代理出口地区,同步返回最新视图
|
||||
// @Tags 设置
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/proxies/{id}/probe [post]
|
||||
func (h *proxyHandler) probe(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
|
||||
@@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -22,8 +23,9 @@ type ipRateLimiter struct {
|
||||
}
|
||||
|
||||
type ipEntry struct {
|
||||
limiter *rate.Limiter
|
||||
lastSeen time.Time
|
||||
limiter *rate.Limiter
|
||||
// lastSeen 是 UnixNano 时间戳;请求路径无锁写、回收循环无锁读
|
||||
lastSeen atomic.Int64
|
||||
}
|
||||
|
||||
func newIPRateLimiter() *ipRateLimiter {
|
||||
@@ -33,6 +35,7 @@ func newIPRateLimiter() *ipRateLimiter {
|
||||
}
|
||||
|
||||
// get 返回 ip 对应的令牌桶,参数与当前配置不一致时就地调整。
|
||||
// lastSeen 用原子时间戳:并发请求与回收循环两侧无锁读写,避免数据竞态。
|
||||
func (l *ipRateLimiter) get(ip string, rps, burst int) *rate.Limiter {
|
||||
l.mu.RLock()
|
||||
entry, ok := l.limiters[ip]
|
||||
@@ -45,7 +48,7 @@ func (l *ipRateLimiter) get(ip string, rps, burst int) *rate.Limiter {
|
||||
}
|
||||
l.mu.Unlock()
|
||||
}
|
||||
entry.lastSeen = time.Now()
|
||||
entry.lastSeen.Store(time.Now().UnixNano())
|
||||
if entry.limiter.Limit() != rate.Limit(rps) || entry.limiter.Burst() != burst {
|
||||
entry.limiter.SetLimit(rate.Limit(rps))
|
||||
entry.limiter.SetBurst(burst)
|
||||
@@ -58,9 +61,9 @@ func (l *ipRateLimiter) evictLoop() {
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
l.mu.Lock()
|
||||
cutoff := time.Now().Add(-ipEvictInterval)
|
||||
cutoff := time.Now().Add(-ipEvictInterval).UnixNano()
|
||||
for ip, e := range l.limiters {
|
||||
if e.lastSeen.Before(cutoff) {
|
||||
if e.lastSeen.Load() < cutoff {
|
||||
delete(l.limiters, ip)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,12 @@ import (
|
||||
)
|
||||
|
||||
// listRegions 返回本地维护的完整区域表(含友好名称),不发起云端请求。
|
||||
//
|
||||
// @Summary 返回本地维护的完整区域表
|
||||
// @Tags 租户配置
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/regions [get]
|
||||
func listRegions(c *gin.Context) {
|
||||
regions, err := oci.AllRegions()
|
||||
if err != nil {
|
||||
@@ -19,6 +25,12 @@ func listRegions(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, regions)
|
||||
}
|
||||
|
||||
// @Summary 区域订阅列表
|
||||
// @Tags 租户配置
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/region-subscriptions [get]
|
||||
func (h *ociConfigHandler) regionSubscriptions(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -36,6 +48,13 @@ type subscribeRegionRequest struct {
|
||||
RegionKey string `json:"regionKey" binding:"required"`
|
||||
}
|
||||
|
||||
// @Summary 订阅新区域
|
||||
// @Tags 租户配置
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param body body subscribeRegionRequest true "请求体"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/region-subscriptions [post]
|
||||
func (h *ociConfigHandler) subscribeRegion(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -54,6 +73,12 @@ func (h *ociConfigHandler) subscribeRegion(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, subs)
|
||||
}
|
||||
|
||||
// @Summary 服务限额查询
|
||||
// @Tags 租户配置
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/limits [get]
|
||||
func (h *ociConfigHandler) limits(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
|
||||
+35
-200
@@ -1,217 +1,52 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"oci-portal/internal/service"
|
||||
)
|
||||
|
||||
// NewRouter 组装全部 HTTP 路由:/auth/login 公开,业务路由要求 JWT。
|
||||
// NewRouter 组装 HTTP 路由骨架:中间件链、公开/鉴权分组与各域注册;
|
||||
// 具体路由按域拆在 routes_*.go。/auth/login 公开,业务路由要求 JWT。
|
||||
func NewRouter(auth *service.AuthService, oauth *service.OAuthService, ociConfigs *service.OciConfigService, tasks *service.TaskService, console *service.ConsoleService, settings *service.SettingService, notifier *service.Notifier, systemLogs *service.SystemLogService, logEvents *service.LogEventService, proxies *service.ProxyService, aiGateway *service.AiGatewayService) *gin.Engine {
|
||||
r := gin.New()
|
||||
// 禁用 gin 内置代理信任:真实 IP 统一由 RealIPMiddleware 按安全设置解析
|
||||
_ = r.SetTrustedProxies(nil)
|
||||
|
||||
// webhook 注册在全局 Logger 之前:访问日志不落含 secret 的路径;
|
||||
// 不挂系统日志中间件——投递高频且已在回传日志留痕,避免刷屏
|
||||
registerWebhook(r, settings, logEvents)
|
||||
|
||||
// 访问日志不落 query string:webconsole WS 的 JWT 经 query 传递,默认 Logger 会打进 stdout/journal
|
||||
accessLog := gin.LoggerWithConfig(gin.LoggerConfig{SkipQueryString: true})
|
||||
r.Use(accessLog, RealIPMiddleware(settings), IPRateMiddleware(settings), gin.Recovery())
|
||||
|
||||
v1 := r.Group("/api/v1")
|
||||
registerAuthPublic(v1, auth, oauth, systemLogs)
|
||||
// AI 网关对外端点:独立密钥鉴权,挂在全局 Use 之后,仍受 IP 限速与 Recovery 保护
|
||||
registerAiGateway(r, aiGateway)
|
||||
|
||||
// 系统日志中间件挂在鉴权之后,写请求自动留痕(webconsole ws 为 GET,天然跳过)
|
||||
secured := v1.Group("", RequireAuth(auth), systemLogMiddleware(systemLogs))
|
||||
registerAuthSecured(secured, auth, oauth, settings, systemLogs)
|
||||
registerConsole(v1, secured, console, auth)
|
||||
registerSettings(secured, settings, notifier, systemLogs, proxies)
|
||||
registerTasksAndLogs(secured, tasks, logEvents)
|
||||
registerOci(secured, ociConfigs)
|
||||
registerAiAdmin(secured, aiGateway)
|
||||
registerSwagger(r)
|
||||
|
||||
// 嵌入的前端产物兜底伺服;失败仅降级为纯 API 服务,不阻塞启动
|
||||
if err := registerWebUI(r); err != nil {
|
||||
log.Printf("[warn] webui disabled: %v", err)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// registerWebhook 挂载 ONS 回传入口。必须注册在全局 Logger 之前:
|
||||
// 访问日志不落含 secret 的路径;不挂系统日志中间件——投递高频且已在回传日志留痕。
|
||||
func registerWebhook(r *gin.Engine, settings *service.SettingService, logEvents *service.LogEventService) {
|
||||
wh := &webhookHandler{events: logEvents}
|
||||
r.POST("/api/v1/webhooks/oci-logs/:secret",
|
||||
gin.Recovery(), RealIPMiddleware(settings), wh.handle)
|
||||
|
||||
r.Use(gin.Logger(), RealIPMiddleware(settings), IPRateMiddleware(settings), gin.Recovery())
|
||||
|
||||
v1 := r.Group("/api/v1")
|
||||
ah := &authHandler{svc: auth, logs: systemLogs}
|
||||
v1.POST("/auth/login", ah.login)
|
||||
|
||||
// 外部身份登录(公开):provider 列表 / 授权跳转(bind 模式 handler 内校验 JWT)/ 回调
|
||||
ax := &authxHandler{auth: auth, oauth: oauth}
|
||||
v1.GET("/auth/oauth/providers", ax.oauthProviders)
|
||||
v1.GET("/auth/oauth/:provider/authorize", ax.oauthAuthorize)
|
||||
v1.GET("/auth/oauth/:provider/callback", ax.oauthCallback)
|
||||
|
||||
// 控制台数据面:浏览器 WebSocket 无法带自定义头,token 经 query 在 handler 内校验
|
||||
ch := &consoleHandler{svc: console, auth: auth}
|
||||
v1.GET("/console-sessions/:sessionId/ws", ch.ws)
|
||||
|
||||
// AI 网关对外端点:独立密钥鉴权(Bearer / x-api-key),不挂 JWT 与系统日志
|
||||
// (高频调用自有 AiCallLog);挂在全局 Use 之后,仍受 IP 限速与 Recovery 保护
|
||||
aih := &aiGatewayHandler{gw: aiGateway}
|
||||
ai := r.Group("/ai/v1", aih.auth)
|
||||
ai.POST("/chat/completions", aih.chatCompletions)
|
||||
ai.POST("/responses", aih.responses)
|
||||
ai.POST("/messages", aih.messages)
|
||||
ai.POST("/embeddings", aih.embeddings)
|
||||
ai.GET("/models", aih.listModels)
|
||||
|
||||
// 系统日志中间件挂在鉴权之后,写请求自动留痕(webconsole ws 为 GET,天然跳过)
|
||||
secured := v1.Group("", RequireAuth(auth), systemLogMiddleware(systemLogs))
|
||||
secured.POST("/auth/logout", ah.logout)
|
||||
secured.GET("/regions", listRegions)
|
||||
secured.GET("/system-logs", (&systemLogHandler{svc: systemLogs}).list)
|
||||
secured.POST("/oci-configs/:id/instances/:instanceId/console-sessions", ch.create)
|
||||
secured.DELETE("/console-sessions/:sessionId", ch.remove)
|
||||
|
||||
st := &settingsHandler{svc: settings, notifier: notifier}
|
||||
secured.GET("/about", about)
|
||||
secured.GET("/settings/telegram", st.getTelegram)
|
||||
secured.PUT("/settings/telegram", st.updateTelegram)
|
||||
secured.POST("/settings/telegram/test", st.testTelegram)
|
||||
secured.GET("/settings/notify-events", st.getNotifyEvents)
|
||||
secured.PUT("/settings/notify-events", st.updateNotifyEvents)
|
||||
secured.GET("/settings/notify-templates", st.listNotifyTemplates)
|
||||
secured.PUT("/settings/notify-templates/:kind", st.updateNotifyTemplate)
|
||||
secured.POST("/settings/notify-templates/:kind/test", st.testNotifyTemplate)
|
||||
secured.GET("/settings/task", st.getTaskSettings)
|
||||
secured.PUT("/settings/task", st.updateTaskSettings)
|
||||
secured.GET("/settings/security", st.getSecurity)
|
||||
secured.PUT("/settings/security", st.updateSecurity)
|
||||
|
||||
// 两步验证与外部身份管理(JWT 组内)
|
||||
secured.GET("/auth/totp", ax.totpStatus)
|
||||
secured.POST("/auth/totp/setup", ax.totpSetup)
|
||||
secured.POST("/auth/totp/activate", ax.totpActivate)
|
||||
secured.POST("/auth/totp/disable", ax.totpDisable)
|
||||
secured.GET("/auth/credentials", ax.getCredentials)
|
||||
secured.PUT("/auth/credentials", ax.updateCredentials)
|
||||
secured.PUT("/auth/password-login", ax.updatePasswordLogin)
|
||||
secured.GET("/auth/identities", ax.identities)
|
||||
secured.DELETE("/auth/identities/:id", ax.unbindIdentity)
|
||||
secured.GET("/settings/oauth", func(c *gin.Context) { ax.getOAuthSettings(c, settings) })
|
||||
secured.PUT("/settings/oauth", func(c *gin.Context) { ax.updateOAuthSettings(c, settings) })
|
||||
|
||||
px := &proxyHandler{svc: proxies}
|
||||
secured.GET("/proxies", px.list)
|
||||
secured.POST("/proxies", px.create)
|
||||
secured.POST("/proxies/import", px.importBatch)
|
||||
secured.PUT("/proxies/:id", px.update)
|
||||
secured.DELETE("/proxies/:id", px.remove)
|
||||
secured.POST("/proxies/:id/probe", px.probe)
|
||||
|
||||
le := &logEventHandler{svc: logEvents}
|
||||
secured.GET("/log-events", le.list)
|
||||
secured.GET("/oci-configs/:id/log-webhook", le.getWebhook)
|
||||
secured.POST("/oci-configs/:id/log-webhook", le.ensureWebhook)
|
||||
secured.DELETE("/oci-configs/:id/log-webhook", le.revokeWebhook)
|
||||
secured.GET("/oci-configs/:id/log-relay", le.getRelay)
|
||||
secured.POST("/oci-configs/:id/log-relay", le.setupRelay)
|
||||
secured.DELETE("/oci-configs/:id/log-relay", le.teardownRelay)
|
||||
|
||||
t := &taskHandler{svc: tasks}
|
||||
secured.POST("/tasks", t.create)
|
||||
secured.GET("/tasks", t.list)
|
||||
secured.GET("/tasks/:id", t.get)
|
||||
secured.PUT("/tasks/:id", t.update)
|
||||
secured.DELETE("/tasks/:id", t.remove)
|
||||
secured.GET("/tasks/:id/logs", t.logs)
|
||||
secured.POST("/tasks/:id/run", t.run)
|
||||
|
||||
h := &ociConfigHandler{svc: ociConfigs}
|
||||
secured.GET("/overview", h.overview)
|
||||
secured.POST("/oci-configs", h.create)
|
||||
secured.GET("/oci-configs", h.list)
|
||||
secured.GET("/oci-configs/:id", h.get)
|
||||
secured.PUT("/oci-configs/:id", h.update)
|
||||
secured.POST("/oci-configs/:id/verify", h.verify)
|
||||
secured.DELETE("/oci-configs/:id", h.remove)
|
||||
secured.GET("/oci-configs/:id/compartments", h.compartments)
|
||||
secured.GET("/oci-configs/:id/cached-regions", h.cachedRegions)
|
||||
secured.GET("/oci-configs/:id/cached-compartments", h.cachedCompartments)
|
||||
secured.GET("/oci-configs/:id/region-subscriptions", h.regionSubscriptions)
|
||||
secured.POST("/oci-configs/:id/region-subscriptions", h.subscribeRegion)
|
||||
secured.GET("/oci-configs/:id/limits", h.limits)
|
||||
secured.GET("/oci-configs/:id/limits/services", h.limitServices)
|
||||
secured.GET("/oci-configs/:id/subscriptions", h.subscriptions)
|
||||
secured.GET("/oci-configs/:id/subscriptions/:subscriptionId", h.subscriptionDetail)
|
||||
secured.GET("/oci-configs/:id/shapes", h.shapes)
|
||||
secured.GET("/oci-configs/:id/images", h.images)
|
||||
secured.GET("/oci-configs/:id/images/:imageId", h.getImage)
|
||||
secured.GET("/oci-configs/:id/vcns", h.listVCNs)
|
||||
secured.POST("/oci-configs/:id/vcns", h.createVCN)
|
||||
secured.GET("/oci-configs/:id/vcns/:vcnId", h.getVCN)
|
||||
secured.PUT("/oci-configs/:id/vcns/:vcnId", h.updateVCN)
|
||||
secured.DELETE("/oci-configs/:id/vcns/:vcnId", h.deleteVCN)
|
||||
secured.POST("/oci-configs/:id/vcns/:vcnId/enable-ipv6", h.enableVCNIPv6)
|
||||
secured.GET("/oci-configs/:id/subnets", h.listSubnets)
|
||||
secured.POST("/oci-configs/:id/subnets", h.createSubnet)
|
||||
secured.GET("/oci-configs/:id/subnets/:subnetId", h.getSubnet)
|
||||
secured.PUT("/oci-configs/:id/subnets/:subnetId", h.updateSubnet)
|
||||
secured.DELETE("/oci-configs/:id/subnets/:subnetId", h.deleteSubnet)
|
||||
secured.GET("/oci-configs/:id/security-lists", h.listSecurityLists)
|
||||
secured.POST("/oci-configs/:id/security-lists", h.createSecurityList)
|
||||
secured.GET("/oci-configs/:id/security-lists/:securityListId", h.getSecurityList)
|
||||
secured.PUT("/oci-configs/:id/security-lists/:securityListId", h.updateSecurityList)
|
||||
secured.DELETE("/oci-configs/:id/security-lists/:securityListId", h.deleteSecurityList)
|
||||
secured.GET("/oci-configs/:id/availability-domains", h.availabilityDomains)
|
||||
secured.GET("/oci-configs/:id/instances", h.listInstances)
|
||||
secured.POST("/oci-configs/:id/instances", h.createInstance)
|
||||
secured.GET("/oci-configs/:id/instances/:instanceId", h.getInstance)
|
||||
secured.PUT("/oci-configs/:id/instances/:instanceId", h.updateInstance)
|
||||
secured.DELETE("/oci-configs/:id/instances/:instanceId", h.terminateInstance)
|
||||
secured.POST("/oci-configs/:id/instances/:instanceId/action", h.instanceAction)
|
||||
secured.GET("/oci-configs/:id/boot-volumes", h.listBootVolumes)
|
||||
secured.GET("/oci-configs/:id/boot-volumes/:bootVolumeId", h.getBootVolume)
|
||||
secured.PUT("/oci-configs/:id/boot-volumes/:bootVolumeId", h.updateBootVolume)
|
||||
secured.DELETE("/oci-configs/:id/boot-volumes/:bootVolumeId", h.deleteBootVolume)
|
||||
secured.POST("/oci-configs/:id/instances/:instanceId/change-public-ip", h.changePublicIP)
|
||||
secured.POST("/oci-configs/:id/instances/:instanceId/ipv6-addresses", h.addInstanceIpv6)
|
||||
secured.DELETE("/oci-configs/:id/instances/:instanceId/ipv6-addresses", h.deleteInstanceIpv6)
|
||||
secured.POST("/oci-configs/:id/instances/:instanceId/console-connections", h.createConsoleConnection)
|
||||
secured.GET("/oci-configs/:id/instances/:instanceId/console-connections", h.listConsoleConnections)
|
||||
secured.DELETE("/oci-configs/:id/console-connections/:connectionId", h.deleteConsoleConnection)
|
||||
secured.GET("/oci-configs/:id/instances/:instanceId/vnics", h.listInstanceVnics)
|
||||
secured.POST("/oci-configs/:id/instances/:instanceId/vnics", h.attachVnic)
|
||||
secured.DELETE("/oci-configs/:id/vnic-attachments/:attachmentId", h.detachVnic)
|
||||
secured.POST("/oci-configs/:id/vnics/:vnicId/ipv6-addresses", h.addVnicIpv6)
|
||||
secured.GET("/oci-configs/:id/instances/:instanceId/boot-volume-attachments", h.listBootVolumeAttachments)
|
||||
secured.POST("/oci-configs/:id/instances/:instanceId/boot-volume-attachments", h.attachBootVolume)
|
||||
secured.POST("/oci-configs/:id/instances/:instanceId/replace-boot-volume", h.replaceBootVolume)
|
||||
secured.DELETE("/oci-configs/:id/boot-volume-attachments/:attachmentId", h.detachBootVolume)
|
||||
secured.GET("/oci-configs/:id/volumes", h.listBlockVolumes)
|
||||
secured.GET("/oci-configs/:id/instances/:instanceId/volume-attachments", h.listVolumeAttachments)
|
||||
secured.POST("/oci-configs/:id/instances/:instanceId/volume-attachments", h.attachVolume)
|
||||
secured.DELETE("/oci-configs/:id/volume-attachments/:attachmentId", h.detachVolume)
|
||||
secured.GET("/oci-configs/:id/instances/:instanceId/traffic", h.instanceTraffic)
|
||||
secured.GET("/oci-configs/:id/costs", h.costs)
|
||||
// AI 网关面板管理:密钥 / 渠道(号池)/ 聚合模型 / 调用日志
|
||||
aiadmin := &aiAdminHandler{gw: aiGateway}
|
||||
secured.GET("/ai-keys", aiadmin.listKeys)
|
||||
secured.POST("/ai-keys", aiadmin.createKey)
|
||||
secured.PUT("/ai-keys/:id", aiadmin.updateKey)
|
||||
secured.PUT("/ai-keys/:id/content-log", aiadmin.updateKeyContentLog)
|
||||
secured.DELETE("/ai-keys/:id", aiadmin.deleteKey)
|
||||
secured.GET("/ai-channels", aiadmin.listChannels)
|
||||
secured.POST("/ai-channels", aiadmin.createChannel)
|
||||
secured.PUT("/ai-channels/:id", aiadmin.updateChannel)
|
||||
secured.DELETE("/ai-channels/:id", aiadmin.deleteChannel)
|
||||
secured.POST("/ai-channels/:id/probe", aiadmin.probeChannel)
|
||||
secured.POST("/ai-channels/:id/sync-models", aiadmin.syncChannelModels)
|
||||
secured.GET("/ai-models", aiadmin.gatewayModels)
|
||||
secured.GET("/ai-logs", aiadmin.listLogs)
|
||||
secured.GET("/ai-content-logs", aiadmin.listContentLogs)
|
||||
secured.GET("/oci-configs/:id/audit-events", h.getAuditEvents)
|
||||
secured.GET("/oci-configs/:id/audit-events/detail", h.getAuditEventDetail)
|
||||
secured.GET("/oci-configs/:id/users", h.listTenantUsers)
|
||||
secured.POST("/oci-configs/:id/users", h.createTenantUser)
|
||||
secured.GET("/oci-configs/:id/users/:userId", h.getTenantUserDetail)
|
||||
secured.PUT("/oci-configs/:id/users/:userId", h.updateTenantUser)
|
||||
secured.DELETE("/oci-configs/:id/users/:userId", h.deleteTenantUser)
|
||||
secured.POST("/oci-configs/:id/users/:userId/reset-password", h.resetTenantUserPassword)
|
||||
secured.DELETE("/oci-configs/:id/users/:userId/mfa-devices", h.deleteTenantUserMfa)
|
||||
secured.DELETE("/oci-configs/:id/users/:userId/api-keys", h.deleteTenantUserApiKeys)
|
||||
secured.GET("/oci-configs/:id/notification-recipients", h.getNotificationRecipients)
|
||||
secured.PUT("/oci-configs/:id/notification-recipients", h.updateNotificationRecipients)
|
||||
secured.GET("/oci-configs/:id/password-policies", h.listPasswordPolicies)
|
||||
secured.PUT("/oci-configs/:id/password-policies/:policyId", h.updatePasswordPolicy)
|
||||
secured.GET("/oci-configs/:id/identity-settings", h.getIdentitySetting)
|
||||
secured.PUT("/oci-configs/:id/identity-settings", h.updateIdentitySetting)
|
||||
secured.GET("/oci-configs/:id/identity-providers", h.listIdentityProviders)
|
||||
secured.POST("/oci-configs/:id/identity-providers", h.createIdentityProvider)
|
||||
secured.POST("/oci-configs/:id/identity-providers/:idpId/activate", h.activateIdentityProvider)
|
||||
secured.DELETE("/oci-configs/:id/identity-providers/:idpId", h.deleteIdentityProvider)
|
||||
secured.GET("/oci-configs/:id/saml-metadata", h.downloadSamlMetadata)
|
||||
secured.GET("/oci-configs/:id/sign-on-rules", h.listSignOnRules)
|
||||
secured.POST("/oci-configs/:id/sign-on-exemptions", h.createMfaExemption)
|
||||
secured.DELETE("/oci-configs/:id/sign-on-exemptions/:ruleId", h.deleteMfaExemption)
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"oci-portal/internal/service"
|
||||
)
|
||||
|
||||
// registerAiGateway 挂载 /ai/v1/* 对外网关端点(独立密钥鉴权,不挂 JWT 与系统日志)。
|
||||
// 挂在全局中间件之后,仍受 IP 限速与 Recovery 保护;高频调用自有 AiCallLog。
|
||||
func registerAiGateway(r *gin.Engine, aiGateway *service.AiGatewayService) {
|
||||
aih := &aiGatewayHandler{gw: aiGateway}
|
||||
ai := r.Group("/ai/v1", aih.auth)
|
||||
ai.POST("/chat/completions", aih.chatCompletions)
|
||||
ai.POST("/responses", aih.responses)
|
||||
ai.POST("/messages", aih.messages)
|
||||
ai.POST("/embeddings", aih.embeddings)
|
||||
ai.GET("/models", aih.listModels)
|
||||
}
|
||||
|
||||
// registerAiAdmin 挂载面板管理侧的 AI 密钥/渠道/模型/日志(JWT 组内)。
|
||||
func registerAiAdmin(secured *gin.RouterGroup, aiGateway *service.AiGatewayService) {
|
||||
aiadmin := &aiAdminHandler{gw: aiGateway}
|
||||
secured.GET("/ai-keys", aiadmin.listKeys)
|
||||
secured.POST("/ai-keys", aiadmin.createKey)
|
||||
secured.PUT("/ai-keys/:id", aiadmin.updateKey)
|
||||
secured.PUT("/ai-keys/:id/content-log", aiadmin.updateKeyContentLog)
|
||||
secured.DELETE("/ai-keys/:id", aiadmin.deleteKey)
|
||||
secured.GET("/ai-channels", aiadmin.listChannels)
|
||||
secured.POST("/ai-channels", aiadmin.createChannel)
|
||||
secured.PUT("/ai-channels/:id", aiadmin.updateChannel)
|
||||
secured.DELETE("/ai-channels/:id", aiadmin.deleteChannel)
|
||||
secured.POST("/ai-channels/:id/probe", aiadmin.probeChannel)
|
||||
secured.POST("/ai-channels/:id/sync-models", aiadmin.syncChannelModels)
|
||||
secured.GET("/ai-models", aiadmin.gatewayModels)
|
||||
secured.GET("/ai-logs", aiadmin.listLogs)
|
||||
secured.GET("/ai-content-logs", aiadmin.listContentLogs)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"oci-portal/internal/service"
|
||||
)
|
||||
|
||||
// registerAuthPublic 公开登录/OAuth 路由(不经 JWT)。
|
||||
func registerAuthPublic(v1 *gin.RouterGroup, auth *service.AuthService, oauth *service.OAuthService, systemLogs *service.SystemLogService) {
|
||||
ah := &authHandler{svc: auth, logs: systemLogs}
|
||||
v1.POST("/auth/login", ah.login)
|
||||
|
||||
// 外部身份登录:provider 列表 / 授权跳转(bind 模式 handler 内校验 JWT)/ 回调
|
||||
ax := &authxHandler{auth: auth, oauth: oauth}
|
||||
v1.GET("/auth/oauth/providers", ax.oauthProviders)
|
||||
v1.GET("/auth/oauth/:provider/authorize", ax.oauthAuthorize)
|
||||
v1.GET("/auth/oauth/:provider/callback", ax.oauthCallback)
|
||||
}
|
||||
|
||||
// registerAuthSecured 登出、两步验证、外部身份管理(JWT 组内)。
|
||||
func registerAuthSecured(secured *gin.RouterGroup, auth *service.AuthService, oauth *service.OAuthService, settings *service.SettingService, systemLogs *service.SystemLogService) {
|
||||
ah := &authHandler{svc: auth, logs: systemLogs}
|
||||
secured.POST("/auth/logout", ah.logout)
|
||||
|
||||
ax := &authxHandler{auth: auth, oauth: oauth}
|
||||
secured.GET("/auth/totp", ax.totpStatus)
|
||||
secured.POST("/auth/totp/setup", ax.totpSetup)
|
||||
secured.POST("/auth/totp/activate", ax.totpActivate)
|
||||
secured.POST("/auth/totp/disable", ax.totpDisable)
|
||||
secured.GET("/auth/credentials", ax.getCredentials)
|
||||
secured.PUT("/auth/credentials", ax.updateCredentials)
|
||||
secured.PUT("/auth/password-login", ax.updatePasswordLogin)
|
||||
secured.GET("/auth/identities", ax.identities)
|
||||
secured.DELETE("/auth/identities/:id", ax.unbindIdentity)
|
||||
secured.GET("/settings/oauth", func(c *gin.Context) { ax.getOAuthSettings(c, settings) })
|
||||
secured.PUT("/settings/oauth", func(c *gin.Context) { ax.updateOAuthSettings(c, settings) })
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"oci-portal/internal/service"
|
||||
)
|
||||
|
||||
// registerConsole 网页控制台:ws 数据面挂 v1 组(浏览器 WebSocket 无法带自定义头,
|
||||
// token 经 query 在 handler 内校验),会话创建/删除走 JWT 组。
|
||||
func registerConsole(v1, secured *gin.RouterGroup, console *service.ConsoleService, auth *service.AuthService) {
|
||||
ch := &consoleHandler{svc: console, auth: auth}
|
||||
v1.GET("/console-sessions/:sessionId/ws", ch.ws)
|
||||
secured.POST("/oci-configs/:id/instances/:instanceId/console-sessions", ch.create)
|
||||
secured.DELETE("/console-sessions/:sessionId", ch.remove)
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"oci-portal/internal/service"
|
||||
)
|
||||
|
||||
// registerOci 挂载 OCI 租户配置及其资源的全部路由;共用 ociConfigHandler。
|
||||
// 按资源域顺序分节注册,便于维护:配置元数据 → 计算 → 网络 → 存储 → IAM/审计。
|
||||
func registerOci(secured *gin.RouterGroup, ociConfigs *service.OciConfigService) {
|
||||
h := &ociConfigHandler{svc: ociConfigs}
|
||||
secured.GET("/regions", listRegions)
|
||||
secured.GET("/overview", h.overview)
|
||||
registerOciConfig(secured, h)
|
||||
registerOciCompute(secured, h)
|
||||
registerOciNetwork(secured, h)
|
||||
registerOciStorage(secured, h)
|
||||
registerOciCost(secured, h)
|
||||
registerOciTenantIAM(secured, h)
|
||||
}
|
||||
|
||||
// registerOciConfig 配置本身 + 区域/区间/订阅/限额/形状/镜像。
|
||||
func registerOciConfig(g *gin.RouterGroup, h *ociConfigHandler) {
|
||||
g.POST("/oci-configs", h.create)
|
||||
g.GET("/oci-configs", h.list)
|
||||
g.GET("/oci-configs/:id", h.get)
|
||||
g.PUT("/oci-configs/:id", h.update)
|
||||
g.POST("/oci-configs/:id/verify", h.verify)
|
||||
g.DELETE("/oci-configs/:id", h.remove)
|
||||
g.GET("/oci-configs/:id/compartments", h.compartments)
|
||||
g.GET("/oci-configs/:id/cached-regions", h.cachedRegions)
|
||||
g.GET("/oci-configs/:id/cached-compartments", h.cachedCompartments)
|
||||
g.GET("/oci-configs/:id/region-subscriptions", h.regionSubscriptions)
|
||||
g.POST("/oci-configs/:id/region-subscriptions", h.subscribeRegion)
|
||||
g.GET("/oci-configs/:id/limits", h.limits)
|
||||
g.GET("/oci-configs/:id/limits/services", h.limitServices)
|
||||
g.GET("/oci-configs/:id/subscriptions", h.subscriptions)
|
||||
g.GET("/oci-configs/:id/subscriptions/:subscriptionId", h.subscriptionDetail)
|
||||
g.GET("/oci-configs/:id/shapes", h.shapes)
|
||||
g.GET("/oci-configs/:id/images", h.images)
|
||||
g.GET("/oci-configs/:id/images/:imageId", h.getImage)
|
||||
g.GET("/oci-configs/:id/availability-domains", h.availabilityDomains)
|
||||
}
|
||||
|
||||
// registerOciCompute 实例、控制台连接、VNIC、流量。
|
||||
func registerOciCompute(g *gin.RouterGroup, h *ociConfigHandler) {
|
||||
g.GET("/oci-configs/:id/instances", h.listInstances)
|
||||
g.POST("/oci-configs/:id/instances", h.createInstance)
|
||||
g.GET("/oci-configs/:id/instances/:instanceId", h.getInstance)
|
||||
g.PUT("/oci-configs/:id/instances/:instanceId", h.updateInstance)
|
||||
g.DELETE("/oci-configs/:id/instances/:instanceId", h.terminateInstance)
|
||||
g.POST("/oci-configs/:id/instances/:instanceId/action", h.instanceAction)
|
||||
g.POST("/oci-configs/:id/instances/:instanceId/change-public-ip", h.changePublicIP)
|
||||
g.POST("/oci-configs/:id/instances/:instanceId/ipv6-addresses", h.addInstanceIpv6)
|
||||
g.DELETE("/oci-configs/:id/instances/:instanceId/ipv6-addresses", h.deleteInstanceIpv6)
|
||||
g.POST("/oci-configs/:id/instances/:instanceId/console-connections", h.createConsoleConnection)
|
||||
g.GET("/oci-configs/:id/instances/:instanceId/console-connections", h.listConsoleConnections)
|
||||
g.DELETE("/oci-configs/:id/console-connections/:connectionId", h.deleteConsoleConnection)
|
||||
g.GET("/oci-configs/:id/instances/:instanceId/vnics", h.listInstanceVnics)
|
||||
g.POST("/oci-configs/:id/instances/:instanceId/vnics", h.attachVnic)
|
||||
g.DELETE("/oci-configs/:id/vnic-attachments/:attachmentId", h.detachVnic)
|
||||
g.POST("/oci-configs/:id/vnics/:vnicId/ipv6-addresses", h.addVnicIpv6)
|
||||
g.GET("/oci-configs/:id/instances/:instanceId/traffic", h.instanceTraffic)
|
||||
}
|
||||
|
||||
// registerOciNetwork VCN、子网、安全列表。
|
||||
func registerOciNetwork(g *gin.RouterGroup, h *ociConfigHandler) {
|
||||
g.GET("/oci-configs/:id/vcns", h.listVCNs)
|
||||
g.POST("/oci-configs/:id/vcns", h.createVCN)
|
||||
g.GET("/oci-configs/:id/vcns/:vcnId", h.getVCN)
|
||||
g.PUT("/oci-configs/:id/vcns/:vcnId", h.updateVCN)
|
||||
g.DELETE("/oci-configs/:id/vcns/:vcnId", h.deleteVCN)
|
||||
g.POST("/oci-configs/:id/vcns/:vcnId/enable-ipv6", h.enableVCNIPv6)
|
||||
g.GET("/oci-configs/:id/subnets", h.listSubnets)
|
||||
g.POST("/oci-configs/:id/subnets", h.createSubnet)
|
||||
g.GET("/oci-configs/:id/subnets/:subnetId", h.getSubnet)
|
||||
g.PUT("/oci-configs/:id/subnets/:subnetId", h.updateSubnet)
|
||||
g.DELETE("/oci-configs/:id/subnets/:subnetId", h.deleteSubnet)
|
||||
g.GET("/oci-configs/:id/security-lists", h.listSecurityLists)
|
||||
g.POST("/oci-configs/:id/security-lists", h.createSecurityList)
|
||||
g.GET("/oci-configs/:id/security-lists/:securityListId", h.getSecurityList)
|
||||
g.PUT("/oci-configs/:id/security-lists/:securityListId", h.updateSecurityList)
|
||||
g.DELETE("/oci-configs/:id/security-lists/:securityListId", h.deleteSecurityList)
|
||||
}
|
||||
|
||||
// registerOciStorage 引导卷、块存储、附加/替换。
|
||||
func registerOciStorage(g *gin.RouterGroup, h *ociConfigHandler) {
|
||||
g.GET("/oci-configs/:id/boot-volumes", h.listBootVolumes)
|
||||
g.GET("/oci-configs/:id/boot-volumes/:bootVolumeId", h.getBootVolume)
|
||||
g.PUT("/oci-configs/:id/boot-volumes/:bootVolumeId", h.updateBootVolume)
|
||||
g.DELETE("/oci-configs/:id/boot-volumes/:bootVolumeId", h.deleteBootVolume)
|
||||
g.GET("/oci-configs/:id/instances/:instanceId/boot-volume-attachments", h.listBootVolumeAttachments)
|
||||
g.POST("/oci-configs/:id/instances/:instanceId/boot-volume-attachments", h.attachBootVolume)
|
||||
g.POST("/oci-configs/:id/instances/:instanceId/replace-boot-volume", h.replaceBootVolume)
|
||||
g.DELETE("/oci-configs/:id/boot-volume-attachments/:attachmentId", h.detachBootVolume)
|
||||
g.GET("/oci-configs/:id/volumes", h.listBlockVolumes)
|
||||
g.GET("/oci-configs/:id/instances/:instanceId/volume-attachments", h.listVolumeAttachments)
|
||||
g.POST("/oci-configs/:id/instances/:instanceId/volume-attachments", h.attachVolume)
|
||||
g.DELETE("/oci-configs/:id/volume-attachments/:attachmentId", h.detachVolume)
|
||||
}
|
||||
|
||||
// registerOciCost 成本快照。
|
||||
func registerOciCost(g *gin.RouterGroup, h *ociConfigHandler) {
|
||||
g.GET("/oci-configs/:id/costs", h.costs)
|
||||
}
|
||||
|
||||
// registerOciTenantIAM 租户 IAM 治理:审计、用户、密码策略、身份提供商、单点登录规则。
|
||||
func registerOciTenantIAM(g *gin.RouterGroup, h *ociConfigHandler) {
|
||||
g.GET("/oci-configs/:id/audit-events", h.getAuditEvents)
|
||||
g.GET("/oci-configs/:id/audit-events/detail", h.getAuditEventDetail)
|
||||
g.GET("/oci-configs/:id/users", h.listTenantUsers)
|
||||
g.POST("/oci-configs/:id/users", h.createTenantUser)
|
||||
g.GET("/oci-configs/:id/users/:userId", h.getTenantUserDetail)
|
||||
g.PUT("/oci-configs/:id/users/:userId", h.updateTenantUser)
|
||||
g.DELETE("/oci-configs/:id/users/:userId", h.deleteTenantUser)
|
||||
g.POST("/oci-configs/:id/users/:userId/reset-password", h.resetTenantUserPassword)
|
||||
g.DELETE("/oci-configs/:id/users/:userId/mfa-devices", h.deleteTenantUserMfa)
|
||||
g.DELETE("/oci-configs/:id/users/:userId/api-keys", h.deleteTenantUserApiKeys)
|
||||
g.GET("/oci-configs/:id/notification-recipients", h.getNotificationRecipients)
|
||||
g.PUT("/oci-configs/:id/notification-recipients", h.updateNotificationRecipients)
|
||||
g.GET("/oci-configs/:id/password-policies", h.listPasswordPolicies)
|
||||
g.PUT("/oci-configs/:id/password-policies/:policyId", h.updatePasswordPolicy)
|
||||
g.GET("/oci-configs/:id/identity-settings", h.getIdentitySetting)
|
||||
g.PUT("/oci-configs/:id/identity-settings", h.updateIdentitySetting)
|
||||
g.GET("/oci-configs/:id/identity-providers", h.listIdentityProviders)
|
||||
g.POST("/oci-configs/:id/identity-providers", h.createIdentityProvider)
|
||||
g.POST("/oci-configs/:id/identity-providers/:idpId/activate", h.activateIdentityProvider)
|
||||
g.DELETE("/oci-configs/:id/identity-providers/:idpId", h.deleteIdentityProvider)
|
||||
g.GET("/oci-configs/:id/saml-metadata", h.downloadSamlMetadata)
|
||||
g.GET("/oci-configs/:id/sign-on-rules", h.listSignOnRules)
|
||||
g.POST("/oci-configs/:id/sign-on-exemptions", h.createMfaExemption)
|
||||
g.DELETE("/oci-configs/:id/sign-on-exemptions/:ruleId", h.deleteMfaExemption)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"oci-portal/internal/service"
|
||||
)
|
||||
|
||||
// registerSettings 挂载 /settings/* 与 /about,以及系统日志与代理管理。
|
||||
func registerSettings(secured *gin.RouterGroup, settings *service.SettingService, notifier *service.Notifier, systemLogs *service.SystemLogService, proxies *service.ProxyService) {
|
||||
secured.GET("/about", about)
|
||||
secured.GET("/system-logs", (&systemLogHandler{svc: systemLogs}).list)
|
||||
|
||||
st := &settingsHandler{svc: settings, notifier: notifier}
|
||||
secured.GET("/settings/telegram", st.getTelegram)
|
||||
secured.PUT("/settings/telegram", st.updateTelegram)
|
||||
secured.POST("/settings/telegram/test", st.testTelegram)
|
||||
secured.GET("/settings/notify-events", st.getNotifyEvents)
|
||||
secured.PUT("/settings/notify-events", st.updateNotifyEvents)
|
||||
secured.GET("/settings/notify-templates", st.listNotifyTemplates)
|
||||
secured.PUT("/settings/notify-templates/:kind", st.updateNotifyTemplate)
|
||||
secured.POST("/settings/notify-templates/:kind/test", st.testNotifyTemplate)
|
||||
secured.GET("/settings/task", st.getTaskSettings)
|
||||
secured.PUT("/settings/task", st.updateTaskSettings)
|
||||
secured.GET("/settings/security", st.getSecurity)
|
||||
secured.PUT("/settings/security", st.updateSecurity)
|
||||
|
||||
px := &proxyHandler{svc: proxies}
|
||||
secured.GET("/proxies", px.list)
|
||||
secured.POST("/proxies", px.create)
|
||||
secured.POST("/proxies/import", px.importBatch)
|
||||
secured.PUT("/proxies/:id", px.update)
|
||||
secured.DELETE("/proxies/:id", px.remove)
|
||||
secured.POST("/proxies/:id/probe", px.probe)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
)
|
||||
|
||||
// registerSwagger 挂载 swagger UI 与 spec(/swagger/index.html)。
|
||||
// 默认关闭,环境变量 SWAGGER=1 时开启——面板管理接口结构不宜默认对外,
|
||||
// 生产按需临时打开;spec 由 `go tool swag init` 生成进 docs/ 包。
|
||||
func registerSwagger(r *gin.Engine) {
|
||||
if os.Getenv("SWAGGER") != "1" {
|
||||
return
|
||||
}
|
||||
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"oci-portal/internal/service"
|
||||
)
|
||||
|
||||
// registerTasksAndLogs 挂载后台任务与 OCI 日志回传相关路由。
|
||||
func registerTasksAndLogs(secured *gin.RouterGroup, tasks *service.TaskService, logEvents *service.LogEventService) {
|
||||
t := &taskHandler{svc: tasks}
|
||||
secured.POST("/tasks", t.create)
|
||||
secured.GET("/tasks", t.list)
|
||||
secured.GET("/tasks/:id", t.get)
|
||||
secured.PUT("/tasks/:id", t.update)
|
||||
secured.DELETE("/tasks/:id", t.remove)
|
||||
secured.GET("/tasks/:id/logs", t.logs)
|
||||
secured.POST("/tasks/:id/run", t.run)
|
||||
|
||||
le := &logEventHandler{svc: logEvents}
|
||||
secured.GET("/log-events", le.list)
|
||||
secured.GET("/oci-configs/:id/log-webhook", le.getWebhook)
|
||||
secured.POST("/oci-configs/:id/log-webhook", le.ensureWebhook)
|
||||
secured.DELETE("/oci-configs/:id/log-webhook", le.revokeWebhook)
|
||||
secured.GET("/oci-configs/:id/log-relay", le.getRelay)
|
||||
secured.POST("/oci-configs/:id/log-relay", le.setupRelay)
|
||||
secured.DELETE("/oci-configs/:id/log-relay", le.teardownRelay)
|
||||
}
|
||||
@@ -16,6 +16,12 @@ type settingsHandler struct {
|
||||
}
|
||||
|
||||
// getTelegram 返回脱敏后的 Telegram 配置,绝不回 token 明文。
|
||||
//
|
||||
// @Summary 返回脱敏后的 Telegram 配置,绝不回 token 明文
|
||||
// @Tags 设置
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/settings/telegram [get]
|
||||
func (h *settingsHandler) getTelegram(c *gin.Context) {
|
||||
view, err := h.svc.TelegramView(c.Request.Context())
|
||||
if err != nil {
|
||||
@@ -34,6 +40,13 @@ type updateTelegramRequest struct {
|
||||
}
|
||||
|
||||
// updateTelegram 保存配置并返回最新脱敏视图。
|
||||
//
|
||||
// @Summary 保存配置并返回最新脱敏视图
|
||||
// @Tags 设置
|
||||
// @Param body body updateTelegramRequest true "请求体"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/settings/telegram [put]
|
||||
func (h *settingsHandler) updateTelegram(c *gin.Context) {
|
||||
var req updateTelegramRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -53,6 +66,12 @@ func (h *settingsHandler) updateTelegram(c *gin.Context) {
|
||||
}
|
||||
|
||||
// testTelegram 用当前已保存配置同步发送一条测试消息。
|
||||
//
|
||||
// @Summary 用当前已保存配置同步发送一条测试消息
|
||||
// @Tags 设置
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/settings/telegram/test [post]
|
||||
func (h *settingsHandler) testTelegram(c *gin.Context) {
|
||||
if err := h.notifier.Test(c.Request.Context()); err != nil {
|
||||
respondError(c, err)
|
||||
@@ -62,6 +81,12 @@ func (h *settingsHandler) testTelegram(c *gin.Context) {
|
||||
}
|
||||
|
||||
// getNotifyEvents 返回全部通知事件开关;键缺省(存量部署)视为开启。
|
||||
//
|
||||
// @Summary 返回全部通知事件开关
|
||||
// @Tags 设置
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/settings/notify-events [get]
|
||||
func (h *settingsHandler) getNotifyEvents(c *gin.Context) {
|
||||
view, err := h.svc.NotifyEvents(c.Request.Context())
|
||||
if err != nil {
|
||||
@@ -73,6 +98,13 @@ func (h *settingsHandler) getNotifyEvents(c *gin.Context) {
|
||||
|
||||
// updateNotifyEvents 全量保存五个事件开关并返回最新值;
|
||||
// 请求体为全量语义,缺字段按 JSON 零值 false 处理。
|
||||
//
|
||||
// @Summary 全量保存五个事件开关并返回最新值
|
||||
// @Tags 设置
|
||||
// @Param body body service.NotifyEventsView true "请求体"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/settings/notify-events [put]
|
||||
func (h *settingsHandler) updateNotifyEvents(c *gin.Context) {
|
||||
var req service.NotifyEventsView
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -87,6 +119,12 @@ func (h *settingsHandler) updateNotifyEvents(c *gin.Context) {
|
||||
}
|
||||
|
||||
// listNotifyTemplates 返回全部通知模板(默认模板 + 自定义现值 + 可用变量)。
|
||||
//
|
||||
// @Summary 返回全部通知模板
|
||||
// @Tags 设置
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/settings/notify-templates [get]
|
||||
func (h *settingsHandler) listNotifyTemplates(c *gin.Context) {
|
||||
items, err := h.svc.NotifyTemplates(c.Request.Context())
|
||||
if err != nil {
|
||||
@@ -97,6 +135,14 @@ func (h *settingsHandler) listNotifyTemplates(c *gin.Context) {
|
||||
}
|
||||
|
||||
// updateNotifyTemplate 保存单个通知模板;空模板清除自定义(恢复默认)。
|
||||
//
|
||||
// @Summary 保存单个通知模板
|
||||
// @Tags 设置
|
||||
// @Param kind path string true "kind"
|
||||
// @Param body body object true "请求体(见接口说明)"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/settings/notify-templates/{kind} [put]
|
||||
func (h *settingsHandler) updateNotifyTemplate(c *gin.Context) {
|
||||
var req struct {
|
||||
Template string `json:"template"`
|
||||
@@ -114,6 +160,14 @@ func (h *settingsHandler) updateNotifyTemplate(c *gin.Context) {
|
||||
|
||||
// testNotifyTemplate 用示例变量渲染模板并同步发送测试消息;
|
||||
// template 非空时按编辑中内容渲染(不落库)。
|
||||
//
|
||||
// @Summary 用示例变量渲染模板并同步发送测试消息
|
||||
// @Tags 设置
|
||||
// @Param kind path string true "kind"
|
||||
// @Param body body object true "请求体(见接口说明)"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/settings/notify-templates/{kind}/test [post]
|
||||
func (h *settingsHandler) testNotifyTemplate(c *gin.Context) {
|
||||
var req struct {
|
||||
Template string `json:"template"`
|
||||
@@ -130,6 +184,12 @@ func (h *settingsHandler) testNotifyTemplate(c *gin.Context) {
|
||||
}
|
||||
|
||||
// getTaskSettings 返回任务行为设置(抢机熔断阈值),阈值未保存过时为默认值。
|
||||
//
|
||||
// @Summary 返回任务行为设置
|
||||
// @Tags 设置
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/settings/task [get]
|
||||
func (h *settingsHandler) getTaskSettings(c *gin.Context) {
|
||||
view, err := h.svc.TaskSettings(c.Request.Context())
|
||||
if err != nil {
|
||||
@@ -140,6 +200,13 @@ func (h *settingsHandler) getTaskSettings(c *gin.Context) {
|
||||
}
|
||||
|
||||
// updateTaskSettings 保存任务行为设置并返回最新值;阈值越界返回 400。
|
||||
//
|
||||
// @Summary 保存任务行为设置并返回最新值
|
||||
// @Tags 设置
|
||||
// @Param body body service.TaskSettingsView true "请求体"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/settings/task [put]
|
||||
func (h *settingsHandler) updateTaskSettings(c *gin.Context) {
|
||||
var req service.TaskSettingsView
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -158,6 +225,12 @@ func (h *settingsHandler) updateTaskSettings(c *gin.Context) {
|
||||
}
|
||||
|
||||
// getSecurity 返回安全设置(WAF 参数 / 真实IP请求头 / 面板地址),未保存过时为默认值。
|
||||
//
|
||||
// @Summary 返回安全设置
|
||||
// @Tags 设置
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/settings/security [get]
|
||||
func (h *settingsHandler) getSecurity(c *gin.Context) {
|
||||
view, err := h.svc.Security(c.Request.Context())
|
||||
if err != nil {
|
||||
@@ -168,6 +241,13 @@ func (h *settingsHandler) getSecurity(c *gin.Context) {
|
||||
}
|
||||
|
||||
// updateSecurity 保存安全设置并返回最新值;越界或非法返回 400,保存后立即生效。
|
||||
//
|
||||
// @Summary 保存安全设置并返回最新值
|
||||
// @Tags 设置
|
||||
// @Param body body service.SecuritySettings true "请求体"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/settings/security [put]
|
||||
func (h *settingsHandler) updateSecurity(c *gin.Context) {
|
||||
var req service.SecuritySettings
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"oci-portal/internal/webui"
|
||||
)
|
||||
|
||||
// registerWebUI 挂载嵌入的前端产物。dev 模式走 vite devServer,不经此路径。
|
||||
func registerWebUI(r *gin.Engine) error {
|
||||
sub, err := webui.Dist()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mountSPA(r, sub)
|
||||
return nil
|
||||
}
|
||||
|
||||
// mountSPA 注册 NoRoute 兜底:命中真实文件直出,API 前缀保持 JSON 404,
|
||||
// 其余路径回 index.html 交给 SPA 路由。
|
||||
func mountSPA(r *gin.Engine, sub fs.FS) {
|
||||
fileServer := http.FileServer(http.FS(sub))
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
p := c.Request.URL.Path
|
||||
if strings.HasPrefix(p, "/api/") || strings.HasPrefix(p, "/ai/") {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
if name := strings.TrimPrefix(path.Clean(p), "/"); name != "" {
|
||||
if _, err := fs.Stat(sub, name); err == nil {
|
||||
setStaticCacheHeader(c, p)
|
||||
fileServer.ServeHTTP(c.Writer, c.Request)
|
||||
return
|
||||
}
|
||||
}
|
||||
// SPA 路由统一回 index.html,禁缓存保证发版后入口即时更新
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Request.URL.Path = "/"
|
||||
fileServer.ServeHTTP(c.Writer, c.Request)
|
||||
})
|
||||
}
|
||||
|
||||
// setStaticCacheHeader 按路径设缓存策略:vite 产物带内容 hash,/assets/* 可永久缓存;
|
||||
// 其余真实文件(favicon 等)不带 hash,禁强缓存。
|
||||
func setStaticCacheHeader(c *gin.Context, p string) {
|
||||
if strings.HasPrefix(p, "/assets/") {
|
||||
c.Header("Cache-Control", "public, max-age=31536000, immutable")
|
||||
return
|
||||
}
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestMountSPA(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
dist := fstest.MapFS{
|
||||
"index.html": {Data: []byte("<html>spa-entry</html>")},
|
||||
"favicon.svg": {Data: []byte("<svg/>")},
|
||||
"assets/app.js": {Data: []byte("console.log(1)")},
|
||||
}
|
||||
r := gin.New()
|
||||
mountSPA(r, dist)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
wantStatus int
|
||||
wantBody string // 包含匹配;空跳过
|
||||
wantCache string // Cache-Control 精确匹配;空跳过
|
||||
wantJSON bool
|
||||
}{
|
||||
{name: "api 未知路径保持 JSON 404", path: "/api/v1/nope", wantStatus: 404, wantBody: "not found", wantJSON: true},
|
||||
{name: "ai 未知路径保持 JSON 404", path: "/ai/v1/nope", wantStatus: 404, wantBody: "not found", wantJSON: true},
|
||||
{name: "根路径出 index 且禁缓存", path: "/", wantStatus: 200, wantBody: "spa-entry", wantCache: "no-cache"},
|
||||
{name: "SPA 路由 fallback 到 index", path: "/ai-gateway/keys", wantStatus: 200, wantBody: "spa-entry", wantCache: "no-cache"},
|
||||
{name: "assets 命中带 immutable 长缓存", path: "/assets/app.js", wantStatus: 200, wantBody: "console.log", wantCache: "public, max-age=31536000, immutable"},
|
||||
{name: "非 assets 真实文件禁强缓存", path: "/favicon.svg", wantStatus: 200, wantCache: "no-cache"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, tt.path, nil)
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != tt.wantStatus {
|
||||
t.Fatalf("status = %d, want %d", w.Code, tt.wantStatus)
|
||||
}
|
||||
if tt.wantBody != "" && !strings.Contains(w.Body.String(), tt.wantBody) {
|
||||
t.Fatalf("body %q does not contain %q", w.Body.String(), tt.wantBody)
|
||||
}
|
||||
if tt.wantCache != "" && w.Header().Get("Cache-Control") != tt.wantCache {
|
||||
t.Fatalf("cache-control = %q, want %q", w.Header().Get("Cache-Control"), tt.wantCache)
|
||||
}
|
||||
if tt.wantJSON && !strings.HasPrefix(w.Header().Get("Content-Type"), "application/json") {
|
||||
t.Fatalf("content-type = %q, want json", w.Header().Get("Content-Type"))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRegisterWebUIPlaceholder 验证嵌入占位产物可挂载(裸 clone 后 go build/test 即过)。
|
||||
func TestRegisterWebUIPlaceholder(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
if err := registerWebUI(r); err != nil {
|
||||
t.Fatalf("registerWebUI: %v", err)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/", nil))
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", w.Code)
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,12 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Summary 订阅列表
|
||||
// @Tags 租户配置
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/subscriptions [get]
|
||||
func (h *ociConfigHandler) subscriptions(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -19,6 +25,13 @@ func (h *ociConfigHandler) subscriptions(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, subs)
|
||||
}
|
||||
|
||||
// @Summary 订阅详情
|
||||
// @Tags 租户配置
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param subscriptionId path string true "subscriptionId"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/subscriptions/{subscriptionId} [get]
|
||||
func (h *ociConfigHandler) subscriptionDetail(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -32,6 +45,12 @@ func (h *ociConfigHandler) subscriptionDetail(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, detail)
|
||||
}
|
||||
|
||||
// @Summary 限额服务列表
|
||||
// @Tags 租户配置
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/limits/services [get]
|
||||
func (h *ociConfigHandler) limitServices(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
|
||||
@@ -15,6 +15,12 @@ type systemLogHandler struct {
|
||||
}
|
||||
|
||||
// list 分页倒序返回系统日志,支持路径 / 用户名关键字模糊过滤。
|
||||
//
|
||||
// @Summary 系统操作日志列表
|
||||
// @Tags 设置
|
||||
// @Success 200 {object} map[string]any "items 与 total"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/system-logs [get]
|
||||
func (h *systemLogHandler) list(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20"))
|
||||
|
||||
@@ -29,6 +29,12 @@ type updateTaskRequest struct {
|
||||
Status *string `json:"status"`
|
||||
}
|
||||
|
||||
// @Summary 创建任务
|
||||
// @Tags 任务与日志回传
|
||||
// @Param body body createTaskRequest true "任务定义(类型/cron/payload)"
|
||||
// @Success 201 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/tasks [post]
|
||||
func (h *taskHandler) create(c *gin.Context) {
|
||||
var req createTaskRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -48,6 +54,11 @@ func (h *taskHandler) create(c *gin.Context) {
|
||||
c.JSON(http.StatusCreated, task)
|
||||
}
|
||||
|
||||
// @Summary 任务列表
|
||||
// @Tags 任务与日志回传
|
||||
// @Success 200 {array} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/tasks [get]
|
||||
func (h *taskHandler) list(c *gin.Context) {
|
||||
tasks, err := h.svc.ListTasks(c.Request.Context())
|
||||
if err != nil {
|
||||
@@ -57,6 +68,12 @@ func (h *taskHandler) list(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, tasks)
|
||||
}
|
||||
|
||||
// @Summary 任务详情
|
||||
// @Tags 任务与日志回传
|
||||
// @Param id path int true "任务 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/tasks/{id} [get]
|
||||
func (h *taskHandler) get(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -70,6 +87,13 @@ func (h *taskHandler) get(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, task)
|
||||
}
|
||||
|
||||
// @Summary 更新任务
|
||||
// @Tags 任务与日志回传
|
||||
// @Param id path int true "任务 ID"
|
||||
// @Param body body updateTaskRequest true "可更新字段"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/tasks/{id} [put]
|
||||
func (h *taskHandler) update(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -93,6 +117,12 @@ func (h *taskHandler) update(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, task)
|
||||
}
|
||||
|
||||
// @Summary 删除任务
|
||||
// @Tags 任务与日志回传
|
||||
// @Param id path int true "任务 ID"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/tasks/{id} [delete]
|
||||
func (h *taskHandler) remove(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -105,6 +135,12 @@ func (h *taskHandler) remove(c *gin.Context) {
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// @Summary 任务执行日志
|
||||
// @Tags 任务与日志回传
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/tasks/{id}/logs [get]
|
||||
func (h *taskHandler) logs(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -119,6 +155,12 @@ func (h *taskHandler) logs(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, logs)
|
||||
}
|
||||
|
||||
// @Summary 立即执行任务
|
||||
// @Tags 任务与日志回传
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/tasks/{id}/run [post]
|
||||
func (h *taskHandler) run(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
|
||||
@@ -14,6 +14,13 @@ import (
|
||||
|
||||
// ---- 流量与成本 ----
|
||||
|
||||
// @Summary ---- 流量与成本 ----
|
||||
// @Tags 计算
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param instanceId path string true "instanceId"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/traffic [get]
|
||||
func (h *ociConfigHandler) instanceTraffic(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -28,6 +35,12 @@ func (h *ociConfigHandler) instanceTraffic(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, traffic)
|
||||
}
|
||||
|
||||
// @Summary 配置成本快照
|
||||
// @Tags 成本
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/costs [get]
|
||||
func (h *ociConfigHandler) costs(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -56,6 +69,13 @@ func (h *ociConfigHandler) costs(c *gin.Context) {
|
||||
|
||||
// getAuditEvents 实时查询租户 OCI 审计事件:hours 首查(缺省 24);
|
||||
// start/end/page 为截断后的同窗续查;窗口越界或非法返回 400。
|
||||
//
|
||||
// @Summary 实时查询租户 OCI 审计事件:hours 首查
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/audit-events [get]
|
||||
func (h *ociConfigHandler) getAuditEvents(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -80,6 +100,13 @@ func (h *ociConfigHandler) getAuditEvents(c *gin.Context) {
|
||||
|
||||
// getAuditEventDetail 按 eventId 取回原始事件 JSON:缓存命中秒开,
|
||||
// 过期走事件时间小窗重查;仍不可得返回 404,前端降级展示精简字段。
|
||||
//
|
||||
// @Summary 按 eventId 取回原始事件 JSON:缓存命中秒开,
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/audit-events/detail [get]
|
||||
func (h *ociConfigHandler) getAuditEventDetail(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -116,6 +143,12 @@ type createTenantUserRequest struct {
|
||||
AddToAdminGroup bool `json:"addToAdminGroup"`
|
||||
}
|
||||
|
||||
// @Summary 租户 IAM 用户列表
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/users [get]
|
||||
func (h *ociConfigHandler) listTenantUsers(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -129,6 +162,13 @@ func (h *ociConfigHandler) listTenantUsers(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, users)
|
||||
}
|
||||
|
||||
// @Summary 租户用户详情
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param userId path string true "userId"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/users/{userId} [get]
|
||||
func (h *ociConfigHandler) getTenantUserDetail(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -142,6 +182,13 @@ func (h *ociConfigHandler) getTenantUserDetail(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, detail)
|
||||
}
|
||||
|
||||
// @Summary 创建租户 IAM 用户
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param body body createTenantUserRequest true "请求体"
|
||||
// @Success 201 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/users [post]
|
||||
func (h *ociConfigHandler) createTenantUser(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -179,6 +226,14 @@ type updateTenantUserRequest struct {
|
||||
AddToAdminGroup *bool `json:"addToAdminGroup"`
|
||||
}
|
||||
|
||||
// @Summary 更新租户 IAM 用户
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param userId path string true "userId"
|
||||
// @Param body body updateTenantUserRequest true "请求体"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/users/{userId} [put]
|
||||
func (h *ociConfigHandler) updateTenantUser(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -204,6 +259,13 @@ func (h *ociConfigHandler) updateTenantUser(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
|
||||
// @Summary 删除租户 IAM 用户
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param userId path string true "userId"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/users/{userId} [delete]
|
||||
func (h *ociConfigHandler) deleteTenantUser(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -216,6 +278,13 @@ func (h *ociConfigHandler) deleteTenantUser(c *gin.Context) {
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// @Summary 重置租户用户密码
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param userId path string true "userId"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/users/{userId}/reset-password [post]
|
||||
func (h *ociConfigHandler) resetTenantUserPassword(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -229,6 +298,13 @@ func (h *ociConfigHandler) resetTenantUserPassword(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"password": password})
|
||||
}
|
||||
|
||||
// @Summary 清除用户 MFA 设备
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param userId path string true "userId"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/users/{userId}/mfa-devices [delete]
|
||||
func (h *ociConfigHandler) deleteTenantUserMfa(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -242,6 +318,13 @@ func (h *ociConfigHandler) deleteTenantUserMfa(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"deletedTotpDevices": deleted})
|
||||
}
|
||||
|
||||
// @Summary 清除用户全部 API Key
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param userId path string true "userId"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/users/{userId}/api-keys [delete]
|
||||
func (h *ociConfigHandler) deleteTenantUserApiKeys(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -258,6 +341,12 @@ func (h *ociConfigHandler) deleteTenantUserApiKeys(c *gin.Context) {
|
||||
|
||||
// ---- 域通知收件人与密码策略 ----
|
||||
|
||||
// @Summary ---- 域通知收件人与密码策略 ----
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/notification-recipients [get]
|
||||
func (h *ociConfigHandler) getNotificationRecipients(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -271,6 +360,13 @@ func (h *ociConfigHandler) getNotificationRecipients(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, recipients)
|
||||
}
|
||||
|
||||
// @Summary 更新通知收件人
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param body body object true "请求体(见接口说明)"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/notification-recipients [put]
|
||||
func (h *ociConfigHandler) updateNotificationRecipients(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -291,6 +387,12 @@ func (h *ociConfigHandler) updateNotificationRecipients(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, recipients)
|
||||
}
|
||||
|
||||
// @Summary 密码策略列表
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/password-policies [get]
|
||||
func (h *ociConfigHandler) listPasswordPolicies(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -304,6 +406,14 @@ func (h *ociConfigHandler) listPasswordPolicies(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, policies)
|
||||
}
|
||||
|
||||
// @Summary 更新密码策略
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param policyId path string true "policyId"
|
||||
// @Param body body object true "请求体(见接口说明)"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/password-policies/{policyId} [put]
|
||||
func (h *ociConfigHandler) updatePasswordPolicy(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -330,6 +440,12 @@ func (h *ociConfigHandler) updatePasswordPolicy(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, policy)
|
||||
}
|
||||
|
||||
// @Summary 身份域设置
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/identity-settings [get]
|
||||
func (h *ociConfigHandler) getIdentitySetting(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -343,6 +459,13 @@ func (h *ociConfigHandler) getIdentitySetting(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, setting)
|
||||
}
|
||||
|
||||
// @Summary 更新身份域设置
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param body body object true "请求体(见接口说明)"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/identity-settings [put]
|
||||
func (h *ociConfigHandler) updateIdentitySetting(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
|
||||
@@ -23,6 +23,15 @@ type createConsoleSessionRequest struct {
|
||||
}
|
||||
|
||||
// create 创建控制台会话(内部清理残留连接并创建 OCI 控制台连接,约 30-90 秒)。
|
||||
//
|
||||
// @Summary 创建网页控制台会话
|
||||
// @Tags 网页控制台
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param instanceId path string true "实例 OCID"
|
||||
// @Param body body object true "{type: serial|vnc}"
|
||||
// @Success 200 {object} map[string]any "sessionId 与 wsPath"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/console-sessions [post]
|
||||
func (h *consoleHandler) create(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
@@ -42,6 +51,13 @@ func (h *consoleHandler) create(c *gin.Context) {
|
||||
}
|
||||
|
||||
// remove 结束会话并删除云端控制台连接。
|
||||
//
|
||||
// @Summary 关闭网页控制台会话
|
||||
// @Tags 网页控制台
|
||||
// @Param sessionId path string true "会话 ID"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/console-sessions/{sessionId} [delete]
|
||||
func (h *consoleHandler) remove(c *gin.Context) {
|
||||
h.svc.CloseSession(c.Param("sessionId"))
|
||||
c.Status(http.StatusNoContent)
|
||||
@@ -55,6 +71,13 @@ var wsUpgrader = websocket.Upgrader{
|
||||
}
|
||||
|
||||
// ws 是控制台数据面:校验 token 后建立两跳 SSH 隧道,按会话类型分派数据泵。
|
||||
//
|
||||
// @Summary 控制台 WebSocket 数据面
|
||||
// @Tags 网页控制台
|
||||
// @Param sessionId path string true "会话 ID"
|
||||
// @Param token query string true "JWT(浏览器 WebSocket 无法带头,经 query 传递)"
|
||||
// @Success 101 "升级为 WebSocket"
|
||||
// @Router /api/v1/console-sessions/{sessionId}/ws [get]
|
||||
func (h *consoleHandler) ws(c *gin.Context) {
|
||||
if _, err := h.auth.ParseToken(c.Query("token")); err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid or expired token"})
|
||||
|
||||
+36
-7
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -15,10 +16,12 @@ import (
|
||||
|
||||
// ONS 消息头(方案A §2.2/§2.3,官方 X-OCI-NS-* 字段)。
|
||||
const (
|
||||
onsHeaderMessageType = "X-OCI-NS-MessageType"
|
||||
onsHeaderMessageID = "X-OCI-NS-MessageId"
|
||||
onsHeaderTimestamp = "X-OCI-NS-Timestamp"
|
||||
onsHeaderConfirmURL = "X-OCI-NS-ConfirmationURL"
|
||||
onsHeaderMessageType = "X-OCI-NS-MessageType"
|
||||
onsHeaderMessageID = "X-OCI-NS-MessageId"
|
||||
onsHeaderTimestamp = "X-OCI-NS-Timestamp"
|
||||
onsHeaderConfirmURL = "X-OCI-NS-ConfirmationURL"
|
||||
onsHeaderSigningCertURL = "X-OCI-NS-SigningCertURL"
|
||||
onsHeaderSignature = "X-OCI-NS-Signature"
|
||||
)
|
||||
|
||||
// webhook 同步路径的防御参数:body 上限高于 ONS 链路 128KB 留余量,
|
||||
@@ -44,6 +47,15 @@ type webhookHandler struct {
|
||||
|
||||
// handle 是 webhook 入口:secret 反查租户后按消息类型分派;
|
||||
// 同步路径只做 secret→分派→读body→幂等入库,5 秒投递预算内返回(方案A §2.4)。
|
||||
//
|
||||
// @Summary OCI 日志回传入口(ONS HTTPS 订阅投递)
|
||||
// @Tags 任务与日志回传
|
||||
// @Param secret path string true "面板生成的回传密钥(鉴权凭据)"
|
||||
// @Param body body object true "ONS 消息原文(SubscriptionConfirmation / Notification)"
|
||||
// @Success 200 "已受理"
|
||||
// @Failure 403 {object} map[string]string "时间戳超窗或证书源非 Oracle 域"
|
||||
// @Failure 404 "secret 无效(不暴露端点存在性)"
|
||||
// @Router /api/v1/webhooks/oci-logs/{secret} [post]
|
||||
func (h *webhookHandler) handle(c *gin.Context) {
|
||||
cfgID, ok := h.events.ResolveSecret(c.Request.Context(), c.Param("secret"))
|
||||
if !ok {
|
||||
@@ -141,8 +153,25 @@ func parseOnsTime(value string) (time.Time, bool) {
|
||||
}
|
||||
|
||||
// verifySignature 校验 ONS 消息签名。
|
||||
// TODO(方案A §2.3 spike):官方未公开签名 canonical form,待 test-server 抓取
|
||||
// 真实消息确认输入格式后实现;当前按方案约定以 secret+时间戳+幂等先行。
|
||||
func verifySignature(http.Header) error {
|
||||
//
|
||||
// 现状:官方未公开 canonical form。归档任务 07-06-log-relay-plan-a 与 07-07-relay-impl
|
||||
// 用真实抓包样本(body+签名+证书公钥)对 15 种常见拼接 × 2 种 PSS salt 长度全部未命中,
|
||||
// 逆向成本高。P0 决策以「secret 路径段 + HTTPS + 时间戳偏差 ≤5min + MessageId 幂等」上线。
|
||||
//
|
||||
// 本函数做能做的**证书源头硬化**:即便未来接入 canonical form + 公钥验签,
|
||||
// SigningCertURL 若允许任意域名,攻击者可自签证书构造 body+签名+cert URL 三元组绕过
|
||||
// (AWS SNS 曾出现同类 GHSA-8jgf-23q5-x7xx)。这里预先把 cert URL 域名锁到 Oracle 云,
|
||||
// 让未来加签名校验成为「相加」而非「返修」。
|
||||
func verifySignature(h http.Header) error {
|
||||
certURL := h.Get(onsHeaderSigningCertURL)
|
||||
if certURL == "" {
|
||||
return nil // 头缺失回退到 secret+时间戳+幂等三重防护,不误杀历史 layout
|
||||
}
|
||||
parsed, err := url.Parse(certURL)
|
||||
if err != nil || parsed.Scheme != "https" || !service.IsOracleHost(parsed.Host) {
|
||||
return errors.New("ons: signing cert url not in oracle domain")
|
||||
}
|
||||
// TODO(canonical form 公开后):按 SigningCertURL 拉证书(含 host 复核 + 缓存),
|
||||
// 用公钥对 signing string 做 SHA256withRSA/PSS 验签,失败返回 error。
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -173,6 +173,36 @@ func TestWebhookDispatch(t *testing.T) {
|
||||
headers: map[string]string{onsHeaderTimestamp: now},
|
||||
wantStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "SigningCertURL 非 Oracle 域名拒绝", secret: "",
|
||||
headers: map[string]string{
|
||||
onsHeaderMessageID: "m-evil-cert",
|
||||
onsHeaderTimestamp: now,
|
||||
onsHeaderSigningCertURL: "https://evil.example.com/cert.crt",
|
||||
},
|
||||
body: `{"eventType":"t"}`,
|
||||
wantStatus: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "SigningCertURL 非 https 拒绝", secret: "",
|
||||
headers: map[string]string{
|
||||
onsHeaderMessageID: "m-http-cert",
|
||||
onsHeaderTimestamp: now,
|
||||
onsHeaderSigningCertURL: "http://objectstorage.eu-paris-1.oraclecloud.com/cert.crt",
|
||||
},
|
||||
body: `{"eventType":"t"}`,
|
||||
wantStatus: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "SigningCertURL 白名单内放行(canonical form 未接入,不做签名校验)", secret: "",
|
||||
headers: map[string]string{
|
||||
onsHeaderMessageID: "m-good-cert",
|
||||
onsHeaderTimestamp: now,
|
||||
onsHeaderSigningCertURL: "https://objectstorage.eu-paris-1.oraclecloud.com/p/x/n/bmc-ons-prod/b/ons-ds/o/sig.crt",
|
||||
},
|
||||
body: `{"eventType":"t"}`,
|
||||
wantStatus: http.StatusOK, wantIngest: 1,
|
||||
},
|
||||
{
|
||||
name: "缺时间戳头放行入库", secret: "",
|
||||
headers: map[string]string{onsHeaderMessageID: "m2"},
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
// Config 保存进程运行所需的全部配置。
|
||||
type Config struct {
|
||||
Addr string // HTTP 监听地址
|
||||
DBDriver string // 数据库驱动:sqlite(默认)/ mysql / postgres
|
||||
DBDSN string // mysql / postgres 的连接串;sqlite 不使用
|
||||
DBPath string // SQLite 文件路径
|
||||
DataKey string // 敏感字段加密主密钥
|
||||
JWTSecret string // JWT 签名密钥
|
||||
@@ -26,8 +28,14 @@ func Load() (*Config, error) {
|
||||
if jwtSecret == "" {
|
||||
return nil, fmt.Errorf("load config: JWT_SECRET is required")
|
||||
}
|
||||
driver, dsn, err := dbFromEnv()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Config{
|
||||
Addr: envOr("ADDR", ":8080"),
|
||||
DBDriver: driver,
|
||||
DBDSN: dsn,
|
||||
DBPath: envOr("DB_PATH", "oci-portal.db"),
|
||||
DataKey: dataKey,
|
||||
JWTSecret: jwtSecret,
|
||||
@@ -37,6 +45,22 @@ func Load() (*Config, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// dbFromEnv 读取并校验数据库驱动配置;外部库缺 DSN 视为配置错误,启动即失败。
|
||||
func dbFromEnv() (driver, dsn string, err error) {
|
||||
driver = envOr("DB_DRIVER", "sqlite")
|
||||
dsn = os.Getenv("DB_DSN")
|
||||
switch driver {
|
||||
case "sqlite":
|
||||
case "mysql", "postgres":
|
||||
if dsn == "" {
|
||||
return "", "", fmt.Errorf("load config: DB_DSN is required when DB_DRIVER=%s", driver)
|
||||
}
|
||||
default:
|
||||
return "", "", fmt.Errorf("load config: unsupported DB_DRIVER %q (sqlite/mysql/postgres)", driver)
|
||||
}
|
||||
return driver, dsn, nil
|
||||
}
|
||||
|
||||
func envOr(name, def string) string {
|
||||
if v := os.Getenv(name); v != "" {
|
||||
return v
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDBFromEnv(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
driver string
|
||||
dsn string
|
||||
wantDriver string
|
||||
wantErr string
|
||||
}{
|
||||
{name: "缺省走 sqlite", wantDriver: "sqlite"},
|
||||
{name: "postgres 带 DSN", driver: "postgres", dsn: "host=x", wantDriver: "postgres"},
|
||||
{name: "mysql 缺 DSN 报错", driver: "mysql", wantErr: "DB_DSN is required"},
|
||||
{name: "postgres 缺 DSN 报错", driver: "postgres", wantErr: "DB_DSN is required"},
|
||||
{name: "非法驱动报错", driver: "oracle", wantErr: "unsupported DB_DRIVER"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Setenv("DB_DRIVER", tt.driver)
|
||||
t.Setenv("DB_DSN", tt.dsn)
|
||||
driver, dsn, err := dbFromEnv()
|
||||
if tt.wantErr != "" {
|
||||
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Fatalf("err = %v, want contains %q", err, tt.wantErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("dbFromEnv: %v", err)
|
||||
}
|
||||
if driver != tt.wantDriver || dsn != tt.dsn {
|
||||
t.Fatalf("got (%q,%q), want (%q,%q)", driver, dsn, tt.wantDriver, tt.dsn)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,29 +4,56 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
"oci-portal/internal/model"
|
||||
)
|
||||
|
||||
// Open 打开 SQLite 数据库并自动迁移全部模型。
|
||||
func Open(path string) (*gorm.DB, error) {
|
||||
db, err := gorm.Open(sqlite.Open(path), &gorm.Config{
|
||||
// Open 按驱动打开数据库并自动迁移全部模型。
|
||||
// driver 取值 sqlite(默认)/mysql/postgres;sqlite 用 path,其余用 dsn。
|
||||
func Open(driver, dsn, path string) (*gorm.DB, error) {
|
||||
dialector, err := buildDialector(driver, dsn, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db, err := gorm.Open(dialector, &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Warn),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open sqlite %s: %w", path, err)
|
||||
return nil, fmt.Errorf("open %s database: %w", dialector.Name(), err)
|
||||
}
|
||||
if err := db.AutoMigrate(
|
||||
if err := autoMigrate(db); err != nil {
|
||||
return nil, fmt.Errorf("auto migrate: %w", err)
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// buildDialector 把驱动名映射为 GORM dialector;三个驱动均为纯 Go,不引入 cgo。
|
||||
// MySQL 设 DefaultStringSize=512:未标 size 的 string 建为 varchar(512),
|
||||
// 避免 OCID / URL 类字段被默认 varchar(191) 截断(SQLite/PG 的 string 天然无长度上限)。
|
||||
func buildDialector(driver, dsn, path string) (gorm.Dialector, error) {
|
||||
switch driver {
|
||||
case "", "sqlite":
|
||||
return sqlite.Open(path), nil
|
||||
case "mysql":
|
||||
return mysql.New(mysql.Config{DSN: dsn, DefaultStringSize: 512}), nil
|
||||
case "postgres":
|
||||
return postgres.Open(dsn), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported DB_DRIVER %q (sqlite/mysql/postgres)", driver)
|
||||
}
|
||||
}
|
||||
|
||||
func autoMigrate(db *gorm.DB) error {
|
||||
return db.AutoMigrate(
|
||||
&model.User{}, &model.UserIdentity{}, &model.OciConfig{}, &model.Task{}, &model.TaskLog{},
|
||||
&model.CheckSnapshot{}, &model.CostSnapshot{},
|
||||
&model.RegionCache{}, &model.CompartmentCache{}, &model.Setting{},
|
||||
&model.SystemLog{}, &model.LogEvent{}, &model.Proxy{},
|
||||
&model.AiKey{}, &model.AiChannel{}, &model.AiModelCache{}, &model.AiCallLog{},
|
||||
&model.AiContentLog{},
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("auto migrate: %w", err)
|
||||
}
|
||||
return db, nil
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildDialector(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
driver string
|
||||
wantName string
|
||||
wantErr string
|
||||
}{
|
||||
{name: "默认空驱动走 sqlite", driver: "", wantName: "sqlite"},
|
||||
{name: "显式 sqlite", driver: "sqlite", wantName: "sqlite"},
|
||||
{name: "mysql", driver: "mysql", wantName: "mysql"},
|
||||
{name: "postgres", driver: "postgres", wantName: "postgres"},
|
||||
{name: "未知驱动报错", driver: "oracle", wantErr: "unsupported DB_DRIVER"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d, err := buildDialector(tt.driver, "user:pass@tcp(127.0.0.1)/db", "test.db")
|
||||
if tt.wantErr != "" {
|
||||
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Fatalf("err = %v, want contains %q", err, tt.wantErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("buildDialector: %v", err)
|
||||
}
|
||||
if got := d.Name(); got != tt.wantName {
|
||||
t.Fatalf("dialector name = %q, want %q", got, tt.wantName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenSQLiteMigrates(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "test.db")
|
||||
db, err := Open("sqlite", "", path)
|
||||
if err != nil {
|
||||
t.Fatalf("Open sqlite: %v", err)
|
||||
}
|
||||
for _, table := range []string{"users", "oci_configs", "ai_channels", "log_events"} {
|
||||
if !db.Migrator().HasTable(table) {
|
||||
t.Fatalf("table %s not migrated", table)
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
-14
@@ -20,7 +20,7 @@ const (
|
||||
// Setting 是系统级键值配置;敏感值(如 telegram_bot_token)以 AES-GCM 密文存储。
|
||||
type Setting struct {
|
||||
Key string `gorm:"primaryKey;size:64" json:"key"`
|
||||
Value string `json:"value"`
|
||||
Value string `gorm:"type:text" json:"value"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
@@ -106,10 +106,10 @@ type Task struct {
|
||||
Name string `gorm:"size:128" json:"name"`
|
||||
Type string `gorm:"size:32;index" json:"type"`
|
||||
CronExpr string `gorm:"size:64" json:"cronExpr"`
|
||||
Payload string `json:"payload"`
|
||||
Payload string `gorm:"type:text" json:"payload"`
|
||||
Status string `gorm:"size:16;index" json:"status"`
|
||||
LastRunAt *time.Time `json:"lastRunAt"`
|
||||
LastError string `json:"lastError"`
|
||||
LastError string `gorm:"type:text" json:"lastError"`
|
||||
RunCount int `json:"runCount"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
@@ -120,7 +120,7 @@ type TaskLog struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
TaskID uint `gorm:"index" json:"taskId"`
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Message string `gorm:"type:text" json:"message"`
|
||||
DurationMs int64 `json:"durationMs"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
@@ -162,8 +162,9 @@ type OciConfig struct {
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
Region string `json:"region"`
|
||||
|
||||
PrivateKeyEnc string `json:"-"`
|
||||
PassphraseEnc string `json:"-"`
|
||||
// 私钥 PEM 密文约 4KB,必须 text;passphrase 密文同档处理
|
||||
PrivateKeyEnc string `gorm:"type:text" json:"-"`
|
||||
PassphraseEnc string `gorm:"type:text" json:"-"`
|
||||
|
||||
TenancyName string `json:"tenancyName"`
|
||||
HomeRegionKey string `json:"homeRegionKey"`
|
||||
@@ -177,7 +178,7 @@ type OciConfig struct {
|
||||
PromotionExpires *time.Time `json:"promotionExpires"`
|
||||
|
||||
AliveStatus string `json:"aliveStatus"`
|
||||
LastError string `json:"lastError"`
|
||||
LastError string `gorm:"type:text" json:"lastError"`
|
||||
LastVerifiedAt *time.Time `json:"lastVerifiedAt"`
|
||||
|
||||
// 多区域 / 多区间支持:开启后订阅区域与 compartment 列表入库缓存,
|
||||
@@ -199,10 +200,11 @@ type LogEvent struct {
|
||||
Source string `gorm:"size:64" json:"source"`
|
||||
SourceIP string `gorm:"size:64" json:"sourceIp"` // 异步解析回填,Audit 事件的发起方 IP
|
||||
EventTime *time.Time `gorm:"index" json:"eventTime"`
|
||||
Payload string `json:"payload"`
|
||||
Truncated bool `json:"truncated"`
|
||||
Processed bool `gorm:"index" json:"processed"`
|
||||
ReceivedAt time.Time `json:"receivedAt"`
|
||||
// 防御上限 256KB 超 MySQL TEXT(64KB),size 上探一档 → MEDIUMTEXT;PG/SQLite 仍为 text
|
||||
Payload string `gorm:"size:16777216" json:"payload"`
|
||||
Truncated bool `json:"truncated"`
|
||||
Processed bool `gorm:"index" json:"processed"`
|
||||
ReceivedAt time.Time `json:"receivedAt"`
|
||||
}
|
||||
|
||||
// RegionCache 是开启多区域支持的配置缓存的订阅区域(每配置多行,整组覆盖)。
|
||||
@@ -316,8 +318,9 @@ type AiContentLog struct {
|
||||
Endpoint string `gorm:"size:16" json:"endpoint"`
|
||||
Model string `gorm:"size:96" json:"model"`
|
||||
Stream bool `json:"stream"`
|
||||
// RequestBody / ResponseBody 是截断后的正文 JSON
|
||||
RequestBody string `json:"requestBody"`
|
||||
ResponseBody string `json:"responseBody"`
|
||||
// RequestBody / ResponseBody 是截断后的正文 JSON;
|
||||
// 64KB 截断恰在 MySQL TEXT 上限(65535B)边界,size 上探一档 → MEDIUMTEXT
|
||||
RequestBody string `gorm:"size:16777216" json:"requestBody"`
|
||||
ResponseBody string `gorm:"size:16777216" json:"responseBody"`
|
||||
CreatedAt time.Time `gorm:"index" json:"createdAt"`
|
||||
}
|
||||
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>OCI Portal</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>前端产物未嵌入:请从前端仓库 Release 下载 dist.zip 解压至此目录(或本地构建前端后拷入),此文件为仓库占位。</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
// Package webui 承载前端构建产物的 go:embed 嵌入。
|
||||
// dist 由构建脚本从 oci-portal-dash/dist 拷入覆盖;
|
||||
// 仓库只提交占位 index.html,保证裸 clone 后 go build 可过。
|
||||
package webui
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
// all: 前缀连同 _ / . 开头文件一并嵌入(vite 产物含下划线开头的 chunk)。
|
||||
//
|
||||
//go:embed all:dist
|
||||
var distFS embed.FS
|
||||
|
||||
// Dist 返回以 dist 为根的只读文件系统,供 HTTP 层直接伺服。
|
||||
func Dist() (fs.FS, error) {
|
||||
return fs.Sub(distFS, "dist")
|
||||
}
|
||||
Reference in New Issue
Block a user