store sessions in the DB
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -23,3 +23,6 @@ dist/
|
|||||||
# Local tracker/node sqlite databases (never commit runtime state)
|
# Local tracker/node sqlite databases (never commit runtime state)
|
||||||
*.sqlite
|
*.sqlite
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
|
logs/tracker/error.log
|
||||||
|
logs/tracker/info.log
|
||||||
|
logs/tracker/warning.log
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ regular user.
|
|||||||
Mutations are append-only events with unique ids — the same replication
|
Mutations are append-only events with unique ids — the same replication
|
||||||
model as ``BillingLedger`` — so accounts and API keys converge across the
|
model as ``BillingLedger`` — so accounts and API keys converge across the
|
||||||
tracker hive via gossip, and every dashboard can serve registration/login.
|
tracker hive via gossip, and every dashboard can serve registration/login.
|
||||||
Sessions are deliberately local to each tracker (bearer tokens in memory).
|
Sessions are local to each tracker and persisted so dashboard cookies survive
|
||||||
|
tracker restarts.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -115,6 +116,8 @@ class AccountStore:
|
|||||||
"account_id": account_id,
|
"account_id": account_id,
|
||||||
"expires": time.time() + SESSION_TTL,
|
"expires": time.time() + SESSION_TTL,
|
||||||
}
|
}
|
||||||
|
self._dirty = True
|
||||||
|
self.save_to_db()
|
||||||
return token
|
return token
|
||||||
|
|
||||||
def session_account(self, token: str | None) -> dict | None:
|
def session_account(self, token: str | None) -> dict | None:
|
||||||
@@ -134,7 +137,9 @@ class AccountStore:
|
|||||||
if not token:
|
if not token:
|
||||||
return
|
return
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._sessions.pop(token, None)
|
if self._sessions.pop(token, None) is not None:
|
||||||
|
self._dirty = True
|
||||||
|
self.save_to_db()
|
||||||
|
|
||||||
# ---- API keys ----
|
# ---- API keys ----
|
||||||
|
|
||||||
@@ -271,6 +276,10 @@ class AccountStore:
|
|||||||
"CREATE TABLE IF NOT EXISTS account_events "
|
"CREATE TABLE IF NOT EXISTS account_events "
|
||||||
"(event_id TEXT PRIMARY KEY, payload TEXT NOT NULL, ts REAL NOT NULL)"
|
"(event_id TEXT PRIMARY KEY, payload TEXT NOT NULL, ts REAL NOT NULL)"
|
||||||
)
|
)
|
||||||
|
con.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS account_sessions "
|
||||||
|
"(token TEXT PRIMARY KEY, account_id TEXT NOT NULL, expires REAL NOT NULL)"
|
||||||
|
)
|
||||||
con.commit()
|
con.commit()
|
||||||
con.close()
|
con.close()
|
||||||
|
|
||||||
@@ -279,6 +288,10 @@ class AccountStore:
|
|||||||
rows = con.execute(
|
rows = con.execute(
|
||||||
"SELECT payload FROM account_events ORDER BY ts, event_id"
|
"SELECT payload FROM account_events ORDER BY ts, event_id"
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
session_rows = con.execute(
|
||||||
|
"SELECT token, account_id, expires FROM account_sessions WHERE expires >= ?",
|
||||||
|
(time.time(),),
|
||||||
|
).fetchall()
|
||||||
con.close()
|
con.close()
|
||||||
with self._lock:
|
with self._lock:
|
||||||
for (payload,) in rows:
|
for (payload,) in rows:
|
||||||
@@ -288,6 +301,11 @@ class AccountStore:
|
|||||||
continue
|
continue
|
||||||
if event.get("id") not in self._seen_event_ids:
|
if event.get("id") not in self._seen_event_ids:
|
||||||
self._apply_locked(event)
|
self._apply_locked(event)
|
||||||
|
self._sessions = {
|
||||||
|
token: {"account_id": account_id, "expires": float(expires)}
|
||||||
|
for token, account_id, expires in session_rows
|
||||||
|
if account_id in self._accounts
|
||||||
|
}
|
||||||
self._dirty = False
|
self._dirty = False
|
||||||
|
|
||||||
def save_to_db(self) -> None:
|
def save_to_db(self) -> None:
|
||||||
@@ -297,11 +315,21 @@ class AccountStore:
|
|||||||
if not self._dirty:
|
if not self._dirty:
|
||||||
return
|
return
|
||||||
events = list(self._event_log)
|
events = list(self._event_log)
|
||||||
|
sessions = [
|
||||||
|
(token, session["account_id"], float(session["expires"]))
|
||||||
|
for token, session in self._sessions.items()
|
||||||
|
if session["expires"] >= time.time()
|
||||||
|
]
|
||||||
self._dirty = False
|
self._dirty = False
|
||||||
con = sqlite3.connect(self._db_path) # type: ignore[arg-type]
|
con = sqlite3.connect(self._db_path) # type: ignore[arg-type]
|
||||||
con.executemany(
|
con.executemany(
|
||||||
"INSERT OR IGNORE INTO account_events (event_id, payload, ts) VALUES (?, ?, ?)",
|
"INSERT OR IGNORE INTO account_events (event_id, payload, ts) VALUES (?, ?, ?)",
|
||||||
[(e["id"], json.dumps(e), float(e.get("ts", 0.0))) for e in events],
|
[(e["id"], json.dumps(e), float(e.get("ts", 0.0))) for e in events],
|
||||||
)
|
)
|
||||||
|
con.execute("DELETE FROM account_sessions")
|
||||||
|
con.executemany(
|
||||||
|
"INSERT INTO account_sessions (token, account_id, expires) VALUES (?, ?, ?)",
|
||||||
|
sessions,
|
||||||
|
)
|
||||||
con.commit()
|
con.commit()
|
||||||
con.close()
|
con.close()
|
||||||
|
|||||||
@@ -224,6 +224,7 @@
|
|||||||
<section data-tab="overview"><h2>Model usage (RPM)</h2><div id="stats" class="empty">loading…</div></section>
|
<section data-tab="overview"><h2>Model usage (RPM)</h2><div id="stats" class="empty">loading…</div></section>
|
||||||
<section data-tab="overview" class="wide"><h2>Call wall</h2><div id="call-wall" class="empty">loading...</div></section>
|
<section data-tab="overview" class="wide"><h2>Call wall</h2><div id="call-wall" class="empty">loading...</div></section>
|
||||||
<section data-tab="chat" class="wide chat-section">
|
<section data-tab="chat" class="wide chat-section">
|
||||||
|
<h2 style="display:none">Chat / inference</h2>
|
||||||
<div class="chat-app">
|
<div class="chat-app">
|
||||||
<aside class="chat-sidebar">
|
<aside class="chat-sidebar">
|
||||||
<button type="button" class="chat-new-btn" onclick="createNewChatSession()">+ New chat</button>
|
<button type="button" class="chat-new-btn" onclick="createNewChatSession()">+ New chat</button>
|
||||||
@@ -273,7 +274,7 @@ async function fetchJson(path) {
|
|||||||
const headers = {};
|
const headers = {};
|
||||||
const token = localStorage.getItem("meshnet_session");
|
const token = localStorage.getItem("meshnet_session");
|
||||||
if (token) headers["Authorization"] = "Bearer " + token;
|
if (token) headers["Authorization"] = "Bearer " + token;
|
||||||
const r = await fetch(path, { headers });
|
const r = await fetch(path, { headers, credentials: "same-origin" });
|
||||||
if (!r.ok) return null;
|
if (!r.ok) return null;
|
||||||
return await r.json();
|
return await r.json();
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
@@ -1029,6 +1030,7 @@ async function apiCall(path, method, body, bearerToken) {
|
|||||||
const r = await fetch(path, {
|
const r = await fetch(path, {
|
||||||
method: method || "GET",
|
method: method || "GET",
|
||||||
headers,
|
headers,
|
||||||
|
credentials: "same-origin",
|
||||||
body: body ? JSON.stringify(body) : undefined,
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
});
|
});
|
||||||
const data = await r.json().catch(() => ({}));
|
const data = await r.json().catch(() => ({}));
|
||||||
@@ -1371,7 +1373,7 @@ renderChatModels();
|
|||||||
renderChatHistory();
|
renderChatHistory();
|
||||||
renderChatAuthHint();
|
renderChatAuthHint();
|
||||||
setInterval(refresh, 4000);
|
setInterval(refresh, 4000);
|
||||||
setInterval(() => { if (sessionToken) renderAccountPanel(); }, 8000);
|
setInterval(() => { if (sessionToken || isLoggedIn) renderAccountPanel(); }, 8000);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ HTTP API contract:
|
|||||||
Response 400/404/503: {"error": str}
|
Response 400/404/503: {"error": str}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import http.cookies
|
||||||
import http.server
|
import http.server
|
||||||
import hashlib
|
import hashlib
|
||||||
import itertools
|
import itertools
|
||||||
@@ -47,14 +48,15 @@ from .wallet_proof import binding_message, verify_wallet_signature
|
|||||||
from .billing import DEFAULT_BILLING_DB_PATH, BillingLedger
|
from .billing import DEFAULT_BILLING_DB_PATH, BillingLedger
|
||||||
from .calibration import DEFAULT_CALIBRATION_DB_PATH, ToplocCalibrationStore
|
from .calibration import DEFAULT_CALIBRATION_DB_PATH, ToplocCalibrationStore
|
||||||
from .hf_pricing import DEFAULT_HF_PRICING_LOG_DB_PATH, HfPricingLog, refresh_preset_price
|
from .hf_pricing import DEFAULT_HF_PRICING_LOG_DB_PATH, HfPricingLog, refresh_preset_price
|
||||||
from .gossip import NodeGossip
|
from .gossip import NodeGossip
|
||||||
from .logging_setup import tracker_logger
|
from .logging_setup import tracker_logger
|
||||||
from .model_files import files_for_layer_range, snapshot_dir_for_repo
|
from .model_files import files_for_layer_range, snapshot_dir_for_repo
|
||||||
from .raft import RaftNode
|
from .raft import RaftNode
|
||||||
|
|
||||||
|
|
||||||
_CONSOLE_LIMIT = 300
|
_CONSOLE_LIMIT = 300
|
||||||
_PROXY_PROGRESS_LOG_INTERVAL = 5.0
|
_PROXY_PROGRESS_LOG_INTERVAL = 5.0
|
||||||
|
_SESSION_COOKIE_NAME = "meshnet_session"
|
||||||
|
|
||||||
|
|
||||||
def _preset_price_keys(name: str, preset: dict) -> set[str]:
|
def _preset_price_keys(name: str, preset: dict) -> set[str]:
|
||||||
@@ -1935,6 +1937,38 @@ def _api_key_from_headers(headers) -> str | None:
|
|||||||
return auth.strip() or None
|
return auth.strip() or None
|
||||||
|
|
||||||
|
|
||||||
|
def _session_token_from_headers(headers) -> str | None:
|
||||||
|
token = _api_key_from_headers(headers)
|
||||||
|
if token:
|
||||||
|
return token
|
||||||
|
cookie_header = headers.get("Cookie")
|
||||||
|
if not cookie_header:
|
||||||
|
return None
|
||||||
|
cookie = http.cookies.SimpleCookie()
|
||||||
|
try:
|
||||||
|
cookie.load(cookie_header)
|
||||||
|
except http.cookies.CookieError:
|
||||||
|
return None
|
||||||
|
morsel = cookie.get(_SESSION_COOKIE_NAME)
|
||||||
|
if morsel is None:
|
||||||
|
return None
|
||||||
|
return morsel.value.strip() or None
|
||||||
|
|
||||||
|
|
||||||
|
def _session_cookie_header(token: str | None) -> str:
|
||||||
|
cookie = http.cookies.SimpleCookie()
|
||||||
|
cookie[_SESSION_COOKIE_NAME] = token or ""
|
||||||
|
morsel = cookie[_SESSION_COOKIE_NAME]
|
||||||
|
morsel["path"] = "/"
|
||||||
|
morsel["httponly"] = True
|
||||||
|
morsel["samesite"] = "Lax"
|
||||||
|
if token:
|
||||||
|
morsel["max-age"] = str(int(7 * 86400))
|
||||||
|
else:
|
||||||
|
morsel["max-age"] = "0"
|
||||||
|
return morsel.OutputString()
|
||||||
|
|
||||||
|
|
||||||
def _usage_total_tokens(payload: dict) -> int | None:
|
def _usage_total_tokens(payload: dict) -> int | None:
|
||||||
usage = payload.get("usage")
|
usage = payload.get("usage")
|
||||||
if not isinstance(usage, dict):
|
if not isinstance(usage, dict):
|
||||||
@@ -2094,7 +2128,7 @@ def _registration_ban_error(contracts: Any | None, wallet_address: str | None) -
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _tracker_log(
|
def _tracker_log(
|
||||||
server: "_TrackerHTTPServer",
|
server: "_TrackerHTTPServer",
|
||||||
level: str,
|
level: str,
|
||||||
message: str,
|
message: str,
|
||||||
@@ -2102,18 +2136,18 @@ def _tracker_log(
|
|||||||
stdout: bool = True,
|
stdout: bool = True,
|
||||||
update_console_key: str | None = None,
|
update_console_key: str | None = None,
|
||||||
**fields: Any,
|
**fields: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
log_level = {
|
log_level = {
|
||||||
"debug": 10,
|
"debug": 10,
|
||||||
"info": 20,
|
"info": 20,
|
||||||
"warn": 30,
|
"warn": 30,
|
||||||
"warning": 30,
|
"warning": 30,
|
||||||
"error": 40,
|
"error": 40,
|
||||||
}.get(level.lower(), 20)
|
}.get(level.lower(), 20)
|
||||||
event = {
|
event = {
|
||||||
"ts": time.time(),
|
"ts": time.time(),
|
||||||
"level": level,
|
"level": level,
|
||||||
"message": message,
|
"message": message,
|
||||||
"fields": {
|
"fields": {
|
||||||
key: value
|
key: value
|
||||||
for key, value in fields.items()
|
for key, value in fields.items()
|
||||||
@@ -2134,13 +2168,13 @@ def _tracker_log(
|
|||||||
break
|
break
|
||||||
if not updated:
|
if not updated:
|
||||||
server.console_events.append(event)
|
server.console_events.append(event)
|
||||||
else:
|
else:
|
||||||
server.console_events.append(event)
|
server.console_events.append(event)
|
||||||
extras = " ".join(f"{key}={value}" for key, value in event["fields"].items())
|
extras = " ".join(f"{key}={value}" for key, value in event["fields"].items())
|
||||||
suffix = f" {extras}" if extras else ""
|
suffix = f" {extras}" if extras else ""
|
||||||
tracker_logger().log(log_level, f"{message}{suffix}")
|
tracker_logger().log(log_level, f"{message}{suffix}")
|
||||||
if stdout:
|
if stdout:
|
||||||
print(f"[tracker] {level}: {message}{suffix}", flush=True)
|
print(f"[tracker] {level}: {message}{suffix}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -2344,11 +2378,13 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
def log_message(self, fmt, *args): # noqa: suppress request logs in tests
|
def log_message(self, fmt, *args): # noqa: suppress request logs in tests
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _send_json(self, status: int, data: dict) -> None:
|
def _send_json(self, status: int, data: dict, headers: dict[str, str] | None = None) -> None:
|
||||||
body = json.dumps(data).encode()
|
body = json.dumps(data).encode()
|
||||||
self.send_response(status)
|
self.send_response(status)
|
||||||
self.send_header("Content-Type", "application/json")
|
self.send_header("Content-Type", "application/json")
|
||||||
self.send_header("Content-Length", str(len(body)))
|
self.send_header("Content-Length", str(len(body)))
|
||||||
|
for name, value in (headers or {}).items():
|
||||||
|
self.send_header(name, value)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
try:
|
try:
|
||||||
self.wfile.write(body)
|
self.wfile.write(body)
|
||||||
@@ -2365,7 +2401,7 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
inference and wallet binding only, never operator endpoints.
|
inference and wallet binding only, never operator endpoints.
|
||||||
"""
|
"""
|
||||||
server: _TrackerHTTPServer = self.server # type: ignore[assignment]
|
server: _TrackerHTTPServer = self.server # type: ignore[assignment]
|
||||||
token = _api_key_from_headers(self.headers)
|
token = _session_token_from_headers(self.headers)
|
||||||
if not token:
|
if not token:
|
||||||
return None, None
|
return None, None
|
||||||
if is_validator_token(token, server.validator_service_token):
|
if is_validator_token(token, server.validator_service_token):
|
||||||
@@ -4209,7 +4245,7 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
server: _TrackerHTTPServer = self.server # type: ignore[assignment]
|
server: _TrackerHTTPServer = self.server # type: ignore[assignment]
|
||||||
if server.accounts is None:
|
if server.accounts is None:
|
||||||
return None
|
return None
|
||||||
return server.accounts.session_account(_api_key_from_headers(self.headers))
|
return server.accounts.session_account(_session_token_from_headers(self.headers))
|
||||||
|
|
||||||
def _require_accounts(self) -> "AccountStore | None":
|
def _require_accounts(self) -> "AccountStore | None":
|
||||||
server: _TrackerHTTPServer = self.server # type: ignore[assignment]
|
server: _TrackerHTTPServer = self.server # type: ignore[assignment]
|
||||||
@@ -4249,7 +4285,11 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
f"[tracker] account registered: {account.get('email') or account.get('wallet')} "
|
f"[tracker] account registered: {account.get('email') or account.get('wallet')} "
|
||||||
f"role={account['role']}", flush=True,
|
f"role={account['role']}", flush=True,
|
||||||
)
|
)
|
||||||
self._send_json(200, {"account": account, "session_token": token, "api_key": api_key})
|
self._send_json(
|
||||||
|
200,
|
||||||
|
{"account": account, "session_token": token, "api_key": api_key},
|
||||||
|
headers={"Set-Cookie": _session_cookie_header(token)},
|
||||||
|
)
|
||||||
|
|
||||||
def _handle_auth_login(self):
|
def _handle_auth_login(self):
|
||||||
accounts = self._require_accounts()
|
accounts = self._require_accounts()
|
||||||
@@ -4264,14 +4304,18 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
self._send_json(401, {"error": "invalid credentials"})
|
self._send_json(401, {"error": "invalid credentials"})
|
||||||
return
|
return
|
||||||
token = accounts.create_session(account["account_id"])
|
token = accounts.create_session(account["account_id"])
|
||||||
self._send_json(200, {"account": account, "session_token": token})
|
self._send_json(
|
||||||
|
200,
|
||||||
|
{"account": account, "session_token": token},
|
||||||
|
headers={"Set-Cookie": _session_cookie_header(token)},
|
||||||
|
)
|
||||||
|
|
||||||
def _handle_auth_logout(self):
|
def _handle_auth_logout(self):
|
||||||
accounts = self._require_accounts()
|
accounts = self._require_accounts()
|
||||||
if accounts is None:
|
if accounts is None:
|
||||||
return
|
return
|
||||||
accounts.destroy_session(_api_key_from_headers(self.headers))
|
accounts.destroy_session(_session_token_from_headers(self.headers))
|
||||||
self._send_json(200, {"ok": True})
|
self._send_json(200, {"ok": True}, headers={"Set-Cookie": _session_cookie_header(None)})
|
||||||
|
|
||||||
def _handle_account_me(self):
|
def _handle_account_me(self):
|
||||||
"""Balance, usage, and API keys for the logged-in account."""
|
"""Balance, usage, and API keys for the logged-in account."""
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ register/login/logout, per-account balance and usage, API-key lifecycle
|
|||||||
(revoked keys rejected by the OpenAI proxy), and the admin listing.
|
(revoked keys rejected by the OpenAI proxy), and the admin listing.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import http.cookies
|
||||||
import json
|
import json
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.request
|
import urllib.request
|
||||||
@@ -68,6 +69,17 @@ def test_sessions_resolve_and_destroy():
|
|||||||
assert store.session_account("bogus") is None
|
assert store.session_account("bogus") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_sessions_persist_across_restart(tmp_path):
|
||||||
|
db = str(tmp_path / "accounts.db")
|
||||||
|
store = AccountStore(db_path=db)
|
||||||
|
account = store.register(email="cookie@example.com", password="secret-123")
|
||||||
|
token = store.create_session(account["account_id"])
|
||||||
|
store.save_to_db()
|
||||||
|
|
||||||
|
reloaded = AccountStore(db_path=db)
|
||||||
|
assert reloaded.session_account(token)["account_id"] == account["account_id"]
|
||||||
|
|
||||||
|
|
||||||
def test_api_key_lifecycle():
|
def test_api_key_lifecycle():
|
||||||
store = AccountStore()
|
store = AccountStore()
|
||||||
account = store.register(email="k@example.com", password="secret-123")
|
account = store.register(email="k@example.com", password="secret-123")
|
||||||
@@ -156,6 +168,59 @@ def test_register_login_and_account_view(account_tracker):
|
|||||||
assert me["usage"]["requests"] == 0
|
assert me["usage"]["requests"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_sets_cookie_and_cookie_auth_survives_tracker_restart(tmp_path):
|
||||||
|
accounts_db = str(tmp_path / "accounts.db")
|
||||||
|
tracker = TrackerServer(
|
||||||
|
billing=BillingLedger(starting_credit=0.0, default_price_per_1k=0.02),
|
||||||
|
accounts_db=accounts_db,
|
||||||
|
starting_credit=0.0,
|
||||||
|
devnet_topup_amount=0.0,
|
||||||
|
)
|
||||||
|
port = tracker.start()
|
||||||
|
url = f"http://127.0.0.1:{port}"
|
||||||
|
try:
|
||||||
|
_call(f"{url}/v1/auth/register", "POST",
|
||||||
|
{"email": "cookie-http@example.com", "password": "secret-123"})
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"{url}/v1/auth/login",
|
||||||
|
data=json.dumps({
|
||||||
|
"identifier": "cookie-http@example.com",
|
||||||
|
"password": "secret-123",
|
||||||
|
}).encode(),
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req) as r:
|
||||||
|
assert json.loads(r.read())["session_token"]
|
||||||
|
cookie_header = r.headers["Set-Cookie"]
|
||||||
|
finally:
|
||||||
|
tracker.stop()
|
||||||
|
|
||||||
|
cookie = http.cookies.SimpleCookie(cookie_header)
|
||||||
|
session_cookie = cookie["meshnet_session"].OutputString()
|
||||||
|
|
||||||
|
restarted = TrackerServer(
|
||||||
|
billing=BillingLedger(starting_credit=0.0, default_price_per_1k=0.02),
|
||||||
|
accounts_db=accounts_db,
|
||||||
|
starting_credit=0.0,
|
||||||
|
devnet_topup_amount=0.0,
|
||||||
|
)
|
||||||
|
restarted_port = restarted.start()
|
||||||
|
restarted_url = f"http://127.0.0.1:{restarted_port}"
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"{restarted_url}/v1/account",
|
||||||
|
headers={"Cookie": session_cookie},
|
||||||
|
method="GET",
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req) as r:
|
||||||
|
me = json.loads(r.read())
|
||||||
|
finally:
|
||||||
|
restarted.stop()
|
||||||
|
|
||||||
|
assert me["account"]["email"] == "cookie-http@example.com"
|
||||||
|
|
||||||
|
|
||||||
def test_bad_credentials_and_missing_session_are_401(account_tracker):
|
def test_bad_credentials_and_missing_session_are_401(account_tracker):
|
||||||
url, _ = account_tracker
|
url, _ = account_tracker
|
||||||
_call(f"{url}/v1/auth/register", "POST",
|
_call(f"{url}/v1/auth/register", "POST",
|
||||||
|
|||||||
Reference in New Issue
Block a user