Files
Bridge-and-Join-s/internal/lkgateway/depo_setup.go
T
zuevav a9915bcc82 Учёт DEPO: ход подготовки по шагам, понятный блок «Логин bj», пауза после неудачного входа
- «Подготовить» / «Проверить подключение» показывают ход: подключение логином
  администратора, проверка, что это база DEPO, применение скрипта, вход логином bj,
  таблицы обмена, задание, процедуры, «Готово». Сервер отдаёт шаги строками JSON
  (stream=1), на ошибке — шаг с крестиком и причина, без перехода со страницы.
- Блоки «Администратор SQL — нужен один раз, не сохраняется» и «Логин bj — под ним
  bj будет работать»: bj сохраняет и использует ровно то, что вписано; «другой
  пароль»; на итоге шага — каким логином bj работает.
- Пауза 15 минут после неудачного входа в DEPO (логин/пароль отвергнуты) для
  фоновых служб: с неверным паролем они за минуты блокировали логин на сервере
  (CHECK_POLICY) у всех, кто им пользуется. Действия человека идут без паузы;
  новый пароль — сразу; системное событие в «Новостях».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
2026-10-06 15:46:25 +03:00

261 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package lkgateway
// Подготовка базы учёта DEPO одним действием (06.10.2026). Раньше это были три экрана мастера — логин bj,
// таблицы обмена, задание SQL Agent — и три скрипта. Теперь один скрипт делает всё:
// - работает с базой, которую назвал клиент (сам по серверу не ищет), и сначала проверяет, что в ней есть
// учётные таблицы DEPO OD_FACES и OD_RESTS — опечатка в имени не уведёт в чужую базу;
// - создаёт логин bj (пароль придумывает bj и запоминает) или обновляет его пароль;
// - создаёт таблицы обмена FTV_M2M* и выдаёт логину bj права: чтение учёта, запись в таблицы обмена;
// - по галочке «обработку запускает SQL Server Agent» — создаёт задание bj2depo, иначе обработку
// вызывает сам bj (тогда логину bj — право запускать процедуру обработки).
// Вариант «bj сделает сам» выполняет тот же скрипт логином администратора SQL (он не сохраняется), вариант
// «скрипт для администратора» отдаёт его файлом, вариант «логин уже есть» только проверяет.
import (
"context"
"crypto/rand"
"fmt"
"math/big"
"strings"
"time"
)
// depoSetupLogin — логин bj в DEPO (если в настройках уже есть допустимый — он).
func depoSetupLogin(f FancyMSSQLSettings) string {
if u := strings.TrimSpace(f.User); u != "" && depoLoginRe.MatchString(u) {
return u
}
return "bj_user"
}
// depoGenPassword — пароль логина bj: 24 знака, есть заглавные, строчные и цифры (политика паролей SQL Server).
func depoGenPassword() string {
const up, low, dig = "ABCDEFGHJKLMNPQRSTUVWXYZ", "abcdefghijkmnpqrstuvwxyz", "23456789"
all := up + low + dig
pick := func(set string) byte {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(set))))
return set[n.Int64()]
}
b := []byte{pick(up), pick(low), pick(dig)}
for len(b) < 24 {
b = append(b, pick(all))
}
for i := len(b) - 1; i > 0; i-- { // перемешать, чтобы обязательные знаки не стояли в начале
j, _ := rand.Int(rand.Reader, big.NewInt(int64(i+1)))
b[i], b[j.Int64()] = b[j.Int64()], b[i]
}
return string(b)
}
// sqlStr — строковый литерал T-SQL / PostgreSQL (одинарная кавычка удваивается).
func sqlStr(s string) string { return strings.ReplaceAll(s, "'", "''") }
// 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)
}
var b strings.Builder
job := "вызывает сам bj"
if agentJob {
job = "задание SQL Server Agent «" + depoJobName + "»"
}
fmt.Fprintf(&b, `-- Bridge&Join: подготовка базы учёта DEPO для bj. Microsoft SQL Server.
SET NOCOUNT ON;
-- ===== Проверьте перед запуском ==========================================
-- База DEPO — точное имя базы учёта на этом сервере.
DECLARE @db sysname = N'%[3]s';
-- Пароль логина bj (%[1]s). Меняете — впишите тот же пароль в bj, шаг «Учёт DEPO».
DECLARE @pass nvarchar(128) = N'%[4]s';
-- =========================================================================
-- Что делает: проверяет, что @db — база DEPO, создаёт логин bj и таблицы обмена FTV_M2M*, выдаёт права
-- (чтение учёта, запись только в таблицы обмена). Обработка обмена: %[2]s.
-- Учётные данные DEPO не меняет. Запускать в SSMS или sqlcmd под логином с правами sysadmin.
-- Можно запускать повторно. В скрипте пароль логина bj — после выполнения файл удалите.
IF @pass = N''
BEGIN
RAISERROR(N'Впишите пароль логина bj в @pass в начале скрипта и запустите снова.', 16, 1);
RETURN;
END
IF @db = N''
BEGIN
RAISERROR(N'Впишите имя базы DEPO в @db в начале скрипта и запустите снова.', 16, 1);
RETURN;
END
IF DB_ID(@db) IS NULL
BEGIN
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;
DECLARE @in nvarchar(300) = N'USE ' + QUOTENAME(@db) + N'; ';
-- 1. Логин bj (если такой логин на сервере уже есть — ему ставится пароль из @pass)
-- (EXEC(...) склеивает только строки и переменные — команда собирается в @sql заранее)
DECLARE @sql nvarchar(max);
IF SUSER_ID(N'%[1]s') IS NULL
SET @sql = N'CREATE LOGIN [%[1]s] WITH PASSWORD = N''' + REPLACE(@pass, N'''', N'''''') + N''', CHECK_POLICY = ON, DEFAULT_DATABASE = ' + QUOTENAME(@db);
ELSE
SET @sql = N'ALTER LOGIN [%[1]s] WITH PASSWORD = N''' + REPLACE(@pass, N'''', N'''''') + N'''';
EXEC(@sql);
-- 2. Таблицы обмена FTV_M2M* (только недостающие таблицы, колонки и индексы — данные не меняются)
`, login, job, sqlStr(db), sqlStr(pass))
for _, s := range depoTablesDDL(false) {
fmt.Fprintf(&b, "EXEC(@in + N'%s');\n", sqlStr(s))
}
b.WriteString("\n-- 3. Права логину bj в базе DEPO: чтение учёта (сверка переводов), запись в таблицы обмена\n")
for _, s := range depoTablesGrantSQL(false, login) {
fmt.Fprintf(&b, "EXEC(@in + N'%s');\n", sqlStr(s))
}
proc := depoJobProcName(f)
if agentJob {
fmt.Fprintf(&b, `
-- 4. Задание SQL Server Agent «%[1]s»: каждые %[3]d с вызывает %[2]s — записи bj становятся документами учёта
IF NOT EXISTS (SELECT 1 FROM msdb.dbo.sysjobs WHERE name = N'%[1]s')
BEGIN
EXEC msdb.dbo.sp_add_job @job_name = N'%[1]s', @enabled = 1, @description = N'bj2depo: обработка таблиц обмена M2M (Bridge&Join)';
EXEC msdb.dbo.sp_add_jobstep @job_name = N'%[1]s', @step_name = N'%[1]s', @subsystem = N'TSQL', @database_name = @db, @command = N'exec %[2]s;';
EXEC msdb.dbo.sp_add_jobschedule @job_name = N'%[1]s', @name = N'%[1]s', @enabled = 1, @freq_type = 4, @freq_interval = 1, @freq_subday_type = 2, @freq_subday_interval = %[3]d, @active_start_time = 0;
EXEC msdb.dbo.sp_add_jobserver @job_name = N'%[1]s';
PRINT N'Задание %[1]s создано.';
END
ELSE
PRINT N'Задание %[1]s уже есть — не изменялось.';
-- логину bj — чтение списка заданий и их истории: bj проверяет, что задание работает
EXEC(N'USE msdb; IF USER_ID(N''%[4]s'') IS NULL CREATE USER [%[4]s] FOR LOGIN [%[4]s]; ELSE IF EXISTS (SELECT 1 FROM sys.database_principals WHERE name = N''%[4]s'' AND sid <> SUSER_SID(N''%[4]s'')) ALTER USER [%[4]s] WITH LOGIN = [%[4]s]; IF IS_ROLEMEMBER(N''SQLAgentReaderRole'', N''%[4]s'') = 0 ALTER ROLE SQLAgentReaderRole ADD MEMBER [%[4]s];');
`, depoJobName, proc, depoJobEvery(f), login)
} else {
fmt.Fprintf(&b, `
-- 4. Обработку вызывает bj: логину bj — право запускать процедуру %[1]s
EXEC(@in + N'IF OBJECT_ID(N''%[1]s'', N''P'') IS NOT NULL GRANT EXECUTE ON %[1]s TO [%[2]s] ELSE PRINT N''Процедуры обработки ещё не установлены: после их установки выполните GRANT EXECUTE ON %[1]s TO [%[2]s]''');
`, proc, login)
}
b.WriteString("\nPRINT N'Готово: база DEPO подготовлена для bj.';\n")
return b.String()
}
// depoSetupScriptPG — то же для PostgreSQL (psql: роль, подключение к базе DEPO, таблицы, права).
func depoSetupScriptPG(f FancyMSSQLSettings, db, login, pass string) string {
var b strings.Builder
fmt.Fprintf(&b, `-- Bridge&Join: подготовка базы учёта DEPO для bj. PostgreSQL 10 и новее.
-- Запуск суперпользователем или владельцем базы: psql -h <сервер> -U postgres -f bj-depo-setup-postgres.sql
-- Создаёт роль bj (%[2]s), таблицы обмена FTV_M2M* и права (чтение учёта, запись только в таблицы обмена).
-- Обработку обмена вызывает сам bj. Учётные данные скрипт не меняет. Можно запускать повторно.
-- Скрипт содержит пароль роли bj — после выполнения файл удалите.
-- Проверьте перед запуском: база DEPO — "%[1]s", роль bj — "%[2]s", её пароль — в строках PASSWORD ниже
-- (меняете — впишите тот же пароль в bj, шаг «Учёт DEPO»).
\set ON_ERROR_STOP on
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '%[2]s') THEN
CREATE ROLE "%[2]s" LOGIN PASSWORD '%[3]s';
ELSE
ALTER ROLE "%[2]s" LOGIN PASSWORD '%[3]s';
END IF;
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")
}
for _, s := range depoTablesGrantSQL(true, login) {
b.WriteString(s + ";\n")
}
proc := depoJobProcName(f)
fmt.Fprintf(&b, "DO $$ BEGIN IF to_regproc('%[1]s') IS NOT NULL THEN EXECUTE 'GRANT EXECUTE ON FUNCTION %[1]s TO \"%[2]s\"'; END IF; END $$;\n", strings.ToLower(proc), login)
return b.String()
}
// depoSetupFileName — имя файла скрипта.
func depoSetupFileName(f FancyMSSQLSettings) string {
if f.IsPostgres() {
return "bj-depo-setup-postgres.sql"
}
return "bj-depo-setup-mssql.sql"
}
// depoStep — ход подготовки для экрана: key — шаг, state — run (начат) | ok | warn, text — что делается или итог.
type depoStep func(key, state, text string)
// depoSetupRun — «bj сделает сам»: тот же скрипт выполняется логином администратора (не сохраняется) в базе
// db, которую назвал клиент; сначала — подключение и проверка, что это база DEPO. step — ход для экрана.
func depoSetupRun(ctx context.Context, f FancyMSSQLSettings, db, adminUser, adminPass, login, pass string, agentJob bool, step depoStep) error {
adm := f
adm.User, adm.Password = strings.TrimSpace(adminUser), adminPass
if !validDBName(db) {
return fmt.Errorf("укажите имя базы DEPO")
}
step("connect", "run", "Подключаюсь к серверу "+f.Server+" логином администратора «"+adm.User+"»")
if _, err := fancyPing(ctx, adm, ""); err != nil {
return fmt.Errorf("к серверу %s логином %s подключиться не удалось — %s", f.Server, adm.User, quikErrorDetail(err))
}
step("connect", "ok", "")
step("isdepo", "run", "Проверяю, что «"+db+"» — база учёта DEPO")
if err := depoCheckIsDepo(ctx, adm, db); err != nil {
return err
}
step("isdepo", "ok", "")
what := "логин bj «" + login + "», таблицы обмена, права"
if agentJob && !f.IsPostgres() {
what += ", задание «" + depoJobName + "»"
}
step("apply", "run", "Применяю скрипт подготовки: "+what)
if f.IsPostgres() {
if err := depoSetupRunPG(ctx, adm, db, login, pass); err != nil {
return err
}
step("apply", "ok", "")
return nil
}
conn, err := fancyOpen(adm, "")
if err != nil {
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 fmt.Errorf("подготовка базы %s логином %s: %s%s", db, adm.User, quikErrorDetail(err), depoTablesRightsHint(f, adm.User, err))
}
step("apply", "ok", "")
return nil
}
func depoSetupRunPG(ctx context.Context, adm FancyMSSQLSettings, db, login, pass string) error {
conn, err := fancyOpen(adm, db)
if err != nil {
return err
}
defer conn.Close()
stmts := []string{
fmt.Sprintf(`DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '%[1]s') THEN CREATE ROLE "%[1]s" LOGIN PASSWORD '%[2]s'; ELSE ALTER ROLE "%[1]s" LOGIN PASSWORD '%[2]s'; END IF; END $$`, login, sqlStr(pass)),
fmt.Sprintf(`GRANT CONNECT ON DATABASE "%s" TO "%s"`, db, login),
}
stmts = append(stmts, depoTablesDDL(true)...)
stmts = append(stmts, depoTablesGrantSQL(true, login)...)
for _, s := range stmts {
if _, err := conn.ExecContext(ctx, s); err != nil {
return fmt.Errorf("%s → %s", depoStmtHead(s), quikErrorDetail(err))
}
}
return nil
}