amber-backend/scripts/seed-providers.py
Claude 9dd677b442 Seed with the token production is actually reachable with
secrets/amber.env holds PB_ADMIN_TOKEN and no superuser password, so the
email/password-only seeder could not be pointed at the live instance
without inventing a credential to satisfy it.

The token is also verified up front against a superuser-only endpoint,
because listing records cannot answer whether it is valid: PocketBase
applies a listRule as a FILTER, so a stale token returns 200 with zero
rows, indistinguishable from an empty collection. The seeder then decided
all four providers were missing and the first visible error was
"[FAIL] prehrajto: HTTP 403 Only superusers can perform this action",
blaming the row instead of the token. Found by trying it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 19:24:25 +02:00

115 lines
5.1 KiB
Python

"""Load (or refresh) the `providers` rows the setup flow renders.
python scripts/seed-providers.py # local docker compose
PB_BASE=https://amber.petruzalekr.cz \
PB_ADMIN_EMAIL=… PB_ADMIN_PASS=… python scripts/seed-providers.py
Reads `scripts/providers.seed.json` and **upserts by `slug`**, so running it
twice is the same as running it once. That matters more than it sounds: prices
and click-paths are edited in the admin UI, and a seeder that inserted blindly
would either duplicate every row or quietly overwrite a correction somebody made
last week. `--force` is the switch for "yes, put the file back over the top".
Without `--force` an existing row is left alone and reported as `kept`, which
makes this safe to run after any deploy — the collection gets populated on a
fresh instance and untouched on one that is already looked after.
Authenticates one of two ways, in this order:
PB_ADMIN_TOKEN an existing superuser token, used as-is
PB_ADMIN_EMAIL/PB_ADMIN_PASS a superuser sign-in (local default below)
The token path exists because that is what the production instance is reachable
with: `secrets/amber.env` holds `PB_ADMIN_TOKEN` and no superuser password, and
inventing one to satisfy this script would be the wrong way round. Never reads that
file itself, so nothing here needs access to the family's credentials, and never
prints the token.
"""
import json, os, pathlib, sys, urllib.error, urllib.request
BASE = os.environ.get("PB_BASE", "http://localhost:8090").rstrip("/")
ADMIN_TOKEN = os.environ.get("PB_ADMIN_TOKEN", "").strip()
ADMIN_EMAIL = os.environ.get("PB_ADMIN_EMAIL", "admin@myanime.local")
ADMIN_PASS = os.environ.get("PB_ADMIN_PASS", "Sup3rSecret!123")
FORCE = "--force" in sys.argv
SEED = pathlib.Path(__file__).with_name("providers.seed.json")
def req(method, path, token=None, body=None):
data = json.dumps(body).encode() if body is not None else None
r = urllib.request.Request(BASE + path, data=data, method=method)
r.add_header("Content-Type", "application/json")
if token:
r.add_header("Authorization", token)
try:
with urllib.request.urlopen(r) as resp:
return resp.status, json.loads(resp.read() or "{}")
except urllib.error.HTTPError as e:
return e.code, json.loads(e.read() or "{}")
def main():
rows = json.loads(SEED.read_text(encoding="utf-8"))
slugs = [r["slug"] for r in rows]
if len(set(slugs)) != len(slugs):
sys.exit(f"duplicate slug in {SEED.name}: {slugs}")
# The page keys its per-service wiring off these four strings; a typo here
# would render a card that leads nowhere, so it is checked rather than
# trusted.
known = {"torbox", "prehrajto", "webshare", "tmdb"}
unknown = set(slugs) - known
if unknown:
sys.exit(f"unknown slug(s) {sorted(unknown)} — the page has no wiring for these")
if ADMIN_TOKEN:
token = ADMIN_TOKEN
else:
st, auth = req("POST", "/api/collections/_superusers/auth-with-password",
body={"identity": ADMIN_EMAIL, "password": ADMIN_PASS})
if st != 200:
sys.exit(f"superuser auth failed against {BASE}: HTTP {st} {auth}")
token = auth["token"]
# Prove the token is really a superuser BEFORE reading anything, against an
# endpoint only a superuser may touch.
#
# Listing records cannot answer this. PocketBase applies a collection's listRule
# as a *filter*, so a stale token comes back HTTP 200 with zero rows — identical
# to a collection that is simply empty. The seeder then decides every provider is
# missing and tries to create all four, and the first thing you see is
# "[FAIL] prehrajto: HTTP 403 Only superusers can perform this action", which
# points at the row instead of at the token. Measured, not assumed.
st, who = req("GET", "/api/collections?perPage=1", token)
if st != 200:
sys.exit(f"HTTP {st} against {BASE}: this is not a valid superuser token. "
"Refresh PB_ADMIN_TOKEN in secrets/amber.env and do not work "
"around it.")
st, existing = req("GET", "/api/collections/providers/records?perPage=200", token)
if st != 200:
sys.exit(f"cannot list providers (is the migration applied?): HTTP {st} {existing}")
by_slug = {i["slug"]: i for i in existing.get("items", [])}
for row in rows:
slug = row["slug"]
cur = by_slug.get(slug)
if cur and not FORCE:
print(f"[kept] {slug} — already present, --force to overwrite")
continue
if cur:
st, res = req("PATCH", f"/api/collections/providers/records/{cur['id']}",
token, row)
verb = "updated"
else:
st, res = req("POST", "/api/collections/providers/records", token, row)
verb = "created"
if st not in (200, 201):
sys.exit(f"[FAIL] {slug}: HTTP {st} {res}")
print(f"[{verb}] {slug}")
print(f"\n{len(rows)} provider(s) in {BASE}. "
"Prices are editable in the admin UI — this file is only the starting point.")
if __name__ == "__main__":
main()