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