feat(dashboard): expose app shell routes

This commit is contained in:
2026-06-06 08:44:26 +08:00
parent 32084f9f4c
commit 485373be80
6 changed files with 139 additions and 9 deletions
+70
View File
@@ -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)
}
+51
View File
@@ -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())
}
}