Пользователи в настройках — как в мастере: простой вид и расширенный под раскрытием
- Блок «Учётные записи» мастера вынесен в 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
This commit is contained in:
@@ -61,7 +61,6 @@
|
||||
.keywiz .card h2 { font-size:16px; margin:0 0 6px; }
|
||||
.keywiz form .btn-ok, .keywiz form .btn-secondary { justify-self:start; border-radius:999px; padding:10px 22px; font-weight:600; }
|
||||
.keywiz form .btn-ok { background:var(--accent); border-color:var(--accent); }
|
||||
.wiz-card .dq-link { background:none; border:0; padding:0; color:var(--accent); font-size:12.5px; font-weight:500; cursor:pointer; border-radius:4px; }
|
||||
/* ход подготовки: строки шагов со значком состояния */
|
||||
.tooltip { display:inline-block; background:var(--border); color:var(--muted); border-radius:50%; width:16px; height:16px; line-height:16px; text-align:center; font-size:11px; cursor:help; margin-left:4px; }
|
||||
.help-block { background:var(--accent-weak); padding:10px 14px; margin:10px 0; font-size:13px; border-radius:10px; }
|
||||
@@ -651,154 +650,7 @@
|
||||
<div class="card">
|
||||
<h2><span class="dot {{if .Done.Users}}ok{{else}}warn{{end}}"></span>Учётные записи</h2>
|
||||
<p>У каждого, кто работает с bj, — своя учётная запись: свой пароль и свои права, а в журнале видно, кто что сделал. Заведите сотрудников по ролям; права, пароли и вход через Active Directory меняются позже в разделе Настройка → Пользователи.</p>
|
||||
<style>
|
||||
.acc-roles{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin:12px 0 4px}
|
||||
.acc-role{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}
|
||||
.acc-role:has(input:checked){border-color:var(--accent);background:var(--accent-weak)}
|
||||
.acc-role input{margin-top:3px;flex:none;width:auto}
|
||||
.acc-role b{display:block;font-size:14.5px}
|
||||
.acc-role small{display:block;color:var(--muted);font-size:13px;line-height:1.4;margin-top:3px}
|
||||
.acc-role .acc-who{display:block;color:var(--text);font-size:12.5px;margin-top:6px}
|
||||
.acc-form{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;max-width:640px;margin-top:12px}
|
||||
.acc-form label{display:block;margin:0}
|
||||
.acc-form label input{width:100%;box-sizing:border-box;margin-top:4px}
|
||||
.acc-pw{display:flex;gap:6px;align-items:flex-end}
|
||||
.ad-st{display:flex;gap:12px;align-items:center;margin:12px 0 4px;padding:12px 16px;border-radius:12px;border:1px solid var(--border);background:var(--card-2);max-width:640px;box-sizing:border-box}
|
||||
.ad-st b{display:block;font-size:15px;font-weight:650}
|
||||
.ad-st small{display:block;color:var(--muted);font-size:13px;line-height:1.4;margin-top:2px;overflow-wrap:anywhere}
|
||||
.ad-st-ic{flex:none;position:relative;width:26px;height:26px;border-radius:50%;background:var(--border-strong)}
|
||||
.ad-st-ok{background:color-mix(in srgb,var(--ok) 9%,var(--card));border-color:color-mix(in srgb,var(--ok) 40%,var(--border))}
|
||||
.ad-st-ok .ad-st-ic{background:var(--ok)}
|
||||
.ad-st-ok .ad-st-ic::after{content:"";position:absolute;left:50%;top:45%;width:6px;height:11px;border:solid #fff;border-width:0 2.2px 2.2px 0;transform:translate(-50%,-50%) rotate(45deg)}
|
||||
.ad-st-warn{background:var(--warn-weak);border-color:color-mix(in srgb,var(--warn) 45%,var(--border))}
|
||||
.ad-st-warn .ad-st-ic{background:var(--warn)}
|
||||
.ad-st-warn .ad-st-ic::after{content:"!";position:absolute;inset:0;display:grid;place-items:center;color:#fff;font-weight:700;font-size:15px}
|
||||
.ad-st-spin{background:none;border:2.5px solid var(--accent-weak);border-top-color:var(--accent);box-sizing:border-box;animation:dq-spin .8s linear infinite}
|
||||
.ad-q-l{display:block;margin:14px 0 0;font-weight:600;font-size:14px;max-width:640px}
|
||||
.ad-q-l input{display:block;width:100%;box-sizing:border-box;margin-top:6px;font-size:15px !important;font-weight:400}
|
||||
.ad-res{list-style:none;margin:8px 0 0;padding:0;display:grid;gap:6px;max-width:640px}
|
||||
.ad-hit{display:flex;gap:10px;align-items:baseline;width:100%;text-align:left;padding:10px 14px;border:1px solid var(--border);border-radius:10px;background:var(--card);color:var(--text);font:inherit;font-size:14px;cursor:pointer}
|
||||
.ad-hit:hover:not(:disabled){border-color:var(--border-strong)}
|
||||
.ad-hit.is-sel{border-color:var(--accent);background:var(--accent-weak)}
|
||||
.ad-hit:disabled{opacity:.6;cursor:default}
|
||||
.ad-kind{flex:none;font-size:11px;font-weight:600;padding:2px 8px;border-radius:999px;background:var(--card-2);color:var(--muted);border:1px solid var(--border)}
|
||||
.ad-kind-g{background:var(--accent-weak);color:var(--accent);border-color:transparent}
|
||||
.ad-pick{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:12px;padding:12px 14px;border:1px solid var(--accent);border-radius:12px;background:color-mix(in srgb,var(--accent-weak) 50%,var(--card));max-width:640px;box-sizing:border-box}
|
||||
.ad-pick[hidden]{display:none}
|
||||
.ad-groups{margin-top:16px;max-width:640px}
|
||||
.ad-grp{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:8px 12px;border:1px solid var(--border);border-radius:10px;margin-top:6px;font-size:14px}
|
||||
.ad-grp form{margin:0}
|
||||
@media (max-width:600px){.acc-roles,.acc-form{grid-template-columns:minmax(0,1fr)}}
|
||||
</style>
|
||||
{{with .Fancy}}{{$U := .Users}}
|
||||
<form method="post" action="/admin/setup/users" id="acc-form">
|
||||
<input type="hidden" name="return_to" value="/admin/wizard?step=13"><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 "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="[^ /\@"'<>]+"></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>
|
||||
<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>
|
||||
{{$A := .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="/admin/wizard?step=13"><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 .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="/admin/wizard?step=13"><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 .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="/admin/wizard?step=13"><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{'&':'&','<':'<','>':'>','"':'"'}[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="/admin/wizard?step=13"><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 "/admin/wizard?step=13")}}{{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{'&':'&','<':'<','>':'>','"':'"'}[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>
|
||||
<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>
|
||||
{{with .Fancy}}{{template "accountsAdd" (withRet . "/admin/wizard?step=13")}}{{template "accountsAD" (withRet . "/admin/wizard?step=13")}}
|
||||
{{end}}
|
||||
{{template "wizNav" .}}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user