239 lines
7.5 KiB
Go
239 lines
7.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"
|
|
"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"`
|
|
}
|
|
|
|
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"`
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|
|
|
|
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.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)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) List(w http.ResponseWriter, _ *http.Request) {
|
|
sites, err := h.ListUseCase.List()
|
|
if err != nil {
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
body, err := json.Marshal(sites)
|
|
if err != nil {
|
|
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
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,
|
|
})
|
|
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)
|
|
}
|