proto(woo): per-browser Woo creds via X-Woo-* request headers
Prototype routing so 2–3 owners can share one store URL with different WP application passwords. Device creds live in localStorage; server uses request override → Settings fallback and never writes overrides into the Settings row. Inference stays on shared Settings. See PROTOTYPE.md for UAT with two browsers and known LAN forge risk.
This commit is contained in:
+57
-3
@@ -1,8 +1,12 @@
|
||||
"""WooCommerce REST v3 client (stdlib urllib). Basic-auth, HTTPS-only, paginated.
|
||||
|
||||
Creds come from Settings (wp_username plain, app_password AES-GCM). Missing creds
|
||||
raise WooCredentialsMissing (a fail-closed condition the caller maps to ok:false),
|
||||
distinct from WooAuthError (a 401 from the store).
|
||||
Default creds come from Settings (wp_username plain, app_password AES-GCM).
|
||||
Missing creds raise WooCredentialsMissing (fail-closed), distinct from
|
||||
WooAuthError (401 from the store).
|
||||
|
||||
Prototype per-user override: callers may pass X-Woo-Username + X-Woo-App-Password
|
||||
request headers into client_for_request(); those build a per-request WooClient
|
||||
and are never written into Settings. See PROTOTYPE.md.
|
||||
"""
|
||||
|
||||
import base64
|
||||
@@ -177,3 +181,53 @@ class WooClient:
|
||||
if code not in (200, 201):
|
||||
raise WooServerError(f"media upload returned {code}")
|
||||
return int(json.loads(resp)["id"])
|
||||
|
||||
|
||||
# --- Per-request Woo credential override (prototype) ---
|
||||
# Scheme: browser sends plaintext WP Application Password credentials on
|
||||
# Woo-touching API calls via:
|
||||
# X-Woo-Username
|
||||
# X-Woo-App-Password
|
||||
# Server uses those for THIS request's WooClient only; never writes them into
|
||||
# the Settings row. If either header is missing/blank, fall back to the
|
||||
# shop-default singleton (woo_wp_username + encrypted woo_app_password).
|
||||
|
||||
HEADER_WOO_USERNAME = "X-Woo-Username"
|
||||
HEADER_WOO_APP_PASSWORD = "X-Woo-App-Password"
|
||||
|
||||
|
||||
def settings_woo_creds(conn) -> tuple[str, str | None, str | None]:
|
||||
"""Shop-default Woo creds from env + Settings (encrypted password)."""
|
||||
import os
|
||||
|
||||
base = os.environ.get("WOO_BASE_URL", "")
|
||||
u = conn.execute("SELECT value FROM settings WHERE key='woo_wp_username'").fetchone()
|
||||
p = conn.execute("SELECT value_enc FROM settings WHERE key='woo_app_password'").fetchone()
|
||||
return base, (u[0] if u else None), (p[0] if p else None)
|
||||
|
||||
|
||||
def request_woo_override(headers) -> tuple[str, str] | None:
|
||||
"""Return (username, app_password) if both override headers are present and non-blank."""
|
||||
if headers is None:
|
||||
return None
|
||||
# Starlette Headers are case-insensitive
|
||||
user = (headers.get(HEADER_WOO_USERNAME) or "").strip()
|
||||
password = (headers.get(HEADER_WOO_APP_PASSWORD) or "").strip()
|
||||
if user and password:
|
||||
return user, password
|
||||
return None
|
||||
|
||||
|
||||
def client_for_request(conn, headers=None, request: Callable[..., tuple[int, dict, bytes]] | None = None) -> WooClient:
|
||||
"""Build a WooClient for one HTTP request: override headers → Settings fallback.
|
||||
|
||||
Never mutates Settings. Inference stays on shared Settings (caller responsibility).
|
||||
"""
|
||||
base, settings_user, settings_enc = settings_woo_creds(conn)
|
||||
override = request_woo_override(headers)
|
||||
if override is not None:
|
||||
user, password = override
|
||||
if request is None:
|
||||
request = _default_request
|
||||
return WooClient(user, password, base, request)
|
||||
return WooClient.from_settings(base, settings_user, settings_enc, request=request)
|
||||
|
||||
Reference in New Issue
Block a user