dana-medical-site/internal/api/admin_banners.go
2026-10-14 13:33:13 +00:00

162 lines
5.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}