refac: article reads, story delete, no leaks, stored SEO status, rate limit, cleanup (%site%, article repo split, old lang repos, one default per site rule to domain)

This commit is contained in:
Florian Sylvain committed 2026-11-03 16:01:52 +01:00
1 parent 9d284e65a9
commit 7c15206170
85 files changed
+1788 -593

No files matched your search

@@ -0,0 +1,92 @@
package useCases
import (
"RenewCMS/internal/domain/article"
"RenewCMS/internal/domain/audit"
"RenewCMS/internal/domain/builtintype"
"RenewCMS/internal/domain/role"
"RenewCMS/internal/domain/transaction"
"context"
"errors"
)
type DeleteStoryUseCase struct {
transactor transaction.Transactor
articles article.Repository
recorder audit.Recorder
}
type DeleteStoryCommand struct {
StoryID uint32
Caller ContentCaller
}
func NewDeleteStoryUseCase(transactor transaction.Transactor, articles article.Repository, recorder audit.Recorder) *DeleteStoryUseCase {
return &DeleteStoryUseCase{transactor: transactor, articles: articles, recorder: recorder}
}
func (u *DeleteStoryUseCase) DeleteStory(ctx context.Context, command DeleteStoryCommand) error {
if err := requireDeleteOnEveryVersion(command.Caller); err != nil {
return err
}
var deleted []article.Article
err := u.transactor.Run(ctx, func(tx transaction.Repositories) error {
versions, err := lockStoryVersions(tx, command.StoryID, command.Caller)
if err != nil {
return err
}
deleted = sortByCreation(versions)
return tx.Articles.DeleteStory(command.StoryID)
})
if err != nil {
u.recordRefusal(ctx, command, err)
return err
}
for _, version := range deleted {
event := articleEvent(audit.ActionArticleDelete, version)
event.Params[audit.ParamStoryID] = command.StoryID
u.recorder.Record(ctx, event)
}
return nil
}
func requireDeleteOnEveryVersion(caller ContentCaller) error {
if caller.Grants.ScopeFor(role.KeyContentDelete, builtintype.KeyArticle) != role.ScopeAll {
return role.MissingPermissionError{Key: role.KeyContentDelete}
}
return nil
}
func lockStoryVersions(tx transaction.Repositories, storyID uint32, caller ContentCaller) ([]article.Article, error) {
locked := []uint32{}
for attempt := 0; attempt < maxSiteLockRounds; attempt++ {
versions, err := tx.Articles.ListByStory(storyID)
if err != nil {
return nil, err
}
if len(caller.readableVersions(versions)) == 0 {
return nil, article.ErrArticleNotFound
}
needed := siteIDsOf(versions)
if allLocked(locked, needed) {
return versions, nil
}
if err := lockSites(tx, needed); err != nil {
return nil, err
}
locked = needed
}
return nil, transaction.ErrLockContention
}
func (u *DeleteStoryUseCase) recordRefusal(ctx context.Context, command DeleteStoryCommand, cause error) {
if isNotFound(cause) || errors.Is(cause, role.ErrMissingPermission) {
return
}
versions, err := u.articles.ListByStory(command.StoryID)
readable := sortByCreation(command.Caller.readableVersions(versions))
if err != nil || len(readable) == 0 {
return
}
recordFailure(ctx, u.recorder, articleEvent(audit.ActionArticleDelete, readable[0]), cause)
}