feat(install): консольный установщик bj и первичная настройка по одноразовому коду

- cmd/bj-installer (вместо веб-мастера): проверка Debian 13 / Astra, пакеты, пользователь и
  каталоги, активация лицензии ключом продукта, PostgreSQL и миграции m2m_core, компоненты
  по лицензии с проверкой подписи, root-скрипты и службы, одноразовый код и итоговый экран
  с адресом http://<сервер>:8080/first-run; проверка в песочнице (--steps, --root)
- bj: /first-run — пока нет пользователей и установщик оставил код, по коду создаётся
  первый администратор (прежние установки без файла работают как раньше)
- internal/release/install.go — общая установка компонентов (bj-component-install и
  установщик); bj-component-install стал компонентом ядра
- открытые ключи вынесены в internal/license и internal/release; миграции и файлы
  развёртывания встроены (migrations/embed.go, deploy/embed.go); unit bj-server исправлен
- scripts/build-release.sh: сборка с версиями, подпись, публикация на сервер обновлений

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zuevav
2026-09-15 23:56:44 +03:00
parent 8099e50ab2
commit 4135435b51
24 changed files with 1313 additions and 1807 deletions
+19 -6
View File
@@ -490,7 +490,7 @@ type AuthSettings struct {
// Перепроверка групп доменных пользователей (adrecheck.go): поиск от имени
// сервисной учётной записи только на чтение. Пусто — роль меняется только при входе.
ADBindUser string `json:"ad_bind_user"` // svc-bj@corp.example.ru, CORP\svc-bj или DN
ADBindUser string `json:"ad_bind_user"` // svc-bj@corp.example.ru, CORP\svc-bj или DN
ADBindPassword string `json:"ad_bind_password"`
ADRecheckMinutes int `json:"ad_recheck_minutes"` // 0 = 15
}
@@ -805,7 +805,7 @@ func currentUser(w http.ResponseWriter) *User {
// authExempt — пути без входа: API модулей (свой ключ/токен), health, статика.
func authExempt(p string) bool {
switch {
case p == "/login" || p == "/logout" || p == "/healthz":
case p == "/login" || p == "/logout" || p == "/healthz" || p == "/first-run":
return true
case strings.HasPrefix(p, "/api/"):
return true
@@ -857,6 +857,14 @@ func authMiddleware(rc *RuntimeConfig, next http.Handler) http.Handler {
next.ServeHTTP(w, r)
return
}
if firstRunPending() { // свежая установка: сначала администратор по одноразовому коду
if isAjax(r) {
http.Error(w, "требуется первичная настройка", http.StatusUnauthorized)
return
}
http.Redirect(w, r, "/first-run", http.StatusSeeOther)
return
}
if authOpen() {
next.ServeHTTP(&authRW{ResponseWriter: w, user: nil}, r)
return
@@ -924,9 +932,14 @@ type loginData struct {
var loginTpl = template.Must(template.New("login").Parse(loginHTML))
func registerAuth(mux *http.ServeMux, rc *RuntimeConfig) {
registerFirstRun(mux)
mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
a := rc.Snapshot().Auth
next := safeReturnPath(r.FormValue("next"))
if firstRunPending() {
http.Redirect(w, r, "/first-run", http.StatusSeeOther)
return
}
if authOpen() {
http.Redirect(w, r, "/admin/setup#users", http.StatusSeeOther)
return
@@ -978,10 +991,10 @@ func registerAuth(mux *http.ServeMux, rc *RuntimeConfig) {
// ---------- обработчики настроек ----------
type UsersView struct {
Users []*User
Roles []roleDef // доступные при текущей лицензии
Hidden []struct{ Title, Missing string } // роли, недоступные без модулей
Modules []struct{ Key, Title string } // подключённые модули
Users []*User
Roles []roleDef // доступные при текущей лицензии
Hidden []struct{ Title, Missing string } // роли, недоступные без модулей
Modules []struct{ Key, Title string } // подключённые модули
Sessions []*Session
Open bool
Me *User