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:
@@ -123,7 +123,7 @@ def register(url_prefix="/auth"):
|
|||||||
sep = "&" if "?" in redirect_uri else "?"
|
sep = "&" if "?" in redirect_uri else "?"
|
||||||
return redirect(
|
return redirect(
|
||||||
f"{redirect_uri}{sep}code={code}&state={state}"
|
f"{redirect_uri}{sep}code={code}&state={state}"
|
||||||
f"&account_did={account_did}"
|
f"&account_did={account_did}&grant_token={grant_token}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- OAuth2 token exchange (for external clients like artdag) -------------
|
# --- OAuth2 token exchange (for external clients like artdag) -------------
|
||||||
|
|||||||
@@ -31,51 +31,13 @@ def register() -> Blueprint:
|
|||||||
result = await handler()
|
result = await handler()
|
||||||
return jsonify(result or {"ok": True})
|
return jsonify(result or {"ok": True})
|
||||||
|
|
||||||
# --- update-page-config ---
|
# --- update-page-config (proxy to cart, where page_configs table lives) ---
|
||||||
async def _update_page_config():
|
async def _update_page_config():
|
||||||
"""Create or update a PageConfig (used by events payment admin)."""
|
"""Create or update a PageConfig — proxies to cart service."""
|
||||||
from shared.models.page_config import PageConfig
|
from shared.infrastructure.actions import call_action
|
||||||
from sqlalchemy import select
|
|
||||||
|
|
||||||
data = await request.get_json(force=True)
|
data = await request.get_json(force=True)
|
||||||
container_type = data.get("container_type", "page")
|
return await call_action("cart", "update-page-config", payload=data)
|
||||||
container_id = data.get("container_id")
|
|
||||||
if container_id is None:
|
|
||||||
return {"error": "container_id required"}, 400
|
|
||||||
|
|
||||||
pc = (await g.s.execute(
|
|
||||||
select(PageConfig).where(
|
|
||||||
PageConfig.container_type == container_type,
|
|
||||||
PageConfig.container_id == container_id,
|
|
||||||
)
|
|
||||||
)).scalar_one_or_none()
|
|
||||||
|
|
||||||
if pc is None:
|
|
||||||
pc = PageConfig(
|
|
||||||
container_type=container_type,
|
|
||||||
container_id=container_id,
|
|
||||||
features=data.get("features", {}),
|
|
||||||
)
|
|
||||||
g.s.add(pc)
|
|
||||||
await g.s.flush()
|
|
||||||
|
|
||||||
if "sumup_merchant_code" in data:
|
|
||||||
pc.sumup_merchant_code = data["sumup_merchant_code"] or None
|
|
||||||
if "sumup_checkout_prefix" in data:
|
|
||||||
pc.sumup_checkout_prefix = data["sumup_checkout_prefix"] or None
|
|
||||||
if "sumup_api_key" in data:
|
|
||||||
pc.sumup_api_key = data["sumup_api_key"] or None
|
|
||||||
|
|
||||||
await g.s.flush()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"id": pc.id,
|
|
||||||
"container_type": pc.container_type,
|
|
||||||
"container_id": pc.container_id,
|
|
||||||
"sumup_merchant_code": pc.sumup_merchant_code,
|
|
||||||
"sumup_checkout_prefix": pc.sumup_checkout_prefix,
|
|
||||||
"sumup_configured": bool(pc.sumup_api_key),
|
|
||||||
}
|
|
||||||
|
|
||||||
_handlers["update-page-config"] = _update_page_config
|
_handlers["update-page-config"] = _update_page_config
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from sqlalchemy.orm import selectinload, joinedload
|
from sqlalchemy.orm import selectinload, joinedload
|
||||||
|
|
||||||
from models.ghost_content import Post, Author, Tag, PostTag
|
from models.ghost_content import Post, Author, Tag, PostTag
|
||||||
from shared.models.page_config import PageConfig
|
|
||||||
from models.tag_group import TagGroup, TagGroupTag
|
from models.tag_group import TagGroup, TagGroupTag
|
||||||
|
|
||||||
|
|
||||||
@@ -280,7 +279,6 @@ class DBClient:
|
|||||||
joinedload(Post.primary_tag),
|
joinedload(Post.primary_tag),
|
||||||
selectinload(Post.authors),
|
selectinload(Post.authors),
|
||||||
selectinload(Post.tags),
|
selectinload(Post.tags),
|
||||||
joinedload(Post.page_config),
|
|
||||||
)
|
)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset_val)
|
.offset(offset_val)
|
||||||
@@ -288,10 +286,24 @@ class DBClient:
|
|||||||
|
|
||||||
rows: List[Post] = list((await self.sess.execute(q)).scalars())
|
rows: List[Post] = list((await self.sess.execute(q)).scalars())
|
||||||
|
|
||||||
|
# Load PageConfigs from cart service (page_configs lives in db_cart)
|
||||||
|
post_ids = [p.id for p in rows]
|
||||||
|
pc_map: Dict[int, dict] = {}
|
||||||
|
if post_ids:
|
||||||
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
try:
|
||||||
|
raw_pcs = await fetch_data("cart", "page-configs-batch",
|
||||||
|
params={"container_type": "page", "ids": ",".join(str(i) for i in post_ids)})
|
||||||
|
if isinstance(raw_pcs, list):
|
||||||
|
for pc in raw_pcs:
|
||||||
|
pc_map[pc["container_id"]] = pc
|
||||||
|
except Exception:
|
||||||
|
pass # graceful degradation — pages render without features
|
||||||
|
|
||||||
def _page_to_public(p: Post) -> Dict[str, Any]:
|
def _page_to_public(p: Post) -> Dict[str, Any]:
|
||||||
d = _post_to_public(p)
|
d = _post_to_public(p)
|
||||||
pc = p.page_config
|
pc = pc_map.get(p.id)
|
||||||
d["features"] = pc.features if pc else {}
|
d["features"] = pc["features"] if pc else {}
|
||||||
return d
|
return d
|
||||||
|
|
||||||
pages_list = [_page_to_public(p) for p in rows]
|
pages_list = [_page_to_public(p) for p in rows]
|
||||||
|
|||||||
@@ -7,25 +7,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from quart import Blueprint, g, jsonify, request
|
from quart import Blueprint, g, jsonify, request
|
||||||
|
|
||||||
from sqlalchemy import select
|
from shared.infrastructure.data_client import DATA_HEADER, fetch_data
|
||||||
|
|
||||||
from shared.infrastructure.data_client import DATA_HEADER
|
|
||||||
from shared.contracts.dtos import dto_to_dict
|
from shared.contracts.dtos import dto_to_dict
|
||||||
from shared.services.registry import services
|
from shared.services.registry import services
|
||||||
from shared.models.page_config import PageConfig
|
|
||||||
|
|
||||||
|
|
||||||
def _page_config_dict(pc: PageConfig) -> dict:
|
|
||||||
"""Serialize PageConfig to a JSON-safe dict."""
|
|
||||||
return {
|
|
||||||
"id": pc.id,
|
|
||||||
"container_type": pc.container_type,
|
|
||||||
"container_id": pc.container_id,
|
|
||||||
"features": pc.features or {},
|
|
||||||
"sumup_merchant_code": pc.sumup_merchant_code,
|
|
||||||
"sumup_api_key": pc.sumup_api_key,
|
|
||||||
"sumup_checkout_prefix": pc.sumup_checkout_prefix,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def register() -> Blueprint:
|
def register() -> Blueprint:
|
||||||
@@ -90,55 +74,30 @@ def register() -> Blueprint:
|
|||||||
|
|
||||||
_handlers["search-posts"] = _search_posts
|
_handlers["search-posts"] = _search_posts
|
||||||
|
|
||||||
# --- page-config ---
|
# --- page-config (proxy to cart, where page_configs table lives) ---
|
||||||
async def _page_config():
|
async def _page_config():
|
||||||
"""Return a single PageConfig by container_type + container_id."""
|
"""Return a single PageConfig by container_type + container_id."""
|
||||||
ct = request.args.get("container_type", "page")
|
return await fetch_data("cart", "page-config",
|
||||||
cid = request.args.get("container_id", type=int)
|
params={"container_type": request.args.get("container_type", "page"),
|
||||||
if cid is None:
|
"container_id": request.args.get("container_id", "")},
|
||||||
return None
|
required=False)
|
||||||
pc = (await g.s.execute(
|
|
||||||
select(PageConfig).where(
|
|
||||||
PageConfig.container_type == ct,
|
|
||||||
PageConfig.container_id == cid,
|
|
||||||
)
|
|
||||||
)).scalar_one_or_none()
|
|
||||||
if not pc:
|
|
||||||
return None
|
|
||||||
return _page_config_dict(pc)
|
|
||||||
|
|
||||||
_handlers["page-config"] = _page_config
|
_handlers["page-config"] = _page_config
|
||||||
|
|
||||||
# --- page-config-by-id ---
|
# --- page-config-by-id (proxy to cart) ---
|
||||||
async def _page_config_by_id():
|
async def _page_config_by_id():
|
||||||
"""Return a single PageConfig by its primary key."""
|
return await fetch_data("cart", "page-config-by-id",
|
||||||
pc_id = request.args.get("id", type=int)
|
params={"id": request.args.get("id", "")},
|
||||||
if pc_id is None:
|
required=False)
|
||||||
return None
|
|
||||||
pc = await g.s.get(PageConfig, pc_id)
|
|
||||||
if not pc:
|
|
||||||
return None
|
|
||||||
return _page_config_dict(pc)
|
|
||||||
|
|
||||||
_handlers["page-config-by-id"] = _page_config_by_id
|
_handlers["page-config-by-id"] = _page_config_by_id
|
||||||
|
|
||||||
# --- page-configs-batch ---
|
# --- page-configs-batch (proxy to cart) ---
|
||||||
async def _page_configs_batch():
|
async def _page_configs_batch():
|
||||||
"""Return PageConfigs for multiple container_ids (comma-separated)."""
|
return await fetch_data("cart", "page-configs-batch",
|
||||||
ct = request.args.get("container_type", "page")
|
params={"container_type": request.args.get("container_type", "page"),
|
||||||
ids_raw = request.args.get("ids", "")
|
"ids": request.args.get("ids", "")},
|
||||||
if not ids_raw:
|
required=False) or []
|
||||||
return []
|
|
||||||
ids = [int(x.strip()) for x in ids_raw.split(",") if x.strip()]
|
|
||||||
if not ids:
|
|
||||||
return []
|
|
||||||
result = await g.s.execute(
|
|
||||||
select(PageConfig).where(
|
|
||||||
PageConfig.container_type == ct,
|
|
||||||
PageConfig.container_id.in_(ids),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return [_page_config_dict(pc) for pc in result.scalars().all()]
|
|
||||||
|
|
||||||
_handlers["page-configs-batch"] = _page_configs_batch
|
_handlers["page-configs-batch"] = _page_configs_batch
|
||||||
|
|
||||||
|
|||||||
@@ -22,24 +22,23 @@ def register():
|
|||||||
@require_admin
|
@require_admin
|
||||||
async def admin(slug: str):
|
async def admin(slug: str):
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
from shared.browser.app.utils.htmx import is_htmx_request
|
||||||
from shared.models.page_config import PageConfig
|
from shared.infrastructure.data_client import fetch_data
|
||||||
from sqlalchemy import select as sa_select
|
|
||||||
|
|
||||||
# Load features for page admin
|
# Load features for page admin (page_configs lives in db_cart)
|
||||||
post = (g.post_data or {}).get("post", {})
|
post = (g.post_data or {}).get("post", {})
|
||||||
features = {}
|
features = {}
|
||||||
sumup_configured = False
|
sumup_configured = False
|
||||||
sumup_merchant_code = ""
|
sumup_merchant_code = ""
|
||||||
sumup_checkout_prefix = ""
|
sumup_checkout_prefix = ""
|
||||||
if post.get("is_page"):
|
if post.get("is_page"):
|
||||||
pc = (await g.s.execute(
|
raw_pc = await fetch_data("cart", "page-config",
|
||||||
sa_select(PageConfig).where(PageConfig.container_type == "page", PageConfig.container_id == post["id"])
|
params={"container_type": "page", "container_id": post["id"]},
|
||||||
)).scalar_one_or_none()
|
required=False)
|
||||||
if pc:
|
if raw_pc:
|
||||||
features = pc.features or {}
|
features = raw_pc.get("features") or {}
|
||||||
sumup_configured = bool(pc.sumup_api_key)
|
sumup_configured = bool(raw_pc.get("sumup_api_key"))
|
||||||
sumup_merchant_code = pc.sumup_merchant_code or ""
|
sumup_merchant_code = raw_pc.get("sumup_merchant_code") or ""
|
||||||
sumup_checkout_prefix = pc.sumup_checkout_prefix or ""
|
sumup_checkout_prefix = raw_pc.get("sumup_checkout_prefix") or ""
|
||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
"features": features,
|
"features": features,
|
||||||
@@ -61,12 +60,9 @@ def register():
|
|||||||
@bp.put("/features/")
|
@bp.put("/features/")
|
||||||
@require_admin
|
@require_admin
|
||||||
async def update_features(slug: str):
|
async def update_features(slug: str):
|
||||||
"""Update PageConfig.features for a page."""
|
"""Update PageConfig.features for a page (page_configs lives in db_cart)."""
|
||||||
from shared.models.page_config import PageConfig
|
from shared.infrastructure.actions import call_action
|
||||||
from models.ghost_content import Post
|
|
||||||
from sqlalchemy import select as sa_select
|
|
||||||
from quart import jsonify
|
from quart import jsonify
|
||||||
import json
|
|
||||||
|
|
||||||
post = g.post_data.get("post")
|
post = g.post_data.get("post")
|
||||||
if not post or not post.get("is_page"):
|
if not post or not post.get("is_page"):
|
||||||
@@ -74,21 +70,9 @@ def register():
|
|||||||
|
|
||||||
post_id = post["id"]
|
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
|
# Parse request body
|
||||||
body = await request.get_json()
|
body = await request.get_json()
|
||||||
if body is None:
|
if body is None:
|
||||||
# Fall back to form data
|
|
||||||
form = await request.form
|
form = await request.form
|
||||||
body = {}
|
body = {}
|
||||||
for key in ("calendar", "market"):
|
for key in ("calendar", "market"):
|
||||||
@@ -99,38 +83,30 @@ def register():
|
|||||||
if not isinstance(body, dict):
|
if not isinstance(body, dict):
|
||||||
return jsonify({"error": "Expected JSON object with feature flags."}), 400
|
return jsonify({"error": "Expected JSON object with feature flags."}), 400
|
||||||
|
|
||||||
# Merge features
|
# Update via cart action (page_configs lives in db_cart)
|
||||||
features = dict(pc.features or {})
|
result = await call_action("cart", "update-page-config", payload={
|
||||||
for key, val in body.items():
|
"container_type": "page",
|
||||||
if isinstance(val, bool):
|
"container_id": post_id,
|
||||||
features[key] = val
|
"features": body,
|
||||||
elif val in ("true", "1", "on"):
|
})
|
||||||
features[key] = True
|
|
||||||
elif val in ("false", "0", "off", None):
|
|
||||||
features[key] = False
|
|
||||||
|
|
||||||
pc.features = features
|
features = result.get("features", {})
|
||||||
from sqlalchemy.orm.attributes import flag_modified
|
|
||||||
flag_modified(pc, "features")
|
|
||||||
await g.s.flush()
|
|
||||||
|
|
||||||
# Return updated features panel
|
|
||||||
html = await render_template(
|
html = await render_template(
|
||||||
"_types/post/admin/_features_panel.html",
|
"_types/post/admin/_features_panel.html",
|
||||||
features=features,
|
features=features,
|
||||||
post=post,
|
post=post,
|
||||||
sumup_configured=bool(pc.sumup_api_key),
|
sumup_configured=result.get("sumup_configured", False),
|
||||||
sumup_merchant_code=pc.sumup_merchant_code or "",
|
sumup_merchant_code=result.get("sumup_merchant_code") or "",
|
||||||
sumup_checkout_prefix=pc.sumup_checkout_prefix or "",
|
sumup_checkout_prefix=result.get("sumup_checkout_prefix") or "",
|
||||||
)
|
)
|
||||||
return await make_response(html)
|
return await make_response(html)
|
||||||
|
|
||||||
@bp.put("/admin/sumup/")
|
@bp.put("/admin/sumup/")
|
||||||
@require_admin
|
@require_admin
|
||||||
async def update_sumup(slug: str):
|
async def update_sumup(slug: str):
|
||||||
"""Update PageConfig SumUp credentials for a page."""
|
"""Update PageConfig SumUp credentials for a page (page_configs lives in db_cart)."""
|
||||||
from shared.models.page_config import PageConfig
|
from shared.infrastructure.actions import call_action
|
||||||
from sqlalchemy import select as sa_select
|
|
||||||
from quart import jsonify
|
from quart import jsonify
|
||||||
|
|
||||||
post = g.post_data.get("post")
|
post = g.post_data.get("post")
|
||||||
@@ -139,37 +115,30 @@ def register():
|
|||||||
|
|
||||||
post_id = post["id"]
|
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
|
form = await request.form
|
||||||
merchant_code = (form.get("merchant_code") or "").strip()
|
merchant_code = (form.get("merchant_code") or "").strip()
|
||||||
api_key = (form.get("api_key") or "").strip()
|
api_key = (form.get("api_key") or "").strip()
|
||||||
checkout_prefix = (form.get("checkout_prefix") or "").strip()
|
checkout_prefix = (form.get("checkout_prefix") or "").strip()
|
||||||
|
|
||||||
pc.sumup_merchant_code = merchant_code or None
|
payload: dict = {
|
||||||
pc.sumup_checkout_prefix = checkout_prefix or None
|
"container_type": "page",
|
||||||
# Only update API key if non-empty (allows updating other fields without re-entering key)
|
"container_id": post_id,
|
||||||
|
"sumup_merchant_code": merchant_code or None,
|
||||||
|
"sumup_checkout_prefix": checkout_prefix or None,
|
||||||
|
}
|
||||||
if api_key:
|
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(
|
html = await render_template(
|
||||||
"_types/post/admin/_features_panel.html",
|
"_types/post/admin/_features_panel.html",
|
||||||
features=features,
|
features=features,
|
||||||
post=post,
|
post=post,
|
||||||
sumup_configured=bool(pc.sumup_api_key),
|
sumup_configured=result.get("sumup_configured", False),
|
||||||
sumup_merchant_code=pc.sumup_merchant_code or "",
|
sumup_merchant_code=result.get("sumup_merchant_code") or "",
|
||||||
sumup_checkout_prefix=pc.sumup_checkout_prefix or "",
|
sumup_checkout_prefix=result.get("sumup_checkout_prefix") or "",
|
||||||
)
|
)
|
||||||
return await make_response(html)
|
return await make_response(html)
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,11 @@ from __future__ import annotations
|
|||||||
import re
|
import re
|
||||||
import unicodedata
|
import unicodedata
|
||||||
|
|
||||||
from sqlalchemy import select
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from shared.models.page_config import PageConfig
|
|
||||||
from shared.contracts.dtos import MarketPlaceDTO
|
from shared.contracts.dtos import MarketPlaceDTO
|
||||||
from shared.infrastructure.actions import call_action, ActionError
|
from shared.infrastructure.actions import call_action, ActionError
|
||||||
|
from shared.infrastructure.data_client import fetch_data
|
||||||
from shared.services.registry import services
|
from shared.services.registry import services
|
||||||
|
|
||||||
|
|
||||||
@@ -42,10 +41,10 @@ async def create_market(sess: AsyncSession, post_id: int, name: str) -> MarketPl
|
|||||||
if not post.is_page:
|
if not post.is_page:
|
||||||
raise MarketError("Markets can only be created on pages, not posts.")
|
raise MarketError("Markets can only be created on pages, not posts.")
|
||||||
|
|
||||||
pc = (await sess.execute(
|
raw_pc = await fetch_data("cart", "page-config",
|
||||||
select(PageConfig).where(PageConfig.container_type == "page", PageConfig.container_id == post_id)
|
params={"container_type": "page", "container_id": post_id},
|
||||||
)).scalar_one_or_none()
|
required=False)
|
||||||
if pc is None or not (pc.features or {}).get("market"):
|
if raw_pc is None or not (raw_pc.get("features") or {}).get("market"):
|
||||||
raise MarketError("Market feature is not enabled for this page. Enable it in page settings first.")
|
raise MarketError("Market feature is not enabled for this page. Enable it in page settings first.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -47,4 +47,66 @@ def register() -> Blueprint:
|
|||||||
|
|
||||||
_handlers["adopt-cart-for-user"] = _adopt_cart
|
_handlers["adopt-cart-for-user"] = _adopt_cart
|
||||||
|
|
||||||
|
# --- update-page-config ---
|
||||||
|
async def _update_page_config():
|
||||||
|
"""Create or update a PageConfig (page_configs lives in db_cart)."""
|
||||||
|
from shared.models.page_config import PageConfig
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm.attributes import flag_modified
|
||||||
|
|
||||||
|
data = await request.get_json(force=True)
|
||||||
|
container_type = data.get("container_type", "page")
|
||||||
|
container_id = data.get("container_id")
|
||||||
|
if container_id is None:
|
||||||
|
return {"error": "container_id required"}, 400
|
||||||
|
|
||||||
|
pc = (await g.s.execute(
|
||||||
|
select(PageConfig).where(
|
||||||
|
PageConfig.container_type == container_type,
|
||||||
|
PageConfig.container_id == container_id,
|
||||||
|
)
|
||||||
|
)).scalar_one_or_none()
|
||||||
|
|
||||||
|
if pc is None:
|
||||||
|
pc = PageConfig(
|
||||||
|
container_type=container_type,
|
||||||
|
container_id=container_id,
|
||||||
|
features=data.get("features", {}),
|
||||||
|
)
|
||||||
|
g.s.add(pc)
|
||||||
|
await g.s.flush()
|
||||||
|
|
||||||
|
if "features" in data:
|
||||||
|
features = dict(pc.features or {})
|
||||||
|
for key, val in data["features"].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
|
||||||
|
pc.features = features
|
||||||
|
flag_modified(pc, "features")
|
||||||
|
|
||||||
|
if "sumup_merchant_code" in data:
|
||||||
|
pc.sumup_merchant_code = data["sumup_merchant_code"] or None
|
||||||
|
if "sumup_checkout_prefix" in data:
|
||||||
|
pc.sumup_checkout_prefix = data["sumup_checkout_prefix"] or None
|
||||||
|
if "sumup_api_key" in data:
|
||||||
|
pc.sumup_api_key = data["sumup_api_key"] or None
|
||||||
|
|
||||||
|
await g.s.flush()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": pc.id,
|
||||||
|
"container_type": pc.container_type,
|
||||||
|
"container_id": pc.container_id,
|
||||||
|
"features": pc.features or {},
|
||||||
|
"sumup_merchant_code": pc.sumup_merchant_code,
|
||||||
|
"sumup_checkout_prefix": pc.sumup_checkout_prefix,
|
||||||
|
"sumup_configured": bool(pc.sumup_api_key),
|
||||||
|
}
|
||||||
|
|
||||||
|
_handlers["update-page-config"] = _update_page_config
|
||||||
|
|
||||||
return bp
|
return bp
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ def register(url_prefix: str) -> Blueprint:
|
|||||||
if order.page_config_id:
|
if order.page_config_id:
|
||||||
from shared.infrastructure.data_client import fetch_data
|
from shared.infrastructure.data_client import fetch_data
|
||||||
from shared.contracts.dtos import CalendarEntryDTO, TicketDTO, dto_from_dict
|
from shared.contracts.dtos import CalendarEntryDTO, TicketDTO, dto_from_dict
|
||||||
raw_pc = await fetch_data("blog", "page-config-by-id",
|
raw_pc = await fetch_data("cart", "page-config-by-id",
|
||||||
params={"id": order.page_config_id},
|
params={"id": order.page_config_id},
|
||||||
required=False)
|
required=False)
|
||||||
post = await fetch_data("blog", "post-by-id",
|
post = await fetch_data("blog", "post-by-id",
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ async def get_cart_grouped_by_page(session) -> list[dict]:
|
|||||||
p = dto_from_dict(PostDTO, raw_p)
|
p = dto_from_dict(PostDTO, raw_p)
|
||||||
posts_by_id[p.id] = p
|
posts_by_id[p.id] = p
|
||||||
|
|
||||||
raw_pcs = await fetch_data("blog", "page-configs-batch",
|
raw_pcs = await fetch_data("cart", "page-configs-batch",
|
||||||
params={"container_type": "page",
|
params={"container_type": "page",
|
||||||
"ids": ",".join(str(i) for i in post_ids)},
|
"ids": ",".join(str(i) for i in post_ids)},
|
||||||
required=False) or []
|
required=False) or []
|
||||||
|
|||||||
@@ -111,4 +111,77 @@ def register() -> Blueprint:
|
|||||||
|
|
||||||
_handlers["cart-items"] = _cart_items
|
_handlers["cart-items"] = _cart_items
|
||||||
|
|
||||||
|
# --- page-config ---
|
||||||
|
async def _page_config():
|
||||||
|
"""Return a single PageConfig by container_type + container_id."""
|
||||||
|
from sqlalchemy import select
|
||||||
|
from shared.models.page_config import PageConfig
|
||||||
|
|
||||||
|
ct = request.args.get("container_type", "page")
|
||||||
|
cid = request.args.get("container_id", type=int)
|
||||||
|
if cid is None:
|
||||||
|
return None
|
||||||
|
pc = (await g.s.execute(
|
||||||
|
select(PageConfig).where(
|
||||||
|
PageConfig.container_type == ct,
|
||||||
|
PageConfig.container_id == cid,
|
||||||
|
)
|
||||||
|
)).scalar_one_or_none()
|
||||||
|
if not pc:
|
||||||
|
return None
|
||||||
|
return _page_config_dict(pc)
|
||||||
|
|
||||||
|
_handlers["page-config"] = _page_config
|
||||||
|
|
||||||
|
# --- page-config-by-id ---
|
||||||
|
async def _page_config_by_id():
|
||||||
|
"""Return a single PageConfig by its primary key."""
|
||||||
|
from shared.models.page_config import PageConfig
|
||||||
|
|
||||||
|
pc_id = request.args.get("id", type=int)
|
||||||
|
if pc_id is None:
|
||||||
|
return None
|
||||||
|
pc = await g.s.get(PageConfig, pc_id)
|
||||||
|
if not pc:
|
||||||
|
return None
|
||||||
|
return _page_config_dict(pc)
|
||||||
|
|
||||||
|
_handlers["page-config-by-id"] = _page_config_by_id
|
||||||
|
|
||||||
|
# --- page-configs-batch ---
|
||||||
|
async def _page_configs_batch():
|
||||||
|
"""Return PageConfigs for multiple container_ids (comma-separated)."""
|
||||||
|
from sqlalchemy import select
|
||||||
|
from shared.models.page_config import PageConfig
|
||||||
|
|
||||||
|
ct = request.args.get("container_type", "page")
|
||||||
|
ids_raw = request.args.get("ids", "")
|
||||||
|
if not ids_raw:
|
||||||
|
return []
|
||||||
|
ids = [int(x.strip()) for x in ids_raw.split(",") if x.strip()]
|
||||||
|
if not ids:
|
||||||
|
return []
|
||||||
|
result = await g.s.execute(
|
||||||
|
select(PageConfig).where(
|
||||||
|
PageConfig.container_type == ct,
|
||||||
|
PageConfig.container_id.in_(ids),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return [_page_config_dict(pc) for pc in result.scalars().all()]
|
||||||
|
|
||||||
|
_handlers["page-configs-batch"] = _page_configs_batch
|
||||||
|
|
||||||
return bp
|
return bp
|
||||||
|
|
||||||
|
|
||||||
|
def _page_config_dict(pc) -> dict:
|
||||||
|
"""Serialize PageConfig to a JSON-safe dict."""
|
||||||
|
return {
|
||||||
|
"id": pc.id,
|
||||||
|
"container_type": pc.container_type,
|
||||||
|
"container_id": pc.container_id,
|
||||||
|
"features": pc.features or {},
|
||||||
|
"sumup_merchant_code": pc.sumup_merchant_code,
|
||||||
|
"sumup_api_key": pc.sumup_api_key,
|
||||||
|
"sumup_checkout_prefix": pc.sumup_checkout_prefix,
|
||||||
|
}
|
||||||
|
|||||||
@@ -95,23 +95,33 @@ def create_oauth_blueprint(app_name: str) -> Blueprint:
|
|||||||
|
|
||||||
if not code or not state or state != expected_state:
|
if not code or not state or state != expected_state:
|
||||||
current_app.logger.warning("OAuth callback: bad state or missing code")
|
current_app.logger.warning("OAuth callback: bad state or missing code")
|
||||||
|
# Set _pnone_at to prevent immediate re-trigger of prompt=none
|
||||||
|
import time as _time
|
||||||
|
qsession["_pnone_at"] = _time.time()
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
|
|
||||||
expected_redirect = app_url(app_name, "/auth/callback")
|
expected_redirect = app_url(app_name, "/auth/callback")
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
# OAuthCode uses hashed storage — look up by code_hash
|
||||||
|
import hashlib
|
||||||
|
code_hash = hashlib.sha256(code.encode()).hexdigest()
|
||||||
|
|
||||||
# OAuthCode lives in db_account — use account session
|
# OAuthCode lives in db_account — use account session
|
||||||
async with get_account_session() as s:
|
async with get_account_session() as s:
|
||||||
async with s.begin():
|
async with s.begin():
|
||||||
result = await s.execute(
|
result = await s.execute(
|
||||||
select(OAuthCode)
|
select(OAuthCode)
|
||||||
.where(OAuthCode.code == code)
|
.where(OAuthCode.code_hash == code_hash)
|
||||||
.with_for_update()
|
.with_for_update()
|
||||||
)
|
)
|
||||||
oauth_code = result.scalar_one_or_none()
|
oauth_code = result.scalar_one_or_none()
|
||||||
|
|
||||||
if not oauth_code:
|
if not oauth_code:
|
||||||
current_app.logger.warning("OAuth callback: code not found")
|
current_app.logger.warning("OAuth callback: code not found")
|
||||||
|
# Set _pnone_at to prevent redirect loop
|
||||||
|
import time as _time
|
||||||
|
qsession["_pnone_at"] = _time.time()
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
|
|
||||||
if oauth_code.used_at is not None:
|
if oauth_code.used_at is not None:
|
||||||
@@ -132,7 +142,9 @@ def create_oauth_blueprint(app_name: str) -> Blueprint:
|
|||||||
|
|
||||||
oauth_code.used_at = now
|
oauth_code.used_at = now
|
||||||
user_id = oauth_code.user_id
|
user_id = oauth_code.user_id
|
||||||
grant_token = oauth_code.grant_token
|
|
||||||
|
# grant_token is passed in the redirect URL (raw, for session storage)
|
||||||
|
grant_token = request.args.get("grant_token")
|
||||||
|
|
||||||
# Set local session with grant token for revocation checking
|
# Set local session with grant token for revocation checking
|
||||||
qsession[SESSION_USER_KEY] = user_id
|
qsession[SESSION_USER_KEY] = user_id
|
||||||
|
|||||||
Reference in New Issue
Block a user