Admin: log in once, keep a session cookie instead of the key in the URL
The admin key used to stay in the URL fragment and was sent with every
request. Now the key is exchanged once for a server-side session:
- POST /api/admin/login {key} sets bingo_admin=<random token>
(HttpOnly, Secure, SameSite=Strict, no Path so it is scoped to the
admin API directory, also behind a subpath proxy)
- POST /api/admin/logout ends the session
- sessions live in memory as sha256(token) -> expiry, absolute lifetime
ADMIN_SESSION_HOURS (default 8); restart logs everyone out
- /admin#k=KEY still works as a login link; the page strips the fragment
from the URL and history before logging in, then shows plain /admin
- wrong key answers 401 after 500 ms; Bearer header still works for scripts
- COOKIE_SECURE=0 for plain-http testing only
Tests: 11 passing (login, cookie flags, logout, expiry, admin disabled).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhHiznVwtQ55ZSguTBWrxn
This commit is contained in:
1 parent
b6f0791243
commit
9b421c9b47
8 files changed
+320
-66
No files matched your search
+28
-6
@@ -233,14 +233,18 @@ curl -s https://bingo.example.com/api/total
|
||||
## 3. Ссылки
|
||||
|
||||
- **Для команды** — `https://bingo.example.com/`. Закрепите в канале Teams сообщением или вкладкой «Веб-сайт».
|
||||
- **Для организаторов** — `https://bingo.example.com/admin#k=ВАШ_ADMIN_KEY`. Часть после `#` не уходит
|
||||
на сервер и не попадает в журналы, ключ передаётся только в заголовке запроса.
|
||||
- **Для организаторов** — `https://bingo.example.com/admin`, вход по ключу из `.env`. Можно и ссылкой
|
||||
для входа `https://bingo.example.com/admin#k=ВАШ_ADMIN_KEY`: страница сразу убирает ключ из адреса
|
||||
и истории браузера, обменивает его на сессию, и дальше открыт обычный `/admin`. Такую ссылку не стоит
|
||||
пересылать в чатах — она сама по себе пароль.
|
||||
|
||||
## Настройки (`.env`)
|
||||
|
||||
| Параметр | По умолчанию | Что значит |
|
||||
|---|---|---|
|
||||
| `ADMIN_KEY` | пусто | Ключ админки, от 16 символов. Пусто — админка выключена |
|
||||
| `ADMIN_SESSION_HOURS` | `8` | Сколько часов действует вход в админку, см. «Админка» |
|
||||
| `COOKIE_SECURE` | включено | `0` — cookie входа без флага Secure. Только для проверки по http, не в работе |
|
||||
| `TIMEZONE` | `Europe/Moscow` | В каком поясе карточка обнуляется в понедельник 00:00 |
|
||||
| `SEASON_START` | `2026-11-02` | Первый день, когда отметки идут в батарейку |
|
||||
| `SEASON_END` | `2026-12-21` | Последний такой день (включительно) |
|
||||
@@ -326,15 +330,33 @@ docker compose up -d --build # данные в томе сохраняютс
|
||||
- Порт слушает только `127.0.0.1`.
|
||||
- Тело запроса ограничено: 2 КБ в приложении, 8 КБ в nginx.
|
||||
- У контейнера есть healthcheck (`/healthz`).
|
||||
- Админка закрыта ключом. Если хочется закрыть её ещё и по сети, добавьте в nginx
|
||||
- Админка: вход по ключу, дальше сессия в cookie (подробности — в разделе «Админка»). Если хочется закрыть её ещё и по сети, добавьте в nginx
|
||||
`location /admin { allow <сеть офиса/VPN>; deny all; proxy_pass … }`.
|
||||
|
||||
## Админка
|
||||
|
||||
`/admin#k=КЛЮЧ` показывает батарейку и шаги по неделям: клетки, собранные ряды, столбцы и диагонали,
|
||||
`/admin` показывает батарейку и шаги по неделям: клетки, собранные ряды, столбцы и диагонали,
|
||||
число устройств. Там же ответы на мини-вопрос по неделям и популярность каждой клетки. Три кнопки
|
||||
выгружают CSV (разделитель `;`, UTF-8 с BOM — открывается в Excel без мастера импорта). Ключ меняется в `.env`,
|
||||
затем `docker compose up -d`.
|
||||
выгружают CSV (разделитель `;`, UTF-8 с BOM — открывается в Excel без мастера импорта).
|
||||
|
||||
**Вход.** Ключ вводится один раз в форме на странице (или через ссылку `/admin#k=КЛЮЧ` — ключ сразу
|
||||
убирается из адреса). Сервер проверяет его и ставит cookie сессии:
|
||||
|
||||
- в cookie лежит случайный токен, а не ключ; сервер хранит только хеш токена;
|
||||
- `HttpOnly` — скрипты страницы его не видят, `Secure` — только по https, `SameSite=Strict` — не уходит
|
||||
с запросами с чужих сайтов;
|
||||
- cookie отправляется только на адреса админского API (`/api/admin/…`), не на страницу бинго;
|
||||
- срок — `ADMIN_SESSION_HOURS` (по умолчанию 8 часов) с момента входа, без продления. Когда закончится,
|
||||
написано внизу страницы, после этого страница снова попросит ключ;
|
||||
- кнопка «Выйти» завершает сессию на сервере сразу;
|
||||
- сессии хранятся в памяти, поэтому перезапуск контейнера (`docker compose up -d` после правки `.env`,
|
||||
обновление) завершает все входы;
|
||||
- неверный ключ отвечает с задержкой полсекунды, чтобы подбор был бессмысленным.
|
||||
|
||||
**Смена ключа:** новый `ADMIN_KEY` в `.env`, затем `docker compose up -d` — старые сессии тоже закончатся.
|
||||
|
||||
Для скриптов можно не входить, а передавать ключ в заголовке:
|
||||
`curl -H "Authorization: Bearer $ADMIN_KEY" https://bingo.example.com/api/admin/stats`.
|
||||
|
||||
## Нагрузка
|
||||
|
||||
|
||||
Reference in new issue
Block a user