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"` 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) } } 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, 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) }