Fix cross-DB queries: move page_configs to cart, fix OAuth code_hash lookup

page_configs table lives in db_cart but blog was querying it directly,
causing UndefinedTableError. Move all PageConfig read/write endpoints to
cart service and have blog proxy via fetch_data/call_action.

Also fix OAuth callback to use code_hash lookup (codes are now stored
hashed) and pass grant_token in redirect URL to prevent auth loops.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 17:43:21 +00:00
parent 98aee1f656
commit dd52417241
11 changed files with 228 additions and 180 deletions

View File

@@ -22,24 +22,23 @@ def register():
@require_admin
async def admin(slug: str):
from shared.browser.app.utils.htmx import is_htmx_request
from shared.models.page_config import PageConfig
from sqlalchemy import select as sa_select
from shared.infrastructure.data_client import fetch_data
# Load features for page admin
# Load features for page admin (page_configs lives in db_cart)
post = (g.post_data or {}).get("post", {})
features = {}
sumup_configured = False
sumup_merchant_code = ""
sumup_checkout_prefix = ""
if post.get("is_page"):
pc = (await g.s.execute(
sa_select(PageConfig).where(PageConfig.container_type == "page", PageConfig.container_id == post["id"])
)).scalar_one_or_none()
if pc:
features = pc.features or {}
sumup_configured = bool(pc.sumup_api_key)
sumup_merchant_code = pc.sumup_merchant_code or ""
sumup_checkout_prefix = pc.sumup_checkout_prefix or ""
raw_pc = await fetch_data("cart", "page-config",
params={"container_type": "page", "container_id": post["id"]},
required=False)
if raw_pc:
features = raw_pc.get("features") or {}
sumup_configured = bool(raw_pc.get("sumup_api_key"))
sumup_merchant_code = raw_pc.get("sumup_merchant_code") or ""
sumup_checkout_prefix = raw_pc.get("sumup_checkout_prefix") or ""
ctx = {
"features": features,
@@ -61,12 +60,9 @@ def register():
@bp.put("/features/")
@require_admin
async def update_features(slug: str):
"""Update PageConfig.features for a page."""
from shared.models.page_config import PageConfig
from models.ghost_content import Post
from sqlalchemy import select as sa_select
"""Update PageConfig.features for a page (page_configs lives in db_cart)."""
from shared.infrastructure.actions import call_action
from quart import jsonify
import json
post = g.post_data.get("post")
if not post or not post.get("is_page"):
@@ -74,21 +70,9 @@ def register():
post_id = post["id"]
# Load or create PageConfig
pc = (await g.s.execute(
sa_select(PageConfig).where(PageConfig.container_type == "page", PageConfig.container_id == post_id)
)).scalar_one_or_none()
if pc is None:
pc = PageConfig(container_type="page", container_id=post_id, features={})
g.s.add(pc)
await g.s.flush()
from shared.services.relationships import attach_child
await attach_child(g.s, "page", post_id, "page_config", pc.id)
# Parse request body
body = await request.get_json()
if body is None:
# Fall back to form data
form = await request.form
body = {}
for key in ("calendar", "market"):
@@ -99,38 +83,30 @@ def register():
if not isinstance(body, dict):
return jsonify({"error": "Expected JSON object with feature flags."}), 400
# Merge features
features = dict(pc.features or {})
for key, val in body.items():
if isinstance(val, bool):
features[key] = val
elif val in ("true", "1", "on"):
features[key] = True
elif val in ("false", "0", "off", None):
features[key] = False
# Update via cart action (page_configs lives in db_cart)
result = await call_action("cart", "update-page-config", payload={
"container_type": "page",
"container_id": post_id,
"features": body,
})
pc.features = features
from sqlalchemy.orm.attributes import flag_modified
flag_modified(pc, "features")
await g.s.flush()
features = result.get("features", {})
# Return updated features panel
html = await render_template(
"_types/post/admin/_features_panel.html",
features=features,
post=post,
sumup_configured=bool(pc.sumup_api_key),
sumup_merchant_code=pc.sumup_merchant_code or "",
sumup_checkout_prefix=pc.sumup_checkout_prefix or "",
sumup_configured=result.get("sumup_configured", False),
sumup_merchant_code=result.get("sumup_merchant_code") or "",
sumup_checkout_prefix=result.get("sumup_checkout_prefix") or "",
)
return await make_response(html)
@bp.put("/admin/sumup/")
@require_admin
async def update_sumup(slug: str):
"""Update PageConfig SumUp credentials for a page."""
from shared.models.page_config import PageConfig
from sqlalchemy import select as sa_select
"""Update PageConfig SumUp credentials for a page (page_configs lives in db_cart)."""
from shared.infrastructure.actions import call_action
from quart import jsonify
post = g.post_data.get("post")
@@ -139,37 +115,30 @@ def register():
post_id = post["id"]
pc = (await g.s.execute(
sa_select(PageConfig).where(PageConfig.container_type == "page", PageConfig.container_id == post_id)
)).scalar_one_or_none()
if pc is None:
pc = PageConfig(container_type="page", container_id=post_id, features={})
g.s.add(pc)
await g.s.flush()
from shared.services.relationships import attach_child
await attach_child(g.s, "page", post_id, "page_config", pc.id)
form = await request.form
merchant_code = (form.get("merchant_code") or "").strip()
api_key = (form.get("api_key") or "").strip()
checkout_prefix = (form.get("checkout_prefix") or "").strip()
pc.sumup_merchant_code = merchant_code or None
pc.sumup_checkout_prefix = checkout_prefix or None
# Only update API key if non-empty (allows updating other fields without re-entering key)
payload: dict = {
"container_type": "page",
"container_id": post_id,
"sumup_merchant_code": merchant_code or None,
"sumup_checkout_prefix": checkout_prefix or None,
}
if api_key:
pc.sumup_api_key = api_key
payload["sumup_api_key"] = api_key
await g.s.flush()
result = await call_action("cart", "update-page-config", payload=payload)
features = pc.features or {}
features = result.get("features", {})
html = await render_template(
"_types/post/admin/_features_panel.html",
features=features,
post=post,
sumup_configured=bool(pc.sumup_api_key),
sumup_merchant_code=pc.sumup_merchant_code or "",
sumup_checkout_prefix=pc.sumup_checkout_prefix or "",
sumup_configured=result.get("sumup_configured", False),
sumup_merchant_code=result.get("sumup_merchant_code") or "",
sumup_checkout_prefix=result.get("sumup_checkout_prefix") or "",
)
return await make_response(html)