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

+36 -13
View File
@@ -97,6 +97,26 @@ type articleResponse struct {
Capabilities []string `json:"capabilities"`
Translations []domain.Translation `json:"translations"`
Source *domain.Source `json:"source"`
*editorialContext
}
type editorialContext struct {
SourceUnavailable bool `json:"source_unavailable"`
MissingLangs []string `json:"missing_langs"`
ExcludedFromPublic bool `json:"excluded_from_public"`
PartialView bool `json:"partial_view"`
}
func editorialContextFor(viewer access.Caller, described useCases.ArticleContext) *editorialContext {
if viewer.UserID == 0 {
return nil
}
return &editorialContext{
SourceUnavailable: described.SourceUnavailable,
MissingLangs: described.MissingLangs,
ExcludedFromPublic: described.ExcludedFromPublic,
PartialView: described.PartialView,
}
}
type listItemResponse struct {
@@ -171,6 +191,7 @@ func toArticleResponse(article domain.Article, format domain.BodyFormat, viewer
Capabilities: described.Capabilities,
Translations: described.Translations,
Source: described.Source,
editorialContext: editorialContextFor(viewer, described),
}, nil
}
@@ -584,11 +605,12 @@ const groupByStory = "story"
type storyItemResponse struct {
listItemResponse
StoryUpdatedAt time.Time `json:"story_updated_at"`
StoryAuthors []author.Byline `json:"story_authors"`
Coverage []domain.Translation `json:"coverage"`
MissingLangs []string `json:"missing_langs"`
Capabilities []string `json:"capabilities"`
StoryUpdatedAt time.Time `json:"story_updated_at"`
StoryAuthors []author.Byline `json:"story_authors"`
Coverage []domain.Translation `json:"coverage"`
MissingLangs []string `json:"missing_langs"`
ExcludedFromPublic bool `json:"excluded_from_public"`
Capabilities []string `json:"capabilities"`
}
type storyListResponse struct {
@@ -704,7 +726,7 @@ func (h *Handler) ListArticles(w http.ResponseWriter, r *http.Request) {
page, err := h.listUseCase.ListArticlesPage(useCases.ListArticlesCommand{
Format: format,
Caller: access.CallerOf(r),
Lang: query.Lang,
Lang: stringOrEmpty(query.Lang),
Author: query.Author,
Page: query.Page,
Size: query.Size,
@@ -771,12 +793,13 @@ func (h *Handler) listStories(w http.ResponseWriter, r *http.Request, format dom
return
}
responses = append(responses, storyItemResponse{
listItemResponse: item,
StoryUpdatedAt: row.UpdatedAt,
StoryAuthors: row.Authors,
Coverage: row.Coverage,
MissingLangs: row.MissingLangs,
Capabilities: row.Capabilities,
listItemResponse: item,
StoryUpdatedAt: row.UpdatedAt,
StoryAuthors: row.Authors,
Coverage: row.Coverage,
MissingLangs: row.MissingLangs,
ExcludedFromPublic: row.ExcludedFromPublic,
Capabilities: row.Capabilities,
})
}
writeListJSON(w, storyListResponse{Items: responses, Total: page.Total, Page: page.Page, Size: page.Size, Counts: page.Counts})
@@ -798,7 +821,7 @@ func (h *Handler) DeleteArticle(w http.ResponseWriter, r *http.Request) {
if err := h.deleteUseCase.DeleteArticle(r.Context(), id, access.CallerOf(r)); err != nil {
if !writeKnownArticleError(w, err) {
http.Error(w, "Failed to delete article", http.StatusInternalServerError)
http.Error(w, apperr.DeleteArticleFailedErrorMessage, http.StatusInternalServerError)
}
return
}
+27 -8
View File
@@ -14,11 +14,17 @@ import (
)
type Handler struct {
MoveUseCase *useCases.MoveStoryUseCase
MoveUseCase *useCases.MoveStoryUseCase
DeleteUseCase *useCases.DeleteStoryUseCase
}
func NewHandler(move *useCases.MoveStoryUseCase) *Handler {
return &Handler{MoveUseCase: move}
func NewHandler(move *useCases.MoveStoryUseCase, delete *useCases.DeleteStoryUseCase) *Handler {
return &Handler{MoveUseCase: move, DeleteUseCase: delete}
}
func parseStoryID(r *http.Request) (uint32, error) {
id, err := strconv.ParseUint(chi.URLParam(r, "story_id"), 10, 32)
return uint32(id), err
}
type moveRequest struct {
@@ -33,7 +39,7 @@ type moveResponse struct {
}
func (h *Handler) Move(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseUint(chi.URLParam(r, "story_id"), 10, 32)
id, err := parseStoryID(r)
if err != nil {
http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest)
return
@@ -43,9 +49,9 @@ func (h *Handler) Move(w http.ResponseWriter, r *http.Request) {
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
return
}
result, err := h.MoveUseCase.MoveStory(r.Context(), useCases.MoveStoryCommand{StoryID: uint32(id), SiteKey: *request.Site, Caller: access.CallerOf(r)})
result, err := h.MoveUseCase.MoveStory(r.Context(), useCases.MoveStoryCommand{StoryID: id, SiteKey: *request.Site, Caller: access.CallerOf(r)})
if err != nil {
writeError(w, err)
writeError(w, err, apperr.MoveStoryFailedErrorMessage)
return
}
body, err := json.Marshal(moveResponse{StoryID: result.StoryID, Site: result.Site.Key, SiteID: result.Site.ID, ArticleIDs: result.ArticleIDs})
@@ -58,12 +64,25 @@ func (h *Handler) Move(w http.ResponseWriter, r *http.Request) {
}
}
func writeError(w http.ResponseWriter, err error) {
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
id, err := parseStoryID(r)
if err != nil {
http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest)
return
}
if err := h.DeleteUseCase.DeleteStory(r.Context(), useCases.DeleteStoryCommand{StoryID: id, Caller: access.CallerOf(r)}); err != nil {
writeError(w, err, apperr.DeleteArticleFailedErrorMessage)
return
}
w.WriteHeader(http.StatusNoContent)
}
func writeError(w http.ResponseWriter, err error, failure string) {
if access.WriteIfMissingPermission(w, err) {
return
}
if articleerr.Write(w, err) {
return
}
http.Error(w, "Failed to move story", http.StatusInternalServerError)
http.Error(w, failure, http.StatusInternalServerError)
}