feat: Sites feature backend
This commit is contained in:
1 parent
45fcbd890d
commit
a11338094e
98 files changed
+5365
-214
No files matched your search
+23
-10
@@ -8,6 +8,7 @@ import (
|
||||
domainInvite "RenewCMS/internal/domain/invite"
|
||||
domainMail "RenewCMS/internal/domain/mail"
|
||||
domainPurge "RenewCMS/internal/domain/purge"
|
||||
domainSite "RenewCMS/internal/domain/site"
|
||||
domainUpdate "RenewCMS/internal/domain/update"
|
||||
domainUser "RenewCMS/internal/domain/user"
|
||||
"RenewCMS/internal/infrastructure/auditing"
|
||||
@@ -47,6 +48,11 @@ type UseCases struct {
|
||||
ListArticlesUseCase *useCases.ListArticlesUseCase
|
||||
UpdateArticleUseCase *useCases.UpdateArticleUseCase
|
||||
DeleteArticleUseCase *useCases.DeleteArticleUseCase
|
||||
ListSitesUseCase *useCases.ListSitesUseCase
|
||||
CreateSiteUseCase *useCases.CreateSiteUseCase
|
||||
UpdateSiteUseCase *useCases.UpdateSiteUseCase
|
||||
SetDefaultSiteUseCase *useCases.SetDefaultSiteUseCase
|
||||
DeleteSiteUseCase *useCases.DeleteSiteUseCase
|
||||
GetUserUseCase *useCases.GetUserUseCase
|
||||
CreateUserUseCase *useCases.CreateUserUseCase
|
||||
AuthenticateUserUseCase *useCases.AuthenticateUserUseCase
|
||||
@@ -123,6 +129,7 @@ func (c *UseCases) RunSchedulers(ctx context.Context) {
|
||||
|
||||
type Repositories struct {
|
||||
ArticleRepo domainArticle.Repository
|
||||
SiteRepo domainSite.Repository
|
||||
UserRepo domainUser.Repository
|
||||
ImageRepo domainImage.Repository
|
||||
VariantRepo domainImage.VariantRepository
|
||||
@@ -241,7 +248,7 @@ func openDatabase(settings config.Settings) *database.Connection {
|
||||
if err != nil {
|
||||
panic("Unable to open the database: " + err.Error())
|
||||
}
|
||||
if err := database.Migrate(connection.DB); err != nil {
|
||||
if err := database.Migrate(connection.DB, settings.PublicURL); err != nil {
|
||||
panic("Failed to migrate database: " + err.Error())
|
||||
}
|
||||
return connection
|
||||
@@ -261,6 +268,7 @@ func getRepositories(connection *database.Connection, settings config.Settings)
|
||||
variantFiles := persistence.NewVariantFileRepository(db)
|
||||
return &Repositories{
|
||||
ArticleRepo: persistence.NewArticleRepository(db),
|
||||
SiteRepo: persistence.NewSiteRepository(db),
|
||||
UserRepo: persistence.NewUserRepository(db),
|
||||
ImageRepo: persistence.NewImageRepository(db),
|
||||
VariantRepo: persistence.NewVariantRepository(db),
|
||||
@@ -293,9 +301,9 @@ func getUseCases(repos *Repositories, imageStorage domainImage.Storage, settings
|
||||
renderer := markdown.NewConverter()
|
||||
keyGenerator := hashing.NewSHA256KeyGenerator()
|
||||
recorder := auditing.NewRecorder(repos.AuditRepo, time.Now, log.Default())
|
||||
mergeMedia := useCases.NewMergeMediaUseCase(repos.ImageRepo, repos.ArticleRepo, imageStorage, repos.VariantFileRepo, recorder)
|
||||
mergeMedia := useCases.NewMergeMediaUseCase(repos.ImageRepo, repos.ArticleRepo, imageStorage, repos.VariantFileRepo, recorder, repos.SiteRepo)
|
||||
|
||||
runPurge := useCases.NewRunPurgeUseCase(repos.ImageRepo, repos.ArticleRepo, mergeMedia, repos.PurgeSettingsRepo, time.Now, log.Default(), recorder)
|
||||
runPurge := useCases.NewRunPurgeUseCase(repos.ImageRepo, repos.ArticleRepo, mergeMedia, repos.PurgeSettingsRepo, time.Now, log.Default(), recorder, repos.SiteRepo)
|
||||
purgeScheduler := useCases.NewPurgeSchedulerUseCase(repos.PurgeSettingsRepo, runPurge, recorder)
|
||||
activityCleanup := useCases.NewCleanupActivityUseCase(repos.AuditRepo, repos.AuditSettingsRepo, recorder)
|
||||
|
||||
@@ -306,11 +314,16 @@ func getUseCases(repos *Repositories, imageStorage domainImage.Storage, settings
|
||||
PurgeScheduler: scheduler.NewSystemRunner(purgeScheduler, log.Default()),
|
||||
ActivityCleanupScheduler: scheduler.NewSystemCleanupRunner(activityCleanup, log.Default()),
|
||||
Token: jwtauth.New("HS256", []byte(settings.JWTSecret), nil),
|
||||
CreateArticleUseCase: useCases.NewCreateArticleUseCase(repos.ArticleRepo, renderer, recorder),
|
||||
CreateArticleUseCase: useCases.NewCreateArticleUseCase(repos.ArticleRepo, renderer, recorder, repos.SiteRepo),
|
||||
GetArticleUseCase: useCases.NewGetArticleUseCase(repos.ArticleRepo, renderer),
|
||||
ListArticlesUseCase: useCases.NewListArticlesUseCase(repos.ArticleRepo, renderer),
|
||||
UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(repos.ArticleRepo, renderer, recorder),
|
||||
ListArticlesUseCase: useCases.NewListArticlesUseCase(repos.ArticleRepo, renderer, repos.SiteRepo),
|
||||
UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(repos.ArticleRepo, renderer, recorder, repos.SiteRepo),
|
||||
DeleteArticleUseCase: useCases.NewDeleteArticleUseCase(repos.ArticleRepo, recorder),
|
||||
ListSitesUseCase: useCases.NewListSitesUseCase(repos.SiteRepo),
|
||||
CreateSiteUseCase: useCases.NewCreateSiteUseCase(repos.SiteRepo, repos.ImageRepo, recorder),
|
||||
UpdateSiteUseCase: useCases.NewUpdateSiteUseCase(repos.SiteRepo, repos.ImageRepo, recorder),
|
||||
SetDefaultSiteUseCase: useCases.NewSetDefaultSiteUseCase(repos.SiteRepo, recorder),
|
||||
DeleteSiteUseCase: useCases.NewDeleteSiteUseCase(repos.SiteRepo, recorder),
|
||||
GetUserUseCase: useCases.NewGetUserUseCase(repos.UserRepo),
|
||||
CreateUserUseCase: useCases.NewCreateUserUseCase(repos.UserRepo, hasher, repos.MailRepo, settings.PublicURL, recorder),
|
||||
AuthenticateUserUseCase: useCases.NewAuthenticateUserUseCase(repos.UserRepo, hasher, recorder),
|
||||
@@ -329,14 +342,14 @@ func getUseCases(repos *Repositories, imageStorage domainImage.Storage, settings
|
||||
SendMailUseCase: useCases.NewSendMailUseCase(repos.MailRepo),
|
||||
CreateImageUseCase: useCases.NewCreateImageUseCase(repos.ImageRepo, imageStorage, recorder),
|
||||
OpenMediaUseCase: useCases.NewOpenMediaUseCase(imageStorage),
|
||||
ListMediaUseCase: useCases.NewListMediaUseCase(repos.ImageRepo, repos.ArticleRepo, repos.UserRepo, repos.VariantFileRepo),
|
||||
ListMediaUseCase: useCases.NewListMediaUseCase(repos.ImageRepo, repos.ArticleRepo, repos.UserRepo, repos.VariantFileRepo, repos.SiteRepo),
|
||||
RenameMediaUseCase: useCases.NewRenameMediaUseCase(repos.ImageRepo, recorder),
|
||||
DeleteMediaUseCase: useCases.NewDeleteMediaUseCase(repos.ImageRepo, imageStorage, repos.VariantFileRepo, recorder),
|
||||
DeleteMediaUseCase: useCases.NewDeleteMediaUseCase(repos.ImageRepo, imageStorage, repos.VariantFileRepo, recorder, repos.SiteRepo),
|
||||
MergeMediaUseCase: mergeMedia,
|
||||
GetMediaUsageUseCase: useCases.NewGetMediaUsageUseCase(repos.ImageRepo, repos.ArticleRepo),
|
||||
GetPurgeSettingsUseCase: useCases.NewGetPurgeSettingsUseCase(repos.PurgeSettingsRepo),
|
||||
UpdatePurgeSettingsUseCase: useCases.NewUpdatePurgeSettingsUseCase(repos.PurgeSettingsRepo, time.Now),
|
||||
PreviewPurgeUseCase: useCases.NewPreviewPurgeUseCase(repos.ImageRepo, repos.ArticleRepo),
|
||||
PreviewPurgeUseCase: useCases.NewPreviewPurgeUseCase(repos.ImageRepo, repos.ArticleRepo, repos.SiteRepo),
|
||||
RunPurgeUseCase: runPurge,
|
||||
GetSettingsUseCase: useCases.NewGetSettingsUseCase(settings.Version),
|
||||
GetUpdateStatusUseCase: useCases.NewGetUpdateStatusUseCase(repos.UpdateStore, repos.UpdateJobs, repos.UpdateHelper, repos.UpdateResults, repos.RestartRequests, settings.Version, settings.Container, time.Now),
|
||||
@@ -348,7 +361,7 @@ func getUseCases(repos *Repositories, imageStorage domainImage.Storage, settings
|
||||
ListAPIKeysUseCase: useCases.NewListAPIKeysUseCase(repos.APIKeyRepo, repos.UserRepo, time.Now),
|
||||
RevokeAPIKeyUseCase: useCases.NewRevokeAPIKeyUseCase(repos.APIKeyRepo, repos.UserRepo, time.Now, recorder),
|
||||
AuthenticateAPIKeyUseCase: useCases.NewAuthenticateAPIKeyUseCase(repos.APIKeyRepo, keyGenerator, repos.UserRepo, time.Now),
|
||||
ListActivityUseCase: useCases.NewListActivityUseCase(repos.AuditRepo, repos.UserRepo, repos.ArticleRepo, repos.ImageRepo, repos.APIKeyRepo, time.Now),
|
||||
ListActivityUseCase: useCases.NewListActivityUseCase(repos.AuditRepo, repos.UserRepo, repos.ArticleRepo, repos.ImageRepo, repos.APIKeyRepo, time.Now, repos.SiteRepo),
|
||||
GetActivitySummaryUseCase: useCases.NewGetActivitySummaryUseCase(repos.AuditRepo, time.Now),
|
||||
ExportActivityUseCase: useCases.NewExportActivityUseCase(repos.AuditRepo, time.Now),
|
||||
GetActivityRetentionUseCase: useCases.NewGetActivityRetentionUseCase(repos.AuditSettingsRepo, repos.AuditRepo, time.Now),
|
||||
|
||||
@@ -29,7 +29,7 @@ type Handler struct {
|
||||
type filterQuery struct {
|
||||
Q string `validate:"max=200"`
|
||||
Actor string `validate:"max=40"`
|
||||
Category string `validate:"omitempty,oneof=articles media users api_keys auth system"`
|
||||
Category string `validate:"omitempty,oneof=articles media users api_keys auth system sites"`
|
||||
Outcome string `validate:"omitempty,oneof=all success failed"`
|
||||
Security string `validate:"omitempty,oneof=true"`
|
||||
Range string `validate:"omitempty,oneof=today 7d 30d all custom"`
|
||||
|
||||
@@ -5,10 +5,12 @@ import (
|
||||
"RenewCMS/api/pkg/claims"
|
||||
validator "RenewCMS/api/pkg/validator"
|
||||
domain "RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -50,6 +52,7 @@ type articleResponse struct {
|
||||
Body json.RawMessage `json:"body"`
|
||||
BodyFormat domain.BodyFormat `json:"body_format"`
|
||||
IsOnline bool `json:"is_online"`
|
||||
SiteID uint32 `json:"site_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
@@ -69,6 +72,7 @@ func toArticleResponse(article domain.Article, format domain.BodyFormat) (articl
|
||||
Body: body,
|
||||
BodyFormat: format,
|
||||
IsOnline: article.IsOnline,
|
||||
SiteID: article.SiteID,
|
||||
CreatedAt: article.CreatedAt,
|
||||
UpdatedAt: article.UpdatedAt,
|
||||
}, nil
|
||||
@@ -173,6 +177,10 @@ func writeArticleErrorResponse(w http.ResponseWriter, err error) {
|
||||
http.Error(w, apperr.ArticleNotFoundErrorMessage, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if errors.Is(err, site.ErrSiteNotFound) {
|
||||
http.Error(w, apperr.SiteNotFoundErrorMessage, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
@@ -213,7 +221,40 @@ func (h *Handler) setIsOnline(w http.ResponseWriter, r *http.Request, isOnline b
|
||||
}
|
||||
|
||||
type UpdateArticleBody struct {
|
||||
Body string `json:"body"`
|
||||
Body *string `json:"body"`
|
||||
SiteID *uint32 `json:"site_id" validate:"omitempty,gte=1"`
|
||||
}
|
||||
|
||||
var updatableArticleFields = []string{"body", "site_id"}
|
||||
|
||||
func readUpdateArticleBody(r *http.Request) (UpdateArticleBody, bool) {
|
||||
raw, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return UpdateArticleBody{}, false
|
||||
}
|
||||
var fields map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &fields); err != nil {
|
||||
return UpdateArticleBody{}, false
|
||||
}
|
||||
known := 0
|
||||
for _, name := range updatableArticleFields {
|
||||
value, present := fields[name]
|
||||
if !present {
|
||||
continue
|
||||
}
|
||||
if string(value) == "null" {
|
||||
return UpdateArticleBody{}, false
|
||||
}
|
||||
known++
|
||||
}
|
||||
if known == 0 {
|
||||
return UpdateArticleBody{}, false
|
||||
}
|
||||
var request UpdateArticleBody
|
||||
if err := json.Unmarshal(raw, &request); err != nil {
|
||||
return UpdateArticleBody{}, false
|
||||
}
|
||||
return request, true
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateArticleBody(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -228,18 +269,22 @@ func (h *Handler) UpdateArticleBody(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var localArticle UpdateArticleBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&localArticle); err != nil {
|
||||
request, ok := readUpdateArticleBody(r)
|
||||
if !ok {
|
||||
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validator.Validate.Struct(localArticle); err != nil {
|
||||
if err := validator.Validate.Struct(request); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
updatedArticle, err := h.UpdateUseCase.UpdateBody(r.Context(), id, localArticle.Body, format)
|
||||
updatedArticle, err := h.UpdateUseCase.Update(r.Context(), id, useCases.UpdateArticleCommand{
|
||||
Body: request.Body,
|
||||
SiteID: request.SiteID,
|
||||
Format: format,
|
||||
})
|
||||
if err != nil {
|
||||
writeArticleErrorResponse(w, err)
|
||||
return
|
||||
@@ -276,11 +321,12 @@ const (
|
||||
)
|
||||
|
||||
type listQuery struct {
|
||||
Page int `validate:"gte=1"`
|
||||
Size int `validate:"oneof=5 25 50 100"`
|
||||
Q string `validate:"max=200"`
|
||||
Status string `validate:"omitempty,oneof=online draft"`
|
||||
Sort string `validate:"oneof=title:asc title:desc status:asc status:desc created_at:asc created_at:desc updated_at:asc updated_at:desc"`
|
||||
Page int `validate:"gte=1"`
|
||||
Size int `validate:"oneof=5 25 50 100"`
|
||||
Q string `validate:"max=200"`
|
||||
Status string `validate:"omitempty,oneof=online draft"`
|
||||
Site *string `validate:"omitempty,min=1"`
|
||||
Sort string `validate:"oneof=title:asc title:desc status:asc status:desc created_at:asc created_at:desc updated_at:asc updated_at:desc"`
|
||||
}
|
||||
|
||||
const defaultListSort = "updated_at:desc"
|
||||
@@ -322,7 +368,15 @@ func readListQuery(values url.Values) (listQuery, error) {
|
||||
if values.Has("sort") {
|
||||
sortValue = values.Get("sort")
|
||||
}
|
||||
return listQuery{Page: page, Size: size, Q: values.Get("q"), Status: status, Sort: sortValue}, nil
|
||||
var siteKey *string
|
||||
if values.Has("site") {
|
||||
key := values.Get("site")
|
||||
if key == "" {
|
||||
return listQuery{}, errors.New("empty site")
|
||||
}
|
||||
siteKey = &key
|
||||
}
|
||||
return listQuery{Page: page, Size: size, Q: values.Get("q"), Status: status, Sort: sortValue, Site: siteKey}, nil
|
||||
}
|
||||
|
||||
func (h *Handler) ListArticles(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -345,6 +399,7 @@ func (h *Handler) ListArticles(w http.ResponseWriter, r *http.Request) {
|
||||
Q: query.Q,
|
||||
Status: query.Status,
|
||||
Sort: parseSort(query.Sort),
|
||||
SiteKey: query.Site,
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to retrieve articles", http.StatusInternalServerError)
|
||||
|
||||
@@ -97,6 +97,7 @@ type usageResponse struct {
|
||||
|
||||
type mergeResponse struct {
|
||||
ArticlesUpdated int `json:"articles_updated"`
|
||||
SitesUpdated int `json:"sites_updated"`
|
||||
Merged int `json:"merged"`
|
||||
}
|
||||
|
||||
@@ -352,7 +353,7 @@ func (h *Handler) Merge(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, mergeResponse{ArticlesUpdated: result.ArticlesUpdated, Merged: result.Merged})
|
||||
writeJSON(w, http.StatusOK, mergeResponse{ArticlesUpdated: result.ArticlesUpdated, SitesUpdated: result.SitesUpdated, Merged: result.Merged})
|
||||
}
|
||||
|
||||
func writeMergeError(w http.ResponseWriter, err error) {
|
||||
|
||||
@@ -53,6 +53,7 @@ type purgeRunResponse struct {
|
||||
GroupsPurged int `json:"groups_purged"`
|
||||
Merged int `json:"merged"`
|
||||
ArticlesUpdated int `json:"articles_updated"`
|
||||
SitesUpdated int `json:"sites_updated"`
|
||||
FailedGroups int `json:"failed_groups"`
|
||||
}
|
||||
|
||||
@@ -164,6 +165,7 @@ func (h *Handler) RunPurge(w http.ResponseWriter, r *http.Request) {
|
||||
GroupsPurged: result.GroupsPurged,
|
||||
Merged: result.Merged,
|
||||
ArticlesUpdated: result.ArticlesUpdated,
|
||||
SitesUpdated: result.SitesUpdated,
|
||||
FailedGroups: result.FailedGroups,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
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)
|
||||
}
|
||||
@@ -57,3 +57,13 @@ const CachePurgeScopeErrorMessage = "Scope must be all, unused, variant or image
|
||||
const CachePurgeTargetErrorMessage = "A variant name or an image id is required for this scope."
|
||||
const VariantUnknownMessage = "Unknown variant"
|
||||
const VariantUnsupportedParameterMessage = "Unsupported parameter"
|
||||
const SiteNotFoundErrorMessage = "site_not_found"
|
||||
const SiteKeyTakenErrorMessage = "site_key_taken"
|
||||
const SiteHasArticlesErrorMessage = "site_has_articles"
|
||||
const DefaultSiteErrorMessage = "default_site"
|
||||
const SiteKeyImmutableErrorMessage = "key_immutable"
|
||||
const InvalidSiteURLErrorMessage = "invalid_site_url"
|
||||
const InvalidLocaleErrorMessage = "invalid_locale"
|
||||
const SiteImageNotFoundErrorMessage = "image_not_found"
|
||||
const UseSetDefaultErrorMessage = "use_set_default"
|
||||
const InvalidReassignErrorMessage = "invalid_reassign"
|
||||
@@ -2,6 +2,7 @@ package validator
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"regexp"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
@@ -9,6 +10,8 @@ import (
|
||||
|
||||
const inviteEmailTag = "inviteemail"
|
||||
const variantNameTag = "variantname"
|
||||
const siteKeyTag = "sitekey"
|
||||
const siteNameTag = "sitename"
|
||||
|
||||
var inviteEmailPattern = regexp.MustCompile(`^[^\s@]+@[^\s@.]+(\.[^\s@.]+)+$`)
|
||||
|
||||
@@ -18,6 +21,16 @@ func InitValidator() {
|
||||
Validate = validator.New()
|
||||
_ = Validate.RegisterValidation(inviteEmailTag, isInviteEmail)
|
||||
_ = Validate.RegisterValidation(variantNameTag, isVariantName)
|
||||
_ = Validate.RegisterValidation(siteKeyTag, isSiteKey)
|
||||
_ = Validate.RegisterValidation(siteNameTag, isSiteName)
|
||||
}
|
||||
|
||||
func isSiteKey(field validator.FieldLevel) bool {
|
||||
return site.IsValidKey(field.Field().String())
|
||||
}
|
||||
|
||||
func isSiteName(field validator.FieldLevel) bool {
|
||||
return site.IsValidName(field.Field().String())
|
||||
}
|
||||
|
||||
func isVariantName(field validator.FieldLevel) bool {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"RenewCMS/api/handlers/imagevariant"
|
||||
"RenewCMS/api/handlers/media"
|
||||
"RenewCMS/api/handlers/settings"
|
||||
"RenewCMS/api/handlers/site"
|
||||
"RenewCMS/api/handlers/staticimages"
|
||||
"RenewCMS/api/handlers/update"
|
||||
"RenewCMS/api/handlers/user"
|
||||
@@ -173,6 +174,36 @@ func NewSettingsRouter(container *UseCases, trustedProxies []string) http.Handle
|
||||
return r
|
||||
}
|
||||
|
||||
func NewSiteRouter(container *UseCases, trustedProxies []string) http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
siteHandler := site.NewHandler(container.ListSitesUseCase, container.CreateSiteUseCase, container.UpdateSiteUseCase, container.SetDefaultSiteUseCase, container.DeleteSiteUseCase)
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(container.Token))
|
||||
r.Use(container.apiKeyAuth())
|
||||
r.Use(jwtauth.Authenticator(container.Token))
|
||||
r.Use(middleware.RequireCurrentToken(container.GetUserUseCase))
|
||||
r.Use(middleware.RequireVerifiedUser(container.GetUserUseCase))
|
||||
r.Get("/", siteHandler.List)
|
||||
})
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.RequireSession)
|
||||
r.Use(jwtauth.Verifier(container.Token))
|
||||
r.Use(middleware.AuditContext(container.GetUserUseCase, trustedProxies))
|
||||
r.Use(middleware.RequireSignedIn)
|
||||
r.Use(jwtauth.Authenticator(container.Token))
|
||||
r.Use(middleware.RequireCurrentToken(container.GetUserUseCase))
|
||||
r.Use(middleware.RequireVerifiedUser(container.GetUserUseCase))
|
||||
r.Use(middleware.RequireOwner(container.GetUserUseCase))
|
||||
r.Post("/", siteHandler.Create)
|
||||
r.Patch("/{id}", siteHandler.Update)
|
||||
r.Patch("/{id}/default", siteHandler.SetDefault)
|
||||
r.Delete("/{id}", siteHandler.Delete)
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
func NewActivityRouter(container *UseCases, trustedProxies []string) http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
@@ -320,6 +351,7 @@ func InitBackendRoutes(container *UseCases, settings config.Settings) *chi.Mux {
|
||||
})
|
||||
r.Mount("/user", NewUserRouter(container, settings.AuthRateLimit, settings.TrustedProxies))
|
||||
r.Mount("/settings", NewSettingsRouter(container, settings.TrustedProxies))
|
||||
r.Mount("/site", NewSiteRouter(container, settings.TrustedProxies))
|
||||
r.Mount("/activity", NewActivityRouter(container, settings.TrustedProxies))
|
||||
r.Mount("/media", NewMediaRouter(container, settings.TrustedProxies))
|
||||
r.Mount("/image-variants", NewImageVariantRouter(container, settings.TrustedProxies))
|
||||
|
||||
Reference in new issue
Block a user