Files
oci-portal/internal/api/tenant.go
T
2026-07-09 19:18:04 +08:00

488 lines
14 KiB
Go

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} 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 {
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 {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 {
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。
//
// @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 {
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,前端降级展示精简字段。
//
// @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 {
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 租户 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 {
return
}
users, err := h.svc.TenantUsers(c.Request.Context(), id)
if err != nil {
respondError(c, err)
return
}
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 {
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)
}
// @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 {
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"`
}
// @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 {
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)
}
// @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 {
return
}
if err := h.svc.DeleteTenantUser(c.Request.Context(), id, c.Param("userId")); err != nil {
respondError(c, err)
return
}
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 {
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})
}
// @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 {
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})
}
// @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 {
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"
// @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 {
return
}
recipients, err := h.svc.NotificationRecipients(c.Request.Context(), id)
if err != nil {
respondError(c, err)
return
}
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 {
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)
}
// @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 {
return
}
policies, err := h.svc.PasswordPolicies(c.Request.Context(), id)
if err != nil {
respondError(c, err)
return
}
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 {
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)
}
// @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 {
return
}
setting, err := h.svc.IdentitySetting(c.Request.Context(), id)
if err != nil {
respondError(c, err)
return
}
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 {
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)
}