Пользователи в настройках — как в мастере: простой вид и расширенный под раскрытием

- Блок «Учётные записи» мастера вынесен в 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:
zuevav
2026-10-07 08:03:16 +03:00
parent 0b677f90ea
commit 6f9896d12e
5 changed files with 254 additions and 204 deletions
+6
View File
@@ -87,6 +87,12 @@ var templateFuncs = template.FuncMap{
v, certs := setupProbes(globalRC) v, certs := setupProbes(globalRC)
return v != "" && certs > 0 return v != "" && certs > 0
}, },
"avatarLetter": func(s string) string { // первая буква имени — для кружка в списке сотрудников
for _, r := range strings.TrimSpace(s) {
return strings.ToUpper(string(r))
}
return "?"
},
"adDraft": adDraftForView, // неудачная попытка подключить домен — форма открывается заполненной "adDraft": adDraftForView, // неудачная попытка подключить домен — форма открывается заполненной
"withRet": func(d any, ret string) map[string]any { return map[string]any{"D": d, "Ret": ret} }, "withRet": func(d any, ret string) map[string]any { return map[string]any{"D": d, "Ret": ret} },
"whForm": func(w WorkHoursSettings, ret string) map[string]any { return map[string]any{"W": w, "Ret": ret} }, "whForm": func(w WorkHoursSettings, ret string) map[string]any { return map[string]any{"W": w, "Ret": ret} },
@@ -107,51 +107,60 @@
<section class="settings-section" id="sec-users"> <section class="settings-section" id="sec-users">
{{$U := .Users}}{{$A := .Settings.Auth}} {{$U := .Users}}{{$A := .Settings.Auth}}
<h1>Пользователи и вход</h1> <h1>Пользователи и вход</h1>
<p class="muted">Кто может входить в bj и что ему разрешено. Вход — локальной учётной записью или доменной (Active Directory), роль доменного пользователя назначается по его группам AD.</p> <p class="muted">Кто может входить в bj и что ему разрешено. Сотрудник входит своей учётной записью bj или доменной (Active Directory) — тогда пароль проверяет домен.</p>
{{if $U.Open}}<div class="card callout" style="padding:10px 14px;margin-bottom:8px;background:color-mix(in srgb,var(--warn) 10%,var(--card));display:flex;gap:8px;align-items:center"><span class="dot warn"></span><span><b>Вход пока не требуется:</b> пользователи не созданы. Создайте первого администратора — после этого все страницы потребуют входа.</span></div>{{end}} {{if $U.Open}}<div class="card callout" style="padding:10px 14px;margin-bottom:8px;background:color-mix(in srgb,var(--warn) 10%,var(--card));display:flex;gap:8px;align-items:center"><span class="dot warn"></span><span><b>Вход пока не требуется:</b> пользователи не созданы. Создайте первого администратора — после этого все страницы потребуют входа.</span></div>{{end}}
<div class="card"> <div class="card">
<h2><span class="dot {{if $U.Open}}warn{{else}}ok{{end}}"></span>Роли</h2> <h2>Сотрудники ({{len $U.Users}})</h2>
{{if $U.Users}}<div class="usr-list">
{{range $U.Users}}
<details class="usr">
<summary>
<span class="usr-av" aria-hidden="true">{{avatarLetter (or .Name .Login)}}</span>
<span class="usr-main"><b>{{if .Name}}{{.Name}}{{else}}{{.Login}}{{end}}</b><small>{{.Login}}{{if .Email}} · {{.Email}}{{end}} · {{if eq .Source "ad"}}доменная учётная запись{{else}}пароль в bj{{end}} · {{if not .LastLogin.IsZero}}вход {{msk .LastLogin}}{{else}}ещё не входил{{end}}</small></span>
{{if not .Active}}<span class="badge rejected">заблокирован</span>{{end}}
<span class="usr-role">{{if $U.RoleShown .Role}}{{roleTitle .Role}}{{else}}{{$U.RoleNote .Role}}{{end}}</span>
<span class="usr-edit">Изменить</span>
</summary>
<form method="post" action="/admin/setup/users" class="usr-form">
<input type="hidden" name="login" value="{{.Login}}"><input type="hidden" name="source" value="{{.Source}}">
<label>Имя<input type="text" name="name" value="{{.Name}}" autocomplete="off"></label>
<label>E-mail<input type="text" name="email" value="{{.Email}}" autocomplete="off"></label>
<label>Роль<select name="role">{{$r := .Role}}{{if not ($U.RoleShown $r)}}<option value="{{$r}}" selected>{{$U.RoleNote $r}}</option>{{end}}{{range $U.Roles}}<option value="{{.Key}}" {{if eq .Key $r}}selected{{end}}>{{.Title}}</option>{{end}}</select></label>
{{if eq .Source "local"}}<label>Новый пароль<input type="password" name="password" placeholder="пусто — оставить прежний" autocomplete="new-password"></label>{{else}}<div class="usr-note">Пароль проверяет домен{{if .ADGroups}} · группы: {{range $i,$g := .ADGroups}}{{if $i}}, {{end}}{{$g}}{{end}}{{end}}{{if .Note}} · {{.Note}}{{end}}</div>{{end}}
<div class="usr-checks">{{$m := .Modules}}{{range $U.Modules}}<label><input type="checkbox" name="modules" value="{{.Key}}" {{if has $m .Key}}checked{{end}}>{{.Title}}</label>{{end}}
<label><input type="checkbox" name="active" value="1" {{if .Active}}checked{{end}}>может входить</label></div>
<div class="usr-act"><button class="btn" type="submit">Сохранить</button>
<button class="usr-del" type="submit" formaction="/admin/setup/users/delete" onclick="return confirm('Удалить сотрудника {{.Login}}? Войти он больше не сможет.')">Удалить сотрудника</button></div>
</form>
</details>
{{end}}</div>{{else}}<p class="muted">Пока никого — добавьте сотрудника ниже.</p>{{end}}
</div>
<div class="card">
<h2>{{if $U.Open}}Создать первого администратора{{else}}Добавить сотрудника{{end}}</h2>
{{template "accountsAdd" (withRet . "/admin/setup")}}
{{template "accountsAD" (withRet . "/admin/setup")}}
</div>
<div class="card">
<h2>Активные сессии ({{len $U.Sessions}})</h2>
{{if $U.Sessions}}<table style="font-size:12.5px"><thead><tr><th>Пользователь</th><th>Вход</th><th>Активность</th><th>Адрес</th><th>Браузер</th><th></th></tr></thead><tbody>
{{range $U.Sessions}}<tr><td><b>{{.Login}}</b></td><td>{{msk .Created}}</td><td>{{msk .Seen}}</td><td><code>{{.IP}}</code></td><td class="muted" style="font-size:11px;max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{.UA}}</td>
<td><form method="post" action="/admin/setup/users/session-drop"><input type="hidden" name="id" value="{{.ID}}"><button class="btn" type="submit">Завершить</button></form></td></tr>{{end}}
</tbody></table>{{else}}<p class="muted">Сессий нет.</p>{{end}}
</div>
<div class="card">
<details class="adv-box">
<summary><span class="sum-h">Расширенные настройки</span><span class="muted">роли и права, домен вручную: контроллер, сертификат, группы, перепроверка, время сессии</span></summary>
<h3 class="adv-h">Роли и права</h3>
<table style="font-size:12.5px"><thead><tr><th>Роль</th><th>Что разрешено</th></tr></thead><tbody> <table style="font-size:12.5px"><thead><tr><th>Роль</th><th>Что разрешено</th></tr></thead><tbody>
{{range $U.Roles}}<tr><td style="white-space:nowrap"><b>{{.Title}}</b><br><code style="font-size:11px">{{.Key}}</code></td><td>{{.Desc}}</td></tr>{{end}} {{range $U.Roles}}<tr><td style="white-space:nowrap"><b>{{.Title}}</b><br><code style="font-size:11px">{{.Key}}</code></td><td>{{.Desc}}</td></tr>{{end}}
{{range $U.Hidden}}<tr class="muted"><td style="white-space:nowrap">{{.Title}}</td><td>Недоступна: в лицензии нет модуля «{{.Missing}}».</td></tr>{{end}} {{range $U.Hidden}}<tr class="muted"><td style="white-space:nowrap">{{.Title}}</td><td>Недоступна: в лицензии нет модуля «{{.Missing}}».</td></tr>{{end}}
</tbody></table> </tbody></table>
<p class="muted" style="font-size:12px;margin-top:6px">{{if $U.Modules}}Подключённые модули ({{range $i, $m := $U.Modules}}{{if $i}}, {{end}}«{{$m.Title}}»{{end}}) можно выдать любому пользователю дополнительно к роли. {{end}}Роли и модули показываются по лицензии: права модуля, которого нет в лицензии, не действуют. Настройки и пользователи — только администратор.</p> <p class="muted" style="font-size:12px;margin-top:6px">{{if $U.Modules}}Подключённые модули ({{range $i, $m := $U.Modules}}{{if $i}}, {{end}}«{{$m.Title}}»{{end}}) можно выдать любому пользователю дополнительно к роли. {{end}}Роли и модули показываются по лицензии: права модуля, которого нет в лицензии, не действуют. Настройки и пользователи — только администратор.</p>
</div> <h3 class="adv-h">Вход через Active Directory</h3>
<div class="card">
<h2>Пользователи ({{len $U.Users}})</h2>
{{range $U.Users}}
<form method="post" action="/admin/setup/users" style="display:grid;grid-template-columns:1.1fr 1.4fr 1.2fr 1fr auto;gap:8px;align-items:end;padding:10px 0;border-bottom:1px solid var(--border)">
<input type="hidden" name="login" value="{{.Login}}"><input type="hidden" name="source" value="{{.Source}}">
<div><div style="font-weight:600">{{.Login}} {{if eq .Source "ad"}}<span class="badge" style="font-size:10px">AD</span>{{end}}{{if not .Active}}<span class="badge rejected" style="font-size:10px">заблокирован</span>{{end}}</div>
<div class="muted" style="font-size:11px">{{if not .LastLogin.IsZero}}вход {{msk .LastLogin}}{{else}}ещё не входил{{end}}{{if .Note}} · {{.Note}}{{end}}</div>
{{if .ADGroups}}<div class="muted" style="font-size:11px">группы: {{range $i,$g := .ADGroups}}{{if $i}}, {{end}}{{$g}}{{end}}</div>{{end}}</div>
<label style="margin:0;font-size:11px">Имя <input type="text" name="name" value="{{.Name}}" style="width:100%"><span class="muted">E-mail</span> <input type="text" name="email" value="{{.Email}}" style="width:100%"></label>
<label style="margin:0;font-size:11px">Роль <select name="role" style="width:100%">{{$r := .Role}}{{if not ($U.RoleShown $r)}}<option value="{{$r}}" selected>{{$U.RoleNote $r}}</option>{{end}}{{range $U.Roles}}<option value="{{.Key}}" {{if eq .Key $r}}selected{{end}}>{{.Title}}</option>{{end}}</select>
<div style="margin-top:4px">{{$m := .Modules}}{{range $U.Modules}}<label style="display:inline-flex;gap:4px;align-items:center;font-weight:400;margin:0 8px 0 0;font-size:11px"><input type="checkbox" name="modules" value="{{.Key}}" {{if has $m .Key}}checked{{end}}>{{.Title}}</label>{{end}}</div></label>
<label style="margin:0;font-size:11px">{{if eq .Source "local"}}Новый пароль <input type="password" name="password" placeholder="оставьте пустым" autocomplete="new-password" style="width:100%">{{else}}<span class="muted">пароль проверяет AD</span>{{end}}
<div style="margin-top:4px"><label style="display:inline-flex;gap:4px;align-items:center;font-weight:400;margin:0;font-size:11px"><input type="checkbox" name="active" value="1" {{if .Active}}checked{{end}}>активен</label></div></label>
<div style="display:flex;gap:6px"><button class="btn btn-ok" type="submit">Сохранить</button><button class="btn" type="submit" formaction="/admin/setup/users/delete" onclick="return confirm('Удалить пользователя {{.Login}}?')">Удалить</button></div>
</form>
{{else}}<p class="muted">Пользователей нет.</p>{{end}}
<h3 style="margin:16px 0 6px">{{if $U.Open}}Создать первого администратора{{else}}Добавить пользователя{{end}}</h3>
<form method="post" action="/admin/setup/users" style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:10px;max-width:900px;align-items:end">
<label>Логин <input type="text" name="login" placeholder="ivanov" required style="width:100%"></label>
<label>Имя <input type="text" name="name" placeholder="Иванов И.И." style="width:100%"></label>
<label>Роль <select name="role" style="width:100%">{{range $U.Roles}}<option value="{{.Key}}" {{if and $U.Open (eq .Key "admin")}}selected{{end}}>{{.Title}}</option>{{end}}</select></label>
<label>Источник <select name="source" style="width:100%" {{if $U.Open}}disabled{{end}}><option value="local">Локальный (пароль в bj)</option><option value="ad" {{if not $A.ADEnabled}}disabled{{end}}>Active Directory</option></select></label>
<label>Пароль (для локального, от 8 символов) <input type="password" name="password" autocomplete="new-password" style="width:100%"></label>
<label>E-mail <input type="text" name="email" style="width:100%"></label>
{{if $U.Modules}}<label>Модули <div>{{range $U.Modules}}<label style="display:inline-flex;gap:4px;align-items:center;font-weight:400;margin:0 8px 0 0;font-size:12px"><input type="checkbox" name="modules" value="{{.Key}}">{{.Title}}</label>{{end}}</div></label>{{end}}
<div><input type="hidden" name="active" value="1"><button class="btn btn-ok" type="submit">{{if $U.Open}}Создать администратора{{else}}Добавить{{end}}</button></div>
</form>
<p class="muted" style="font-size:12px;margin-top:8px">Доменного пользователя можно заранее завести с источником «Active Directory», чтобы выдать ему модули; роль при входе возьмётся из групп AD. Если потерян доступ администратора — удалите файл <code>/var/lib/bj/.bj/users.json</code> на сервере: система вернётся в режим первичной настройки.</p>
</div>
<div class="card">
<h2><span class="dot {{if $A.ADEnabled}}ok{{else}}warn{{end}}"></span>Вход через Active Directory</h2>
<p class="muted">bj подключается к контроллеру домена и проверяет пароль пользователя (LDAP bind его же учётной записью). Роль назначается по членству в группах AD, включая вложенные. Пользователь появляется в списке при первом входе. Чтобы изменения групп действовали без повторного входа, укажите сервисную учётную запись только на чтение: bj будет перечитывать группы доменных пользователей по расписанию, менять роль и снимать доступ у удалённых из групп.</p> <p class="muted">bj подключается к контроллеру домена и проверяет пароль пользователя (LDAP bind его же учётной записью). Роль назначается по членству в группах AD, включая вложенные. Пользователь появляется в списке при первом входе. Чтобы изменения групп действовали без повторного входа, укажите сервисную учётную запись только на чтение: bj будет перечитывать группы доменных пользователей по расписанию, менять роль и снимать доступ у удалённых из групп.</p>
<form method="post" action="/admin/setup/auth" style="display:grid;gap:10px;max-width:820px;margin-top:10px"> <form method="post" action="/admin/setup/auth" style="display:grid;gap:10px;max-width:820px;margin-top:10px">
<label style="display:flex;gap:8px;align-items:center;font-weight:600;margin:0"><input type="checkbox" name="ad_enabled" value="1" {{if $A.ADEnabled}}checked{{end}}> Разрешить вход доменной учётной записью</label> <label style="display:flex;gap:8px;align-items:center;font-weight:600;margin:0"><input type="checkbox" name="ad_enabled" value="1" {{if $A.ADEnabled}}checked{{end}}> Разрешить вход доменной учётной записью</label>
@@ -159,19 +168,19 @@
<label>Контроллер домена <input type="text" name="ad_server" value="{{$A.ADServer}}" placeholder="ldaps://dc1.corp.example.ru:636" style="width:100%"></label> <label>Контроллер домена <input type="text" name="ad_server" value="{{$A.ADServer}}" placeholder="ldaps://dc1.corp.example.ru:636" style="width:100%"></label>
<label>Домен (суффикс UPN) <input type="text" name="ad_domain" value="{{$A.ADDomain}}" placeholder="corp.example.ru" style="width:100%"></label> <label>Домен (суффикс UPN) <input type="text" name="ad_domain" value="{{$A.ADDomain}}" placeholder="corp.example.ru" style="width:100%"></label>
</div> </div>
<label>База поиска (Base DN) <span class="muted">пусто = взять с контроллера</span> <input type="text" name="ad_base_dn" value="{{$A.ADBaseDN}}" placeholder="DC=corp,DC=example,DC=ru" style="width:100%"></label> <label>База поиска (Base DN) <input type="text" name="ad_base_dn" value="{{$A.ADBaseDN}}" placeholder="пусто — возьмётся с контроллера" style="width:100%"></label>
<div style="display:flex;gap:18px;flex-wrap:wrap"> <div style="display:flex;gap:18px;flex-wrap:wrap">
<label style="display:flex;gap:6px;align-items:center;font-weight:400;margin:0"><input type="checkbox" name="ad_starttls" value="1" {{if $A.ADStartTLS}}checked{{end}}> StartTLS для ldap://</label> <label style="display:flex;gap:6px;align-items:center;font-weight:400;margin:0"><input type="checkbox" name="ad_starttls" value="1" {{if $A.ADStartTLS}}checked{{end}}> StartTLS для ldap://</label>
<label style="display:flex;gap:6px;align-items:center;font-weight:400;margin:0"><input type="checkbox" name="ad_skip_verify" value="1" {{if $A.ADSkipVerify}}checked{{end}}> Не проверять сертификат контроллера</label> <label style="display:flex;gap:6px;align-items:center;font-weight:400;margin:0"><input type="checkbox" name="ad_skip_verify" value="1" {{if $A.ADSkipVerify}}checked{{end}}> Не проверять сертификат контроллера</label>
</div> </div>
<label>Сертификат корпоративного ЦС (PEM), если проверяем <textarea name="ad_ca_pem" rows="3" style="width:100%;font-family:ui-monospace,monospace;font-size:11px" placeholder="-----BEGIN CERTIFICATE-----">{{$A.ADCAPEM}}</textarea></label> <label>Сертификат центра сертификации домена (PEM) <textarea name="ad_ca_pem" rows="3" style="width:100%;font-family:ui-monospace,monospace;font-size:11px" placeholder="-----BEGIN CERTIFICATE-----">{{$A.ADCAPEM}}</textarea></label>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px"> <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px">
<label>Сервисная учётная запись <span class="muted">для перепроверки групп, только чтение</span> <input type="text" name="ad_bind_user" value="{{$A.ADBindUser}}" placeholder="svc-bj@corp.example.ru" autocomplete="off" style="width:100%"></label> <label>Служебная учётная запись <input type="text" name="ad_bind_user" value="{{$A.ADBindUser}}" placeholder="svc-bj@corp.example.ru" autocomplete="off" style="width:100%"></label>
<label>Пароль сервисной учётной записи <input type="password" name="ad_bind_password" placeholder="{{if $A.ADBindPassword}}•••••• — оставьте пустым, чтобы не менять{{else}}пароль{{end}}" autocomplete="new-password" style="width:100%"></label> <label>Её пароль <input type="password" name="ad_bind_password" placeholder="{{if $A.ADBindPassword}}задан — пусто, чтобы не менять{{else}}пароль{{end}}" autocomplete="new-password" style="width:100%"></label>
<label>Перепроверять группы, минут <input type="number" name="ad_recheck_minutes" min="5" max="1440" value="{{if $A.ADRecheckMinutes}}{{$A.ADRecheckMinutes}}{{else}}15{{end}}" style="width:100%"></label> <label>Перепроверять группы, минут <input type="number" name="ad_recheck_minutes" min="5" max="1440" value="{{if $A.ADRecheckMinutes}}{{$A.ADRecheckMinutes}}{{else}}15{{end}}" style="width:100%"></label>
</div> </div>
<div> <div>
<div style="font-weight:600;margin-bottom:4px">Группы AD → роли <span class="muted" style="font-weight:400;font-size:12px">(имя группы или полный DN; первое совпадение сверху)</span></div> <div style="font-weight:600;font-size:13.5px;margin:4px 0 6px">Группы домена → роли <span class="muted" style="font-weight:400;font-size:12.5px">имя группы или полный DN; действует первое совпадение сверху</span></div>
<div id="ad-groups" style="display:grid;gap:6px"> <div id="ad-groups" style="display:grid;gap:6px">
{{range $A.ADGroups}}<div style="display:grid;grid-template-columns:2fr 1fr;gap:8px"><input type="text" name="ad_group" value="{{.Group}}" placeholder="BJ-Operators"><select name="ad_role">{{$r := .Role}}{{if not ($U.RoleShown $r)}}<option value="{{$r}}" selected>{{$U.RoleNote $r}} (группа не действует)</option>{{end}}{{range $U.Roles}}<option value="{{.Key}}" {{if eq .Key $r}}selected{{end}}>{{.Title}}</option>{{end}}</select></div>{{end}} {{range $A.ADGroups}}<div style="display:grid;grid-template-columns:2fr 1fr;gap:8px"><input type="text" name="ad_group" value="{{.Group}}" placeholder="BJ-Operators"><select name="ad_role">{{$r := .Role}}{{if not ($U.RoleShown $r)}}<option value="{{$r}}" selected>{{$U.RoleNote $r}} (группа не действует)</option>{{end}}{{range $U.Roles}}<option value="{{.Key}}" {{if eq .Key $r}}selected{{end}}>{{.Title}}</option>{{end}}</select></div>{{end}}
{{range $i := list "1" "2"}}<div style="display:grid;grid-template-columns:2fr 1fr;gap:8px"><input type="text" name="ad_group" value="" placeholder="имя группы AD"><select name="ad_role">{{range $U.Roles}}<option value="{{.Key}}">{{.Title}}</option>{{end}}</select></div>{{end}} {{range $i := list "1" "2"}}<div style="display:grid;grid-template-columns:2fr 1fr;gap:8px"><input type="text" name="ad_group" value="" placeholder="имя группы AD"><select name="ad_role">{{range $U.Roles}}<option value="{{.Key}}">{{.Title}}</option>{{end}}</select></div>{{end}}
@@ -181,30 +190,23 @@
<label>Роль, если ни одна группа не совпала <select name="ad_default_role" style="width:100%"><option value="">отказать во входе</option>{{if and $A.ADDefaultRole (not ($U.RoleShown $A.ADDefaultRole))}}<option value="{{$A.ADDefaultRole}}" selected>{{$U.RoleNote $A.ADDefaultRole}} (вход будет отклонён)</option>{{end}}{{range $U.Roles}}<option value="{{.Key}}" {{if eq .Key $A.ADDefaultRole}}selected{{end}}>{{.Title}}</option>{{end}}</select></label> <label>Роль, если ни одна группа не совпала <select name="ad_default_role" style="width:100%"><option value="">отказать во входе</option>{{if and $A.ADDefaultRole (not ($U.RoleShown $A.ADDefaultRole))}}<option value="{{$A.ADDefaultRole}}" selected>{{$U.RoleNote $A.ADDefaultRole}} (вход будет отклонён)</option>{{end}}{{range $U.Roles}}<option value="{{.Key}}" {{if eq .Key $A.ADDefaultRole}}selected{{end}}>{{.Title}}</option>{{end}}</select></label>
<label>Выход при бездействии, часов <input type="number" name="session_hours" min="1" max="24" value="{{if $A.SessionHours}}{{$A.SessionHours}}{{else}}8{{end}}" style="width:100%"></label> <label>Выход при бездействии, часов <input type="number" name="session_hours" min="1" max="24" value="{{if $A.SessionHours}}{{$A.SessionHours}}{{else}}8{{end}}" style="width:100%"></label>
</div> </div>
<div style="display:flex;gap:8px;flex-wrap:wrap"><button class="btn btn-ok" type="submit">Сохранить</button></div> <div style="display:flex;gap:8px;flex-wrap:wrap"><button class="btn" type="submit">Сохранить</button></div>
</form> </form>
{{with adDraft}}{{template "adTrustCard" (withRet . "")}}{{end}}
<form method="post" action="/admin/setup/auth/test" style="display:grid;grid-template-columns:1fr 1fr auto;gap:10px;max-width:820px;margin-top:12px;align-items:end"> <form method="post" action="/admin/setup/auth/test" style="display:grid;grid-template-columns:1fr 1fr auto;gap:10px;max-width:820px;margin-top:12px;align-items:end">
<label>Проверка: логин домена <input type="text" name="test_login" placeholder="ivanov" style="width:100%" autocomplete="off"></label> <label>Проверить вход: логин домена <input type="text" name="test_login" placeholder="ivanov" style="width:100%" autocomplete="off"></label>
<label>Пароль <input type="password" name="test_password" style="width:100%" autocomplete="off"></label> <label>Пароль <input type="password" name="test_password" style="width:100%" autocomplete="off"></label>
<button class="btn" type="submit">Проверить AD</button> <button class="btn btn-secondary" type="submit">Проверить</button>
</form> </form>
<p class="muted" style="font-size:12px;margin-top:6px">Без логина и пароля проверяется только доступность контроллера. С логином — вход, группы и вычисленная роль. Пароль проверки никуда не сохраняется.</p> <p class="muted" style="font-size:12px;margin-top:6px">Без логина и пароля проверяется только доступность контроллера. С логином — вход, группы и вычисленная роль. Пароль проверки никуда не сохраняется.</p>
{{$R := $U.ADRecheck}} {{$R := $U.ADRecheck}}
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:10px;font-size:12.5px"> <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:10px;font-size:12.5px">
{{if and $A.ADEnabled $A.ADBindUser $A.ADBindPassword}} {{if and $A.ADEnabled $A.ADBindUser $A.ADBindPassword}}
<span><span class="dot {{if $R.Err}}err{{else if $R.At.IsZero}}warn{{else}}ok{{end}}"></span>Перепроверка групп каждые {{if $A.ADRecheckMinutes}}{{$A.ADRecheckMinutes}}{{else}}15{{end}} мин: {{if $R.At.IsZero}}ещё не выполнялась{{else}}последняя {{msk $R.At}}, проверено {{$R.Checked}}, изменений {{$R.Changed}}{{if $R.Err}} — <b>ошибка:</b> {{$R.Err}}{{end}}{{end}}</span> <span><span class="dot {{if $R.Err}}err{{else if $R.At.IsZero}}warn{{else}}ok{{end}}"></span>Перепроверка групп каждые {{if $A.ADRecheckMinutes}}{{$A.ADRecheckMinutes}}{{else}}15{{end}} мин: {{if $R.At.IsZero}}ещё не выполнялась{{else}}последняя {{msk $R.At}}, проверено {{$R.Checked}}, изменений {{$R.Changed}}{{if $R.Err}} — <b>ошибка:</b> {{$R.Err}}{{end}}{{end}}</span>
<form method="post" action="/admin/setup/auth/recheck"><button class="btn" type="submit">Перепроверить сейчас</button></form> <form method="post" action="/admin/setup/auth/recheck"><button class="btn btn-secondary" type="submit">Перепроверить сейчас</button></form>
{{else}}<span class="muted"><span class="dot warn"></span>Перепроверка групп выключена: роль доменного пользователя меняется только при следующем входе. Укажите сервисную учётную запись выше.</span>{{end}} {{else}}<span class="muted"><span class="dot warn"></span>Перепроверка групп выключена: роль доменного пользователя меняется только при следующем входе. Укажите сервисную учётную запись выше.</span>{{end}}
</div> </div>
</div> <p class="muted" style="font-size:12.5px;margin-top:14px">Если потерян доступ администратора — удалите файл <code>/var/lib/bj/.bj/users.json</code> на сервере: система вернётся в режим первичной настройки.</p>
</details>
<div class="card">
<h2>Активные сессии ({{len $U.Sessions}})</h2>
{{if $U.Sessions}}<table style="font-size:12.5px"><thead><tr><th>Пользователь</th><th>Вход</th><th>Активность</th><th>Адрес</th><th>Браузер</th><th></th></tr></thead><tbody>
{{range $U.Sessions}}<tr><td><b>{{.Login}}</b></td><td>{{msk .Created}}</td><td>{{msk .Seen}}</td><td><code>{{.IP}}</code></td><td class="muted" style="font-size:11px;max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{.UA}}</td>
<td><form method="post" action="/admin/setup/users/session-drop"><input type="hidden" name="id" value="{{.ID}}"><button class="btn" type="submit">Завершить</button></form></td></tr>{{end}}
</tbody></table>{{else}}<p class="muted">Сессий нет.</p>{{end}}
</div> </div>
</section> </section>
@@ -61,7 +61,6 @@
.keywiz .card h2 { font-size:16px; margin:0 0 6px; } .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, .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); } .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; } .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; } .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"> <div class="card">
<h2><span class="dot {{if .Done.Users}}ok{{else}}warn{{end}}"></span>Учётные записи</h2> <h2><span class="dot {{if .Done.Users}}ok{{else}}warn{{end}}"></span>Учётные записи</h2>
<p>У каждого, кто работает с bj, — своя учётная запись: свой пароль и свои права, а в журнале видно, кто что сделал. Заведите сотрудников по ролям; права, пароли и вход через Active Directory меняются позже в разделе Настройка → Пользователи.</p> <p>У каждого, кто работает с bj, — своя учётная запись: свой пароль и свои права, а в журнале видно, кто что сделал. Заведите сотрудников по ролям; права, пароли и вход через Active Directory меняются позже в разделе Настройка → Пользователи.</p>
<style> {{with .Fancy}}{{template "accountsAdd" (withRet . "/admin/wizard?step=13")}}{{template "accountsAD" (withRet . "/admin/wizard?step=13")}}
.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="[^ /\@&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>
<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{'&':'&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="/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{'&':'&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>
<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}} {{end}}
{{template "wizNav" .}} {{template "wizNav" .}}
</div> </div>
@@ -301,6 +301,78 @@ label { font-size:13px; font-weight:500; }
:is(.settings-section, .wiz-card) .grid > div > label:not(:has(input[type=checkbox], input[type=radio])) { display:block; margin:0; } :is(.settings-section, .wiz-card) .grid > div > label:not(:has(input[type=checkbox], input[type=radio])) { display:block; margin:0; }
:is(.settings-section, .wiz-card) .grid > div > :is(input:not([type=checkbox]):not([type=radio]):not([type=hidden]), select, textarea) { display:block; width:100%; box-sizing:border-box; margin-top:5px !important; } :is(.settings-section, .wiz-card) .grid > div > :is(input:not([type=checkbox]):not([type=radio]):not([type=hidden]), select, textarea) { display:block; width:100%; box-sizing:border-box; margin-top:5px !important; }
@media (max-width:820px) { .settings-section.active { padding:20px 18px 18px; } } @media (max-width:820px) { .settings-section.active { padding:20px 18px 18px; } }
/* Учётные записи (part_accounts: мастер и Настройка → Пользователи) */
.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)}}
.acc-mods{display:flex;gap:6px 16px;flex-wrap:wrap;align-items:center;margin:12px 0 0;font-size:13.5px}
.acc-mods label{display:inline-flex;gap:6px;align-items:center;font-weight:400;margin:0;font-size:13.5px}
/* сотрудники в Настройка → Пользователи: строка — кто и с какой ролью, «Изменить» раскрывает правку */
.usr-list{display:grid;gap:8px;margin-top:10px}
.usr{border:1px solid var(--border);border-radius:12px;background:var(--card)}
.usr[open]{border-color:color-mix(in srgb, var(--accent) 45%, var(--border))}
.usr>summary{display:flex;align-items:center;gap:12px;padding:10px 14px;cursor:pointer;list-style:none}
.usr>summary::-webkit-details-marker{display:none}
.usr>summary:hover .usr-edit{text-decoration:underline}
.usr-av{flex:none;width:34px;height:34px;border-radius:50%;background:var(--accent-weak);color:var(--accent);display:grid;place-items:center;font-weight:700;font-size:14px}
.usr-main{flex:1;min-width:0}
.usr-main b{display:block;font-size:14.5px;font-weight:600}
.usr-main small{display:block;color:var(--muted);font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.usr-role{flex:none;font-size:12.5px;font-weight:600;padding:3px 10px;border-radius:999px;background:var(--card-2);border:1px solid var(--border);color:var(--text-2)}
.usr-edit{flex:none;font-size:13px;color:var(--accent);min-width:64px;text-align:right}
.usr[open] .usr-edit{visibility:hidden}
.usr-form{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;padding:14px;border-top:1px solid var(--border)}
.usr-form label{display:block;margin:0}
.usr-form label input:not([type=checkbox]), .usr-form label select{display:block;width:100%;box-sizing:border-box;margin-top:5px}
.usr-note{align-self:end;color:var(--muted);font-size:13px;line-height:1.4;padding-bottom:10px}
.usr-checks{grid-column:1/-1;display:flex;gap:6px 18px;flex-wrap:wrap}
.usr-checks label{display:inline-flex;gap:6px;align-items:center;font-weight:400;font-size:13.5px}
.usr-act{grid-column:1/-1;display:flex;justify-content:space-between;align-items:center;gap:8px}
.usr-del{background:none !important;border:0 !important;color:var(--err);font-size:13.5px;font-weight:500;padding:6px 2px;cursor:pointer}
.usr-del:hover{text-decoration:underline}
@media (max-width:600px){.usr-form{grid-template-columns:minmax(0,1fr)} .usr-main small{white-space:normal}}
/* «Расширенные настройки» — под раскрытием, заголовком подблока */
.adv-box>summary{display:flex;flex-direction:column;gap:2px;cursor:pointer;list-style:none}
.adv-box>summary::-webkit-details-marker{display:none}
.adv-box>summary .sum-h{font-size:16px;font-weight:650;color:var(--accent)}
.adv-box>summary .sum-h::before{content:"▸ ";color:var(--muted)}
.adv-box[open]>summary .sum-h::before{content:"▾ "}
.adv-box>summary .muted{font-size:13px}
.adv-h{font-size:14.5px;font-weight:650;margin:22px 0 8px}
@media (max-width:820px) { @media (max-width:820px) {
.settings { grid-template-columns:1fr; } .settings { grid-template-columns:1fr; }
.settings-nav { flex-direction:row; overflow-x:auto; position:static; padding-bottom:8px; } .settings-nav { flex-direction:row; overflow-x:auto; position:static; padding-bottom:8px; }
@@ -390,6 +462,7 @@ details:has(> pre.copyable) { min-width:0; max-width:100%; }
.dq-box-h { font-weight:600; font-size:14.5px; margin:0 0 10px; } .dq-box-h { font-weight:600; font-size:14.5px; margin:0 0 10px; }
.dq-box-h span { color:var(--muted); font-weight:400; font-size:13px; } .dq-box-h span { color:var(--muted); font-weight:400; font-size:13px; }
.dq-lbl-row { display:flex; justify-content:space-between; align-items:baseline; gap:8px; } .dq-lbl-row { display:flex; justify-content:space-between; align-items:baseline; gap:8px; }
button.dq-link, .dq-link { background:none !important; border:0 !important; padding:0 !important; color:var(--accent) !important; font-size:12.5px; font-weight:500; cursor:pointer; border-radius:4px; box-shadow:none !important; }
.dq-link:hover { text-decoration:underline; } .dq-link:hover { text-decoration:underline; }
.dq-warn { margin:10px 0 0; padding:8px 12px; border-radius:8px; background:var(--warn-weak); color:var(--text-2); font-size:13px; } .dq-warn { margin:10px 0 0; padding:8px 12px; border-radius:8px; background:var(--warn-weak); color:var(--text-2); font-size:13px; }
.dq-run { margin-top:16px; border:1px solid var(--border); border-radius:12px; padding:12px 16px; } .dq-run { margin-top:16px; border:1px solid var(--border); border-radius:12px; padding:12px 16px; }
@@ -0,0 +1,117 @@
{{/* Учётные записи: добавить сотрудника (роль плиткой, логин, пароль) и сотрудники из домена (подключение,
поиск сотрудников и групп, доступ группам). Общий блок шага мастера «Учётные записи» и раздела
Настройка → Пользователи. Вход: 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}}