feat(help-center): expose public portal show
This commit is contained in:
@@ -65,6 +65,9 @@ func (h *ArticleHandler) PublicList(c *gin.Context) {
|
||||
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to list articles")
|
||||
return
|
||||
}
|
||||
for i := range articles {
|
||||
articles[i].Portal = *portal
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"payload": publicArticlePayloads(articles, portal.Slug),
|
||||
@@ -747,14 +750,38 @@ func publicPortalPayload(portal *model.Portal) gin.H {
|
||||
if portal == nil {
|
||||
return gin.H{}
|
||||
}
|
||||
return gin.H{
|
||||
categories := make([]gin.H, 0, len(portal.Categories))
|
||||
for i := range portal.Categories {
|
||||
categories = append(categories, publicCategoryPayload(&portal.Categories[i]))
|
||||
}
|
||||
payload := gin.H{
|
||||
"custom_domain": portal.CustomDomain,
|
||||
"header_text": portal.HeaderText,
|
||||
"homepage_link": portal.HomepageLink,
|
||||
"name": portal.Name,
|
||||
"page_title": portal.PageTitle,
|
||||
"slug": portal.Slug,
|
||||
"categories": categories,
|
||||
"meta": gin.H{
|
||||
"articles_count": publicPortalPublishedArticleCount(portal.Articles),
|
||||
"categories_count": len(portal.Categories),
|
||||
"default_locale": portalDefaultLocale(portal),
|
||||
},
|
||||
}
|
||||
if portal.LogoURL != "" {
|
||||
payload["logo"] = gin.H{"file_url": portal.LogoURL, "portal_id": portal.ID, "account_id": portal.AccountID}
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func publicPortalPublishedArticleCount(articles []model.Article) int {
|
||||
count := 0
|
||||
for _, article := range articles {
|
||||
if article.Status == string(model.ArticleStatusPublished) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func publicAuthorPayload(user *model.User) gin.H {
|
||||
|
||||
@@ -310,6 +310,8 @@ func (s *ArticleHandlerTestSuite) TestPublicList_WidgetPopularArticles() {
|
||||
assert.Equal(s.T(), "hc/test-portal/articles/public-high-views", first["link"])
|
||||
assert.Equal(s.T(), "guides", first["category"].(map[string]interface{})["slug"])
|
||||
assert.Equal(s.T(), "Writer", first["author"].(map[string]interface{})["available_name"])
|
||||
portal := first["portal"].(map[string]interface{})
|
||||
assert.EqualValues(s.T(), 3, portal["meta"].(map[string]interface{})["articles_count"])
|
||||
meta := resp["meta"].(map[string]interface{})
|
||||
assert.EqualValues(s.T(), 2, meta["articles_count"])
|
||||
}
|
||||
|
||||
@@ -23,6 +23,26 @@ func NewPortalHandler(svc *service.PortalService) *PortalHandler {
|
||||
return &PortalHandler{svc: svc}
|
||||
}
|
||||
|
||||
// PublicRedirectDefaultLocale redirects /hc/:slug to the portal default locale.
|
||||
// GET /hc/:slug
|
||||
func (h *PortalHandler) PublicRedirectDefaultLocale(c *gin.Context) {
|
||||
portal, ok := h.resolvePublicPortal(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
c.Redirect(http.StatusFound, "/hc/"+portal.Slug+"/"+portalDefaultLocale(portal))
|
||||
}
|
||||
|
||||
// PublicGet returns the public help-center portal JSON payload.
|
||||
// GET /hc/:slug/:locale
|
||||
func (h *PortalHandler) PublicGet(c *gin.Context) {
|
||||
portal, ok := h.resolvePublicPortal(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, publicPortalPayload(portal))
|
||||
}
|
||||
|
||||
// Create creates a new portal.
|
||||
// POST /api/v1/accounts/:account_id/portals
|
||||
func (h *PortalHandler) Create(c *gin.Context) {
|
||||
@@ -240,6 +260,28 @@ func (h *PortalHandler) SSLStatus(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, portalSSLStatusPayload(portal))
|
||||
}
|
||||
|
||||
func (h *PortalHandler) resolvePublicPortal(c *gin.Context) (*model.Portal, bool) {
|
||||
slug := strings.TrimSuffix(c.Param("slug"), ".json")
|
||||
portal, err := h.svc.ResolvePublicBySlug(c.Request.Context(), slug)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "portal not found")
|
||||
return nil, false
|
||||
}
|
||||
return portal, true
|
||||
}
|
||||
|
||||
func portalDefaultLocale(portal *model.Portal) string {
|
||||
if portal == nil {
|
||||
return "en"
|
||||
}
|
||||
config := portalConfig(portal.PortalConfiguration)
|
||||
defaultLocale := configString(config, "default_locale", portal.Locale)
|
||||
if defaultLocale == "" {
|
||||
return "en"
|
||||
}
|
||||
return strings.TrimSuffix(defaultLocale, ".json")
|
||||
}
|
||||
|
||||
func portalPayload(portal *model.Portal, locale string, currentUserID uint) gin.H {
|
||||
if portal == nil {
|
||||
return gin.H{}
|
||||
|
||||
@@ -44,6 +44,8 @@ func (s *PortalHandlerTestSuite) SetupSuite() {
|
||||
}
|
||||
|
||||
func (s *PortalHandlerTestSuite) SetupTest() {
|
||||
s.db.Exec("DELETE FROM articles")
|
||||
s.db.Exec("DELETE FROM categories")
|
||||
s.db.Exec("DELETE FROM portals")
|
||||
}
|
||||
|
||||
@@ -125,6 +127,70 @@ func (s *PortalHandlerTestSuite) TestGet_BySlugReturnsChatwootMeta() {
|
||||
assert.Len(s.T(), locales, 2)
|
||||
}
|
||||
|
||||
func (s *PortalHandlerTestSuite) TestPublicRedirectDefaultLocale() {
|
||||
portal := &model.Portal{AccountID: s.account.ID, Name: "Public", Slug: "public", Locale: "en", PortalConfiguration: json.RawMessage(`{"default_locale":"fr"}`)}
|
||||
s.Require().NoError(s.db.Create(portal).Error)
|
||||
|
||||
r := gin.New()
|
||||
r.GET("/hc/:slug", s.handler.PublicRedirectDefaultLocale)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/hc/public", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusFound, w.Code)
|
||||
assert.Equal(s.T(), "/hc/public/fr", w.Header().Get("Location"))
|
||||
}
|
||||
|
||||
func (s *PortalHandlerTestSuite) TestPublicGet_ReturnsChatwootHCPortalPayload() {
|
||||
portal := &model.Portal{AccountID: s.account.ID, Name: "Help", Slug: "help", HeaderText: "How can we help?", HomepageLink: "https://example.com", PageTitle: "Help Center", LogoURL: "https://cdn.example/logo.png", Locale: "en", PortalConfiguration: json.RawMessage(`{"default_locale":"en"}`)}
|
||||
s.Require().NoError(s.db.Create(portal).Error)
|
||||
category := &model.Category{AccountID: s.account.ID, PortalID: portal.ID, Name: "Basics", Slug: "basics", Locale: "en", Description: "Start here", Position: 1}
|
||||
s.Require().NoError(s.db.Create(category).Error)
|
||||
s.Require().NoError(s.db.Create(&model.Article{AccountID: s.account.ID, PortalID: portal.ID, CategoryID: &category.ID, Title: "Published", Slug: "published", Status: "published", Locale: "en"}).Error)
|
||||
s.Require().NoError(s.db.Create(&model.Article{AccountID: s.account.ID, PortalID: portal.ID, CategoryID: &category.ID, Title: "Draft", Slug: "draft", Status: "draft", Locale: "en"}).Error)
|
||||
|
||||
r := gin.New()
|
||||
r.GET("/hc/:slug/:locale", s.handler.PublicGet)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/hc/help/en.json", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var payload map[string]any
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &payload))
|
||||
assert.Equal(s.T(), "help", payload["slug"])
|
||||
assert.Equal(s.T(), "How can we help?", payload["header_text"])
|
||||
assert.NotContains(s.T(), payload, "id")
|
||||
assert.NotContains(s.T(), payload, "account_id")
|
||||
categories := payload["categories"].([]any)
|
||||
assert.Len(s.T(), categories, 1)
|
||||
cat := categories[0].(map[string]any)
|
||||
assert.Equal(s.T(), "basics", cat["slug"])
|
||||
assert.EqualValues(s.T(), 1, cat["meta"].(map[string]any)["articles_count"])
|
||||
meta := payload["meta"].(map[string]any)
|
||||
assert.EqualValues(s.T(), 1, meta["articles_count"])
|
||||
assert.EqualValues(s.T(), 1, meta["categories_count"])
|
||||
assert.Equal(s.T(), "en", meta["default_locale"])
|
||||
logo := payload["logo"].(map[string]any)
|
||||
assert.Equal(s.T(), "https://cdn.example/logo.png", logo["file_url"])
|
||||
}
|
||||
|
||||
func (s *PortalHandlerTestSuite) TestPublicGet_NotFoundForArchivedPortal() {
|
||||
portal := &model.Portal{AccountID: s.account.ID, Name: "Archived", Slug: "archived", Archived: true}
|
||||
s.Require().NoError(s.db.Create(portal).Error)
|
||||
|
||||
r := gin.New()
|
||||
r.GET("/hc/:slug/:locale", s.handler.PublicGet)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/hc/archived/en", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
||||
func (s *PortalHandlerTestSuite) TestUpdate_Success() {
|
||||
portal := &model.Portal{AccountID: s.account.ID, Name: "update-portal", Slug: "update-slug"}
|
||||
s.Require().NoError(s.db.Create(portal).Error)
|
||||
|
||||
Reference in New Issue
Block a user