Админка: кнопки в шапке сайта, красный выход и путь, который нигде не утекает
Интерфейс: - после входа на главной появляются «Админка» и «Выйти» — в шапке, перед переключателем темы и после него соответственно (порядок: Админка, тема, Выйти); - «Выйти» теперь красная и заметно реагирует на наведение (темнеет, приподнимается, тень) — и на сайте, и в админке (одна кнопка в шапке, из карточки убрана); - в админке убраны «осталось попыток», путь к файлу настроек и карточка «Данные расписания» (дублировала шапку); блокировка показывается отдельной плашкой только когда действительно активна. Безопасность пути: - путь к панели не отдаётся ни в HTML, ни в JSON (из /api/admin/state убран), ни в JS: на главной для вошедшего только флаг, а кнопка «Админка» ведёт на служебный /api/admin/goto, который отвечает 302 на секретный путь; - без сессии /api/admin/goto и /api/admin/state отвечают 401, неизвестные пути дают 404 — перебором путь не находится; - страница панели: noindex/nofollow/noarchive, Referrer-Policy: no-referrer, X-Frame-Options: DENY, Cache-Control: no-store; - robots.txt без пути (только Disallow: /api/), внешние ссылки в панели — rel=noopener noreferrer. Тесты: 161 (состояние без пути, /goto: 401 без входа и 302 с ним).
This commit is contained in:
@@ -85,7 +85,6 @@
|
||||
}
|
||||
.admin-btn:hover { filter: brightness(1.06); }
|
||||
.admin-btn.secondary { background: var(--surface-2); color: var(--text); border-color: var(--border-2); }
|
||||
.admin-btn.danger { background: transparent; color: var(--accent-red-bright); border-color: var(--accent-red-bright); }
|
||||
.admin-btn-lg { font-size: 15px; padding: 11px 18px; }
|
||||
.admin-btn[disabled] { opacity: .55; cursor: default; filter: none; }
|
||||
|
||||
@@ -163,6 +162,7 @@
|
||||
<svg class="icon icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
|
||||
<svg class="icon icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
||||
</button>
|
||||
<button class="topbar-logout" id="logout" type="button">Выйти</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -197,10 +197,8 @@
|
||||
</label>
|
||||
<div class="admin-card-actions">
|
||||
<button class="admin-btn" id="save-link" type="button">Сохранить ссылку</button>
|
||||
<a class="admin-link" id="link-open" href="#" target="_blank" rel="noopener">открыть таблицу</a>
|
||||
<a class="admin-link" id="link-open" href="#" target="_blank" rel="noopener noreferrer">открыть таблицу</a>
|
||||
</div>
|
||||
<p class="admin-hint">Ссылка проверяется скачиванием: если не открывается — не сохранится, данные на сайте останутся прежними.
|
||||
Значение хранится в файле настроек и <b>имеет приоритет над .env</b>.</p>
|
||||
</article>
|
||||
|
||||
<article class="admin-card">
|
||||
@@ -216,8 +214,6 @@
|
||||
<div><dt>Дата файла на Диске</dt><dd id="fact-modified">—</dd></div>
|
||||
<div><dt>Отпечаток файла</dt><dd id="fact-hash" class="admin-mono">—</dd></div>
|
||||
</dl>
|
||||
<p class="admin-hint">По интервалу таблица перечитывается, только если на Диске изменилась дата файла.
|
||||
Кнопка «Обновить сейчас» скачивает таблицу всегда — её и нажимайте сразу после правок.</p>
|
||||
</article>
|
||||
|
||||
<article class="admin-card">
|
||||
@@ -225,21 +221,8 @@
|
||||
<dl class="admin-facts">
|
||||
<div><dt>Последняя попытка</dt><dd id="auth-attempt">—</dd></div>
|
||||
<div><dt>Последний успешный вход</dt><dd id="auth-success">—</dd></div>
|
||||
<div><dt>Осталось попыток</dt><dd id="auth-throttle">—</dd></div>
|
||||
</dl>
|
||||
<div class="admin-card-actions" style="margin-top:auto">
|
||||
<button class="admin-btn danger" id="logout" type="button">Выйти</button>
|
||||
</div>
|
||||
<p class="admin-hint">Каждая попытка запоминается: откуда, когда и чем закончилась. Порог и время блокировки задаются в .env.</p>
|
||||
</article>
|
||||
|
||||
<article class="admin-card">
|
||||
<h2>Данные расписания</h2>
|
||||
<dl class="admin-facts">
|
||||
<div><dt>Групп</dt><dd id="fact-groups">—</dd></div>
|
||||
<div><dt>Обновлено</dt><dd id="fact-updated">—</dd></div>
|
||||
<div><dt>Файл настроек</dt><dd id="fact-settings" class="admin-mono">—</dd></div>
|
||||
</dl>
|
||||
<div id="auth-lock" class="admin-alert warn hidden" style="margin:10px 0 0"></div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
@@ -373,16 +356,13 @@
|
||||
setText("auth-attempt", loginText(attempt), attempt ? (attempt.ok ? "good" : "bad") : null);
|
||||
setText("auth-success", loginText(state.last_success));
|
||||
const throttle = state.throttle || {};
|
||||
const lock = $("auth-lock");
|
||||
if (throttle.locked_until) {
|
||||
setText("auth-throttle", `заблокировано до ${fmt(throttle.locked_until)}`, "bad");
|
||||
} else if (throttle.max_attempts) {
|
||||
setText("auth-throttle", `${throttle.attempts_left} из ${throttle.max_attempts}`);
|
||||
lock.textContent = `Вход заблокирован до ${fmt(throttle.locked_until)} — слишком много неудачных попыток.`;
|
||||
lock.classList.remove("hidden");
|
||||
} else {
|
||||
lock.classList.add("hidden");
|
||||
}
|
||||
|
||||
// данные
|
||||
setText("fact-groups", String(state.groups));
|
||||
setText("fact-updated", state.last_updated ? `${fmt(state.last_updated)} (${ago(state.last_updated)})` : "—");
|
||||
setText("fact-settings", state.settings_file || "—");
|
||||
}
|
||||
|
||||
function showLogin(show) {
|
||||
|
||||
Reference in New Issue
Block a user