Files
Bridge-and-Join-s/internal/lkgateway/web/templates/admin_setup.html
T
fontvielle 0ef75e05e8 feat(admin): импорт сертификатов через UI + список сертификатов на токенах + URL контуров НРД
После реальной установки КриптоПро CSP добавлены следующие
функциональности:

cryptocli/client.go:
- FindCertificates() — перечисляет CKO_CERTIFICATE объекты на всех
  подключенных слотах через PKCS#11, парсит X.509, извлекает CN, ИНН
  (OID 1.2.643.3.131.1.1), серийник, срок действия. Для каждого
  сертификата проверяет наличие парного приватного ключа (CKO_PRIVATE_KEY
  с тем же CKA_ID).
- Тип Certificate с полями: SubjectCN, IssuerCN, INN, Serial, NotBefore,
  NotAfter, DER, HasPrivateKey, TokenLabel, SlotID.

internal/lkgateway/setup.go:
- handler importCertificate (POST /admin/setup/crypto/import-cert,
  multipart). Принимает .pfx/.p12 (с PIN) или .cer/.crt. Запускает
  certmgr -inst -pfx или -inst с выбором хранилища (uMy/mroot/uRoot).
- listCertsForUI() — вспомогательный метод renderSetup для подгрузки
  актуального списка сертификатов с подключенных токенов при каждом
  открытии страницы.

internal/lkgateway/web/templates/admin_setup.html:
- секция «Сертификаты на токенах» с таблицей (Кому/Кем выдан/ИНН/срок/
  токен/есть-ли-приватный-ключ).
- форма «Импорт сертификата (.pfx/.cer/.crt)» с полями для PIN и
  выбора хранилища.
- блок «Интеграционный шлюз НРД»: добавлен JS автозаполнения URL ONYX
  и контейнера по выбору профиля (guest/test3/prod × gost/rsa) —
  значения из DOC/Ссылки для доступа в тестовые контуры.pdf.

internal/lkgateway/web/templates/admin_help_systems.html:
- секция «Интеграционный шлюз НРД и контуры тестирования» дополнена
  полной таблицей URL-ов сервисов GUEST/TEST3 (ONYX, Agate, DCS,
  Единый кабинет, Корпоративные действия). IP gost.nsd.ru для
  настройки межсетевого экрана.
- новая секция «Сертификаты УЦ НРД (для проверки квитанций)» с
  пошаговой инструкцией: куда импортировать корневой сертификат УЦ
  НРД, куда промежуточные, куда наши сертификаты из стороннего УЦ.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:34:32 +03:00

235 lines
22 KiB
HTML
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.
{{define "content"}}
{{if .Flash}}<div style="padding:12px 16px;background:rgba(63,191,108,0.1);border-left:3px solid var(--ok);border-radius:4px;margin-bottom:16px">{{.Flash}}</div>{{end}}
<div class="card">
<h2>Готовность системы: {{.ReadyCount}} из {{.TotalCount}}</h2>
<div style="display:flex;gap:8px;margin-top:8px">
{{range .Readiness}}
<div style="flex:1;text-align:center;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px">
<span class="dot {{if .Configured}}ok{{else}}err{{end}}"></span>
<strong>{{.Name}}</strong><br>
<span class="muted" style="font-size:11px">{{if .Configured}}настроено{{else}}не настроено{{end}}</span>
</div>
{{end}}
</div>
</div>
<!-- PostgreSQL -->
<div class="card">
<h2><span class="dot {{if .Settings.Postgres.DSN}}ok{{else}}err{{end}}"></span>PostgreSQL</h2>
<p class="muted">Принимающая БД (fansy-store) и журнал сделок m2m-core. Сейчас:
{{if .Settings.Postgres.DSN}}<code>настроено</code>{{else}}<code>in-memory</code> (M2-демо){{end}}.</p>
<details {{if not .Settings.Postgres.DSN}}open{{end}}>
<summary style="cursor:pointer;color:var(--accent);font-size:13px">Изменить параметры подключения</summary>
<form method="post" action="/admin/setup/postgres" style="margin-top:12px">
<div class="form-row" style="display:grid;grid-template-columns:200px 1fr;gap:12px;align-items:center">
<label>DSN</label>
<input type="text" name="dsn" value="{{.Settings.Postgres.DSN}}" placeholder="postgres://bj:secret@127.0.0.1:5432/bj?sslmode=disable" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">
</div>
<p class="muted" style="margin-top:8px">При сохранении выполняется Ping. Если драйвер pgx ещё не подключён в коде, тест упадёт — это ожидаемо до M2-шага-3.</p>
<button type="submit" class="btn" style="background:var(--accent);color:white;border:none;padding:8px 16px;border-radius:4px;margin-top:8px">Сохранить и проверить</button>
</form>
</details>
</div>
<!-- СКЗИ через PKCS#11: КриптоПро CSP / Рутокен / Валидата / ViPNet -->
<div class="card">
<h2><span class="dot {{if and .Settings.Crypto.JCPPath .Settings.Crypto.LicenseKey}}ok{{else}}err{{end}}"></span>СКЗИ (КриптоПро CSP, Рутокен и др. через PKCS#11)</h2>
<p class="muted">Go-клиент подключается к СКЗИ напрямую через стандартный PKCS#11 интерфейс. Поддерживаются КриптоПро CSP, Рутокен ЭЦП 2.0, Валидата, ViPNet — один клиент, разные .so модули. Подробно — раздел <a href="/admin/help/cryptopro">«КриптоПро»</a> в инструкциях.</p>
<table style="margin-bottom:12px">
<tr><td style="width:220px" class="muted">Текущий провайдер</td><td><code>{{.Settings.Crypto.Provider}}</code></td></tr>
<tr><td class="muted">Путь к модулю PKCS#11</td><td><code>{{if .Settings.Crypto.JCPPath}}{{.Settings.Crypto.JCPPath}}{{else}}—{{end}}</code></td></tr>
<tr><td class="muted">UDS-сокет (legacy)</td><td><code>{{.Settings.Crypto.SocketPath}}</code></td></tr>
<tr><td class="muted">Лицензия введена</td><td>{{if .Settings.Crypto.LicenseKey}}<span style="color:var(--ok)">да</span>{{else}}<span style="color:var(--err)">нет</span>{{end}}</td></tr>
</table>
<details {{if or (eq .Settings.Crypto.Provider "stub") (not .Settings.Crypto.JCPPath)}}open{{end}}>
<summary style="cursor:pointer;color:var(--accent);font-size:13px">Изменить параметры СКЗИ</summary>
<form method="post" action="/admin/setup/crypto" style="margin-top:12px;display:grid;gap:10px">
<div class="form-row" style="display:grid;grid-template-columns:220px 1fr;gap:12px;align-items:center">
<label>Провайдер СКЗИ</label>
<select name="provider" style="padding:8px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">
<option value="stub" {{if eq .Settings.Crypto.Provider "stub"}}selected{{end}}>stub — без криптографии (демо)</option>
<option value="cryptopro" {{if eq .Settings.Crypto.Provider "cryptopro"}}selected{{end}}>КриптоПро CSP (через PKCS#11)</option>
<option value="rutoken" {{if eq .Settings.Crypto.Provider "rutoken"}}selected{{end}}>Рутокен ЭЦП 2.0 (для подписи оператора)</option>
<option value="validata" {{if eq .Settings.Crypto.Provider "validata"}}selected{{end}}>Валидата</option>
<option value="vipnet" {{if eq .Settings.Crypto.Provider "vipnet"}}selected{{end}}>ViPNet</option>
</select>
</div>
<div class="form-row" style="display:grid;grid-template-columns:220px 1fr;gap:12px;align-items:center">
<label>Путь к модулю PKCS#11</label>
<input type="text" name="jcp_path" value="{{.Settings.Crypto.JCPPath}}" placeholder="/opt/cprocsp/lib/amd64/libcppkcs11.so" style="padding:8px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">
</div>
<div class="form-row" style="display:grid;grid-template-columns:220px 1fr;gap:12px;align-items:center">
<label>UDS-сокет (legacy)</label>
<input type="text" name="socket_path" value="{{.Settings.Crypto.SocketPath}}" placeholder="/run/bj/crypto.sock (только для совместимости со старым Java crypto-service)" style="padding:8px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">
</div>
<div class="form-row" style="display:grid;grid-template-columns:220px 1fr;gap:12px;align-items:flex-start">
<label>Серийный номер лицензии</label>
<textarea name="license_key" rows="3" placeholder="XXXX-XXXXX-XXXXX-XXXXX-XXXXX (серийный номер КриптоПро CSP)" style="padding:8px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;font-family:monospace;font-size:12px">{{.Settings.Crypto.LicenseKey}}</textarea>
</div>
<p class="muted">
<strong>КриптоПро CSP</strong>: установить пакеты <code>rpm -i cprocsp-*.rpm</code>, активировать лицензию командой <code>cpconfig -license -set XXXX-...</code>, указать <code>/opt/cprocsp/lib/amd64/libcppkcs11.so</code>.<br>
<strong>Рутокен</strong>: подключить токен USB, указать <code>/usr/lib64/librtpkcs11ecp.so</code>.<br>
Полная инструкция: <a href="/admin/help/cryptopro">/admin/help/cryptopro</a>. При сохранении проверим, что файл модуля существует.
</p>
<div style="display:flex;gap:8px">
<button type="submit" class="btn" style="background:var(--accent);color:white;border:none;padding:8px 16px;border-radius:4px">Сохранить</button>
</div>
</form>
<form method="post" action="/admin/setup/crypto/check" style="margin-top:12px">
<button type="submit" class="btn" style="background:var(--border);color:var(--text);border:none;padding:8px 16px;border-radius:4px">Проверить подключение СКЗИ</button>
<span class="muted" style="margin-left:8px">Загрузит PKCS#11 модуль, опросит список токенов, покажет результат сверху страницы.</span>
</form>
<hr style="margin:18px 0;border:none;border-top:1px solid var(--border)">
<h3 style="font-size:14px;margin:0 0 8px">Установка КриптоПро CSP</h3>
<p class="muted">Дистрибутив с <a href="https://www.cryptopro.ru/products/csp/downloads" target="_blank" rel="noopener">cryptopro.ru</a> (например, <code>linux-amd64.tgz</code> или <code>linux-amd64.tar</code> для РЕД ОС/ALT/ROSA). Загрузите файл здесь — он будет распакован и установлен через <code>sudo rpm -Uvh</code>. Установка длится ~30 секунд.</p>
<form method="post" action="/admin/setup/crypto/install" enctype="multipart/form-data" style="margin-top:12px;display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<input type="file" name="dist" accept=".tar,.tgz,.gz,.rpm" required style="padding:6px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;flex:1;min-width:300px">
<button type="submit" class="btn" style="background:var(--accent);color:white;border:none;padding:8px 16px;border-radius:4px">Загрузить и установить</button>
</form>
<hr style="margin:18px 0;border:none;border-top:1px solid var(--border)">
<h3 style="font-size:14px;margin:0 0 8px">Сертификаты на токенах</h3>
{{if .Certificates}}
<table>
<thead><tr><th>Кому</th><th>Кем выдан</th><th>ИНН</th><th>Действителен</th><th>Токен</th><th>Приватный ключ</th></tr></thead>
<tbody>
{{range .Certificates}}
<tr>
<td>{{.SubjectCN}}</td>
<td class="muted">{{.IssuerCN}}</td>
<td><code>{{.INN}}</code></td>
<td class="muted">до {{.NotAfter.Format "02.01.2006"}}</td>
<td class="muted">«{{.TokenLabel}}» (slot {{.SlotID}})</td>
<td>{{if .HasPrivateKey}}<span style="color:var(--ok)">есть</span>{{else}}<span class="muted">нет</span>{{end}}</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<p class="muted">На подключенных токенах сертификатов не найдено. Загрузите .pfx ниже или подключите Рутокен с сертификатом.</p>
{{end}}
<hr style="margin:18px 0;border:none;border-top:1px solid var(--border)">
<h3 style="font-size:14px;margin:0 0 8px">Импорт сертификата (.pfx / .cer / .crt)</h3>
<p class="muted">PFX с приватным ключом (с PIN) — для серверной подписи и подписи оператора. CER/CRT без приватного ключа — для проверки чужих подписей (например, сертификаты УЦ НРД для проверки квитанций). Подробно — <a href="/admin/help/cryptopro">/admin/help/cryptopro</a>.</p>
<form method="post" action="/admin/setup/crypto/import-cert" enctype="multipart/form-data" style="margin-top:8px;display:grid;gap:8px;grid-template-columns:auto auto auto auto;align-items:center">
<input type="file" name="cert" accept=".pfx,.p12,.cer,.crt" required style="padding:6px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">
<input type="text" name="pin" placeholder="PIN (только для .pfx/.p12)" style="padding:6px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;font-family:monospace">
<select name="store" style="padding:6px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">
<option value="uMy">uMy — личный (для подписи)</option>
<option value="mroot">mroot — корневой УЦ (для проверки)</option>
<option value="uRoot">uRoot — промежуточные УЦ</option>
</select>
<button type="submit" class="btn" style="background:var(--accent);color:white;border:none;padding:8px 16px;border-radius:4px">Импортировать</button>
</form>
<hr style="margin:18px 0;border:none;border-top:1px solid var(--border)">
<h3 style="font-size:14px;margin:0 0 8px">Активация лицензии</h3>
<form method="post" action="/admin/setup/crypto/activate" style="margin-top:6px;display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<input type="text" name="license_key" value="{{.Settings.Crypto.LicenseKey}}" placeholder="XXXX-XXXXX-XXXXX-XXXXX-XXXXX (серийный номер КриптоПро CSP)" style="padding:8px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;font-family:monospace;font-size:12px;min-width:340px">
<button type="submit" class="btn" style="background:var(--ok);color:#0a0f1a;border:none;padding:8px 16px;border-radius:4px;font-weight:600">Активировать лицензию</button>
</form>
<p class="muted" style="margin-top:8px">Вызовет <code>cpconfig -license -set</code> и сохранит серийник. Если КриптоПро CSP ещё не установлен — покажет инструкцию.</p>
</details>
</div>
<!-- nsd-adapter / ИШ НРД -->
<div class="card">
<h2><span class="dot {{if .Settings.NSD.IGWBaseURL}}ok{{else}}err{{end}}"></span>Интеграционный шлюз НРД (ИШ)</h2>
<p class="muted">{{if not .Settings.NSD.IGWBaseURL}}Сейчас <code>mock-режим</code> — Decision эмитируется через 3 секунды после Send.{{else}}Профиль <code>{{.Settings.NSD.Profile}}</code>, ИШ <code>{{.Settings.NSD.IGWBaseURL}}</code>.{{end}}</p>
<p class="muted">Подключение к стендам: <a href="/admin/help/systems">/admin/help/systems</a> — там полная таблица URL контуров GUEST/TEST3/PROD и инструкция по установке ИШ. Дистрибутив ИШ скачивается с <code>www.nsd.ru/workflow/system/programs/#0-widget-faq-0-4</code>.</p>
<details {{if not .Settings.NSD.IGWBaseURL}}open{{end}}>
<summary style="cursor:pointer;color:var(--accent);font-size:13px">Изменить параметры ИШ</summary>
<form method="post" action="/admin/setup/nsd" style="margin-top:12px;display:grid;gap:10px">
<div class="form-row" style="display:grid;grid-template-columns:200px 1fr;gap:12px;align-items:center">
<label>Профиль</label>
<select name="profile" id="nsd-profile" style="padding:8px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">
<option value="" {{if eq .Settings.NSD.Profile ""}}selected{{end}}>— mock (демо без реального ИШ) —</option>
<option value="guest-gost" {{if eq .Settings.NSD.Profile "guest-gost"}}selected{{end}}>guest-gost — контур GUEST, ГОСТ ключи</option>
<option value="guest-rsa" {{if eq .Settings.NSD.Profile "guest-rsa"}}selected{{end}}>guest-rsa — контур GUEST, RSA ключи</option>
<option value="test3-gost" {{if eq .Settings.NSD.Profile "test3-gost"}}selected{{end}}>test3-gost — контур TEST3, ГОСТ ключи</option>
<option value="test3-rsa" {{if eq .Settings.NSD.Profile "test3-rsa"}}selected{{end}}>test3-rsa — контур TEST3, RSA ключи</option>
<option value="prod-gost" {{if eq .Settings.NSD.Profile "prod-gost"}}selected{{end}}>prod-gost — продуктивный, ГОСТ</option>
<option value="prod-rsa" {{if eq .Settings.NSD.Profile "prod-rsa"}}selected{{end}}>prod-rsa — продуктивный, RSA</option>
</select>
</div>
<div class="form-row" style="display:grid;grid-template-columns:200px 1fr;gap:12px;align-items:center">
<label>URL ONYX (WSL) НРД</label>
<input type="text" name="igw_base_url" id="nsd-url" value="{{.Settings.NSD.IGWBaseURL}}" placeholder="будет заполнено по профилю" style="padding:8px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">
</div>
<div class="form-row" style="display:grid;grid-template-columns:200px 1fr;gap:12px;align-items:center">
<label>Ключевой контейнер</label>
<input type="text" name="key_container" id="nsd-container" value="{{.Settings.NSD.KeyContainer}}" placeholder="GUEST_GOST_CONTAINER (или ваш контейнер УЦ НРД)" style="padding:8px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">
</div>
<p class="muted">При смене профиля URL ONYX автозаполнится по таблице НРД (из <code>DOC/Ссылки для доступа в тестовые контуры.pdf</code>). При сохранении проверяется доступность URL.</p>
<button type="submit" class="btn" style="background:var(--accent);color:white;border:none;padding:8px 16px;border-radius:4px">Сохранить и проверить</button>
</form>
<script>
// Автозаполнение URL ONYX и дефолтного контейнера по выбранному профилю.
(function() {
var urls = {
"guest-gost": ["https://gost-gt.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo", "GUEST_GOST_CONTAINER"],
"guest-rsa": ["https://rsa-gt.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo", "GUEST_RSA_CONTAINER"],
"test3-gost": ["https://gost-t3.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo", "TEST3_GOST_CONTAINER"],
"test3-rsa": ["https://rsa-t3.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo", "TEST3_RSA_CONTAINER"],
"prod-gost": ["https://gost.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo", "PROD_GOST_CONTAINER"],
"prod-rsa": ["https://rsa.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo", "PROD_RSA_CONTAINER"]
};
var profile = document.getElementById("nsd-profile");
var urlInput = document.getElementById("nsd-url");
var contInput = document.getElementById("nsd-container");
profile.addEventListener("change", function() {
var p = profile.value;
if (urls[p]) {
if (!urlInput.value || confirm("Заменить URL и контейнер на дефолт для профиля " + p + "?")) {
urlInput.value = urls[p][0];
contInput.value = urls[p][1];
}
}
});
})();
</script>
</details>
</div>
<!-- LK callback -->
<div class="card">
<h2><span class="dot {{if .Settings.LK.CallbackURL}}ok{{else}}err{{end}}"></span>Callback в ЛК</h2>
<p class="muted">{{if .Settings.LK.CallbackURL}}Callback URL: <code>{{.Settings.LK.CallbackURL}}</code>{{else}}Сейчас используется встроенный lk-emulator (он сам зарегистрировал свой адрес при старте).{{end}}</p>
<details {{if not .Settings.LK.CallbackURL}}open{{end}}>
<summary style="cursor:pointer;color:var(--accent);font-size:13px">Указать URL реального ЛК</summary>
<form method="post" action="/admin/setup/lk" style="margin-top:12px">
<div class="form-row" style="display:grid;grid-template-columns:200px 1fr;gap:12px;align-items:center">
<label>Callback URL</label>
<input type="text" name="callback_url" value="{{.Settings.LK.CallbackURL}}" placeholder="http://lk.example.com" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">
</div>
<p class="muted" style="margin-top:8px">URL до базового хоста ЛК (без /api). При сохранении выполняется GET <code>{URL}/healthz</code>.</p>
<button type="submit" class="btn" style="background:var(--accent);color:white;border:none;padding:8px 16px;border-radius:4px;margin-top:8px">Сохранить и проверить</button>
</form>
</details>
</div>
<!-- Тестовый прогон -->
<div class="card">
<h2>Тестовый прогон сквозной заявки</h2>
<p class="muted">Создаст заявку с предзаполненными данными (инвестор Иванов И.И., 1500 акций Газпрома, ИИС T03), отправит её через всю цепочку и дождётся финального статуса. Если ИШ НРД настроен — пойдёт в реальный ИШ; иначе через mock с задержкой 3 сек.</p>
<form method="post" action="/admin/setup/test-run" style="margin-top:12px">
<button type="submit" class="btn" style="background:var(--accent);color:white;border:none;padding:10px 20px;border-radius:4px;font-weight:600">Запустить тестовую заявку</button>
</form>
{{if .Settings.LastTest}}
<div style="margin-top:16px;padding:12px;background:var(--bg);border:1px solid var(--border);border-radius:4px">
<strong>Последний прогон:</strong>
<table style="margin-top:8px">
<tr><td style="width:160px" class="muted">Статус</td><td>{{if .Settings.LastTest.OK}}<span style="color:var(--ok)">✓ успешно</span>{{else}}<span style="color:var(--err)">✗ не прошёл</span>{{end}}</td></tr>
<tr><td class="muted">Финальный FSM-статус</td><td><code>{{.Settings.LastTest.FinalStatus}}</code></td></tr>
<tr><td class="muted">ClaimID</td><td><code>{{.Settings.LastTest.ClaimID}}</code> {{if .Settings.LastTest.ClaimID}}<a href="/admin/claims/{{.Settings.LastTest.ClaimID}}">→ открыть карточку</a>{{end}}</td></tr>
<tr><td class="muted">Когда</td><td>{{.Settings.LastTest.StartedAt.Format "02.01.2006 15:04:05"}} — длительность {{.Settings.LastTest.FinishedAt.Sub .Settings.LastTest.StartedAt}}</td></tr>
<tr><td class="muted">Сообщение</td><td>{{.Settings.LastTest.Message}}</td></tr>
</table>
</div>
{{end}}
</div>
{{end}}