package api import ( "errors" "net/http" "strconv" "time" "github.com/gin-gonic/gin" "oci-portal/internal/oci" "oci-portal/internal/service" ) // ---- 流量与成本 ---- // @Summary ---- 流量与成本 ---- // @Tags 计算 // @Param id path int true "配置 ID" // @Param instanceId path string true "instanceId" // @Success 200 {object} oci.InstanceTraffic // @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 { return } days, _ := strconv.Atoi(c.DefaultQuery("days", "30")) traffic, err := h.svc.InstanceTraffic(c.Request.Context(), id, c.Query("region"), c.Param("instanceId"), days) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, traffic) } // @Summary 配置成本快照 // @Tags 成本 // @Param id path int true "配置 ID" // @Param startTime query string false "起始时间 RFC3339,缺省为 endTime-30 天;返回行严格限于 [startTime, endTime) 窗口(Usage API HOURLY 粒度会把起点下扩到 UTC 日零点,越界行已在服务端过滤)" // @Param endTime query string false "结束时间 RFC3339,缺省为当前时刻" // @Param granularity query string false "HOURLY / DAILY / MONTHLY,缺省 DAILY" // @Param queryType query string false "COST / USAGE,缺省 COST" // @Param groupBy query string false "分组维度,单维或复合(如 service,skuName),缺省 service" // @Success 200 {array} oci.CostItem // @Security BearerAuth // @Router /api/v1/oci-configs/{id}/costs [get] func (h *ociConfigHandler) costs(c *gin.Context) { id, ok := pathID(c) if !ok { return } q := oci.CostQuery{ Granularity: c.Query("granularity"), QueryType: c.Query("queryType"), GroupBy: c.Query("groupBy"), } if t, err := time.Parse(time.RFC3339, c.Query("startTime")); err == nil { q.StartTime = t } if t, err := time.Parse(time.RFC3339, c.Query("endTime")); err == nil { q.EndTime = t } items, err := h.svc.Costs(c.Request.Context(), id, q) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, items) } // ---- 租户审计日志 ---- // getAuditEvents 批式懒加载查询审计事件:cursor 为空自当前时刻首查, // 非空从上次响应游标继续向更早回溯;limit 单批目标条数(缺省 100,上限 200); // q 为服务端全文检索关键字,仅首查生效,续查沿用游标内嵌关键字。 // // @Summary 批式懒加载查询租户 OCI 审计事件 // @Tags 租户 IAM // @Param id path int true "配置 ID" // @Param cursor query string false "续查游标(上次响应原样带回)" // @Param limit query int false "单批目标条数,缺省 100,上限 200" // @Param q query string false "检索关键字(服务端全文匹配,支持 * 通配;仅首查生效)" // @Success 200 {object} service.AuditEventsView // @Security BearerAuth // @Router /api/v1/oci-configs/{id}/audit-events [get] func (h *ociConfigHandler) getAuditEvents(c *gin.Context) { id, ok := pathID(c) if !ok { return } limit, _ := strconv.Atoi(c.Query("limit")) q := service.AuditQuery{Region: c.Query("region"), Cursor: c.Query("cursor"), Limit: limit, Q: c.Query("q")} result, err := h.svc.AuditEvents(c.Request.Context(), id, q) if errors.Is(err, service.ErrInvalidAuditCursor) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, result) } // getAuditEventDetail 按 eventId 取回原始事件 JSON:缓存命中秒开, // 过期走事件时间小窗重查;仍不可得返回 404,前端降级展示精简字段。 // // @Summary 按 eventId 取回原始事件 JSON:缓存命中秒开, // @Tags 租户 IAM // @Param id path int true "配置 ID" // @Success 200 {object} rawDetailResponse "raw 为事件原文 JSON" // @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 { return } eventID := c.Query("eventId") eventTime, terr := time.Parse(time.RFC3339, c.Query("eventTime")) if eventID == "" || terr != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "eventId 与 eventTime(RFC3339)必填"}) return } raw, err := h.svc.AuditEventDetail(c.Request.Context(), id, c.Query("region"), eventID, eventTime) if errors.Is(err, service.ErrAuditEventGone) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, gin.H{"raw": raw}) } // ---- 租户用户管理 ---- type createTenantUserRequest struct { Name string `json:"name" binding:"required"` Description string `json:"description"` Email string `json:"email"` GivenName string `json:"givenName" binding:"required"` FamilyName string `json:"familyName" binding:"required"` // 同时勾选时先授身份域管理员,再加入管理员组 GrantDomainAdmin bool `json:"grantDomainAdmin"` AddToAdminGroup bool `json:"addToAdminGroup"` } // @Summary 租户身份域列表 // @Tags 租户 IAM // @Param id path int true "配置 ID" // @Success 200 {array} oci.IdentityDomain // @Security BearerAuth // @Router /api/v1/oci-configs/{id}/domains [get] func (h *ociConfigHandler) listIdentityDomains(c *gin.Context) { id, ok := pathID(c) if !ok { return } domains, err := h.svc.IdentityDomains(c.Request.Context(), id) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, domains) } // @Summary 租户 IAM 用户列表 // @Tags 租户 IAM // @Param id path int true "配置 ID" // @Param domainId query string false "身份域 OCID(缺省 Default 域)" // @Success 200 {array} oci.TenantUser // @Security BearerAuth // @Router /api/v1/oci-configs/{id}/users [get] func (h *ociConfigHandler) listTenantUsers(c *gin.Context) { id, ok := pathID(c) if !ok { return } users, err := h.svc.TenantUsers(c.Request.Context(), id, c.Query("domainId")) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, users) } // @Summary 租户用户详情 // @Tags 租户 IAM // @Param id path int true "配置 ID" // @Param domainId query string false "身份域 OCID(缺省 Default 域)" // @Param userId path string true "userId" // @Success 200 {object} oci.TenantUserDetail // @Security BearerAuth // @Router /api/v1/oci-configs/{id}/users/{userId} [get] func (h *ociConfigHandler) getTenantUserDetail(c *gin.Context) { id, ok := pathID(c) if !ok { return } detail, err := h.svc.TenantUserDetail(c.Request.Context(), id, c.Query("domainId"), c.Param("userId")) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, detail) } // @Summary 创建租户 IAM 用户 // @Tags 租户 IAM // @Param id path int true "配置 ID" // @Param domainId query string false "身份域 OCID(缺省 Default 域)" // @Param body body createTenantUserRequest true "请求体" // @Success 201 {object} oci.TenantUser // @Security BearerAuth // @Router /api/v1/oci-configs/{id}/users [post] func (h *ociConfigHandler) createTenantUser(c *gin.Context) { id, ok := pathID(c) if !ok { return } var req createTenantUserRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } user, err := h.svc.CreateTenantUser(c.Request.Context(), id, c.Query("domainId"), oci.CreateTenantUserInput{ Name: req.Name, Description: req.Description, Email: req.Email, GivenName: req.GivenName, FamilyName: req.FamilyName, GrantDomainAdmin: req.GrantDomainAdmin, AddToAdminGroup: req.AddToAdminGroup, }) if err != nil { respondError(c, err) return } c.JSON(http.StatusCreated, user) } // updateTenantUserRequest 的字段全部可缺省:nil 不修改,传空串表示清空。 // 管理员字段为三态:true 授予/加入,false 撤销/移出,缺省不动。 type updateTenantUserRequest struct { Description *string `json:"description"` Email *string `json:"email"` GivenName *string `json:"givenName"` FamilyName *string `json:"familyName"` GrantDomainAdmin *bool `json:"grantDomainAdmin"` AddToAdminGroup *bool `json:"addToAdminGroup"` } // @Summary 更新租户 IAM 用户 // @Tags 租户 IAM // @Param id path int true "配置 ID" // @Param domainId query string false "身份域 OCID(缺省 Default 域)" // @Param userId path string true "userId" // @Param body body updateTenantUserRequest true "请求体" // @Success 200 {object} oci.TenantUser // @Security BearerAuth // @Router /api/v1/oci-configs/{id}/users/{userId} [put] func (h *ociConfigHandler) updateTenantUser(c *gin.Context) { id, ok := pathID(c) if !ok { return } var req updateTenantUserRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } user, err := h.svc.UpdateTenantUser(c.Request.Context(), id, c.Query("domainId"), c.Param("userId"), oci.UpdateTenantUserInput{ Description: req.Description, Email: req.Email, GivenName: req.GivenName, FamilyName: req.FamilyName, GrantDomainAdmin: req.GrantDomainAdmin, AddToAdminGroup: req.AddToAdminGroup, }) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, user) } // @Summary 删除租户 IAM 用户 // @Tags 租户 IAM // @Param id path int true "配置 ID" // @Param domainId query string false "身份域 OCID(缺省 Default 域)" // @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 { return } if err := h.svc.DeleteTenantUser(c.Request.Context(), id, c.Query("domainId"), c.Param("userId")); err != nil { respondError(c, err) return } c.Status(http.StatusNoContent) } // @Summary 重置租户用户密码 // @Tags 租户 IAM // @Param id path int true "配置 ID" // @Param domainId query string false "身份域 OCID(缺省 Default 域)" // @Param userId path string true "userId" // @Success 200 {object} passwordResponse "一次性明文密码" // @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 { return } password, err := h.svc.ResetTenantUserPassword(c.Request.Context(), id, c.Query("domainId"), c.Param("userId")) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, gin.H{"password": password}) } // @Summary 清除用户 MFA 设备 // @Tags 租户 IAM // @Param id path int true "配置 ID" // @Param domainId query string false "身份域 OCID(缺省 Default 域)" // @Param userId path string true "userId" // @Success 200 {object} deletedTotpResponse // @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 { return } deleted, err := h.svc.DeleteTenantUserMfa(c.Request.Context(), id, c.Query("domainId"), c.Param("userId")) if err != nil { respondError(c, err) return } 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} deletedApiKeysResponse // @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 { return } includeCurrent := c.Query("includeCurrent") == "true" deleted, err := h.svc.DeleteTenantUserApiKeys(c.Request.Context(), id, c.Param("userId"), includeCurrent) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, gin.H{"deletedApiKeys": deleted}) } // @Summary 列出用户 API Key // @Tags 租户 IAM // @Param id path int true "配置 ID" // @Param userId path string true "userId" // @Success 200 {object} userApiKeysResponse // @Security BearerAuth // @Router /api/v1/oci-configs/{id}/users/{userId}/api-keys [get] func (h *ociConfigHandler) listTenantUserApiKeys(c *gin.Context) { id, ok := pathID(c) if !ok { return } keys, err := h.svc.TenantUserApiKeys(c.Request.Context(), id, c.Param("userId")) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, gin.H{"items": keys}) } // @Summary 为用户新增 API Key // @Description 后端生成 RSA-2048 密钥对并上传公钥;私钥仅本次响应返回,不落库。 // @Tags 租户 IAM // @Param id path int true "配置 ID" // @Param userId path string true "userId" // @Success 200 {object} service.CreatedApiKey // @Security BearerAuth // @Router /api/v1/oci-configs/{id}/users/{userId}/api-keys [post] func (h *ociConfigHandler) addTenantUserApiKey(c *gin.Context) { id, ok := pathID(c) if !ok { return } created, err := h.svc.AddTenantUserApiKey(c.Request.Context(), id, c.Param("userId")) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, created) } // @Summary 删除用户单把 API Key // @Description 当前配置正在使用的 key 拒删(409),避免面板失联。 // @Tags 租户 IAM // @Param id path int true "配置 ID" // @Param userId path string true "userId" // @Param fingerprint path string true "key 指纹" // @Success 204 "无内容" // @Security BearerAuth // @Router /api/v1/oci-configs/{id}/users/{userId}/api-keys/{fingerprint} [delete] func (h *ociConfigHandler) deleteTenantUserApiKey(c *gin.Context) { id, ok := pathID(c) if !ok { return } err := h.svc.DeleteTenantUserApiKey(c.Request.Context(), id, c.Param("userId"), c.Param("fingerprint")) if errors.Is(err, service.ErrCurrentApiKey) { c.JSON(http.StatusConflict, gin.H{"error": err.Error()}) return } if err != nil { respondError(c, err) return } c.Status(http.StatusNoContent) } // @Summary 启用 API Key 为面板签名凭据 // @Description 将刚创建的 key 设为本配置签名凭据:验证可用后落库,不删除旧 key;私钥为创建时下发的那份回传。userId 给定且异于当前签名用户时,一并把签名用户切换为该用户。 // @Tags 租户 IAM // @Param id path int true "配置 ID" // @Param body body object true "请求体:fingerprint 与 privateKey,可选 userId(切换签名用户)" // @Success 204 "无内容" // @Security BearerAuth // @Router /api/v1/oci-configs/{id}/activate-api-key [post] func (h *ociConfigHandler) activateApiKey(c *gin.Context) { id, ok := pathID(c) if !ok { return } var req struct { UserID string `json:"userId"` Fingerprint string `json:"fingerprint"` PrivateKey string `json:"privateKey"` } if err := c.ShouldBindJSON(&req); err != nil || req.Fingerprint == "" || req.PrivateKey == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "fingerprint 与 privateKey 必填"}) return } if err := h.svc.ActivateApiKey(c.Request.Context(), id, req.UserID, req.Fingerprint, req.PrivateKey); err != nil { respondError(c, err) return } c.Status(http.StatusNoContent) } // ---- 域通知收件人与密码策略 ---- // @Summary ---- 域通知收件人与密码策略 ---- // @Tags 租户 IAM // @Param id path int true "配置 ID" // @Param domainId query string false "身份域 OCID(缺省 Default 域)" // @Success 200 {object} oci.NotificationRecipients // @Security BearerAuth // @Router /api/v1/oci-configs/{id}/notification-recipients [get] func (h *ociConfigHandler) getNotificationRecipients(c *gin.Context) { id, ok := pathID(c) if !ok { return } recipients, err := h.svc.NotificationRecipients(c.Request.Context(), id, c.Query("domainId")) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, recipients) } // @Summary 更新通知收件人 // @Tags 租户 IAM // @Param id path int true "配置 ID" // @Param domainId query string false "身份域 OCID(缺省 Default 域)" // @Param body body object true "请求体(见接口说明)" // @Success 200 {object} oci.NotificationRecipients // @Security BearerAuth // @Router /api/v1/oci-configs/{id}/notification-recipients [put] func (h *ociConfigHandler) updateNotificationRecipients(c *gin.Context) { id, ok := pathID(c) if !ok { return } var req struct { Recipients []string `json:"recipients"` // 空列表表示关闭 test mode 恢复默认发送 } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } recipients, err := h.svc.UpdateNotificationRecipients(c.Request.Context(), id, c.Query("domainId"), req.Recipients) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, recipients) } // @Summary 密码策略列表 // @Tags 租户 IAM // @Param id path int true "配置 ID" // @Param domainId query string false "身份域 OCID(缺省 Default 域)" // @Success 200 {array} oci.PasswordPolicyInfo // @Security BearerAuth // @Router /api/v1/oci-configs/{id}/password-policies [get] func (h *ociConfigHandler) listPasswordPolicies(c *gin.Context) { id, ok := pathID(c) if !ok { return } policies, err := h.svc.PasswordPolicies(c.Request.Context(), id, c.Query("domainId")) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, policies) } // @Summary 更新密码策略 // @Tags 租户 IAM // @Param id path int true "配置 ID" // @Param domainId query string false "身份域 OCID(缺省 Default 域)" // @Param policyId path string true "policyId" // @Param body body object true "请求体(见接口说明)" // @Success 200 {object} oci.PasswordPolicyInfo // @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 { return } var req struct { PasswordExpiresAfter *int `json:"passwordExpiresAfter"` MinLength *int `json:"minLength"` NumPasswordsInHistory *int `json:"numPasswordsInHistory"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } policy, err := h.svc.UpdatePasswordPolicy(c.Request.Context(), id, c.Query("domainId"), c.Param("policyId"), oci.UpdatePasswordPolicyInput{ PasswordExpiresAfter: req.PasswordExpiresAfter, MinLength: req.MinLength, NumPasswordsInHistory: req.NumPasswordsInHistory, }) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, policy) } // @Summary 身份域设置 // @Tags 租户 IAM // @Param id path int true "配置 ID" // @Param domainId query string false "身份域 OCID(缺省 Default 域)" // @Success 200 {object} oci.IdentitySettingInfo // @Security BearerAuth // @Router /api/v1/oci-configs/{id}/identity-settings [get] func (h *ociConfigHandler) getIdentitySetting(c *gin.Context) { id, ok := pathID(c) if !ok { return } setting, err := h.svc.IdentitySetting(c.Request.Context(), id, c.Query("domainId")) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, setting) } // @Summary 更新身份域设置 // @Tags 租户 IAM // @Param id path int true "配置 ID" // @Param domainId query string false "身份域 OCID(缺省 Default 域)" // @Param body body object true "请求体(见接口说明)" // @Success 200 {object} oci.IdentitySettingInfo // @Security BearerAuth // @Router /api/v1/oci-configs/{id}/identity-settings [put] func (h *ociConfigHandler) updateIdentitySetting(c *gin.Context) { id, ok := pathID(c) if !ok { return } var req struct { PrimaryEmailRequired *bool `json:"primaryEmailRequired" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } setting, err := h.svc.UpdateIdentitySetting(c.Request.Context(), id, c.Query("domainId"), *req.PrimaryEmailRequired) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, setting) }