The repo already layered file defaults under environment overrides, on both the backend (settings.yml < GOAUTO_*) and the frontend (.env.production < process.env). What was missing was a translator for production: config.yaml only ever existed for the PowerShell launchers, so a packaged binary read none of it and had no database credentials either — SYB was inheriting an existing gap, not creating one. The server now reads config.yaml itself, between settings.yml and the environment. Lookup is GOAUTO_CONFIG, then ./config.yaml, then beside the executable, so a packaged binary works wherever it is started. An absent file is not an error: containers supply everything through the environment. Scalars are read by YAML type and coerced, so an unquoted all-digit password cannot take startup down over a quoting detail. This removed the need for a Read-SybConfig in PowerShell: the launcher just hands over the path it already knows, rather than reimplementing a YAML parser. The server also serves the built frontend when dist is present, which is what .env.production's empty VUE_APP_BASE_API already assumes. The history fallback is restricted to non-API GETs, and is not installed at all without dist, so development 404s stay 404s. Precedence is mutation-tested: applying the local file after the environment instead of before makes the layering test fail. Not verified: the PowerShell change and any Windows deployment — both need a run on the Windows side. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
73 lines
2.2 KiB
Go
73 lines
2.2 KiB
Go
package router
|
|
|
|
import (
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// SPADirEnv overrides where the built frontend lives. Without it the server
|
|
// looks for ./dist next to the working directory.
|
|
const SPADirEnv = "GOAUTO_WEB_DIST"
|
|
|
|
// InitSPARouter serves the built Vue app from the same origin as the API.
|
|
//
|
|
// Same-origin is what web/.env.production already assumes: VUE_APP_BASE_API is
|
|
// empty there, so the bundle issues relative requests. Serving dist from this
|
|
// binary makes that assumption true without a second web server and without
|
|
// CORS.
|
|
//
|
|
// `[必须]` Registering this is conditional on dist actually existing. In
|
|
// development the frontend runs under vite on its own port and there is no
|
|
// 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 {
|
|
return
|
|
}
|
|
|
|
engine.Static("/assets", filepath.Join(dist, "assets"))
|
|
for _, name := range []string{"favicon.ico", "logo.png"} {
|
|
if path := filepath.Join(dist, name); fileExists(path) {
|
|
engine.StaticFile("/"+name, path)
|
|
}
|
|
}
|
|
|
|
// History-mode routing means any unmatched non-API GET is a client route
|
|
// and must return index.html so the app can boot and route it itself.
|
|
//
|
|
// `[必须]` API paths are excluded. Answering a mistyped API call with 200
|
|
// and an HTML body would surface in the client as a JSON parse error
|
|
// instead of the 404 it actually is.
|
|
engine.NoRoute(func(c *gin.Context) {
|
|
if c.Request.Method != http.MethodGet || isAPIPath(c.Request.URL.Path) {
|
|
c.Status(http.StatusNotFound)
|
|
return
|
|
}
|
|
c.File(index)
|
|
})
|
|
}
|
|
|
|
// 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"} {
|
|
if strings.HasPrefix(path, prefix) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func fileExists(path string) bool {
|
|
info, err := os.Stat(path)
|
|
return err == nil && !info.IsDir()
|
|
}
|