Админка: кнопки в шапке сайта, красный выход и путь, который нигде не утекает

Интерфейс:
- после входа на главной появляются «Админка» и «Выйти» — в шапке, перед
  переключателем темы и после него соответственно (порядок: Админка, тема,
  Выйти);
- «Выйти» теперь красная и заметно реагирует на наведение (темнеет,
  приподнимается, тень) — и на сайте, и в админке (одна кнопка в шапке,
  из карточки убрана);
- в админке убраны «осталось попыток», путь к файлу настроек и карточка
  «Данные расписания» (дублировала шапку); блокировка показывается отдельной
  плашкой только когда действительно активна.

Безопасность пути:
- путь к панели не отдаётся ни в 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:
2026-10-10 12:54:54 +03:00
parent 71f83a27dd
commit 34a46b6832
8 changed files with 159 additions and 34 deletions

View File

@@ -75,6 +75,18 @@ def test_login_and_state(client, cache):
assert state["groups"] == 2
assert state["error"] is None
assert state["throttle"]["max_attempts"] == ADMIN.max_attempts
# путь к панели не должен утекать в JSON: переход идёт через /goto
assert "admin_path" not in state
assert ADMIN.path not in str(state)
def test_goto_redirects_to_panel_only_for_authorized(client):
"""Переход в панель: без сессии 401, с сессией — редирект на секретный путь."""
assert client.get("/api/admin/goto", follow_redirects=False).status_code == 401
do_login(client)
resp = client.get("/api/admin/goto", follow_redirects=False)
assert resp.status_code == 302
assert resp.headers["location"] == ADMIN.path
def test_logout(client):