297 lines
9.5 KiB
Go
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)
|
|
}
|