feat: articles format option with markdown new default

This commit is contained in:
fsylvain committed 2026-10-04 19:04:12 +02:00
1 parent 68401d66e0
commit 7e9bd9ba30
61 files changed
+2269 -243

No files matched your search

+6 -4
View File
@@ -10,6 +10,7 @@ import (
"RenewCMS/internal/infrastructure/config"
"RenewCMS/internal/infrastructure/hashing"
"RenewCMS/internal/infrastructure/mailer"
"RenewCMS/internal/infrastructure/markdown"
"RenewCMS/internal/infrastructure/persistence/models"
persistence "RenewCMS/internal/infrastructure/persistence/repositories"
"RenewCMS/internal/infrastructure/update"
@@ -152,13 +153,14 @@ func getRepositories(db *gorm.DB, settings config.Settings) *Repositories {
func getUseCases(repos *Repositories, settings config.Settings) *UseCases {
hasher := hashing.NewBcryptHasher()
renderer := markdown.NewConverter()
return &UseCases{
Token: jwtauth.New("HS256", []byte(settings.JWTSecret), nil),
CreateArticleUseCase: useCases.NewCreateArticleUseCase(repos.ArticleRepo),
GetArticleUseCase: useCases.NewGetArticleUseCase(repos.ArticleRepo),
ListArticlesUseCase: useCases.NewListArticlesUseCase(repos.ArticleRepo),
UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(repos.ArticleRepo),
CreateArticleUseCase: useCases.NewCreateArticleUseCase(repos.ArticleRepo, renderer),
GetArticleUseCase: useCases.NewGetArticleUseCase(repos.ArticleRepo, renderer),
ListArticlesUseCase: useCases.NewListArticlesUseCase(repos.ArticleRepo, renderer),
UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(repos.ArticleRepo, renderer),
DeleteArticleUseCase: useCases.NewDeleteArticleUseCase(repos.ArticleRepo),
GetUserUseCase: useCases.NewGetUserUseCase(repos.UserRepo),
CreateUserUseCase: useCases.NewCreateUserUseCase(repos.UserRepo, hasher, repos.MailRepo, settings.PublicURL),
+102 -24
View File
@@ -12,6 +12,7 @@ import (
"log"
"net/http"
"strconv"
"time"
"github.com/go-chi/chi/v5"
)
@@ -41,6 +42,53 @@ func NewHandler(create *useCases.CreateArticleUseCase, get *useCases.GetArticleU
}
}
type articleResponse struct {
ID uint32 `json:"id"`
Title string `json:"title"`
Body json.RawMessage `json:"body"`
BodyFormat domain.BodyFormat `json:"body_format"`
IsOnline bool `json:"is_online"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func toArticleResponse(article domain.Article, format domain.BodyFormat) (articleResponse, error) {
body := json.RawMessage(article.Body)
if format != domain.BodyFormatJSON {
encoded, err := json.Marshal(article.Body)
if err != nil {
return articleResponse{}, err
}
body = encoded
}
return articleResponse{
ID: article.ID,
Title: article.Title,
Body: body,
BodyFormat: format,
IsOnline: article.IsOnline,
CreatedAt: article.CreatedAt,
UpdatedAt: article.UpdatedAt,
}, nil
}
func encodeArticle(article domain.Article, format domain.BodyFormat) ([]byte, error) {
response, err := toArticleResponse(article, format)
if err != nil {
return nil, err
}
return json.Marshal(response)
}
func parseBodyFormat(w http.ResponseWriter, r *http.Request) (domain.BodyFormat, bool) {
format, err := domain.ParseBodyFormat(r.URL.Query().Get("format"))
if err != nil {
http.Error(w, apperr.ArticleBodyFormatErrorMessage, http.StatusBadRequest)
return "", false
}
return format, true
}
func parseArticleId(r *http.Request) (uint32, error) {
id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32)
return uint32(id), err
@@ -53,11 +101,16 @@ func (h *Handler) GetArticle(w http.ResponseWriter, r *http.Request) {
return
}
format, ok := parseBodyFormat(w, r)
if !ok {
return
}
var article domain.Article
if h.callerIsVerified(r) {
article, err = h.GetUseCase.GetArticle(id)
article, err = h.GetUseCase.GetArticle(id, format)
} else {
article, err = h.GetUseCase.GetOnlineArticle(id)
article, err = h.GetUseCase.GetOnlineArticle(id, format)
}
if err != nil {
if errors.Is(err, domain.ErrArticleNotFound) {
@@ -68,7 +121,7 @@ func (h *Handler) GetArticle(w http.ResponseWriter, r *http.Request) {
return
}
articleJson, err := json.Marshal(article)
articleJson, err := encodeArticle(article, format)
if err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
@@ -80,6 +133,11 @@ func (h *Handler) GetArticle(w http.ResponseWriter, r *http.Request) {
}
func (h *Handler) PostArticle(w http.ResponseWriter, r *http.Request) {
format, ok := parseBodyFormat(w, r)
if !ok {
return
}
var localArticle PostArticle
if err := json.NewDecoder(r.Body).Decode(&localArticle); err != nil {
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
@@ -92,8 +150,9 @@ func (h *Handler) PostArticle(w http.ResponseWriter, r *http.Request) {
}
createdArticle, err := h.CreateUseCase.CreateArticle(useCases.CreateArticleCommand{
Title: localArticle.Title,
Body: localArticle.Body,
Title: localArticle.Title,
Body: localArticle.Body,
Format: format,
})
if err != nil {
if errors.Is(err, domain.ErrInvalidTitle) {
@@ -104,16 +163,7 @@ func (h *Handler) PostArticle(w http.ResponseWriter, r *http.Request) {
return
}
articleJson, err := json.Marshal(createdArticle)
if err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
if _, err := w.Write(articleJson); err != nil {
log.Printf("Failed to write response: %v", err)
}
writeArticleJsonResponse(w, http.StatusCreated, createdArticle, format)
}
func writeArticleErrorResponse(w http.ResponseWriter, err error) {
@@ -124,13 +174,16 @@ func writeArticleErrorResponse(w http.ResponseWriter, err error) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
func writeArticleJsonResponse(w http.ResponseWriter, article domain.Article) {
articleJson, err := json.Marshal(article)
func writeArticleJsonResponse(w http.ResponseWriter, status int, article domain.Article, format domain.BodyFormat) {
articleJson, err := encodeArticle(article, format)
if err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
if status != http.StatusOK {
w.WriteHeader(status)
}
if _, err := w.Write(articleJson); err != nil {
log.Printf("Failed to write response: %v", err)
}
@@ -143,13 +196,18 @@ func (h *Handler) setIsOnline(w http.ResponseWriter, r *http.Request, isOnline b
return
}
updatedArticle, err := h.UpdateUseCase.UpdateIsOnline(id, isOnline)
format, ok := parseBodyFormat(w, r)
if !ok {
return
}
updatedArticle, err := h.UpdateUseCase.UpdateIsOnline(id, isOnline, format)
if err != nil {
writeArticleErrorResponse(w, err)
return
}
writeArticleJsonResponse(w, updatedArticle)
writeArticleJsonResponse(w, http.StatusOK, updatedArticle, format)
}
type UpdateArticleBody struct {
@@ -163,6 +221,11 @@ func (h *Handler) UpdateArticleBody(w http.ResponseWriter, r *http.Request) {
return
}
format, ok := parseBodyFormat(w, r)
if !ok {
return
}
var localArticle UpdateArticleBody
if err := json.NewDecoder(r.Body).Decode(&localArticle); err != nil {
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
@@ -174,13 +237,13 @@ func (h *Handler) UpdateArticleBody(w http.ResponseWriter, r *http.Request) {
return
}
updatedArticle, err := h.UpdateUseCase.UpdateBody(id, localArticle.Body)
updatedArticle, err := h.UpdateUseCase.UpdateBody(id, localArticle.Body, format)
if err != nil {
writeArticleErrorResponse(w, err)
return
}
writeArticleJsonResponse(w, updatedArticle)
writeArticleJsonResponse(w, http.StatusOK, updatedArticle, format)
}
func (h *Handler) PublishArticle(w http.ResponseWriter, r *http.Request) {
@@ -206,19 +269,34 @@ func (h *Handler) callerIsVerified(r *http.Request) bool {
}
func (h *Handler) ListArticles(w http.ResponseWriter, r *http.Request) {
format, ok := parseBodyFormat(w, r)
if !ok {
return
}
var articles []domain.Article
var err error
if h.callerIsVerified(r) {
articles, err = h.ListUseCase.ListAllArticles()
articles, err = h.ListUseCase.ListAllArticles(format)
} else {
articles, err = h.ListUseCase.ListOnlineArticles()
articles, err = h.ListUseCase.ListOnlineArticles(format)
}
if err != nil {
http.Error(w, "Failed to retrieve articles", http.StatusInternalServerError)
return
}
articlesJson, err := json.Marshal(articles)
responses := make([]articleResponse, 0, len(articles))
for _, article := range articles {
response, err := toArticleResponse(article, format)
if err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
responses = append(responses, response)
}
articlesJson, err := json.Marshal(responses)
if err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
+1
View File
@@ -34,3 +34,4 @@ const UpdateAlreadyRunningErrorMessage = "An update is already running or waitin
const NothingToUpdateErrorMessage = "There is nothing to update."
const UpdateRefusedErrorMessage = "This update cannot be downloaded."
const UpdateRestartMismatchErrorMessage = "The update state changed. Reload the page and try again."
const ArticleBodyFormatErrorMessage = "The format must be one of: markdown, html, json."