Задачи 034-035: публикация через Caddy, последний вход пользователя
034 Публикация стенда как https://rxipam.rxmsk.ru через общий Caddy хоста (Caddyfile вне репозитория): UI и /api/v1 доступны из интернета, /docs, /redoc, /openapi.json — только из частных сетей (RFC 1918/4193, loopback). Caddy проксирует на опубликованный порт приложения — общая Docker-сеть отклонена из-за коллизии имён app/db с Nextcloud. TRUSTED_PROXIES=172.16.0.0/12: реальный IP клиента из X-Forwarded-For. Интеграционный тест журнала больше не проверяет подделку XFF с хоста (хост за Caddy доверенный), логику покрывает unit-тест. 035 Дата и IP последнего входа в разделе «Пользователи»: users.last_login_at / last_login_ip (миграция 0014 с заполнением из журнала), запись при успешном входе, UserOut, колонка в UI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
744a025960
commit
8384c2c311
13 files changed
+330
-11
No files matched your search
+3
-2
@@ -698,7 +698,7 @@ screens.users = async () => {
|
||||
const q = S.q || "";
|
||||
const { items, total } = await api("/users", { params: { q, limit: 500 } });
|
||||
S.rows = items;
|
||||
const cols = selCols("minmax(200px,1.5fr) 190px minmax(180px,1fr) 190px minmax(140px,1fr) 44px"); // изменение 032: добавлен столбец для организации
|
||||
const cols = selCols("minmax(200px,1.5fr) 190px minmax(180px,1fr) 190px 180px minmax(140px,1fr) 44px"); // изменение 032: столбец организации; изменение 035: столбец последнего входа
|
||||
setSelectable(items.filter((u) => u.id !== user?.id).map((u) => u.id)); // свою запись выбрать нельзя
|
||||
const rows = items.map((u, n) => {
|
||||
const self = u.id === user?.id;
|
||||
@@ -713,12 +713,13 @@ screens.users = async () => {
|
||||
<span>${badge(u.role === "superadmin" ? "purple" : u.role === "admin" ? "blue" : "", ROLE_RU[u.role] || u.role)}</span>
|
||||
<span class="muted" style="font-size:13px">${u.role === "superadmin" ? "—" : esc(orgName)}</span>
|
||||
<span>${u.is_active ? badge("green", "Доступ разрешён") : badge("red", "Отключён")}</span>
|
||||
<span style="font-size:13px">${u.last_login_at ? `<span style="display:flex;flex-direction:column;gap:2px"><span>${fmtDate(u.last_login_at)}</span><span class="mono muted" style="font-size:12px">${esc(u.last_login_ip || "—")}</span></span>` : '<span class="muted">—</span>'}</span>
|
||||
<span class="muted" style="font-size:13px">${self ? "свои роль и доступ менять нельзя" : ""}</span>
|
||||
<span class="cell-actions rel">${iconBtn(I.dots(), "menu", "Действия", `data-menu="user-row-${u.id}"`)}${popMenu("user-row-" + u.id, items2, "row-pop")}</span></div>`;
|
||||
}).join("");
|
||||
return shell("users", `${header("Пользователи", `${total} ${plural(total, "учётная запись", "учётные записи", "учётных записей")} · роли: суперадминистратор — полные права, администратор — запись в организации, просмотр — чтение`, btn("Добавить пользователя", "users-new", { cls: "primary", icon: I.plus() }))}
|
||||
<div class="card"><div class="filters">${searchBox(q, "Поиск по логину", 260)}</div>
|
||||
${bulkBar(btn("Разрешить доступ", "bulk-access", { data: 'data-value="1"' }) + btn("Отключить доступ", "bulk-access", { data: 'data-value="0"' }))}<div class="tr th" style="--cols:${cols};--h:40px">${selAllCell()}<span>Логин</span><span>Роль</span><span>Организация</span><span>Статус</span><span></span><span></span></div>
|
||||
${bulkBar(btn("Разрешить доступ", "bulk-access", { data: 'data-value="1"' }) + btn("Отключить доступ", "bulk-access", { data: 'data-value="0"' }))}<div class="tr th" style="--cols:${cols};--h:40px">${selAllCell()}<span>Логин</span><span>Роль</span><span>Организация</span><span>Статус</span><span>Последний вход</span><span></span><span></span></div>
|
||||
${rows || '<div class="empty">Пользователей нет</div>'}<div class="foot">Показано ${items.length} из ${total} ${plural(total, "записи", "записей", "записей")}</div></div>`);
|
||||
};
|
||||
function userDialog(u) {
|
||||
|
||||
Reference in new issue
Block a user