Files
Bridge-and-Join-s/internal/lkgateway/web/templates/part_accounts.html
T
zuevav 6f9896d12e Пользователи в настройках — как в мастере: простой вид и расширенный под раскрытием
- Блок «Учётные записи» мастера вынесен в part_accounts.html (accountsAdd, accountsAD) и стоит и в мастере, и в Настройка → Пользователи: роли плитками, создать сотрудника (с модулями), сотрудники и группы из домена.
- Список сотрудников: строка — кружок, имя, логин, источник, последний вход, роль; «Изменить» раскрывает правку (имя, e-mail, роль, пароль, модули, «может входить»), «Удалить сотрудника» — тихой ссылкой с подтверждением.
- «Расширенные настройки» под раскрытием: роли и права, домен вручную (контроллер, Base DN, сертификат ЦС, служебная запись, группы → роли, роль по умолчанию, время сессии), проверка входа, перепроверка групп; подписи укорочены, поля в ряд.
- Ссылки-кнопки (dq-link, «убрать») одинаковые в настройках и мастере.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
2026-10-07 08:03:16 +03:00

118 lines
15 KiB
HTML

{{/* Учётные записи: добавить сотрудника (роль плиткой, логин, пароль) и сотрудники из домена (подключение,
поиск сотрудников и групп, доступ группам). Общий блок шага мастера «Учётные записи» и раздела
Настройка → Пользователи. Вход: withRet <SetupData> <куда вернуться после сохранения>. */}}
{{define "accountsAdd"}}{{$U := .D.Users}}
<form method="post" action="/admin/setup/users" id="acc-form">
<input type="hidden" name="return_to" value="{{$.Ret}}"><input type="hidden" name="create" value="1">
<input type="hidden" name="active" value="1"><input type="hidden" name="source" value="local">
<h3 style="margin:16px 0 0">Кого добавить</h3>
<div class="acc-roles">
{{range $r := $U.Roles}}
<label class="acc-role"><input type="radio" name="role" value="{{$r.Key}}" {{if eq $r.Key (or (and $U.Open "admin") "operator")}}checked{{end}}>
<span><b>{{$r.Title}}</b><small>{{$r.Desc}}</small>
<span class="acc-who">Уже есть: {{$n := 0}}{{range $U.Users}}{{if eq .Role $r.Key}}{{if $n}}, {{end}}{{$n = 1}}{{.Login}}{{if .Name}} ({{.Name}}){{end}}{{if not .Active}} — заблокирован{{end}}{{end}}{{end}}{{if not $n}}<span class="muted">никого</span>{{end}}</span></span></label>
{{end}}
</div>
<div class="acc-form">
<label>Логин<input type="text" name="login" placeholder="ivanov" required autocomplete="off" pattern="[^ /\@&quot;'<>]+"></label>
<label>Имя<input type="text" name="name" placeholder="Иванов Иван Иванович" autocomplete="off"></label>
<label>E-mail<input type="email" name="email" placeholder="ivanov@example.ru" autocomplete="off"></label>
<div class="acc-pw"><label style="flex:1">Пароль (от 8 символов)<input type="text" name="password" id="acc-pw" minlength="8" required autocomplete="off" spellcheck="false"></label>
<button type="button" class="btn btn-secondary" id="acc-gen" style="white-space:nowrap">Придумать</button></div>
</div>
{{if $U.Modules}}<div class="acc-mods"><span class="muted">Дополнительно к роли:</span>{{range $U.Modules}}<label><input type="checkbox" name="modules" value="{{.Key}}">{{.Title}}</label>{{end}}</div>{{end}}
<p class="muted" style="font-size:12.5px;margin:8px 0 0">Пароль передайте сотруднику лично; сменить его можно позже: Настройка → Пользователи → «Изменить».</p>
<div style="margin-top:12px"><button type="submit" class="btn btn-ok">Создать учётную запись</button></div>
</form>
<script>(function(){
var g=document.getElementById('acc-gen'), f=document.getElementById('acc-pw'); if(!g||!f||!window.crypto) return;
g.addEventListener('click',function(){ var a='abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789', b=new Uint32Array(12), p='';
crypto.getRandomValues(b); for(var i=0;i<b.length;i++){ p+=a[b[i]%a.length]; if(i===3||i===7) p+='-'; } f.value=p; f.select(); });
})();</script>
{{end}}
{{define "accountsAD"}}
{{$A := $.D.Settings.Auth}}{{$D := adDraft}}{{$on := and $A.ADEnabled $A.ADBindUser}}
<details class="acc-ad" id="acc-ad" {{if or $D $on}}open{{end}} style="margin-top:20px;padding-top:14px;border-top:1px solid var(--border)">
<summary style="cursor:pointer;color:var(--accent);font-size:14px">Сотрудники из домена (Active Directory) — необязательно</summary>
{{if $on}}
<div class="ad-st ad-st-ok"><span class="ad-st-ic"></span><div><b>Домен {{$A.ADDomain}} подключён</b><small>контроллер {{$A.ADServerHost}} · служебная запись {{$A.ADBindUser}}{{if $A.ADCAPEM}} · сертификат контроллера проверяется{{end}}</small></div></div>
<label class="ad-q-l">Найдите сотрудника или группу<input type="search" id="acc-ad-q" placeholder="Фамилия, логин, почта или название группы" autocomplete="off"></label>
<div id="acc-ad-st" class="muted" style="font-size:13px;margin-top:6px" aria-live="polite"></div>
<ul id="acc-ad-res" class="ad-res"></ul>
<form method="post" action="/admin/setup/users" id="acc-ad-form" class="ad-pick" hidden>
<input type="hidden" name="return_to" value="{{$.Ret}}"><input type="hidden" name="section" value="acc-ad"><input type="hidden" name="create" value="1"><input type="hidden" name="active" value="1"><input type="hidden" name="source" value="ad">
<input type="hidden" name="login"><input type="hidden" name="name"><input type="hidden" name="email">
<span>Сотрудник <b id="acc-ad-who"></b> — входит своим доменным логином и паролем, с ролью</span>
<select name="role">{{range $.D.Users.Roles}}<option value="{{.Key}}" {{if eq .Key "operator"}}selected{{end}}>{{.Title}}</option>{{end}}</select>
<button type="submit" class="btn btn-ok">Добавить сотрудника</button>
</form>
<form method="post" action="/admin/setup/users/ad-group" id="acc-ad-gform" class="ad-pick" hidden>
<input type="hidden" name="return_to" value="{{$.Ret}}"><input type="hidden" name="section" value="acc-ad"><input type="hidden" name="group">
<span>Все сотрудники группы <b id="acc-ad-gwho"></b> — входят своим доменным логином и паролем, с ролью</span>
<select name="role">{{range $.D.Users.Roles}}<option value="{{.Key}}" {{if eq .Key "operator"}}selected{{end}}>{{.Title}}</option>{{end}}</select>
<button type="submit" class="btn btn-ok">Дать доступ группе</button>
</form>
{{if $A.ADGroups}}<div class="ad-groups"><div class="muted" style="font-size:13px;margin-bottom:6px">Группы домена с доступом в bj — их сотрудники входят без отдельной учётной записи:</div>
{{range $A.ADGroups}}<div class="ad-grp"><span><b>{{.Name}}</b> → {{roleTitle .Role}}</span>
<form method="post" action="/admin/setup/users/ad-group"><input type="hidden" name="return_to" value="{{$.Ret}}"><input type="hidden" name="section" value="acc-ad"><input type="hidden" name="group" value="{{.Name}}"><input type="hidden" name="remove" value="1"><button type="submit" class="dq-link">убрать</button></form></div>{{end}}
</div>{{end}}
<script>(function(){
var q=document.getElementById('acc-ad-q'), st=document.getElementById('acc-ad-st'), ul=document.getElementById('acc-ad-res'), f=document.getElementById('acc-ad-form'), gf=document.getElementById('acc-ad-gform'), who=document.getElementById('acc-ad-who'), gwho=document.getElementById('acc-ad-gwho'), t=0;
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]})}
q.addEventListener('input',function(){ clearTimeout(t); t=setTimeout(function(){
var v=q.value.trim(); f.hidden=true; gf.hidden=true; if(v.length<2){ ul.innerHTML=''; st.textContent=''; return; } st.textContent='Ищу в домене…';
fetch('/admin/setup/users/ad-search?q='+encodeURIComponent(v),{cache:'no-store',headers:{'Accept':'application/json'}}).then(function(r){return r.json()}).then(function(d){
if(!d.ok){ st.textContent='Не получилось: '+d.error; ul.innerHTML=''; return; }
st.textContent=d.users.length?'Найдено: '+d.users.length+' — выберите, кого добавить':'Никого не нашлось';
ul.innerHTML=d.users.map(function(u,i){ var g=u.kind==='group';
return '<li><button type="button" class="ad-hit" data-i="'+i+'"'+(u.have?' disabled':'')+'><span class="ad-kind'+(g?' ad-kind-g':'')+'">'+(g?'группа':'сотрудник')+'</span><b>'+esc(u.name||u.login)+'</b> <span class="muted">'+(g?esc(u.desc||u.login):esc(u.login)+(u.email?' · '+esc(u.email):''))+(u.have?(g?' · доступ уже дан':' · уже есть в bj'):'')+'</span></button></li>'; }).join('');
ul.querySelectorAll('button').forEach(function(b){ b.onclick=function(){ var u=d.users[+b.dataset.i];
ul.querySelectorAll('.ad-hit').forEach(function(x){ x.classList.toggle('is-sel',x===b); });
if(u.kind==='group'){ gf.group.value=u.login; gwho.textContent='«'+(u.name||u.login)+'»'; gf.hidden=false; f.hidden=true; gf.scrollIntoView({block:'nearest',behavior:'smooth'}); return; }
f.login.value=u.login; f.name.value=u.name||''; f.email.value=u.email||''; who.textContent=(u.name||u.login)+' ('+u.login+')'; f.hidden=false; gf.hidden=true; f.scrollIntoView({block:'nearest',behavior:'smooth'}); }; });
}).catch(function(){ st.textContent='Домен не ответил'; });
},350); });
if(document.getElementById('acc-ad').open && !document.querySelector('#ad-connect[open]')) q.focus({preventScroll:true});
})();</script>
{{end}}
<details id="ad-connect" {{if or $D (not (and $A.ADEnabled $A.ADBindUser))}}open{{end}} style="margin-top:12px"><summary style="cursor:pointer;font-size:13px;color:var(--accent)">{{if and $A.ADEnabled $A.ADBindUser}}Другой домен или учётная запись{{else}}Подключить домен{{end}}</summary>
{{/* пароль в страницу не попадает никогда — только пометка, что он есть */}}
{{$bu := $A.ADBindUser}}{{$pw := ""}}{{if $A.ADBindPassword}}{{$pw = "задан"}}{{end}}{{$fs := ""}}{{$fd := ""}}{{$adv := false}}
{{if $D}}{{$bu = $D.BindUser}}{{$pw = ""}}{{if $D.HasPassword}}{{$pw = "введён"}}{{end}}{{$fs = $D.Form.Server}}{{$fd = $D.Form.Domain}}{{$adv = $D.Form.Adv}}{{end}}
<p class="muted" style="font-size:13px;margin:8px 0 0">Введите учётную запись домена и пароль — домен и его контроллеры bj найдёт сам. Прав администратора домена не нужно; лучше отдельная служебная запись (например svc-bj): bj хранит её пароль, чтобы искать сотрудников.</p>
<div id="ad-found" class="ad-st" hidden aria-live="polite"></div>
<form method="post" action="/admin/setup/users/ad-connect" class="acc-form" id="ad-form">
<input type="hidden" name="return_to" value="{{$.Ret}}"><input type="hidden" name="section" value="acc-ad">
<label>Учётная запись домена<input type="text" name="ad_bind_user" data-need="учётная запись домена" value="{{$bu}}" placeholder="svc-bj или svc-bj@corp.example.ru" autocomplete="off"></label>
<label>Пароль{{if $pw}} <span class="muted">({{$pw}}; пусто — оставить)</span>{{end}}<input type="password" name="ad_bind_password" {{if not $pw}}data-need="пароль"{{end}} autocomplete="new-password"></label>
<details id="ad-adv" {{if $adv}}open{{end}} style="grid-column:1/-1;margin-top:2px"><summary style="cursor:pointer;font-size:13px;color:var(--muted)">Расширенные настройки — если домен не найдётся сам</summary>
<div class="acc-form" style="margin-top:8px">
<label>Домен<input type="text" name="ad_domain" value="{{$fd}}" placeholder="найдётся сам, например corp.example.ru" autocomplete="off"></label>
<label>Контроллер домена<input type="text" name="ad_server" value="{{$fs}}" placeholder="найдётся сам, например dc1.corp.example.ru" autocomplete="off"></label>
</div>
<p class="muted" style="font-size:12.5px;margin:6px 0 0">Пусто — bj ищет сам. Контроллер можно указать именем (bj попробует LDAPS 636, затем LDAP 389 со StartTLS) или адресом со схемой: ldaps://dc1.corp.example.ru:636.</p>
</details>
<div><button type="submit" class="btn">Подключить</button></div>
</form>
{{if $D}}<p style="margin:12px 0 0;padding:10px 12px;border-radius:10px;background:var(--card-2);font-size:13.5px;line-height:1.45"><b style="color:var(--err)">Не подключено:</b> {{$D.Err}}{{if not $D.Cert}} Поправьте и нажмите «Подключить» ещё раз — введённое сохранено.{{end}}</p>
{{template "adTrustCard" (withRet $D $.Ret)}}{{end}}
<script>(function(){
var box=document.getElementById('ad-connect'), outer=box.closest('details.acc-ad'), out=document.getElementById('ad-found'), adv=document.getElementById('ad-adv'), f=document.getElementById('ad-form'), done=false;
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]})}
function look(){ if(done||!box.open||(outer&&!outer.open)) return; done=true; out.hidden=false; out.className='ad-st'; out.innerHTML='<span class="ad-st-ic ad-st-spin"></span><div><b>Ищу домен…</b><small>по сетевым настройкам сервера bj</small></div>';
fetch('/admin/setup/users/ad-discover',{cache:'no-store',headers:{'Accept':'application/json'}}).then(function(r){return r.json()}).then(function(d){
var x=d.found;
out.hidden=false;
if(d.ok){ out.className='ad-st ad-st-ok'; out.innerHTML='<span class="ad-st-ic"></span><div><b>Найден домен '+esc(x.domain)+'</b><small>контроллеры: '+x.dcs.map(esc).join(', ')+' · '+esc(x.from)+'</small></div>';
f.ad_domain.placeholder=x.domain+' — найден сам'; f.ad_server.placeholder=x.dcs[0]+' — найден сам'; return; }
out.className='ad-st ad-st-warn'; out.innerHTML='<span class="ad-st-ic"></span><div><b>'+(x&&x.domain?'Домен '+esc(x.domain)+' найден, но его контроллеры — нет':'Домен сам не нашёлся')+'</b><small>'+(x&&x.domain?'укажите контроллер в расширенных настройках ниже':'укажите домен в расширенных настройках ниже или впишите учётную запись с доменом: svc-bj@corp.example.ru')+'</small></div>';
adv.open=true;
}).catch(function(){ out.hidden=true; done=false; });
}
box.addEventListener('toggle',look); if(outer) outer.addEventListener('toggle',look); look();
})();</script>
</details>
</details>
{{end}}