Админка: кнопки в шапке сайта, красный выход и путь, который нигде не утекает
Интерфейс: - после входа на главной появляются «Админка» и «Выйти» — в шапке, перед переключателем темы и после него соответственно (порядок: Админка, тема, Выйти); - «Выйти» теперь красная и заметно реагирует на наведение (темнеет, приподнимается, тень) — и на сайте, и в админке (одна кнопка в шапке, из карточки убрана); - в админке убраны «осталось попыток», путь к файлу настроек и карточка «Данные расписания» (дублировала шапку); блокировка показывается отдельной плашкой только когда действительно активна. Безопасность пути: - путь к панели не отдаётся ни в 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:
@@ -28,6 +28,7 @@ from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
||||
from fastapi.responses import RedirectResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from schedule import ScheduleCache, ScheduleError
|
||||
@@ -245,6 +246,8 @@ def create_admin_router(
|
||||
ip = _client_ip(request)
|
||||
until = throttle.locked_until(ip)
|
||||
payload = cache.state()
|
||||
# путь к панели здесь НЕ отдаём: он не должен попадать ни в JSON, ни в
|
||||
# HTML — переход в панель идёт через /api/admin/goto (редирект)
|
||||
payload["throttle"] = {
|
||||
"max_attempts": config.max_attempts,
|
||||
"lockout_minutes": config.lockout_minutes,
|
||||
@@ -253,6 +256,20 @@ def create_admin_router(
|
||||
}
|
||||
return payload
|
||||
|
||||
@router.get("/goto", dependencies=[Depends(require_auth)])
|
||||
def goto() -> Response:
|
||||
"""Переход в панель.
|
||||
|
||||
Так секретный путь не хранится в разметке главной страницы: кнопка
|
||||
«Админка» дёргает этот адрес, и браузер уже получает редирект.
|
||||
Без валидной сессии ответ — 401, то есть ботам тут ничего не видно.
|
||||
"""
|
||||
if not config.path:
|
||||
raise HTTPException(status_code=500, detail="ADMIN_PATH не задан")
|
||||
return RedirectResponse(
|
||||
url=config.path, status_code=302, headers={"Cache-Control": "no-store"}
|
||||
)
|
||||
|
||||
@router.post("/refresh", dependencies=[Depends(require_auth)])
|
||||
def refresh() -> dict:
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user