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" // @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)。 // // @Summary 批式懒加载查询租户 OCI 审计事件 // @Tags 租户 IAM // @Param id path int true "配置 ID" // @Param cursor query string false "续查游标(上次响应原样带回)" // @Param limit query int false "单批目标条数,缺省 100,上限 200" // @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} 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 ---- 域通知收件人与密码策略 ---- // @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) }