214 lines
6.3 KiB
Go
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
|
|
}
|