* feat(groups): 支持配置和选择密钥测试协议 * fix(groups): 完善测试模型选择并遵循渠道协议声明 * fix(groups): 修复禁用分组测试并统一测试设置布局 * fix(groups): 统一测试控件样式并复用模型输入
1310 lines
38 KiB
Go
1310 lines
38 KiB
Go
package control
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"gpt-load/internal/channel"
|
|
"gpt-load/internal/outboundproxy"
|
|
"gpt-load/internal/platform/config"
|
|
app_errors "gpt-load/internal/platform/errors"
|
|
"gpt-load/internal/platform/i18n"
|
|
"gpt-load/internal/platform/response"
|
|
"gpt-load/internal/platform/utils"
|
|
"gpt-load/internal/releasecheck"
|
|
"gpt-load/internal/subscription/providers/importfile"
|
|
subscriptionruntime "gpt-load/internal/subscription/runtime"
|
|
)
|
|
|
|
// ReleaseUpdateChecker is the control-plane on-demand view of the public release checker.
|
|
type ReleaseUpdateChecker interface {
|
|
Check(context.Context, bool) (*releasecheck.Update, error)
|
|
}
|
|
|
|
type Server struct {
|
|
authDigest [sha256.Size]byte
|
|
service *Service
|
|
systemInfo systemInfoResponse
|
|
authFailures *authFailureLimiter
|
|
compareDigest func([]byte, []byte) int
|
|
logger *logrus.Logger
|
|
authFailureEvents *utils.RateLimitedEventCounter
|
|
startedAt time.Time
|
|
now func() time.Time
|
|
releaseChecker ReleaseUpdateChecker
|
|
}
|
|
|
|
const maxControlJSONBodyBytes int64 = 32 << 20
|
|
const (
|
|
maxCredentialStageChannelIDBytes int64 = 128
|
|
maxCredentialStageGroupIDBytes int64 = 32
|
|
)
|
|
|
|
func NewServer(cfg *config.Config, service *Service) *Server {
|
|
now := time.Now
|
|
if service != nil && service.oauthCallback != nil {
|
|
service.oauthCallback.configureForServerHost(cfg.Server.Host)
|
|
}
|
|
return &Server{
|
|
authDigest: sha256.Sum256([]byte(cfg.AuthKey)),
|
|
service: service,
|
|
systemInfo: newSystemInfoResponse(cfg),
|
|
authFailures: newAuthFailureLimiter(),
|
|
compareDigest: subtle.ConstantTimeCompare,
|
|
logger: logrus.StandardLogger(),
|
|
startedAt: now().UTC(),
|
|
now: now,
|
|
authFailureEvents: utils.NewRateLimitedEventCounter(
|
|
time.Minute,
|
|
time.Now,
|
|
),
|
|
}
|
|
}
|
|
|
|
// NewServerWithReleaseUpdateChecker wires the on-demand public update checker.
|
|
func NewServerWithReleaseUpdateChecker(
|
|
cfg *config.Config,
|
|
service *Service,
|
|
releaseChecker ReleaseUpdateChecker,
|
|
) *Server {
|
|
server := NewServer(cfg, service)
|
|
server.releaseChecker = releaseChecker
|
|
return server
|
|
}
|
|
|
|
func (s *Server) handleGetSettings(c *gin.Context) {
|
|
result, err := s.service.GetSettings(c.Request.Context())
|
|
if err != nil {
|
|
writeServiceError(c, "get_settings", err)
|
|
return
|
|
}
|
|
writeSettingsResponse(c, result)
|
|
}
|
|
|
|
type credentialAuthorizationRequest struct {
|
|
ChannelID string `json:"channel_id"`
|
|
Proxy *outboundproxy.Config `json:"proxy"`
|
|
GroupID *uint `json:"group_id"`
|
|
}
|
|
|
|
func (s *Server) handleBeginCredentialAuthorization(c *gin.Context) {
|
|
var request credentialAuthorizationRequest
|
|
if err := bindStrictJSON(c, &request); err != nil {
|
|
writeServiceError(c, "begin_credential_authorization", mapControlJSONError(err))
|
|
return
|
|
}
|
|
var result CredentialStageResult
|
|
var err error
|
|
switch {
|
|
case request.GroupID != nil && (*request.GroupID == 0 || request.Proxy != nil):
|
|
err = app_errors.ErrValidation
|
|
case request.GroupID != nil:
|
|
result, err = s.service.BeginGroupCredentialAuthorization(
|
|
c.Request.Context(), *request.GroupID, channel.ID(request.ChannelID),
|
|
)
|
|
default:
|
|
result, err = s.service.BeginCredentialAuthorization(
|
|
c.Request.Context(), channel.ID(request.ChannelID), request.Proxy,
|
|
)
|
|
}
|
|
if err != nil {
|
|
writeServiceError(c, "begin_credential_authorization", err)
|
|
return
|
|
}
|
|
if result.RedirectURI != "" && s.service.oauthCallback != nil {
|
|
if err := s.service.oauthCallback.EnsureStarted(subscriptionruntime.LocalCallbackSpec{RedirectURI: result.RedirectURI}); err != nil {
|
|
s.logger.WithField("event", "oauth.callback_start_failed").WithError(err).Warn("OAuth callback listener is unavailable; manual callback remains available")
|
|
}
|
|
}
|
|
setMutationResourceLocator(c, "credential-stage:"+result.StageID)
|
|
setSecretResponseHeaders(c)
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
type credentialAuthorizationCallbackRequest struct {
|
|
CallbackURL string `json:"callback_url"`
|
|
}
|
|
|
|
func (s *Server) handleCredentialAuthorizationCallback(c *gin.Context) {
|
|
var request credentialAuthorizationCallbackRequest
|
|
if err := bindStrictJSON(c, &request); err != nil {
|
|
writeServiceError(c, "complete_credential_authorization", mapControlJSONError(err))
|
|
return
|
|
}
|
|
stageID := strings.TrimSpace(c.Param("stage_id"))
|
|
result, err := s.service.CompleteCredentialAuthorizationCallback(
|
|
c.Request.Context(),
|
|
stageID,
|
|
request.CallbackURL,
|
|
)
|
|
if err != nil {
|
|
writeServiceError(c, "complete_credential_authorization", err)
|
|
return
|
|
}
|
|
setSecretResponseHeaders(c)
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handlePollCredentialDeviceAuthorization(c *gin.Context) {
|
|
result, err := s.service.PollCredentialDeviceAuthorization(
|
|
c.Request.Context(), strings.TrimSpace(c.Param("stage_id")),
|
|
)
|
|
if err != nil {
|
|
writeServiceError(c, "poll_credential_device_authorization", err)
|
|
return
|
|
}
|
|
setMutationResourceLocator(c, "credential-stage:"+result.StageID)
|
|
setSecretResponseHeaders(c)
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleImportCredentialStage(c *gin.Context) {
|
|
s.handleCredentialImport(c, false)
|
|
}
|
|
|
|
func (s *Server) handleImportCredentialBatch(c *gin.Context) {
|
|
s.handleCredentialImport(c, true)
|
|
}
|
|
|
|
func (s *Server) handleCredentialImport(c *gin.Context, batch bool) {
|
|
fileLimit := int64(maxOAuthFileBytes)
|
|
multipartOverhead := int64(256 * 1024)
|
|
if batch {
|
|
fileLimit = importfile.MaxFileBytes
|
|
// 每个文件部件预留 4 KiB,容纳 UTF-8 文件名、MIME 头和 boundary。
|
|
// 表单字段另有 256 KiB 余量;文件内容总量仍在读取后独立校验。
|
|
multipartOverhead += int64(importfile.MaxEntries) * 4 * 1024
|
|
}
|
|
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, fileLimit+multipartOverhead)
|
|
reader, err := c.Request.MultipartReader()
|
|
if err != nil {
|
|
writeServiceError(c, "import_credential_stage", app_errors.ErrOAuthFileInvalid)
|
|
return
|
|
}
|
|
var raw []byte
|
|
var files [][]byte
|
|
defer func() {
|
|
clear(raw)
|
|
for _, file := range files {
|
|
clear(file)
|
|
}
|
|
}()
|
|
var channelID channel.ID
|
|
var proxyConfig *outboundproxy.Config
|
|
var groupID uint
|
|
fileCount := 0
|
|
channelCount := 0
|
|
proxyCount := 0
|
|
groupCount := 0
|
|
preparedCount := 0
|
|
var preparedIDs []string
|
|
totalFileBytes := 0
|
|
for {
|
|
part, nextErr := reader.NextPart()
|
|
if errors.Is(nextErr, io.EOF) {
|
|
break
|
|
}
|
|
if nextErr != nil {
|
|
writeServiceError(c, "import_credential_stage", credentialImportReadError(nextErr))
|
|
return
|
|
}
|
|
switch {
|
|
case part.FormName() == "channel_id" && part.FileName() == "":
|
|
channelCount++
|
|
value, readErr := io.ReadAll(io.LimitReader(part, maxCredentialStageChannelIDBytes+1))
|
|
_ = part.Close()
|
|
if readErr != nil || int64(len(value)) > maxCredentialStageChannelIDBytes || channelCount != 1 {
|
|
writeServiceError(c, "import_credential_stage", credentialImportReadError(readErr))
|
|
return
|
|
}
|
|
channelID = channel.ID(strings.TrimSpace(string(value)))
|
|
case part.FormName() == "file" && part.FileName() != "":
|
|
fileCount++
|
|
if !batch && fileCount != 1 {
|
|
_ = part.Close()
|
|
writeServiceError(c, "import_credential_stage", app_errors.ErrOAuthFileInvalid)
|
|
return
|
|
}
|
|
if batch && fileCount > importfile.MaxEntries {
|
|
_ = part.Close()
|
|
writeServiceError(c, "import_credential_stage", credentialImportDocumentError(&importfile.Error{Code: "too_many_entries"}))
|
|
return
|
|
}
|
|
raw, err = io.ReadAll(io.LimitReader(part, fileLimit+1))
|
|
_ = part.Close()
|
|
if err != nil {
|
|
writeServiceError(c, "import_credential_stage", credentialImportReadError(err))
|
|
return
|
|
}
|
|
if batch {
|
|
totalFileBytes += len(raw)
|
|
files = append(files, raw)
|
|
if totalFileBytes > importfile.MaxFileBytes {
|
|
writeServiceError(c, "import_credential_stage", credentialImportDocumentError(&importfile.Error{Code: "file_too_large"}))
|
|
return
|
|
}
|
|
}
|
|
case batch && part.FormName() == "prepared_import_ids" && part.FileName() == "":
|
|
preparedCount++
|
|
value, readErr := io.ReadAll(io.LimitReader(part, 80*1024+1))
|
|
_ = part.Close()
|
|
if preparedCount != 1 || readErr != nil || len(value) > 80*1024 || json.Unmarshal(value, &preparedIDs) != nil || len(preparedIDs) > importfile.MaxEntries {
|
|
writeServiceError(c, "import_credential_stage", credentialImportReadError(readErr))
|
|
return
|
|
}
|
|
case part.FormName() == "proxy" && part.FileName() == "":
|
|
proxyCount++
|
|
value, readErr := io.ReadAll(io.LimitReader(part, 16*1024+1))
|
|
_ = part.Close()
|
|
if readErr != nil || len(value) > 16*1024 || proxyCount != 1 {
|
|
writeServiceError(c, "import_credential_stage", credentialImportReadError(readErr))
|
|
return
|
|
}
|
|
config, decodeErr := outboundproxy.Decode(string(value))
|
|
if decodeErr != nil || config.Mode == outboundproxy.ModeInherit {
|
|
writeServiceError(c, "import_credential_stage", app_errors.ErrOAuthFileInvalid)
|
|
return
|
|
}
|
|
proxyConfig = &config
|
|
case part.FormName() == "group_id" && part.FileName() == "":
|
|
groupCount++
|
|
value, readErr := io.ReadAll(io.LimitReader(part, maxCredentialStageGroupIDBytes+1))
|
|
_ = part.Close()
|
|
parsed, parseErr := parseCanonicalSafePlatformUint(strings.TrimSpace(string(value)))
|
|
if readErr != nil || int64(len(value)) > maxCredentialStageGroupIDBytes ||
|
|
groupCount != 1 || parseErr != nil || parsed == 0 {
|
|
writeServiceError(c, "import_credential_stage", credentialImportReadError(readErr))
|
|
return
|
|
}
|
|
groupID = parsed
|
|
default:
|
|
_ = part.Close()
|
|
writeServiceError(c, "import_credential_stage", app_errors.ErrOAuthFileInvalid)
|
|
return
|
|
}
|
|
}
|
|
if fileCount == 0 || channelCount != 1 || channelID == "" ||
|
|
(groupCount != 0 && proxyCount != 0) {
|
|
writeServiceError(c, "import_credential_stage", app_errors.ErrOAuthFileInvalid)
|
|
return
|
|
}
|
|
if batch {
|
|
result, importErr := s.service.ImportCredentialFiles(c.Request.Context(), channelID, files, groupID, proxyConfig, preparedIDs)
|
|
if importErr != nil {
|
|
writeServiceError(c, "import_credential_stage", importErr)
|
|
return
|
|
}
|
|
setSecretResponseHeaders(c)
|
|
response.SuccessI18n(c, "common.success", result)
|
|
return
|
|
}
|
|
var result CredentialStageResult
|
|
if groupCount == 1 {
|
|
result, err = s.service.ImportGroupCredentialStage(c.Request.Context(), groupID, channelID, raw)
|
|
} else {
|
|
result, err = s.service.ImportCredentialStage(c.Request.Context(), channelID, raw, proxyConfig)
|
|
}
|
|
if err != nil {
|
|
writeServiceError(c, "import_credential_stage", err)
|
|
return
|
|
}
|
|
setMutationResourceLocator(c, "credential-stage:"+result.StageID)
|
|
setSecretResponseHeaders(c)
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func credentialImportReadError(err error) *app_errors.APIError {
|
|
var maxBytesError *http.MaxBytesError
|
|
if errors.As(err, &maxBytesError) {
|
|
return app_errors.ErrRequestTooLarge
|
|
}
|
|
return app_errors.ErrOAuthFileInvalid
|
|
}
|
|
|
|
func (s *Server) handleGetCredentialStage(c *gin.Context) {
|
|
result, err := s.service.GetCredentialStage(c.Request.Context(), strings.TrimSpace(c.Param("stage_id")))
|
|
if err != nil {
|
|
writeServiceError(c, "get_credential_stage", err)
|
|
return
|
|
}
|
|
setSecretResponseHeaders(c)
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleCancelCredentialStage(c *gin.Context) {
|
|
if err := s.service.CancelCredentialStage(c.Request.Context(), strings.TrimSpace(c.Param("stage_id"))); err != nil {
|
|
writeServiceError(c, "cancel_credential_stage", err)
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "common.success", nil)
|
|
}
|
|
|
|
func (s *Server) handleUpdateSettings(c *gin.Context) {
|
|
var request SettingsUpdateRequest
|
|
if err := bindStrictJSON(c, &request); err != nil {
|
|
writeServiceError(c, "update_settings", mapControlJSONError(err))
|
|
return
|
|
}
|
|
result, err := s.service.UpdateSettings(c.Request.Context(), request)
|
|
if err != nil {
|
|
writeServiceError(c, "update_settings", err)
|
|
return
|
|
}
|
|
writeSettingsResponse(c, result)
|
|
}
|
|
|
|
func (s *Server) handleGetGroupSummary(c *gin.Context) {
|
|
id, ok := groupID(c, "get_group_summary")
|
|
if !ok {
|
|
return
|
|
}
|
|
result, err := s.service.GetGroupSummary(c.Request.Context(), id)
|
|
if err != nil {
|
|
writeServiceError(c, "get_group_summary", err)
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleGetGroupSettings(c *gin.Context) {
|
|
id, ok := groupID(c, "get_group_settings")
|
|
if !ok {
|
|
return
|
|
}
|
|
result, err := s.service.GetGroupSettings(c.Request.Context(), id)
|
|
if err != nil {
|
|
writeServiceError(c, "get_group_settings", err)
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleCreateGroup(c *gin.Context) {
|
|
idempotencyKey, ok := requiredIdempotencyKey(c, "create_group")
|
|
if !ok {
|
|
return
|
|
}
|
|
var request GroupCreateRequest
|
|
if err := bindStrictJSON(c, &request); err != nil {
|
|
writeServiceError(c, "create_group", mapControlJSONError(err))
|
|
return
|
|
}
|
|
result, err := s.service.CreateGroupIdempotent(
|
|
c.Request.Context(),
|
|
idempotencyKey,
|
|
request,
|
|
)
|
|
if err != nil {
|
|
writeServiceError(c, "create_group", err)
|
|
return
|
|
}
|
|
setMutationResourceLocator(
|
|
c,
|
|
fmt.Sprintf("group:%d", result.GroupID),
|
|
)
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleUpdateGroupSettings(c *gin.Context) {
|
|
id, ok := groupID(c, "update_group_settings")
|
|
if !ok {
|
|
return
|
|
}
|
|
var request GroupSettingsUpdateRequest
|
|
if err := bindStrictJSON(c, &request); err != nil {
|
|
writeServiceError(c, "update_group_settings", mapControlJSONError(err))
|
|
return
|
|
}
|
|
result, err := s.service.UpdateGroupSettings(c.Request.Context(), id, request)
|
|
if err != nil {
|
|
writeServiceError(c, "update_group_settings", err)
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleUpdateGroupModels(c *gin.Context) {
|
|
id, ok := groupID(c, "update_group_models")
|
|
if !ok {
|
|
return
|
|
}
|
|
var request GroupModelsUpdateRequest
|
|
if err := bindStrictJSON(c, &request); err != nil {
|
|
writeServiceError(c, "update_group_models", mapControlJSONError(err))
|
|
return
|
|
}
|
|
result, err := s.service.UpdateGroupModels(c.Request.Context(), id, request)
|
|
if err != nil {
|
|
writeServiceError(c, "update_group_models", err)
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleGetGroupModels(c *gin.Context) {
|
|
id, ok := groupID(c, "get_group_models")
|
|
if !ok {
|
|
return
|
|
}
|
|
result, err := s.service.GetGroupModels(c.Request.Context(), id)
|
|
if err != nil {
|
|
writeServiceError(c, "get_group_models", err)
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleDeleteGroup(c *gin.Context) {
|
|
id, ok := groupID(c, "delete_group")
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := s.service.DeleteGroup(c.Request.Context(), id); err != nil {
|
|
writeServiceError(c, "delete_group", err)
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "common.success", nil)
|
|
}
|
|
|
|
func (s *Server) handleListGroupCredentials(c *gin.Context) {
|
|
id, ok := groupID(c, "list_group_credentials")
|
|
if !ok {
|
|
return
|
|
}
|
|
query, apiErr := parseCredentialCollectionQuery(c.Request.URL.RawQuery)
|
|
if apiErr != nil {
|
|
writeServiceError(c, "list_group_credentials", apiErr)
|
|
return
|
|
}
|
|
result, err := s.service.ListGroupCredentials(c.Request.Context(), id, query)
|
|
if err != nil {
|
|
writeServiceError(c, "list_group_credentials", err)
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleGetGroupCredential(c *gin.Context) {
|
|
groupID, ok := groupID(c, "get_group_credential")
|
|
if !ok {
|
|
return
|
|
}
|
|
credentialID, ok := credentialID(c, "get_group_credential")
|
|
if !ok {
|
|
return
|
|
}
|
|
result, err := s.service.GetCredentialDetail(c.Request.Context(), groupID, credentialID)
|
|
if err != nil {
|
|
writeServiceError(c, "get_group_credential", err)
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleRefreshGroupCredentialObservation(c *gin.Context) {
|
|
groupID, ok := groupID(c, "refresh_group_credential_observation")
|
|
if !ok {
|
|
return
|
|
}
|
|
credentialID, ok := credentialID(c, "refresh_group_credential_observation")
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := bindOptionalEmptyJSONObject(c); err != nil {
|
|
writeServiceError(c, "refresh_group_credential_observation", mapControlJSONError(err))
|
|
return
|
|
}
|
|
result, err := s.service.RefreshCredentialObservation(c.Request.Context(), groupID, credentialID)
|
|
if err != nil {
|
|
writeServiceError(c, "refresh_group_credential_observation", err)
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleRefreshGroupCredential(c *gin.Context) {
|
|
groupID, ok := groupID(c, "refresh_group_credential")
|
|
if !ok {
|
|
return
|
|
}
|
|
credentialID, ok := credentialID(c, "refresh_group_credential")
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := bindOptionalEmptyJSONObject(c); err != nil {
|
|
writeServiceError(c, "refresh_group_credential", mapControlJSONError(err))
|
|
return
|
|
}
|
|
result, err := s.service.RefreshGroupCredential(c.Request.Context(), groupID, credentialID)
|
|
if err != nil {
|
|
writeServiceError(c, "refresh_group_credential", err)
|
|
return
|
|
}
|
|
setSecretResponseHeaders(c)
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleConsumeGroupCredentialResetCredit(c *gin.Context) {
|
|
groupID, ok := groupID(c, "consume_group_credential_reset_credit")
|
|
if !ok {
|
|
return
|
|
}
|
|
credentialID, ok := credentialID(c, "consume_group_credential_reset_credit")
|
|
if !ok {
|
|
return
|
|
}
|
|
idempotencyKey, ok := requiredIdempotencyKey(c, "consume_group_credential_reset_credit")
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := bindOptionalEmptyJSONObject(c); err != nil {
|
|
writeServiceError(c, "consume_group_credential_reset_credit", mapControlJSONError(err))
|
|
return
|
|
}
|
|
result, err := s.service.ConsumeCredentialResetCredit(
|
|
c.Request.Context(),
|
|
groupID,
|
|
credentialID,
|
|
idempotencyKey,
|
|
)
|
|
if err != nil {
|
|
writeServiceError(c, "consume_group_credential_reset_credit", err)
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleRevealGroupCredential(c *gin.Context) {
|
|
groupID, ok := groupID(c, "reveal_group_credential")
|
|
if !ok {
|
|
return
|
|
}
|
|
credentialID, ok := credentialID(c, "reveal_group_credential")
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := bindOptionalEmptyJSONObject(c); err != nil {
|
|
writeServiceError(c, "reveal_group_credential", mapControlJSONError(err))
|
|
return
|
|
}
|
|
result, err := s.service.RevealGroupCredential(c.Request.Context(), groupID, credentialID)
|
|
if err != nil {
|
|
writeServiceError(c, "reveal_group_credential", err)
|
|
return
|
|
}
|
|
setSecretResponseHeaders(c)
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleDownloadGroupCredential(c *gin.Context) {
|
|
groupID, ok := groupID(c, "download_group_credential")
|
|
if !ok {
|
|
return
|
|
}
|
|
credentialID, ok := credentialID(c, "download_group_credential")
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := bindOptionalEmptyJSONObject(c); err != nil {
|
|
writeServiceError(c, "download_group_credential", mapControlJSONError(err))
|
|
return
|
|
}
|
|
result, err := s.service.DownloadGroupCredential(c.Request.Context(), groupID, credentialID)
|
|
if err != nil {
|
|
writeServiceError(c, "download_group_credential", err)
|
|
return
|
|
}
|
|
setSecretResponseHeaders(c)
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleDownloadAllGroupCredentials(c *gin.Context) {
|
|
groupID, ok := groupID(c, "download_all_group_credentials")
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := bindOptionalEmptyJSONObject(c); err != nil {
|
|
writeServiceError(c, "download_all_group_credentials", mapControlJSONError(err))
|
|
return
|
|
}
|
|
result, err := s.service.DownloadAllGroupCredentials(c.Request.Context(), groupID)
|
|
if err != nil {
|
|
writeServiceError(c, "download_all_group_credentials", err)
|
|
return
|
|
}
|
|
setSecretResponseHeaders(c)
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleUpdateGroupCredential(c *gin.Context) {
|
|
groupID, ok := groupID(c, "update_group_credential")
|
|
if !ok {
|
|
return
|
|
}
|
|
credentialID, ok := credentialID(c, "update_group_credential")
|
|
if !ok {
|
|
return
|
|
}
|
|
var request CredentialUpdateRequest
|
|
if err := bindStrictJSON(c, &request); err != nil {
|
|
writeServiceError(c, "update_group_credential", mapControlJSONError(err))
|
|
return
|
|
}
|
|
result, err := s.service.UpdateGroupCredential(c.Request.Context(), groupID, credentialID, request)
|
|
if err != nil {
|
|
writeServiceError(c, "update_group_credential", err)
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleDeleteGroupCredential(c *gin.Context) {
|
|
groupID, ok := groupID(c, "delete_group_credential")
|
|
if !ok {
|
|
return
|
|
}
|
|
credentialID, ok := credentialID(c, "delete_group_credential")
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := bindOptionalEmptyJSONObject(c); err != nil {
|
|
writeServiceError(c, "delete_group_credential", mapControlJSONError(err))
|
|
return
|
|
}
|
|
if err := s.service.DeleteGroupCredential(c.Request.Context(), groupID, credentialID); err != nil {
|
|
writeServiceError(c, "delete_group_credential", err)
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "common.success", nil)
|
|
}
|
|
|
|
func (s *Server) handleRestoreGroupCredential(c *gin.Context) {
|
|
groupID, ok := groupID(c, "restore_group_credential")
|
|
if !ok {
|
|
return
|
|
}
|
|
credentialID, ok := credentialID(c, "restore_group_credential")
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := bindOptionalEmptyJSONObject(c); err != nil {
|
|
writeServiceError(c, "restore_group_credential", mapControlJSONError(err))
|
|
return
|
|
}
|
|
result, err := s.service.RestoreGroupCredential(c.Request.Context(), groupID, credentialID)
|
|
if err != nil {
|
|
writeServiceError(c, "restore_group_credential", err)
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleTestGroupCredential(c *gin.Context) {
|
|
groupID, ok := groupID(c, "test_group_credential")
|
|
if !ok {
|
|
return
|
|
}
|
|
credentialID, ok := credentialID(c, "test_group_credential")
|
|
if !ok {
|
|
return
|
|
}
|
|
var request CredentialProbeRequest
|
|
if err := bindOptionalProbeJSON(c, &request); err != nil {
|
|
writeServiceError(c, "test_group_credential", mapControlJSONError(err))
|
|
return
|
|
}
|
|
result, err := s.service.TestGroupCredential(c.Request.Context(), groupID, credentialID, request)
|
|
if err != nil {
|
|
writeServiceError(c, "test_group_credential", err)
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleRestoreTestedGroupCredential(c *gin.Context) {
|
|
groupID, ok := groupID(c, "restore_tested_group_credential")
|
|
if !ok {
|
|
return
|
|
}
|
|
credentialID, ok := credentialID(c, "restore_tested_group_credential")
|
|
if !ok {
|
|
return
|
|
}
|
|
var request CredentialProbeRestoreRequest
|
|
if err := bindStrictJSON(c, &request); err != nil {
|
|
writeServiceError(c, "restore_tested_group_credential", mapControlJSONError(err))
|
|
return
|
|
}
|
|
result, err := s.service.RestoreTestedGroupCredential(
|
|
c.Request.Context(),
|
|
groupID,
|
|
credentialID,
|
|
request.RestoreProof,
|
|
)
|
|
if err != nil {
|
|
writeServiceError(c, "restore_tested_group_credential", err)
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleBatchGroupCredentials(c *gin.Context) {
|
|
groupID, ok := groupID(c, "batch_group_credentials")
|
|
if !ok {
|
|
return
|
|
}
|
|
var request CredentialBatchRequest
|
|
if err := bindStrictJSON(c, &request); err != nil {
|
|
writeServiceError(c, "batch_group_credentials", mapControlJSONError(err))
|
|
return
|
|
}
|
|
result, err := s.service.BatchGroupCredentials(c.Request.Context(), groupID, request)
|
|
if err != nil {
|
|
writeServiceError(c, "batch_group_credentials", err)
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleImportGroupCredentials(c *gin.Context) {
|
|
id, ok := groupID(c, "import_group_credentials")
|
|
if !ok {
|
|
return
|
|
}
|
|
idempotencyKey, ok := requiredIdempotencyKey(c, "import_group_credentials")
|
|
if !ok {
|
|
return
|
|
}
|
|
var request CredentialImportRequest
|
|
if err := bindStrictJSON(c, &request); err != nil {
|
|
writeServiceError(c, "import_group_credentials", mapControlJSONError(err))
|
|
return
|
|
}
|
|
result, err := s.service.ImportGroupCredentialsIdempotent(
|
|
c.Request.Context(), idempotencyKey, id, request,
|
|
)
|
|
if err != nil {
|
|
writeServiceError(c, "import_group_credentials", err)
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleConnectGroupCredentials(c *gin.Context) {
|
|
id, ok := groupID(c, "connect_group_credentials")
|
|
if !ok {
|
|
return
|
|
}
|
|
idempotencyKey, ok := requiredIdempotencyKey(c, "connect_group_credentials")
|
|
if !ok {
|
|
return
|
|
}
|
|
var request CredentialConnectRequest
|
|
if err := bindStrictJSON(c, &request); err != nil {
|
|
writeServiceError(c, "connect_group_credentials", mapControlJSONError(err))
|
|
return
|
|
}
|
|
result, err := s.service.ConnectGroupCredentialsIdempotent(
|
|
c.Request.Context(), idempotencyKey, id, request.StagedCredentialIDs,
|
|
)
|
|
if err != nil {
|
|
writeServiceError(c, "connect_group_credentials", err)
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleInspectGroupCredentialConnection(c *gin.Context) {
|
|
id, ok := groupID(c, "inspect_group_credential_connection")
|
|
if !ok {
|
|
return
|
|
}
|
|
var request CredentialConnectRequest
|
|
if err := bindStrictJSON(c, &request); err != nil {
|
|
writeServiceError(c, "inspect_group_credential_connection", mapControlJSONError(err))
|
|
return
|
|
}
|
|
result, err := s.service.InspectGroupCredentialConnection(
|
|
c.Request.Context(), id, request.StagedCredentialIDs,
|
|
)
|
|
if err != nil {
|
|
writeServiceError(c, "inspect_group_credential_connection", err)
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleDiscoverGroupModels(c *gin.Context) {
|
|
id, ok := groupID(c, "discover_group_models")
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := bindOptionalEmptyJSONObject(c); err != nil {
|
|
writeServiceError(c, "discover_group_models", mapControlJSONError(err))
|
|
return
|
|
}
|
|
result, err := s.service.DiscoverGroupModels(c.Request.Context(), id)
|
|
if err != nil {
|
|
writeServiceError(c, "discover_group_models", err)
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleDiscoverModels(c *gin.Context) {
|
|
var request ModelDiscoveryRequest
|
|
if err := bindStrictJSON(c, &request); err != nil {
|
|
writeDiscoveryError(c, mapControlJSONError(err))
|
|
return
|
|
}
|
|
result, err := s.service.DiscoverModels(c.Request.Context(), request)
|
|
if err != nil {
|
|
writeDiscoveryError(c, err)
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleCreateAccessKey(c *gin.Context) {
|
|
idempotencyKey, ok := requiredIdempotencyKey(c, "create_access_key")
|
|
if !ok {
|
|
return
|
|
}
|
|
var request AccessKeyCreateRequest
|
|
if err := bindStrictJSON(c, &request); err != nil {
|
|
writeServiceError(c, "create_access_key", mapControlJSONError(err))
|
|
return
|
|
}
|
|
if request.Key != "" {
|
|
digest := sha256.Sum256([]byte(request.Key))
|
|
if s.compareDigest(digest[:], s.authDigest[:]) == 1 {
|
|
writeServiceError(c, "create_access_key", app_errors.ErrAccessKeyAdminConflict)
|
|
return
|
|
}
|
|
}
|
|
result, err := s.service.CreateAccessKeyIdempotent(
|
|
c.Request.Context(),
|
|
idempotencyKey,
|
|
request,
|
|
)
|
|
if err != nil {
|
|
writeServiceError(c, "create_access_key", err)
|
|
return
|
|
}
|
|
setMutationResourceLocator(
|
|
c,
|
|
fmt.Sprintf("access-key:%d", result.ID),
|
|
)
|
|
setSecretResponseHeaders(c)
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleListAccessKeyOptions(c *gin.Context) {
|
|
result, err := s.service.ListAccessKeyOptions(c.Request.Context())
|
|
if err != nil {
|
|
writeServiceError(c, "list_access_key_options", err)
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleRevealAccessKey(c *gin.Context) {
|
|
id, ok := accessKeyID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
result, err := s.service.RevealAccessKey(c.Request.Context(), id)
|
|
if err != nil {
|
|
writeServiceError(c, "reveal_access_key", err)
|
|
return
|
|
}
|
|
setSecretResponseHeaders(c)
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleRotateAccessKey(c *gin.Context) {
|
|
id, ok := accessKeyID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
idempotencyKey, ok := requiredIdempotencyKey(c, "rotate_access_key")
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := bindOptionalEmptyJSONObject(c); err != nil {
|
|
writeServiceError(c, "rotate_access_key", mapControlJSONError(err))
|
|
return
|
|
}
|
|
result, err := s.service.RotateAccessKeyIdempotent(
|
|
c.Request.Context(),
|
|
idempotencyKey,
|
|
id,
|
|
)
|
|
if err != nil {
|
|
writeServiceError(c, "rotate_access_key", err)
|
|
return
|
|
}
|
|
setMutationResourceLocator(c, fmt.Sprintf("access-key:%d", result.ID))
|
|
setSecretResponseHeaders(c)
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleUpdateAccessKey(c *gin.Context) {
|
|
id, ok := accessKeyID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var request AccessKeyUpdateRequest
|
|
if err := bindStrictJSON(c, &request); err != nil {
|
|
writeServiceError(c, "update_access_key", mapControlJSONError(err))
|
|
return
|
|
}
|
|
var result AccessKeyMetadata
|
|
var err error
|
|
if request.Key != "" {
|
|
idempotencyKey, ok := requiredIdempotencyKey(c, "update_access_key")
|
|
if !ok {
|
|
return
|
|
}
|
|
digest := sha256.Sum256([]byte(request.Key))
|
|
if s.compareDigest(digest[:], s.authDigest[:]) == 1 {
|
|
writeServiceError(c, "update_access_key", app_errors.ErrAccessKeyAdminConflict)
|
|
return
|
|
}
|
|
result, err = s.service.UpdateAccessKeyIdempotent(c.Request.Context(), idempotencyKey, id, request)
|
|
} else {
|
|
result, err = s.service.UpdateAccessKey(c.Request.Context(), id, request)
|
|
}
|
|
if err != nil {
|
|
writeServiceError(c, "update_access_key", err)
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "common.success", result)
|
|
}
|
|
|
|
func (s *Server) handleResetAccessKeyCostLimits(c *gin.Context) {
|
|
id, ok := accessKeyID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var request AccessKeyCostLimitResetRequest
|
|
if err := bindStrictJSON(c, &request); err != nil {
|
|
writeServiceError(c, "reset_access_key_cost_limits", mapControlJSONError(err))
|
|
return
|
|
}
|
|
if err := s.service.ResetAccessKeyCostLimitRules(
|
|
c.Request.Context(),
|
|
id,
|
|
request.RuleIDs,
|
|
); err != nil {
|
|
writeServiceError(c, "reset_access_key_cost_limits", err)
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "common.success", nil)
|
|
}
|
|
|
|
func (s *Server) handleDeleteAccessKey(c *gin.Context) {
|
|
id, ok := accessKeyID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := s.service.DeleteAccessKey(c.Request.Context(), id); err != nil {
|
|
writeServiceError(c, "delete_access_key", err)
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "common.success", nil)
|
|
}
|
|
|
|
func bindStrictJSON(c *gin.Context, target any) error {
|
|
if c.Request.ContentLength > maxControlJSONBodyBytes {
|
|
return &http.MaxBytesError{Limit: maxControlJSONBodyBytes}
|
|
}
|
|
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxControlJSONBodyBytes)
|
|
raw, err := io.ReadAll(c.Request.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return decodeStrictControlJSONObject(raw, target)
|
|
}
|
|
|
|
func bindOptionalEmptyJSONObject(c *gin.Context) error {
|
|
decoder, err := newControlJSONDecoder(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var object map[string]json.RawMessage
|
|
if err := decoder.Decode(&object); err != nil {
|
|
if errors.Is(err, io.EOF) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
if object == nil || len(object) != 0 {
|
|
return fmt.Errorf("request body must be an empty JSON object")
|
|
}
|
|
var extra json.RawMessage
|
|
if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
|
|
if err == nil {
|
|
return fmt.Errorf("decode JSON request: multiple values")
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func newControlJSONDecoder(c *gin.Context) (*json.Decoder, error) {
|
|
if c.Request.ContentLength > maxControlJSONBodyBytes {
|
|
return nil, &http.MaxBytesError{Limit: maxControlJSONBodyBytes}
|
|
}
|
|
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxControlJSONBodyBytes)
|
|
return json.NewDecoder(c.Request.Body), nil
|
|
}
|
|
|
|
func mapControlJSONError(err error) *app_errors.APIError {
|
|
if errors.Is(err, app_errors.ErrValidation) {
|
|
return app_errors.ErrValidation
|
|
}
|
|
var maxBytesError *http.MaxBytesError
|
|
if errors.As(err, &maxBytesError) {
|
|
return app_errors.ErrRequestTooLarge
|
|
}
|
|
return app_errors.ErrInvalidJSON
|
|
}
|
|
|
|
func requiredIdempotencyKey(
|
|
c *gin.Context,
|
|
operation string,
|
|
) (string, bool) {
|
|
values := c.Request.Header.Values("Idempotency-Key")
|
|
if len(values) == 0 || len(values) == 1 && values[0] == "" {
|
|
writeServiceError(c, operation, app_errors.ErrIdempotencyKeyRequired)
|
|
return "", false
|
|
}
|
|
if len(values) != 1 || validateIdempotencyKey(values[0]) != nil {
|
|
writeServiceError(c, operation, app_errors.ErrInvalidIdempotencyKey)
|
|
return "", false
|
|
}
|
|
return values[0], true
|
|
}
|
|
|
|
func writeSettingsResponse(c *gin.Context, settings SettingsResponse) {
|
|
c.Header("Cache-Control", "no-store")
|
|
c.Header("Pragma", "no-cache")
|
|
c.Header("Content-Language", i18n.GetLanguageFromContext(c))
|
|
c.Header("Vary", "Accept-Language")
|
|
response.SuccessI18n(c, "common.success", settings)
|
|
}
|
|
|
|
func setSecretResponseHeaders(c *gin.Context) {
|
|
c.Header("Cache-Control", "no-store")
|
|
c.Header("Pragma", "no-cache")
|
|
}
|
|
|
|
func accessKeyID(c *gin.Context) (uint, bool) {
|
|
parsed, err := strconv.ParseUint(c.Param("id"), 10, strconv.IntSize)
|
|
if err != nil || parsed == 0 {
|
|
writeServiceError(c, "access_key_id", app_errors.ErrBadRequest)
|
|
return 0, false
|
|
}
|
|
return uint(parsed), true
|
|
}
|
|
|
|
func groupID(c *gin.Context, operation string) (uint, bool) {
|
|
parsed, err := strconv.ParseUint(c.Param("group_id"), 10, strconv.IntSize)
|
|
if err != nil || parsed == 0 {
|
|
writeServiceError(c, operation, app_errors.ErrBadRequest)
|
|
return 0, false
|
|
}
|
|
return uint(parsed), true
|
|
}
|
|
|
|
func credentialID(c *gin.Context, operation string) (uint, bool) {
|
|
parsed, err := strconv.ParseUint(c.Param("credential_id"), 10, strconv.IntSize)
|
|
if err != nil || parsed == 0 {
|
|
writeServiceError(c, operation, app_errors.ErrBadRequest)
|
|
return 0, false
|
|
}
|
|
return uint(parsed), true
|
|
}
|
|
|
|
func writeServiceError(c *gin.Context, operation string, err error) {
|
|
writeServiceErrorResponse(c, operation, err)
|
|
}
|
|
|
|
func writeDiscoveryError(c *gin.Context, err error) {
|
|
writeServiceErrorResponse(c, "discover_models", err)
|
|
}
|
|
|
|
func writeServiceErrorResponse(
|
|
c *gin.Context,
|
|
operation string,
|
|
err error,
|
|
) {
|
|
if requestWasCanceled(c.Request.Context(), err) {
|
|
return
|
|
}
|
|
|
|
var apiErr *app_errors.APIError
|
|
if errors.As(err, &apiErr) {
|
|
setMutationErrorCode(c, apiErr.Code)
|
|
if apiErr.HTTPStatus >= http.StatusInternalServerError {
|
|
logServiceError(operation, err, apiErr.Code)
|
|
}
|
|
response.ErrorI18nFromAPIError(
|
|
c,
|
|
apiErr,
|
|
serviceErrorMessageID(operation, err, apiErr),
|
|
)
|
|
return
|
|
}
|
|
|
|
setMutationErrorCode(c, app_errors.ErrInternalServer.Code)
|
|
logServiceError(operation, err, app_errors.ErrInternalServer.Code)
|
|
response.ErrorI18nFromAPIError(c, app_errors.ErrInternalServer, "internal_error")
|
|
}
|
|
|
|
func requestWasCanceled(ctx context.Context, err error) bool {
|
|
return ctx != nil &&
|
|
errors.Is(ctx.Err(), context.Canceled) &&
|
|
errors.Is(err, context.Canceled)
|
|
}
|
|
|
|
func serviceErrorMessageID(
|
|
operation string,
|
|
err error,
|
|
apiErr *app_errors.APIError,
|
|
) string {
|
|
switch apiErr.Code {
|
|
case app_errors.ErrIdempotencyKeyRequired.Code:
|
|
return "idempotency.required"
|
|
case app_errors.ErrInvalidIdempotencyKey.Code:
|
|
return "idempotency.invalid"
|
|
case app_errors.ErrIdempotencyKeyReused.Code:
|
|
return "idempotency.reused"
|
|
case app_errors.ErrIdempotencyResultExpired.Code:
|
|
return "idempotency.expired"
|
|
case app_errors.ErrControlOperationIncomplete.Code:
|
|
return "control.operation_incomplete"
|
|
case app_errors.ErrControlRecoveryPending.Code:
|
|
return "control.recovery_pending"
|
|
case app_errors.ErrResetCreditUnavailable.Code:
|
|
return "reset_credit.unavailable"
|
|
case app_errors.ErrResetCreditRejected.Code:
|
|
return "reset_credit.rejected"
|
|
case app_errors.ErrResetCreditOutcomeUnknown.Code:
|
|
return "reset_credit.outcome_unknown"
|
|
case app_errors.ErrModelPriceUnpricedConfirmationRequired.Code:
|
|
return "model_price.unpriced_confirmation_required"
|
|
case app_errors.ErrModelPriceReferenced.Code:
|
|
return "model_price.referenced"
|
|
case app_errors.ErrModelPriceAutomaticDeleteForbidden.Code:
|
|
return "model_price.automatic_delete_forbidden"
|
|
case app_errors.ErrRequestTooLarge.Code:
|
|
return "request_too_large"
|
|
case app_errors.ErrBadRequest.Code, app_errors.ErrInvalidJSON.Code, app_errors.ErrValidation.Code:
|
|
return "bad_request"
|
|
case app_errors.ErrResourceNotFound.Code:
|
|
switch operation {
|
|
case "update_model_price", "reset_model_price", "delete_model_price":
|
|
return "model_price.not_found"
|
|
}
|
|
var resourceErr *controlResourceNotFoundError
|
|
if errors.As(err, &resourceErr) {
|
|
if resourceErr.resource == "group" {
|
|
return "group.not_found"
|
|
}
|
|
return "credential.not_found"
|
|
}
|
|
switch operation {
|
|
case "list_groups", "get_group_summary", "get_group_settings", "get_group_models",
|
|
"update_group_settings", "delete_group",
|
|
"update_group_models", "import_group_credentials",
|
|
"discover_group_models", "list_group_credentials", "download_all_group_credentials":
|
|
return "group.not_found"
|
|
default:
|
|
return "credential.not_found"
|
|
}
|
|
case app_errors.ErrNoActiveCredential.Code:
|
|
return "group.no_active_credential"
|
|
case app_errors.ErrInvalidCustomAccessKey.Code:
|
|
return "access_key.custom_invalid"
|
|
case app_errors.ErrAccessKeyAdminConflict.Code:
|
|
return "access_key.admin_conflict"
|
|
case app_errors.ErrDuplicateResource.Code:
|
|
if operation == "create_access_key" || operation == "update_access_key" {
|
|
return "access_key.exists"
|
|
}
|
|
if operation == "create_group" || operation == "update_group_settings" {
|
|
return "group.name_exists"
|
|
}
|
|
return "bad_request"
|
|
case app_errors.ErrGroupInUse.Code:
|
|
return "group.in_use"
|
|
case app_errors.ErrChannelTargetConflict.Code:
|
|
return "group.channel_target_conflict"
|
|
case app_errors.ErrModelNameConflict.Code:
|
|
return "group.model_name_conflict"
|
|
case app_errors.ErrBadGateway.Code:
|
|
return "bad_gateway"
|
|
default:
|
|
return "internal_error"
|
|
}
|
|
}
|
|
|
|
func logServiceError(operation string, err error, code string) {
|
|
fields := logrus.Fields{
|
|
"operation": operation,
|
|
"error_code": code,
|
|
"error_type": fmt.Sprintf("%T", err),
|
|
}
|
|
var operationErr *controlOperationError
|
|
if errors.As(err, &operationErr) {
|
|
if operationErr.stage != "" {
|
|
fields["stage"] = operationErr.stage
|
|
}
|
|
if operationErr.mismatchKind != "" {
|
|
fields["mismatch_kind"] = operationErr.mismatchKind
|
|
}
|
|
if operationErr.groupID != 0 {
|
|
fields["group_id"] = operationErr.groupID
|
|
}
|
|
if operationErr.credentialID != 0 {
|
|
fields["credential_id"] = operationErr.credentialID
|
|
}
|
|
}
|
|
utils.LogPlaneBestEffort(
|
|
logrus.StandardLogger(),
|
|
logrus.ErrorLevel,
|
|
utils.LogPlaneControl,
|
|
fields,
|
|
"Operation failed",
|
|
)
|
|
}
|
|
|
|
func bindOptionalProbeJSON(c *gin.Context, target any) error {
|
|
if c.Request.ContentLength > maxControlJSONBodyBytes {
|
|
return &http.MaxBytesError{Limit: maxControlJSONBodyBytes}
|
|
}
|
|
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxControlJSONBodyBytes)
|
|
raw, err := io.ReadAll(c.Request.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if strings.TrimSpace(string(raw)) == "" {
|
|
return nil
|
|
}
|
|
return decodeStrictControlJSONObject(raw, target)
|
|
}
|