feat: живой цикл M2M с НРД + мастер установки ключа на флешку
Инфраструктура M2M (живой обмен с НРД через ИШ): - обработка M2MTransferResponse: ERROR(M2Mxx) → заявка Отклонена, сохранение ответа; INFO → ждём Decision; идемпотентность поллера - fallback-корреляция ответов с нулевым GUID (M2M14/M2M17) по FIFO - сырой XML ответа НРД в карточке заявки (для пересылки в ТП) - тестовый пакет роботу приведён к эталону m2m_robot_samples (CostInfo=Yes, 4 бумаги, IsolationStatus, DocumentSeries=сценарий); override паспорта - редирект из теста сразу в карточку заявки Мастер установки ключа Валидаты на флешку (admin/setup/keywizard): - пошаговый: загрузка .7z+пароль → выбор флешки → запись → справочник сертификатов (CRL) → перезапуск+проверка ИШ → готово - привилегированный воркер (bj-keymedia) в host-namespace через файл-обмен, bj-server остаётся в песочнице - сохранение структуры профиля архива (spr<N>), перечисление съёмных USB Прочее: - пакет-доказательство для ТП НРД + форма регистрации участника M2M - эталонные образцы робота (DOC/m2m_robot_samples) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user