fix(server): serve the Admin SPA at / when dist exists (#346)

go-admin registered its welcome page on GET / in every non-prod mode, so a
reverse proxy that forwarded / to the server showed 「GO-ADMIN欢迎您」
instead of the Admin (happened after the 2026-09-28 server migration).
When dist/index.html exists, / now returns it; without a dist (vite
development) the previous welcome/prod behaviour is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
This commit is contained in:
QiuSW
2026-09-28 16:07:49 +08:00
co-authored by Claude Opus 5.5
parent 9dace6ac89
commit 0b01b92c98
3 changed files with 78 additions and 9 deletions
+34 -6
View File
@@ -25,12 +25,8 @@ const SPADirEnv = "GOAUTO_WEB_DIST"
// dist; a NoRoute handler installed anyway would turn every genuine 404 into
// an HTML page, which is far more confusing than a plain 404.
func InitSPARouter(engine *gin.Engine) {
dist := strings.TrimSpace(os.Getenv(SPADirEnv))
if dist == "" {
dist = "dist"
}
index := filepath.Join(dist, "index.html")
if _, err := os.Stat(index); err != nil {
dist, index, ok := spaIndex()
if !ok {
return
}
@@ -56,6 +52,38 @@ func InitSPARouter(engine *gin.Engine) {
})
}
// spaIndex resolves the built frontend directory and reports whether its
// index.html exists.
func spaIndex() (dist, index string, ok bool) {
dist = strings.TrimSpace(os.Getenv(SPADirEnv))
if dist == "" {
dist = "dist"
}
index = filepath.Join(dist, "index.html")
if _, err := os.Stat(index); err != nil {
return dist, index, false
}
return dist, index, true
}
// registerRootRoute decides what `GET /` returns (#346).
//
// `[必须]` When the built frontend exists, `/` must be the Admin SPA. go-admin's
// welcome page used to own `/` in every non-prod mode, so any reverse proxy
// that forwarded `/` to this server (instead of serving dist itself) showed
// "GO-ADMIN欢迎您" instead of the Admin — which is exactly what happened after
// the 2026-09-28 server migration. The welcome page is kept only for
// development without a dist, where the frontend runs under vite.
func registerRootRoute(r gin.IRoutes, mode string, welcome gin.HandlerFunc) {
if _, index, ok := spaIndex(); ok {
r.GET("/", func(c *gin.Context) { c.File(index) })
return
}
if mode != "prod" {
r.GET("/", welcome)
}
}
// isAPIPath reports whether a path belongs to the server rather than the SPA.
func isAPIPath(path string) bool {
for _, prefix := range []string{"/api/", "/swagger/", "/static/", "/form-generator/", "/ws/", "/wslogout/", "/info"} {
+43
View File
@@ -91,3 +91,46 @@ func TestWithoutDistNoFallbackIsInstalled(t *testing.T) {
t.Fatalf("没有 dist 时接口仍应正常: %d", response.Code)
}
}
// #346: with a built frontend, `/` must be the Admin SPA — never go-admin's
// welcome page, even in non-prod modes where the welcome page used to own `/`.
func TestRootServesSPAWhenDistExists(t *testing.T) {
gin.SetMode(gin.TestMode)
dist := filepath.Join(t.TempDir(), "dist")
if err := os.MkdirAll(dist, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dist, "index.html"), []byte("<!doctype html>SPA"), 0o644); err != nil {
t.Fatal(err)
}
t.Setenv(SPADirEnv, dist)
for _, mode := range []string{"dev", "test", "prod"} {
engine := gin.New()
registerRootRoute(engine, mode, func(c *gin.Context) { c.String(http.StatusOK, "GO-ADMIN欢迎您") })
InitSPARouter(engine)
response := do(engine, http.MethodGet, "/")
if response.Code != http.StatusOK || response.Body.String() != "<!doctype html>SPA" {
t.Fatalf("mode=%s: / should serve index.html, got %d %q", mode, response.Code, response.Body.String())
}
}
}
// Without a dist (development under vite) the previous behaviour is kept:
// welcome page outside prod, nothing registered in prod.
func TestRootWithoutDistKeepsPreviousBehaviour(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Setenv(SPADirEnv, filepath.Join(t.TempDir(), "missing-dist"))
welcome := func(c *gin.Context) { c.String(http.StatusOK, "GO-ADMIN欢迎您") }
dev := gin.New()
registerRootRoute(dev, "dev", welcome)
if response := do(dev, http.MethodGet, "/"); response.Code != http.StatusOK || response.Body.String() != "GO-ADMIN欢迎您" {
t.Fatalf("dev without dist should keep the welcome page, got %d %q", response.Code, response.Body.String())
}
prod := gin.New()
registerRootRoute(prod, "prod", welcome)
if response := do(prod, http.MethodGet, "/"); response.Code != http.StatusNotFound {
t.Fatalf("prod without dist should not register /, got %d", response.Code)
}
}
+1 -3
View File
@@ -40,9 +40,7 @@ func sysBaseRouter(r *gin.RouterGroup) {
go ws.WebsocketManager.SendService()
go ws.WebsocketManager.SendAllService()
if config.ApplicationConfig.Mode != "prod" {
r.GET("/", apis.GoAdmin)
}
registerRootRoute(r, config.ApplicationConfig.Mode, apis.GoAdmin)
r.GET("/info", handler.Ping)
}