Update application UI and functionality

This commit is contained in:
2026-07-26 16:20:36 +08:00
parent b9aff58f32
commit 97ea6fb7aa
48 changed files with 2790 additions and 628 deletions
@@ -3,10 +3,13 @@ package web
import (
"bytes"
"errors"
"fmt"
"log"
"mime"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"time"
@@ -46,6 +49,7 @@ func serveStaticAsset(w http.ResponseWriter, req *http.Request, root, embedRoot,
if serveEmbeddedFile(w, req, embedRoot+"/"+filepath.ToSlash(assetPath)) {
return
}
w.Header().Set("Cache-Control", "no-store")
http.NotFound(w, req)
}
@@ -53,6 +57,29 @@ func (r *router) serveServerAsset(w http.ResponseWriter, req *http.Request, asse
serveStaticAsset(w, req, filepath.Join(r.cfg.BaseDir, "assets"), "", assetPath)
}
func (r *router) serveAdminAsset(w http.ResponseWriter, req *http.Request, assetPath string) {
if strings.Contains(assetPath, "..") || strings.ContainsAny(assetPath, `\`) {
writeError(w, http.StatusForbidden, "FORBIDDEN", errors.New("invalid asset path"))
return
}
setStaticCacheHeaders(w, assetPath)
if err := validateAdminDiskBuild(r.cfg.AdminWebDir); err == nil {
if tryServeDiskFile(w, req, r.cfg.AdminWebDir, assetPath) {
return
}
log.Printf("admin web disk build is missing requested asset: %s", assetPath)
} else {
if !errors.Is(err, os.ErrNotExist) {
log.Printf("admin web disk build is incomplete; serving embedded assets: %v", err)
}
if serveEmbeddedFile(w, req, "admin/dist/"+filepath.ToSlash(assetPath)) {
return
}
}
w.Header().Set("Cache-Control", "no-store")
http.NotFound(w, req)
}
func serveSetupServerAsset(w http.ResponseWriter, req *http.Request, cfgRoot, assetPath string) {
serveStaticAsset(w, req, filepath.Join(cfgRoot, "assets"), "", assetPath)
}
@@ -108,11 +135,13 @@ func (r *router) servePortal(w http.ResponseWriter, req *http.Request) {
}
func (r *router) serveAdmin(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Cache-Control", "no-store, must-revalidate")
index := filepath.Join(r.cfg.AdminWebDir, "index.html")
if _, err := os.Stat(index); err == nil {
if err := validateAdminDiskBuild(r.cfg.AdminWebDir); err == nil {
http.ServeFile(w, req, index)
return
} else if !errors.Is(err, os.ErrNotExist) {
log.Printf("admin web disk build is incomplete: %v", err)
}
if serveEmbeddedFile(w, req, "admin/dist/index.html") {
return
@@ -121,6 +150,35 @@ func (r *router) serveAdmin(w http.ResponseWriter, req *http.Request) {
_, _ = w.Write([]byte(`<!doctype html><html><head><meta charset="utf-8"><title>YMhut Admin</title></head><body><main><h1>YMhut Admin</h1><p>Build web/admin to enable the Vue console.</p></main></body></html>`))
}
var adminAssetReferencePattern = regexp.MustCompile(`(?:src|href)=["'](/admin/assets/[^"'?#]+)`)
func validateAdminDiskBuild(root string) error {
index := filepath.Join(root, "index.html")
data, err := os.ReadFile(index)
if err != nil {
return err
}
matches := adminAssetReferencePattern.FindAllSubmatch(data, -1)
if len(matches) == 0 {
return errors.New("index.html does not reference any admin assets")
}
for _, match := range matches {
assetPath := strings.TrimPrefix(string(match[1]), "/admin/")
if strings.Contains(assetPath, "..") || strings.ContainsAny(assetPath, `\`) {
return fmt.Errorf("invalid admin asset reference %s", assetPath)
}
path := filepath.Join(root, filepath.FromSlash(assetPath))
info, statErr := os.Stat(path)
if statErr != nil {
return fmt.Errorf("missing %s: %w", assetPath, statErr)
}
if info.IsDir() {
return fmt.Errorf("%s is not a file", assetPath)
}
}
return nil
}
func setStaticCacheHeaders(w http.ResponseWriter, assetPath string) {
extension := strings.ToLower(filepath.Ext(assetPath))
if strings.HasPrefix(filepath.ToSlash(assetPath), "assets/") && extension != ".ico" {