Учёт DEPO: имя базы обязательно, bj базу по серверу не ищет
Имя базы DEPO задаёт клиент — скрипт и подготовка логином администратора работают только с ней. Автопоиск по серверу убран (экран, скрипт, подготовка, проверка подключения, старый экран conn), выбор из нескольких найденных баз — тоже. Вместо поиска — защита от опечатки: скрипт (SQL Server и PostgreSQL), подготовка и проверка подключения сначала убеждаются, что в названной базе есть учётные таблицы OD_FACES и OD_RESTS, иначе останавливаются, ничего не меняя. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
package lkgateway
|
||||
|
||||
// Проверка, что указанная база — учёт DEPO (06.10.2026). Имя базы клиент задаёт сам: bj по серверу базы не
|
||||
// ищет. Чтобы опечатка в имени не привела bj в чужую базу, перед подготовкой и при проверке подключения
|
||||
// bj смотрит, есть ли в ней учётные таблицы Fancy DEPO — лица OD_FACES и остатки OD_RESTS.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// depoMarkerTables — по каким таблицам база опознаётся как DEPO (Fancy).
|
||||
var depoMarkerTables = []string{"OD_FACES", "OD_RESTS"}
|
||||
|
||||
// depoCheckIsDepo — в базе db есть учётные таблицы DEPO, видимые логину s.User.
|
||||
func depoCheckIsDepo(ctx context.Context, s FancyMSSQLSettings, db string) error {
|
||||
cctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
conn, err := fancyOpen(s, db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
if err := conn.PingContext(cctx); err != nil {
|
||||
return fmt.Errorf("база %s недоступна логину %s: %s", db, s.User, quikErrorDetail(err))
|
||||
}
|
||||
var miss []string
|
||||
for _, t := range depoMarkerTables {
|
||||
if !depoTableVisible(cctx, conn, s.IsPostgres(), t) {
|
||||
miss = append(miss, t)
|
||||
}
|
||||
}
|
||||
if len(miss) > 0 {
|
||||
return fmt.Errorf("в базе %s нет учётных таблиц DEPO (%s) — проверьте имя базы: это не база учёта DEPO или у логина %s нет к ней доступа",
|
||||
db, strings.Join(miss, ", "), s.User)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -2,8 +2,8 @@ package lkgateway
|
||||
|
||||
// Подготовка базы учёта DEPO одним действием (06.10.2026). Раньше это были три экрана мастера — логин bj,
|
||||
// таблицы обмена, задание SQL Agent — и три скрипта. Теперь один скрипт делает всё:
|
||||
// - находит базу DEPO на сервере сам (по учётным таблицам OD_FACES и OD_RESTS) — имя базы клиенту знать
|
||||
// не нужно (SQL Server; для PostgreSQL базу назвать придётся: psql не умеет искать по чужим базам);
|
||||
// - работает с базой, которую назвал клиент (сам по серверу не ищет), и сначала проверяет, что в ней есть
|
||||
// учётные таблицы DEPO OD_FACES и OD_RESTS — опечатка в имени не уведёт в чужую базу;
|
||||
// - создаёт логин bj (пароль придумывает bj и запоминает) или обновляет его пароль;
|
||||
// - создаёт таблицы обмена FTV_M2M* и выдаёт логину bj права: чтение учёта, запись в таблицы обмена;
|
||||
// - по галочке «обработку запускает SQL Server Agent» — создаёт задание bj2depo, иначе обработку
|
||||
@@ -50,8 +50,8 @@ func depoGenPassword() string {
|
||||
// sqlStr — строковый литерал T-SQL / PostgreSQL (одинарная кавычка удваивается).
|
||||
func sqlStr(s string) string { return strings.ReplaceAll(s, "'", "''") }
|
||||
|
||||
// depoSetupScript — один скрипт подготовки базы DEPO. db — имя базы (пусто — найти на сервере; только SQL
|
||||
// Server), agentJob — создать задание SQL Server Agent (иначе обработку вызывает bj).
|
||||
// depoSetupScript — один скрипт подготовки базы DEPO. db — имя базы DEPO (задаёт клиент), agentJob — создать
|
||||
// задание SQL Server Agent (иначе обработку вызывает bj).
|
||||
func depoSetupScript(f FancyMSSQLSettings, db, login, pass string, agentJob bool) string {
|
||||
if f.IsPostgres() {
|
||||
return depoSetupScriptPG(f, db, login, pass)
|
||||
@@ -65,13 +65,13 @@ func depoSetupScript(f FancyMSSQLSettings, db, login, pass string, agentJob bool
|
||||
SET NOCOUNT ON;
|
||||
|
||||
-- ===== Проверьте перед запуском ==========================================
|
||||
-- База DEPO: пусто — скрипт найдёт её сам по таблицам OD_FACES и OD_RESTS.
|
||||
-- База DEPO — точное имя базы учёта на этом сервере.
|
||||
DECLARE @db sysname = N'%[3]s';
|
||||
-- Пароль логина bj (%[1]s). Меняете — впишите тот же пароль в bj, шаг «Учёт DEPO».
|
||||
DECLARE @pass nvarchar(128) = N'%[4]s';
|
||||
-- =========================================================================
|
||||
|
||||
-- Что делает: находит базу DEPO, создаёт логин bj и таблицы обмена FTV_M2M*, выдаёт права
|
||||
-- Что делает: проверяет, что @db — база DEPO, создаёт логин bj и таблицы обмена FTV_M2M*, выдаёт права
|
||||
-- (чтение учёта, запись только в таблицы обмена). Обработка обмена: %[2]s.
|
||||
-- Учётные данные DEPO не меняет. Запускать в SSMS или sqlcmd под логином с правами sysadmin.
|
||||
-- Можно запускать повторно. В скрипте пароль логина bj — после выполнения файл удалите.
|
||||
@@ -81,18 +81,19 @@ BEGIN
|
||||
RAISERROR(N'Впишите пароль логина bj в @pass в начале скрипта и запустите снова.', 16, 1);
|
||||
RETURN;
|
||||
END
|
||||
DECLARE @found int;
|
||||
IF @db = N''
|
||||
BEGIN
|
||||
SELECT @found = COUNT(*) FROM sys.databases WHERE state = 0 AND database_id > 4
|
||||
AND OBJECT_ID(QUOTENAME(name) + N'.dbo.OD_FACES') IS NOT NULL AND OBJECT_ID(QUOTENAME(name) + N'.dbo.OD_RESTS') IS NOT NULL;
|
||||
IF @found = 1
|
||||
SELECT @db = name FROM sys.databases WHERE state = 0 AND database_id > 4
|
||||
AND OBJECT_ID(QUOTENAME(name) + N'.dbo.OD_FACES') IS NOT NULL AND OBJECT_ID(QUOTENAME(name) + N'.dbo.OD_RESTS') IS NOT NULL;
|
||||
RAISERROR(N'Впишите имя базы DEPO в @db в начале скрипта и запустите снова.', 16, 1);
|
||||
RETURN;
|
||||
END
|
||||
IF @db = N'' OR DB_ID(@db) IS NULL
|
||||
IF DB_ID(@db) IS NULL
|
||||
BEGIN
|
||||
RAISERROR(N'База DEPO не найдена или на сервере их несколько. Впишите имя базы DEPO в @db в начале скрипта и запустите снова.', 16, 1);
|
||||
RAISERROR(N'Базы %%s на этом сервере нет. Проверьте имя в @db в начале скрипта.', 16, 1, @db);
|
||||
RETURN;
|
||||
END
|
||||
IF OBJECT_ID(QUOTENAME(@db) + N'.dbo.OD_FACES', N'U') IS NULL OR OBJECT_ID(QUOTENAME(@db) + N'.dbo.OD_RESTS', N'U') IS NULL
|
||||
BEGIN
|
||||
RAISERROR(N'В базе %%s нет учётных таблиц DEPO (OD_FACES, OD_RESTS) — это не база DEPO. Проверьте имя в @db.', 16, 1, @db);
|
||||
RETURN;
|
||||
END
|
||||
PRINT N'База DEPO: ' + @db;
|
||||
@@ -162,6 +163,12 @@ END $$;
|
||||
GRANT CONNECT ON DATABASE "%[1]s" TO "%[2]s";
|
||||
|
||||
\c "%[1]s"
|
||||
-- это база DEPO? (учётные таблицы OD_FACES и OD_RESTS) — иначе остановиться, ничего не создавая
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('od_faces') IS NULL OR to_regclass('od_rests') IS NULL THEN
|
||||
RAISE EXCEPTION 'В базе %% нет учётных таблиц DEPO (OD_FACES, OD_RESTS) — проверьте имя базы', current_database();
|
||||
END IF;
|
||||
END $$;
|
||||
`, db, login, sqlStr(pass))
|
||||
for _, s := range depoTablesDDL(true) {
|
||||
b.WriteString(s + ";\n")
|
||||
@@ -182,37 +189,31 @@ func depoSetupFileName(f FancyMSSQLSettings) string {
|
||||
return "bj-depo-setup-mssql.sql"
|
||||
}
|
||||
|
||||
// depoSetupRun — «bj сделает сам»: тот же скрипт выполняется логином администратора (не сохраняется).
|
||||
// Возвращает найденную базу DEPO.
|
||||
func depoSetupRun(ctx context.Context, f FancyMSSQLSettings, db, adminUser, adminPass, login, pass string, agentJob bool) (string, error) {
|
||||
// depoSetupRun — «bj сделает сам»: тот же скрипт выполняется логином администратора (не сохраняется) в базе
|
||||
// db, которую назвал клиент; сначала — проверка, что это база DEPO.
|
||||
func depoSetupRun(ctx context.Context, f FancyMSSQLSettings, db, adminUser, adminPass, login, pass string, agentJob bool) error {
|
||||
adm := f
|
||||
adm.User, adm.Password = strings.TrimSpace(adminUser), adminPass
|
||||
if db == "" {
|
||||
found, err := depoDiscover(ctx, adm)
|
||||
switch {
|
||||
case err != nil:
|
||||
return "", fmt.Errorf("список баз сервера логином %s: %s", adm.User, quikErrorDetail(err))
|
||||
case len(found) == 0:
|
||||
return "", fmt.Errorf("на сервере нет базы учёта DEPO (с таблицами %s), доступной логину %s", strings.Join(depoMarkerTables, " и "), adm.User)
|
||||
case len(found) > 1:
|
||||
return "", fmt.Errorf("на сервере несколько баз DEPO (%s) — укажите нужную в поле «База DEPO»", strings.Join(found, ", "))
|
||||
if !validDBName(db) {
|
||||
return fmt.Errorf("укажите имя базы DEPO")
|
||||
}
|
||||
db = found[0]
|
||||
if err := depoCheckIsDepo(ctx, adm, db); err != nil {
|
||||
return err
|
||||
}
|
||||
if f.IsPostgres() {
|
||||
return db, depoSetupRunPG(ctx, adm, db, login, pass)
|
||||
return depoSetupRunPG(ctx, adm, db, login, pass)
|
||||
}
|
||||
conn, err := fancyOpen(adm, "")
|
||||
if err != nil {
|
||||
return db, err
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
cctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
|
||||
defer cancel()
|
||||
if _, err := conn.ExecContext(cctx, depoSetupScript(f, db, login, pass, agentJob)); err != nil {
|
||||
return db, fmt.Errorf("подготовка базы %s логином %s: %s%s", db, adm.User, quikErrorDetail(err), depoTablesRightsHint(f, adm.User, err))
|
||||
return fmt.Errorf("подготовка базы %s логином %s: %s%s", db, adm.User, quikErrorDetail(err), depoTablesRightsHint(f, adm.User, err))
|
||||
}
|
||||
return db, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func depoSetupRunPG(ctx context.Context, adm FancyMSSQLSettings, db, login, pass string) error {
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
package lkgateway
|
||||
|
||||
// Поиск базы DEPO на сервере (05.10.2026). Клиент не знает и не должен знать, как называется база учёта у
|
||||
// него на сервере: на шаге «Подключение» он вводит только адрес, логин и пароль, а bj сам находит базы с
|
||||
// учётными таблицами Fancy DEPO (лица OD_FACES и остатки OD_RESTS), к которым у этого логина есть доступ.
|
||||
// Одна база — выбирается сразу, несколько — мастер предлагает выбрать, ни одной — понятная ошибка.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// depoMarkerTables — по каким таблицам база опознаётся как DEPO (Fancy).
|
||||
var depoMarkerTables = []string{"OD_FACES", "OD_RESTS"}
|
||||
|
||||
// depoDiscoverMax — сколько баз сервера просматривать (на сервере могут быть десятки баз).
|
||||
const depoDiscoverMax = 60
|
||||
|
||||
// depoDiscover — базы DEPO на сервере, доступные этому логину.
|
||||
func depoDiscover(ctx context.Context, s FancyMSSQLSettings) ([]string, error) {
|
||||
cctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
if s.IsPostgres() {
|
||||
return depoDiscoverPG(cctx, s)
|
||||
}
|
||||
conn, err := fancyOpen(s, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
rows, err := conn.QueryContext(cctx, `SELECT name FROM sys.databases
|
||||
WHERE state = 0 AND database_id > 4 AND HAS_DBACCESS(name) = 1 ORDER BY name`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("список баз сервера: %w", err)
|
||||
}
|
||||
var names []string
|
||||
for rows.Next() {
|
||||
var n string
|
||||
if rows.Scan(&n) == nil && validDBName(n) && len(names) < depoDiscoverMax {
|
||||
names = append(names, n)
|
||||
}
|
||||
}
|
||||
rows.Close()
|
||||
var found []string
|
||||
for _, n := range names {
|
||||
// имя проверено validDBName — подставлять в [ ] безопасно
|
||||
q := "SELECT CASE WHEN " + strings.Join(func() []string {
|
||||
var c []string
|
||||
for _, t := range depoMarkerTables {
|
||||
c = append(c, "OBJECT_ID(N'["+n+"].dbo."+t+"') IS NOT NULL")
|
||||
}
|
||||
return c
|
||||
}(), " AND ") + " THEN 1 ELSE 0 END"
|
||||
var ok int
|
||||
if conn.QueryRowContext(cctx, q).Scan(&ok) == nil && ok == 1 {
|
||||
found = append(found, n)
|
||||
}
|
||||
}
|
||||
return found, nil
|
||||
}
|
||||
|
||||
func depoDiscoverPG(ctx context.Context, s FancyMSSQLSettings) ([]string, error) {
|
||||
conn, err := fancyOpen(s, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
rows, err := conn.QueryContext(ctx, `SELECT datname FROM pg_database
|
||||
WHERE datallowconn AND NOT datistemplate AND has_database_privilege(datname, 'CONNECT') ORDER BY datname`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("список баз сервера: %w", err)
|
||||
}
|
||||
var names []string
|
||||
for rows.Next() {
|
||||
var n string
|
||||
if rows.Scan(&n) == nil && validDBName(n) && n != "postgres" && len(names) < depoDiscoverMax {
|
||||
names = append(names, n)
|
||||
}
|
||||
}
|
||||
rows.Close()
|
||||
var found []string
|
||||
for _, n := range names {
|
||||
db, err := fancyOpen(s, n)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var cnt int
|
||||
err = db.QueryRowContext(ctx, `SELECT count(DISTINCT lower(table_name)) FROM information_schema.tables
|
||||
WHERE lower(table_name) = ANY($1::text[])`, "{"+strings.ToLower(strings.Join(depoMarkerTables, ","))+"}").Scan(&cnt)
|
||||
db.Close()
|
||||
if err == nil && cnt == len(depoMarkerTables) {
|
||||
found = append(found, n)
|
||||
}
|
||||
}
|
||||
return found, nil
|
||||
}
|
||||
@@ -83,7 +83,7 @@ func letterFor(s Settings, key string) (Letter, bool) {
|
||||
}
|
||||
return Letter{Key: key, Title: "Письмо администратору базы DEPO", To: "Администратор СУБД учёта DEPO",
|
||||
Subject: "Bridge&Join: подготовка базы DEPO на сервере " + orBracket(f.Server, "сервер DEPO"),
|
||||
Body: fmt.Sprintf("Коллеги, добрый день!\n\nДля подключения системы Bridge&Join к учёту DEPO нужно на сервере %s выполнить один скрипт %s %s (файл прилагается — он скачивается в мастере настройки bj). Скрипт:\n— в начале — имя базы DEPO (пусто — скрипт найдёт её сам по учётным таблицам) и пароль логина bj: их можно поправить;\n— создаёт логин bj: права — чтение учётных таблиц DEPO и запись только в таблицы обмена FTV_M2M*;\n— создаёт таблицы обмена FTV_M2M*; учётные данные он не меняет;\n%s\nСкрипт можно запускать повторно. В нём пароль логина bj — после выполнения файл удалите.\n\nТакже нужно разрешить подключение к серверу базы с сервера Bridge&Join (адрес %s). Процедуры обработки обмена FTV_M2M_* поставляются отдельно.%s",
|
||||
Body: fmt.Sprintf("Коллеги, добрый день!\n\nДля подключения системы Bridge&Join к учёту DEPO нужно на сервере %s выполнить один скрипт %s %s (файл прилагается — он скачивается в мастере настройки bj). Скрипт:\n— в начале — имя базы DEPO и пароль логина bj: проверьте их; скрипт сначала убедится, что это база DEPO (есть учётные таблицы OD_FACES и OD_RESTS), иначе остановится, ничего не меняя;\n— создаёт логин bj: права — чтение учётных таблиц DEPO и запись только в таблицы обмена FTV_M2M*;\n— создаёт таблицы обмена FTV_M2M*; учётные данные он не меняет;\n%s\nСкрипт можно запускать повторно. В нём пароль логина bj — после выполнения файл удалите.\n\nТакже нужно разрешить подключение к серверу базы с сервера Bridge&Join (адрес %s). Процедуры обработки обмена FTV_M2M_* поставляются отдельно.%s",
|
||||
orBracket(f.Server, "сервер DEPO"), depoSetupFileName(f), run, what, orBracket(ip, "IP-адрес сервера bj"), sign)}, true
|
||||
}
|
||||
return Letter{}, false
|
||||
|
||||
@@ -347,17 +347,18 @@ func TestFromLocalBundle(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Один скрипт подготовки базы DEPO: сам ищет базу, создаёт логин, таблицы, права и (по выбору) задание.
|
||||
// Один скрипт подготовки базы DEPO: в названной базе (сам не ищет, но проверяет, что это DEPO) создаёт логин,
|
||||
// таблицы, права и (по выбору) задание.
|
||||
func TestDepoSetupScript(t *testing.T) {
|
||||
f := FancyMSSQLSettings{}
|
||||
s := depoSetupScript(f, "", "bj_user", "Pass1word", true)
|
||||
for _, want := range []string{"DECLARE @db sysname = N'';", "DECLARE @pass nvarchar(128) = N'Pass1word';", "OD_FACES", "CREATE LOGIN [bj_user] WITH PASSWORD = N''' + REPLACE(@pass, N'''', N'''''')", "FTV_M2M_EVENTS", "db_datareader", "sp_add_job @job_name = N'bj2depo'", "@database_name = @db", "SQLAgentReaderRole"} {
|
||||
s := depoSetupScript(f, "DEPO_DATA", "bj_user", "Pass1word", true)
|
||||
for _, want := range []string{"DECLARE @db sysname = N'DEPO_DATA';", "RAISERROR(N'Базы %s на этом сервере нет.", "OBJECT_ID(QUOTENAME(@db) + N'.dbo.OD_FACES', N'U') IS NULL", "DECLARE @pass nvarchar(128) = N'Pass1word';", "OD_FACES", "CREATE LOGIN [bj_user] WITH PASSWORD = N''' + REPLACE(@pass, N'''', N'''''')", "FTV_M2M_EVENTS", "db_datareader", "sp_add_job @job_name = N'bj2depo'", "@database_name = @db", "SQLAgentReaderRole"} {
|
||||
if !strings.Contains(s, want) {
|
||||
t.Fatalf("в скрипте нет %q", want)
|
||||
}
|
||||
}
|
||||
if strings.Contains(s, "BJ_DEPO_DATA") || strings.Contains(s, "\nGO") {
|
||||
t.Fatal("в скрипте — имя нашей базы или GO (переменные @db между пакетами не живут)")
|
||||
if strings.Contains(s, "BJ_DEPO_DATA") || strings.Contains(s, "\nGO") || strings.Contains(s, "sys.databases") || strings.Contains(s, "%!") {
|
||||
t.Fatal("в скрипте — имя нашей базы, GO (переменные @db между пакетами не живут), поиск базы по серверу или сбой подстановки")
|
||||
}
|
||||
// каждая строка EXEC(@in + N'…') — закрытый литерал: чётное число кавычек
|
||||
for _, line := range strings.Split(s, "\n") {
|
||||
@@ -370,7 +371,7 @@ func TestDepoSetupScript(t *testing.T) {
|
||||
t.Fatal("обработка силами bj: без задания, с правом на процедуру и указанной базой")
|
||||
}
|
||||
pg := depoSetupScript(FancyMSSQLSettings{DBType: "postgres"}, "depo", "bj_user", "p'w", false)
|
||||
if !strings.Contains(pg, `\c "depo"`) || !strings.Contains(pg, "PASSWORD 'p''w'") {
|
||||
if !strings.Contains(pg, `\c "depo"`) || !strings.Contains(pg, "PASSWORD 'p''w'") || !strings.Contains(pg, "RAISE EXCEPTION 'В базе % нет учётных таблиц DEPO") {
|
||||
t.Fatal("PostgreSQL: подключение к базе и экранирование пароля")
|
||||
}
|
||||
if p := depoGenPassword(); len(p) != 24 || !strings.ContainsAny(p, "ABCDEFGHJKLMNPQRSTUVWXYZ") || !strings.ContainsAny(p, "23456789") {
|
||||
@@ -443,7 +444,7 @@ func TestDumpOnboardingPages(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = os.WriteFile(filepath.Join(dir, "prepare.html"), []byte(b.String()), 0o644)
|
||||
_ = os.WriteFile(filepath.Join(dir, "depo-script.sql"), []byte(depoSetupScript(FancyMSSQLSettings{}, "", "bj_user", "Xk7pQ2mN9vR4tL8wZ3hB6cJd", true)), 0o644)
|
||||
_ = os.WriteFile(filepath.Join(dir, "depo-script.sql"), []byte(depoSetupScript(FancyMSSQLSettings{}, "DEPO_DATA", "bj_user", "Xk7pQ2mN9vR4tL8wZ3hB6cJd", true)), 0o644)
|
||||
}
|
||||
|
||||
// Первая страница (администратор) — для снимка в TestDumpOnboardingPages.
|
||||
@@ -503,9 +504,12 @@ func TestDepoScriptView(t *testing.T) {
|
||||
if f := rc.Snapshot().Fancy; f.Server != "10.0.0.5" || f.Password != "Secret123x" {
|
||||
t.Fatalf("без адреса сервера настройки не должны меняться: %q", f.Server)
|
||||
}
|
||||
// PostgreSQL — свой скрипт; без имени базы — понятная причина вместо скрипта
|
||||
form.Set("db_type", "postgres")
|
||||
// имя базы обязательно (bj по серверу не ищет) — без него вместо скрипта понятная причина
|
||||
form.Set("depo_database", "")
|
||||
if w := post(form); w.Code != http.StatusBadRequest || !strings.Contains(w.Body.String(), "имя базы") {
|
||||
t.Fatalf("SQL Server без базы: %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
form.Set("db_type", "postgres")
|
||||
if w := post(form); w.Code != http.StatusBadRequest || !strings.Contains(w.Body.String(), "имя базы") {
|
||||
t.Fatalf("PostgreSQL без базы: %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
@@ -174,7 +174,6 @@ type WizardData struct {
|
||||
DepoJobDef string // способ обработки по умолчанию для мастера
|
||||
Fancy *SetupData // шаг 3: общий блок «Обмен Fancy» (тот же, что в настройках)
|
||||
Sub string // экран шага 3 (учёт DEPO): intro|conn|tables|job|done
|
||||
DepoCands []string // шаг 3: найдено несколько баз DEPO — выбор
|
||||
DepoUser string // шаг 3, «скрипт администратору»: логин bj, который создаст скрипт
|
||||
DepoPass string // и его пароль (сохранённый или придуманный сейчас — сохраняется со скриптом)
|
||||
DepoMode string // выбранный способ подготовки базы: auto|script|existing
|
||||
@@ -324,7 +323,6 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
if step == 3 {
|
||||
d.Sub = depoWizardSub(r.URL.Query().Get("sub"))
|
||||
d.DepoCands = depoCands(r.URL.Query().Get("cands"))
|
||||
d.DepoUser, d.DepoPass = depoSetupLogin(s.Fancy), s.Fancy.Password
|
||||
if s.Fancy.User != d.DepoUser || d.DepoPass == "" {
|
||||
d.DepoPass = depoGenPassword()
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
.dq-sql { margin-top:14px; }
|
||||
.dq-sql-head { display:flex; align-items:center; gap:8px; margin:0 0 6px; justify-content:flex-start; }
|
||||
.dq-sql-st { flex:1; font-size:12.5px; color:var(--muted); }
|
||||
.dq-sql-st.bad { color:var(--warn); }
|
||||
.dq-sql pre:empty::before { content:attr(data-msg); color:var(--muted); font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif; font-size:13px; }
|
||||
.dq-sql pre { max-height:380px; min-height:140px; font-size:12px; margin:0; transition:opacity .2s; }
|
||||
.dq-sql pre.dq-busy { opacity:.5; }
|
||||
.dq.is-pg [data-ms], .dq:not(.is-pg) [data-pg] { display:none !important; }
|
||||
@@ -179,10 +179,9 @@
|
||||
<div class="dq-grid dq-srv">
|
||||
<label>Адрес сервера<input type="text" name="server" value="{{.Settings.Fancy.Server}}" placeholder="IP-адрес или имя\экземпляр" required autofocus spellcheck="false"></label>
|
||||
<label>Порт<input type="text" name="port" value="{{if .Settings.Fancy.Port}}{{.Settings.Fancy.Port}}{{else if $pg}}5432{{else}}1433{{end}}" inputmode="numeric"></label>
|
||||
{{if .DepoCands}}<label>Имя базы <span class="dq-opt">— выберите</span><select name="depo_database">{{range .DepoCands}}<option>{{.}}</option>{{end}}</select></label>
|
||||
{{else}}<label>Имя базы <span class="dq-opt" data-ms>— необязательно</span><input type="text" name="depo_database" value="{{.Settings.Fancy.DepoDatabase}}" placeholder="bj найдёт сам" pattern="[A-Za-z][A-Za-z0-9_]*" title="латинские буквы, цифры и _, начинается с буквы" spellcheck="false" {{if $pg}}required{{end}}></label>{{end}}
|
||||
<label>Имя базы<input type="text" name="depo_database" value="{{.Settings.Fancy.DepoDatabase}}" placeholder="как называется на сервере" required pattern="[A-Za-z][A-Za-z0-9_]*" title="латинские буквы, цифры и _, начинается с буквы" spellcheck="false"></label>
|
||||
</div>
|
||||
<p class="dq-note">{{if .DepoCands}}Логину bj видны несколько баз DEPO — выберите нужную.{{else}}<span data-ms>Не знаете имя базы — оставьте пустым: bj найдёт её сам по учётным таблицам OD_FACES и OD_RESTS.</span><span data-pg>Для PostgreSQL имя базы нужно указать.</span>{{end}}</p>
|
||||
<p class="dq-note">Точное имя базы учёта DEPO — bj будет работать только с ней. Перед подготовкой bj проверит, что в ней есть учётные таблицы DEPO, и в чужую базу ничего не запишет.</p>
|
||||
</section>
|
||||
|
||||
<section class="dq-s">
|
||||
@@ -221,9 +220,9 @@
|
||||
<div class="dq-sql-head copy-bar">
|
||||
<span class="dq-sql-st" id="wiz-depo-st">Скрипт собирается из ответов выше и обновляется сам.</span>
|
||||
<button type="button" id="wiz-depo-copy">Скопировать</button>
|
||||
<button type="submit" formaction="/admin/setup/wizard-depo/script.sql" formnovalidate>Скачать файлом</button>
|
||||
<button type="submit" formaction="/admin/setup/wizard-depo/script.sql">Скачать файлом</button>
|
||||
</div>
|
||||
<pre class="copyable" id="wiz-depo-sql"></pre>
|
||||
<pre class="copyable" id="wiz-depo-sql" data-msg="Впишите имя базы DEPO — скрипт появится здесь."></pre>
|
||||
</div>
|
||||
<p class="dq-note"><span data-ms>Администратор выполняет скрипт в SSMS или sqlcmd под логином с правами sysadmin.</span><span data-pg>Администратор выполняет скрипт в psql суперпользователем (postgres).</span> Когда выполнит — нажмите «Проверить подключение».</p>
|
||||
{{template "letterCard" (letter "dba")}}
|
||||
@@ -240,13 +239,12 @@
|
||||
</section>
|
||||
</form>
|
||||
<script>(function(){
|
||||
var f=document.getElementById('wiz-depo'), pre=document.getElementById('wiz-depo-sql'), st=document.getElementById('wiz-depo-st');
|
||||
var f=document.getElementById('wiz-depo'), pre=document.getElementById('wiz-depo-sql');
|
||||
var timer=0, seq=0, last='';
|
||||
function sync(){ // СУБД меняет подсказки, порт и обязательность имени базы; способ — какую панель показать
|
||||
var pg=f.db_type.value==='postgres', m=f.mode.value, db=f.querySelector('input[name=depo_database]');
|
||||
var pg=f.db_type.value==='postgres', m=f.mode.value;
|
||||
f.classList.toggle('is-pg', pg);
|
||||
if(pg&&f.port.value==='1433') f.port.value='5432'; else if(!pg&&f.port.value==='5432') f.port.value='1433';
|
||||
if(db){ db.required=pg; db.placeholder=pg?'обязательно':'bj найдёт сам'; }
|
||||
f.admin_user.placeholder=pg?'postgres':'sa';
|
||||
['auto','script','existing'].forEach(function(k){ document.getElementById('wiz-depo-'+k).hidden=m!==k; });
|
||||
}
|
||||
@@ -259,9 +257,9 @@
|
||||
fetch('/admin/setup/wizard-depo/script.sql',{method:'POST',body:body,headers:{'Content-Type':'application/x-www-form-urlencoded'},cache:'no-store'})
|
||||
.then(function(r){return r.text().then(function(t){return {ok:r.ok,t:t}})})
|
||||
.then(function(x){ if(my!==seq) return; pre.classList.remove('dq-busy');
|
||||
if(x.ok){ pre.textContent=x.t; st.textContent='Скрипт собирается из ответов выше и обновляется сам.'; st.classList.remove('bad'); }
|
||||
else { st.textContent=x.t.trim().replace(/^./,function(c){return c.toUpperCase()})+' — скрипт обновится, когда поправите.'; st.classList.add('bad'); } })
|
||||
.catch(function(){ if(my!==seq) return; last=''; pre.classList.remove('dq-busy'); st.textContent='Не получилось собрать скрипт — обновите страницу.'; st.classList.add('bad'); });
|
||||
if(x.ok){ pre.textContent=x.t; }
|
||||
else { pre.textContent=''; pre.dataset.msg=x.t.trim().replace(/^./,function(c){return c.toUpperCase()})+' — скрипт появится здесь.'; } })
|
||||
.catch(function(){ if(my!==seq) return; last=''; pre.classList.remove('dq-busy'); pre.textContent=''; pre.dataset.msg='Не получилось собрать скрипт — обновите страницу.'; });
|
||||
}
|
||||
function changed(){ sync(); clearTimeout(timer); timer=setTimeout(load, 350); }
|
||||
f.addEventListener('input', changed); f.addEventListener('change', changed);
|
||||
|
||||
@@ -49,15 +49,15 @@ func wizDepoGo(w http.ResponseWriter, r *http.Request, sub, msg string, ok bool)
|
||||
}
|
||||
|
||||
// wizardDepoConn — POST /admin/setup/wizard-depo/conn: сохранить подключение и сразу проверить его. Имя базы
|
||||
// DEPO клиент не вводит: bj находит её на сервере сам (depoDiscover); вручную — только в «Дополнительно».
|
||||
// DEPO клиент задаёт сам — bj по серверу не ищет, только проверяет, что это база DEPO.
|
||||
func (h *setupHandlers) wizardDepoConn(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || !requireDepo(w, r) {
|
||||
return
|
||||
}
|
||||
s := h.fancyFromForm(r)
|
||||
manual := strings.TrimSpace(r.FormValue("depo_database"))
|
||||
if manual != "" && !validDBName(manual) {
|
||||
wizDepoGo(w, r, "conn", "имя базы DEPO — только латинские буквы, цифры и _, начинается с буквы", false)
|
||||
if !validDBName(manual) {
|
||||
wizDepoGo(w, r, "conn", "укажите имя базы DEPO — латинские буквы, цифры и _, начинается с буквы", false)
|
||||
return
|
||||
}
|
||||
if s.Server == "" || s.User == "" {
|
||||
@@ -74,23 +74,10 @@ func (h *setupHandlers) wizardDepoConn(w http.ResponseWriter, r *http.Request) {
|
||||
wizDepoGo(w, r, "conn", "к серверу подключиться не удалось — "+err.Error()+". Проверьте адрес, порт, логин и пароль.", false)
|
||||
return
|
||||
}
|
||||
db, how := manual, "указана вручную"
|
||||
if db == "" {
|
||||
found, err := depoDiscover(ctx, s)
|
||||
switch {
|
||||
case err != nil:
|
||||
wizDepoGo(w, r, "conn", "сервер отвечает, но список баз получить не удалось — "+err.Error()+". Укажите базу DEPO вручную (ниже, «Дополнительно»).", false)
|
||||
db := manual
|
||||
if err := depoCheckIsDepo(ctx, s, db); err != nil {
|
||||
wizDepoGo(w, r, "conn", err.Error(), false)
|
||||
return
|
||||
case len(found) == 0:
|
||||
wizDepoGo(w, r, "conn", "на сервере не найдена база учёта DEPO (Fancy), доступная логину "+s.User+
|
||||
": нет базы с таблицами "+strings.Join(depoMarkerTables, " и ")+". Проверьте, что логину выдан доступ к базе DEPO, или укажите её вручную («Дополнительно»).", false)
|
||||
return
|
||||
case len(found) > 1:
|
||||
http.Redirect(w, r, "/admin/wizard?step=3&sub=conn&cands="+url.QueryEscape(strings.Join(found, ","))+
|
||||
"&flash="+url.QueryEscape("На сервере несколько баз DEPO — выберите, с какой работает bj."), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
db, how = found[0], "найдена на сервере"
|
||||
}
|
||||
if db != s.DepoDatabase {
|
||||
s.DepoTablesCheck, s.DepoTablesCheckOK = "", false
|
||||
@@ -100,22 +87,7 @@ func (h *setupHandlers) wizardDepoConn(w http.ResponseWriter, r *http.Request) {
|
||||
wizDepoGo(w, r, "conn", "не сохранилось: "+err.Error(), false)
|
||||
return
|
||||
}
|
||||
if _, err := fancyPing(ctx, s, db); err != nil {
|
||||
wizDepoGo(w, r, "conn", "сервер отвечает, но база "+db+" недоступна этим логином — "+err.Error(), false)
|
||||
return
|
||||
}
|
||||
wizDepoGo(w, r, "tables", "подключение к "+s.Server+" работает; база DEPO — "+db+" ("+how+")", true)
|
||||
}
|
||||
|
||||
// depoCands — базы DEPO для выбора на экране «Подключение» (из адреса после поиска на сервере).
|
||||
func depoCands(v string) []string {
|
||||
var out []string
|
||||
for _, n := range strings.Split(v, ",") {
|
||||
if n = strings.TrimSpace(n); validDBName(n) {
|
||||
out = append(out, n)
|
||||
}
|
||||
}
|
||||
return out
|
||||
wizDepoGo(w, r, "tables", "подключение к "+s.Server+" работает; база DEPO — "+db, true)
|
||||
}
|
||||
|
||||
// wizardDepoTables — POST /admin/setup/wizard-depo/tables: создать таблицы (bj) или проверить созданные
|
||||
@@ -236,8 +208,8 @@ func (h *setupHandlers) wizardDepoRecheck(w http.ResponseWriter, r *http.Request
|
||||
wizDepoGo(w, r, "done", "всё перепроверено", true)
|
||||
}
|
||||
|
||||
// depoSetupForm — сервер DEPO и способ обработки из формы экрана «Подготовка базы» (логин bj не трогается;
|
||||
// пустой адрес сервера не ошибка — скрипту он не нужен, подключению проверяют вызывающие).
|
||||
// depoSetupForm — сервер DEPO, имя базы (обязательно) и способ обработки из формы экрана «Подготовка базы»
|
||||
// (логин bj не трогается; пустой адрес сервера не ошибка — скрипту он не нужен, подключению проверяют вызывающие).
|
||||
func (h *setupHandlers) depoSetupForm(r *http.Request) (FancyMSSQLSettings, string, bool, string) {
|
||||
s := h.rc.Snapshot().Fancy
|
||||
dbType := "mssql"
|
||||
@@ -263,15 +235,12 @@ func (h *setupHandlers) depoSetupForm(r *http.Request) (FancyMSSQLSettings, stri
|
||||
s.DepoJobMode = "sql-auto"
|
||||
}
|
||||
db := strings.TrimSpace(r.FormValue("depo_database"))
|
||||
if db != "" && !validDBName(db) {
|
||||
if db == "" { // bj по серверу базу не ищет: с какой базой работать, клиент говорит сам
|
||||
return s, "", agent, "укажите имя базы DEPO — как она называется на сервере"
|
||||
}
|
||||
if !validDBName(db) {
|
||||
return s, "", agent, "имя базы DEPO — только латинские буквы, цифры и _, начинается с буквы"
|
||||
}
|
||||
if db == "" {
|
||||
db = s.DepoDatabase
|
||||
}
|
||||
if dbType == "postgres" && db == "" {
|
||||
return s, "", agent, "для PostgreSQL укажите имя базы DEPO"
|
||||
}
|
||||
return s, db, agent, ""
|
||||
}
|
||||
|
||||
@@ -374,13 +343,12 @@ func (h *setupHandlers) wizardDepoSetup(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
depoEnsureLogin(&s)
|
||||
found, err := depoSetupRun(ctx, s, db, au, r.FormValue("admin_password"), s.User, s.Password, agent)
|
||||
if err != nil {
|
||||
if err := depoSetupRun(ctx, s, db, au, r.FormValue("admin_password"), s.User, s.Password, agent); err != nil {
|
||||
wizDepoGo(w, r, "setup", "база не подготовлена — "+err.Error(), false)
|
||||
return
|
||||
}
|
||||
s.DepoDatabase, s.DepoTablesMode = found, "auto"
|
||||
note = "база " + found + " подготовлена: логин " + s.User + ", таблицы обмена, права"
|
||||
s.DepoTablesMode = "auto"
|
||||
note = "база " + db + " подготовлена: логин " + s.User + ", таблицы обмена, права"
|
||||
if agent {
|
||||
note += ", задание " + depoJobName
|
||||
}
|
||||
@@ -405,9 +373,7 @@ func (h *setupHandlers) wizardDepoSetup(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
}
|
||||
if db != "" {
|
||||
s.DepoDatabase = db
|
||||
}
|
||||
if err := h.rc.UpdateFancy(s); err != nil {
|
||||
wizDepoGo(w, r, "setup", "не сохранилось: "+err.Error(), false)
|
||||
return
|
||||
@@ -416,19 +382,9 @@ func (h *setupHandlers) wizardDepoSetup(w http.ResponseWriter, r *http.Request)
|
||||
wizDepoGo(w, r, "setup", note+"войти логином "+s.User+" не удалось — "+quikErrorDetail(err)+". Выполнен ли скрипт на сервере "+s.Server+"?", false)
|
||||
return
|
||||
}
|
||||
if s.DepoDatabase == "" { // база не указана и скрипт её нашёл сам — найти и нам
|
||||
found, err := depoDiscover(ctx, s)
|
||||
switch {
|
||||
case err != nil || len(found) == 0:
|
||||
wizDepoGo(w, r, "setup", "логину "+s.User+" не видна ни одна база DEPO — выполнен ли скрипт? Можно вписать имя базы в поле «Имя базы»", false)
|
||||
if err := depoCheckIsDepo(ctx, s, db); err != nil { // имя базы задал клиент — та ли это база
|
||||
wizDepoGo(w, r, "setup", note+err.Error(), false)
|
||||
return
|
||||
case len(found) > 1:
|
||||
http.Redirect(w, r, "/admin/wizard?step=3&sub=setup&cands="+url.QueryEscape(strings.Join(found, ","))+
|
||||
"&flash="+url.QueryEscape("Логину bj видны несколько баз DEPO — выберите нужную и проверьте ещё раз."), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
s.DepoDatabase = found[0]
|
||||
_ = h.rc.UpdateFancy(s)
|
||||
}
|
||||
msg, ok := depoTablesCheck(ctx, s)
|
||||
h.saveDepoTablesCheck(msg, ok)
|
||||
|
||||
Reference in New Issue
Block a user