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 }