feat(dashboard): expose app shell routes
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"html"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -189,6 +194,11 @@ func RegisterRoutes(
|
||||
// Swagger UI — interactive API documentation
|
||||
engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
|
||||
// Dashboard shell routes used by Chatwoot mailer and push deep links.
|
||||
// Reference: Chatwoot routes.rb `get '/app'`, `get '/app/*params'` -> DashboardController#index.
|
||||
engine.GET("/app", dashboardIndex)
|
||||
engine.GET("/app/*params", dashboardIndex)
|
||||
|
||||
// Auth routes — PUBLIC, no AuthRequired middleware
|
||||
v1.RegisterAuthRoutes(engine.Group("/api/v1"), handlers.Auth)
|
||||
v1.RegisterChatwootAuthRoutes(engine.Group("/auth"), handlers.Auth)
|
||||
@@ -1963,3 +1973,63 @@ func healthCheck(c *gin.Context) {
|
||||
"uptimeSeconds": uint64(uptime.Seconds()),
|
||||
})
|
||||
}
|
||||
|
||||
func dashboardIndex(c *gin.Context) {
|
||||
if dashboardWantsJSON(c) {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": "Please use API routes instead of dashboard routes for JSON requests"})
|
||||
return
|
||||
}
|
||||
|
||||
installationName := strings.TrimSpace(os.Getenv("INSTALLATION_NAME"))
|
||||
if installationName == "" {
|
||||
installationName = "GoChat"
|
||||
}
|
||||
frontendURL := strings.TrimRight(os.Getenv("FRONTEND_URL"), "/")
|
||||
helpCenterURL := strings.TrimRight(os.Getenv("HELPCENTER_URL"), "/")
|
||||
|
||||
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(dashboardHTML(installationName, frontendURL, helpCenterURL)))
|
||||
}
|
||||
|
||||
func dashboardWantsJSON(c *gin.Context) bool {
|
||||
if strings.HasSuffix(c.Request.URL.Path, ".json") {
|
||||
return true
|
||||
}
|
||||
accept := strings.ToLower(c.GetHeader("Accept"))
|
||||
return strings.Contains(accept, "application/json") && !strings.Contains(accept, "text/html")
|
||||
}
|
||||
|
||||
func dashboardHTML(installationName string, frontendURL string, helpCenterURL string) string {
|
||||
name := html.EscapeString(installationName)
|
||||
chatwootConfig := dashboardJSON(map[string]any{
|
||||
"hostURL": frontendURL,
|
||||
"helpCenterURL": helpCenterURL,
|
||||
"allowedLoginMethods": []string{"email"},
|
||||
"signupEnabled": "false",
|
||||
"isEnterprise": "true",
|
||||
"selectedLocale": "en",
|
||||
})
|
||||
globalConfig := dashboardJSON(map[string]any{"INSTALLATION_NAME": installationName})
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>` + name + `</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=0">
|
||||
<script>
|
||||
window.chatwootConfig = ` + chatwootConfig + `;
|
||||
window.globalConfig = ` + globalConfig + `;
|
||||
</script>
|
||||
</head>
|
||||
<body class="text-slate-600">
|
||||
<div id="app"></div>
|
||||
<noscript id="noscript">This app works best with JavaScript enabled.</noscript>
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
|
||||
func dashboardJSON(value any) string {
|
||||
b, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return "{}"
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -31,6 +34,8 @@ func TestRegisterRoutesBootsWithChatwootParityConflictGroups(t *testing.T) {
|
||||
}
|
||||
|
||||
expected := []string{
|
||||
"GET /app",
|
||||
"GET /app/*params",
|
||||
"GET /api/v1/accounts/:account_id/captain/assistants/tools",
|
||||
"GET /api/v1/accounts/:account_id/captain/assistants/:assistant_id",
|
||||
"GET /api/v1/widget/conversations",
|
||||
@@ -63,3 +68,49 @@ func TestRegisterRoutesBootsWithChatwootParityConflictGroups(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDashboardIndexServesChatwootShell(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
t.Setenv("INSTALLATION_NAME", "GoChat Test")
|
||||
t.Setenv("FRONTEND_URL", "https://app.example.test/")
|
||||
t.Setenv("HELPCENTER_URL", "https://help.example.test/")
|
||||
|
||||
engine := gin.New()
|
||||
engine.GET("/app", dashboardIndex)
|
||||
engine.GET("/app/*params", dashboardIndex)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/app/accounts/1/conversations/42", nil)
|
||||
req.Header.Set("Accept", "text/html")
|
||||
engine.ServeHTTP(recorder, req)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", recorder.Code)
|
||||
}
|
||||
body := recorder.Body.String()
|
||||
if !strings.Contains(body, `<div id="app"></div>`) {
|
||||
t.Fatalf("expected dashboard app mount in response: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, `"hostURL":"https://app.example.test"`) {
|
||||
t.Fatalf("expected frontend URL in chatwoot config: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDashboardIndexRejectsJSONLikeChatwoot(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
engine := gin.New()
|
||||
engine.GET("/app", dashboardIndex)
|
||||
engine.GET("/app/*params", dashboardIndex)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/app/accounts/1/conversations/42", nil)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
engine.ServeHTTP(recorder, req)
|
||||
|
||||
if recorder.Code != http.StatusNotAcceptable {
|
||||
t.Fatalf("expected 406, got %d", recorder.Code)
|
||||
}
|
||||
if !strings.Contains(recorder.Body.String(), "Please use API routes instead of dashboard routes for JSON requests") {
|
||||
t.Fatalf("expected Chatwoot dashboard JSON error, got %s", recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user