136 lines
4.6 KiB
Go
136 lines
4.6 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"git.pcux.cn/xuans/Scert/backend/internal/domain"
|
|
"git.pcux.cn/xuans/Scert/backend/internal/repository"
|
|
)
|
|
|
|
func (s *Server) registerProjectLifecycleRoutes() {
|
|
api := s.router.Group("/api/v1/projects", privateProjectResponse)
|
|
api.POST("", s.projectFailureAudit("project.create"), s.requireAuthenticated, s.createProject)
|
|
api.PUT("/:projectID", s.projectFailureAudit("project.update"), s.requireAuthenticated, s.updateProject)
|
|
api.DELETE("/:projectID", s.projectFailureAudit("project.delete"), s.requireAuthenticated, s.deleteProject)
|
|
}
|
|
|
|
func (s *Server) projectLifecycleReady(c *gin.Context, creating bool) bool {
|
|
if s.projectLifecycle == nil {
|
|
s.writeError(c, http.StatusServiceUnavailable, "项目管理服务尚未就绪", nil)
|
|
return false
|
|
}
|
|
if c.Request.URL.RawQuery != "" || (!creating && !validMemberIdentifier(c.Param("projectID"))) {
|
|
s.writeError(c, http.StatusBadRequest, "项目请求参数无效", nil)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
type projectDetailsRequest struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
func (s *Server) projectDetails(c *gin.Context) (projectDetailsRequest, bool) {
|
|
var input projectDetailsRequest
|
|
if err := bindJSON(c, &input, jsonRequestBodyLimit); err != nil {
|
|
s.writeError(c, http.StatusBadRequest, "项目资料格式无效", nil)
|
|
return input, false
|
|
}
|
|
item := (domain.Project{ID: "validation", Name: input.Name, Description: input.Description}).Normalize()
|
|
if err := item.Validate(); err != nil {
|
|
s.writeError(c, http.StatusBadRequest, err.Error(), nil)
|
|
return input, false
|
|
}
|
|
return projectDetailsRequest{Name: item.Name, Description: item.Description}, true
|
|
}
|
|
|
|
func (s *Server) createProject(c *gin.Context) {
|
|
if !s.projectLifecycleReady(c, true) {
|
|
return
|
|
}
|
|
input, ok := s.projectDetails(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
item, err := s.projectLifecycle.CreateManagedProject(c.Request.Context(), c.GetString(authenticatedActorKey), input.Name, input.Description)
|
|
if err != nil {
|
|
s.writeProjectLifecycleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusCreated, item)
|
|
}
|
|
|
|
func (s *Server) updateProject(c *gin.Context) {
|
|
if !s.projectLifecycleReady(c, false) {
|
|
return
|
|
}
|
|
input, ok := s.projectDetails(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := s.projectLifecycle.UpdateManagedProject(c.Request.Context(), c.GetString(authenticatedActorKey), c.Param("projectID"), input.Name, input.Description); err != nil {
|
|
s.writeProjectLifecycleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"message": "项目资料已更新"})
|
|
}
|
|
|
|
func (s *Server) deleteProject(c *gin.Context) {
|
|
if !s.projectLifecycleReady(c, false) {
|
|
return
|
|
}
|
|
var input struct {
|
|
Confirmation string `json:"confirmation"`
|
|
}
|
|
if err := bindJSON(c, &input, jsonRequestBodyLimit); err != nil || input.Confirmation != c.Param("projectID") {
|
|
s.writeError(c, http.StatusBadRequest, "请确认要删除的项目,确认标识必须与目标项目一致", nil)
|
|
return
|
|
}
|
|
if err := s.projectLifecycle.DeleteManagedProject(c.Request.Context(), c.GetString(authenticatedActorKey), c.Param("projectID")); err != nil {
|
|
s.writeProjectLifecycleError(c, err)
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func (s *Server) writeProjectLifecycleError(c *gin.Context, err error) {
|
|
switch {
|
|
case errors.Is(err, repository.ErrProjectAccessDenied):
|
|
s.writeError(c, http.StatusForbidden, "无权访问项目或执行此操作", nil)
|
|
case errors.Is(err, repository.ErrDefaultProject):
|
|
s.writeError(c, http.StatusConflict, "默认项目不能删除", nil)
|
|
case errors.Is(err, repository.ErrProjectReferenced):
|
|
s.writeError(c, http.StatusConflict, "项目仍有关联资源或成员,请先移出后重试", nil)
|
|
default:
|
|
s.writeError(c, http.StatusInternalServerError, "项目操作失败,请稍后重试", nil)
|
|
}
|
|
}
|
|
|
|
func (s *Server) projectFailureAudit(action string) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
c.Next()
|
|
if c.Writer.Status() < 400 || s.projectLifecycle == nil {
|
|
return
|
|
}
|
|
projectID := c.Param("projectID")
|
|
if !validMemberIdentifier(projectID) {
|
|
projectID = ""
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.WithoutCancel(c.Request.Context()), 3*time.Second)
|
|
defer cancel()
|
|
err := s.projectLifecycle.RecordProjectFailure(ctx, repository.ProjectFailure{
|
|
ActorID: c.GetString(authenticatedActorKey), ProjectID: projectID, Action: action,
|
|
StatusCode: c.Writer.Status(), RequestID: requestIDFromContext(c),
|
|
})
|
|
if err != nil && s.logger != nil {
|
|
s.logger.Error("项目失败审计未保存", "request_id", requestIDFromContext(c), "status", c.Writer.Status())
|
|
}
|
|
}
|
|
}
|