package main import ( "embed" "encoding/json" "io/fs" "log" "net/http" "strings" ) //go:embed web var webFS embed.FS type server struct { state *State mux *http.ServeMux } func newServer(st *State) *server { s := &server{state: st, mux: http.NewServeMux()} // Статика (HTML/CSS/JS из embed) sub, _ := fs.Sub(webFS, "web") s.mux.Handle("/", http.FileServer(http.FS(sub))) // API s.mux.HandleFunc("/api/state", s.handleState) s.mux.HandleFunc("/api/precheck", s.handlePrecheck) s.mux.HandleFunc("/api/config", s.handleConfig) s.mux.HandleFunc("/api/install", s.handleInstall) s.mux.HandleFunc("/api/events", s.handleSSE) s.mux.HandleFunc("/api/reset", s.handleReset) return s } func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Защита: только localhost (даже если addr 0.0.0.0 поставят) host := r.RemoteAddr if i := strings.LastIndex(host, ":"); i != -1 { host = host[:i] } switch host { case "127.0.0.1", "::1", "[::1]", "localhost": // ok default: http.Error(w, "installer is local-only", http.StatusForbidden) return } s.mux.ServeHTTP(w, r) } // GET /api/state — полный snapshot для холодного открытия страницы. func (s *server) handleState(w http.ResponseWriter, r *http.Request) { snap := s.state.Snapshot() writeJSON(w, snap) } // POST /api/precheck — запускает все pre-check проверки и возвращает результат. // Wizard переходит на стадию precheck. func (s *server) handlePrecheck(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "POST only", http.StatusMethodNotAllowed) return } s.state.setStage(StagePrecheck) results := runPrechecks(s.state.artifactsDir) s.state.setPrecheck(results) writeJSON(w, results) } // POST /api/config — сохраняет org INN, email, license. Переход на стадию config. func (s *server) handleConfig(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "POST only", http.StatusMethodNotAllowed) return } var c Config if err := json.NewDecoder(r.Body).Decode(&c); err != nil { http.Error(w, "invalid json", http.StatusBadRequest) return } s.state.setConfig(c) s.state.setStage(StageConfig) writeJSON(w, map[string]bool{"ok": true}) } // POST /api/install — стартует установку (в горутине), переход на стадию installing. // UI слушает /api/events для прогресса. func (s *server) handleInstall(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "POST only", http.StatusMethodNotAllowed) return } s.state.setStage(StageInstalling) go func() { if err := runInstallation(s.state); err != nil { log.Printf("install error: %v", err) s.state.setError(err.Error()) return } s.state.setStage(StageDone) }() writeJSON(w, map[string]bool{"ok": true}) } // POST /api/reset — сброс wizard'а на welcome (после ошибки). func (s *server) handleReset(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "POST only", http.StatusMethodNotAllowed) return } s.state.mu.Lock() s.state.Stage = StageWelcome s.state.ErrorMsg = "" s.state.Precheck = nil s.state.Steps = buildStepList() s.state.mu.Unlock() s.state.bus.publish(event{Type: "reset", Data: "{}"}) writeJSON(w, map[string]bool{"ok": true}) } func writeJSON(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") enc := json.NewEncoder(w) enc.SetIndent("", " ") _ = enc.Encode(v) }