Files
Bridge-and-Join-s/internal/lkgateway/web/templates/part_keywiz.html
T

272 lines
26 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.
{{/* Установка ключа Валидаты: загрузка архива НРД → носитель → справочник → проверка ИШ. Один и тот же блок —
на шаге «Ключ подписи» мастера и на отдельной странице /admin/setup/keywizard (данные — keyWizardData). */}}
{{define "keyWizRet"}}{{if .InWizard}}<input type="hidden" name="ret" value="wizard">{{end}}{{if .WantContour}}<input type="hidden" name="want" value="{{.WantContour}}">{{end}}{{end}}
{{define "keyWizBody"}}
{{$s := .State}}
<style>
.kw-tiles{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:6px}
.kw-tile{display:flex;gap:10px;align-items:flex-start;padding:12px 14px;border:1px solid var(--border);border-radius:12px;cursor:pointer;font-weight:400;margin:0}
.kw-tile:has(input:checked){border-color:var(--accent);background:var(--accent-weak)}
.kw-tile input{margin-top:3px;flex:none;width:auto}
.kw-tile b{display:block;font-size:14.5px}
.kw-tile small{display:block;color:var(--muted);font-size:13px;line-height:1.4;margin-top:3px}
.kw-panel{margin-top:16px}
.kw-done{display:flex;gap:14px;align-items:flex-start;padding:16px 18px;margin-top:4px;border-radius:14px;background:color-mix(in srgb,var(--ok) 9%,var(--card));border:1px solid color-mix(in srgb,var(--ok) 32%,transparent)}
.kw-done-ic{flex:none;width:30px;height:30px;border-radius:50%;background:var(--ok);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:15px}
.kw-done-b b{display:block;font-size:15.5px;margin-top:4px}
.kw-done-b p{margin:6px 0 0;font-size:14px;line-height:1.5}
.kw-done-act{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:12px}
.kw-link{background:none;border:0;padding:0;color:var(--accent);font:inherit;font-size:13.5px;cursor:pointer;text-decoration:underline;text-underline-offset:3px}
.kw-howto{margin:0 0 12px}.kw-howto summary{cursor:pointer;color:var(--accent);font-size:13.5px;margin-bottom:8px}
.kw-drive{display:flex;gap:10px;align-items:flex-start;padding:10px;border:1px solid var(--border,#ddd);border-radius:8px;cursor:pointer;margin:0}
.kw-pad{height:200px;margin-top:12px;border:2px dashed var(--border);border-radius:12px;display:flex;align-items:center;justify-content:center;text-align:center;padding:0 16px;color:var(--muted);user-select:none;-webkit-user-select:none;touch-action:none;cursor:default}
.kw-pad.on{border-color:var(--accent);background:var(--accent-weak);color:var(--accent);cursor:crosshair}
.kw-bar{height:8px;margin-top:12px;border-radius:4px;background:var(--surface-2,#eee);overflow:hidden}
.kw-bar i{display:block;height:100%;width:0;background:var(--accent);transition:width .3s}
.kw-rng.ok .kw-bar i{background:var(--ok)}
.kw-rng-row{display:flex;gap:12px;align-items:center;justify-content:space-between;flex-wrap:wrap;margin-top:10px}
.kw-locked{opacity:.45;pointer-events:none;filter:grayscale(.3)}
@media (max-width:600px){.kw-tiles{grid-template-columns:minmax(0,1fr)}}
</style>
{{/* ===== Датчик случайных чисел Валидаты: один раз для сервера, до первой работы с ключом (validatarng.go) ===== */}}
{{if not $s.Done}}
<div class="card kw-rng" id="kw-rng">
<h2>Датчик случайных чисел Валидаты</h2>
<p class="muted" style="margin:0">Перед первой работой с ключом СКЗИ Валидата набирает случайность от движений мыши — на рабочем месте она показывает окно «Пожалуйста, двигайте мышью». У сервера экрана нет: окно Валидаты открыто на сервере, а мышью водите вы — здесь, bj передаёт ваши движения в это окно. Делается один раз для сервера, около минуты.</p>
<div class="kw-pad" id="kw-pad">Нажмите «Начать» и водите мышью по этому полю, пока полоса не заполнится</div>
<div class="kw-bar"><i id="kw-rng-bar"></i></div>
<div class="kw-rng-row"><span id="kw-rng-msg" class="muted">Датчик ещё не инициализирован — без этого ключ на сервере не работает.</span>
<span style="display:flex;gap:8px"><button type="button" class="btn btn-secondary" id="kw-rng-stop" hidden>Остановить</button><button type="button" class="btn btn-ok" id="kw-rng-go">Начать</button></span></div>
</div>
<script>(function(){
var card=document.getElementById('kw-rng'), pad=document.getElementById('kw-pad'),
bar=document.getElementById('kw-rng-bar'), msg=document.getElementById('kw-rng-msg'), go=document.getElementById('kw-rng-go'), stop=document.getElementById('kw-rng-stop');
var active=false, buf=[], lastT=0, sending=false, lastSent=0, shown=false, finished=false;
var J={'Accept':'application/json'};
function lock(on){ var m=document.getElementById('kw-main'); if(m) m.classList.toggle('kw-locked', on); }
function pct(v){ return Math.round(Math.min(1,v||0)*100); }
function show(v){
if(finished) return;
if(v.ready){ lock(false); active=false;
if(shown||v.state==='done'){ card.hidden=false; card.classList.add('ok'); finished=true; bar.style.width='100%'; pad.hidden=true; go.hidden=true; stop.hidden=true;
msg.textContent='✓ Датчик готов — теперь подключите ключ ниже. Повторять это не нужно.'; }
else { card.hidden=true; }
return; }
shown=true; card.hidden=false; lock(true);
if(v.state==='running'||v.state==='starting'){
active=true; pad.classList.add('on'); go.hidden=true; stop.hidden=false; bar.style.width=pct(v.progress)+'%';
pad.textContent=v.window?'Водите мышью по полю — '+pct(v.progress)+' %':'Открываю окно Валидаты на сервере…';
msg.textContent=v.window?'Ведите мышью без остановки, пока полоса не заполнится.':'Ещё пара секунд…';
} else {
active=false; pad.classList.remove('on'); go.hidden=false; stop.hidden=true; bar.style.width='0';
pad.textContent='Нажмите «Начать» и водите мышью по этому полю, пока полоса не заполнится';
go.textContent=(v.state==='failed'||v.state==='cancelled')?'Начать заново':'Начать';
msg.textContent=v.error?('Не получилось: '+v.error):(v.ok===false?'Состояние датчика не проверено':'Датчик ещё не инициализирован — без этого ключ на сервере не работает.');
}
}
function status(){ return fetch('/admin/setup/rng/status',{headers:J,cache:'no-store'}).then(function(r){return r.json()}).then(show).catch(function(){}); }
go.addEventListener('click',function(){ go.disabled=true; msg.textContent='Открываю окно Валидаты на сервере…';
fetch('/admin/setup/rng/start',{method:'POST',headers:J,cache:'no-store'}).then(function(r){return r.json()}).then(show)
.catch(function(){ msg.textContent='Сервер не ответил — попробуйте ещё раз'; }).then(function(){ go.disabled=false; }); });
stop.addEventListener('click',function(){ active=false; buf=[];
fetch('/admin/setup/rng/cancel',{method:'POST',headers:J,cache:'no-store'}).then(function(r){return r.json()}).then(show).catch(function(){}); });
pad.addEventListener('pointermove',function(e){ if(!active) return;
var r=pad.getBoundingClientRect(), list=(e.getCoalescedEvents&&e.getCoalescedEvents().length)?e.getCoalescedEvents():[e];
list.forEach(function(ev){ var x=Math.round((ev.clientX-r.left)/r.width*1000), y=Math.round((ev.clientY-r.top)/r.height*1000), t=ev.timeStamp;
var dt=lastT?Math.round(t-lastT):0; lastT=t;
buf.push([Math.max(0,Math.min(1000,x)),Math.max(0,Math.min(1000,y)),Math.max(0,Math.min(1000,dt))]); });
});
setInterval(function(){
if(!active||sending) return;
if(buf.length){ var m=buf.splice(0,1500); sending=true; lastSent=Date.now();
fetch('/admin/setup/rng/moves',{method:'POST',headers:{'Accept':'application/json','Content-Type':'application/json'},body:JSON.stringify({m:m}),cache:'no-store'})
.then(function(r){return r.json()}).then(show).catch(function(){}).then(function(){ sending=false; });
} else if(Date.now()-lastSent>1000){ sending=true; lastSent=Date.now(); status().then(function(){ sending=false; }); }
},250);
document.addEventListener('DOMContentLoaded',status); if(document.readyState!=='loading') status();
})();</script>
{{end}}
<div id="kw-main">
{{/* ===== Лента шагов — когда путь выбран (флешка с ключом или новая флешка из архива) ===== */}}
{{if or $s.StagingID $s.Done}}
{{if $s.Done}}<details class="kw-howto"><summary>Как прошла установка — {{len $s.Steps}} этапов</summary>{{end}}
<div class="card">
<ol style="list-style:none;padding:0;margin:0;display:grid;gap:12px">
{{range $i, $step := $s.Steps}}
<li style="display:flex;gap:12px;align-items:flex-start">
<span style="flex:0 0 28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:13px;
{{if eq $step.Status "ok"}}background:var(--ok-weak);color:var(--ok)
{{else if eq $step.Status "error"}}background:var(--err-weak);color:var(--err)
{{else if eq $step.Status "active"}}background:var(--accent-weak);color:var(--accent)
{{else}}background:var(--surface-2,#eee);color:var(--muted,#999){{end}}">
{{if eq $step.Status "ok"}}✓{{else if eq $step.Status "error"}}{{else}}{{add $i 1}}{{end}}
</span>
<div style="flex:1">
<div style="font-weight:600">{{$step.Title}}</div>
{{if $step.Detail}}<div class="muted" style="font-size:13px;margin-top:2px">{{$step.Detail}}</div>{{end}}
</div>
</li>
{{end}}
</ol>
</div>
{{if $s.Done}}</details>{{end}}
{{/* ===== Действие в зависимости от состояния ===== */}}
{{end}}
{{if $s.Done}}
<div class="kw-done">
<span class="kw-done-ic" aria-hidden="true">✓</span>
<div class="kw-done-b">
<b>Ключ подключён и подписывает</b>
<p>{{if $s.MediaDevice}}Флешка с ключом подключена к шлюзу НРД как есть — на неё ничего не записывалось.{{else}}Ключ записан на флешку, справочник сертификатов сформирован, шлюз НРД подписывает им документы.{{end}} Флешка должна оставаться в сервере: без неё шлюз не запустится.</p>
{{if $s.Backup}}<p class="muted" style="font-size:12.5px">Копия прежнего содержимого флешки: <code>{{$s.Backup}}</code></p>{{end}}
<div class="kw-done-act">
{{if devOn}}<form method="post" action="/admin/setup/test-nsd" style="margin:0"><input type="hidden" name="scenario" value="2001"><button type="submit" class="btn btn-ok">→ Отправить тестовый документ роботу</button></form>{{else if not .InWizard}}<a href="/admin/wizard?step=8" class="btn btn-ok">→ Вернуться в мастер настройки</a> <a href="/admin/status" class="btn btn-secondary">Проверить подключения</a>{{end}}
<form method="post" action="/admin/setup/keywizard/reset" style="margin:0">{{template "keyWizRet" .}}<button type="submit" class="kw-link" title="Когда истечёт сертификат или понадобится ключ другого контура">Заменить ключ</button></form>
</div>
</div>
</div>
{{else if and $s.StagingID $s.MediaDevice}}
{{/* ключ с подготовленной флешки — используется как есть, на флешку ничего не пишется */}}
<div class="card">
<h2>Шаг 2 — использовать ключ с флешки</h2>
<p class="muted">На флешке уже подготовленный ключ: <code>{{fallbackTpl $s.VDK "—"}}</code>{{if $s.MediaProfile}}, профиль <code>{{$s.MediaProfile}}</code>{{end}}.
bj подключит его к шлюзу НРД <b>как есть</b>: на флешку ничего не записывается — справочник для ИШ собирается на диске сервера, флешка подключается только для чтения.</p>
<form method="post" action="/admin/setup/keywizard/install" style="margin-top:12px;display:grid;gap:12px;max-width:640px"
onsubmit="this.querySelector('button[type=submit]').disabled=true;this.querySelector('button[type=submit]').textContent='Подключаю… (до пары минут)';">
{{template "keyWizRet" .}}
<input type="hidden" name="as_is" value="1">
{{$c := .SuggestContour}}{{if not $c}}{{$c = .WantContour}}{{end}}{{if not $c}}{{$c = currentContour}}{{end}}
{{if .WantContour}}
<p class="kw-contour-sum" style="margin:0">Контур НРД: <b>{{if eq .WantContour "prod"}}Боевой{{else}}Тестовый TEST3{{end}}</b> <span class="muted" style="font-size:12px">— выбран в мастере</span>{{if and .SuggestContour (ne .SuggestContour .WantContour)}}<br><span style="color:var(--warn);font-size:12px">⚠ по сертификату ключ похож на {{if eq .SuggestContour "test3"}}тестовый{{else}}боевой{{end}} — проверьте выбор</span>{{end}}
<button type="button" class="kw-link" style="margin-left:8px" onclick="this.closest('form').querySelector('.kw-contour-pick').hidden=false;this.closest('.kw-contour-sum').hidden=true">Изменить</button></p>
{{end}}
<div class="kw-contour-pick"{{if .WantContour}} hidden{{end}}>
{{if not .WantContour}}<label style="font-weight:600;display:block;margin-bottom:6px">Для какого контура НРД этот ключ</label>{{end}}
{{if .SuggestIssuer}}<p class="muted" style="font-size:12px;margin:0 0 4px">Сертификат от «{{.SuggestIssuer}}» — это {{if eq .SuggestContour "test3"}}тестовый{{else}}боевой{{end}} контур.</p>{{end}}
<label style="display:flex;gap:8px;align-items:center;font-weight:400"><input type="radio" name="contour" value="test3" style="width:auto" {{if ne $c "prod"}}checked{{end}}> Тестовый TEST3</label>
<label style="display:flex;gap:8px;align-items:center;font-weight:400"><input type="radio" name="contour" value="prod" style="width:auto" {{if eq $c "prod"}}checked{{end}}> Боевой</label>
</div>
<button type="submit" class="btn btn-ok">Использовать ключ с флешки</button>
</form>
<form method="post" action="/admin/setup/keywizard/reset" style="margin-top:8px">{{template "keyWizRet" .}}
<button type="submit" class="btn btn-secondary">Отмена</button>
</form>
</div>
{{else if $s.StagingID}}
{{/* Архив загружен — выбор флешки + запись */}}
<div class="card">
<h2>Шаг 2 — вставьте новую флешку и запишите ключ</h2>
<p class="muted">Архив распакован. Ключ: <code>{{fallbackTpl $s.VDK "—"}}</code>.
Вставьте в сервер чистую флешку — на неё запишется ключ и справочник сертификатов, затем bj
подключит её к шлюзу и перезапустит ИШ. Копия прежнего содержимого флешки сохраняется на сервере.
Не используйте флешку с рабочим ключом другого сервера.</p>
{{if not .Drives}}<p style="margin:10px 0 0">Флешка не найдена — вставьте её в сервер и <a href="javascript:location.reload()">обновите страницу</a>.</p>{{else}}
<form method="post" action="/admin/setup/keywizard/install" style="margin-top:12px;display:grid;gap:12px;max-width:640px"
onsubmit="this.querySelector('button[type=submit]').disabled=true;this.querySelector('button[type=submit]').textContent='Устанавливаю… (до нескольких минут)';">
{{template "keyWizRet" .}}
<div>
<label style="font-weight:600;display:block;margin-bottom:6px">Целевая флешка</label>
{{if .Drives}}
{{range $i, $d := .Drives}}
<label style="display:flex;gap:10px;align-items:flex-start;padding:10px;border:1px solid var(--border,#ddd);border-radius:8px;margin-bottom:8px;cursor:pointer">
<input type="radio" name="target_device" value="{{$d.Device}}" {{if $s.MediaDevice}}{{if eq $d.Device $s.MediaDevice}}checked{{end}}{{else if $d.IsKeymedia}}checked{{else if and (eq $i 0) (not (anyKeymedia $.Drives))}}checked{{end}} style="margin-top:3px">
<span>
<b>{{fallbackTpl $d.Model "USB-носитель"}}</b> · {{$d.Size}} · {{$d.FSType}}
{{if $d.Label}}· метка «{{$d.Label}}»{{end}}<br>
<span class="muted" style="font-size:12px">{{$d.Device}}{{if $d.Mountpoint}} · {{$d.Mountpoint}}{{end}}
{{if $d.IsKeymedia}}<b style="color:var(--accent)">← текущий ключевой носитель ИШ (рекомендуется)</b>{{end}}</span>
</span>
</label>
{{end}}
{{end}}
</div>
<div>
<label style="font-weight:600;display:block;margin-bottom:6px">Для какого контура НРД этот ключ</label>
{{$c := .SuggestContour}}{{if not $c}}{{$c = .WantContour}}{{end}}{{if not $c}}{{$c = currentContour}}{{end}}
{{if .SuggestIssuer}}<p class="muted" style="font-size:12px;margin:0 0 4px">В архиве сертификат от «{{.SuggestIssuer}}» — это {{if eq .SuggestContour "test3"}}тестовый{{else}}боевой{{end}} ключ.</p>{{end}}
<label style="display:flex;gap:8px;align-items:center;font-weight:400"><input type="radio" name="contour" value="test3" style="width:auto" {{if ne $c "prod"}}checked{{end}}> Тестовый TEST3 <span class="muted" style="font-size:12px">— сертификат от «Тестовый УЦ»</span></label>
<label style="display:flex;gap:8px;align-items:center;font-weight:400"><input type="radio" name="contour" value="prod" style="width:auto" {{if eq $c "prod"}}checked{{end}}> Боевой <span class="muted" style="font-size:12px">— выдан УЦ Московской биржи для работы</span></label>
<span class="muted" style="font-size:12px">У тестового и боевого контуров разные ключи: ключ подключается только к каналу ИШ своего контура.</span>
</div>
<div>
<label style="font-weight:600;display:block;margin-bottom:6px">Имя профиля в справочнике (необязательно)</label>
<input type="text" name="profile_name" value="{{$s.MediaProfile}}" placeholder="Авто из архива (напр. PrUser1046)" autocomplete="off"
pattern="[A-Za-z0-9_-]*" style="width:100%">
<span class="muted" style="font-size:12px">Пусто = имя берётся из архива автоматически.</span>
</div>
<button type="submit" class="btn btn-ok">Записать ключ на флешку</button>
</form>{{end}}
<form method="post" action="/admin/setup/keywizard/reset" style="margin-top:8px">{{template "keyWizRet" .}}
<button type="submit" class="btn btn-secondary">Отмена</button>
</form>
</div>
{{else}}
{{/* Начало: где ключ — на подготовленной флешке или в архиве НРД (тогда — на новую флешку) */}}
<div class="card">
<h2>Где ваш ключ?</h2>
<div class="kw-tiles">
<label class="kw-tile"><input type="radio" name="kwmode" value="media" checked><span><b>Подключить флешку с ключом</b><small>Флешка уже подготовлена (например, с другого сервера bj): bj найдёт на ней ключ и подключит к шлюзу как есть — ничего на неё не записывая.</small></span></label>
<label class="kw-tile"><input type="radio" name="kwmode" value="new"><span><b>Сформировать ключ на новой флешке</b><small>Есть архив ключа от НРД и пароль: bj распакует его и запишет ключ на чистую флешку.</small></span></label>
</div>
<details class="kw-howto" style="margin-top:12px"><summary>Что должно быть на флешке</summary>
<ul class="muted" style="font-size:13px;margin:4px 0 0;padding-left:18px;line-height:1.6">
<li><b>Ключ Валидаты</b> — файл <code>*.vdk</code> (обязательно; без него флешку не примет).</li>
<li><b>Справочник сертификатов</b> — папка с файлами <code>local.gdbm</code> и <code>local.pse</code> (оба обязательны). Имя папки любое — <code>spr1046</code>, <code>LAP_NEW</code> и т. п.; bj ищет эти файлы по имени в любой папке флешки.</li>
<li>Необязательно: файлы отзыва <code>*.crl</code>; из архива НРД также <code>key.reg</code> и папка <code>vdkeys/</code> — их наличие не требуется.</li>
</ul>
<p class="muted" style="font-size:13px;margin:8px 0 0">Раскладка любая: как в архиве НРД (<code>key.reg</code> + папка справочника + <code>vdkeys/*.vdk</code>) или произвольно — bj сам находит <code>*.vdk</code> и <code>local.gdbm</code>/<code>local.pse</code> в любой папке. Флешка FAT32/exFAT/NTFS или ext, подключается только на чтение.</p>
</details>
<div class="kw-panel" id="kw-media">
{{if .Drives}}
<form method="post" action="/admin/setup/keywizard/from-media" style="display:grid;gap:8px;max-width:640px">
{{template "keyWizRet" .}}
<p class="muted" style="margin:0">Флешка с ключом:</p>
{{range $i, $d := .Drives}}
<label class="kw-drive">
<input type="radio" name="source_device" value="{{$d.Device}}" {{if eq $i 0}}checked{{end}} style="margin-top:3px;width:auto">
<span><b>{{fallbackTpl $d.Model "USB-носитель"}}</b> · {{$d.Size}} · {{$d.FSType}}{{if $d.Label}} · метка «{{$d.Label}}»{{end}}<br>
<span class="muted" style="font-size:12px">{{$d.Device}}{{if $d.IsKeymedia}} · <b style="color:var(--accent)">сейчас ключевой носитель ИШ</b>{{end}}</span></span>
</label>
{{end}}
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center">
<button type="submit" class="btn btn-ok" onclick="this.textContent='Ищу ключ на флешке…'">Подключить флешку</button>
<button type="button" class="btn btn-secondary" onclick="this.textContent='Ищу флешку…';location.reload()">↻ Перепроверить флешку</button>
</div>
</form>
{{else}}
<p style="margin:0 0 8px">Флешка не найдена. Вставьте флешку с ключом в сервер и нажмите «Перепроверить флешку».</p>
<button type="button" class="btn btn-ok" onclick="this.textContent='Ищу флешку…';location.reload()">↻ Перепроверить флешку</button>
<p class="muted" style="font-size:12.5px;margin:8px 0 0">Если сервер — виртуальная машина (VMware/ESXi/Hyper-V), USB-носитель нужно сначала пробросить в гостя (в настройках ВМ подключить USB-устройство). Без проброса флешка в сервере не появляется. USB 3.0-флешке нужен контроллер <b>USB 3.1 (xHCI)</b> в настройках ВМ — на контроллере USB 2.0 она может не пробрасываться (в <code>lsusb</code> её не видно); смените контроллер на USB 3.1, переподключите устройство и перезагрузите ВМ.</p>
{{end}}
</div>
<div class="kw-panel" id="kw-new" hidden>
<p class="muted" style="margin:0 0 8px">Шаг 1 — архив ключа от НРД (.7z) и пароль архива. Флешку bj попросит после распаковки.</p>
<form method="post" action="/admin/setup/keywizard/upload" enctype="multipart/form-data"
style="display:grid;gap:10px;max-width:560px">
{{template "keyWizRet" .}}
<input type="file" name="archive" accept=".7z,.zip" required>
<input type="password" name="password" placeholder="Пароль архива (например 11)" autocomplete="off">
<button type="submit" class="btn btn-ok">Загрузить и распаковать</button>
</form>
</div>
</div>
<script>(function(){
var r=document.querySelectorAll('input[name=kwmode]');
function sync(){ var m=document.querySelector('input[name=kwmode]:checked'); m=m?m.value:'media';
document.getElementById('kw-media').hidden=m!=='media'; document.getElementById('kw-new').hidden=m!=='new'; }
r.forEach(function(x){ x.addEventListener('change', sync); }); sync();
})();</script>
{{end}}
</div>
{{end}}