Админка: секретный путь, смена ссылки, интервал и принудительное обновление
Панель без публичного входа: страница живёт по ADMIN_PATH, логин/пароль — ADMIN_LOGIN/ADMIN_PASSWORD из .env (пусто = админка выключена и маршруты /api/admin/* не регистрируются, прод можно не трогать). Умеет: посмотреть текущую ссылку и состояние кэша, вставить новую ссылку (проверяется скачиванием и разбором до сохранения, при ошибке откат), принудительно обновить таблицу вне интервала и без проверки modified, задать интервал в минутах (1..1440). Реализация: - backend/admin.py — cookie-сессия на HMAC (ключ = пароль, 12 часов, HttpOnly/SameSite/Secure-за-https), сравнение за постоянное время; compare_digest по байтам — кириллица в логине не роняет сервер; - schedule/settings.py — ссылка и интервал в JSON (SETTINGS_FILE), атомарная запись, терпимость к битому/недоступному файлу (тогда живёт в памяти); - schedule/cache.py — интервал вместо TTL, apply_link с откатом, force_reload, state() для панели; кэш без ссылки больше не роняет старт; - frontend: страница admin.html (вне static, прямой /admin.html закрыт) и прокси /api/admin/* с телом и Set-Cookie; - docker-compose: том schedule_data для настроек, ADMIN_* в окружении; - deploy/: nginx-конфиг и инструкция для стенда dev.schedule.our-place.ru. Тесты: 138 (новые — админ-API, сессии, ссылка/интервал, force, хранилище).
This commit is contained in:
73
README.md
73
README.md
@@ -86,28 +86,63 @@ nginx (https://schedule.our-place.ru) → 127.0.0.1:8090
|
||||
- Приложение можно «установить» на главный экран (манифест + service
|
||||
worker + иконки приложения); на iOS иконку берёт `apple-touch-icon`.
|
||||
|
||||
## Админка
|
||||
|
||||
Отдельного входа на сайте нет: панель открывается по секретному пути из
|
||||
`ADMIN_PATH` (например `/manage-7f3a91`), логин и пароль — `ADMIN_LOGIN` /
|
||||
`ADMIN_PASSWORD`. Если эти переменные пусты, админка полностью выключена и
|
||||
маршруты `/api/admin/*` не регистрируются (прод можно не трогать).
|
||||
|
||||
Что умеет администратор:
|
||||
|
||||
- **посмотреть текущую ссылку** и состояние кэша (обновлено, изменена,
|
||||
ошибка, число групп);
|
||||
- **вставить новую ссылку**: она проверяется скачиванием и разбором таблицы
|
||||
до сохранения — нерабочая не применится, данные на сайте не пострадают;
|
||||
- **принудительно обновить расписание** — вне интервала и без проверки
|
||||
«таблица не менялась» (то же, что делает фоновое обновление, но сейчас);
|
||||
- **задать интервал обновления** в минутах (`1…1440`).
|
||||
|
||||
Как это устроено:
|
||||
|
||||
- вход — `POST /api/admin/login`, сессия в подписанной HMAC-cookie
|
||||
(`HttpOnly`, `SameSite=Lax`, `Secure` при https), ключ — пароль админа,
|
||||
срок 12 часов; неудачные попытки логируются;
|
||||
- все действия — под `/api/admin/*`, без cookie отвечают `401`;
|
||||
- ссылка и интервал хранятся в `SETTINGS_FILE` (в docker — том
|
||||
`schedule_data`, `/data/settings.json`), поэтому переживают перезапуск;
|
||||
если файл недоступен, настройки живут в памяти;
|
||||
- страница панели отдаётся фронтендом по `ADMIN_PATH` с заголовком
|
||||
`X-Robots-Tag: noindex`; путь нигде не публикуется и не попадает в `/`.
|
||||
|
||||
## Структура кода
|
||||
|
||||
```
|
||||
src/
|
||||
backend/
|
||||
main.py FastAPI: маршруты, create_app(cache), /healthz
|
||||
main.py FastAPI: маршруты, create_app(cache), /healthz
|
||||
admin.py админка: cookie-сессия, ссылка, интервал, принудительное обновление
|
||||
schedule/
|
||||
xlsx.py чтение xlsx: общие строки, объединённые ячейки,
|
||||
скрытые столбцы, красный текст, серийные даты
|
||||
source.py скачивание файла по публичной ссылке Я.Диска
|
||||
lessons.py разбор ячеек занятий (общий конвейер ролей)
|
||||
parsing.py лист → расписание, недели, индексы преподавателей/аудиторий
|
||||
cache.py ScheduleCache: блокировка, TTL, modified, stale-while-error
|
||||
errors.py ScheduleError
|
||||
xlsx.py чтение xlsx: sharedStrings, мержи, скрытые столбцы,
|
||||
красный текст, серийные даты
|
||||
source.py загрузка файла по публичной ссылке Я.Диска
|
||||
lessons.py разбор ячеек занятий (общий конвейер ролей)
|
||||
parsing.py листы → расписание, недели, индексы преподавателей/аудиторий
|
||||
cache.py ScheduleCache: блокировка, интервал, modified, stale-while-error
|
||||
settings.py хранилище настроек админки (ссылка, интервал) в JSON-файле
|
||||
errors.py ScheduleError
|
||||
frontend/
|
||||
main.py FastAPI: статика + прокси /api/* (таймаут 150 c)
|
||||
admin.html страница админки (вне static: прямой доступ закрыт)
|
||||
static/
|
||||
index.html, app.js, style.css
|
||||
manifest.json PWA: имя, иконки, standalone-режим
|
||||
sw.js минимальный service worker (без кэша) для установки приложения
|
||||
img/ логотипы (owl.svg, owl-white.svg), иконки приложения
|
||||
(app-icon.svg, icon-192/512.png, apple-touch-icon.png, favicon-32.png)
|
||||
deploy/
|
||||
nginx-dev.conf сайт тестового стенда (dev.schedule.our-place.ru)
|
||||
README-dev.md как поднять тестовый стенд рядом с продом
|
||||
tools/
|
||||
make_icons.py генерация иконок приложения из логотипа (нужен rsvg-convert)
|
||||
tests/
|
||||
@@ -150,8 +185,12 @@ tests/
|
||||
| Переменная | По умолчанию | Где | Описание |
|
||||
|---|---|---|---|
|
||||
| `SCHEDULE_LINK` | — | api | публичная ссылка Я.Диска, **обязательна** |
|
||||
| `SCHEDULE_CACHE_TTL` | `900` | api | время жизни кэша, сек |
|
||||
| `SCHEDULE_INTERVAL_MINUTES` | `15` | api | интервал обновления, мин (правится в админке) |
|
||||
| `SCHEDULE_CACHE_TTL` | `900` | api | устаревшее: интервал в секундах (если не задан предыдущий) |
|
||||
| `SCHEDULE_RETRY_AFTER` | `60` | api | пауза между попытками при сбое источника |
|
||||
| `ADMIN_PATH` | — | api+frontend | секретный путь панели (пусто = админка выключена) |
|
||||
| `ADMIN_LOGIN` / `ADMIN_PASSWORD` | — | api | учётные данные администратора |
|
||||
| `SETTINGS_FILE` | `/data/settings.json` | api | где хранить ссылку и интервал |
|
||||
| `SCHEDULE_API` | `http://127.0.0.1:8000` | frontend | адрес бэкенда (в docker: `http://api:8000`) |
|
||||
| `SCHEDULE_PROXY_TIMEOUT` | `150` | frontend | таймаут прокси к бэкенду, сек |
|
||||
| `API_PORT` | `8000` | docker | внешний порт api |
|
||||
@@ -204,12 +243,22 @@ cd /opt/schedule && git pull && sudo docker compose up -d --build
|
||||
nginx-сайт `schedule.our-place.ru` проксирует 443 → 127.0.0.1:8090 (фронт),
|
||||
сертификат Let's Encrypt. Порты api и frontend слушаются только на loopback.
|
||||
|
||||
### Тестовый стенд
|
||||
|
||||
Новые штуки обкатываются на отдельном стенде `dev.schedule.our-place.ru`
|
||||
(ветка `admin-panel`), прод при этом не меняется: другой каталог
|
||||
(`/opt/schedule-dev`), другие порты (8001/8091), свой том настроек.
|
||||
Пошаговая инструкция — `deploy/README-dev.md`, конфиг nginx —
|
||||
`deploy/nginx-dev.conf`.
|
||||
|
||||
## Замечания
|
||||
|
||||
- Принудительное обновление таблицы (пропуск кэша, `force`) из API намеренно
|
||||
убрано; планируется вернуть вместе с авторизацией и ролью администратора.
|
||||
- Принудительное обновление и смена ссылки живут только в админке
|
||||
(`/api/admin/*` под cookie-сессией): публичного `force` в API нет.
|
||||
- Кэш живёт в памяти единственного воркера: при горизонтальном масштабировании
|
||||
api его придётся выносить в общее хранилище.
|
||||
api его придётся выносить в общее хранилище (настройки админки уже в файле).
|
||||
- `ADMIN_PATH` защищает только от «случайного» обнаружения панели; реальная
|
||||
защита — логин и пароль. Стоит держать их длинными и не публиковать путь.
|
||||
- Строки внутри дня в таблице строго чередуются «числитель / знаменатель».
|
||||
Если строку посреди блока вставят или удалят, бэкенд запишет предупреждение
|
||||
«нечётное число строк, недели могли сдвинуться».
|
||||
|
||||
Reference in New Issue
Block a user