package api import ( "errors" "net/http" "strconv" "time" "github.com/gin-gonic/gin" "oci-portal/internal/oci" "oci-portal/internal/service" ) // ---- 流量与成本 ---- 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) } 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 实时查询租户 OCI 审计事件:hours 首查(缺省 24); // start/end/page 为截断后的同窗续查;窗口越界或非法返回 400。 func (h *ociConfigHandler) getAuditEvents(c *gin.Context) { id, ok := pathID(c) if !ok { return } hours, _ := strconv.Atoi(c.DefaultQuery("hours", "24")) q := service.AuditQuery{ Region: c.Query("region"), Hours: hours, Start: c.Query("start"), End: c.Query("end"), Page: c.Query("page"), } result, err := h.svc.AuditEvents(c.Request.Context(), id, q) if errors.Is(err, service.ErrInvalidAuditHours) || errors.Is(err, service.ErrInvalidAuditWindow) { 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,前端降级展示精简字段。 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"` } func (h *ociConfigHandler) listTenantUsers(c *gin.Context) { id, ok := pathID(c) if !ok { return } users, err := h.svc.TenantUsers(c.Request.Context(), id) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, users) } func (h *ociConfigHandler) getTenantUserDetail(c *gin.Context) { id, ok := pathID(c) if !ok { return } detail, err := h.svc.TenantUserDetail(c.Request.Context(), id, c.Param("userId")) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, detail) } 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, 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"` } 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.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) } func (h *ociConfigHandler) deleteTenantUser(c *gin.Context) { id, ok := pathID(c) if !ok { return } if err := h.svc.DeleteTenantUser(c.Request.Context(), id, c.Param("userId")); err != nil { respondError(c, err) return } c.Status(http.StatusNoContent) } func (h *ociConfigHandler) resetTenantUserPassword(c *gin.Context) { id, ok := pathID(c) if !ok { return } password, err := h.svc.ResetTenantUserPassword(c.Request.Context(), id, c.Param("userId")) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, gin.H{"password": password}) } func (h *ociConfigHandler) deleteTenantUserMfa(c *gin.Context) { id, ok := pathID(c) if !ok { return } deleted, err := h.svc.DeleteTenantUserMfa(c.Request.Context(), id, c.Param("userId")) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, gin.H{"deletedTotpDevices": deleted}) } 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}) } // ---- 域通知收件人与密码策略 ---- func (h *ociConfigHandler) getNotificationRecipients(c *gin.Context) { id, ok := pathID(c) if !ok { return } recipients, err := h.svc.NotificationRecipients(c.Request.Context(), id) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, recipients) } 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, req.Recipients) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, recipients) } func (h *ociConfigHandler) listPasswordPolicies(c *gin.Context) { id, ok := pathID(c) if !ok { return } policies, err := h.svc.PasswordPolicies(c.Request.Context(), id) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, policies) } 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.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) } func (h *ociConfigHandler) getIdentitySetting(c *gin.Context) { id, ok := pathID(c) if !ok { return } setting, err := h.svc.IdentitySetting(c.Request.Context(), id) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, setting) } 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, *req.PrimaryEmailRequired) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, setting) }