package configmgmt import ( "encoding/json" "net/http" "strings" "ds2api/internal/config" ) func (h *Handler) configImport(w http.ResponseWriter, r *http.Request) { var req map[string]any if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "invalid json"}) return } mode := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("mode"))) if mode == "" { mode = strings.TrimSpace(strings.ToLower(fieldString(req, "mode"))) } if mode == "" { mode = "merge" } if mode != "merge" && mode != "replace" { writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "mode must be merge or replace"}) return } payload := req if raw, ok := req["config"].(map[string]any); ok && len(raw) > 0 { payload = raw } rawJSON, err := json.Marshal(payload) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "invalid config payload"}) return } var incoming config.Config if err := json.Unmarshal(rawJSON, &incoming); err != nil { writeJSON(w, http.StatusBadRequest, map[string]any{"detail": err.Error()}) return } incoming.ClearAccountTokens() importedKeys, importedAccounts := 0, 0 err = h.Store.Update(func(c *config.Config) error { next := c.Clone() if mode == "replace" { next = incoming.Clone() next.Accounts = normalizeAndDedupeAccounts(next.Accounts) next.VercelSyncHash = c.VercelSyncHash next.VercelSyncTime = c.VercelSyncTime importedKeys = len(next.APIKeys) importedAccounts = len(next.Accounts) } else { var changed int next.APIKeys, changed = mergeAPIKeysPreferStructured(next.APIKeys, incoming.APIKeys) importedKeys += changed existingAccounts := map[string]struct{}{} for _, acc := range next.Accounts { acc = normalizeAccountForStorage(acc) key := accountDedupeKey(acc) if key != "" { existingAccounts[key] = struct{}{} } } for _, acc := range incoming.Accounts { acc = normalizeAccountForStorage(acc) key := accountDedupeKey(acc) if key == "" { continue } if _, ok := existingAccounts[key]; ok { continue } existingAccounts[key] = struct{}{} next.Accounts = append(next.Accounts, acc) importedAccounts++ } if len(incoming.ModelAliases) > 0 { if next.ModelAliases == nil { next.ModelAliases = map[string]string{} } for k, v := range incoming.ModelAliases { next.ModelAliases[k] = v } } if incoming.Responses.StoreTTLSeconds > 0 { next.Responses.StoreTTLSeconds = incoming.Responses.StoreTTLSeconds } if strings.TrimSpace(incoming.Embeddings.Provider) != "" { next.Embeddings.Provider = incoming.Embeddings.Provider } incomingVercel := config.NormalizeVercelConfig(incoming.Vercel) if strings.TrimSpace(incomingVercel.Token) != "" || strings.TrimSpace(incomingVercel.ProjectID) != "" || strings.TrimSpace(incomingVercel.TeamID) != "" { next.Vercel = incomingVercel } if strings.TrimSpace(incoming.Admin.PasswordHash) != "" { next.Admin.PasswordHash = incoming.Admin.PasswordHash } if incoming.Admin.JWTExpireHours > 0 { next.Admin.JWTExpireHours = incoming.Admin.JWTExpireHours } if incoming.Admin.JWTValidAfterUnix > 0 { next.Admin.JWTValidAfterUnix = incoming.Admin.JWTValidAfterUnix } if incoming.Runtime.AccountMaxInflight > 0 { next.Runtime.AccountMaxInflight = incoming.Runtime.AccountMaxInflight } if incoming.Runtime.AccountMaxQueue > 0 { next.Runtime.AccountMaxQueue = incoming.Runtime.AccountMaxQueue } if incoming.Runtime.GlobalMaxInflight > 0 { next.Runtime.GlobalMaxInflight = incoming.Runtime.GlobalMaxInflight } if incoming.Runtime.TokenRefreshIntervalHours > 0 { next.Runtime.TokenRefreshIntervalHours = incoming.Runtime.TokenRefreshIntervalHours } } normalizeSettingsConfig(&next) if err := validateSettingsConfig(next); err != nil { return newRequestError(err.Error()) } *c = next return nil }) if err != nil { if detail, ok := requestErrorDetail(err); ok { writeJSON(w, http.StatusBadRequest, map[string]any{"detail": detail}) return } writeJSON(w, http.StatusInternalServerError, map[string]any{"detail": err.Error()}) return } h.Pool.Reset() writeJSON(w, http.StatusOK, map[string]any{ "success": true, "mode": mode, "imported_keys": importedKeys, "imported_accounts": importedAccounts, "message": "config imported", }) }