Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/api/internal/openplatform/standalone.go
2026-07-27 16:46:15 +08:00

102 lines
3.2 KiB
Go

package openplatform
import (
"database/sql"
"encoding/json"
"net/http"
"os"
"path/filepath"
"strings"
"time"
)
type StandaloneConfig struct {
StaticDir string
SessionTTL time.Duration
RequestTimeout time.Duration
Release string
TDengine *sql.DB
TDengineDatabase string
}
func NewStandaloneServer(db *sql.DB, cfg StandaloneConfig) http.Handler {
repository := NewMySQLRepository(db)
if cfg.TDengine != nil {
repository.WithTDengine(cfg.TDengine, cfg.TDengineDatabase)
}
service := NewService(repository)
portal := NewPortalService(db, cfg.SessionTTL)
external := NewExternalHandler(service, portal)
api := http.NewServeMux()
api.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
writeStandaloneJSON(w, http.StatusOK, map[string]any{
"status": "ok",
"service": "open-platform-api",
"release": strings.TrimSpace(cfg.Release),
})
})
api.Handle("/", external)
handler := standaloneStatic(cfg.StaticDir, api)
handler = WithDocs(handler)
handler = standaloneSecurityHeaders(handler)
timeout := cfg.RequestTimeout
if timeout <= 0 {
timeout = 10 * time.Second
}
return http.TimeoutHandler(handler, timeout, `{"error":{"code":"REQUEST_TIMEOUT","message":"请求处理超时"}}`)
}
func standaloneStatic(dir string, fallback http.Handler) http.Handler {
dir = strings.TrimSpace(dir)
if dir == "" {
return fallback
}
files := http.FileServer(http.Dir(dir))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if isStandaloneAPIRoute(r.URL.Path) {
fallback.ServeHTTP(w, r)
return
}
clean := filepath.Clean("/" + r.URL.Path)
path := filepath.Join(dir, clean)
if info, err := os.Stat(path); err == nil && !info.IsDir() {
if clean == "/index.html" {
w.Header().Set("Cache-Control", "no-cache")
} else if strings.HasPrefix(clean, "/assets/") {
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
}
files.ServeHTTP(w, r)
return
}
w.Header().Set("Cache-Control", "no-cache")
http.ServeFile(w, r, filepath.Join(dir, "index.html"))
})
}
func isStandaloneAPIRoute(path string) bool {
return path == "/healthz" ||
strings.HasPrefix(path, "/api/") ||
strings.HasPrefix(path, "/portal-api/") ||
strings.HasPrefix(path, "/open-api/")
}
func standaloneSecurityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
w.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'")
next.ServeHTTP(w, r)
})
}
func writeStandaloneJSON(w http.ResponseWriter, status int, body any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(body)
}