349 lines
10 KiB
Go
349 lines
10 KiB
Go
package httpapi
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"errors"
|
||
|
|
"net/http"
|
||
|
|
"strconv"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/support"
|
||
|
|
"github.com/go-chi/chi/v5"
|
||
|
|
"github.com/google/uuid"
|
||
|
|
)
|
||
|
|
|
||
|
|
func (s *Server) handleAdminListKBArticles(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if s.Support == nil {
|
||
|
|
Error(w, http.StatusServiceUnavailable, "support unavailable")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
publishedOnly := r.URL.Query().Get("published") == "1" || r.URL.Query().Get("published") == "true"
|
||
|
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||
|
|
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
||
|
|
items, total, err := s.Support.ListKBArticlesOpts(r.Context(), support.KBArticleListOpts{
|
||
|
|
PublishedOnly: publishedOnly,
|
||
|
|
Category: r.URL.Query().Get("category"),
|
||
|
|
Query: r.URL.Query().Get("q"),
|
||
|
|
Limit: limit,
|
||
|
|
Offset: offset,
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not list kb articles", err, support.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, map[string]any{"items": items, "total": total})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleAdminListKBCategories(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if s.Support == nil {
|
||
|
|
Error(w, http.StatusServiceUnavailable, "support unavailable")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
items, err := s.Support.ListKBCategories(r.Context())
|
||
|
|
if err != nil {
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not list kb categories", err, support.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, map[string]any{"items": items})
|
||
|
|
}
|
||
|
|
|
||
|
|
const kbImageMaxUpload = 3 << 20 // parse budget slightly above 2 MiB file cap
|
||
|
|
|
||
|
|
func (s *Server) handleAdminUploadKBImage(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if s.Support == nil {
|
||
|
|
Error(w, http.StatusServiceUnavailable, "support unavailable")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if err := r.ParseMultipartForm(kbImageMaxUpload); err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid multipart form")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
file, header, err := r.FormFile("file")
|
||
|
|
if err != nil {
|
||
|
|
file, header, err = r.FormFile("image")
|
||
|
|
}
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "file field required")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
defer file.Close()
|
||
|
|
|
||
|
|
out, err := support.SaveKBImage(
|
||
|
|
s.Config.UploadDir,
|
||
|
|
s.Config.PublicAPIURL,
|
||
|
|
s.Config.TokenSigningSecret,
|
||
|
|
header.Filename,
|
||
|
|
header.Header.Get("Content-Type"),
|
||
|
|
file,
|
||
|
|
)
|
||
|
|
if err != nil {
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not upload kb image", err, support.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusCreated, out)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleAdminGetKBImage(w http.ResponseWriter, r *http.Request) {
|
||
|
|
name := chi.URLParam(r, "filename")
|
||
|
|
s.serveKBImage(w, r, name)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handlePublicKBImage(w http.ResponseWriter, r *http.Request) {
|
||
|
|
name := chi.URLParam(r, "filename")
|
||
|
|
sig := strings.TrimSpace(r.URL.Query().Get("sig"))
|
||
|
|
secret := strings.TrimSpace(s.Config.TokenSigningSecret)
|
||
|
|
if err := support.VerifyKBImageSig(secret, name, sig); err != nil {
|
||
|
|
Error(w, http.StatusForbidden, "invalid image signature")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
s.serveKBImage(w, r, name)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) serveKBImage(w http.ResponseWriter, r *http.Request, name string) {
|
||
|
|
f, contentType, err := support.OpenKBImage(s.Config.UploadDir, name)
|
||
|
|
if err != nil {
|
||
|
|
switch {
|
||
|
|
case errors.Is(err, support.ErrKBImageInvalidName), errors.Is(err, support.ErrKBImageForbidden):
|
||
|
|
Error(w, http.StatusBadRequest, "invalid image path")
|
||
|
|
case errors.Is(err, support.ErrKBImageNotFound):
|
||
|
|
Error(w, http.StatusNotFound, "image not found")
|
||
|
|
default:
|
||
|
|
Error(w, http.StatusInternalServerError, "could not open image")
|
||
|
|
}
|
||
|
|
return
|
||
|
|
}
|
||
|
|
defer f.Close()
|
||
|
|
|
||
|
|
st, err := f.Stat()
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusInternalServerError, "could not stat image")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
w.Header().Set("Content-Type", contentType)
|
||
|
|
w.Header().Set("Cache-Control", "public, max-age=86400")
|
||
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||
|
|
http.ServeContent(w, r, name, st.ModTime(), f)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleAdminGetKBArticle(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if s.Support == nil {
|
||
|
|
Error(w, http.StatusServiceUnavailable, "support unavailable")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := s.Support.GetKBArticle(r.Context(), id)
|
||
|
|
if err != nil {
|
||
|
|
if err == support.ErrKBNotFound {
|
||
|
|
Error(w, http.StatusNotFound, err.Error())
|
||
|
|
return
|
||
|
|
}
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not get kb article", err, support.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, item)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleAdminCreateKBArticle(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if s.Support == nil {
|
||
|
|
Error(w, http.StatusServiceUnavailable, "support unavailable")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var body support.KBArticleInput
|
||
|
|
if err := DecodeJSON(r, &body); err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid json")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := s.Support.CreateKBArticle(r.Context(), body)
|
||
|
|
if err != nil {
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not create kb article", err, support.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusCreated, item)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleAdminUpdateKBArticle(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if s.Support == nil {
|
||
|
|
Error(w, http.StatusServiceUnavailable, "support unavailable")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var body support.KBArticleInput
|
||
|
|
if err := DecodeJSON(r, &body); err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid json")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := s.Support.UpdateKBArticle(r.Context(), id, body)
|
||
|
|
if err != nil {
|
||
|
|
if err == support.ErrKBNotFound {
|
||
|
|
Error(w, http.StatusNotFound, err.Error())
|
||
|
|
return
|
||
|
|
}
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not update kb article", err, support.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, item)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleAdminDeleteKBArticle(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if s.Support == nil {
|
||
|
|
Error(w, http.StatusServiceUnavailable, "support unavailable")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if err := s.Support.DeleteKBArticle(r.Context(), id); err != nil {
|
||
|
|
if err == support.ErrKBNotFound {
|
||
|
|
Error(w, http.StatusNotFound, err.Error())
|
||
|
|
return
|
||
|
|
}
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not delete kb article", err, support.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
w.WriteHeader(http.StatusNoContent)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleAdminListReplyTemplates(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if s.Support == nil {
|
||
|
|
Error(w, http.StatusServiceUnavailable, "support unavailable")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
activeOnly := r.URL.Query().Get("active") == "1" || r.URL.Query().Get("active") == "true"
|
||
|
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||
|
|
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
||
|
|
items, total, err := s.Support.ListReplyTemplates(r.Context(), activeOnly, limit, offset)
|
||
|
|
if err != nil {
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not list templates", err, support.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, map[string]any{"items": items, "total": total})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleAdminGetReplyTemplate(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if s.Support == nil {
|
||
|
|
Error(w, http.StatusServiceUnavailable, "support unavailable")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := s.Support.GetReplyTemplate(r.Context(), id)
|
||
|
|
if err != nil {
|
||
|
|
if err == support.ErrTemplateNotFound {
|
||
|
|
Error(w, http.StatusNotFound, err.Error())
|
||
|
|
return
|
||
|
|
}
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not get template", err, support.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, item)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleAdminCreateReplyTemplate(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if s.Support == nil {
|
||
|
|
Error(w, http.StatusServiceUnavailable, "support unavailable")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var body support.ReplyTemplateInput
|
||
|
|
if err := DecodeJSON(r, &body); err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid json")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := s.Support.CreateReplyTemplate(r.Context(), body)
|
||
|
|
if err != nil {
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not create template", err, support.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusCreated, item)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleAdminUpdateReplyTemplate(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if s.Support == nil {
|
||
|
|
Error(w, http.StatusServiceUnavailable, "support unavailable")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var body support.ReplyTemplateInput
|
||
|
|
if err := DecodeJSON(r, &body); err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid json")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := s.Support.UpdateReplyTemplate(r.Context(), id, body)
|
||
|
|
if err != nil {
|
||
|
|
if err == support.ErrTemplateNotFound {
|
||
|
|
Error(w, http.StatusNotFound, err.Error())
|
||
|
|
return
|
||
|
|
}
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not update template", err, support.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, item)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleAdminDeleteReplyTemplate(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if s.Support == nil {
|
||
|
|
Error(w, http.StatusServiceUnavailable, "support unavailable")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if err := s.Support.DeleteReplyTemplate(r.Context(), id); err != nil {
|
||
|
|
if err == support.ErrTemplateNotFound {
|
||
|
|
Error(w, http.StatusNotFound, err.Error())
|
||
|
|
return
|
||
|
|
}
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not delete template", err, support.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
w.WriteHeader(http.StatusNoContent)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleAdminGetSupportAutoConfig(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if s.Support == nil {
|
||
|
|
Error(w, http.StatusServiceUnavailable, "support unavailable")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
cfg, err := s.Support.GetAutoConfig(r.Context())
|
||
|
|
if err != nil {
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not load auto config", err, support.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, cfg)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleAdminPutSupportAutoConfig(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if s.Support == nil {
|
||
|
|
Error(w, http.StatusServiceUnavailable, "support unavailable")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var body support.AutoConfigInput
|
||
|
|
if err := DecodeJSON(r, &body); err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid json")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
cfg, err := s.Support.UpdateAutoConfig(r.Context(), body)
|
||
|
|
if err != nil {
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not update auto config", err, support.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, cfg)
|
||
|
|
}
|