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

@@ -2,9 +2,12 @@ package useCases
import (
"RenewCMS/internal/domain/article"
"RenewCMS/internal/domain/builtintype"
"RenewCMS/internal/domain/language"
"RenewCMS/internal/domain/localization"
"RenewCMS/internal/domain/role"
"errors"
"slices"
)
type DescribeArticleUseCase struct {
@@ -13,9 +16,13 @@ type DescribeArticleUseCase struct {
}
type ArticleContext struct {
Capabilities []string
Translations []article.Translation
Source *article.Source
Capabilities []string
Translations []article.Translation
Source *article.Source
SourceUnavailable bool
MissingLangs []string
ExcludedFromPublic bool
PartialView bool
}
func NewDescribeArticleUseCase(articles article.Repository, localizationRepository localization.Repository) *DescribeArticleUseCase {
@@ -23,73 +30,75 @@ func NewDescribeArticleUseCase(articles article.Repository, localizationReposito
}
func (d *DescribeArticleUseCase) Describe(art article.Article, caller ContentCaller) (ArticleContext, error) {
described := ArticleContext{Capabilities: caller.capabilitiesFor(art), Translations: []article.Translation{}}
rows, err := d.localization.Rows(art.SiteID)
if err != nil {
return ArticleContext{}, err
}
described := ArticleContext{
Capabilities: caller.capabilitiesFor(art),
Translations: []article.Translation{},
MissingLangs: []string{},
ExcludedFromPublic: !localization.PublicTag(rows, art.Lang),
PartialView: caller.Grants.ScopeFor(role.KeyContentRead, builtintype.KeyArticle) != role.ScopeAll,
}
if caller.isAnonymous() {
return described, nil
}
translations, err := d.translations(art, caller)
readable, err := d.readableVersions(art, caller)
if err != nil {
return ArticleContext{}, err
}
described.Translations = translations
described.Source, err = d.source(art, caller)
if err != nil {
return ArticleContext{}, err
}
return described, nil
described.Translations = translationsOf(caller, sortByLanguage(readable, rows), rows)
described.MissingLangs = missingLanguages(readable, rows, caller.Grants.HoldsFor(role.KeyContentCreate, builtintype.KeyArticle))
described.Source, described.SourceUnavailable, err = d.source(art, caller)
return described, err
}
func (d *DescribeArticleUseCase) translations(art article.Article, caller ContentCaller) ([]article.Translation, error) {
versions := []article.Article{art}
if art.StoryID != 0 {
found, err := d.articles.ListByStory(art.StoryID)
if err != nil {
return nil, err
}
versions = found
}
rows, err := d.rows(art.SiteID)
if err != nil {
return nil, err
}
translations := []article.Translation{}
for _, version := range sortByLanguage(caller.readableVersions(versions), rows) {
translations = append(translations, article.Translation{
ID: version.ID,
Lang: version.Lang,
Title: version.Title,
IsOnline: version.IsOnline,
LanguageEnabled: d.languageEnabled(rows, version.Lang),
Capabilities: caller.capabilitiesFor(version),
})
}
return translations, nil
}
func (d *DescribeArticleUseCase) rows(siteID uint32) ([]localization.Row, error) {
return d.localization.Rows(siteID)
}
func (d *DescribeArticleUseCase) languageEnabled(rows []localization.Row, tag string) bool {
if tag == language.Unassigned {
return true
}
return localization.EnabledTag(rows, tag)
}
func (d *DescribeArticleUseCase) source(art article.Article, caller ContentCaller) (*article.Source, error) {
func (d *DescribeArticleUseCase) source(art article.Article, caller ContentCaller) (*article.Source, bool, error) {
if art.SourceArticleID == nil {
return nil, nil
return nil, false, nil
}
origin, err := d.articles.Get(*art.SourceArticleID)
if errors.Is(err, article.ErrArticleNotFound) {
return nil, nil
if errors.Is(err, article.ErrArticleNotFound) || err == nil && !caller.seesArticle(origin) {
return nil, true, nil
}
if err != nil {
return nil, false, err
}
return &article.Source{ArticleID: origin.ID, Revision: art.SourceRevision}, false, nil
}
func (d *DescribeArticleUseCase) readableVersions(art article.Article, caller ContentCaller) ([]article.Article, error) {
if art.StoryID == 0 {
return []article.Article{art}, nil
}
versions, err := d.articles.ListByStory(art.StoryID)
if err != nil {
return nil, err
}
if !caller.seesArticle(origin) {
return nil, nil
return caller.readableVersions(versions), nil
}
func translationsOf(caller ContentCaller, ordered []article.Article, rows []localization.Row) []article.Translation {
translations := make([]article.Translation, 0, len(ordered))
for _, version := range ordered {
translations = append(translations, article.Translation{
ID: version.ID,
Lang: version.Lang,
Title: version.Title,
IsOnline: version.IsOnline,
LanguageEnabled: version.Lang == language.Unassigned || localization.EnabledTag(rows, version.Lang),
ExcludedFromPublic: !localization.PublicTag(rows, version.Lang),
SourceArticleID: trackedSourceID(version, ordered),
Capabilities: caller.capabilitiesFor(version),
})
}
return &article.Source{ArticleID: origin.ID, Revision: art.SourceRevision}, nil
return translations
}
func trackedSourceID(version article.Article, readable []article.Article) *uint32 {
if version.SourceArticleID == nil || !slices.ContainsFunc(readable, func(candidate article.Article) bool { return candidate.ID == *version.SourceArticleID }) {
return nil
}
return version.SourceArticleID
}