package httpapi import ( "context" "encoding/json" "errors" "io" "knowledge-governance/internal/qaopsorg" "net/http" "net/url" "strings" ) type QAOrgSyncStore interface { GetSyncStatus(context.Context) (qaopsorg.SyncStatus, error) EnqueueSync(context.Context, string, string) (qaopsorg.SyncReceipt, bool, error) } func authorizeQAOrganizationWrite(w http.ResponseWriter, r *http.Request) bool { return authorizeQAIntent(w, r, "qa-organization") } func authorizeQAIntent(w http.ResponseWriter, r *http.Request, intent string) bool { if r.Header.Get("X-Governance-Action") != intent || r.Header.Get("Sec-Fetch-Site") == "cross-site" { writeJSON(w, 403, map[string]string{"error": "此操作需要同源操作意图"}) return false } // Fetch Metadata survives same-origin proxies that rewrite Host; untrusted // forwarded headers must not grant cross-origin write authority. if raw := r.Header.Get("Origin"); raw != "" && r.Header.Get("Sec-Fetch-Site") != "same-origin" { origin, err := url.Parse(raw) if err != nil || (origin.Scheme != "http" && origin.Scheme != "https") || origin.Host != r.Host { writeJSON(w, 403, map[string]string{"error": "拒绝跨站配置请求"}) return false } } return true } func qaOrganizationSync(w http.ResponseWriter, r *http.Request, c Config) { if !authorizeQAOps(w, r) { return } if r.URL.RawQuery != "" { writeOrgError(w, qaopsorg.ErrInvalid) return } w.Header().Set("Cache-Control", "no-store") if r.Method == http.MethodGet { if c.QAOrgSync == nil { writeJSON(w, 200, map[string]any{"data": qaopsorg.SyncStatus{}}) return } result, err := c.QAOrgSync.GetSyncStatus(r.Context()) if err != nil { writeOrgError(w, err) return } result.Configured = c.QAOrgSyncEnabled writeJSON(w, 200, map[string]any{"data": result}) return } if !authorizeQAOrganizationWrite(w, r) { return } key := r.Header.Get("Idempotency-Key") if key == "" || len(key) > 128 || strings.TrimSpace(key) != key { writeJSON(w, 400, map[string]string{"error": "同步请求需要有效 Idempotency-Key,重试须复用原标识"}) return } var body *struct{} decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1024)) decoder.DisallowUnknownFields() if decoder.Decode(&body) != nil || body == nil || decoder.Decode(&struct{}{}) != io.EOF { writeOrgError(w, qaopsorg.ErrInvalid) return } if !c.QAOrgSyncEnabled || c.QAOrgSync == nil { writeJSON(w, 503, map[string]string{"error": "通讯录后台同步未接线,未创建任务"}) return } identity, _ := sessionFrom(r.Context()) receipt, replayed, err := c.QAOrgSync.EnqueueSync(r.Context(), key, identity.Username) if err != nil { if errors.Is(err, qaopsorg.ErrSyncWorkerUnavailable) { writeJSON(w, 503, map[string]string{"error": "通讯录执行器未在线,未创建任务;请检查后台同步配置"}) return } writeOrgError(w, err) return } operation := "created" if replayed { operation = "replayed" } writeJSON(w, 202, map[string]any{"data": receipt, "operation": operation}) }