162 lines
5.6 KiB
Go
162 lines
5.6 KiB
Go
package api
|
||
|
||
import (
|
||
"encoding/json"
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
"unicode/utf8"
|
||
|
||
"dana/internal/storage"
|
||
)
|
||
|
||
// Совпадает со значением по умолчанию в миграции 0006.
|
||
const defaultBannerDisclaimer = "Имеются противопоказания, необходима консультация специалиста"
|
||
|
||
const (
|
||
maxBannerChips = 6
|
||
maxBannerChipLen = 40
|
||
)
|
||
|
||
func (a *API) adminBanners(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) {
|
||
list, err := a.store.AdminBanners(r.Context(), r.URL.Query().Get("position"))
|
||
if err != nil {
|
||
handleErr(w, err)
|
||
return
|
||
}
|
||
ok(w, list, map[string]any{"total": len(list)})
|
||
}
|
||
|
||
func decodeBanner(w http.ResponseWriter, r *http.Request) (storage.BannerInput, bool) {
|
||
var in storage.BannerInput
|
||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||
fail(w, http.StatusBadRequest, "bad_request", "Некорректный запрос")
|
||
return in, false
|
||
}
|
||
in.Position = strings.TrimSpace(in.Position)
|
||
in.Layout = strings.TrimSpace(in.Layout)
|
||
in.Theme = strings.TrimSpace(in.Theme)
|
||
in.Title = strings.TrimSpace(in.Title)
|
||
in.Subtitle = strings.TrimSpace(in.Subtitle)
|
||
in.Badge = strings.TrimSpace(in.Badge)
|
||
in.LinkURL = strings.TrimSpace(in.LinkURL)
|
||
in.Disclaimer = strings.TrimSpace(in.Disclaimer)
|
||
if in.Position == "" {
|
||
in.Position = "home_hero"
|
||
}
|
||
if in.Layout == "" {
|
||
in.Layout = "image"
|
||
}
|
||
if in.Theme == "" {
|
||
in.Theme = "navy"
|
||
}
|
||
// Пустое поле из формы перетёрло бы значение по умолчанию из схемы, и реклама
|
||
// медуслуг ушла бы на сайт без обязательного предупреждения (ФЗ «О рекламе», ст. 24).
|
||
if in.Disclaimer == "" {
|
||
in.Disclaimer = defaultBannerDisclaimer
|
||
}
|
||
|
||
switch in.Layout {
|
||
case "image":
|
||
// Текст на таких слайдах нарисован на картинке, поэтому достаточно
|
||
// либо картинки, либо заголовка — но хоть что-то показывать нужно.
|
||
if in.ImageID == nil && in.Title == "" {
|
||
fail(w, http.StatusUnprocessableEntity, "validation", "Нужна картинка или заголовок")
|
||
return in, false
|
||
}
|
||
case "text":
|
||
if in.Title == "" {
|
||
fail(w, http.StatusUnprocessableEntity, "validation", "У текстового слайда нужен заголовок")
|
||
return in, false
|
||
}
|
||
case "doctor":
|
||
if in.Title == "" || in.ImageID == nil {
|
||
fail(w, http.StatusUnprocessableEntity, "validation", "У слайда с врачом нужны заголовок и фото")
|
||
return in, false
|
||
}
|
||
default:
|
||
fail(w, http.StatusUnprocessableEntity, "validation", "Неизвестный вид слайда")
|
||
return in, false
|
||
}
|
||
if in.Theme != "navy" && in.Theme != "lilac" && in.Theme != "orange" {
|
||
fail(w, http.StatusUnprocessableEntity, "validation", "Неизвестный цвет слайда")
|
||
return in, false
|
||
}
|
||
|
||
chips := make([]string, 0, len(in.Chips))
|
||
for _, c := range in.Chips {
|
||
if c = strings.TrimSpace(c); c == "" {
|
||
continue
|
||
}
|
||
if utf8.RuneCountInString(c) > maxBannerChipLen {
|
||
fail(w, http.StatusUnprocessableEntity, "validation", "Плашка длиннее 40 символов: «"+c+"»")
|
||
return in, false
|
||
}
|
||
chips = append(chips, c)
|
||
}
|
||
if len(chips) > maxBannerChips {
|
||
fail(w, http.StatusUnprocessableEntity, "validation", "Не больше 6 плашек на слайде")
|
||
return in, false
|
||
}
|
||
in.Chips = chips
|
||
|
||
// Ссылка только на страницу сайта или на http(s): javascript: и прочее отсекаем.
|
||
if in.LinkURL != "" && !strings.HasPrefix(in.LinkURL, "/") &&
|
||
!strings.HasPrefix(in.LinkURL, "https://") && !strings.HasPrefix(in.LinkURL, "http://") {
|
||
fail(w, http.StatusUnprocessableEntity, "validation",
|
||
"Ссылка должна вести на страницу сайта (/...) или начинаться с https://")
|
||
return in, false
|
||
}
|
||
if in.StartsAt != nil && in.EndsAt != nil && !in.EndsAt.After(*in.StartsAt) {
|
||
fail(w, http.StatusUnprocessableEntity, "validation", "Окончание показа должно быть позже начала")
|
||
return in, false
|
||
}
|
||
return in, true
|
||
}
|
||
|
||
func (a *API) adminCreateBanner(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) {
|
||
in, valid := decodeBanner(w, r)
|
||
if !valid {
|
||
return
|
||
}
|
||
id, err := a.store.AdminCreateBanner(r.Context(), in)
|
||
if err != nil {
|
||
handleErr(w, err)
|
||
return
|
||
}
|
||
a.store.Audit(r.Context(), u.ID, "create", "banner", id, clientIP(r))
|
||
writeJSON(w, http.StatusCreated, envelope{Data: map[string]any{"id": id}})
|
||
}
|
||
|
||
func (a *API) adminUpdateBanner(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) {
|
||
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||
if err != nil {
|
||
fail(w, http.StatusBadRequest, "bad_request", "Некорректный id")
|
||
return
|
||
}
|
||
in, valid := decodeBanner(w, r)
|
||
if !valid {
|
||
return
|
||
}
|
||
if err := a.store.AdminUpdateBanner(r.Context(), id, in); err != nil {
|
||
handleErr(w, err)
|
||
return
|
||
}
|
||
a.store.Audit(r.Context(), u.ID, "update", "banner", id, clientIP(r))
|
||
ok(w, map[string]any{"id": id}, nil)
|
||
}
|
||
|
||
func (a *API) adminDeleteBanner(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) {
|
||
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||
if err != nil {
|
||
fail(w, http.StatusBadRequest, "bad_request", "Некорректный id")
|
||
return
|
||
}
|
||
if err := a.store.AdminDeleteBanner(r.Context(), id); err != nil {
|
||
handleErr(w, err)
|
||
return
|
||
}
|
||
a.store.Audit(r.Context(), u.ID, "delete", "banner", id, clientIP(r))
|
||
ok(w, map[string]any{"id": id}, nil)
|
||
}
|