仓库自包含化:Docker/CI/文档/规范入库,路由分组拆分
CI / test (push) Successful in 15s

This commit is contained in:
Wang Defa
2026-07-09 19:18:04 +08:00
parent b9a3e97e84
commit 3e0389c1e9
83 changed files with 20241 additions and 248 deletions
+6
View File
@@ -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,
+86
View File
@@ -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"))
+29
View File
@@ -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 {
+112
View File
@@ -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 {
+17
View File
@@ -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 != "" {
+96
View File
@@ -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 {
+22
View File
@@ -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 {
+54
View File
@@ -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 {
+77
View File
@@ -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 {
+50
View File
@@ -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 {
+132
View File
@@ -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 {
+57
View File
@@ -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 {
+6
View File
@@ -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 {
+42
View File
@@ -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 {
+8 -5
View File
@@ -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)
}
}
+25
View File
@@ -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
View File
@@ -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
}
+38
View File
@@ -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)
}
+38
View File
@@ -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) })
}
+16
View File
@@ -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)
}
+134
View File
@@ -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)
}
+35
View File
@@ -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)
}
+19
View File
@@ -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))
}
+28
View File
@@ -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)
}
+80
View File
@@ -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 {
+56
View File
@@ -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")
}
+71
View File
@@ -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)
}
}
+19
View File
@@ -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 {
+6
View File
@@ -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"))
+42
View File
@@ -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 {
+123
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+30
View File
@@ -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"},
+24
View File
@@ -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
+41
View File
@@ -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)
}
})
}
}
+36 -9
View File
@@ -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
)
}
+52
View File
@@ -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
View File
@@ -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"`
}
+11
View File
@@ -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>
+19
View File
@@ -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")
}