Allow header-based invite auth for seeding over HTTP.
CI / python-ci (pull_request) Successful in 1m15s
CI / secret-scan (pull_request) Successful in 31s

Secure cookies blocked the seed script against the LAN port; X-Stork-Invite + display-name headers fix that.
This commit is contained in:
2026-08-06 16:45:53 -04:00
parent b88791a0b7
commit c0c102da7c
3 changed files with 31 additions and 11 deletions
+6 -9
View File
@@ -1,12 +1,7 @@
"""Seed starter first names into a local or remote Stork instance.
Usage:
# against local make run (cookies via invite):
STORK_URL=http://127.0.0.1:8094 STORK_INVITE=local-dev-invite \\
.venv/bin/python scripts/seed_names.py
# against prod after deploy:
STORK_URL=https://stork.levkin.ca STORK_INVITE="$(ssh root@10.0.10.45 grep STORK_INVITE_TOKEN /opt/stork/.env | cut -d= -f2-)" \\
STORK_URL=http://10.0.10.45:8094 STORK_INVITE=... \\
.venv/bin/python scripts/seed_names.py
"""
@@ -31,9 +26,11 @@ def main() -> int:
print("Set STORK_INVITE", file=sys.stderr)
return 1
names = json.loads(SEED.read_text())["names"]
with httpx.Client(base_url=base, timeout=30.0) as client:
session = client.post("/api/session", json={"invite": invite, "display_name": display})
session.raise_for_status()
headers = {
"X-Stork-Invite": invite,
"X-Stork-Display-Name": display,
}
with httpx.Client(base_url=base, timeout=30.0, headers=headers) as client:
for item in names:
res = client.post("/api/names", json=item)
if res.status_code == 400 and "already exists" in res.text:
+9 -2
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import hashlib
import os
import secrets
from pathlib import Path
@@ -67,12 +68,18 @@ def _session(
stork_voter: str | None = Cookie(default=None),
stork_name: str | None = Cookie(default=None),
x_stork_invite: str | None = Header(default=None),
x_stork_display_name: str | None = Header(default=None),
x_stork_voter: str | None = Header(default=None),
) -> dict[str, str]:
invite = x_stork_invite or stork_invite
if not _invite_ok(invite):
raise HTTPException(401, "Invite required")
voter = (stork_voter or "").strip()
name = (stork_name or "").strip()
voter = (x_stork_voter or stork_voter or "").strip()
name = (x_stork_display_name or stork_name or "").strip()
if name and not voter:
# API/script clients without cookies (e.g. seed over HTTP while Secure cookies are on).
digest = hashlib.sha256(name.lower().encode()).hexdigest()[:24]
voter = f"hdr-{digest}"
if not voter or not name:
raise HTTPException(401, "Session required — open the invite link and enter your name")
return {"voter_key": voter, "display_name": name}
+16
View File
@@ -103,3 +103,19 @@ def test_second_voter_ranking(client: TestClient, authed: TestClient) -> None:
listed = client.get("/api/names?kind=first").json()["items"]
assert listed[0]["score"] == 2
assert listed[0]["vote_count"] == 2
def test_header_auth_without_cookies(client: TestClient) -> None:
headers = {
"X-Stork-Invite": "test-invite-token",
"X-Stork-Display-Name": "Seed Bot",
}
res = client.post(
"/api/names",
headers=headers,
json={"kind": "first", "spelling": "HeaderOnly"},
)
assert res.status_code == 200
listed = client.get("/api/names?kind=first", headers=headers)
assert listed.status_code == 200
assert any(n["spelling"] == "HeaderOnly" for n in listed.json()["items"])