diff --git a/internal/lkgateway/auth.go b/internal/lkgateway/auth.go index 06f0589..0627b6f 100644 --- a/internal/lkgateway/auth.go +++ b/internal/lkgateway/auth.go @@ -861,6 +861,8 @@ func authExempt(p string) bool { switch { case p == "/login" || p == "/logout" || p == "/healthz" || p == "/first-run": return true + case strings.HasPrefix(p, "/first-run/"): // восстановление из копии при переустановке — по одноразовому коду + return true case strings.HasPrefix(p, "/api/"): return true case strings.HasPrefix(p, "/admin/npr/static/"): diff --git a/internal/lkgateway/firstrun.go b/internal/lkgateway/firstrun.go index 3a6780d..619d4e9 100644 --- a/internal/lkgateway/firstrun.go +++ b/internal/lkgateway/firstrun.go @@ -44,7 +44,69 @@ type firstRunData struct { var firstRunTpl = template.Must(template.New("first-run").Parse(firstRunHTML)) +// firstRunCodeOK — одноразовый код первичной настройки верен (с ограничением попыток по адресу). +func firstRunCodeOK(r *http.Request) (bool, string) { + key := "first-run|" + clientIP(r) + if loginFailCount(key) >= 8 { + return false, "Слишком много неудачных попыток. Подождите 15 минут." + } + code := strings.NewReplacer("-", "", " ", "").Replace(strings.TrimSpace(r.FormValue("code"))) + var st firstRunState + raw, err := os.ReadFile(firstRunFile) + if err == nil { + _ = json.Unmarshal(raw, &st) + } + if err != nil || st.CodeHash == "" || bcrypt.CompareHashAndPassword([]byte(st.CodeHash), []byte(code)) != nil { + loginFailAdd(key) + time.Sleep(700 * time.Millisecond) + log.Printf("auth: первичная настройка: неверный код с %s", clientIP(r)) + return false, "Неверный одноразовый код" + } + return true, "" +} + +// registerFirstRunRestore — восстановление из резервной копии прямо со страницы первичной настройки: при +// переустановке сервера не нужно заводить администратора, которого восстановление тут же заменит. +func registerFirstRunRestore(mux *http.ServeMux) { + op := func(name string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method", http.StatusMethodNotAllowed) + return + } + if !firstRunPending() { + jsonOut(w, map[string]any{"ok": false, "error": "первичная настройка уже выполнена — восстановление доступно администратору в мастере настройки"}) + return + } + if ok, msg := firstRunCodeOK(r); !ok { + jsonOut(w, map[string]any{"ok": false, "error": msg}) + return + } + ctx, cancel := context.WithTimeout(r.Context(), time.Minute) + defer cancel() + if name == "restore" { + log.Printf("auth: первичная настройка: восстановление копии %s с %s", r.FormValue("name"), clientIP(r)) + } + jsonOut(w, opResult(backupOp(ctx, name, backupParams(r)))) + } + } + mux.HandleFunc("/first-run/backup/list", op("list")) + mux.HandleFunc("/first-run/backup/restore", op("restore")) + mux.HandleFunc("/first-run/backup/status", func(w http.ResponseWriter, r *http.Request) { + out := map[string]any{} + if raw, err := os.ReadFile(restoreStatusFile); err == nil { + var rs map[string]any + if json.Unmarshal(raw, &rs) == nil { + out["restore"] = rs + } + } + out["pending"] = firstRunPending() + jsonOut(w, out) + }) +} + func registerFirstRun(mux *http.ServeMux) { + registerFirstRunRestore(mux) mux.HandleFunc("/first-run", func(w http.ResponseWriter, r *http.Request) { if !firstRunPending() { http.Redirect(w, r, "/login", http.StatusSeeOther) @@ -151,4 +213,51 @@ code{font-size:12px} {{if .Error}}
{{.Error}}
{{end}}
Код потерян или истёк: на сервере выполните sudo bj-installer first-run-code.
+
Переустановка сервера? Восстановить из резервной копии +

Вместо создания администратора можно вернуть сервер из резервной копии: настройки, пользователи, лицензия, заявки, база и папка шлюза НРД. Нужен одноразовый код (поле вверху) и доступ к хранилищу копий.

+ + + + + + + +
+
+ ` diff --git a/internal/lkgateway/onboarding_test.go b/internal/lkgateway/onboarding_test.go index 7559878..306494a 100644 --- a/internal/lkgateway/onboarding_test.go +++ b/internal/lkgateway/onboarding_test.go @@ -1,6 +1,8 @@ package lkgateway import ( + "net/http" + "net/http/httptest" "os" "path/filepath" "strings" @@ -75,3 +77,31 @@ func TestStoreCertKindAndExpiry(t *testing.T) { t.Fatalf("отметка 7 не сохранена: %s", raw) } } + +func TestSafeReturnPathKeepsStep(t *testing.T) { + for in, want := range map[string]string{ + "/admin/wizard?step=4": "/admin/wizard?step=4", + "/admin/wizard?step=3&sub=done&flash=x": "/admin/wizard?step=3&sub=done", + "/admin/setup#sec-org": "/admin/setup", + "/admin/wizard?step=4%0d%0aX": "/admin/wizard", + "//evil.example/admin": "", + "/admin/x?a= + {{template "notifyGroupsCard" .}} {{/* ============ ОБНОВЛЕНИЯ ============ */}} @@ -992,68 +938,7 @@ {{/* ============ РЕЗЕРВНОЕ КОПИРОВАНИЕ ============ */}}

Резервное копирование

- {{$b := .Settings.Backup}}{{$bs := backupStatus}} -
-

Копии сервера bj на сетевое хранилище

-

Что копируется: база bj (заявки, сделки, кабинет), база и папка шлюза НРД (настройки, сертификаты, полученные от НРД документы), настройки и данные bj. Пароль хранилища bj у себя не хранит — он лежит на сервере в файле, доступном только системе. Восстановить копию можно в мастере настройки (шаг 1, «Восстановить из резервной копии»).

-

{{if $bs}}Последняя копия: {{backupWhen $bs.At}} — {{if $bs.OK}}✓ {{$bs.Dir}}, {{$bs.Msg}}{{else}}✗ {{$bs.Msg}}{{end}}{{else}}Копий ещё не было.{{end}}

- -
- -
- - -
-
- - -
-
- - -
-
- - - - -
-
-
- -
+ {{template "backupCard" .}}
diff --git a/internal/lkgateway/web/templates/admin_wizard.html b/internal/lkgateway/web/templates/admin_wizard.html index 9e68341..0483edf 100644 --- a/internal/lkgateway/web/templates/admin_wizard.html +++ b/internal/lkgateway/web/templates/admin_wizard.html @@ -29,7 +29,7 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }

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

-
+

Пропустить мастер и перейти в систему → — вернуться к нему можно из «Настройка → Мастер настройки»

@@ -366,6 +367,7 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); } Если МНП уже есть. Адрес сервера и учётные данные — у вашего администратора QUIK.
{{if not .DepoLicensed}}

Модуль неторговых поручений не входит в текущую лицензию — шаг пропускается.

{{else}}{{with .Fancy}}{{template "quikSection" .}}{{end}} + {{if .Settings.Quik.Server}}{{template "mnpCheck" .}}{{end}} {{if .Settings.Quik.M2MMissing}}

✗ Подключение есть, но под этим логином не видно объектов M2M: {{.Settings.Quik.M2MMissing}}. Проверьте имя базы МНП и права логина (роль InstrEu) и нажмите «Проверить» ещё раз.

{{end}}
Справочник депозитариев-получателей. Чтобы QUIK принимал поручения M2M, в МНП должен быть справочник депозитариев-получателей (M2MDepoRecipients): bj заполняет его сам по справочнику участников НРД. Модуль МНП читает справочник только при запуске — после первого заполнения попросите администратора QUIK перезапустить модуль неторговых поручений, иначе QUIK ответит «Неизвестный депозитарий получателя».
{{quikRecipientsInfo}}
{{end}}
@@ -499,11 +501,34 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
{{end}} -{{/* ============= ШАГ 5: Тест-ран (режим разработки) / проверка подключений ============= */}} +{{/* ============= ШАГ 9: Оповещения и резервные копии ============= */}} {{if eq .Step 9}} +
+

Шаг 9. Оповещения и резервные копии

+
Зачем. bj сам следит за обменом: НРД не отвечает, перевод не доставлен, ошибка обработки в DEPO, истекает сертификат ключа. Чтобы об этом узнали сразу, а не открыв bj, — укажите почту. Резервные копии позволят восстановить сервер целиком (настройки, заявки, шлюз НРД) после сбоя или переустановки.
+ Можно позже. Шаг необязательный — обмен работает и без него, но мы настоятельно советуем настроить оба пункта.
+ +
+ {{with .Fancy}} +

Почта для оповещений

+ {{template "smtpCard" .}} + {{template "notifyGroupsCard" .}} +

Резервные копии

+ {{template "backupCard" .}} + {{end}} +
+
+ ← К шагу 8 + {{if .Done.Alerts}}К проверке →{{else}}Пропустить →{{end}} +
+
+{{end}} + +{{/* ============= ШАГ 10: Проверка готовности (в режиме разработки — ещё тестовая заявка) ============= */}} +{{if eq .Step 10}}
{{if devOn}} -

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

+

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

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

@@ -523,23 +548,33 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); } {{if .Settings.LastTest.Message}}Сообщение{{.Settings.LastTest.Message}}{{end}} {{end}} -{{else}} -

Шаг 9. Проверка подключений

-

Проверьте, что база данных, СКЗИ и шлюз НРД доступны: на странице «Статус» bj проверяет каждое подключение и показывает, что нужно исправить.

- Открыть проверку подключений → {{end}} + {{if not devOn}}

Шаг 10. Готовность к работе

{{else}}

Готовность к работе

{{end}} +

bj проверяет всё, что нужно для обмена с НРД, прямо сейчас. Красное — обмен работать не будет, жёлтое — работать будет, но лучше исправить. У каждой строки — ссылка туда, где это исправляется.

+
Проверяю…
+
+

+ + -

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

- - - - - - {{if devOn}}{{end}} -
PostgreSQL{{if .Done.Postgres}}настроен{{else}}in-memory{{end}}
Крипто-провайдер{{if .Done.Crypto}}Валидата Клиент L{{if .ValidataVer}} {{.ValidataVer}}{{end}}{{else}}не настроен{{end}}
Сертификатов установлено{{len .Certs}}
ИШ НРД{{if .Done.NSD}}{{.Settings.NSD.Profile}}{{else}}{{if devOn}}mock-режим{{else}}не настроен{{end}}{{end}}
Тестовый прогон{{if .Done.TestRun}}пройден{{else}}не запускался{{end}}
- ← К шагу 8 + ← К шагу 9 Перейти к дашборду
diff --git a/internal/lkgateway/web/templates/layout.html b/internal/lkgateway/web/templates/layout.html index d7f7274..987b9ac 100644 --- a/internal/lkgateway/web/templates/layout.html +++ b/internal/lkgateway/web/templates/layout.html @@ -469,7 +469,7 @@ details:has(> pre.copyable) { min-width:0; max-width:100%; } // получает Referer, поэтому говорим путь явно. if (!f.querySelector('input[name="return_to"]')) { var rt = document.createElement('input'); - rt.type = 'hidden'; rt.name = 'return_to'; rt.value = location.pathname; + rt.type = 'hidden'; rt.name = 'return_to'; rt.value = location.pathname + location.search.replace(/[?&](flash|scrollto)=[^&]*/g, '').replace(/^&/, '?'); f.appendChild(rt); } // Подкладываем секцию (ближайшую карточку) — сервер вернёт ?scrollto=. diff --git a/internal/lkgateway/web/templates/part_alerts.html b/internal/lkgateway/web/templates/part_alerts.html new file mode 100644 index 0000000..2fcf963 --- /dev/null +++ b/internal/lkgateway/web/templates/part_alerts.html @@ -0,0 +1,64 @@ +{{/* Оповещения: почта и адресаты — общие для «Настройка → Оповещения» и мастера (данные — SetupData). */}} +{{define "smtpCard"}} +
+

Почта (SMTP)

+

Отправка уведомлений по e-mail. Пароль хранится в конфигурации сервиса.

+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+ {{if .Settings.Alerts.SMTP.LastMsg}}

Последняя проверка: {{.Settings.Alerts.SMTP.LastMsg}}

{{end}} +
+ + +{{end}} + +{{define "notifyGroupsCard"}} +
+

Адресаты уведомлений: депозитарий, бэк-офис, мидл-офис

+

Три группы адресов и таблица «какой тип уведомления кому отправлять». Письма идут через SMTP из раздела «Почта». Адрес алертов из раздела «Почта» получает системные оповещения всегда; группы ниже добавляются по маршруту.

+
+ +
+
+
+
+
+ + {{range .NotifyGroups}}{{end}} + {{$inc := .Settings.Incoming}}{{range $k := .NotifyKinds}}{{range $g := $.NotifyGroups}}{{end}}{{end}} +
Тип уведомления{{.Title}}
{{$k.Title}}
+
+ + +
+
+

Требуется настроенный и включённый SMTP в разделе «Почта».

+
+{{end}} diff --git a/internal/lkgateway/web/templates/part_backup.html b/internal/lkgateway/web/templates/part_backup.html new file mode 100644 index 0000000..bd605c9 --- /dev/null +++ b/internal/lkgateway/web/templates/part_backup.html @@ -0,0 +1,65 @@ +{{/* Резервное копирование — общее для «Настройка → Резервное копирование» и мастера (данные — SetupData). */}} +{{define "backupCard"}} + {{$b := .Settings.Backup}}{{$bs := backupStatus}} +
+

Копии сервера bj на сетевое хранилище

+

Что копируется: база bj (заявки, сделки, кабинет), база и папка шлюза НРД (настройки, сертификаты, полученные от НРД документы), настройки и данные bj. Пароль хранилища bj у себя не хранит — он лежит на сервере в файле, доступном только системе. Восстановить копию можно в мастере настройки (шаг 1, «Восстановить из резервной копии»).

+

{{if $bs}}Последняя копия: {{backupWhen $bs.At}} — {{if $bs.OK}}✓ {{$bs.Dir}}, {{$bs.Msg}}{{else}}✗ {{$bs.Msg}}{{end}}{{else}}Копий ещё не было.{{end}}

+ +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + + + +
+
+
+ +
+{{end}} diff --git a/internal/lkgateway/web/templates/part_quik.html b/internal/lkgateway/web/templates/part_quik.html index 960fc14..56cd3b7 100644 --- a/internal/lkgateway/web/templates/part_quik.html +++ b/internal/lkgateway/web/templates/part_quik.html @@ -80,3 +80,26 @@
{{end}} {{end}} + +{{define "mnpCheck"}} +
+

Проверка модуля неторговых поручений

+

Только чтение: всё, без чего цикл M2M в МНП не работает. Ничего в QUIK не меняет.

+
+

+
+ +{{end}}