365 lines
9.4 KiB
Go
365 lines
9.4 KiB
Go
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)
|
|
}
|