package api import ( "encoding/json" "errors" "net/http" "strconv" "strings" "dana/internal/storage" ) // documentKinds — допустимые виды документов (как в CHECK таблицы documents). var documentKinds = map[string]bool{ "license": true, "certificate": true, "award": true, "supervisor": true, "other": true, } type documentRequest struct { Kind string `json:"kind"` Title string `json:"title"` Body string `json:"body"` FileID *int64 `json:"file_id"` PreviewID *int64 `json:"preview_id"` IssuedAt *string `json:"issued_at"` Sort int `json:"sort"` Published bool `json:"published"` } func decodeDocument(w http.ResponseWriter, r *http.Request) (storage.DocumentInput, bool) { var req documentRequest if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(&req); err != nil { fail(w, http.StatusBadRequest, "bad_request", "Некорректный запрос") return storage.DocumentInput{}, false } in := storage.DocumentInput{ Kind: req.Kind, Title: strings.TrimSpace(req.Title), Body: strings.TrimSpace(strings.ReplaceAll(req.Body, "\r", "")), FileID: req.FileID, PreviewID: req.PreviewID, Sort: req.Sort, Published: req.Published, } if in.Sort == 0 { in.Sort = 100 } if !documentKinds[in.Kind] { fail(w, http.StatusUnprocessableEntity, "validation", "Выберите вид документа") return in, false } if in.Title == "" { fail(w, http.StatusUnprocessableEntity, "validation", "Укажите название") return in, false } var err error if in.IssuedAt, err = parseDate(req.IssuedAt); err != nil { fail(w, http.StatusUnprocessableEntity, "validation", "Дата выдачи в формате ГГГГ-ММ-ДД") return in, false } // лицензию или награду без скана и файла на сайте показать нечем if in.Published && in.Kind != "supervisor" && in.FileID == nil && in.PreviewID == nil { fail(w, http.StatusUnprocessableEntity, "validation", "Чтобы показать документ на сайте, загрузите скан или PDF") return in, false } return in, true } // checkDocumentMedia не даёт поставить PDF на место картинки-скана. func (a *API) checkDocumentMedia(w http.ResponseWriter, r *http.Request, in storage.DocumentInput) bool { if in.PreviewID == nil { return true } mime, err := a.store.MediaMime(r.Context(), *in.PreviewID) if errors.Is(err, storage.ErrNotFound) || (err == nil && !strings.HasPrefix(mime, "image/")) { fail(w, http.StatusUnprocessableEntity, "validation", "Скан для просмотра должен быть картинкой, а PDF загрузите в поле «Файл документа»") return false } if err != nil { handleErr(w, err) return false } return true } func (a *API) adminDocuments(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) { list, err := a.store.AdminDocuments(r.Context()) if err != nil { handleErr(w, err) return } ok(w, list, map[string]any{"total": len(list)}) } func (a *API) adminCreateDocument(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) { in, valid := decodeDocument(w, r) if !valid || !a.checkDocumentMedia(w, r, in) { return } id, err := a.store.AdminCreateDocument(r.Context(), in) if err != nil { handleErr(w, err) return } a.store.Audit(r.Context(), u.ID, "create", "document", id, clientIP(r)) writeJSON(w, http.StatusCreated, envelope{Data: map[string]any{"id": id}}) } func (a *API) adminUpdateDocument(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 := decodeDocument(w, r) if !valid || !a.checkDocumentMedia(w, r, in) { return } if err := a.store.AdminUpdateDocument(r.Context(), id, in); err != nil { handleErr(w, err) return } a.store.Audit(r.Context(), u.ID, "update", "document", id, clientIP(r)) ok(w, map[string]any{"id": id}, nil) } func (a *API) adminDeleteDocument(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.AdminDeleteDocument(r.Context(), id); err != nil { handleErr(w, err) return } a.store.Audit(r.Context(), u.ID, "delete", "document", id, clientIP(r)) ok(w, map[string]any{"id": id}, nil) } // ---------- вопросы и ответы ---------- type faqRequest struct { Question string `json:"question"` Answer string `json:"answer"` ServiceID *int64 `json:"service_id"` Sort int `json:"sort"` Published bool `json:"published"` } func decodeFAQ(w http.ResponseWriter, r *http.Request) (storage.FAQInput, bool) { var req faqRequest if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(&req); err != nil { fail(w, http.StatusBadRequest, "bad_request", "Некорректный запрос") return storage.FAQInput{}, false } in := storage.FAQInput{ Question: strings.TrimSpace(req.Question), Answer: strings.TrimSpace(strings.ReplaceAll(req.Answer, "\r", "")), ServiceID: req.ServiceID, Sort: req.Sort, Published: req.Published, } if in.Sort == 0 { in.Sort = 100 } if in.Question == "" { fail(w, http.StatusUnprocessableEntity, "validation", "Укажите вопрос") return in, false } if in.Published && in.Answer == "" { fail(w, http.StatusUnprocessableEntity, "validation", "Чтобы показать вопрос на сайте, напишите ответ") return in, false } return in, true } func (a *API) adminFAQ(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) { list, err := a.store.AdminFAQ(r.Context()) if err != nil { handleErr(w, err) return } ok(w, list, map[string]any{"total": len(list)}) } func (a *API) adminCreateFAQ(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) { in, valid := decodeFAQ(w, r) if !valid { return } id, err := a.store.AdminCreateFAQ(r.Context(), in) if err != nil { handleErr(w, err) return } a.store.Audit(r.Context(), u.ID, "create", "faq", id, clientIP(r)) writeJSON(w, http.StatusCreated, envelope{Data: map[string]any{"id": id}}) } func (a *API) adminUpdateFAQ(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 := decodeFAQ(w, r) if !valid { return } if err := a.store.AdminUpdateFAQ(r.Context(), id, in); err != nil { handleErr(w, err) return } a.store.Audit(r.Context(), u.ID, "update", "faq", id, clientIP(r)) ok(w, map[string]any{"id": id}, nil) } func (a *API) adminDeleteFAQ(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.AdminDeleteFAQ(r.Context(), id); err != nil { handleErr(w, err) return } a.store.Audit(r.Context(), u.ID, "delete", "faq", id, clientIP(r)) ok(w, map[string]any{"id": id}, nil) }