Centralize template extension, magic-byte, and HTML/script rejection in upload.ValidateTemplate so admin template POST matches project asset hardening. Co-authored-by: Cursor <cursoragent@cursor.com>
152 lines
4.4 KiB
Go
152 lines
4.4 KiB
Go
package workflowdef
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"chat-agent-system/internal/storage"
|
|
"chat-agent-system/internal/upload"
|
|
)
|
|
|
|
// Service 工作流定义与模板领域服务。
|
|
type Service struct {
|
|
store *Store
|
|
cas *storage.CAS
|
|
}
|
|
|
|
func NewService(pool *pgxpool.Pool, cas *storage.CAS) *Service {
|
|
if pool == nil {
|
|
return &Service{}
|
|
}
|
|
return &Service{store: NewStore(pool), cas: cas}
|
|
}
|
|
|
|
func (s *Service) Available() bool {
|
|
return s != nil && s.store != nil && s.store.enabled()
|
|
}
|
|
|
|
func (s *Service) Store() *Store {
|
|
return s.store
|
|
}
|
|
|
|
func (s *Service) ListDefinitions(ctx context.Context) ([]Definition, error) {
|
|
return s.store.ListDefinitions(ctx)
|
|
}
|
|
|
|
func (s *Service) GetDefinition(ctx context.Context, id int64) (*Definition, error) {
|
|
return s.store.GetDefinitionByID(ctx, id)
|
|
}
|
|
|
|
func (s *Service) CreateDefinition(ctx context.Context, in CreateDefinitionInput, createdBy int64) (*Definition, *Version, error) {
|
|
res := ValidateDefinition(in.Definition, s.templateReady(ctx), s.upstreamExists(ctx))
|
|
if !res.Valid {
|
|
return nil, nil, ErrValidationFailed
|
|
}
|
|
return s.store.CreateDefinition(ctx, in, createdBy)
|
|
}
|
|
|
|
func (s *Service) UpdateDraft(ctx context.Context, workflowID int64, version int, def VersionDefinition) (*Version, error) {
|
|
res := ValidateDefinition(def, s.templateReady(ctx), s.upstreamExists(ctx))
|
|
if !res.Valid {
|
|
return nil, ErrValidationFailed
|
|
}
|
|
return s.store.UpdateDraftVersion(ctx, workflowID, version, def)
|
|
}
|
|
|
|
func (s *Service) ValidateVersion(ctx context.Context, workflowID int64, version int) (ValidationResult, error) {
|
|
v, err := s.store.GetVersion(ctx, workflowID, version)
|
|
if err != nil {
|
|
return ValidationResult{}, err
|
|
}
|
|
return ValidateDefinition(v.Definition, s.templateReady(ctx), s.upstreamExists(ctx)), nil
|
|
}
|
|
|
|
func (s *Service) PublishVersion(ctx context.Context, workflowID int64, version int, publishedBy int64) (*Version, error) {
|
|
res, err := s.ValidateVersion(ctx, workflowID, version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !res.Valid {
|
|
return nil, ErrValidationFailed
|
|
}
|
|
return s.store.PublishVersion(ctx, workflowID, version, publishedBy)
|
|
}
|
|
|
|
func (s *Service) RetireVersion(ctx context.Context, workflowID int64, version int) (*Version, error) {
|
|
return s.store.RetireVersion(ctx, workflowID, version)
|
|
}
|
|
|
|
func (s *Service) ListVersions(ctx context.Context, workflowID int64) ([]Version, error) {
|
|
return s.store.ListVersions(ctx, workflowID)
|
|
}
|
|
|
|
func (s *Service) ListTemplates(ctx context.Context) ([]TemplateAsset, error) {
|
|
return s.store.ListTemplateAssets(ctx)
|
|
}
|
|
|
|
func (s *Service) ListTemplateVersions(ctx context.Context, assetKey string) ([]TemplateVersion, error) {
|
|
return s.store.ListTemplateVersions(ctx, assetKey)
|
|
}
|
|
|
|
// UploadTemplate 上传模板文件并创建 template_version(扩展名白名单 + MIME/魔数校验后标记 ready)。
|
|
func (s *Service) UploadTemplate(ctx context.Context, assetKey, name string, data []byte, fileName string, createdBy int64) (*TemplateVersion, error) {
|
|
if !s.Available() || s.cas == nil {
|
|
return nil, ErrNotAvailable
|
|
}
|
|
ext := strings.ToLower(filepath.Ext(fileName))
|
|
mime, err := upload.ValidateTemplate(fileName, "", data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %v", ErrUploadRejected, err)
|
|
}
|
|
|
|
ref, err := s.cas.StoreFile(ctx, 0, createdBy, fileName, "template", data, mime)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
assetID, err := s.store.UpsertTemplateAsset(ctx, assetKey, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ver, err := s.store.NextTemplateVersion(ctx, assetID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
scanStatus := ScanReady
|
|
if ext == ".docx" || ext == ".xlsx" || ext == ".pptx" {
|
|
scanStatus = ScanPending
|
|
}
|
|
tv, err := s.store.CreateTemplateVersion(ctx, assetID, ver, ref.BlobSHA256, mime, int64(len(data)), scanStatus, createdBy)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tv.AssetKey = assetKey
|
|
return tv, nil
|
|
}
|
|
|
|
func (s *Service) templateReady(ctx context.Context) templateReadyChecker {
|
|
return func(assetKey string, version int) bool {
|
|
if assetKey == "" {
|
|
return true
|
|
}
|
|
if version <= 0 {
|
|
version = 1
|
|
}
|
|
return s.store.IsTemplateReady(ctx, assetKey, version)
|
|
}
|
|
}
|
|
|
|
func (s *Service) upstreamExists(ctx context.Context) upstreamExistsChecker {
|
|
return func(workflowKey string) bool {
|
|
if workflowKey == "" {
|
|
return true
|
|
}
|
|
d, err := s.store.GetDefinitionByKey(ctx, workflowKey)
|
|
return err == nil && d != nil
|
|
}
|
|
}
|