feature: API-ключи клиентов и журнал использования в Postgres (#4, #17)

- Postgres в compose; пакет db (SQLAlchemy async): clients, windows, requests.
- Все эндпоинты, кроме /health, — только с ключом в X-API-Key, иначе 401; без
  выпущенных ключей доступа нет. В базе — только sha256 ключа.
- Задача и окно сессии привязаны к клиенту: чужой ключ их не видит.
- Воркер пишет журнал: окно (открыто, последнее обращение, закрыто) и задачу
  (в очереди, начало, конец, итог, данные без секретов). Сбой записи не роняет задачу.
- python -m db.cli create|list|revoke|stats; make key-create/key-list/key-revoke/stats.
- smoke и loadtest с ключом.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
protokeyandClaude Opus 5.5 committed 2026-10-13 21:48:48 +04:00
1 parent 9dc2c0b619
commit 1a4bd8d672
34 files changed
+857 -81

No files matched your search

+8 -7
View File
@@ -90,7 +90,8 @@ def test_window_held_and_reused(env):
sid = env.client.cookies["session_id"]
engine = FakeSession.instances[0]
assert not engine.closed and env.pool.used == 1
assert env.redis.hgetall(session_key(sid)) == {"worker_id": WORKER_ID, "idle_timeout": "5"}
window = env.redis.hgetall(session_key(sid))
assert (window["worker_id"], window["idle_timeout"], window["client_id"]) == (WORKER_ID, "5", "1")
assert 290 < env.redis.ttl(session_key(sid)) <= 300
# следующая задача сессии — в персональную очередь воркера с окном, без url — на текущей странице
@@ -180,7 +181,7 @@ def test_dead_worker_window_goes_to_common_queue(env):
assert env.messages[-1].queue_name == COMMON_QUEUE
def test_old_profiles_cleaned_up(env, tmp_path):
def test_old_profiles_cleaned_up(env):
"""#20: профили старше PROFILE_TTL_DAYS удаляются, кроме тех, где открыто окно."""
import os
import time
@@ -194,18 +195,18 @@ def test_old_profiles_cleaned_up(env, tmp_path):
long_ago = time.time() - 8 * 86400
for sid in (held, elsewhere, old, fresh, "not-a-session"):
(tmp_path / sid).mkdir(exist_ok=True)
marker = tmp_path / sid / LAST_USED_FILE
(env.profiles / sid).mkdir(exist_ok=True)
marker = env.profiles / sid / LAST_USED_FILE
marker.touch()
if sid != fresh:
os.utime(marker, (long_ago, long_ago))
assert env.call(env.pool.cleanup_profiles) == [old]
assert sorted(p.name for p in tmp_path.iterdir()) == sorted([held, elsewhere, fresh, "not-a-session"])
assert sorted(p.name for p in env.profiles.iterdir()) == sorted([held, elsewhere, fresh, "not-a-session"])
def test_profile_marked_as_used(env, tmp_path):
def test_profile_marked_as_used(env):
from worker.windows import LAST_USED_FILE
create(env, url="https://example.com")
assert (tmp_path / env.client.cookies["session_id"] / LAST_USED_FILE).exists()
assert (env.profiles / env.client.cookies["session_id"] / LAST_USED_FILE).exists()