Files
Agent-System/internal/server/project_handlers.go
T
weichen 3145550a59 feat(security): complete P0 release blocking remediation
Unify auth middleware and remove X-User-ID trust, add project_members
authorization, sandbox workspaces via resolver, and admin-only reconcile
by run_id. Introduce goose migrations with startup schema gate, fail closed
when DB unavailable outside explicit ALLOW_IN_MEMORY, bundle web/dist in
app Dockerfile, and remove frontend pseudo-login fallback.
2026-09-13 02:50:22 +00:00

273 lines
8.6 KiB
Go

package server
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"chat-agent-system/internal/projectworkflow"
)
// handleCreateProject 服务端权威创建项目接口(严格从 Token 获取创建人)
func (s *Server) handleCreateProject(w http.ResponseWriter, r *http.Request) {
user, err := s.authenticateUser(r)
if err != nil || user == nil {
http.Error(w, `{"error":"ERR_UNAUTHENTICATED","message":"请先登录"}`, http.StatusUnauthorized)
return
}
var req projectworkflow.CreateProjectInput
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"无效的 JSON 请求体"}`, http.StatusBadRequest)
return
}
proj, err := s.pwSvc.CreateProject(r.Context(), user, req)
if err != nil {
if errors.Is(err, projectworkflow.ErrInvalidInput) {
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"`+err.Error()+`"}`, http.StatusBadRequest)
return
}
if errors.Is(err, projectworkflow.ErrWorkflowNotFound) {
http.Error(w, `{"error":"ERR_WORKFLOW_NOT_FOUND","message":"`+err.Error()+`"}`, http.StatusBadRequest)
return
}
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
return
}
if proj != nil && s.authorizer != nil {
_ = s.authorizer.AddOwnerOnCreate(r.Context(), proj.ID, user.ID)
}
writeJSONResponse(w, http.StatusCreated, proj)
}
// handleListProjects 查询项目清单;兼容 session_id 检索旧行为
func (s *Server) handleListProjects(w http.ResponseWriter, r *http.Request) {
if sessionID := r.URL.Query().Get("session_id"); sessionID != "" {
s.handleGetProject(w, r)
return
}
user, ok := s.requirePrincipal(w, r)
if !ok {
return
}
list, err := s.pwSvc.ListProjects(r.Context(), user.ID)
if err != nil {
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
return
}
if list == nil {
list = make([]*projectworkflow.Project, 0)
}
writeJSONResponse(w, http.StatusOK, list)
}
// handleGetProjectByID 获取指定项目的详细信息
func (s *Server) handleGetProjectByID(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil || id <= 0 {
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`, http.StatusBadRequest)
return
}
if _, ok := s.requireProjectAccess(w, r, id); !ok {
return
}
proj, err := s.pwSvc.GetProject(r.Context(), id, 0)
if err != nil {
if errors.Is(err, projectworkflow.ErrProjectNotFound) {
http.Error(w, `{"error":"ERR_PROJECT_NOT_FOUND","message":"项目不存在或无权限访问"}`, http.StatusNotFound)
return
}
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
return
}
writeJSONResponse(w, http.StatusOK, proj)
}
// handleListProjectWorkflows 获取项目挂载的工作流清单
func (s *Server) handleListProjectWorkflows(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil || id <= 0 {
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`, http.StatusBadRequest)
return
}
if _, ok := s.requireProjectAccess(w, r, id); !ok {
return
}
wfs, err := s.pwSvc.ListProjectWorkflows(r.Context(), id)
if err != nil {
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
return
}
if wfs == nil {
wfs = make([]*projectworkflow.ProjectWorkflow, 0)
}
writeJSONResponse(w, http.StatusOK, wfs)
}
// handleActivateWorkflow 切换项目当前激活的工作流焦点(创建人权限受控)
func (s *Server) handleActivateWorkflow(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil || id <= 0 {
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`, http.StatusBadRequest)
return
}
user, err := s.authenticateUser(r)
if err != nil || user == nil {
http.Error(w, `{"error":"ERR_UNAUTHENTICATED","message":"请先登录"}`, http.StatusUnauthorized)
return
}
var req projectworkflow.ActivateWorkflowInput
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"无效的请求体"}`, http.StatusBadRequest)
return
}
res, err := s.pwSvc.ActivateWorkflow(r.Context(), id, user.ID, req)
if err != nil {
if errors.Is(err, projectworkflow.ErrForbiddenNotCreator) {
http.Error(w, `{"error":"ERR_NOT_PROJECT_CREATOR","message":"仅项目创建人有权切换激活工作流"}`, http.StatusForbidden)
return
}
if errors.Is(err, projectworkflow.ErrConcurrentConflict) {
http.Error(w, `{"error":"ERR_CONCURRENT_SWITCH","message":"工作流选择版本落后或并发冲突,请刷新后重试"}`, http.StatusConflict)
return
}
if errors.Is(err, projectworkflow.ErrWorkflowNotFound) {
http.Error(w, `{"error":"ERR_WORKFLOW_NOT_FOUND","message":"工作流定义不存在或未在项目启用"}`, http.StatusNotFound)
return
}
if errors.Is(err, projectworkflow.ErrProjectNotFound) {
http.Error(w, `{"error":"ERR_PROJECT_NOT_FOUND","message":"项目不存在"}`, http.StatusNotFound)
return
}
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
return
}
writeJSONResponse(w, http.StatusOK, res)
}
// handleListWorkflowDefinitions 获取全局工作流规范目录
func (s *Server) handleListWorkflowDefinitions(w http.ResponseWriter, r *http.Request) {
defs := s.pwSvc.Catalog().List()
writeJSONResponse(w, http.StatusOK, defs)
}
// handleRunProjectWorkflow 针对特定项目与工作流触发运行并落盘运行记录
func (s *Server) handleRunProjectWorkflow(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil || id <= 0 {
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`, http.StatusBadRequest)
return
}
wfKey := r.PathValue("workflow_key")
if wfKey == "" {
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"workflow_key is required"}`, http.StatusBadRequest)
return
}
def, ok := s.pwSvc.Catalog().Get(wfKey)
if !ok {
http.Error(w, `{"error":"ERR_WORKFLOW_NOT_FOUND","message":"unknown workflow"}`, http.StatusNotFound)
return
}
user, ok := s.requireProjectAccess(w, r, id)
if !ok {
return
}
var req TaskRunRequestDTO
if r.Body != nil {
_ = json.NewDecoder(r.Body).Decode(&req)
}
req.ProjectID = id
req.WorkflowKey = wfKey
req.UserID = user.ID
if req.TaskKey == "" {
req.TaskKey = def.TaskKey
}
bodyBytes, _ := json.Marshal(req)
r.Body = io.NopCloser(bytes.NewReader(bodyBytes))
s.handleRunTask(w, r)
}
// handleListProjectWorkflowRuns 查询项目某工作流的历史运行记录
func (s *Server) handleListProjectWorkflowRuns(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil || id <= 0 {
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`, http.StatusBadRequest)
return
}
wfKey := r.PathValue("workflow_key")
runs, err := s.pwSvc.ListWorkflowRuns(r.Context(), id, wfKey, 20)
if err != nil {
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
return
}
if runs == nil {
runs = make([]*projectworkflow.WorkflowRun, 0)
}
writeJSONResponse(w, http.StatusOK, runs)
}
// handleGetProjectLineage 查询项目文档血缘关系及失效状态
func (s *Server) handleGetProjectLineage(w http.ResponseWriter, r *http.Request) {
docID := strings.TrimSpace(r.URL.Query().Get("doc_id"))
if docID == "" {
idStr := r.PathValue("id")
id, _ := strconv.ParseInt(idStr, 10, 64)
if id > 0 {
docID = fmt.Sprintf("doc_proj%d_test_manager", id)
}
}
if docID == "" {
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"doc_id is required"}`, http.StatusBadRequest)
return
}
lineages, err := s.pwSvc.GetLineageForDownstream(r.Context(), docID, 0)
if err != nil {
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
return
}
if len(lineages) == 0 {
idStr := r.PathValue("id")
if id, _ := strconv.ParseInt(idStr, 10, 64); id > 0 {
hyphenDocID := fmt.Sprintf("doc-proj%d-test_manager", id)
if hyphenDocID != docID {
if fb, fbErr := s.pwSvc.GetLineageForDownstream(r.Context(), hyphenDocID, 0); fbErr == nil && len(fb) > 0 {
lineages = fb
}
}
}
}
if lineages == nil {
lineages = make([]*projectworkflow.DocumentLineage, 0)
}
writeJSONResponse(w, http.StatusOK, lineages)
}