118 lines
4.0 KiB
Go
118 lines
4.0 KiB
Go
package frontend
|
|
|
|
import (
|
|
"autolibrary.top/webserver/config"
|
|
"autolibrary.top/webserver/frontend/handlers/errors_handler"
|
|
"autolibrary.top/webserver/utils"
|
|
"context"
|
|
"fmt"
|
|
"github.com/gin-gonic/gin"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
var hashedAsset = regexp.MustCompile(`-[a-zA-Z0-9_-]{8,}\.[a-z0-9]+$`)
|
|
|
|
func createProxy(targetURL string) gin.HandlerFunc {
|
|
target, _ := url.Parse(targetURL)
|
|
proxy := httputil.NewSingleHostReverseProxy(target)
|
|
proxy.ErrorHandler = func(w http.ResponseWriter, _ *http.Request, err error) {
|
|
log.Printf("[W] backend proxy: %v", err)
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
w.WriteHeader(http.StatusBadGateway)
|
|
_, _ = w.Write([]byte(`{"code":502,"msg":"Backend service is unavailable","data":null}`))
|
|
}
|
|
return func(c *gin.Context) { c.Request.URL.Path = c.Param("path"); proxy.ServeHTTP(c.Writer, c.Request) }
|
|
}
|
|
|
|
// ResolveStaticPath also accepts the legacy public configuration if a sibling
|
|
// production build exists. The configured path can be absolute or relative.
|
|
func ResolveStaticPath(configured string) (string, error) {
|
|
path := filepath.Clean(configured)
|
|
if filepath.Base(path) == "public" {
|
|
sibling := filepath.Join(filepath.Dir(path), "dist")
|
|
if _, err := os.Stat(filepath.Join(sibling, "index.html")); err == nil {
|
|
path = sibling
|
|
}
|
|
}
|
|
if _, err := os.Stat(filepath.Join(path, "index.html")); err != nil {
|
|
return "", fmt.Errorf("frontend build missing at %s: run npm --prefix frontend run build: %w", path, err)
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
func NewRouter(cfg *config.Config) (*gin.Engine, error) {
|
|
staticPath, err := ResolveStaticPath(cfg.Frontend.Static.Path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
entry, err := os.ReadFile(filepath.Join(staticPath, "index.html"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
gin.SetMode(gin.ReleaseMode)
|
|
r := gin.New()
|
|
r.Use(gin.Recovery())
|
|
pages := errors_handler.New(entry)
|
|
r.Use(pages.APIAccessMiddleware())
|
|
r.Use(func(c *gin.Context) {
|
|
c.Header("X-Content-Type-Options", "nosniff")
|
|
if strings.HasPrefix(c.Request.URL.Path, "/assets/") {
|
|
if hashedAsset.MatchString(c.Request.URL.Path) {
|
|
c.Header("Cache-Control", "public, max-age=31536000, immutable")
|
|
} else {
|
|
c.Header("Cache-Control", "public, max-age=3600")
|
|
}
|
|
} else {
|
|
c.Header("Cache-Control", "no-cache")
|
|
}
|
|
c.Next()
|
|
})
|
|
r.Static("/assets", filepath.Join(staticPath, "assets"))
|
|
backendHost := cfg.Backend.Host
|
|
if backendHost == "0.0.0.0" || backendHost == "::" || backendHost == "" {
|
|
backendHost = "127.0.0.1"
|
|
}
|
|
r.Any("/api/*path", createProxy("http://"+net.JoinHostPort(backendHost, cfg.Backend.Port)))
|
|
serve := func(c *gin.Context) { c.Data(http.StatusOK, "text/html; charset=utf-8", entry) }
|
|
for _, path := range []string{"/", "/downloads", "/marketplace", "/marketplace/upload", "/marketplace/profile", "/marketplace/admin"} {
|
|
r.GET(path, serve)
|
|
r.HEAD(path, serve)
|
|
}
|
|
for _, method := range []string{http.MethodGet, http.MethodHead} {
|
|
r.Handle(method, "/changelog", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/downloads") })
|
|
}
|
|
pages.RegisterGetErrorRoutes(r, []int{400, 401, 403, 404, 500, 502, 503})
|
|
r.NoRoute(func(c *gin.Context) {
|
|
if c.Request.URL.Path == "/manuals" || strings.HasPrefix(c.Request.URL.Path, "/manuals/") {
|
|
c.Redirect(http.StatusMovedPermanently, "https://manuals.autolibrary.kenanzhu.com")
|
|
return
|
|
}
|
|
if strings.HasPrefix(c.Request.URL.Path, "/api/") {
|
|
c.JSON(http.StatusNotFound, gin.H{"code": 404, "msg": "API route not found", "data": nil})
|
|
return
|
|
}
|
|
pages.GetErrorsPage(c, http.StatusNotFound)
|
|
})
|
|
return r, nil
|
|
}
|
|
|
|
func StartFrontend() error { return StartFrontendContext(context.Background()) }
|
|
func StartFrontendContext(ctx context.Context) error {
|
|
cfg := config.GetConfig()
|
|
router, err := NewRouter(cfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
address := net.JoinHostPort(cfg.Frontend.Host, cfg.Frontend.Port)
|
|
log.Printf("[I] frontend server is running on: http://%s", address)
|
|
return utils.ServeHTTP(ctx, address, router)
|
|
}
|