Scout/internal/httpapi/projects.go
2026-08-31 21:19:15 +08:00

214 lines
6.3 KiB
Go

package httpapi
import (
"net/http"
"net/url"
"strings"
"scout/internal/access"
"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
}
if len(parts) >= 2 && parts[1] == "members" {
s.projectMembersRoute(writer, request, projectID, parts[2:])
return
}
if len(parts) >= 2 && parts[1] == "tokens" {
s.projectTokensRoute(writer, request, projectID, parts[2:])
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 {
if !s.requireProjectAccess(writer, request, projectID, access.RoleViewer) {
return
}
scoped.packageList(writer, scopedRequest(request, "/api/v1/packages"))
return
}
if len(parts) >= 3 && parts[2] == "upload" && request.Method == http.MethodPost {
if !s.requireProjectAccess(writer, request, projectID, access.RoleDeveloper) {
return
}
scoped.upload(writer, scopedRequest(request, "/api/v1/packages/upload"))
return
}
if request.Method == http.MethodGet || request.Method == http.MethodHead {
if !s.requireProjectAccess(writer, request, projectID, access.RoleViewer) {
return
}
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 {
if !s.requireProjectAccess(writer, request, projectID, access.RoleViewer) {
return
}
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,
Access: s.Access,
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
}