Scert/backend/internal/httpapi/project_lifecycle_api.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())
}
}
}