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