Files
Bridge-and-Join-s/internal/lkgateway/onec.go
T
zuevav 73b246831c Пароль домена не попадает в страницу; домен в мастере — открыт, крупно, группы; справочник 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
2026-10-07 01:19:30 +03:00

129 lines
5.7 KiB
Go

package lkgateway
// База 1С бэк-офиса — модуль лицензии onec-back (07.10.2026, тестовый). Пока это подключение: адрес
// публикации базы на веб-сервере, пользователь и пароль 1С; bj проверяет их через стандартный интерфейс OData
// ($metadata). Обмен данными с базой — следующий шаг модуля.
import (
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"strings"
"time"
)
// OneCSettings — подключение к базе 1С бэк-офиса.
type OneCSettings struct {
URL string `json:"url,omitempty"` // адрес публикации: http://1c-server/middle
User string `json:"user,omitempty"`
Password string `json:"password,omitempty"`
CheckedAt time.Time `json:"checked_at,omitempty"`
CheckOK bool `json:"check_ok,omitempty"`
CheckMsg string `json:"check_msg,omitempty"`
}
func (r *RuntimeConfig) UpdateOneC(s OneCSettings) error {
r.mu.Lock()
r.data.OneC = s
r.data.UpdatedAt = time.Now().UTC()
r.mu.Unlock()
return r.save()
}
// oneCBase — адрес публикации без хвоста /odata/… и без завершающего «/».
func oneCBase(u string) string {
u = strings.TrimSpace(u)
if i := strings.Index(strings.ToLower(u), "/odata/"); i >= 0 {
u = u[:i]
}
u = strings.TrimRight(u, "/")
if u != "" && !strings.Contains(u, "://") {
u = "http://" + u
}
return u
}
// saveOneC — POST /admin/setup/onec: сохранить подключение и сразу проверить его.
func (h *setupHandlers) saveOneC(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
s := h.rc.Snapshot().OneC
s.URL = oneCBase(r.FormValue("onec_url"))
s.User = strings.TrimSpace(r.FormValue("onec_user"))
if pw := r.FormValue("onec_password"); pw != "" {
s.Password = pw
}
var miss []string
for _, f := range []struct{ v, n string }{{s.URL, "адрес публикации базы"}, {s.User, "пользователь 1С"}, {s.Password, "пароль"}} {
if f.v == "" {
miss = append(miss, f.n)
}
}
if len(miss) > 0 {
setupFlash(w, r, "1С: не заполнено: "+strings.Join(miss, ", "))
return
}
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
s.CheckOK, s.CheckMsg = oneCCheck(ctx, s)
cancel()
s.CheckedAt = time.Now()
if err := h.rc.UpdateOneC(s); err != nil {
setupFlash(w, r, "1С: настройки не сохранены — "+err.Error())
return
}
if s.CheckOK {
setupFlash(w, r, "1С: подключение сохранено — "+s.CheckMsg)
return
}
setupFlash(w, r, "1С: подключение сохранено, но проверка не прошла — "+s.CheckMsg)
}
// oneCCheck — база отвечает по стандартному интерфейсу OData и пускает пользователя.
func oneCCheck(ctx context.Context, s OneCSettings) (bool, string) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, oneCBase(s.URL)+"/odata/standard.odata/$metadata", nil)
if err != nil {
return false, "адрес не разобран: " + err.Error()
}
req.SetBasicAuth(s.User, s.Password)
cl := &http.Client{Transport: &http.Transport{Proxy: nil, DialContext: (&net.Dialer{Timeout: 8 * time.Second}).DialContext}}
resp, err := cl.Do(req)
if err != nil {
var ne net.Error
if errors.As(err, &ne) && ne.Timeout() {
return false, "сервер 1С не ответил вовремя — проверьте адрес и что веб-сервер с публикацией базы доступен с сервера bj"
}
if strings.Contains(err.Error(), "certificate") {
return false, "сертификат HTTPS веб-сервера 1С не доверенный — укажите адрес по http или выпустите доверенный сертификат (" + err.Error() + ")"
}
return false, "сервер 1С не отвечает: " + err.Error()
}
defer resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(resp.Body, 64<<20))
switch resp.StatusCode {
case http.StatusOK:
case http.StatusUnauthorized, http.StatusForbidden:
return false, "1С не пустила пользователя «" + s.User + "»: неверный логин или пароль, либо у пользователя нет права на внешнее подключение"
case http.StatusNotFound:
return false, "по адресу нет стандартного интерфейса OData: в конфигураторе 1С — Администрирование → Публикация на веб-сервере, отметьте «Публиковать стандартный интерфейс OData» и переопубликуйте базу"
default:
t := strings.TrimSpace(string(body))
if len([]rune(t)) > 200 {
t = string([]rune(t)[:200]) + "…"
}
return false, fmt.Sprintf("веб-сервер 1С ответил %d %s", resp.StatusCode, t)
}
if !strings.Contains(string(body), "edmx") {
return false, "по адресу отвечает не 1С (нет описания OData) — проверьте адрес публикации базы"
}
n := strings.Count(string(body), "<EntityType ")
if n == 0 {
return true, "база отвечает, пользователь входит; объекты для OData ещё не открыты — их состав задаётся в базе 1С (УстановитьСоставСтандартногоИнтерфейсаOData)"
}
return true, fmt.Sprintf("база отвечает, пользователь входит; объектов в OData — %d", n)
}