Files
RenewCMS/api/handlers/site/siteHandler.go
T

297 lines
9.5 KiB
Go

package site
import (
"RenewCMS/api/pkg/apperr"
validator "RenewCMS/api/pkg/validator"
"RenewCMS/internal/domain/image"
domain "RenewCMS/internal/domain/site"
"RenewCMS/internal/infrastructure/useCases"
"encoding/json"
"errors"
"log"
"net/http"
"net/url"
"strconv"
"github.com/go-chi/chi/v5"
)
type Handler struct {
ListUseCase *useCases.ListSitesUseCase
CreateUseCase *useCases.CreateSiteUseCase
UpdateUseCase *useCases.UpdateSiteUseCase
SetDefaultUseCase *useCases.SetDefaultSiteUseCase
DeleteUseCase *useCases.DeleteSiteUseCase
}
func NewHandler(list *useCases.ListSitesUseCase, create *useCases.CreateSiteUseCase, update *useCases.UpdateSiteUseCase, setDefault *useCases.SetDefaultSiteUseCase, deleteUseCase *useCases.DeleteSiteUseCase) *Handler {
return &Handler{ListUseCase: list, CreateUseCase: create, UpdateUseCase: update, SetDefaultUseCase: setDefault, DeleteUseCase: deleteUseCase}
}
type PostSite struct {
Name string `json:"name" validate:"required,sitename"`
Key string `json:"key" validate:"required,sitekey"`
BaseURL string `json:"base_url" validate:"required"`
DefaultRobots string `json:"default_robots" validate:"omitempty,oneof=index noindex"`
Locale *string `json:"locale"`
DefaultSocialImageID *uint32 `json:"default_social_image_id"`
IsDefault bool `json:"is_default"`
SiteName string `json:"site_name" validate:"max=100"`
TitleTemplate *string `json:"title_template" validate:"omitempty,max=200"`
DefaultDescription string `json:"default_description" validate:"max=320"`
}
type optional[T any] struct {
Set bool
Value *T
}
func (o *optional[T]) UnmarshalJSON(data []byte) error {
o.Set = true
if string(data) == "null" {
o.Value = nil
return nil
}
var value T
if err := json.Unmarshal(data, &value); err != nil {
return err
}
o.Value = &value
return nil
}
type PatchSite struct {
Name *string `json:"name" validate:"omitempty,sitename"`
Key *string `json:"key" validate:"omitempty,sitekey"`
BaseURL *string `json:"base_url"`
DefaultRobots *string `json:"default_robots" validate:"omitempty,oneof=index noindex"`
Locale optional[string] `json:"locale"`
DefaultSocialImageID optional[uint32] `json:"default_social_image_id"`
IsDefault *bool `json:"is_default"`
SiteName *string `json:"site_name" validate:"omitempty,max=100"`
TitleTemplate *string `json:"title_template" validate:"omitempty,max=200"`
DefaultDescription *string `json:"default_description" validate:"omitempty,max=320"`
}
func (p PatchSite) toPatch() domain.Patch {
return domain.Patch{
Name: p.Name,
Key: p.Key,
BaseURL: p.BaseURL,
DefaultRobots: p.DefaultRobots,
Locale: domain.Nullable[string]{Set: p.Locale.Set, Value: p.Locale.Value},
DefaultSocialImageID: domain.Nullable[uint32]{Set: p.DefaultSocialImageID.Set, Value: p.DefaultSocialImageID.Value},
IsDefault: p.IsDefault,
SiteName: p.SiteName,
TitleTemplate: p.TitleTemplate,
DefaultDescription: p.DefaultDescription,
}
}
func parseSiteId(r *http.Request) (uint32, error) {
id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32)
return uint32(id), err
}
func writeSite(w http.ResponseWriter, status int, s domain.Site) {
body, err := json.Marshal(s)
if err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
if status != http.StatusOK {
w.WriteHeader(status)
}
if _, err := w.Write(body); err != nil {
log.Printf("Failed to write response: %v", err)
}
}
func writeSiteError(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, domain.ErrSiteNotFound):
http.Error(w, apperr.SiteNotFoundErrorMessage, http.StatusNotFound)
case errors.Is(err, domain.ErrSiteKeyTaken):
http.Error(w, apperr.SiteKeyTakenErrorMessage, http.StatusConflict)
case errors.Is(err, domain.ErrSiteHasArticles):
http.Error(w, apperr.SiteHasArticlesErrorMessage, http.StatusConflict)
case errors.Is(err, domain.ErrDefaultSite):
http.Error(w, apperr.DefaultSiteErrorMessage, http.StatusForbidden)
case errors.Is(err, domain.ErrKeyImmutable):
http.Error(w, apperr.SiteKeyImmutableErrorMessage, http.StatusBadRequest)
case errors.Is(err, domain.ErrInvalidSiteURL):
http.Error(w, apperr.InvalidSiteURLErrorMessage, http.StatusBadRequest)
case errors.Is(err, domain.ErrInvalidTitleTemplate):
http.Error(w, apperr.InvalidTitleTemplateErrorMessage, http.StatusBadRequest)
case errors.Is(err, domain.ErrInvalidLocale):
http.Error(w, apperr.InvalidLocaleErrorMessage, http.StatusBadRequest)
case errors.Is(err, image.ErrImageNotFound):
http.Error(w, apperr.SiteImageNotFoundErrorMessage, http.StatusBadRequest)
case errors.Is(err, domain.ErrUseSetDefault):
http.Error(w, apperr.UseSetDefaultErrorMessage, http.StatusBadRequest)
case errors.Is(err, domain.ErrInvalidReassign):
http.Error(w, apperr.InvalidReassignErrorMessage, http.StatusBadRequest)
default:
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}
const (
defaultListPage = 1
defaultListSize = 25
)
type listQuery struct {
Page int `validate:"gte=1"`
Size int `validate:"omitempty,oneof=5 25 50 100"`
Q string `validate:"max=200"`
}
func intQueryParam(values url.Values, name string, fallback int) (int, error) {
if !values.Has(name) {
return fallback, nil
}
return strconv.Atoi(values.Get(name))
}
func readListQuery(values url.Values) (listQuery, error) {
paged := values.Has("page") || values.Has("size")
page, err := intQueryParam(values, "page", defaultListPage)
if err != nil {
return listQuery{}, err
}
size := 0
if paged {
size, err = intQueryParam(values, "size", defaultListSize)
if err != nil {
return listQuery{}, err
}
if size == 0 {
return listQuery{}, errors.New("invalid size")
}
}
return listQuery{Page: page, Size: size, Q: values.Get("q")}, nil
}
func (h *Handler) List(w http.ResponseWriter, r *http.Request) {
query, err := readListQuery(r.URL.Query())
if err != nil || validator.Validate.Struct(query) != nil {
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
return
}
page, err := h.ListUseCase.ListPage(useCases.ListSitesCommand{Q: query.Q, Page: query.Page, Size: query.Size})
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
body, err := json.Marshal(page.Items)
if err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
w.Header().Set("X-Total-Count", strconv.Itoa(page.Total))
if _, err := w.Write(body); err != nil {
log.Printf("Failed to write response: %v", err)
}
}
func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
var request PostSite
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
return
}
if err := validator.Validate.Struct(request); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
created, err := h.CreateUseCase.Create(r.Context(), useCases.CreateSiteCommand{
Name: request.Name,
Key: request.Key,
BaseURL: request.BaseURL,
DefaultRobots: request.DefaultRobots,
Locale: request.Locale,
DefaultSocialImageID: request.DefaultSocialImageID,
IsDefault: request.IsDefault,
SiteName: request.SiteName,
TitleTemplate: request.TitleTemplate,
DefaultDescription: request.DefaultDescription,
})
if err != nil {
writeSiteError(w, err)
return
}
writeSite(w, http.StatusCreated, created)
}
func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
id, err := parseSiteId(r)
if err != nil {
http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest)
return
}
var request PatchSite
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
return
}
if err := validator.Validate.Struct(request); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
updated, err := h.UpdateUseCase.Update(r.Context(), id, request.toPatch())
if err != nil {
writeSiteError(w, err)
return
}
writeSite(w, http.StatusOK, updated)
}
func (h *Handler) SetDefault(w http.ResponseWriter, r *http.Request) {
id, err := parseSiteId(r)
if err != nil {
http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest)
return
}
updated, err := h.SetDefaultUseCase.SetDefault(r.Context(), id)
if err != nil {
writeSiteError(w, err)
return
}
writeSite(w, http.StatusOK, updated)
}
const reassignQueryParam = "reassign_to"
func readReassign(r *http.Request) (*uint32, bool) {
query := r.URL.Query()
if !query.Has(reassignQueryParam) {
return nil, false
}
parsed, err := strconv.ParseUint(query.Get(reassignQueryParam), 10, 32)
if err != nil {
return nil, true
}
destination := uint32(parsed)
return &destination, false
}
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
id, err := parseSiteId(r)
if err != nil {
http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest)
return
}
reassignTo, invalid := readReassign(r)
if err := h.DeleteUseCase.Delete(r.Context(), useCases.DeleteSiteCommand{ID: id, ReassignTo: reassignTo, ReassignInvalid: invalid}); err != nil {
writeSiteError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}