Кабинет: веб-установщик и управление пользователями в ядре

- установщик в самом кабинете: до настройки любая страница ведёт в /install; три шага —
  проверка сервера (каталог данных, https), связь с ядром (режим, адрес, ключ; связь
  проверяется до сохранения, в режиме ожидания страница ждёт подключения ядра),
  администратор кабинета (заводится в ядре через /api/lk/admin/bootstrap, только пока
  администраторов нет); настройки в data/lk.json 0600, окружение перекрывает файл;
  после установки установщик закрыт
- настройки кабинета можно менять на ходу: Server за мьютексом, один mux на всё время
- ключ модуля больше не кладётся в архив: его вводят в установщике; на странице настроек
  ключ показан целиком 10 минут после создания, потом маска
- ядро: карточка «Пользователи кабинета» в настройках — список, заведение, смена пароля
  (гасит сессии), блокировка, права сотрудника, разрешение поручений, удаление; письма
  не отправляются
- инструкция /admin/help/lk дополнена шагами установщика и разделом о подходящем хостинге
- тесты: TestInstaller, TestNotConfigured, TestSetupPageLKUsersAndLink; установщик
  проверен вживую вторым экземпляром против боевого ядра

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
zuevav
2026-09-18 09:30:47 +03:00
parent e1bf7c440f
commit 222d48e740
14 changed files with 977 additions and 105 deletions
+36
View File
@@ -3,6 +3,7 @@ package lkgateway
import (
"crypto/subtle"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
@@ -24,6 +25,7 @@ import (
func registerLKAPI(mux *http.ServeMux, rc *RuntimeConfig, svc *Service) {
sess := newLKSessions()
sess.bindNetwork = func() bool { return rc.Snapshot().LK.BindNetwork }
globalLKSessions = sess
// проверка ключа модуля
auth := func(w http.ResponseWriter, r *http.Request) bool {
@@ -117,6 +119,40 @@ func registerLKAPI(mux *http.ServeMux, rc *RuntimeConfig, svc *Service) {
})
})
// Первый администратор кабинета из веб-установщика. Только по ключу модуля и только
// пока администраторов нет: потом учётные записи заводит администратор в настройках.
mux.HandleFunc("/api/lk/admin/bootstrap", func(w http.ResponseWriter, r *http.Request) {
if !auth(w, r) {
return
}
if r.Method != http.MethodPost {
lkJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "нужен POST"})
return
}
has, err := lkHasAdmin(r.Context(), rc)
if err != nil {
lkJSON(w, http.StatusBadGateway, map[string]string{"error": "база кабинета недоступна: " + err.Error()})
return
}
if has {
lkJSON(w, http.StatusConflict, map[string]string{"error": "администратор кабинета уже создан — войдите под ним или пропустите шаг"})
return
}
var in struct{ Login, Password, FullName, Email string }
if json.NewDecoder(io.LimitReader(r.Body, 8<<10)).Decode(&in) != nil {
lkJSON(w, http.StatusBadRequest, map[string]string{"error": "некорректный JSON"})
return
}
u := LKUser{Login: strings.TrimSpace(in.Login), Contract: "ADMIN", FullName: strings.TrimSpace(in.FullName),
Email: strings.TrimSpace(in.Email), Active: true, IsAdmin: true}
if err := lkCreateUser(r.Context(), rc, u, in.Password); err != nil {
lkJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
raiseSystemAlert(rc, "Кабинет: создан администратор", fmt.Sprintf("Из установщика кабинета создан администратор «%s».", u.Login))
lkJSON(w, http.StatusOK, map[string]any{"ok": true, "message": "администратор создан"})
})
mux.HandleFunc("/api/lk/logout", func(w http.ResponseWriter, r *http.Request) {
if !auth(w, r) {
return