package issuance import ( "context" "fmt" "strings" "time" "unicode" "unicode/utf8" "github.com/google/uuid" "gorm.io/gorm" "git.apinb.com/ops/license/internal/subject" "git.apinb.com/ops/license/sdk/licence" ) const ( maximumOperatorRunes = 128 maximumPageSize = 100 ) type Service struct { repository *Repository subjectRepository *subject.Repository signer *FileSigner } func NewService(repository *Repository, subjectRepository *subject.Repository, signer *FileSigner) (*Service, error) { if repository == nil || repository.db == nil || subjectRepository == nil || signer == nil || signer.active == nil { return nil, ErrInvalidInput } return &Service{ repository: repository, subjectRepository: subjectRepository, signer: signer, }, nil } func (s *Service) Issue(ctx context.Context, subjectID uuid.UUID, input IssueInput, operator string) (Record, error) { operator, err := s.validateWrite(ctx, subjectID, operator) if err != nil { return Record{}, err } var issued Record err = s.repository.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { item, err := s.subjectRepository.GetForUpdate(ctx, tx, subjectID) if err != nil { return mapSubjectError(err) } now := time.Now().UTC() claims, content, err := s.signer.Build(item, input, now) if err != nil { return err } record, err := buildRecord(item.ID, "issue", nil, claims, content, now, operator) if err != nil { return err } if err := s.repository.Insert(ctx, tx, record); err != nil { return err } issued = record return nil }) if err != nil { return Record{}, err } issued.FileContent = cloneBytes(issued.FileContent) return issued, nil } func (s *Service) Renew(ctx context.Context, previousID uuid.UUID, input IssueInput, operator string) (Record, error) { operator, err := s.validateWrite(ctx, previousID, operator) if err != nil { return Record{}, err } var renewed Record err = s.repository.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { previous, err := s.repository.lock(ctx, tx, previousID) if err != nil { return err } item, err := s.subjectRepository.GetForUpdate(ctx, tx, previous.SubjectID) if err != nil { return mapSubjectError(err) } now := time.Now().UTC() claims, content, err := s.signer.Build(item, input, now) if err != nil { return err } record, err := buildRecord(item.ID, "renewal", &previousID, claims, content, now, operator) if err != nil { return err } if err := s.repository.Insert(ctx, tx, record); err != nil { return err } renewed = record return nil }) if err != nil { return Record{}, err } renewed.FileContent = cloneBytes(renewed.FileContent) return renewed, nil } func (s *Service) Get(ctx context.Context, id uuid.UUID) (Record, error) { if !s.usable(ctx) || id == uuid.Nil { return Record{}, ErrInvalidInput } return s.repository.Get(ctx, id) } func (s *Service) List(ctx context.Context, filter Filter, page, pageSize int) ([]RecordSummary, int64, error) { if !s.usable(ctx) || !validPage(page, pageSize) { return nil, 0, ErrInvalidInput } normalized, err := normalizeFilter(filter) if err != nil { return nil, 0, err } return s.repository.List(ctx, normalized, page, pageSize) } func (s *Service) ListBySubject(ctx context.Context, subjectID uuid.UUID, page, pageSize int) ([]RecordSummary, int64, error) { if !s.usable(ctx) || subjectID == uuid.Nil || !validPage(page, pageSize) { return nil, 0, ErrInvalidInput } return s.repository.ListBySubject(ctx, subjectID, page, pageSize) } func (s *Service) Download(ctx context.Context, id uuid.UUID) ([]byte, DownloadMetadata, error) { if !s.usable(ctx) || id == uuid.Nil { return nil, DownloadMetadata{}, ErrInvalidInput } return s.repository.Download(ctx, id) } func (s *Service) validateWrite(ctx context.Context, id uuid.UUID, operator string) (string, error) { if !s.usable(ctx) || id == uuid.Nil { return "", ErrInvalidInput } operator = strings.TrimSpace(operator) if operator == "" || !utf8.ValidString(operator) || utf8.RuneCountInString(operator) > maximumOperatorRunes { return "", ErrInvalidInput } for _, character := range operator { if unicode.IsControl(character) { return "", ErrInvalidInput } } return operator, nil } func (s *Service) usable(ctx context.Context) bool { return s != nil && s.repository != nil && s.repository.db != nil && s.subjectRepository != nil && s.signer != nil && s.signer.active != nil && ctx != nil } func validPage(page, pageSize int) bool { return page >= 1 && pageSize >= 1 && pageSize <= maximumPageSize } func normalizeFilter(filter Filter) (Filter, error) { filter.PlatformName = strings.TrimSpace(filter.PlatformName) filter.Workspace = strings.TrimSpace(filter.Workspace) var err error filter.ValidFrom, err = normalizeDateFilter(filter.ValidFrom) if err != nil { return Filter{}, err } filter.ExpiresOn, err = normalizeDateFilter(filter.ExpiresOn) if err != nil { return Filter{}, err } filter.IssuedOn, err = normalizeDateFilter(filter.IssuedOn) if err != nil { return Filter{}, err } return filter, nil } func normalizeDateFilter(value *time.Time) (*time.Time, error) { if value == nil { return nil, nil } if value.IsZero() { return nil, ErrInvalidInput } date, err := licence.NewDate(*value) if err != nil { return nil, ErrInvalidInput } normalized, err := date.Time() if err != nil { return nil, ErrInvalidInput } return &normalized, nil } func buildRecord(subjectID uuid.UUID, issuanceType string, previousID *uuid.UUID, claims licence.Claims, content []byte, issuedAt time.Time, operator string) (Record, error) { id, err := uuid.Parse(claims.ID) if err != nil { return Record{}, fmt.Errorf("解析许可证 ID: %w", err) } signingKeyID, err := uuid.Parse(claims.SigningKeyID) if err != nil { return Record{}, fmt.Errorf("解析签发密钥 ID: %w", err) } validFrom, err := claims.ValidFrom.Time() if err != nil { return Record{}, fmt.Errorf("解析生效日期: %w", err) } expiresOn, err := claims.ExpiresOn.Time() if err != nil { return Record{}, fmt.Errorf("解析到期日期: %w", err) } return Record{ ID: id, SubjectID: subjectID, IssuanceType: issuanceType, PreviousLicenceID: cloneUUID(previousID), ValidFrom: validFrom, ExpiresOn: expiresOn, Quotas: claims.Quotas, SigningKeyID: signingKeyID, Payload: claims, FileContent: cloneBytes(content), IssuedAt: issuedAt, OperatorName: operator, }, nil } func mapSubjectError(err error) error { if err == subject.ErrNotFound { return ErrNotFound } return err }