231 lines
7.3 KiB
Go
231 lines
7.3 KiB
Go
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)
|
||
}
|