package httpapi import ( "net/http" "net/url" "strings" "scout/internal/registry" "scout/internal/users" ) type projectCreateRequest struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` } type projectUpdateRequest struct { Name *string `json:"name"` Description *string `json:"description"` Status *string `json:"status"` } func (s *Server) projectList(writer http.ResponseWriter, _ *http.Request) { projects := make([]registry.Project, 0, 1) projects = append(projects, registry.Project{ ID: registry.DefaultProject, Name: "兼容项目", Status: "active", }) if s.Projects != nil { projects = append(projects, s.Projects.List()...) } writeJSON(writer, http.StatusOK, map[string]any{"projects": projects}) } func (s *Server) projectCreate(writer http.ResponseWriter, request *http.Request) { if s.Projects == nil { writeError(writer, http.StatusServiceUnavailable, "项目管理尚未启用") return } if !s.requireRole(writer, request, users.RoleAdmin) { return } var input projectCreateRequest if err := decodeJSON(request, &input); err != nil { writeError(writer, http.StatusBadRequest, err.Error()) return } project, err := s.Projects.Create(registry.Project{ ID: input.ID, Name: input.Name, Description: input.Description, }) if err != nil { status := http.StatusBadRequest if strings.Contains(err.Error(), "已经存在") { status = http.StatusConflict } writeError(writer, status, err.Error()) return } s.sendAdminsEmail("Scout 创建了新项目", "项目 "+project.ID+" 已创建。") writeJSON(writer, http.StatusCreated, project) } func (s *Server) projectRoute(writer http.ResponseWriter, request *http.Request) { if s.Projects == nil { writeError(writer, http.StatusServiceUnavailable, "项目管理尚未启用") return } parts := strings.Split(strings.Trim(strings.TrimPrefix(request.URL.Path, "/api/v1/projects/"), "/"), "/") if len(parts) == 0 || parts[0] == "" { writeError(writer, http.StatusNotFound, "项目不存在") return } projectID, err := url.PathUnescape(parts[0]) if err != nil || registry.ValidateProjectID(projectID) != nil { writeError(writer, http.StatusBadRequest, "项目标识无效") return } if projectID == registry.DefaultProject { writeError(writer, http.StatusNotFound, "default 项目请使用兼容接口") return } project, exists := s.Projects.Get(projectID) if !exists { writeError(writer, http.StatusNotFound, "项目不存在") return } if len(parts) == 1 { if request.Method == http.MethodGet { writeJSON(writer, http.StatusOK, project) return } if request.Method == http.MethodPatch { if !s.requireRole(writer, request, users.RoleAdmin) { return } var input projectUpdateRequest if err := decodeJSON(request, &input); err != nil { writeError(writer, http.StatusBadRequest, err.Error()) return } update := registry.Project{ Name: project.Name, Description: project.Description, Status: project.Status, } if input.Name != nil { update.Name = *input.Name } if input.Description != nil { update.Description = *input.Description } if input.Status != nil { update.Status = *input.Status } updated, err := s.Projects.Update(projectID, update) if err != nil { writeError(writer, http.StatusBadRequest, err.Error()) return } s.sendAdminsEmail("Scout 项目已更新", "项目 "+updated.ID+" 的配置已更新。") writeJSON(writer, http.StatusOK, updated) return } writeError(writer, http.StatusMethodNotAllowed, "请求方法不支持") return } if project.Status != "active" { writeError(writer, http.StatusForbidden, "项目当前已停用") return } store, ok := s.Projects.Store(projectID) if !ok { writeError(writer, http.StatusNotFound, "项目存储不存在") return } scoped := s.scopedRegistry(store, projectID) if parts[1] == "packages" { if len(parts) == 2 && request.Method == http.MethodGet { scoped.packageList(writer, scopedRequest(request, "/api/v1/packages")) return } if len(parts) >= 3 && parts[2] == "upload" && request.Method == http.MethodPost { scoped.upload(writer, scopedRequest(request, "/api/v1/packages/upload")) return } if request.Method == http.MethodGet || request.Method == http.MethodHead { rest := strings.Join(parts[1:], "/") scoped.packageRoute(writer, scopedRequest(request, "/api/v1/"+rest)) return } } if len(parts) == 2 && parts[1] == "updates" { writeError(writer, http.StatusNotFound, "接口不存在") return } if len(parts) == 3 && parts[1] == "updates" && parts[2] == "stream" && request.Method == http.MethodGet { scoped.stream(writer, scopedRequest(request, "/api/v1/updates/stream")) return } writeError(writer, http.StatusNotFound, "接口不存在") } func (s *Server) scopedRegistry(store *registry.Registry, projectID string) *Server { return &Server{ Registry: store, Projects: s.Projects, DeveloperToken: s.DeveloperToken, MaxUploadBytes: s.MaxUploadBytes, Users: s.Users, Mailer: s.Mailer, SessionDuration: s.SessionDuration, AllowedOrigins: s.AllowedOrigins, AllowedHosts: s.AllowedHosts, ForceHTTPS: s.ForceHTTPS, TrustProxy: s.TrustProxy, AllowLegacyToken: s.AllowLegacyToken, RateLimitPerMinute: s.RateLimitPerMinute, Logger: s.Logger, projectScope: projectID, } } func scopedRequest(request *http.Request, path string) *http.Request { clone := request.Clone(request.Context()) clone.URL.Path = path clone.RequestURI = "" return clone }