Пароль домена не попадает в страницу; домен в мастере — открыт, крупно, группы; справочник DEPO без переводов; 1С — бэк-офис

- Безопасность: в мастере подпись к паролю выводила сам сохранённый пароль служебной записи домена — теперь только «задан/введён» (тест: пароль не попадает в HTML). Список сессий в настройках больше не содержит токены (по ним можно было войти под чужим именем) — короткий ID.
- Домен: после подключения и «Доверять» блок остаётся открытым, страница возвращается к нему; карточка «Домен подключён» и «Найден домен» — крупно; поиск сотрудников и групп; группе — доступ с ролью (ADGroups), список групп с «убрать».
- «Обмен с DEPO»: справочник FTV_M2M_HANDBOOK считается и когда переводов ещё нет (на свежей установке писало «не создана»).
- Модуль 1С: onec-back «1С: база бэк-офиса (тест)» вместо мидл-офиса (в лицензиях не выдавался).

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-07 01:19:30 +03:00
parent 13a7be17ef
commit 73b246831c
13 changed files with 238 additions and 71 deletions
+35 -9
View File
@@ -15,6 +15,7 @@ package lkgateway
import (
"context"
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"crypto/tls"
"crypto/x509"
@@ -479,20 +480,45 @@ func (s *sessionStore) DropUser(login string) {
s.mu.Unlock()
}
func (s *sessionStore) Active() []*Session {
// SessionView — сессия для списка в настройках: без токена (по нему можно войти под чужим именем), вместо него — ID.
type SessionView struct {
ID, Login, IP, UA string
Created, Seen time.Time
}
// sessionID — короткий отпечаток токена: им список ссылается на сессию, сам токен в страницу не попадает.
func sessionID(token string) string {
sum := sha256.Sum256([]byte("bj-session-id:" + token))
return hex.EncodeToString(sum[:9])
}
func (s *sessionStore) Active() []SessionView {
s.mu.Lock()
defer s.mu.Unlock()
out := make([]*Session, 0, len(s.list))
for _, x := range s.list {
out := make([]SessionView, 0, len(s.list))
for t, x := range s.list {
if time.Since(x.Created) < sessionMaxLife {
cp := *x
out = append(out, &cp)
out = append(out, SessionView{ID: sessionID(t), Login: x.Login, IP: x.IP, UA: x.UA, Created: x.Created, Seen: x.Seen})
}
}
sort.Slice(out, func(i, j int) bool { return out[i].Seen.After(out[j].Seen) })
return out
}
// DropID — завершить сессию по её ID из списка.
func (s *sessionStore) DropID(id string) bool {
s.mu.Lock()
defer s.mu.Unlock()
for t := range s.list {
if id != "" && sessionID(t) == id {
delete(s.list, t)
s.save()
return true
}
}
return false
}
// ---------- защита от перебора ----------
var loginFails = struct {
@@ -910,12 +936,12 @@ func moduleForPath(p string) string {
case p == "/admin/setup/lk", has("/admin/setup/lk-check"), has("/admin/setup/lk-integration"), has("/admin/setup/lk-pull"):
return "esia-finance" // интеграция с платформой ЕСИА Финанс — отдельный модуль лицензии
case has("/admin/setup/onec"):
return "onec-middle"
return "onec-back"
}
return ""
}
var moduleTitles = map[string]string{"esia-finance": "ЕСИА Финанс", "quik-mnp": "QUIK: неторговые поручения и обмен с DEPO", "quik-export": "Экспорт биржевой информации QUIK", "web-cabinet": "Личный кабинет клиента", "npr": "Риск-монитор НПР / УДС", "onec-middle": "1С: база мидл-офиса"}
var moduleTitles = map[string]string{"esia-finance": "ЕСИА Финанс", "quik-mnp": "QUIK: неторговые поручения и обмен с DEPO", "quik-export": "Экспорт биржевой информации QUIK", "web-cabinet": "Личный кабинет клиента", "npr": "Риск-монитор НПР / УДС", "onec-back": "1С: база бэк-офиса"}
// moduleTitle — признак лицензии словами (в мастере и на странице лицензии — не коды вроде quik-mnp).
func moduleTitle(f string) string {
@@ -1186,7 +1212,7 @@ type UsersView struct {
Roles []roleDef // доступные при текущей лицензии
Hidden []struct{ Title, Missing string } // роли, недоступные без модулей
Modules []struct{ Key, Title string } // подключённые модули
Sessions []*Session
Sessions []SessionView
Open bool
Me *User
ADRecheck ADRecheckStatus
@@ -1373,7 +1399,7 @@ func (h *setupHandlers) dropSession(w http.ResponseWriter, r *http.Request) {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
globalSessions.Drop(r.FormValue("token"))
globalSessions.DropID(r.FormValue("id"))
h.usersFlash(w, r, "Сессия завершена.")
}