diff --git a/internal/lkgateway/admin.go b/internal/lkgateway/admin.go index d1379a7..2bdbedb 100644 --- a/internal/lkgateway/admin.go +++ b/internal/lkgateway/admin.go @@ -21,6 +21,7 @@ var templatesFS embed.FS type admin struct { home, claims, claim, status, setup *template.Template help, helpDatabase, helpLK, helpCryptoPro, helpSystems *template.Template + wizard *template.Template } // templateFuncs — функции, доступные внутри шаблонов. Главная задача — @@ -121,9 +122,14 @@ func newAdmin() (*admin, error) { if err != nil { return nil, fmt.Errorf("parse admin_help_systems: %w", err) } + wizard, err := parse("admin_wizard.html") + if err != nil { + return nil, fmt.Errorf("parse admin_wizard: %w", err) + } return &admin{ home: home, claims: claims, claim: claim, status: status, setup: setup, help: help, helpDatabase: helpDB, helpLK: helpLK, helpCryptoPro: helpCP, helpSystems: helpSys, + wizard: wizard, }, nil } diff --git a/internal/lkgateway/setup.go b/internal/lkgateway/setup.go index 6d04442..17d954b 100644 --- a/internal/lkgateway/setup.go +++ b/internal/lkgateway/setup.go @@ -11,6 +11,7 @@ import ( "os" "os/exec" "path/filepath" + "strconv" "strings" "time" @@ -59,6 +60,7 @@ func registerSetup(mux *http.ServeMux, a *admin, rc *RuntimeConfig, svc *Service h.renderSetup(w, r, "") }) mux.HandleFunc("/admin/setup/postgres", h.savePostgres) + mux.HandleFunc("/admin/setup/postgres/quick-start", h.quickStartPostgres) mux.HandleFunc("/admin/setup/crypto", h.saveCrypto) mux.HandleFunc("/admin/setup/crypto/check", h.checkCrypto) mux.HandleFunc("/admin/setup/crypto/activate", h.activateLicense) @@ -67,6 +69,90 @@ func registerSetup(mux *http.ServeMux, a *admin, rc *RuntimeConfig, svc *Service mux.HandleFunc("/admin/setup/nsd", h.saveNSD) mux.HandleFunc("/admin/setup/lk", h.saveLK) mux.HandleFunc("/admin/setup/test-run", h.testRun) + + // Пошаговый мастер настройки для нетехнических пользователей. + mux.HandleFunc("/admin/wizard", h.renderWizard) +} + +// WizardData — данные для шаблона /admin/wizard. +type WizardData struct { + page + Step int + Settings Settings + Certs []cryptocli.Certificate + Flash string + CryptoProInstalled bool + CryptoProVersion string + Done struct { + Postgres bool + Crypto bool + Certs bool + NSD bool + TestRun bool + } +} + +// renderWizard рисует одну из 5 страниц мастера. Шаг управляется query +// параметром ?step=N (1..5). По умолчанию шаг определяется автоматически +// по первому незавершённому пункту — это даёт «продолжить с того места». +func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "method", http.StatusMethodNotAllowed) + return + } + s := h.rc.Snapshot() + d := WizardData{ + page: nowPage("Мастер настройки", "wizard"), + Settings: s, + Certs: h.listCertsForUI(), + Flash: r.URL.Query().Get("flash"), + } + d.Done.Postgres = s.Postgres.DSN != "" + d.Done.Crypto = s.Crypto.Provider != "" && s.Crypto.Provider != "stub" && s.Crypto.JCPPath != "" + d.Done.Certs = len(d.Certs) > 0 + d.Done.NSD = s.NSD.IGWBaseURL != "" && s.NSD.Profile != "" + d.Done.TestRun = s.LastTest != nil + + // Проверяем установлен ли КриптоПро CSP. + if _, err := os.Stat("/opt/cprocsp/sbin/amd64/cpconfig"); err == nil { + d.CryptoProInstalled = true + ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second) + defer cancel() + if ver, _ := runCmd(ctx, "/opt/cprocsp/sbin/amd64/cpconfig", "-license", "-view"); ver != "" { + d.CryptoProVersion = firstLine(ver) + } + } + + // Определяем текущий шаг. + step := 1 + if v := r.URL.Query().Get("step"); v != "" { + if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 5 { + step = n + } + } else { + // Авто: первый незавершённый. + switch { + case !d.Done.Postgres: + step = 1 + case !d.Done.Crypto: + step = 2 + case !d.Done.Certs: + step = 3 + case !d.Done.NSD: + step = 4 + default: + step = 5 + } + } + d.Step = step + render(w, h.tpl.a.wizard, d) +} + +func firstLine(s string) string { + if i := strings.IndexByte(s, '\n'); i >= 0 { + return strings.TrimSpace(s[:i]) + } + return strings.TrimSpace(s) } // installCryptoPro — POST /admin/setup/crypto/install (multipart). @@ -365,6 +451,88 @@ func (h *setupHandlers) savePostgres(w http.ResponseWriter, r *http.Request) { setupFlash(w, r, "PostgreSQL настройки сохранены") } +// quickStartPostgres — POST /admin/setup/postgres/quick-start. +// «Большая зелёная кнопка» для пользователя без IT-навыков: поднимает +// локальный postgres-контейнер через podman-compose, ждёт pg_isready, +// накатывает все миграции (fansy-store + m2m-core), сохраняет дефолтный +// DSN в runtime-конфиге. После этого пользователю остаётся перезапустить +// bj-server (или мы сделаем это автоматически в дальнейших версиях). +func (h *setupHandlers) quickStartPostgres(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method", http.StatusMethodNotAllowed) + return + } + ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second) + defer cancel() + + // 1. Поднимаем postgres контейнер через podman-compose. + composePath := "deploy/docker-compose/docker-compose.yml" + if out, err := runCmd(ctx, "podman-compose", "-f", composePath, "up", "-d", "postgres"); err != nil { + setupFlash(w, r, "Шаг 1/3: podman-compose не смог поднять контейнер. "+ + "Установите podman-compose или проверьте docker-compose.yml. Подсказка: "+ + "sudo dnf install -y podman-compose. Вывод: "+strings.TrimSpace(out)) + return + } + + // 2. Ждём pg_isready. + dsn := "postgres://bj:bj_dev@127.0.0.1:5432/bj?sslmode=disable" + deadline := time.Now().Add(30 * time.Second) + for time.Now().Before(deadline) { + if err := tryPingPostgres(dsn); err == nil { + break + } + time.Sleep(time.Second) + } + if err := tryPingPostgres(dsn); err != nil { + setupFlash(w, r, "Шаг 2/3: контейнер запущен, но БД не отвечает за 30 сек. Ошибка: "+err.Error()) + return + } + + // 3. Применяем миграции через podman exec. + migrations := []string{ + "migrations/fansy-store/000__roles.sql", + "migrations/fansy-store/001__schemas.sql", + "migrations/fansy-store/002__working.sql", + "migrations/fansy-store/003__staging.sql", + "migrations/fansy-store/004__seed_participants.sql", + "migrations/m2m-core/001__deals.sql", + "migrations/m2m-core/002__stages.sql", + } + for _, mig := range migrations { + if err := applyMigration(ctx, mig); err != nil { + // Миграция могла быть уже применена ранее (например, ROLE уже + // существует) — это не критично, продолжаем. + log.Printf("quick-start: миграция %s: %v (продолжаем)", mig, err) + } + } + + // 4. Сохраняем DSN в runtime-конфиг. + if err := h.rc.UpdatePostgres(PostgresSettings{DSN: dsn}); err != nil { + setupFlash(w, r, "Шаг 3/3: не получилось сохранить DSN: "+err.Error()) + return + } + + setupFlash(w, r, "Локальный PostgreSQL поднят и настроен. DSN сохранён. "+ + "Перезапустите bj-server (или подождите пока systemd сам перезапустит сервис), "+ + "чтобы Repository подключился к БД. После этого статус PostgreSQL будет зелёным.") +} + +// applyMigration выполняет одну SQL-миграцию через podman exec в bj-postgres. +func applyMigration(ctx context.Context, path string) error { + data, err := os.ReadFile(path) + if err != nil { + return err + } + cmd := exec.CommandContext(ctx, "podman", "exec", "-i", "bj-postgres", + "psql", "-U", "bj", "-d", "bj", "-v", "ON_ERROR_STOP=1") + cmd.Stdin = strings.NewReader(string(data)) + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("%w / output: %s", err, strings.TrimSpace(string(out))) + } + return nil +} + func (h *setupHandlers) saveCrypto(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method", http.StatusMethodNotAllowed) @@ -573,9 +741,21 @@ func tryHTTPHealth(u string) error { return nil } -// setupFlash шлёт 303 на /admin/setup с flash-сообщением в query. +// setupFlash шлёт 303 с flash-сообщением в query. Если запрос пришёл +// со страницы мастера (/admin/wizard), возвращаем туда же с сохранением +// номера шага — пользователь не должен «выпадать» из визарда после POST. func setupFlash(w http.ResponseWriter, r *http.Request, msg string) { - http.Redirect(w, r, "/admin/setup?flash="+url.QueryEscape(msg), http.StatusSeeOther) + target := "/admin/setup" + if ref := r.Header.Get("Referer"); ref != "" { + if u, err := url.Parse(ref); err == nil && strings.HasPrefix(u.Path, "/admin/wizard") { + q := u.Query() + q.Set("flash", msg) + target = u.Path + "?" + q.Encode() + http.Redirect(w, r, target, http.StatusSeeOther) + return + } + } + http.Redirect(w, r, target+"?flash="+url.QueryEscape(msg), http.StatusSeeOther) } // _q извлекает Request из ResponseWriter trick — здесь не нужно diff --git a/internal/lkgateway/web/templates/admin_setup.html b/internal/lkgateway/web/templates/admin_setup.html index 01c9839..1a247f7 100644 --- a/internal/lkgateway/web/templates/admin_setup.html +++ b/internal/lkgateway/web/templates/admin_setup.html @@ -19,14 +19,26 @@

PostgreSQL

Принимающая БД (fansy-store) и журнал сделок m2m-core. Сейчас: {{if .Settings.Postgres.DSN}}настроено{{else}}in-memory (M2-демо){{end}}.

-
- Изменить параметры подключения + + {{if not .Settings.Postgres.DSN}} +
+

Самый простой вариант — подключить автоматически

+

Если у вас ещё нет своего PostgreSQL, мы поднимем его сами в контейнере (podman-compose), применим все миграции и запишем DSN. Подходит для дев-стенда и тестирования. Для прода — лучше указать свой DSN ниже.

+
+ + Займёт ~10-30 секунд. Требуется установленный podman-compose. +
+
+ {{end}} + +
+ {{if .Settings.Postgres.DSN}}Изменить параметры подключения{{else}}…или ввести параметры подключения вручную (для существующего PostgreSQL){{end}}
- +
-

При сохранении выполняется Ping. Если драйвер pgx ещё не подключён в коде, тест упадёт — это ожидаемо до M2-шага-3.

+

При сохранении выполняется Ping. Если БД недоступна — будет ошибка.

diff --git a/internal/lkgateway/web/templates/admin_wizard.html b/internal/lkgateway/web/templates/admin_wizard.html new file mode 100644 index 0000000..c6ea97e --- /dev/null +++ b/internal/lkgateway/web/templates/admin_wizard.html @@ -0,0 +1,305 @@ +{{define "content"}} + + +
+

Мастер настройки

+

Пошаговая настройка системы. Подходит для первого запуска. После каждого шага состояние сохраняется и можно вернуться позже.

+
+ +
+
+ Шаг 1 + PostgreSQL +
+
+ Шаг 2 + КриптоПро / Рутокен +
+
+ Шаг 3 + Сертификаты +
+
+ Шаг 4 + Шлюз НРД +
+
+ Шаг 5 + Тестовая заявка +
+
+ +{{if .Flash}}

{{.Flash}}

{{end}} + +{{/* ============= ШАГ 1: PostgreSQL ============= */}} +{{if eq .Step 1}} +
+

Шаг 1. PostgreSQL

+

Сюда система пишет журнал сделок и принимает данные от команды Fansy.

+ +
+ Что выбрать? Если у вас уже есть рабочий PostgreSQL — нажмите «У меня уже есть PostgreSQL» и введите DSN. Если впервые настраиваете — выберите «Поднять автоматически», система сама развернёт контейнер с PostgreSQL и накатит миграции. +
+ + {{if not .Settings.Postgres.DSN}} +
+

Вариант А — для тех, у кого нет своего PostgreSQL

+

Bridge-and-Join-s сам поднимет PostgreSQL в контейнере (podman-compose), создаст БД bj и накатит миграции. Подходит для дев-стенда. Для продакшена лучше указать свой DSN.

+
+ + ~10-30 сек +
+
+ {{end}} + +
+ Вариант Б — у меня уже есть PostgreSQL, введу DSN сам +
+ + +

При сохранении выполняется тестовое подключение (Ping). Если БД недоступна — будет ошибка.

+ +
+
+ +
+ + {{if .Done.Postgres}}К шагу 2 →{{else}}{{end}} +
+ {{if not .Done.Postgres}}

Пропустить (буду работать в режиме in-memory — без сохранения сделок)

{{end}} +
+{{end}} + +{{/* ============= ШАГ 2: Крипто ============= */}} +{{if eq .Step 2}} +
+

Шаг 2. Крипто-провайдер (КриптоПро CSP или Рутокен)

+

СКЗИ нужен для подписи XMLDSig и проверки квитанций НРД.

+ +
+ Что это? КриптоПро CSP — российский криптопровайдер с поддержкой ГОСТ Р 34.10-2012. Рутокен ЭЦП 2.0 — USB-токен для безопасного хранения ключей. Можно использовать оба: CSP — для серверной части, Рутокен — для подписи действий оператора.
+ Где взять? Дистрибутив КриптоПро CSP 5.0 R4 — cryptopro.ru/products/csp/downloads (нужна регистрация в личном кабинете). Лицензия — там же или у дилера. Демо-лицензия на 3 месяца встроена в дистрибутив. +
+ + {{if not .CryptoProInstalled}} +
+

Шаг 2a — загрузить и установить КриптоПро CSP

+

Скачайте с cryptopro.ru архив linux-amd64.tgz или linux-amd64.tar (КриптоПро CSP 5.0 R4 для Linux) и загрузите его сюда. Bj-server сам распакует и установит нужные пакеты.

+
+ + +
+
+ {{else}} +

✓ КриптоПро CSP установлен. Версия: {{.CryptoProVersion}}

+ {{end}} + +
+ Шаг 2b — указать провайдер и путь к PKCS#11 модулю +
+
+ + +
+
+ + +
+ +
+
+ + {{if and .Done.Crypto (not .Settings.Crypto.LicenseKey)}} +
+ Шаг 2c — активировать лицензию (если демо не подходит) +
+ + + +
+
+ {{end}} + +
+ ← К шагу 1 + {{if .Done.Crypto}}К шагу 3 →{{else}}Пропустить →{{end}} +
+
+{{end}} + +{{/* ============= ШАГ 3: Сертификаты ============= */}} +{{if eq .Step 3}} +
+

Шаг 3. Сертификаты

+

Импортируйте сертификаты вашей организации и сертификаты УЦ НРД (для проверки квитанций).

+ +
+ Какие сертификаты нужны? +
    +
  1. Ваш сертификат организации с приватным ключом (.pfx / .p12 на диске или контейнер на Рутокене) — для подписи отправляемых пакетов.
  2. +
  3. Корневой сертификат вашего УЦ (.cer) — в хранилище mroot.
  4. +
  5. Корневой и подписной сертификаты УЦ НРД — для проверки квитанций. Скачиваются с nsd.ru/workflow/system/cryptography/
  6. +
+ Где взять? Сертификат вашей организации — у вашего УЦ (Контур, СКБ Контур, ИнфоТеКС, КриптоПро УЦ, …). Сертификаты УЦ НРД — на сайте НРД (см. выше). +
+ +

Импорт сертификата

+
+
+ + +
+
+ + +
+
+ + +
+ +
+ + {{if .Certs}} +

Установленные сертификаты ({{len .Certs}})

+ + + + {{range .Certs}} + + + + + + + + {{end}} + +
ВладелецИздательДействителен доИННКлюч
{{.SubjectCN}}{{.IssuerCN}}{{.NotAfter.Format "02.01.2006"}}{{if .INN}}{{.INN}}{{else}}—{{end}}{{if .HasPrivateKey}}есть{{else}}нет{{end}}
+ {{else}} +

Пока сертификаты не импортированы.

+ {{end}} + +
+ ← К шагу 2 + К шагу 4 → +
+
+{{end}} + +{{/* ============= ШАГ 4: НРД ============= */}} +{{if eq .Step 4}} +
+

Шаг 4. Интеграционный шлюз НРД

+

Адрес web-сервиса ONYX и имя ключевого контейнера НРД.

+ +
+ Что это? Интеграционный шлюз (ИШ) НРД — это компонент, через который наши M2M-сообщения отправляются в НРД. У НРД есть 4 контура: GUEST (для разработки) и TEST3 (предпродакшен), каждый в варианте ГОСТ или RSA.
+ Где взять? Дистрибутив ИШ и инструкции — на сайте НРД nsd.ru/workflow/system/programs/. Доступ к тестовым контурам выдаётся НРД по заявке (см. DOC/instr_podkl_stend_v3.pdf). +
+ +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+ ← К шагу 3 + {{if .Done.NSD}}К шагу 5 →{{else}}Пропустить (mock-режим) →{{end}} +
+
+{{end}} + +{{/* ============= ШАГ 5: Тест-ран ============= */}} +{{if eq .Step 5}} +
+

Шаг 5. Тестовая заявка

+

Прогон полного цикла: создание заявки → валидация → подпись → отправка в НРД (или mock) → ожидание Decision → подтверждение.

+ +
+ Что произойдёт? Система создаст тестовую M2M-сделку, проведёт её через всю стейт-машину, и покажет результат каждого этапа. Если ИШ НРД не настроен — сработает mock (синтетический Decision через 3 секунды). +
+ +
+ +
+ + {{if .Settings.LastTest}} +

Последний прогон: {{.Settings.LastTest.StartedAt.Format "02.01.2006 15:04:05"}}

+ + + + + {{if .Settings.LastTest.Message}}{{end}} +
Заявка{{.Settings.LastTest.ClaimID}}
Финальное состояние{{ruState .Settings.LastTest.FinalStatus}}
Результат{{if .Settings.LastTest.OK}}успех{{else}}ошибка{{end}}
Сообщение{{.Settings.LastTest.Message}}
+ {{end}} + +

Итоговая сводка

+ + + + + + +
PostgreSQL{{if .Done.Postgres}}настроен{{else}}in-memory{{end}}
Крипто-провайдер{{if .Done.Crypto}}{{.Settings.Crypto.Provider}}{{else}}не настроен{{end}}
Сертификатов установлено{{len .Certs}}
ИШ НРД{{if .Done.NSD}}{{.Settings.NSD.Profile}}{{else}}mock-режим{{end}}
Тестовый прогон{{if .Done.TestRun}}пройден{{else}}не запускался{{end}}
+ +
+ ← К шагу 4 + Перейти к дашборду +
+
+{{end}} + +{{end}} diff --git a/internal/lkgateway/web/templates/layout.html b/internal/lkgateway/web/templates/layout.html index 9ebe73e..4197e72 100644 --- a/internal/lkgateway/web/templates/layout.html +++ b/internal/lkgateway/web/templates/layout.html @@ -45,6 +45,7 @@ button:hover, .btn:hover { opacity: .9; }

lk-gateway