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

- установщик в самом кабинете: до настройки любая страница ведёт в /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
+20 -3
View File
@@ -19,6 +19,7 @@ import (
"errors"
"net/http"
"sync"
"sync/atomic"
"time"
)
@@ -50,6 +51,15 @@ type agentQueue struct {
waiting map[string]*agentTask
signal chan struct{}
seq uint64
// lastFetch — когда ядро в последний раз забирало запросы: по этому установщик
// понимает, что ядро дозвонилось до кабинета.
lastFetch atomic.Int64
}
// coreSeen — ядро подключалось за последнюю минуту.
func (q *agentQueue) coreSeen() bool {
t := q.lastFetch.Load()
return t > 0 && time.Since(time.Unix(t, 0)) < time.Minute
}
func newAgentQueue() *agentQueue {
@@ -142,8 +152,13 @@ func idFromSeq(n uint64) string {
// registerAgent — точки, куда подключается ядро (только в режиме ожидания).
func (s *Server) registerAgent(mux *http.ServeMux) {
// В прямом режиме этих точек как бы нет: очередь не создана — отвечаем 404.
keyOK := func(w http.ResponseWriter, r *http.Request) bool {
if !secureEqual(r.Header.Get("X-BJ-Key"), s.cfg.BJKey) {
if s.agentQueue() == nil {
http.NotFound(w, r)
return false
}
if !secureEqual(r.Header.Get("X-BJ-Key"), s.conf().BJKey) {
http.Error(w, "неверный ключ модуля", http.StatusForbidden)
return false
}
@@ -155,7 +170,9 @@ func (s *Server) registerAgent(mux *http.ServeMux) {
return
}
wait := 25 * time.Second
t := s.agent.next(r.Context(), wait)
q := s.agentQueue()
q.lastFetch.Store(time.Now().Unix())
t := q.next(r.Context(), wait)
if t == nil {
w.WriteHeader(http.StatusNoContent)
return
@@ -182,7 +199,7 @@ func (s *Server) registerAgent(mux *http.ServeMux) {
return
}
body, _ := base64.StdEncoding.DecodeString(in.Body)
if !s.agent.reply(in.ID, agentResult{Status: in.Status, Body: body}) {
if !s.agentQueue().reply(in.ID, agentResult{Status: in.Status, Body: body}) {
w.WriteHeader(http.StatusGone) // запрос уже не ждут: клиент ушёл или истёк срок
return
}