增加原子批量启用、禁用和删除接口及管理端多选操作,目标缺失时整批回滚。\n\n拆分管理端与 API Key 权限缓存并在弹窗保存后刷新候选;补齐失效规则一键清理样式、固定右侧操作列和 OpenAPI 契约。
438 lines
15 KiB
Go
438 lines
15 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
|
)
|
|
|
|
// batchGatewayUsers godoc
|
|
// @Summary 批量操作用户
|
|
// @Description 管理端原子批量启用、禁用或软删除最多 500 个用户;任一目标不存在时整批不变更。
|
|
// @Tags identity
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param input body store.IdentityBatchInput true "用户批量操作"
|
|
// @Success 200 {object} IdentityBatchResponse
|
|
// @Failure 400 {object} ErrorEnvelope
|
|
// @Failure 401 {object} ErrorEnvelope
|
|
// @Failure 403 {object} ErrorEnvelope
|
|
// @Failure 404 {object} ErrorEnvelope
|
|
// @Failure 500 {object} ErrorEnvelope
|
|
// @Router /api/admin/users/batch [post]
|
|
func (s *Server) batchGatewayUsers(w http.ResponseWriter, r *http.Request) {
|
|
var input store.IdentityBatchInput
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid json body")
|
|
return
|
|
}
|
|
result, err := s.store.BatchGatewayUsers(r.Context(), input)
|
|
if err != nil {
|
|
writeIdentityBatchError(w, s, err, "users")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, result)
|
|
}
|
|
|
|
// batchUserGroups godoc
|
|
// @Summary 批量操作用户组
|
|
// @Description 管理端原子批量启用、禁用或删除最多 500 个用户组;删除时同步删除其访问规则,关联默认用户组外键按数据库约束置空。
|
|
// @Tags identity
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param input body store.IdentityBatchInput true "用户组批量操作"
|
|
// @Success 200 {object} IdentityBatchResponse
|
|
// @Failure 400 {object} ErrorEnvelope
|
|
// @Failure 401 {object} ErrorEnvelope
|
|
// @Failure 403 {object} ErrorEnvelope
|
|
// @Failure 404 {object} ErrorEnvelope
|
|
// @Failure 500 {object} ErrorEnvelope
|
|
// @Router /api/admin/user-groups/batch [post]
|
|
func (s *Server) batchUserGroups(w http.ResponseWriter, r *http.Request) {
|
|
var input store.IdentityBatchInput
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid json body")
|
|
return
|
|
}
|
|
result, err := s.store.BatchUserGroups(r.Context(), input)
|
|
if err != nil {
|
|
writeIdentityBatchError(w, s, err, "user groups")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, result)
|
|
}
|
|
|
|
func writeIdentityBatchError(w http.ResponseWriter, s *Server, err error, target string) {
|
|
switch {
|
|
case errors.Is(err, store.ErrInvalidIdentityBatch):
|
|
writeError(w, http.StatusBadRequest, "action must be enable, disable or delete and ids must contain 1 to 500 UUIDs")
|
|
case errors.Is(err, store.ErrIdentityBatchTargetNotFound):
|
|
writeError(w, http.StatusNotFound, "one or more "+target+" were not found")
|
|
default:
|
|
s.logger.Error("batch identity operation failed", "target", target, "error", err)
|
|
writeError(w, http.StatusInternalServerError, "batch "+target+" operation failed")
|
|
}
|
|
}
|
|
|
|
// createTenant godoc
|
|
// @Summary 创建租户
|
|
// @Description 管理端创建网关租户,tenantKey 和 name 必填。
|
|
// @Tags identity
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param input body store.GatewayTenantInput true "租户请求"
|
|
// @Success 201 {object} store.GatewayTenant
|
|
// @Failure 400 {object} ErrorEnvelope
|
|
// @Failure 401 {object} ErrorEnvelope
|
|
// @Failure 403 {object} ErrorEnvelope
|
|
// @Failure 409 {object} ErrorEnvelope
|
|
// @Failure 500 {object} ErrorEnvelope
|
|
// @Router /api/admin/tenants [post]
|
|
func (s *Server) createTenant(w http.ResponseWriter, r *http.Request) {
|
|
var input store.GatewayTenantInput
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid json body")
|
|
return
|
|
}
|
|
if !validTenantInput(input) {
|
|
writeError(w, http.StatusBadRequest, "tenantKey and name are required")
|
|
return
|
|
}
|
|
item, err := s.store.CreateTenant(r.Context(), input)
|
|
if err != nil {
|
|
if store.IsUniqueViolation(err) {
|
|
writeError(w, http.StatusConflict, "tenant key or external tenant id already exists")
|
|
return
|
|
}
|
|
s.logger.Error("create tenant failed", "error", err)
|
|
writeError(w, http.StatusInternalServerError, "create tenant failed")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, item)
|
|
}
|
|
|
|
// updateTenant godoc
|
|
// @Summary 更新租户
|
|
// @Description 管理端更新网关租户信息。
|
|
// @Tags identity
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param tenantID path string true "租户 ID"
|
|
// @Param input body store.GatewayTenantInput true "租户请求"
|
|
// @Success 200 {object} store.GatewayTenant
|
|
// @Failure 400 {object} ErrorEnvelope
|
|
// @Failure 401 {object} ErrorEnvelope
|
|
// @Failure 403 {object} ErrorEnvelope
|
|
// @Failure 404 {object} ErrorEnvelope
|
|
// @Failure 409 {object} ErrorEnvelope
|
|
// @Failure 500 {object} ErrorEnvelope
|
|
// @Router /api/admin/tenants/{tenantID} [patch]
|
|
func (s *Server) updateTenant(w http.ResponseWriter, r *http.Request) {
|
|
var input store.GatewayTenantInput
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid json body")
|
|
return
|
|
}
|
|
if !validTenantInput(input) {
|
|
writeError(w, http.StatusBadRequest, "tenantKey and name are required")
|
|
return
|
|
}
|
|
item, err := s.store.UpdateTenant(r.Context(), r.PathValue("tenantID"), input)
|
|
if err != nil {
|
|
if store.IsNotFound(err) {
|
|
writeError(w, http.StatusNotFound, "tenant not found")
|
|
return
|
|
}
|
|
if store.IsUniqueViolation(err) {
|
|
writeError(w, http.StatusConflict, "tenant key or external tenant id already exists")
|
|
return
|
|
}
|
|
s.logger.Error("update tenant failed", "error", err)
|
|
writeError(w, http.StatusInternalServerError, "update tenant failed")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, item)
|
|
}
|
|
|
|
// deleteTenant godoc
|
|
// @Summary 删除租户
|
|
// @Description 管理端删除网关租户。
|
|
// @Tags identity
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param tenantID path string true "租户 ID"
|
|
// @Success 204 "No Content"
|
|
// @Failure 401 {object} ErrorEnvelope
|
|
// @Failure 403 {object} ErrorEnvelope
|
|
// @Failure 404 {object} ErrorEnvelope
|
|
// @Failure 500 {object} ErrorEnvelope
|
|
// @Router /api/admin/tenants/{tenantID} [delete]
|
|
func (s *Server) deleteTenant(w http.ResponseWriter, r *http.Request) {
|
|
if err := s.store.DeleteTenant(r.Context(), r.PathValue("tenantID")); err != nil {
|
|
if store.IsNotFound(err) {
|
|
writeError(w, http.StatusNotFound, "tenant not found")
|
|
return
|
|
}
|
|
s.logger.Error("delete tenant failed", "error", err)
|
|
writeError(w, http.StatusInternalServerError, "delete tenant failed")
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// createGatewayUser godoc
|
|
// @Summary 创建用户
|
|
// @Description 管理端创建网关用户;password 为空时不设置本地密码,非空时至少 8 位。
|
|
// @Tags identity
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param input body store.GatewayUserInput true "用户请求"
|
|
// @Success 201 {object} store.GatewayUser
|
|
// @Failure 400 {object} ErrorEnvelope
|
|
// @Failure 401 {object} ErrorEnvelope
|
|
// @Failure 403 {object} ErrorEnvelope
|
|
// @Failure 409 {object} ErrorEnvelope
|
|
// @Failure 500 {object} ErrorEnvelope
|
|
// @Router /api/admin/users [post]
|
|
func (s *Server) createGatewayUser(w http.ResponseWriter, r *http.Request) {
|
|
var input store.GatewayUserInput
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid json body")
|
|
return
|
|
}
|
|
if !validGatewayUserInput(input) {
|
|
writeError(w, http.StatusBadRequest, "username is required")
|
|
return
|
|
}
|
|
if !validOptionalPassword(input.Password) {
|
|
writeError(w, http.StatusBadRequest, store.ErrWeakPassword.Error())
|
|
return
|
|
}
|
|
item, err := s.store.CreateGatewayUser(r.Context(), input)
|
|
if err != nil {
|
|
if store.IsUniqueViolation(err) {
|
|
writeError(w, http.StatusConflict, "user key, email or external user id already exists")
|
|
return
|
|
}
|
|
s.logger.Error("create gateway user failed", "error", err)
|
|
writeError(w, http.StatusInternalServerError, "create gateway user failed")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, item)
|
|
}
|
|
|
|
// updateGatewayUser godoc
|
|
// @Summary 更新用户
|
|
// @Description 管理端更新网关用户资料、角色、默认用户组和可选本地密码。
|
|
// @Tags identity
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param userID path string true "用户 ID"
|
|
// @Param input body store.GatewayUserInput true "用户请求"
|
|
// @Success 200 {object} store.GatewayUser
|
|
// @Failure 400 {object} ErrorEnvelope
|
|
// @Failure 401 {object} ErrorEnvelope
|
|
// @Failure 403 {object} ErrorEnvelope
|
|
// @Failure 404 {object} ErrorEnvelope
|
|
// @Failure 409 {object} ErrorEnvelope
|
|
// @Failure 500 {object} ErrorEnvelope
|
|
// @Router /api/admin/users/{userID} [patch]
|
|
func (s *Server) updateGatewayUser(w http.ResponseWriter, r *http.Request) {
|
|
var input store.GatewayUserInput
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid json body")
|
|
return
|
|
}
|
|
if !validGatewayUserInput(input) {
|
|
writeError(w, http.StatusBadRequest, "username is required")
|
|
return
|
|
}
|
|
if !validOptionalPassword(input.Password) {
|
|
writeError(w, http.StatusBadRequest, store.ErrWeakPassword.Error())
|
|
return
|
|
}
|
|
item, err := s.store.UpdateGatewayUser(r.Context(), r.PathValue("userID"), input)
|
|
if err != nil {
|
|
if store.IsNotFound(err) {
|
|
writeError(w, http.StatusNotFound, "user not found")
|
|
return
|
|
}
|
|
if store.IsUniqueViolation(err) {
|
|
writeError(w, http.StatusConflict, "user key, email or external user id already exists")
|
|
return
|
|
}
|
|
s.logger.Error("update gateway user failed", "error", err)
|
|
writeError(w, http.StatusInternalServerError, "update gateway user failed")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, item)
|
|
}
|
|
|
|
// deleteGatewayUser godoc
|
|
// @Summary 删除用户
|
|
// @Description 管理端删除网关用户。
|
|
// @Tags identity
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param userID path string true "用户 ID"
|
|
// @Success 204 "No Content"
|
|
// @Failure 401 {object} ErrorEnvelope
|
|
// @Failure 403 {object} ErrorEnvelope
|
|
// @Failure 404 {object} ErrorEnvelope
|
|
// @Failure 500 {object} ErrorEnvelope
|
|
// @Router /api/admin/users/{userID} [delete]
|
|
func (s *Server) deleteGatewayUser(w http.ResponseWriter, r *http.Request) {
|
|
if err := s.store.DeleteGatewayUser(r.Context(), r.PathValue("userID")); err != nil {
|
|
if store.IsNotFound(err) {
|
|
writeError(w, http.StatusNotFound, "user not found")
|
|
return
|
|
}
|
|
s.logger.Error("delete gateway user failed", "error", err)
|
|
writeError(w, http.StatusInternalServerError, "delete gateway user failed")
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// createUserGroup godoc
|
|
// @Summary 创建用户组
|
|
// @Description 管理端创建用户组,可配置默认定价、运行策略、限流和配额策略。
|
|
// @Tags identity
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param input body store.UserGroupInput true "用户组请求"
|
|
// @Success 201 {object} store.UserGroup
|
|
// @Failure 400 {object} ErrorEnvelope
|
|
// @Failure 401 {object} ErrorEnvelope
|
|
// @Failure 403 {object} ErrorEnvelope
|
|
// @Failure 409 {object} ErrorEnvelope
|
|
// @Failure 500 {object} ErrorEnvelope
|
|
// @Router /api/admin/user-groups [post]
|
|
func (s *Server) createUserGroup(w http.ResponseWriter, r *http.Request) {
|
|
var input store.UserGroupInput
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid json body")
|
|
return
|
|
}
|
|
if !validUserGroupInput(input) {
|
|
writeError(w, http.StatusBadRequest, "groupKey and name are required")
|
|
return
|
|
}
|
|
item, err := s.store.CreateUserGroup(r.Context(), input)
|
|
if err != nil {
|
|
if store.IsInvalidRateLimitPolicy(err) {
|
|
writeError(w, http.StatusBadRequest, err.Error(), "invalid_parameter")
|
|
return
|
|
}
|
|
if store.IsUniqueViolation(err) {
|
|
writeError(w, http.StatusConflict, "user group key already exists")
|
|
return
|
|
}
|
|
s.logger.Error("create user group failed", "error", err)
|
|
writeError(w, http.StatusInternalServerError, "create user group failed")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, item)
|
|
}
|
|
|
|
// updateUserGroup godoc
|
|
// @Summary 更新用户组
|
|
// @Description 管理端更新用户组基础信息和策略配置。
|
|
// @Tags identity
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param groupID path string true "用户组 ID"
|
|
// @Param input body store.UserGroupInput true "用户组请求"
|
|
// @Success 200 {object} store.UserGroup
|
|
// @Failure 400 {object} ErrorEnvelope
|
|
// @Failure 401 {object} ErrorEnvelope
|
|
// @Failure 403 {object} ErrorEnvelope
|
|
// @Failure 404 {object} ErrorEnvelope
|
|
// @Failure 409 {object} ErrorEnvelope
|
|
// @Failure 500 {object} ErrorEnvelope
|
|
// @Router /api/admin/user-groups/{groupID} [patch]
|
|
func (s *Server) updateUserGroup(w http.ResponseWriter, r *http.Request) {
|
|
var input store.UserGroupInput
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid json body")
|
|
return
|
|
}
|
|
if !validUserGroupInput(input) {
|
|
writeError(w, http.StatusBadRequest, "groupKey and name are required")
|
|
return
|
|
}
|
|
item, err := s.store.UpdateUserGroup(r.Context(), r.PathValue("groupID"), input)
|
|
if err != nil {
|
|
if store.IsInvalidRateLimitPolicy(err) {
|
|
writeError(w, http.StatusBadRequest, err.Error(), "invalid_parameter")
|
|
return
|
|
}
|
|
if store.IsNotFound(err) {
|
|
writeError(w, http.StatusNotFound, "user group not found")
|
|
return
|
|
}
|
|
if store.IsUniqueViolation(err) {
|
|
writeError(w, http.StatusConflict, "user group key already exists")
|
|
return
|
|
}
|
|
s.logger.Error("update user group failed", "error", err)
|
|
writeError(w, http.StatusInternalServerError, "update user group failed")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, item)
|
|
}
|
|
|
|
// deleteUserGroup godoc
|
|
// @Summary 删除用户组
|
|
// @Description 管理端删除用户组。
|
|
// @Tags identity
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param groupID path string true "用户组 ID"
|
|
// @Success 204 "No Content"
|
|
// @Failure 401 {object} ErrorEnvelope
|
|
// @Failure 403 {object} ErrorEnvelope
|
|
// @Failure 404 {object} ErrorEnvelope
|
|
// @Failure 500 {object} ErrorEnvelope
|
|
// @Router /api/admin/user-groups/{groupID} [delete]
|
|
func (s *Server) deleteUserGroup(w http.ResponseWriter, r *http.Request) {
|
|
if err := s.store.DeleteUserGroup(r.Context(), r.PathValue("groupID")); err != nil {
|
|
if store.IsNotFound(err) {
|
|
writeError(w, http.StatusNotFound, "user group not found")
|
|
return
|
|
}
|
|
s.logger.Error("delete user group failed", "error", err)
|
|
writeError(w, http.StatusInternalServerError, "delete user group failed")
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func validTenantInput(input store.GatewayTenantInput) bool {
|
|
return strings.TrimSpace(input.TenantKey) != "" && strings.TrimSpace(input.Name) != ""
|
|
}
|
|
|
|
func validGatewayUserInput(input store.GatewayUserInput) bool {
|
|
return strings.TrimSpace(input.Username) != ""
|
|
}
|
|
|
|
func validOptionalPassword(password string) bool {
|
|
password = strings.TrimSpace(password)
|
|
return password == "" || len(password) >= 8
|
|
}
|
|
|
|
func validUserGroupInput(input store.UserGroupInput) bool {
|
|
return strings.TrimSpace(input.GroupKey) != "" && strings.TrimSpace(input.Name) != ""
|
|
}
|