Учёт DEPO: ход подготовки по шагам, понятный блок «Логин bj», пауза после неудачного входа

- «Подготовить» / «Проверить подключение» показывают ход: подключение логином
  администратора, проверка, что это база DEPO, применение скрипта, вход логином bj,
  таблицы обмена, задание, процедуры, «Готово». Сервер отдаёт шаги строками JSON
  (stream=1), на ошибке — шаг с крестиком и причина, без перехода со страницы.
- Блоки «Администратор SQL — нужен один раз, не сохраняется» и «Логин bj — под ним
  bj будет работать»: bj сохраняет и использует ровно то, что вписано; «другой
  пароль»; на итоге шага — каким логином bj работает.
- Пауза 15 минут после неудачного входа в DEPO (логин/пароль отвергнуты) для
  фоновых служб: с неверным паролем они за минуты блокировали логин на сервере
  (CHECK_POLICY) у всех, кто им пользуется. Действия человека идут без паузы;
  новый пароль — сразу; системное событие в «Новостях».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
zuevav
2026-10-06 15:46:25 +03:00
parent fbf6a23667
commit a9915bcc82
8 changed files with 395 additions and 53 deletions
+73 -36
View File
@@ -7,6 +7,7 @@ package lkgateway
import (
"context"
"encoding/json"
"net/http"
"net/url"
"strconv"
@@ -33,6 +34,11 @@ func depoWizardSub(v string) string {
// wizDepoGo — перейти на экран шага 3 с сообщением (ok=false — сообщение об ошибке).
func wizDepoGo(w http.ResponseWriter, r *http.Request, sub, msg string, ok bool) {
http.Redirect(w, r, wizDepoURL(sub, msg, ok), http.StatusSeeOther)
}
// wizDepoURL — адрес экрана шага 3 с сообщением.
func wizDepoURL(sub, msg string, ok bool) string {
if sub == "intro" || sub == "conn" || sub == "tables" || sub == "job" {
sub = "setup"
}
@@ -45,7 +51,7 @@ func wizDepoGo(w http.ResponseWriter, r *http.Request, sub, msg string, ok bool)
}
u += "&flash=" + url.QueryEscape(msg)
}
http.Redirect(w, r, u, http.StatusSeeOther)
return u
}
// wizardDepoConn — POST /admin/setup/wizard-depo/conn: сохранить подключение и сразу проверить его. Имя базы
@@ -68,7 +74,7 @@ func (h *setupHandlers) wizardDepoConn(w http.ResponseWriter, r *http.Request) {
wizDepoGo(w, r, "conn", "не сохранилось: "+err.Error(), false)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 45*time.Second)
ctx, cancel := context.WithTimeout(depoLoginForce(r.Context()), 45*time.Second)
defer cancel()
if _, err := fancyPing(ctx, s, ""); err != nil {
wizDepoGo(w, r, "conn", "к серверу подключиться не удалось — "+err.Error()+". Проверьте адрес, порт, логин и пароль.", false)
@@ -105,7 +111,7 @@ func (h *setupHandlers) wizardDepoTables(w http.ResponseWriter, r *http.Request)
s.DepoTablesMode = mode
_ = h.rc.UpdateFancy(s)
}
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
ctx, cancel := context.WithTimeout(depoLoginForce(r.Context()), 2*time.Minute)
defer cancel()
if mode == "auto" && r.FormValue("action") != "check" {
if _, err := depoTablesCreate(ctx, s, r.FormValue("admin_user"), r.FormValue("admin_password")); err != nil {
@@ -144,7 +150,7 @@ func (h *setupHandlers) wizardDepoJob(w http.ResponseWriter, r *http.Request) {
wizDepoGo(w, r, "job", "не сохранилось: "+err.Error(), false)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 90*time.Second)
ctx, cancel := context.WithTimeout(depoLoginForce(r.Context()), 90*time.Second)
defer cancel()
jobNote := ""
if mode == "sql-auto" && r.FormValue("action") != "check" {
@@ -183,7 +189,7 @@ func (h *setupHandlers) wizardDepoRecheck(w http.ResponseWriter, r *http.Request
return
}
s := h.rc.Snapshot().Fancy
ctx, cancel := context.WithTimeout(r.Context(), 90*time.Second)
ctx, cancel := context.WithTimeout(depoLoginForce(r.Context()), 90*time.Second)
defer cancel()
if _, err := fancyPing(ctx, s, s.DepoDB()); err != nil {
wizDepoGo(w, r, "setup", "подключение не работает — "+err.Error(), false)
@@ -332,37 +338,60 @@ func depoDraft(s, saved FancyMSSQLSettings) FancyMSSQLSettings {
// wizardDepoSetup — POST /admin/setup/wizard-depo/setup: подготовить базу (mode=auto — bj сам логином
// администратора), проверить после скрипта (mode=script) или с готовым логином (mode=existing).
// stream=1 (экран с JavaScript) — ход по шагам строками JSON, чтобы после «Подготовить» было видно, что
// происходит: подключение, проверка базы, скрипт, вход bj, проверки; последняя строка — итог и куда перейти.
func (h *setupHandlers) wizardDepoSetup(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || !requireDepo(w, r) {
return
}
ctx, cancel := context.WithTimeout(depoLoginForce(r.Context()), 3*time.Minute)
defer cancel()
if r.FormValue("stream") == "" {
sub, msg, ok := h.depoSetupDo(ctx, r, func(string, string, string) {})
wizDepoGo(w, r, sub, msg, ok)
return
}
w.Header().Set("Content-Type", "application/x-ndjson; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("X-Accel-Buffering", "no")
fl, _ := w.(http.Flusher)
enc := json.NewEncoder(w)
send := func(v any) {
_ = enc.Encode(v)
if fl != nil {
fl.Flush()
}
}
sub, msg, ok := h.depoSetupDo(ctx, r, func(key, state, text string) {
send(map[string]string{"step": key, "state": state, "text": text})
})
send(map[string]any{"done": true, "ok": ok, "msg": msg, "go": wizDepoURL(sub, msg, ok)})
}
// depoSetupDo — подготовка или проверка базы DEPO по форме экрана; итог — куда перейти (sub) и сообщение.
func (h *setupHandlers) depoSetupDo(ctx context.Context, r *http.Request, step depoStep) (string, string, bool) {
fail := func(msg string) (string, string, bool) { return "setup", msg, false }
saved := h.rc.Snapshot().Fancy
s, db, agent, bad := h.depoSetupForm(r)
if bad == "" && s.Server == "" {
bad = "укажите адрес сервера DEPO"
}
if bad != "" {
wizDepoGo(w, r, "setup", bad, false)
return
return fail(bad)
}
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Minute)
defer cancel()
note := ""
switch r.FormValue("mode") {
mode, note := r.FormValue("mode"), ""
switch mode {
case "auto":
au := strings.TrimSpace(r.FormValue("admin_user"))
if au == "" {
wizDepoGo(w, r, "setup", "введите логин и пароль администратора SQL (они используются один раз и не сохраняются)", false)
return
return fail("введите логин и пароль администратора SQL (они используются один раз и не сохраняются)")
}
if bad := depoBjCreds(r, &s); bad != "" {
wizDepoGo(w, r, "setup", bad, false)
return
return fail(bad)
}
depoEnsureLogin(&s)
if err := depoSetupRun(ctx, s, db, au, r.FormValue("admin_password"), s.User, s.Password, agent); err != nil {
wizDepoGo(w, r, "setup", "база не подготовлена — "+err.Error(), false)
return
if err := depoSetupRun(ctx, s, db, au, r.FormValue("admin_password"), s.User, s.Password, agent, step); err != nil {
return fail("база не подготовлена — " + err.Error())
}
s.DepoTablesMode = "auto"
note = "база " + db + " подготовлена: логин " + s.User + ", таблицы обмена, права"
@@ -373,60 +402,68 @@ func (h *setupHandlers) wizardDepoSetup(w http.ResponseWriter, r *http.Request)
case "existing":
u := strings.TrimSpace(r.FormValue("user"))
if u == "" {
wizDepoGo(w, r, "setup", "введите логин bj и пароль", false)
return
return fail("введите логин bj и пароль")
}
if u != s.User || r.FormValue("password") != "" {
s.User, s.Password = u, r.FormValue("password")
}
default: // script — администратор выполнил скрипт: логин и пароль bj — с экрана (они же в настройках)
if bad := depoBjCreds(r, &s); bad != "" {
wizDepoGo(w, r, "setup", bad, false)
return
return fail(bad)
}
s.DepoTablesMode = "script"
if s.User == "" || s.Password == "" {
wizDepoGo(w, r, "setup", "сначала скачайте скрипт и передайте администратору SQL", false)
return
return fail("сначала скачайте скрипт и передайте администратору SQL")
}
}
s.DepoDatabase = db
if err := h.rc.UpdateFancy(depoDraft(s, saved)); err != nil {
wizDepoGo(w, r, "setup", "не сохранилось: "+err.Error(), false)
return
return fail("не сохранилось: " + err.Error())
}
step("login", "run", "Вхожу в DEPO логином bj «"+s.User+"»")
if _, err := fancyPing(ctx, s, ""); err != nil {
wizDepoGo(w, r, "setup", note+"войти логином "+s.User+" не удалось — "+quikErrorDetail(err)+". Выполнен ли скрипт на сервере "+s.Server+"?", false)
return
return fail(note + "войти логином " + s.User + " не удалось — " + quikErrorDetail(err) + ". Выполнен ли скрипт на сервере " + s.Server + "?")
}
s.DepoNewUser, s.DepoNewPass = "", "" // вход проверен — логин и пароль в работу
if err := h.rc.UpdateFancy(s); err != nil {
wizDepoGo(w, r, "setup", "не сохранилось: "+err.Error(), false)
return
return fail("не сохранилось: " + err.Error())
}
if err := depoCheckIsDepo(ctx, s, db); err != nil { // имя базы задал клиент — та ли это база
wizDepoGo(w, r, "setup", note+err.Error(), false)
return
step("login", "ok", "bj сохранил логин «"+s.User+"» и пароль — дальше работает под ними")
if mode != "auto" { // при подготовке база уже проверена логином администратора
step("isdepo", "run", "Проверяю, что «"+db+"» — база учёта DEPO")
if err := depoCheckIsDepo(ctx, s, db); err != nil { // имя базы задал клиент — та ли это база
return fail(note + err.Error())
}
step("isdepo", "ok", "")
}
step("tables", "run", "Проверяю таблицы обмена FTV_M2M и права логина bj")
msg, ok := depoTablesCheck(ctx, s)
h.saveDepoTablesCheck(msg, ok)
if !ok {
wizDepoGo(w, r, "setup", note+msg, false)
return
return fail(note + msg)
}
if !s.IsPostgres() {
_ = depoWriteHandbook(h.rc)
}
step("tables", "ok", "")
// запуск обработки: только что созданное задание запускается в течение интервала — подождать
if agent {
step("job", "run", "Проверяю задание SQL Server Agent «"+depoJobName+"» — до 30 секунд")
} else {
step("job", "run", "Проверяю, что bj может запускать обработку обмена")
}
jmsg, jok := depoJobRunnerCheck(ctx, h.rc, "", "")
for i := 0; !jok && i < 6 && strings.Contains(jmsg, "ни разу не запускалась"); i++ {
time.Sleep(5 * time.Second)
jmsg, jok = depoJobRunnerCheck(ctx, h.rc, "", "")
}
step("job", map[bool]string{true: "ok", false: "warn"}[jok], jmsg)
step("procs", "run", "Проверяю процедуры обработки обмена")
pmsg, pok := depoProcCheck(ctx, h.rc) // процедуры обработки ставятся отдельно — шаг не держат
step("procs", map[bool]string{true: "ok", false: "warn"}[pok], pmsg)
cur := h.rc.Snapshot().Fancy
cur.DepoJobCheck, cur.DepoJobCheckOK, cur.DepoJobCheckAt = jmsg, jok, time.Now()
cur.DepoProcCheck, cur.DepoProcCheckOK, cur.DepoProcCheckAt = pmsg, pok, time.Now()
_ = h.rc.UpdateFancy(cur)
wizDepoGo(w, r, "done", note+"подключение и таблицы обмена проверены", true)
return "done", note + "подключение и таблицы обмена проверены", true
}