Индикатор «Шлюз НРД», проверка ключа ИШ, папки шлюза по сети, сотрудники из домена, переключатель QUIK в настройках
- журнал ИШ: номер процесса выравнивается пробелами — опросы НРД не распознавались (серый индикатор) - «Проверить СКЗИ и ключ»: Валидата, датчик случайных чисел, подпись профилем ИШ (а не старый bj-crypto); индикатор «Криптографии» — по готовности ключа; карточка автозагрузки УЦ — только для разработки - шаг «Шлюз НРД»: папки обмена по сети (Samba): логин/пароль, ручная настройка не трогается - «Учётные записи»: необязательно — подключить домен, проверить связь, найти сотрудника и добавить с ролью; роль из bj действует, пока группы домена с ролями не сопоставлены - настройка QUIK МНП: «Только чтение / Чтение и запись», тонкая настройка цикла свёрнута - индикатор «Обновления» — только когда есть что обновить; «Мастер настройки» в меню и в настройках Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -321,5 +321,47 @@ SQL
|
||||
err "канал $CODE$dep записан, но движок ИШ не перешёл в Running — см. журнал ИШ $DATA_DIR/logs-svc/log.txt (отпечаток УЦ, адрес контура, ключ)"
|
||||
fi ;;
|
||||
|
||||
# папки обмена шлюза по сети (SMB): для программ организации, которые кладут/забирают файлы через ИШ. bj это не
|
||||
# нужно. Блок общих папок в smb.conf — между метками bj; заведённые вручную [igate_exchange] не трогаем.
|
||||
share)
|
||||
CONF=/etc/samba/smb.conf; B='# >>> bj: папки шлюза НРД'; E='# <<< bj: папки шлюза НРД'
|
||||
on=$(j enabled); su=$(j user); sp=$(j password)
|
||||
manual=false; [ -f "$CONF" ] && sed "/^$B/,/^$E/d" "$CONF" | grep -q '^\[igate_exchange\]' && manual=true
|
||||
if [ "${on:-}" = status ] || [ -z "${on:-}" ]; then
|
||||
mine=false; [ -f "$CONF" ] && grep -q "^$B" "$CONF" && mine=true
|
||||
u=$( [ "$mine" = true ] && sed -n "/^$B/,/^$E/p" "$CONF" | sed -n 's/^ *valid users *= *//p' | head -1 )
|
||||
[ "$manual" = true ] && [ -z "$u" ] && u=$(sed -n '/^\[igate_exchange\]/,/^\[/p' "$CONF" | sed -n 's/^ *valid users *= *//p' | head -1)
|
||||
echo "{\"ok\":true,\"on\":$( [ "$mine" = true ] || [ "$manual" = true ] && echo true || echo false),\"manual\":$manual,\"user\":$(q "$u"),\"smbd\":$(q "$(systemctl is-active smbd 2>/dev/null)")}"; exit 0
|
||||
fi
|
||||
[ "$manual" = true ] && err "папка igate_exchange уже открыта вручную в $CONF — bj её не меняет; чтобы управлять из bj, уберите её из smb.conf"
|
||||
if [ "$on" != true ]; then
|
||||
[ -f "$CONF" ] && sed -i "/^$B/,/^$E/d" "$CONF"
|
||||
systemctl reload smbd >/dev/null 2>&1 || true
|
||||
ok "доступ к папкам шлюза по сети закрыт"; exit 0
|
||||
fi
|
||||
[[ "$su" =~ ^[a-z][a-z0-9_-]{2,31}$ ]] || err "логин — латинские строчные буквы, цифры, _ и -, от 3 символов, начинается с буквы"
|
||||
case "$su" in root|igate|bj|postgres) err "логин $su занят системой — выберите другой" ;; esac
|
||||
if ! command -v smbd >/dev/null 2>&1; then
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -q samba >/dev/null 2>&1 || err "не удалось установить Samba (apt-get install samba) — нужен доступ к репозиторию"
|
||||
fi
|
||||
id "$su" >/dev/null 2>&1 || useradd --system --no-create-home --shell /usr/sbin/nologin "$su" || err "пользователь $su не создан"
|
||||
if [ -n "$sp" ]; then
|
||||
[ ${#sp} -ge 8 ] || err "пароль — не короче 8 символов"
|
||||
printf '%s\n%s\n' "$sp" "$sp" | smbpasswd -s -a "$su" >/dev/null 2>&1 || err "пароль для $su не задан (smbpasswd)"
|
||||
smbpasswd -e "$su" >/dev/null 2>&1
|
||||
elif ! pdbedit -L 2>/dev/null | grep -q "^$su:"; then
|
||||
err "задайте пароль для $su"
|
||||
fi
|
||||
[ -f "$CONF" ] && sed -i "/^$B/,/^$E/d" "$CONF"
|
||||
{ echo "$B"
|
||||
for c in test3 prod; do d=$(exchange_dir "$c"); [ -d "$d" ] || continue
|
||||
n=igate_exchange; [ "$c" = prod ] && n=igate_exchange_prod
|
||||
printf '[%s]\n comment = Шлюз НРД: папки обмена %s (INBOX/OUTBOX/SENT/ERRORS)\n path = %s\n browseable = yes\n read only = no\n valid users = %s\n force user = %s\n force group = %s\n' "$n" "$c" "$d" "$su" "$SVC_USER" "$SVC_USER"
|
||||
done
|
||||
echo "$E"; } >> "$CONF"
|
||||
testparm -s "$CONF" >/dev/null 2>&1 || { sed -i "/^$B/,/^$E/d" "$CONF"; err "smb.conf после правки не проходит проверку (testparm) — правка отменена"; }
|
||||
systemctl enable smbd >/dev/null 2>&1; systemctl restart smbd >/dev/null 2>&1 || err "служба smbd не перезапустилась"
|
||||
ok "папки шлюза открыты по сети для $su: \\\\$(hostname -I | awk '{print $1}')\\igate_exchange" ;;
|
||||
|
||||
*) err "неизвестная команда: $cmd" ;;
|
||||
esac
|
||||
|
||||
@@ -60,6 +60,22 @@ keysvc_restart(){
|
||||
sleep 1
|
||||
}
|
||||
|
||||
# ---- проверка ключа: подписать проверочный документ профилем ИШ активного контура (как подписывает ИШ) ----
|
||||
KEYCHECK="skip"; KEYDIAG=""
|
||||
keysign(){ # 0 — ключ подписал проверочный документ под igate
|
||||
local kt ko; kt=$(mktemp -d); echo "bj key check" > "$kt/in"; chown -R igate:igate "$kt"
|
||||
ko=$(timeout -k 5 60 runuser -u igate -- env HOME=/var/lib/igate LD_LIBRARY_PATH="$VDLIB" "$ZPKI" -sign -profile "$PROFILE" -registry -in "$kt/in" -out "$kt/out.p7s" 2>&1)
|
||||
if [ -s "$kt/out.p7s" ]; then rm -rf "$kt"; KEYCHECK=ok; return 0; fi
|
||||
rm -rf "$kt"
|
||||
KEYCHECK="fail: $(printf '%s' "$ko" | grep -iE 'ошибк|результат|error' | head -1 | tr -d '"\\' | head -c 200)"; return 1
|
||||
}
|
||||
keydiag(){ # что видит ИШ: смонтирована ли ключевая флешка и какие ключи (*.vdk) ему доступны
|
||||
local m v
|
||||
m=$(findmnt -rn -o SOURCE,FSTYPE "$DEFAULT_KEYMEDIA" 2>/dev/null | grep -v autofs | head -1)
|
||||
v=$(runuser -u igate -- env HOME=/var/lib/igate sh -c 'ls -1L "$HOME"/.Validata/vdkeys/*/*.vdk "$HOME"/.Validata/vdkeys/*.vdk 2>/dev/null' | xargs -r -n1 basename | tr '\n' ' ')
|
||||
KEYDIAG="флешка в $DEFAULT_KEYMEDIA: ${m:-не смонтирована}; ключи, видимые ИШ: ${v:-нет}; служба ключей Валидаты (vdcrysvc): $(pgrep -x vdcrysvc >/dev/null && echo работает || echo 'не запущена')"
|
||||
}
|
||||
|
||||
if [ "$ACTION" = "restart-ish" ]; then
|
||||
systemctl restart igate.service >/dev/null 2>&1 || systemctl restart igate-svc >/dev/null 2>&1 || true
|
||||
echo '{"ok":true,"action":"restart-ish"}'; exit 0
|
||||
@@ -106,6 +122,14 @@ for p in sys.argv[1:]:
|
||||
print(json.dumps(out,ensure_ascii=False))
|
||||
PY2
|
||||
}
|
||||
if [ "$ACTION" = "keycheck" ]; then
|
||||
prof_dir; PROFILE="$PROFILE_NAME"
|
||||
RNG=$([ -x /usr/local/sbin/bj-validata-rng ] && /usr/local/sbin/bj-validata-rng status 2>/dev/null | grep -o '"ready":[a-z]*' | cut -d: -f2)
|
||||
keysvc_up_quiet(){ pgrep -x vdcrysvc >/dev/null || keysvc_restart; }; keysvc_up_quiet
|
||||
keysign || keydiag
|
||||
printf '{"ok":true,"profile":"%s","rng":"%s","keycheck":"%s","keydiag":"%s"}\n' "$PROFILE" "${RNG:-}" "$KEYCHECK" "$(printf '%s' "$KEYDIAG" | tr -d '"\\')"
|
||||
exit 0
|
||||
fi
|
||||
if [ "$ACTION" = "certs-list" ]; then
|
||||
prof_dir
|
||||
echo "{\"ok\":true,\"profile\":\"$PROFILE_NAME\",\"dir\":\"$PROFDIR\",\"certs\":$(certs_list_json)}"; exit 0
|
||||
@@ -328,20 +352,6 @@ PY
|
||||
fi
|
||||
|
||||
# ---- проверка: ключ подписывает под igate (как будет подписывать ИШ) — проверочный документ профилем ----
|
||||
KEYCHECK="skip"; KEYDIAG=""
|
||||
keysign(){ # 0 — ключ подписал проверочный документ под igate
|
||||
local kt ko; kt=$(mktemp -d); echo "bj key check" > "$kt/in"; chown -R igate:igate "$kt"
|
||||
ko=$(timeout -k 5 60 runuser -u igate -- env HOME=/var/lib/igate LD_LIBRARY_PATH="$VDLIB" "$ZPKI" -sign -profile "$PROFILE" -registry -in "$kt/in" -out "$kt/out.p7s" 2>&1)
|
||||
if [ -s "$kt/out.p7s" ]; then rm -rf "$kt"; KEYCHECK=ok; return 0; fi
|
||||
rm -rf "$kt"
|
||||
KEYCHECK="fail: $(printf '%s' "$ko" | grep -iE 'ошибк|результат|error' | head -1 | tr -d '"\\' | head -c 200)"; return 1
|
||||
}
|
||||
keydiag(){ # что видит ИШ: смонтирована ли ключевая флешка и какие ключи (*.vdk) ему доступны
|
||||
local m v
|
||||
m=$(findmnt -rn -o SOURCE,FSTYPE "$DEFAULT_KEYMEDIA" 2>/dev/null | grep -v autofs | head -1)
|
||||
v=$(runuser -u igate -- env HOME=/var/lib/igate sh -c 'ls -1L "$HOME"/.Validata/vdkeys/*/*.vdk "$HOME"/.Validata/vdkeys/*.vdk 2>/dev/null' | xargs -r -n1 basename | tr '\n' ' ')
|
||||
KEYDIAG="флешка в $DEFAULT_KEYMEDIA: ${m:-не смонтирована}; ключи, видимые ИШ: ${v:-нет}; служба ключей Валидаты (vdcrysvc): $(pgrep -x vdcrysvc >/dev/null && echo работает || echo 'не запущена')"
|
||||
}
|
||||
if [ "$IGATE_RES" = connected ] || [ "$IGATE_RES" = pending-channel ]; then
|
||||
if ! keysign; then
|
||||
if [ -n "$TARGET_DEV" ] && [ -b "$TARGET_DEV" ] && [ -z "$(findmnt -rn -o FSTYPE "$DEFAULT_KEYMEDIA" 2>/dev/null | grep -v autofs)" ]; then
|
||||
|
||||
@@ -12,7 +12,7 @@ process_one(){
|
||||
quikexport-*) out=$(BJ_EXTRA="$extra" /usr/local/sbin/bj-quikexport-admin "${action#quikexport-}" "$staging" 2>&1) ;;
|
||||
component-install) out=$(/usr/local/sbin/bj-component-install "$staging" 2>&1) ;;
|
||||
# интеграционный шлюз НРД: состояние, установка, канал контура (TEST3 | боевой); параметры — JSON в extra
|
||||
igate-status|igate-install|igate-contour) out=$(BJ_EXTRA="$extra" /usr/local/sbin/bj-igate-admin "${action#igate-}" 2>&1) ;;
|
||||
igate-status|igate-install|igate-contour|igate-share) out=$(BJ_EXTRA="$extra" /usr/local/sbin/bj-igate-admin "${action#igate-}" 2>&1) ;;
|
||||
# датчик случайных чисел Валидаты: проверка и сеанс «двигайте мышью» (движения — из мастера bj)
|
||||
rng-status|rng-start|rng-cancel) out=$(/usr/local/sbin/bj-validata-rng "${action#rng-}" 2>&1) ;;
|
||||
# системные настройки сервера: серверы времени (NTP)
|
||||
|
||||
@@ -62,6 +62,15 @@ var templateFuncs = template.FuncMap{
|
||||
"smbWin": smbWin,
|
||||
"smbView": smbView,
|
||||
"pgLocal": pgDSNLocal,
|
||||
// СКЗИ готова: Валидата установлена и ключ организации подключён (как шаги мастера «Валидата» и «Ключ»)
|
||||
"cryptoReady": func() bool {
|
||||
if globalRC == nil {
|
||||
return false
|
||||
}
|
||||
v, certs := setupProbes(globalRC)
|
||||
return v != "" && certs > 0
|
||||
},
|
||||
"withRet": func(d any, ret string) map[string]any { return map[string]any{"D": d, "Ret": ret} },
|
||||
"whForm": func(w WorkHoursSettings, ret string) map[string]any { return map[string]any{"W": w, "Ret": ret} },
|
||||
"weekdayNames": func() []string { return weekdayShort },
|
||||
"depoAccPrefix": depoAccPrefix,
|
||||
|
||||
@@ -93,6 +93,11 @@ func adBindService(conn *ldap.Conn, a AuthSettings) error {
|
||||
// errADNotFound или errADDisabled; roleErr — nil или *adNoGroupError.
|
||||
// dropSessions — доступ снят, сессии нужно завершить.
|
||||
func adApplyRecheck(u *User, res *adResult, lookupErr, roleErr error) (changed, dropSessions bool, msg string) {
|
||||
var noGroup *adNoGroupError
|
||||
if lookupErr == nil && errors.As(roleErr, &noGroup) && u.BJRole != "" && roleAvailable(u.BJRole) {
|
||||
// групп с ролью у сотрудника нет, но роль назначена в bj — она и действует
|
||||
roleErr, res = nil, &adResult{Role: u.BJRole, MatchedBy: "роль, назначенная в bj", Groups: groupsOf(res)}
|
||||
}
|
||||
if lookupErr != nil || roleErr != nil {
|
||||
reason := "нет в сопоставленных группах"
|
||||
if lookupErr != nil {
|
||||
@@ -225,3 +230,10 @@ func (h *setupHandlers) recheckAD(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
h.usersFlash(w, r, fmt.Sprintf("AD: перепроверено пользователей %d, изменений %d.", st.Checked, st.Changed))
|
||||
}
|
||||
|
||||
func groupsOf(res *adResult) []string {
|
||||
if res == nil {
|
||||
return nil
|
||||
}
|
||||
return res.Groups
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package lkgateway
|
||||
|
||||
// Сотрудники из домена в шаге «Учётные записи» (07.10.2026). Основной путь — создать учётную запись bj сразу
|
||||
// (логин и пароль); второй, необязательный, — подключить домен Active Directory прямо здесь, проверить связь и
|
||||
// выбрать сотрудника поиском: он входит своим доменным логином и паролем, bj проверяет пароль в домене.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
func (h *setupHandlers) registerADWizard(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/admin/setup/users/ad-connect", h.adConnect)
|
||||
mux.HandleFunc("/admin/setup/users/ad-search", h.adSearch)
|
||||
}
|
||||
|
||||
// adConnect — POST: адрес контроллера, домен, сервисная учётная запись (для поиска сотрудников); остальные
|
||||
// настройки входа не трогает. Сразу проверяет связь: контроллер отвечает и сервисная запись входит.
|
||||
func (h *setupHandlers) adConnect(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
a := h.rc.Snapshot().Auth
|
||||
a.ADServer = strings.TrimSpace(r.FormValue("ad_server"))
|
||||
a.ADDomain = strings.TrimSpace(strings.TrimPrefix(r.FormValue("ad_domain"), "@"))
|
||||
a.ADBindUser = strings.TrimSpace(r.FormValue("ad_bind_user"))
|
||||
if pw := r.FormValue("ad_bind_password"); pw != "" {
|
||||
a.ADBindPassword = pw
|
||||
}
|
||||
if a.ADServer != "" && !strings.Contains(a.ADServer, "://") {
|
||||
a.ADServer = "ldaps://" + a.ADServer // как в проводнике вводят имя — по умолчанию защищённое подключение
|
||||
}
|
||||
var miss []string
|
||||
for _, f := range []struct{ v, n string }{{a.ADServer, "контроллер домена"}, {a.ADDomain, "домен"}, {a.ADBindUser, "сервисная учётная запись"}, {a.ADBindPassword, "её пароль"}} {
|
||||
if f.v == "" {
|
||||
miss = append(miss, f.n)
|
||||
}
|
||||
}
|
||||
if len(miss) > 0 {
|
||||
h.usersFlash(w, r, "Домен: не заполнено: "+strings.Join(miss, ", "))
|
||||
return
|
||||
}
|
||||
probe, err := adProbe(a)
|
||||
if err != nil {
|
||||
h.usersFlash(w, r, "Домен: контроллер не отвечает — "+err.Error()+". Настройки не сохранены.")
|
||||
return
|
||||
}
|
||||
conn, err := adDial(a)
|
||||
if err == nil {
|
||||
err = adBindService(conn, a)
|
||||
conn.Close()
|
||||
}
|
||||
if err != nil {
|
||||
h.usersFlash(w, r, "Домен: "+err.Error()+". Настройки не сохранены.")
|
||||
return
|
||||
}
|
||||
a.ADEnabled = true
|
||||
if err := h.rc.UpdateAuth(a); err != nil {
|
||||
h.usersFlash(w, r, "Домен: настройки не сохранены — "+err.Error())
|
||||
return
|
||||
}
|
||||
h.usersFlash(w, r, "Домен подключён ("+probe+"): можно добавлять сотрудников из домена — они входят своим доменным логином и паролем.")
|
||||
}
|
||||
|
||||
// adUserView — сотрудник домена в результатах поиска.
|
||||
type adUserView struct {
|
||||
Login string `json:"login"` // sAMAccountName
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Have bool `json:"have,omitempty"` // уже есть в bj
|
||||
}
|
||||
|
||||
// adSearch — GET ?q=: сотрудники домена по фамилии, логину или почте (до 20, только включённые учётные записи).
|
||||
func (h *setupHandlers) adSearch(w http.ResponseWriter, r *http.Request) {
|
||||
a := h.rc.Snapshot().Auth
|
||||
q := strings.TrimSpace(r.URL.Query().Get("q"))
|
||||
if !a.ADEnabled || a.ADBindUser == "" {
|
||||
jsonOut(w, map[string]any{"ok": false, "error": "домен не подключён"})
|
||||
return
|
||||
}
|
||||
if len([]rune(q)) < 2 {
|
||||
jsonOut(w, map[string]any{"ok": true, "users": []adUserView{}})
|
||||
return
|
||||
}
|
||||
users, err := adSearchUsers(a, q, 20)
|
||||
if err != nil {
|
||||
jsonOut(w, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
jsonOut(w, map[string]any{"ok": true, "users": users})
|
||||
}
|
||||
|
||||
func adSearchUsers(a AuthSettings, q string, limit int) ([]adUserView, error) {
|
||||
conn, err := adDial(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
conn.SetTimeout(10 * time.Second)
|
||||
if err := adBindService(conn, a); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
base := a.ADBaseDN
|
||||
if base == "" {
|
||||
base = adDefaultBase(conn)
|
||||
}
|
||||
e := ldap.EscapeFilter(q)
|
||||
filter := fmt.Sprintf("(&(objectCategory=person)(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(|(sAMAccountName=*%s*)(displayName=*%s*)(sn=*%s*)(mail=*%s*)))", e, e, e, e)
|
||||
sr, err := conn.Search(ldap.NewSearchRequest(base, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, limit, 10, false,
|
||||
filter, []string{"sAMAccountName", "displayName", "mail"}, nil))
|
||||
if err != nil && (sr == nil || len(sr.Entries) == 0) { // превышение лимита — не ошибка, берём найденное
|
||||
return nil, fmt.Errorf("поиск в домене: %v", err)
|
||||
}
|
||||
var out []adUserView
|
||||
for _, en := range sr.Entries {
|
||||
u := adUserView{Login: en.GetAttributeValue("sAMAccountName"), Name: en.GetAttributeValue("displayName"), Email: en.GetAttributeValue("mail")}
|
||||
if u.Login == "" {
|
||||
continue
|
||||
}
|
||||
u.Have = globalUsers.Get(u.Login) != nil
|
||||
out = append(out, u)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
@@ -179,7 +180,10 @@ type User struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
LastLogin time.Time `json:"last_login,omitempty"`
|
||||
ADGroups []string `json:"ad_groups,omitempty"`
|
||||
Note string `json:"note,omitempty"`
|
||||
// BJRole — роль доменного пользователя, назначенная в bj (мастер, «Добавить из домена»): действует, когда
|
||||
// ни одна группа домена не сопоставлена с ролью; сопоставленные группы — главнее.
|
||||
BJRole string `json:"bj_role,omitempty"`
|
||||
Note string `json:"note,omitempty"`
|
||||
}
|
||||
|
||||
func (u *User) Can(perm string) bool {
|
||||
@@ -785,9 +789,6 @@ func authenticate(rc *RuntimeConfig, login, password string) (*User, error) {
|
||||
return nil, fmt.Errorf("неверный логин или пароль")
|
||||
}
|
||||
res, err := adAuthenticate(a, login, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key := login
|
||||
if i := strings.Index(key, "@"); i > 0 {
|
||||
key = key[:i]
|
||||
@@ -795,6 +796,15 @@ func authenticate(rc *RuntimeConfig, login, password string) (*User, error) {
|
||||
if i := strings.Index(key, `\`); i > 0 {
|
||||
key = key[i+1:]
|
||||
}
|
||||
var noGroup *adNoGroupError
|
||||
if err != nil && res != nil && errors.As(err, &noGroup) {
|
||||
if pre := globalUsers.Get(key); pre != nil && pre.Source == "ad" && pre.BJRole != "" && roleAvailable(pre.BJRole) {
|
||||
res.Role, res.MatchedBy, err = pre.BJRole, "роль, назначенная в bj", nil
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u = globalUsers.Get(key); u == nil {
|
||||
u = &User{Login: key, Source: "ad", Active: true, CreatedAt: time.Now(), Role: res.Role}
|
||||
} else if !u.Active {
|
||||
@@ -1280,6 +1290,9 @@ func (h *setupHandlers) saveUser(w http.ResponseWriter, r *http.Request) {
|
||||
if src := r.FormValue("source"); src == "ad" || src == "local" {
|
||||
u.Source = src
|
||||
}
|
||||
if u.Source == "ad" {
|
||||
u.BJRole = role // роль из bj — пока ни одна группа домена не сопоставлена
|
||||
}
|
||||
pw := r.FormValue("password")
|
||||
if u.Source == "local" {
|
||||
if pw != "" {
|
||||
@@ -1445,6 +1458,7 @@ func (h *setupHandlers) testAD(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func registerUsersSetup(mux *http.ServeMux, h *setupHandlers) {
|
||||
mux.HandleFunc("/admin/setup/users", h.saveUser)
|
||||
h.registerADWizard(mux)
|
||||
mux.HandleFunc("/admin/setup/users/delete", h.deleteUser)
|
||||
mux.HandleFunc("/admin/setup/users/session-drop", h.dropSession)
|
||||
mux.HandleFunc("/admin/setup/auth", h.saveAuth)
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -258,6 +259,17 @@ func registerIgateAdmin(mux *http.ServeMux, rc *RuntimeConfig) {
|
||||
go igateContourRun(rc, contour, crypto, dep)
|
||||
jsonOut(w, map[string]any{"ok": true, "started": true})
|
||||
})
|
||||
// папки обмена шлюза по сети (SMB) — для программ организации; GET — состояние, POST — открыть/закрыть
|
||||
mux.HandleFunc("/admin/setup/igate/share", func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute) // первый раз — установка Samba
|
||||
defer cancel()
|
||||
p := map[string]string{"enabled": "status"}
|
||||
if r.Method == http.MethodPost {
|
||||
p = map[string]string{"enabled": strconv.FormatBool(r.FormValue("enabled") != ""), "user": strings.TrimSpace(r.FormValue("user")), "password": r.FormValue("password")}
|
||||
}
|
||||
extra, _ := json.Marshal(p)
|
||||
jsonOut(w, opResult(runKeymediaOp(ctx, "igate-share", map[string]string{"extra": string(extra)})))
|
||||
})
|
||||
// ход записи канала: страница показывает его и после перезагрузки
|
||||
mux.HandleFunc("/admin/setup/igate/contour-status", func(w http.ResponseWriter, r *http.Request) {
|
||||
jsonOut(w, igateContourJob.view())
|
||||
|
||||
@@ -206,3 +206,16 @@ func TestDeferForInstaller(t *testing.T) {
|
||||
t.Fatalf("без нового установщика: оставлено %d, отложено %v", len(kept), deferred)
|
||||
}
|
||||
}
|
||||
|
||||
// Роль, назначенная в bj доменному сотруднику (мастер, «Добавить из домена»), действует, когда группы домена
|
||||
// с ролями не сопоставлены; без неё — доступ снимается, как раньше.
|
||||
func TestADRecheckBJRole(t *testing.T) {
|
||||
u := &User{Login: "ivanov", Source: "ad", Role: "operator", BJRole: "operator", Active: true}
|
||||
if changed, drop, _ := adApplyRecheck(u, &adResult{}, nil, &adNoGroupError{sam: "ivanov"}); drop || u.Role != "operator" || changed {
|
||||
t.Fatalf("роль из bj должна остаться: changed=%v drop=%v role=%q", changed, drop, u.Role)
|
||||
}
|
||||
v := &User{Login: "petrov", Source: "ad", Role: "operator", Active: true}
|
||||
if _, drop, _ := adApplyRecheck(v, &adResult{}, nil, &adNoGroupError{sam: "petrov"}); !drop || v.Role != "" {
|
||||
t.Fatalf("без роли из bj доступ снимается: drop=%v role=%q", drop, v.Role)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,10 @@ const (
|
||||
var igateLogFile = "/var/lib/igate/.local/share/NSD Integration Gateway/logs-svc/log.txt"
|
||||
|
||||
// PID_363248 2026-09-25 15:42:59.1873 T16 DEBUG [WSL] WSL TEST3 GOST: GetPackageListFull (GET) -> #404 (HTTP Error 404: NotFound)
|
||||
var nsdPollRe = regexp.MustCompile(`^PID_\d+ (\d{4}-\d\d-\d\d \d\d:\d\d:\d\d)\.\d+ .*WSL ([^:]+): GetPackageListFull \(GET\) -> (OK|#\d+[^\r\n]*)`)
|
||||
// PID_36882 2026-10-07 00:02:13.4942 T16 … — ИШ выравнивает номер процесса пробелами: с коротким номером (после
|
||||
// перезапуска сервера) пробелов больше одного, и прежний шаблон с одним пробелом не находил ни одного опроса
|
||||
// («НРД: нет данных опроса», серый индикатор при работающем обмене).
|
||||
var nsdPollRe = regexp.MustCompile(`^PID_\d+\s+(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d)\.\d+ .*WSL ([^:]+): GetPackageListFull \(GET\) -> (OK|#\d+[^\r\n]*)`)
|
||||
|
||||
type nsdPoll struct {
|
||||
At time.Time
|
||||
|
||||
@@ -18,3 +18,13 @@ PID_1 2026-09-25 15:40:00.1 T16 DEBUG [WSL] WSL TEST3 GOST: GetPa
|
||||
t.Fatalf("восстановление: %+v %+v", last, ff)
|
||||
}
|
||||
}
|
||||
|
||||
// ИШ выравнивает номер процесса пробелами: короткий номер — несколько пробелов (07.10.2026, серый индикатор).
|
||||
func TestNsdLastPollsPaddedPID(t *testing.T) {
|
||||
log := "PID_36882 2026-10-07 00:02:13.4942 T16 DEBUG [WSL] WSL TEST3 GOST: GetPackageListFull (GET) -> OK (HTTP 200)\n" +
|
||||
"PID_1050 2026-10-07 00:02:23.4942 T16 DEBUG [WSL] WSL TEST3 GOST: GetPackageListFull (GET) -> OK (HTTP 200)\n"
|
||||
last, ff := nsdLastPolls([]byte(log))
|
||||
if last == nil || !last.OK || ff != nil || last.At.Second() != 23 {
|
||||
t.Fatalf("опрос с выровненным номером процесса не распознан: %+v", last)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,6 +429,35 @@ func (h *setupHandlers) checkCrypto(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
s := h.rc.Snapshot()
|
||||
// Подписывает документы для НРД шлюз (ИШ) профилем Валидаты, который создал шаг «Ключ подписи»: проверяем
|
||||
// именно его — версия СКЗИ, датчик случайных чисел, подпись проверочного документа этим профилем.
|
||||
if !devMode(h.rc) || igateInstalled() {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
|
||||
defer cancel()
|
||||
ver, _, _ := validataVersion(ctx)
|
||||
if ver == "" {
|
||||
setupFlash(w, r, "СКЗИ «Валидата» на сервере не найдена — её ставит установщик bj")
|
||||
return
|
||||
}
|
||||
res, err := runKeymediaOp(ctx, "keycheck", map[string]string{"channel": contourChannel(contourOf(s.NSD.Profile))})
|
||||
if err != nil {
|
||||
setupFlash(w, r, "СКЗИ «Валидата» "+ver+": ключ не проверен — "+err.Error())
|
||||
return
|
||||
}
|
||||
prof, _ := res["profile"].(string)
|
||||
kc, _ := res["keycheck"].(string)
|
||||
rng := "датчик случайных чисел готов"
|
||||
if res["rng"] == "false" {
|
||||
rng = "датчик случайных чисел не инициализирован (шаг «Ключ подписи»)"
|
||||
}
|
||||
if kc == "ok" {
|
||||
setupFlash(w, r, "✓ СКЗИ «Валидата» "+ver+", "+rng+"; ключ организации (профиль ИШ «"+prof+"») подписывает проверочный документ")
|
||||
return
|
||||
}
|
||||
diag, _ := res["keydiag"].(string)
|
||||
setupFlash(w, r, "✗ СКЗИ «Валидата» "+ver+", "+rng+"; ключ профиля «"+prof+"» не подписывает: "+strings.TrimPrefix(kc, "fail: ")+prefixed(". ", diag))
|
||||
return
|
||||
}
|
||||
cli := cryptocli.New(cryptocli.Config{
|
||||
Provider: cryptocli.Provider(s.Crypto.Provider),
|
||||
SocketPath: s.Crypto.SocketPath,
|
||||
|
||||
@@ -4,19 +4,20 @@
|
||||
<div class="settings">
|
||||
{{/* ===== Боковая навигация разделов с индикаторами ===== */}}
|
||||
<nav class="settings-nav">
|
||||
<a href="/admin/wizard" class="snav-wiz" style="display:block;margin:0 0 10px;padding:8px 12px;border:1px dashed var(--border-strong,#c9cdd8);border-radius:10px;font-size:13.5px;text-decoration:none;color:var(--accent)">↺ Мастер настройки — пройти по шагам</a>
|
||||
<button data-sec="overview" class="active"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="3"/></svg></span>Обзор</button>
|
||||
<button data-sec="db"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5.5" rx="7" ry="2.6"/><path d="M5 5.5v13c0 1.4 3.1 2.6 7 2.6s7-1.2 7-2.6v-13"/><path d="M5 12c0 1.4 3.1 2.6 7 2.6s7-1.2 7-2.6"/></svg></span>База данных<span class="ind {{if .Settings.Postgres.DSN}}ok{{else}}err{{end}}"></span></button>
|
||||
{{if mod "quik-mnp"}}<button data-sec="fancy"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M4 8h12M12 4l4 4-4 4"/><path d="M20 16H8M12 20l-4-4 4-4"/></svg></span>Обмен Fancy<span class="ind {{if .Settings.Fancy.DepoTablesCheckOK}}ok{{else if .Settings.Fancy.Server}}warn{{end}}"></span></button>
|
||||
<button data-sec="quik"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20V4M4 20h16"/><rect x="7" y="12" width="2.6" height="5"/><rect x="12" y="8" width="2.6" height="9"/><rect x="17" y="14" width="2.6" height="3"/></svg></span>QUIK МНП<span class="ind {{if .Settings.Quik.Verified}}ok{{else if .Settings.Quik.Server}}warn{{end}}"></span></button>{{end}}
|
||||
{{if or (mod "quik-export") (mod "npr")}}<button data-sec="quikexport"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M4 17l5-5 4 4 7-8"/><path d="M14 8h6v6"/></svg></span>Экспорт QUIK<span class="ind {{.QuikExport.Ind}}"></span></button>{{end}}
|
||||
<button data-sec="users"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8" r="3.5"/><path d="M3 20c0-3.3 2.7-6 6-6s6 2.7 6 6"/><circle cx="17" cy="9" r="2.5"/><path d="M15.5 14.5c2.8.3 5.5 2.5 5.5 5.5"/></svg></span>Пользователи<span class="ind {{if .Users.Open}}warn{{else}}ok{{end}}"></span></button>
|
||||
<button data-sec="crypto"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg></span>Криптография<span class="ind {{if .Settings.Crypto.Profile}}ok{{else}}warn{{end}}"></span></button>
|
||||
<button data-sec="crypto"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg></span>Криптография<span class="ind {{if cryptoReady}}ok{{else}}warn{{end}}"></span></button>
|
||||
<button data-sec="nsd"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21h18M4 21V10M20 21V10M3 10l9-6 9 6M8 21v-7M12 21v-7M16 21v-7"/></svg></span>НРД<span class="ind {{if .Settings.NSD.IGWBaseURL}}ok{{else}}warn{{end}}"></span></button>
|
||||
{{if mod "web-cabinet"}}<button data-sec="cabinet"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="9" r="3.2"/><path d="M6 19c.6-3 3-4.6 6-4.6s5.4 1.6 6 4.6"/><rect x="3" y="4" width="18" height="16" rx="2"/></svg></span>Кабинет клиента<span class="ind {{if .Settings.LK.APIKey}}ok{{else}}warn{{end}}"></span></button>{{end}}
|
||||
{{if mod "esia-finance"}}<button data-sec="lk"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="9" cy="11" r="2"/><path d="M6 16c.4-1.4 1.6-2.2 3-2.2s2.6.8 3 2.2"/><path d="M15 10h4M15 13h3"/></svg></span>ЕСИА Финанс<span class="ind {{if .Settings.LK.PublicURL}}ok{{else}}warn{{end}}"></span></button>{{end}}
|
||||
<button data-sec="tests"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3h6M10 3v6l-5 8.5A2 2 0 0 0 6.7 21h10.6a2 2 0 0 0 1.7-3.5L14 9V3"/><path d="M8 15h8"/></svg></span>Диагностика</button>
|
||||
<button data-sec="alerts"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></svg></span>Оповещения<span class="ind {{if or .Settings.Alerts.SMTP.Enabled .Settings.Alerts.Zabbix.Enabled}}ok{{end}}"></span></button>
|
||||
<button data-sec="update"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20V8M6 12l6-6 6 6"/><path d="M5 4h14"/></svg></span>Обновления{{if .Settings.Update.Available}}<span class="ind warn"></span>{{end}}</button>
|
||||
<button data-sec="update"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20V8M6 12l6-6 6 6"/><path d="M5 4h14"/></svg></span>Обновления{{if .UpdatesAvailable}}<span class="ind warn"></span>{{end}}</button>
|
||||
<button data-sec="backup"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="6" rx="7" ry="2.5"/><path d="M5 6v6c0 1.4 3.1 2.5 7 2.5s7-1.1 7-2.5V6"/><path d="M12 17v4M9 19l3 2 3-2"/></svg></span>Резервное копирование{{$bs := backupStatus}}{{if and .Settings.Backup.Enabled $bs}}{{if not $bs.OK}}<span class="ind err"></span>{{end}}{{else if not .Settings.Backup.Enabled}}<span class="ind warn"></span>{{end}}</button>
|
||||
<button data-sec="server"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="7" rx="1.5"/><rect x="3" y="13" width="18" height="7" rx="1.5"/><path d="M7 7.5h.01M7 16.5h.01"/></svg></span>Сервер: HTTPS, время, сеть{{if or (not .Settings.HTTPS.RedirectOn) (not timeSynced)}}<span class="ind warn"></span>{{end}}</button>
|
||||
<button data-sec="license"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="12" r="4"/><path d="M12 12h9M18 12v3M15 12v2"/></svg></span>Лицензия{{if .License.Present}}<span class="ind {{if .License.Valid}}ok{{else}}err{{end}}"></span>{{end}}</button>
|
||||
@@ -238,13 +239,15 @@
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2><span class="dot {{if .Settings.Crypto.Profile}}ok{{else}}warn{{end}}"></span>СКЗИ «Валидата Клиент L»</h2>
|
||||
<p class="muted">Активный профиль: <code>{{if .Settings.Crypto.Profile}}{{.Settings.Crypto.Profile}}{{else}}—{{end}}</code> · провайдер {{if and (not devOn) (or (eq .Settings.Crypto.Provider "stub") (eq .Settings.Crypto.Provider ""))}}<code>не настроен</code>{{else}}<code>{{.Settings.Crypto.Provider}}</code>{{end}}. {{if internalDocs}}Подробно — <a href="/admin/help/crypto">справка</a>{{end}}.</p>
|
||||
<h2><span class="dot {{if cryptoReady}}ok{{else}}warn{{end}}"></span>СКЗИ «Валидата Клиент L»</h2>
|
||||
<p style="margin:6px 0">{{if cryptoReady}}<span style="color:var(--ok)">✓ СКЗИ установлена, ключ организации подключён — документы для НРД подписывает шлюз НРД.</span>{{else}}<span style="color:var(--warn)">⚠ Ключ организации не подключён — мастер настройки, шаг «Ключ подписи».</span>{{end}}</p>
|
||||
{{if devOn}}<p class="muted">Активный профиль: <code>{{if .Settings.Crypto.Profile}}{{.Settings.Crypto.Profile}}{{else}}—{{end}}</code> · провайдер {{if and (not devOn) (or (eq .Settings.Crypto.Provider "stub") (eq .Settings.Crypto.Provider ""))}}<code>не настроен</code>{{else}}<code>{{.Settings.Crypto.Provider}}</code>{{end}}. {{if internalDocs}}Подробно — <a href="/admin/help/crypto">справка</a>{{end}}.</p>{{end}}
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:12px">
|
||||
<form method="post" action="/admin/setup/crypto/check" style="margin:0"><button type="submit" class="btn btn-ok">✓ Проверить СКЗИ</button></form>
|
||||
<form method="post" action="/admin/setup/crypto/test-sign" style="margin:0"><button type="submit" class="btn">✎ Подписать проверочный документ</button></form>
|
||||
<form method="post" action="/admin/setup/crypto/check" style="margin:0"><button type="submit" class="btn btn-ok">✓ Проверить СКЗИ и ключ</button></form>
|
||||
{{if devOn}}<form method="post" action="/admin/setup/crypto/test-sign" style="margin:0"><button type="submit" class="btn">✎ Подписать проверочный документ</button></form>
|
||||
<form method="post" action="/admin/setup/restart-crypto" style="margin:0" onsubmit="return confirm('Перезапустить проверку ключа подписи? Займёт около 5 секунд.');"><button type="submit" class="btn btn-warn">↻ Перезапустить проверку ключа</button></form>
|
||||
<form method="post" action="/admin/setup/restart-server" style="margin:0" onsubmit="return confirm('Перезапустить bj-server? Через 5-10 сек страница вернётся.');"><button type="submit" class="btn btn-secondary">↻ bj-server</button></form>
|
||||
{{end}}
|
||||
</div>
|
||||
<details style="margin-top:14px">
|
||||
<summary style="cursor:pointer;color:var(--accent);font-size:13px">Параметры провайдера (для совместимости)</summary>
|
||||
@@ -288,7 +291,8 @@
|
||||
<p class="muted" style="font-size:12px;margin-top:8px">Свои сертификаты можно положить в <code>/var/lib/bj/.bj/nsd-certs/*.cer</code> — они добавляются по тем же правилам и никогда не удаляются автоматически.</p>
|
||||
</div>
|
||||
|
||||
{{/* Сертификаты УЦ */}}
|
||||
{{/* Сертификаты УЦ (старый способ: теперь сертификаты НРД приходят комплектом с bj) — только для разработки */}}
|
||||
{{if devOn}}
|
||||
<div class="card" id="sec-cacerts">
|
||||
<h2><span class="dot {{if .Settings.CACerts.URLs}}ok{{else}}warn{{end}}"></span>Сертификаты УЦ (авто-загрузка)</h2>
|
||||
<p class="muted">URL .cer-файлов УЦ НРД (<a href="https://www.nsd.ru/workflow/system/cryptography/" target="_blank" rel="noopener">nsd.ru</a>). Скачиваются, парсятся и сохраняются в <code>/var/lib/bj/ca-certs/</code>.</p>
|
||||
@@ -308,6 +312,7 @@
|
||||
</table>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
{{/* ============ НРД ============ */}}
|
||||
|
||||
@@ -520,33 +520,7 @@
|
||||
{{if .Settings.Quik.Server}}{{template "mnpCheck" .}}{{end}}
|
||||
{{if .Settings.Quik.M2MMissing}}<p style="color:var(--err);margin:8px 0">✗ Подключение есть, но под этим логином не видно объектов M2M: {{.Settings.Quik.M2MMissing}}. Проверьте имя базы МНП и права логина (роль InstrEu) и нажмите «Проверить» ещё раз.</p>{{end}}
|
||||
{{if .Settings.Quik.Verified}}
|
||||
<div class="card" id="quik-mode">
|
||||
<h2>Как bj работает с QUIK</h2>
|
||||
<form method="post" action="/admin/setup/quik/mode" id="quik-mode-form" class="dq">
|
||||
<input type="hidden" name="return_to" value="/admin/wizard?step=5">
|
||||
<div class="dq-tiles">
|
||||
<label class="dq-tile"><input type="radio" name="mode" value="check" {{if not .Settings.Quik.WritesEnabled}}checked{{end}}><span><b>Только чтение</b><small>bj подключён к QUIK и читает данные, но ничего в нём не меняет: не блокирует и не разблокирует бумаги, не меняет лимиты. Для настройки и проверки — когда будете готовы, включите «Чтение и запись».</small></span></label>
|
||||
<label class="dq-tile"><input type="radio" name="mode" value="auto" {{if .Settings.Quik.WritesEnabled}}checked{{end}}><span><b>Чтение и запись</b><small>Боевая работа: bj сам блокирует бумаги в QUIK при отправке перевода, списывает или снимает блокировку по ответу НРД, зачисляет входящие.</small></span></label>
|
||||
</div>
|
||||
<div class="dq-jobname" id="quik-acc">
|
||||
<label>Торговый счёт для поручений<input type="text" name="account" value="{{.Settings.Quik.Account}}" placeholder="торговый счёт фирмы в QUIK" spellcheck="false" class="dq-mono"></label>
|
||||
</div>
|
||||
<p class="dq-note" id="quik-mode-st" aria-live="polite">Выбор сохраняется сразу. Тонкая настройка — инструменты МНП, коды клиентов, поручения из терминала — в «Настройка → QUIK».</p>
|
||||
<noscript><button type="submit" class="btn">Сохранить</button></noscript>
|
||||
</form>
|
||||
<p class="dq-note">{{quikRecipientsInfo}} После первого заполнения справочника депозитариев-получателей попросите администратора QUIK перезапустить модуль неторговых поручений — QUIK читает справочник только при запуске.</p>
|
||||
</div>
|
||||
<script>(function(){
|
||||
var f=document.getElementById('quik-mode-form'), st=document.getElementById('quik-mode-st'), acc=document.getElementById('quik-acc');
|
||||
function sync(){ acc.hidden=f.mode.value!=='auto'; }
|
||||
f.addEventListener('change', function(){ sync();
|
||||
fetch(f.action,{method:'POST',body:new URLSearchParams(new FormData(f)),headers:{'Content-Type':'application/x-www-form-urlencoded','X-BJ-Ajax':'1'},cache:'no-store'})
|
||||
.then(function(r){ return r.text().then(function(t){ st.textContent=(r.ok?'✓ Сохранено: ':'✗ ')+t; }); })
|
||||
.catch(function(){ st.textContent='✗ Не сохранилось — обновите страницу и выберите ещё раз.'; });
|
||||
});
|
||||
f.addEventListener('submit', function(e){ if(window.fetch) e.preventDefault(); });
|
||||
sync();
|
||||
})();</script>
|
||||
{{template "quikModeCard" (withRet . "/admin/wizard?step=5")}}
|
||||
{{else}}<p class="muted" style="margin-top:12px">Когда подключение проверено, здесь появится выбор, как bj работает с QUIK.</p>{{end}}{{end}}
|
||||
{{template "wizNav" .}}
|
||||
</div>
|
||||
@@ -774,6 +748,47 @@
|
||||
<p class="muted" style="font-size:12.5px;margin:8px 0 0">Пароль передайте сотруднику лично; сменить его можно в разделе Настройка → Пользователи.</p>
|
||||
<div style="margin-top:12px"><button type="submit" class="btn btn-ok">Создать учётную запись</button></div>
|
||||
</form>
|
||||
{{$A := .Settings.Auth}}
|
||||
<details class="acc-ad" style="margin-top:20px;padding-top:14px;border-top:1px solid var(--border)">
|
||||
<summary style="cursor:pointer;color:var(--accent);font-size:14px">Добавить сотрудника из домена (Active Directory) — необязательно</summary>
|
||||
{{if and $A.ADEnabled $A.ADBindUser}}
|
||||
<p class="muted" style="font-size:13.5px;margin:10px 0 0">Домен <b>{{$A.ADDomain}}</b> подключён. Найдите сотрудника — он будет входить в bj своим доменным логином и паролем; пароль проверяет домен.</p>
|
||||
<input type="text" id="acc-ad-q" placeholder="Фамилия, логин или почта" autocomplete="off" style="width:100%;max-width:420px;margin-top:10px">
|
||||
<div id="acc-ad-st" class="muted" style="font-size:13px;margin-top:6px"></div>
|
||||
<ul id="acc-ad-res" style="list-style:none;margin:8px 0 0;padding:0;display:grid;gap:6px;max-width:640px"></ul>
|
||||
<form method="post" action="/admin/setup/users" id="acc-ad-form" hidden style="margin-top:12px;display:flex;gap:10px;align-items:end;flex-wrap:wrap">
|
||||
<input type="hidden" name="return_to" value="/admin/wizard?step=13"><input type="hidden" name="create" value="1"><input type="hidden" name="active" value="1"><input type="hidden" name="source" value="ad">
|
||||
<input type="hidden" name="login"><input type="hidden" name="name"><input type="hidden" name="email">
|
||||
<span>Добавить <b id="acc-ad-who"></b> с ролью</span>
|
||||
<select name="role">{{range .Users.Roles}}<option value="{{.Key}}" {{if eq .Key "operator"}}selected{{end}}>{{.Title}}</option>{{end}}</select>
|
||||
<button type="submit" class="btn btn-ok">Добавить из домена</button>
|
||||
</form>
|
||||
<script>(function(){
|
||||
var q=document.getElementById('acc-ad-q'), st=document.getElementById('acc-ad-st'), ul=document.getElementById('acc-ad-res'), f=document.getElementById('acc-ad-form'), who=document.getElementById('acc-ad-who'), t=0;
|
||||
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return{'&':'&','<':'<','>':'>','"':'"'}[c]})}
|
||||
q.addEventListener('input',function(){ clearTimeout(t); t=setTimeout(function(){
|
||||
var v=q.value.trim(); if(v.length<2){ ul.innerHTML=''; st.textContent=''; return; } st.textContent='Ищу в домене…';
|
||||
fetch('/admin/setup/users/ad-search?q='+encodeURIComponent(v),{cache:'no-store',headers:{'Accept':'application/json'}}).then(function(r){return r.json()}).then(function(d){
|
||||
if(!d.ok){ st.textContent='Не получилось: '+d.error; ul.innerHTML=''; return; }
|
||||
st.textContent=d.users.length?'Найдено: '+d.users.length:'Никого не нашлось';
|
||||
ul.innerHTML=d.users.map(function(u,i){ return '<li><button type="button" class="btn btn-secondary" style="width:100%;text-align:left" data-i="'+i+'"'+(u.have?' disabled':'')+'><b>'+esc(u.name||u.login)+'</b> <span class="muted">'+esc(u.login)+(u.email?' · '+esc(u.email):'')+(u.have?' · уже есть в bj':'')+'</span></button></li>'; }).join('');
|
||||
ul.querySelectorAll('button').forEach(function(b){ b.onclick=function(){ var u=d.users[+b.dataset.i]; f.login.value=u.login; f.name.value=u.name||''; f.email.value=u.email||''; who.textContent=(u.name||u.login)+' ('+u.login+')'; f.hidden=false; }; });
|
||||
}).catch(function(){ st.textContent='Домен не ответил'; });
|
||||
},350); });
|
||||
})();</script>
|
||||
{{end}}
|
||||
<details {{if not (and $A.ADEnabled $A.ADBindUser)}}open{{end}} style="margin-top:12px"><summary style="cursor:pointer;font-size:13px;color:var(--accent)">{{if and $A.ADEnabled $A.ADBindUser}}Другой домен или учётная запись{{else}}Подключить домен{{end}}</summary>
|
||||
<p class="muted" style="font-size:13px;margin:8px 0 0">Нужны адрес контроллера домена и учётная запись для поиска сотрудников — подойдёт обычная, без прав администратора домена. bj сразу проверит связь.</p>
|
||||
<form method="post" action="/admin/setup/users/ad-connect" class="acc-form">
|
||||
<input type="hidden" name="return_to" value="/admin/wizard?step=13">
|
||||
<label>Контроллер домена<input type="text" name="ad_server" data-need="контроллер домена" value="{{$A.ADServer}}" placeholder="dc1.corp.local или ldaps://dc1.corp.local:636" autocomplete="off"></label>
|
||||
<label>Домен<input type="text" name="ad_domain" data-need="домен" value="{{$A.ADDomain}}" placeholder="corp.local" autocomplete="off"></label>
|
||||
<label>Учётная запись для поиска<input type="text" name="ad_bind_user" data-need="учётная запись для поиска" value="{{$A.ADBindUser}}" placeholder="svc-bj" autocomplete="off"></label>
|
||||
<label>Пароль{{if $A.ADBindPassword}} <span class="muted">(задан; пусто — оставить)</span>{{end}}<input type="password" name="ad_bind_password" {{if not $A.ADBindPassword}}data-need="пароль учётной записи"{{end}} autocomplete="new-password"></label>
|
||||
<div><button type="submit" class="btn">Подключить и проверить связь</button></div>
|
||||
</form>
|
||||
</details>
|
||||
</details>
|
||||
<script>(function(){
|
||||
var g=document.getElementById('acc-gen'), f=document.getElementById('acc-pw'); if(!g||!f||!window.crypto) return;
|
||||
g.addEventListener('click',function(){ var a='abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789', b=new Uint32Array(12), p='';
|
||||
|
||||
@@ -377,6 +377,7 @@ details:has(> pre.copyable) { min-width:0; max-width:100%; }
|
||||
</span>
|
||||
<span class="ddmenu ddmenu-right">
|
||||
{{if can "settings"}}<a href="/admin/setup" class="{{if eq .Active "setup"}}active{{end}}">Настройка</a>
|
||||
<a href="/admin/wizard">Мастер настройки</a>
|
||||
<a href="/admin/status" class="{{if eq .Active "status"}}active{{end}}">Статус системы</a>{{end}}
|
||||
<a href="/admin/help" class="{{if eq .Active "help"}}active{{end}}">Справка</a>
|
||||
{{with curUser}}<span class="ddsep"></span><a href="/logout">Выйти</a>{{else}}{{if authOpen}}<span class="ddsep"></span><a href="/admin/setup#users">Создать пользователей</a>{{end}}{{end}}
|
||||
|
||||
@@ -58,6 +58,17 @@
|
||||
<button type="button" class="btn" id="ig-goprod" style="background:var(--err);border-color:var(--err)">Перейти на боевой</button>
|
||||
</div>
|
||||
</div>
|
||||
<details class="ig-share" id="ig-share" style="margin-top:14px">
|
||||
<summary style="cursor:pointer;color:var(--accent);font-size:13.5px">Доступ к папкам шлюза по сети (необязательно)</summary>
|
||||
<p class="muted" style="font-size:13px;margin:8px 0 0">Папки обмена шлюза (INBOX, OUTBOX, SENT, ERRORS) можно открыть по сети Windows — если другие программы организации кладут или забирают файлы через этот шлюз. bj для работы это не нужно. Доступ — по своему логину и паролю, файлы пишутся от имени шлюза.</p>
|
||||
<div id="ig-share-st" class="muted" style="font-size:13px;margin-top:8px">Проверяю…</div>
|
||||
<form id="ig-share-f" style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;max-width:560px;margin-top:10px">
|
||||
<label style="grid-column:1/-1;display:flex;gap:8px;align-items:center;font-weight:400;margin:0"><input type="checkbox" name="enabled" style="width:auto"> Открыть папки шлюза по сети</label>
|
||||
<label class="f">Логин<input name="user" value="igate-share" autocomplete="off" spellcheck="false"></label>
|
||||
<label class="f">Пароль (от 8 символов)<input name="password" type="text" autocomplete="off" spellcheck="false" placeholder="пусто — прежний"></label>
|
||||
<div style="grid-column:1/-1;display:flex;gap:8px;align-items:center;flex-wrap:wrap"><button type="submit" class="btn sm">Сохранить</button><span id="ig-share-out" style="font-size:13px"></span></div>
|
||||
</form>
|
||||
</details>
|
||||
<div class="ig-job" id="ig-job" hidden aria-live="polite">
|
||||
<div class="ig-job-h"><span class="ig-spin"></span><b id="ig-job-t">Записываю канал в шлюз НРД</b><span class="muted" id="ig-job-time"></span></div>
|
||||
<ol class="dq-steps" id="ig-job-steps"></ol>
|
||||
@@ -128,6 +139,22 @@
|
||||
}).catch(function(){});
|
||||
return {watch:watch};
|
||||
})();
|
||||
// доступ к папкам шлюза по сети (Samba): состояние и сохранение
|
||||
(function(){
|
||||
var f=document.getElementById('ig-share-f'), stl=document.getElementById('ig-share-st'), o=document.getElementById('ig-share-out');
|
||||
if(!f) return;
|
||||
function show(d){
|
||||
if(!d.ok){ stl.innerHTML=bad(d.error||'состояние не получено'); return; }
|
||||
if(d.manual){ stl.innerHTML=ok('папки открыты вручную (smb.conf) для '+(d.user||'—')+' — bj их не меняет'); f.hidden=true; return; }
|
||||
stl.innerHTML=d.on?ok('открыты для '+esc(d.user)+': \\\\'+location.hostname+'\\igate_exchange'):'<span class="muted">закрыты</span>';
|
||||
f.enabled.checked=!!d.on; if(d.user) f.user.value=d.user;
|
||||
}
|
||||
fetch('/admin/setup/igate/share',{cache:'no-store'}).then(function(r){return r.json()}).then(show).catch(function(){ stl.textContent='Состояние не получено.'; });
|
||||
f.addEventListener('submit',function(e){ e.preventDefault(); o.textContent='Сохраняю… (первый раз — установка Samba, до пары минут)';
|
||||
var p=new URLSearchParams(); if(f.enabled.checked) p.set('enabled','1'); p.set('user',f.user.value.trim()); p.set('password',f.password.value);
|
||||
post('/admin/setup/igate/share',p).then(function(d){ o.innerHTML=d.ok?ok(d.output):bad(d.error); f.password.value='';
|
||||
return fetch('/admin/setup/igate/share',{cache:'no-store'}).then(function(r){return r.json()}).then(show); }).catch(function(){ o.innerHTML=bad('не сохранено'); }); });
|
||||
})();
|
||||
var st=null;
|
||||
function load(){
|
||||
fetch('/admin/setup/igate/status',{cache:'no-store'}).then(function(r){return r.json()}).then(function(d){
|
||||
|
||||
@@ -39,7 +39,9 @@
|
||||
</div>
|
||||
|
||||
{{if not .InWizard}}{{/* в мастере вместо этой карточки — выбор «Как bj работает с QUIK» (шаг QUIK) */}}
|
||||
<div class="card" id="sec-quik-cycle">
|
||||
{{if .Settings.Quik.Verified}}{{template "quikModeCard" (withRet . "")}}{{end}}
|
||||
<details class="card" id="sec-quik-cycle-more" style="padding-top:12px"><summary style="cursor:pointer;color:var(--accent);font-size:13.5px">Тонкая настройка автоматического цикла (обычно не нужна)</summary>
|
||||
<div id="sec-quik-cycle">
|
||||
<h2><span class="dot {{if .Settings.Quik.Emulate}}warn{{else if not .Settings.Quik.WritesEnabled}}warn{{else if or .Settings.Quik.AutoOut .Settings.Quik.AutoIn}}ok{{else}}warn{{end}}"></span>Автоматический цикл M2M в МНП{{if .Settings.Quik.Emulate}} <span class="hero-status warn">{{if devOn}}имитация{{else}}остановлен{{end}}</span>{{end}}</h2>
|
||||
<p class="muted">Что делает bj в Модуле неторговых поручений по ходу перевода: <strong>исходящий</strong> — при отправке в НРД регистрирует «Вывод ЦБ (M2M)» (QUIK блокирует бумаги), по подтверждению подписывает (списание), по отказу снимает с причиной; <strong>входящий</strong> — после приёма нашего поручения НРД регистрирует «Ввод ЦБ (M2M)» (зачисление сразу). Каждый шаг виден в карточке перевода и на странице <a href="/admin/quik">QUIK МНП</a>.</p>
|
||||
<form method="post" action="/admin/setup/quik/cycle" style="margin-top:12px;display:grid;gap:10px;max-width:640px">
|
||||
@@ -72,6 +74,7 @@
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap"><button type="submit" class="btn">Сохранить</button></div>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
{{end}}
|
||||
|
||||
{{if .Settings.Quik.LastReport}}
|
||||
@@ -123,3 +126,35 @@
|
||||
{{if internalDocs}}<a href="/admin/help/quik">Подробная внутренняя справка →</a>{{end}}
|
||||
</details>
|
||||
{{end}}
|
||||
|
||||
{{/* Как bj работает с QUIK: только чтение или чтение и запись (боевая работа) — одинаково в мастере (шаг 5) и в
|
||||
Настройка → QUIK МНП. Данные — withRet: .D — данные страницы (нужно .D.Settings), .Ret — куда вернуться без JS. */}}
|
||||
{{define "quikModeCard"}}
|
||||
<div class="card" id="quik-mode">
|
||||
<h2>Как bj работает с QUIK</h2>
|
||||
<form method="post" action="/admin/setup/quik/mode" id="quik-mode-form" class="dq">
|
||||
{{if .Ret}}<input type="hidden" name="return_to" value="{{.Ret}}">{{end}}
|
||||
<div class="dq-tiles">
|
||||
<label class="dq-tile"><input type="radio" name="mode" value="check" {{if not .D.Settings.Quik.WritesEnabled}}checked{{end}}><span><b>Только чтение</b><small>bj подключён к QUIK и читает данные, но ничего в нём не меняет: не блокирует и не разблокирует бумаги, не меняет лимиты. Для настройки и проверки — когда будете готовы, включите «Чтение и запись».</small></span></label>
|
||||
<label class="dq-tile"><input type="radio" name="mode" value="auto" {{if .D.Settings.Quik.WritesEnabled}}checked{{end}}><span><b>Чтение и запись</b><small>Боевая работа: bj сам блокирует бумаги в QUIK при отправке перевода, списывает или снимает блокировку по ответу НРД, зачисляет входящие.</small></span></label>
|
||||
</div>
|
||||
<div class="dq-jobname" id="quik-acc">
|
||||
<label>Торговый счёт для поручений<input type="text" name="account" value="{{.D.Settings.Quik.Account}}" placeholder="торговый счёт фирмы в QUIK" spellcheck="false" class="dq-mono"></label>
|
||||
</div>
|
||||
<p class="dq-note" id="quik-mode-st" aria-live="polite">Выбор сохраняется сразу. Тонкая настройка — инструменты МНП, коды клиентов, поручения из терминала — в «Настройка → QUIK».</p>
|
||||
<noscript><button type="submit" class="btn">Сохранить</button></noscript>
|
||||
</form>
|
||||
<p class="dq-note">{{quikRecipientsInfo}} После первого заполнения справочника депозитариев-получателей попросите администратора QUIK перезапустить модуль неторговых поручений — QUIK читает справочник только при запуске.</p>
|
||||
</div>
|
||||
<script>(function(){
|
||||
var f=document.getElementById('quik-mode-form'), st=document.getElementById('quik-mode-st'), acc=document.getElementById('quik-acc');
|
||||
function sync(){ acc.hidden=f.mode.value!=='auto'; }
|
||||
f.addEventListener('change', function(){ sync();
|
||||
fetch(f.action,{method:'POST',body:new URLSearchParams(new FormData(f)),headers:{'Content-Type':'application/x-www-form-urlencoded','X-BJ-Ajax':'1'},cache:'no-store'})
|
||||
.then(function(r){ return r.text().then(function(t){ st.textContent=(r.ok?'✓ Сохранено: ':'✗ ')+t; }); })
|
||||
.catch(function(){ st.textContent='✗ Не сохранилось — обновите страницу и выберите ещё раз.'; });
|
||||
});
|
||||
f.addEventListener('submit', function(e){ if(window.fetch) e.preventDefault(); });
|
||||
sync();
|
||||
})();</script>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user