Папки шлюза по сети — два режима: простой (один логин) или через домен (группы и сотрудники bj)
- «Через домен»: сервер bj подключается к домену AD (winbind, один раз — учётной записью администратора домена, bj её не сохраняет); папки igate_exchange(_prod) открыты группам с ролью в bj и доменным сотрудникам bj — своим входом в Windows; краткое имя домена bj берёт из AD (crossRef nETBIOSName). - Список доступа bj сверяет сам: после изменения групп, сотрудников и настроек входа (через несколько секунд) и раз в 15 минут. - «Простой»: как было — один логин и пароль Samba; переход с домена выключает вход через домен (учётная запись компьютера в домене остаётся). - bj-igate-admin share: режимы simple/domain, join (realm, workgroup, admin), проверка списка доступа на недопустимые символы, состояние — mode/joined/realm. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -543,5 +543,6 @@ func (h *setupHandlers) adGroupSave(w http.ResponseWriter, r *http.Request) {
|
||||
h.usersFlash(w, r, "Группа домена: "+err.Error())
|
||||
return
|
||||
}
|
||||
igateShareSyncSoon(h.rc) // папки шлюза «через домен» — тем же группам
|
||||
h.usersFlash(w, r, msg)
|
||||
}
|
||||
|
||||
@@ -1366,6 +1366,7 @@ func (h *setupHandlers) saveUser(w http.ResponseWriter, r *http.Request) {
|
||||
h.usersFlash(w, r, "Создан первый администратор "+u.Login+". Теперь вход обязателен — войдите под ним.")
|
||||
return
|
||||
}
|
||||
igateShareSyncSoon(h.rc) // доменный сотрудник — и к папкам шлюза «через домен»
|
||||
h.usersFlash(w, r, "Пользователь "+u.Login+" сохранён.")
|
||||
}
|
||||
|
||||
@@ -1390,6 +1391,7 @@ func (h *setupHandlers) deleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
_ = globalUsers.Delete(login)
|
||||
igateShareSyncSoon(h.rc)
|
||||
globalSessions.DropUser(login)
|
||||
h.usersFlash(w, r, "Пользователь "+login+" удалён.")
|
||||
}
|
||||
@@ -1445,6 +1447,7 @@ func (h *setupHandlers) saveAuth(w http.ResponseWriter, r *http.Request) {
|
||||
h.usersFlash(w, r, "Вход: "+err.Error())
|
||||
return
|
||||
}
|
||||
igateShareSyncSoon(h.rc)
|
||||
h.usersFlash(w, r, "Настройки входа сохранены.")
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -259,17 +258,7 @@ 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)})))
|
||||
})
|
||||
registerIgateShare(mux, rc) // папки обмена шлюза по сети (igateshare.go)
|
||||
// ход записи канала: страница показывает его и после перезагрузки
|
||||
mux.HandleFunc("/admin/setup/igate/contour-status", func(w http.ResponseWriter, r *http.Request) {
|
||||
jsonOut(w, igateContourJob.view())
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
package lkgateway
|
||||
|
||||
// Папки обмена шлюза НРД по сети (07.10.2026): два режима на выбор.
|
||||
// simple — один логин и пароль Samba (как было);
|
||||
// domain — сервер bj подключён к домену AD (winbind), папки открыты тем же группам и сотрудникам домена,
|
||||
// у которых есть доступ к bj: группы с ролью (Auth.ADGroups) и доменные учётные записи bj. Сотрудник
|
||||
// открывает папку своим входом в Windows. Список доступа bj сверяет сам — при изменении групп и
|
||||
// сотрудников и раз в 15 минут.
|
||||
// Подключение к домену — один раз, учётной записью администратора домена; bj её не сохраняет.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
func registerIgateShare(mux *http.ServeMux, rc *RuntimeConfig) {
|
||||
// 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()
|
||||
if r.Method != http.MethodPost {
|
||||
st := opResult(igateShareStatus(ctx))
|
||||
a := rc.Snapshot().Auth
|
||||
st["ad_ready"] = a.ADEnabled && a.ADBindUser != "" && a.ADDomain != ""
|
||||
st["ad_domain"] = a.ADDomain
|
||||
st["access"] = igateShareAccessView(a)
|
||||
jsonOut(w, st)
|
||||
return
|
||||
}
|
||||
p := map[string]string{"enabled": fmt.Sprint(r.FormValue("enabled") != ""), "mode": "simple",
|
||||
"user": strings.TrimSpace(r.FormValue("user")), "password": r.FormValue("password")}
|
||||
if r.FormValue("mode") == "domain" {
|
||||
valid, err := igateShareValid(rc.Snapshot().Auth)
|
||||
if err != nil {
|
||||
jsonOut(w, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
p = map[string]string{"enabled": fmt.Sprint(r.FormValue("enabled") != ""), "mode": "domain", "valid": valid}
|
||||
}
|
||||
extra, _ := json.Marshal(p)
|
||||
jsonOut(w, opResult(runKeymediaOp(ctx, "igate-share", map[string]string{"extra": string(extra)})))
|
||||
})
|
||||
// подключить сервер к домену (один раз) и открыть папки сотрудникам домена с доступом к bj
|
||||
mux.HandleFunc("/admin/setup/igate/share-join", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
a := rc.Snapshot().Auth
|
||||
admin, pw := strings.TrimSpace(r.FormValue("admin")), r.FormValue("adminpw")
|
||||
if !a.ADEnabled || a.ADDomain == "" || a.ADBindUser == "" {
|
||||
jsonOut(w, map[string]any{"ok": false, "error": "домен в bj не подключён — Настройка → Пользователи → «Сотрудники из домена»"})
|
||||
return
|
||||
}
|
||||
if admin == "" || pw == "" {
|
||||
jsonOut(w, map[string]any{"ok": false, "error": "не заполнено: логин и пароль администратора домена"})
|
||||
return
|
||||
}
|
||||
wg, err := adNetbiosName(a)
|
||||
if err != nil {
|
||||
jsonOut(w, map[string]any{"ok": false, "error": "краткое имя домена не определено: " + err.Error()})
|
||||
return
|
||||
}
|
||||
valid, err := igateShareValidWG(a, wg)
|
||||
if err != nil {
|
||||
jsonOut(w, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 6*time.Minute) // установка winbind и подключение к домену
|
||||
defer cancel()
|
||||
extra, _ := json.Marshal(map[string]string{"enabled": "true", "mode": "domain", "join": "1", "realm": a.ADDomain,
|
||||
"workgroup": wg, "admin": admin, "adminpw": pw, "valid": valid})
|
||||
res := opResult(runKeymediaOp(ctx, "igate-share", map[string]string{"extra": string(extra)}))
|
||||
if ok, _ := res["ok"].(bool); ok {
|
||||
log.Printf("igate-share: сервер подключён к домену %s (%s), папки открыты: %s", a.ADDomain, wg, valid)
|
||||
}
|
||||
jsonOut(w, res)
|
||||
})
|
||||
// сейчас сверить список доступа (после изменения групп и сотрудников)
|
||||
mux.HandleFunc("/admin/setup/igate/share-sync", func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
|
||||
defer cancel()
|
||||
msg, err := igateShareSync(ctx, rc)
|
||||
if err != nil {
|
||||
jsonOut(w, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
jsonOut(w, map[string]any{"ok": true, "output": msg})
|
||||
})
|
||||
}
|
||||
|
||||
func igateShareStatus(ctx context.Context) (map[string]any, error) {
|
||||
extra, _ := json.Marshal(map[string]string{"enabled": "status"})
|
||||
return runKeymediaOp(ctx, "igate-share", map[string]string{"extra": string(extra)})
|
||||
}
|
||||
|
||||
// adNetbiosName — краткое имя домена (CORP) из раздела конфигурации AD; запоминается на время работы bj.
|
||||
var netbiosCache sync.Map // ADDomain → имя
|
||||
|
||||
func adNetbiosName(a AuthSettings) (string, error) {
|
||||
if v, ok := netbiosCache.Load(strings.ToLower(a.ADDomain)); ok {
|
||||
return v.(string), nil
|
||||
}
|
||||
conn, err := adDial(a)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer conn.Close()
|
||||
if err := adBindService(conn, a); err != nil {
|
||||
return "", err
|
||||
}
|
||||
base := adDefaultBase(conn)
|
||||
if base == "" {
|
||||
return "", fmt.Errorf("контроллер не назвал базу домена")
|
||||
}
|
||||
sr, err := conn.Search(ldap.NewSearchRequest("CN=Partitions,CN=Configuration,"+base, ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 5, 10, false,
|
||||
"(&(objectClass=crossRef)(nETBIOSName=*))", []string{"nETBIOSName", "dnsRoot"}, nil))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, e := range sr.Entries {
|
||||
if strings.EqualFold(e.GetAttributeValue("dnsRoot"), a.ADDomain) || len(sr.Entries) == 1 {
|
||||
n := strings.ToUpper(e.GetAttributeValue("nETBIOSName"))
|
||||
netbiosCache.Store(strings.ToLower(a.ADDomain), n)
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("в домене %s не найдено краткое имя", a.ADDomain)
|
||||
}
|
||||
|
||||
// igateShareMembers — кому открыть папки: группы домена с ролью в bj и доменные учётные записи bj (включённые).
|
||||
func igateShareMembers(a AuthSettings) (groups, users []string) {
|
||||
seen := map[string]bool{}
|
||||
for _, g := range a.ADGroups {
|
||||
n := strings.TrimSpace(dnCN(g.Group))
|
||||
if n == "" || !roleAvailable(g.Role) || seen["g:"+strings.ToLower(n)] {
|
||||
continue
|
||||
}
|
||||
seen["g:"+strings.ToLower(n)] = true
|
||||
groups = append(groups, n)
|
||||
}
|
||||
if globalUsers != nil {
|
||||
for _, u := range globalUsers.List() {
|
||||
if u.Source != "ad" || !u.Active || seen["u:"+strings.ToLower(u.Login)] {
|
||||
continue
|
||||
}
|
||||
seen["u:"+strings.ToLower(u.Login)] = true
|
||||
users = append(users, u.Login)
|
||||
}
|
||||
}
|
||||
sort.Strings(groups)
|
||||
sort.Strings(users)
|
||||
return groups, users
|
||||
}
|
||||
|
||||
// igateShareAccessView — список доступа словами (для страницы).
|
||||
func igateShareAccessView(a AuthSettings) map[string]any {
|
||||
g, u := igateShareMembers(a)
|
||||
return map[string]any{"groups": g, "users": u}
|
||||
}
|
||||
|
||||
func igateShareValid(a AuthSettings) (string, error) {
|
||||
wg, err := adNetbiosName(a)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("краткое имя домена не определено: %v", err)
|
||||
}
|
||||
return igateShareValidWG(a, wg)
|
||||
}
|
||||
|
||||
// igateShareValidWG — строка «valid users» для Samba: @"CORP\Группа", "CORP\логин".
|
||||
func igateShareValidWG(a AuthSettings, wg string) (string, error) {
|
||||
groups, users := igateShareMembers(a)
|
||||
var parts []string
|
||||
clean := func(s string) string { return strings.NewReplacer(`"`, "", "\n", "", "\r", "", ",", "").Replace(s) }
|
||||
for _, g := range groups {
|
||||
parts = append(parts, `@"`+wg+`\`+clean(g)+`"`)
|
||||
}
|
||||
for _, u := range users {
|
||||
parts = append(parts, `"`+wg+`\`+clean(u)+`"`)
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return "", fmt.Errorf("в bj нет групп и сотрудников домена с доступом — добавьте их в Настройка → Пользователи («Сотрудники из домена»)")
|
||||
}
|
||||
return strings.Join(parts, ", "), nil
|
||||
}
|
||||
|
||||
// igateShareSync — режим «через домен»: если состав групп и сотрудников с доступом к bj изменился, переписать
|
||||
// доступ к папкам. В простом режиме и при закрытых папках ничего не делает.
|
||||
func igateShareSync(ctx context.Context, rc *RuntimeConfig) (string, error) {
|
||||
st, err := igateShareStatus(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if on, _ := st["on"].(bool); !on {
|
||||
return "папки закрыты — сверять нечего", nil
|
||||
}
|
||||
if m, _ := st["mode"].(string); m != "domain" {
|
||||
return "простой режим — доступ по одному логину", nil
|
||||
}
|
||||
valid, err := igateShareValid(rc.Snapshot().Auth)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if cur, _ := st["user"].(string); cur == valid {
|
||||
return "список доступа не изменился", nil
|
||||
}
|
||||
extra, _ := json.Marshal(map[string]string{"enabled": "true", "mode": "domain", "valid": valid})
|
||||
res, err := runKeymediaOp(ctx, "igate-share", map[string]string{"extra": string(extra)})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if ok, _ := res["ok"].(bool); !ok {
|
||||
e, _ := res["error"].(string)
|
||||
return "", fmt.Errorf("%s", e)
|
||||
}
|
||||
log.Printf("igate-share: доступ к папкам шлюза обновлён: %s", valid)
|
||||
return "доступ обновлён: " + valid, nil
|
||||
}
|
||||
|
||||
// igateShareSyncSoon — после изменения групп или сотрудников: сверить доступ к папкам в фоне (раз в несколько
|
||||
// секунд, сколько бы изменений ни пришло подряд).
|
||||
var igateShareSyncTimer struct {
|
||||
sync.Mutex
|
||||
t *time.Timer
|
||||
}
|
||||
|
||||
func igateShareSyncSoon(rc *RuntimeConfig) {
|
||||
if rc == nil {
|
||||
return
|
||||
}
|
||||
igateShareSyncTimer.Lock()
|
||||
defer igateShareSyncTimer.Unlock()
|
||||
if igateShareSyncTimer.t != nil {
|
||||
igateShareSyncTimer.t.Stop()
|
||||
}
|
||||
igateShareSyncTimer.t = time.AfterFunc(5*time.Second, func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
if _, err := igateShareSync(ctx, rc); err != nil {
|
||||
log.Printf("igate-share: сверка доступа к папкам: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// igateShareSyncLoop — подстраховка: раз в 15 минут (группы могли поменять в настройках, пока bj перезапускался).
|
||||
func (s *Server) igateShareSyncLoop(ctx context.Context) {
|
||||
t := time.NewTicker(15 * time.Minute)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
}
|
||||
c, cancel := context.WithTimeout(ctx, 2*time.Minute)
|
||||
if _, err := igateShareSync(c, s.rc); err != nil {
|
||||
log.Printf("igate-share: сверка доступа к папкам: %v", err)
|
||||
}
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package lkgateway
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Папки шлюза «через домен»: доступ — группам с ролью в bj и доменным сотрудникам bj (включённым).
|
||||
func TestIgateShareValid(t *testing.T) {
|
||||
prev := globalUsers
|
||||
globalUsers = &userStore{users: map[string]*User{}}
|
||||
defer func() { globalUsers = prev }()
|
||||
globalUsers.users["azuev"] = &User{Login: "azuev", Source: "ad", Active: true, Role: "operator"}
|
||||
globalUsers.users["old"] = &User{Login: "old", Source: "ad", Active: false, Role: "viewer"}
|
||||
globalUsers.users["admin"] = &User{Login: "admin", Source: "local", Active: true, Role: "admin"}
|
||||
a := AuthSettings{ADGroups: []ADGroupRole{{Group: "CN=BJ-Operators,OU=Groups,DC=corp,DC=local", Role: "operator"}, {Group: "Депозитарий \"Главный\"", Role: "viewer"}}}
|
||||
got, err := igateShareValidWG(a, "CORP")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := `@"CORP\BJ-Operators", @"CORP\Депозитарий Главный", "CORP\azuev"`
|
||||
if got != want {
|
||||
t.Fatalf("\n got %s\nwant %s", got, want)
|
||||
}
|
||||
if strings.Contains(got, "old") || strings.Contains(got, "admin") {
|
||||
t.Fatal("в доступ попали отключённый или локальный пользователь")
|
||||
}
|
||||
globalUsers = &userStore{users: map[string]*User{}}
|
||||
if _, err := igateShareValidWG(AuthSettings{}, "CORP"); err == nil {
|
||||
t.Fatal("пустой список доступа должен быть ошибкой")
|
||||
}
|
||||
}
|
||||
@@ -303,6 +303,7 @@ func (s *Server) Run(ctx context.Context) error {
|
||||
go superviseLoop(ctx, "consume-decisions", s.rc, s.consumeDecisions)
|
||||
go superviseLoop(ctx, "igw-watch", s.rc, s.igwWatchLoop) // сторож ИШ НРД: оповещения и автоперезапуск
|
||||
go superviseLoop(ctx, "units-watch", s.rc, s.unitsWatchLoop) // службы модулей и ключей Валидаты: письмо администратору
|
||||
go superviseLoop(ctx, "igate-share", s.rc, s.igateShareSyncLoop) // папки шлюза «через домен»: список доступа = группы и сотрудники bj
|
||||
go handbookLoop(ctx, s.rc) // справочник участников M2M с сервера лицензий (для установок без шлюза НРД)
|
||||
|
||||
// Поллер входящих от НРД (только в реальном режиме ИШ): забирает
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
.ct-act .btn[disabled]{opacity:.5;cursor:not-allowed}
|
||||
@media (max-width:760px){.ct-grid{grid-template-columns:minmax(0,1fr)}}
|
||||
.ig-share{margin-top:18px;padding-top:16px;border-top:1px solid var(--border)}
|
||||
.ig-sh-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:10px}
|
||||
.ig-sh-grid label.f input{width:100%;box-sizing:border-box}
|
||||
@media (max-width:600px){.ig-sh-grid{grid-template-columns:minmax(0,1fr)}}
|
||||
.ig-addr{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:6px}
|
||||
.ig-addr code{font-size:14px;padding:4px 10px;border-radius:8px;background:var(--card);border:1px solid var(--border);user-select:all}
|
||||
.ig-addr .muted{font-size:12.5px}
|
||||
@@ -81,13 +84,29 @@
|
||||
</div>
|
||||
<div class="ig-share" id="ig-share">
|
||||
<h3>Папки шлюза по сети <span class="muted" style="font-weight:400;font-size:13px">— необязательно</span></h3>
|
||||
<p class="muted" style="font-size:13px;margin:8px 0 0">Папки обмена шлюза (INBOX, OUTBOX, SENT, ERRORS) можно открыть по сети Windows — если другие программы организации кладут или забирают файлы через этот шлюз. bj для работы это не нужно. Доступ — по своему логину и паролю, файлы пишутся от имени шлюза.</p>
|
||||
<p class="muted" style="font-size:13px;margin:4px 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="password" autocomplete="new-password" 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 id="ig-share-f" class="dq" style="margin-top:12px;max-width:680px">
|
||||
<div class="dq-tiles">
|
||||
<label class="dq-tile"><input type="radio" name="mode" value="simple" checked><span><b>Простой</b><small>Один логин и пароль на всех, кому нужны папки.</small></span></label>
|
||||
<label class="dq-tile" id="ig-sh-dtile"><input type="radio" name="mode" value="domain"><span><b>Через домен</b><small id="ig-sh-dnote">Те же группы и сотрудники домена, что и в bj, — своим входом в Windows, без отдельного пароля. Убрали из группы — доступ пропал.</small></span></label>
|
||||
</div>
|
||||
<label style="display:flex;gap:8px;align-items:center;font-weight:500;margin:12px 0 0"><input type="checkbox" name="enabled" style="width:auto"> Открыть папки шлюза по сети</label>
|
||||
<div id="ig-sh-simple" class="ig-sh-grid">
|
||||
<label class="f">Логин<input name="user" value="igate-share" autocomplete="off" spellcheck="false"></label>
|
||||
<label class="f">Пароль (от 8 символов)<input name="password" type="password" autocomplete="new-password" spellcheck="false" placeholder="пусто — прежний"></label>
|
||||
</div>
|
||||
<div id="ig-sh-domain" hidden>
|
||||
<div id="ig-sh-access" class="muted" style="font-size:13px;margin-top:10px"></div>
|
||||
<div id="ig-sh-join" hidden style="margin-top:10px">
|
||||
<p style="margin:0;font-size:13.5px">Сервер bj ещё не подключён к домену <b id="ig-sh-realm"></b>. Подключить нужно один раз — учётной записью с правом добавлять компьютеры в домен; bj её не сохраняет. В домене появится компьютер <b id="ig-sh-host"></b>.</p>
|
||||
<div class="ig-sh-grid">
|
||||
<label class="f">Администратор домена<input name="admin" autocomplete="off" spellcheck="false" placeholder="например admin или admin@corp.local"></label>
|
||||
<label class="f">Пароль<input name="adminpw" type="password" autocomplete="new-password"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:12px"><button type="submit" class="btn sm" id="ig-sh-save">Сохранить</button><span id="ig-share-out" style="font-size:13px"></span></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="ig-job" id="ig-job" hidden aria-live="polite">
|
||||
@@ -160,28 +179,54 @@
|
||||
}).catch(function(){});
|
||||
return {watch:watch};
|
||||
})();
|
||||
// доступ к папкам шлюза по сети (Samba): состояние и сохранение
|
||||
// доступ к папкам шлюза по сети (Samba): простой режим или через домен
|
||||
(function(){
|
||||
var f=document.getElementById('ig-share-f'), stl=document.getElementById('ig-share-st'), o=document.getElementById('ig-share-out');
|
||||
if(!f) return;
|
||||
var cur={}, sim=document.getElementById('ig-sh-simple'), dom=document.getElementById('ig-sh-domain'), join=document.getElementById('ig-sh-join'), acc=document.getElementById('ig-sh-access');
|
||||
function mode(){ return f.mode.value; }
|
||||
function sync(){ var d=mode()==='domain'; sim.hidden=d; dom.hidden=!d; join.hidden=!(d && !cur.joined);
|
||||
document.getElementById('ig-sh-save').textContent=(d && !cur.joined)?'Подключить сервер к домену и открыть папки':'Сохранить'; }
|
||||
function addrs(d){ var hosts=[d.ip||location.hostname]; if(d.host && d.host!==hosts[0]) hosts.push(d.host);
|
||||
var names=(d.shares&&d.shares.length)?d.shares:['igate_exchange'], rows=[];
|
||||
names.forEach(function(n){ hosts.forEach(function(h){ var p='\\\\'+h+'\\'+n;
|
||||
rows.push('<div class="ig-addr"><code>'+esc(p)+'</code><button type="button" class="btn sm ghost" data-copy="'+esc(p)+'">Скопировать</button><span class="muted">'+(n.indexOf('prod')>=0?'боевой контур':'тестовый контур TEST3')+(h===d.host?' · по имени сервера':'')+'</span></div>'); }); });
|
||||
return rows.join(''); }
|
||||
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; }
|
||||
cur=d;
|
||||
if(d.manual){ stl.innerHTML=ok('папки открыты вручную (smb.conf) для '+esc(d.user||'—')+' — bj их не меняет'); f.hidden=true; return; }
|
||||
var a=d.access||{}, g=a.groups||[], u=a.users||[];
|
||||
acc.innerHTML=(g.length||u.length)?('Доступ получат: '+(g.length?'группы <b>'+g.map(esc).join('</b>, <b>')+'</b>':'')+(g.length&&u.length?'; ':'')+(u.length?'сотрудники <b>'+u.map(esc).join('</b>, <b>')+'</b>':'')+'. Список — из Настройка → Пользователи, обновляется сам.'):'<span style="color:var(--warn)">В bj пока нет групп и сотрудников домена с доступом — добавьте их в Настройка → Пользователи («Сотрудники из домена»).</span>';
|
||||
var dt=document.getElementById('ig-sh-dtile'), dr=f.querySelector('input[name=mode][value=domain]');
|
||||
dr.disabled=!d.ad_ready; dt.style.opacity=d.ad_ready?'':'.55';
|
||||
if(!d.ad_ready) document.getElementById('ig-sh-dnote').textContent='Сначала подключите домен в bj: Настройка → Пользователи → «Сотрудники из домена».';
|
||||
document.getElementById('ig-sh-realm').textContent=d.ad_domain||d.realm||'';
|
||||
document.getElementById('ig-sh-host').textContent=d.host||'';
|
||||
f.querySelector('input[name=mode][value='+(d.mode==='domain'?'domain':'simple')+']').checked=true;
|
||||
if(d.on){
|
||||
var hosts=[d.ip||location.hostname]; if(d.host && d.host!==hosts[0]) hosts.push(d.host);
|
||||
var names=(d.shares&&d.shares.length)?d.shares:['igate_exchange'];
|
||||
var rows=[]; names.forEach(function(n){ hosts.forEach(function(h){ var p='\\\\'+h+'\\'+n;
|
||||
rows.push('<div class="ig-addr"><code>'+esc(p)+'</code><button type="button" class="btn sm ghost" data-copy="'+esc(p)+'">Скопировать</button><span class="muted">'+(n.indexOf('prod')>=0?'боевой контур':'тестовый контур TEST3')+(h===d.host?' · по имени сервера':'')+'</span></div>'); }); });
|
||||
stl.innerHTML='<div class="stc stc-ok" style="margin-top:4px"><span class="stc-ic"></span><div class="stc-b"><b>Папки открыты по сети</b><small>Вставьте адрес в адресную строку проводника Windows; вход — логин <b>'+esc(d.user||'—')+'</b> и пароль, который задали здесь.</small>'+rows.join('')+'</div></div>';
|
||||
var who=d.mode==='domain'?'Входят сотрудники домена с доступом к bj — своей учётной записью Windows, пароль не спрашивается.':'Вход — логин <b>'+esc(d.user||'—')+'</b> и пароль, который задали здесь.';
|
||||
stl.innerHTML='<div class="stc stc-ok" style="margin-top:4px"><span class="stc-ic"></span><div class="stc-b"><b>Папки открыты по сети'+(d.mode==='domain'?' — через домен '+esc(d.realm||''):'')+'</b><small>Вставьте адрес в адресную строку проводника Windows. '+who+'</small>'+addrs(d)+'</div></div>';
|
||||
stl.querySelectorAll('[data-copy]').forEach(function(b){ b.onclick=function(){ (window.bjCopy||function(t){navigator.clipboard&&navigator.clipboard.writeText(t)})(b.dataset.copy); b.textContent='Скопировано ✓'; setTimeout(function(){b.textContent='Скопировать'},1500); }; });
|
||||
} else stl.innerHTML='<span class="muted">Сейчас закрыты. Отметьте «Открыть папки шлюза по сети», задайте пароль и нажмите «Сохранить» — здесь появится адрес для проводника.</span>';
|
||||
f.enabled.checked=!!d.on; if(d.user) f.user.value=d.user;
|
||||
} else stl.innerHTML='<span class="muted">Сейчас закрыты. Выберите, как входить, отметьте «Открыть папки шлюза по сети» и сохраните — здесь появится адрес для проводника.</span>';
|
||||
f.enabled.checked=!!d.on; if(d.mode!=='domain' && d.user) f.user.value=d.user;
|
||||
sync();
|
||||
}
|
||||
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('не сохранено'); }); });
|
||||
function load(){ return fetch('/admin/setup/igate/share',{cache:'no-store'}).then(function(r){return r.json()}).then(show).catch(function(){ stl.textContent='Состояние не получено.'; }); }
|
||||
f.addEventListener('change',function(e){ if(e.target.name==='mode') sync(); });
|
||||
f.addEventListener('submit',function(e){ e.preventDefault();
|
||||
var p=new URLSearchParams(), url='/admin/setup/igate/share', d=mode()==='domain';
|
||||
if(d && !cur.joined){
|
||||
if(!f.admin.value.trim()||!f.adminpw.value){ o.innerHTML=bad('не заполнено: администратор домена и пароль'); return; }
|
||||
url='/admin/setup/igate/share-join'; p.set('admin',f.admin.value.trim()); p.set('adminpw',f.adminpw.value);
|
||||
o.textContent='Подключаю сервер к домену… (первый раз — установка winbind, до пары минут)';
|
||||
} else {
|
||||
p.set('mode',mode()); if(f.enabled.checked) p.set('enabled','1');
|
||||
if(!d){ p.set('user',f.user.value.trim()); p.set('password',f.password.value); }
|
||||
o.textContent='Сохраняю… (первый раз — установка Samba, до пары минут)';
|
||||
}
|
||||
post(url,p).then(function(r){ o.innerHTML=r.ok?ok(r.output):bad(r.error); f.password.value=''; f.adminpw.value=''; return load(); }).catch(function(){ o.innerHTML=bad('не сохранено'); }); });
|
||||
load();
|
||||
})();
|
||||
var st=null;
|
||||
function load(){
|
||||
|
||||
Reference in New Issue
Block a user