package identity import ( "encoding/json" "net/http" "strings" "aigateway.local/core/internal/platform/apiresponse" ) // registerSocialAdmin 注册扫码登录身份源的管理端点。 func (h *ManagementHTTPHandler) registerSocialAdmin() { h.mux.HandleFunc("GET /api/v1/admin/social-providers", h.listSocialProviders) h.mux.HandleFunc("POST /api/v1/admin/social-providers", h.createSocialProvider) h.mux.HandleFunc("PUT /api/v1/admin/social-providers/{kind}", h.updateSocialProvider) h.mux.HandleFunc("DELETE /api/v1/admin/social-providers/{kind}", h.deleteSocialProvider) } type socialProviderInput struct { Code string `json:"code"` DisplayName string `json:"display_name"` ClientID string `json:"client_id"` AgentID string `json:"agent_id"` Secret *string `json:"secret"` RedirectURI string `json:"redirect_uri"` PortalReturnURL string `json:"portal_return_url"` AutoProvision bool `json:"auto_provision"` DefaultDepartmentID *string `json:"default_department_id"` Enabled bool `json:"enabled"` } func (h *ManagementHTTPHandler) decodeSocialProvider(w http.ResponseWriter, r *http.Request, creating bool) (socialProviderInput, SocialProvider, bool) { var input socialProviderInput decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)) decoder.DisallowUnknownFields() if decoder.Decode(&input) != nil { apiresponse.Error(w, 400, "请求格式无效") return input, SocialProvider{}, false } input.Code = strings.ToLower(strings.TrimSpace(input.Code)) input.DisplayName = strings.TrimSpace(input.DisplayName) input.ClientID = strings.TrimSpace(input.ClientID) input.AgentID = strings.TrimSpace(input.AgentID) if input.Code == "" || input.DisplayName == "" || input.ClientID == "" || (creating && input.Secret == nil) || (input.Secret != nil && strings.TrimSpace(*input.Secret) == "") { apiresponse.Error(w, 400, "身份源代码、名称、AppID 或 AppSecret 无效") return input, SocialProvider{}, false } redirectURI, err := validateAbsoluteURL(input.RedirectURI) if err != nil || strings.Contains(redirectURI, "#") { apiresponse.Error(w, 400, "回调 URL 无效") return input, SocialProvider{}, false } returnURL, err := validateAbsoluteURL(input.PortalReturnURL) if err != nil { apiresponse.Error(w, 400, "门户返回 URL 无效") return input, SocialProvider{}, false } if input.DefaultDepartmentID != nil && *input.DefaultDepartmentID != "" { department, err := h.service.repository.GetDepartment(r.Context(), *input.DefaultDepartmentID) if err != nil || !department.Active { apiresponse.Error(w, 400, "默认部门不存在或已停用") return input, SocialProvider{}, false } } return input, SocialProvider{Code: input.Code, DisplayName: input.DisplayName, ClientID: input.ClientID, AgentID: input.AgentID, RedirectURI: redirectURI, PortalReturnURL: returnURL, AutoProvision: input.AutoProvision, DefaultDepartmentID: input.DefaultDepartmentID, Enabled: input.Enabled}, true } func (h *ManagementHTTPHandler) setSocialCredentials(record *SocialProvider, secret string) error { payload, _ := json.Marshal(socialCredentials{Secret: secret}) encrypted, version, err := h.service.idpCipher.Encrypt(payload) if err != nil { return err } record.EncryptedCredentials, record.CredentialKEKVersion = encrypted, version return nil } func (h *ManagementHTTPHandler) socialProviderView(record SocialProvider) map[string]any { configured := false if plaintext, err := h.service.idpCipher.Decrypt(record.EncryptedCredentials, record.CredentialKEKVersion); err == nil { var credentials socialCredentials configured = json.Unmarshal(plaintext, &credentials) == nil && credentials.Secret != "" } return map[string]any{"id": record.ID, "code": record.Code, "kind": record.Kind, "display_name": record.DisplayName, "client_id": record.ClientID, "agent_id": record.AgentID, "secret_configured": configured, "redirect_uri": record.RedirectURI, "portal_return_url": record.PortalReturnURL, "auto_provision": record.AutoProvision, "default_department_id": record.DefaultDepartmentID, "enabled": record.Enabled, "revision": record.Revision, "created_at": record.CreatedAt, "updated_at": record.UpdatedAt} } func (h *ManagementHTTPHandler) listSocialProviders(w http.ResponseWriter, r *http.Request) { if _, ok := h.requirePermission(w, r); !ok { return } records, err := h.service.repository.ListSocialProviders(r.Context()) if err != nil { h.writeError(w, err) return } items := make([]map[string]any, 0, len(records)) for _, record := range records { items = append(items, h.socialProviderView(record)) } apiresponse.OK(w, items) } func (h *ManagementHTTPHandler) createSocialProvider(w http.ResponseWriter, r *http.Request) { actor, ok := h.requirePermission(w, r) if !ok { return } kind := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("kind"))) if !socialKindSupported(kind) { apiresponse.Error(w, 400, "扫码登录平台必须是 wecom/dingtalk/feishu") return } input, record, ok := h.decodeSocialProvider(w, r, true) if !ok { return } if kind == "wecom" && input.AgentID == "" { apiresponse.Error(w, 400, "企业微信身份源需要 AgentID") return } if err := h.setSocialCredentials(&record, strings.TrimSpace(*input.Secret)); err != nil { h.writeError(w, err) return } record.Kind = kind created, err := h.service.repository.SaveSocialProvider(r.Context(), record, actor.ID, true, true) if err != nil { h.writeError(w, err) return } apiresponse.OK(w, h.socialProviderView(created)) } func (h *ManagementHTTPHandler) updateSocialProvider(w http.ResponseWriter, r *http.Request) { actor, ok := h.requirePermission(w, r) if !ok { return } kind := strings.ToLower(strings.TrimSpace(r.PathValue("kind"))) if !socialKindSupported(kind) { apiresponse.Error(w, 400, "扫码登录平台必须是 wecom/dingtalk/feishu") return } existing, err := h.service.repository.GetSocialProviderByKind(r.Context(), kind) if err != nil { h.writeError(w, err) return } input, record, ok := h.decodeSocialProvider(w, r, false) if !ok { return } if kind == "wecom" && input.AgentID == "" { apiresponse.Error(w, 400, "企业微信身份源需要 AgentID") return } record.ID = existing.ID record.Kind = kind replace := input.Secret != nil if replace { if err := h.setSocialCredentials(&record, strings.TrimSpace(*input.Secret)); err != nil { h.writeError(w, err) return } } updated, err := h.service.repository.SaveSocialProvider(r.Context(), record, actor.ID, false, replace) if err != nil { h.writeError(w, err) return } apiresponse.OK(w, h.socialProviderView(updated)) } func (h *ManagementHTTPHandler) deleteSocialProvider(w http.ResponseWriter, r *http.Request) { if _, ok := h.requirePermission(w, r); !ok { return } if err := h.service.repository.DeleteSocialProvider(r.Context(), r.PathValue("kind")); err != nil { h.writeError(w, err) return } apiresponse.OK(w, map[string]bool{"deleted": true}) }