Установка: приветственный экран консоли (bj-welcome: адрес веб-входа и состояние, всегда), ядро с образа без ключа продукта (--bundle), ключ продукта на первой веб-странице, повтор вопроса при каждой загрузке

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
This commit is contained in:
zuevav
2026-09-23 08:53:13 +03:00
parent 925c516e1e
commit 492e226e54
9 changed files with 263 additions and 25 deletions
+24
View File
@@ -7,10 +7,12 @@ package lkgateway
// (открытый режим до создания первого пользователя).
import (
"context"
"encoding/json"
"html/template"
"log"
"net/http"
"net/url"
"os"
"strings"
"time"
@@ -36,6 +38,8 @@ func firstRunPending() bool {
type firstRunData struct {
Error, Login, Name string
NeedKey bool // лицензия ещё не активирована — предложить ввести ключ продукта здесь
ProductKey string
}
var firstRunTpl = template.Must(template.New("first-run").Parse(firstRunHTML))
@@ -47,11 +51,13 @@ func registerFirstRun(mux *http.ServeMux) {
return
}
var d firstRunData
d.NeedKey = globalRC != nil && !licenseStatus(globalRC).Valid
w.Header().Set("Cache-Control", "no-store")
if r.Method == http.MethodPost {
ip := clientIP(r)
key := "first-run|" + ip
d.Login, d.Name = strings.TrimSpace(r.FormValue("login")), strings.TrimSpace(r.FormValue("name"))
d.ProductKey = strings.ToUpper(strings.NewReplacer(" ", "").Replace(strings.TrimSpace(r.FormValue("product_key"))))
pw, pw2 := r.FormValue("password"), r.FormValue("password2")
code := strings.NewReplacer("-", "", " ", "").Replace(strings.TrimSpace(r.FormValue("code")))
var st firstRunState
@@ -88,6 +94,23 @@ func registerFirstRun(mux *http.ServeMux) {
s := globalSessions.New(u.Login, ip, r.UserAgent())
http.SetCookie(w, &http.Cookie{Name: sessionCookie, Value: s.Token, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, Secure: r.TLS != nil, MaxAge: int(sessionMaxLife.Seconds())})
log.Printf("auth: первичная настройка: создан администратор %s с %s", u.Login, ip)
// ключ продукта, если его не вводили на сервере: активируем сразу, чтобы модули и обновления заработали
if d.NeedKey && d.ProductKey != "" && globalRC != nil {
actx, acancel := context.WithTimeout(r.Context(), 30*time.Second)
err := LicenseActivate(actx, globalRC, d.ProductKey)
acancel()
if err != nil {
log.Printf("auth: первичная настройка: ключ продукта не принят: %v", err)
http.Redirect(w, r, "/admin/setup?flash="+url.QueryEscape("Лицензия: ключ продукта не принят — "+err.Error()+". Введите его в разделе «Лицензия».")+"#sec-license", http.StatusSeeOther)
return
}
http.Redirect(w, r, "/admin/setup?flash="+url.QueryEscape("Лицензия активирована. Дальше — подключение к базе DEPO, затем реквизиты НРД (кнопка «Определить из DEPO»)."), http.StatusSeeOther)
return
}
if d.NeedKey {
http.Redirect(w, r, "/admin/setup?flash="+url.QueryEscape("Администратор создан. Лицензия ещё не активирована — введите ключ продукта в разделе «Лицензия», иначе модули и обновления недоступны.")+"#sec-license", http.StatusSeeOther)
return
}
http.Redirect(w, r, "/admin/setup", http.StatusSeeOther)
return
}
@@ -123,6 +146,7 @@ code{font-size:12px}
<label>Имя</label><input type="text" name="name" value="{{.Name}}" autocomplete="name">
<label>Пароль, не короче 10 символов</label><input type="password" name="password" minlength="10" autocomplete="new-password" required>
<label>Повтор пароля</label><input type="password" name="password2" minlength="10" autocomplete="new-password" required>
{{if .NeedKey}}<label>Ключ продукта (BJ-XXXX-XXXX-XXXX) — если не вводили на сервере; можно позже в «Настройка → Лицензия»</label><input type="text" name="product_key" value="{{.ProductKey}}" placeholder="BJ-XXXX-XXXX-XXXX" autocomplete="off" style="text-transform:uppercase">{{end}}
<button type="submit">Создать администратора</button>
{{if .Error}}<div class="err">{{.Error}}</div>{{end}}
<div class="hint">Код потерян или истёк: на сервере выполните <code>sudo bj-installer first-run-code</code>.</div>