diff --git a/internal/lkgateway/dbhist2_live_test.go b/internal/lkgateway/dbhist2_live_test.go new file mode 100644 index 0000000..962bd87 --- /dev/null +++ b/internal/lkgateway/dbhist2_live_test.go @@ -0,0 +1,45 @@ +//go:build live + +package lkgateway + +import ( + "context" + "fmt" + "testing" +) + +// Только чтение: что сейчас в BJ_DEPO_DATA — таблицы обмена и тестовые остатки. +func TestDepoNowLive(t *testing.T) { + rc, err := NewRuntimeConfig("/var/lib/bj/.bj/setup.json") + if err != nil { + t.Fatal(err) + } + fs := rc.Snapshot().Fancy + db, err := fancyOpen(fs, fs.DepoDB()) + if err != nil { + t.Fatal(err) + } + defer db.Close() + ctx := context.Background() + for _, tb := range []string{"FTV_M2M", "FTV_M2M_LINES", "FTV_M2M_EVENTS", "FTV_M2M_HANDBOOK"} { + var n int + err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM dbo."+tb).Scan(&n) + fmt.Printf("%-18s строк %d %v\n", tb, n, err) + } + rows, err := db.QueryContext(ctx, `SELECT c.NUMBER, v.ISIN, CAST(rst.K AS float) FROM OD_RESTS rst JOIN OD_CLIENTS c ON c.ID=rst.REG_1 JOIN OD_VALUES v ON v.ID=rst.VALUE_ID + WHERE c.NUMBER IN ('ВТНКЦ00002000','ВТНКЦ00000816') AND v.ISIN='RU0009029540'`) + if err == nil { + for rows.Next() { + var a, i string + var k float64 + _ = rows.Scan(&a, &i, &k) + fmt.Println("остаток SBER:", a, k) + } + } + var mx string + _ = db.QueryRowContext(ctx, "SELECT ISNULL(CONVERT(varchar(19),MAX(D_DATE),120),'') FROM OD_DOCS").Scan(&mx) + fmt.Println("последний документ OD_DOCS:", mx) + var jobs int + _ = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM sys.objects WHERE name LIKE '%M2M%' AND type IN ('P','TR','U','V')").Scan(&jobs) + fmt.Println("объектов с M2M в имени (таблицы/процедуры/триггеры):", jobs) +} diff --git a/internal/lkgateway/dbhist_live_test.go b/internal/lkgateway/dbhist_live_test.go new file mode 100644 index 0000000..2677407 --- /dev/null +++ b/internal/lkgateway/dbhist_live_test.go @@ -0,0 +1,60 @@ +//go:build live + +package lkgateway + +import ( + "context" + "database/sql" + "fmt" + "testing" +) + +// Только чтение: базы BJ_DEPO*, режим восстановления, резервные копии и восстановления (msdb). +func TestDBHistoryLive(t *testing.T) { + rc, err := NewRuntimeConfig("/var/lib/bj/.bj/setup.json") + if err != nil { + t.Fatal(err) + } + fs := rc.Snapshot().Fancy + fmt.Println("база DEPO в настройках bj:", fs.DepoDB()) + db, err := fancyOpen(fs, "master") + if err != nil { + t.Fatal(err) + } + defer db.Close() + ctx := context.Background() + dump := func(title, q string) { + fmt.Println("==", title) + rows, err := db.QueryContext(ctx, q) + if err != nil { + fmt.Println(" ошибка:", err) + return + } + cols, _ := rows.Columns() + n := 0 + for rows.Next() { + vals := make([]sql.NullString, len(cols)) + ptrs := make([]any, len(cols)) + for i := range vals { + ptrs[i] = &vals[i] + } + _ = rows.Scan(ptrs...) + line := "" + for i, c := range cols { + line += c + "=" + vals[i].String + " | " + } + fmt.Println(" ", line) + n++ + } + if n == 0 { + fmt.Println(" (пусто)") + } + } + dump("базы", `SELECT name, recovery_model_desc, state_desc, CONVERT(varchar(19),create_date,120) created FROM sys.databases WHERE name LIKE 'BJ%' OR name LIKE '%DEPO%' ORDER BY name`) + dump("резервные копии (последние 30)", `SELECT TOP 30 b.database_name, b.type, CONVERT(varchar(19),b.backup_start_date,120) start, CONVERT(varchar(19),b.backup_finish_date,120) finish, m.physical_device_name + FROM msdb.dbo.backupset b JOIN msdb.dbo.backupmediafamily m ON m.media_set_id=b.media_set_id + WHERE b.database_name LIKE 'BJ%' ORDER BY b.backup_finish_date DESC`) + dump("восстановления (последние 20)", `SELECT TOP 20 r.destination_database_name, CONVERT(varchar(19),r.restore_date,120) restored, r.restore_type, r.user_name, b.database_name src_db, CONVERT(varchar(19),b.backup_finish_date,120) src_backup, m.physical_device_name + FROM msdb.dbo.restorehistory r JOIN msdb.dbo.backupset b ON b.backup_set_id=r.backup_set_id JOIN msdb.dbo.backupmediafamily m ON m.media_set_id=b.media_set_id + WHERE r.destination_database_name LIKE 'BJ%' ORDER BY r.restore_date DESC`) +} diff --git a/internal/lkgateway/depologin.go b/internal/lkgateway/depologin.go new file mode 100644 index 0000000..fb83a56 --- /dev/null +++ b/internal/lkgateway/depologin.go @@ -0,0 +1,102 @@ +package lkgateway + +// Логин bj в базе DEPO (05.10.2026): на шаге «Подключение» мастера — что нужно и скрипт для администратора +// СУБД. Скрипт создаёт логин (SQL Server) / роль (PostgreSQL) и даёт чтение учётных таблиц DEPO; права на +// таблицы обмена FTV_M2M* выдаёт скрипт таблиц на следующем шаге (или bj, если таблицы создаёт сам). + +import ( + "fmt" + "net/http" + "strings" +) + +// depoLoginScript — скрипт создания логина bj. pg — PostgreSQL; db — база DEPO; login — имя логина. +func depoLoginScript(pg bool, db, login string) string { + if !validDBName(db) { + db = depoDefaultDatabase + } + if !depoLoginRe.MatchString(login) { + login = "bj_user" + } + var b strings.Builder + if pg { + fmt.Fprintf(&b, `-- Bridge&Join: роль (логин) для bj в базе DEPO. PostgreSQL 10 и новее. +-- Выполнить суперпользователем или владельцем базы, например: +-- psql -h <сервер> -U postgres -f bj-depo-login-postgres.sql +-- Пароль замените на свой и передайте его тому, кто настраивает bj (в мастере, шаг «Подключение»). +-- Права: только подключение и чтение учётных таблиц DEPO. Запись — только в таблицы обмена FTV_M2M*, +-- её выдаёт скрипт таблиц на следующем шаге мастера. + +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '%[2]s') THEN + CREATE ROLE "%[2]s" LOGIN PASSWORD 'ЗАМЕНИТЕ_НА_ПАРОЛЬ'; + END IF; +END $$; + +GRANT CONNECT ON DATABASE "%[1]s" TO "%[2]s"; + +\c "%[1]s" +GRANT USAGE ON SCHEMA public TO "%[2]s"; +GRANT SELECT ON ALL TABLES IN SCHEMA public TO "%[2]s"; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO "%[2]s"; +`, db, login) + return b.String() + } + fmt.Fprintf(&b, `-- Bridge&Join: логин для bj в базе DEPO. Microsoft SQL Server. +-- Выполнить в SSMS или sqlcmd под логином с правами sysadmin (или securityadmin + db_owner в базе %[1]s). +-- Пароль замените на свой и передайте его тому, кто настраивает bj (в мастере, шаг «Подключение»). +-- Права: только чтение учётных таблиц DEPO (db_datareader). Запись — только в таблицы обмена FTV_M2M*, +-- её выдаёт скрипт таблиц на следующем шаге мастера. Скрипт можно запускать повторно. + +USE [master]; +GO + +IF SUSER_ID(N'%[2]s') IS NULL + CREATE LOGIN [%[2]s] WITH PASSWORD = N'ЗАМЕНИТЕ_НА_ПАРОЛЬ', CHECK_POLICY = ON, DEFAULT_DATABASE = [%[1]s]; +GO + +USE [%[1]s]; +GO + +IF USER_ID(N'%[2]s') IS NULL CREATE USER [%[2]s] FOR LOGIN [%[2]s]; +GO + +IF IS_ROLEMEMBER(N'db_datareader', N'%[2]s') = 0 ALTER ROLE db_datareader ADD MEMBER [%[2]s]; +GO +`, db, login) + return b.String() +} + +// fancyDepoLoginScript — GET /admin/setup/fancy/depo-login/script.sql?type=mssql|postgres&db=…&login=… +// Без параметров — по сохранённым настройкам; download=0 — показать текстом (предпросмотр в мастере). +func (h *setupHandlers) fancyDepoLoginScript(w http.ResponseWriter, r *http.Request) { + s := h.rc.Snapshot().Fancy + q := r.URL.Query() + pg := s.IsPostgres() + if t := q.Get("type"); t != "" { + pg = t == "postgres" + } + db := strings.TrimSpace(q.Get("db")) + if db == "" { + db = s.DepoDB() + } + login := strings.TrimSpace(q.Get("login")) + if login == "" { + login = pickStr(strings.TrimSpace(s.User), "bj_user") + } + body := depoLoginScript(pg, db, login) + name := "bj-depo-login-mssql.sql" + if pg { + name = "bj-depo-login-postgres.sql" + } else { + body = "\uFEFF" + body // SSMS без BOM читает кириллицу в комментариях как ANSI + } + if q.Get("download") == "0" { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + _, _ = w.Write([]byte(strings.TrimPrefix(body, "\uFEFF"))) + return + } + w.Header().Set("Content-Type", "application/sql; charset=utf-8") + w.Header().Set("Content-Disposition", `attachment; filename="`+name+`"`) + _, _ = w.Write([]byte(body)) +} diff --git a/internal/lkgateway/depologin_test.go b/internal/lkgateway/depologin_test.go new file mode 100644 index 0000000..9ecc94a --- /dev/null +++ b/internal/lkgateway/depologin_test.go @@ -0,0 +1,24 @@ +package lkgateway + +import ( + "strings" + "testing" +) + +func TestDepoLoginScript(t *testing.T) { + ms := depoLoginScript(false, "CLIENT_DEPO", "bj_svc") + for _, w := range []string{"CREATE LOGIN [bj_svc]", "USE [CLIENT_DEPO]", "CREATE USER [bj_svc] FOR LOGIN [bj_svc]", "db_datareader"} { + if !strings.Contains(ms, w) { + t.Fatalf("MS SQL: нет %q", w) + } + } + pg := depoLoginScript(true, "depo", "bj_svc") + for _, w := range []string{`CREATE ROLE "bj_svc" LOGIN`, `GRANT CONNECT ON DATABASE "depo"`, `GRANT SELECT ON ALL TABLES`} { + if !strings.Contains(pg, w) { + t.Fatalf("PostgreSQL: нет %q", w) + } + } + if s := depoLoginScript(false, "bad;db", "x'; DROP"); !strings.Contains(s, "[BJ_DEPO_DATA]") || !strings.Contains(s, "[bj_user]") { + t.Fatal("недопустимые имена — подставляются значения по умолчанию") + } +} diff --git a/internal/lkgateway/fancydb_handlers.go b/internal/lkgateway/fancydb_handlers.go index f63086c..21d53b9 100644 --- a/internal/lkgateway/fancydb_handlers.go +++ b/internal/lkgateway/fancydb_handlers.go @@ -24,6 +24,7 @@ func (h *setupHandlers) registerFancy(mux *http.ServeMux) { mux.HandleFunc("/admin/setup/fancy/depo-tables/create", h.createFancyDepoTables) mux.HandleFunc("/admin/setup/fancy/depo-tables/check", h.checkFancyDepoTables) mux.HandleFunc("/admin/setup/fancy/depo-tables/script.sql", h.fancyDepoTablesScript) + mux.HandleFunc("/admin/setup/fancy/depo-login/script.sql", h.fancyDepoLoginScript) mux.HandleFunc("/admin/fancy-queue", h.fancyQueue) mux.HandleFunc("/admin/fancy-queue/requeue", h.fancyQueueRequeue) } diff --git a/internal/lkgateway/web/templates/admin_wizard.html b/internal/lkgateway/web/templates/admin_wizard.html index 1b41bc9..b438a29 100644 --- a/internal/lkgateway/web/templates/admin_wizard.html +++ b/internal/lkgateway/web/templates/admin_wizard.html @@ -20,7 +20,8 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); } .where { font-size:12px; color:var(--accent); margin-left:8px; } .help-block { background:var(--accent-weak); background:var(--accent-weak); padding:10px 14px; margin:10px 0; font-size:13px; border-radius:0 8px 8px 0; } .help-block strong { color:var(--accent); } -.wiz-card { max-width:680px; margin:0 auto; } +.wiz-card { max-width:680px; margin:0 auto; min-width:0; } +.wiz-card * { min-width:0; }
bj нужен отдельный логин в базе DEPO. Его создаёт администратор СУБД. Права минимальные:
+FTV_M2M* — их права выдаются на следующем экране, вместе с созданием таблиц.Вариант 1 — администратор создаёт логин сам с этими правами. Вариант 2 — передайте ему скрипт: в нём уже подставлены тип СУБД, база и имя логина из полей выше, нужно только заменить пароль.
+ Скачать скрипт создания логина +…+
После выполнения скрипта впишите логин и пароль в поля выше и нажмите «Проверить и далее».
+