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) }