Первичная настройка, часть 2: шаг 9 «Оповещения и резервные копии», шаг 10 — итоговая проверка готовности (живые проверки со ссылкой «Исправить»); проверка модуля неторговых поручений QUIK (объекты M2M, инструменты, справочник получателей, цикл, ошибки операций); восстановление из копии на /first-run по одноразовому коду; возврат на тот же шаг мастера после «Сохранить» (return_to с ?step); пароль SMTP не выводится в страницу; карточки почты, адресатов и копий — общие для настроек и мастера

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-05 16:18:28 +03:00
parent fdf0ddd498
commit 17df53f25b
15 changed files with 847 additions and 155 deletions
+29 -8
View File
@@ -14,6 +14,7 @@ import (
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
@@ -188,6 +189,7 @@ type WizardData struct {
Certs bool
NSD bool
TestRun bool
Alerts bool // оповещения настроены и резервные копии включены
}
}
@@ -276,7 +278,7 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) {
}
cancel()
}
if step >= 3 && step <= 5 { // DEPO, организация, QUIK МНП — общие блоки из «Настройки»
if (step >= 3 && step <= 5) || step == 9 { // DEPO, организация, QUIK МНП, оповещения и копии — общие блоки из «Настройки»
fd := h.setupData(w, r, "")
d.Fancy = &fd
}
@@ -287,6 +289,7 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) {
}
}
d.Done.Org = doneOrg(s)
d.Done.Alerts = doneAlerts(s)
d.Done.Quik = !d.DepoLicensed || doneQuik(s)
if step == 3 && d.DepoLicensed && strings.TrimSpace(s.Fancy.Server) != "" {
// Есть ли у сервера SQL Agent: у Express его нет, у PostgreSQL — тоже; тогда обработку
@@ -329,8 +332,8 @@ func depoServerEdition(ctx context.Context, f FancyMSSQLSettings) (string, bool)
}
// wizardSteps — шагов в мастере: 1 лицензия, 2 база bj, 3 учёт DEPO, 4 наша организация, 5 QUIK МНП,
// 6 Валидата, 7 сертификаты, 8 шлюз НРД, 9 проверка.
const wizardSteps = 9
// 6 Валидата, 7 сертификаты, 8 шлюз НРД, 9 оповещения и резервные копии, 10 проверка готовности.
const wizardSteps = 10
// wizardProgressFile — номер последнего пройденного шага мастера (переход «Далее» на шаг N
// означает, что шаги до N пройдены).
@@ -998,7 +1001,7 @@ func (h *setupHandlers) saveAlertsSMTP(w http.ResponseWriter, r *http.Request) {
Port: atoiDefault(r.FormValue("port"), 587),
Security: nonEmpty(r.FormValue("security"), "starttls"),
Username: strings.TrimSpace(r.FormValue("username")),
Password: r.FormValue("password"),
Password: pickStr(r.FormValue("password"), a.SMTP.Password), // пароль в страницу не выводится: пусто — прежний
From: strings.TrimSpace(r.FormValue("from")),
To: strings.TrimSpace(r.FormValue("to")),
LastMsg: a.SMTP.LastMsg,
@@ -1686,7 +1689,11 @@ func setupFlash(w http.ResponseWriter, r *http.Request, msg string) {
if s := r.FormValue("section"); s != "" && isAnchorName(s) {
q.Set("scrollto", s)
}
http.Redirect(w, r, dest+"?"+q.Encode(), http.StatusSeeOther)
sep := "?"
if strings.Contains(dest, "?") {
sep = "&"
}
http.Redirect(w, r, dest+sep+q.Encode(), http.StatusSeeOther)
}
// safeReturnPath пропускает только наши же admin-пути ("/admin/..."), без
@@ -1701,13 +1708,27 @@ func safeReturnPath(p string) string {
if strings.HasPrefix(p, "//") || strings.ContainsAny(p, ":\\\r\n") {
return ""
}
// только путь, без query/fragment
if i := strings.IndexAny(p, "?#"); i >= 0 {
if i := strings.IndexByte(p, '#'); i >= 0 {
p = p[:i]
}
return p
// путь и безопасные параметры (например ?step=4&sub=done в мастере); flash/scrollto — свежие
path, query, _ := strings.Cut(p, "?")
var keep []string
for _, kv := range strings.Split(query, "&") {
k, v, ok := strings.Cut(kv, "=")
if !ok || k == "flash" || k == "scrollto" || !safeQueryPart.MatchString(k) || !safeQueryPart.MatchString(v) {
continue
}
keep = append(keep, k+"="+v)
}
if len(keep) > 0 {
return path + "?" + strings.Join(keep, "&")
}
return path
}
var safeQueryPart = regexp.MustCompile(`^[A-Za-z0-9_-]{1,40}$`)
// isAnchorName пропускает только безопасные id-якоря (буквы/цифры/-/_),
// чтобы не дать заголовкам ответа инжектить произвольное.
func isAnchorName(s string) bool {