128 lines
4.4 KiB
Go
128 lines
4.4 KiB
Go
package article
|
|
|
|
import (
|
|
"RenewCMS/api/pkg/apperr"
|
|
validator "RenewCMS/api/pkg/validator"
|
|
"RenewCMS/internal/infrastructure/useCases"
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
)
|
|
|
|
type SEOHandler struct {
|
|
ResolveUseCase *useCases.ResolveArticleSEOUseCase
|
|
CheckUseCase *useCases.CheckArticleSEOUseCase
|
|
GetUserUseCase *useCases.GetUserUseCase
|
|
}
|
|
|
|
func NewSEOHandler(resolve *useCases.ResolveArticleSEOUseCase, check *useCases.CheckArticleSEOUseCase, getUser *useCases.GetUserUseCase) *SEOHandler {
|
|
return &SEOHandler{ResolveUseCase: resolve, CheckUseCase: check, GetUserUseCase: getUser}
|
|
}
|
|
|
|
type CheckArticleSEOBody struct {
|
|
Body *string `json:"body"`
|
|
Slug *string `json:"slug" validate:"omitempty,articleslug"`
|
|
Excerpt *string `json:"excerpt" validate:"omitempty,max=300"`
|
|
SEOTitle *string `json:"seo_title" validate:"omitempty,max=120"`
|
|
SEODescription *string `json:"seo_description" validate:"omitempty,max=320"`
|
|
CoverImageID optionalImageID `json:"cover_image_id"`
|
|
CoverImageAlt *string `json:"cover_image_alt" validate:"omitempty,max=200"`
|
|
SocialTitle *string `json:"social_title" validate:"omitempty,max=120"`
|
|
SocialDescription *string `json:"social_description" validate:"omitempty,max=320"`
|
|
SocialImageID optionalImageID `json:"social_image_id"`
|
|
CanonicalURL *string `json:"canonical_url" validate:"omitempty,max=2048"`
|
|
Noindex *bool `json:"noindex"`
|
|
SEODismissed *[]string `json:"seo_dismissed" validate:"omitempty,dive,oneof=title_long title_empty desc_short desc_long cover_alt cover_none slug_long no_au no_h body_alt short"`
|
|
}
|
|
|
|
var checkNonNullableFields = []string{"body", "slug", "excerpt", "seo_title", "seo_description", "cover_image_alt", "social_title", "social_description", "canonical_url", "noindex", "seo_dismissed"}
|
|
|
|
func readCheckBody(r *http.Request) (CheckArticleSEOBody, bool) {
|
|
raw, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
return CheckArticleSEOBody{}, false
|
|
}
|
|
var fields map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw, &fields); err != nil || fields == nil {
|
|
return CheckArticleSEOBody{}, false
|
|
}
|
|
known, ok := countKnownFields(fields, checkNonNullableFields)
|
|
if !ok || known != len(fields) {
|
|
return CheckArticleSEOBody{}, false
|
|
}
|
|
var request CheckArticleSEOBody
|
|
decoder := json.NewDecoder(bytes.NewReader(raw))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&request); err != nil {
|
|
return CheckArticleSEOBody{}, false
|
|
}
|
|
return request, true
|
|
}
|
|
|
|
func (h *SEOHandler) Resolve(w http.ResponseWriter, r *http.Request) {
|
|
id, err := parseArticleId(r)
|
|
if err != nil {
|
|
http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
resolved, err := h.ResolveUseCase.Resolve(id, isVerifiedCaller(r, h.GetUserUseCase))
|
|
if err != nil {
|
|
writeArticleErrorResponse(w, err)
|
|
return
|
|
}
|
|
writeSEOJson(w, resolved)
|
|
}
|
|
|
|
func (h *SEOHandler) Check(w http.ResponseWriter, r *http.Request) {
|
|
id, err := parseArticleId(r)
|
|
if err != nil {
|
|
http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
request, ok := readCheckBody(r)
|
|
if !ok {
|
|
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := validator.Validate.Struct(request); err != nil {
|
|
writeValidationError(w, err)
|
|
return
|
|
}
|
|
|
|
checked, err := h.CheckUseCase.Check(id, useCases.CheckArticleSEOCommand{
|
|
Body: request.Body,
|
|
Slug: request.Slug,
|
|
Excerpt: request.Excerpt,
|
|
SEOTitle: request.SEOTitle,
|
|
SEODescription: request.SEODescription,
|
|
CoverImageID: request.CoverImageID.toNullable(),
|
|
CoverImageAlt: request.CoverImageAlt,
|
|
SocialTitle: request.SocialTitle,
|
|
SocialDescription: request.SocialDescription,
|
|
SocialImageID: request.SocialImageID.toNullable(),
|
|
CanonicalURL: request.CanonicalURL,
|
|
Noindex: request.Noindex,
|
|
SEODismissed: request.SEODismissed,
|
|
})
|
|
if err != nil {
|
|
writeArticleErrorResponse(w, err)
|
|
return
|
|
}
|
|
writeSEOJson(w, checked)
|
|
}
|
|
|
|
func writeSEOJson(w http.ResponseWriter, payload any) {
|
|
encoded, err := json.Marshal(payload)
|
|
if err != nil {
|
|
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if _, err := w.Write(encoded); err != nil {
|
|
log.Printf("Failed to write response: %v", err)
|
|
}
|
|
}
|