Critical: Add ownership checks to all order routes (IDOR fix). High: Redis rate limiting on auth endpoints, HMAC-signed internal service calls replacing header-presence-only checks, nh3 HTML sanitization on ghost_sync and product import, internal auth on market API endpoints, SHA-256 hashed OAuth grant/code tokens. Medium: SECRET_KEY production guard, AP signature enforcement, is_admin param removal, cart_sid validation, SSRF protection on remote actor fetch. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
146 lines
4.7 KiB
Python
146 lines
4.7 KiB
Python
"""Blog app data endpoints.
|
|
|
|
Exposes read-only JSON queries at ``/internal/data/<query_name>`` for
|
|
cross-app callers via the internal data client.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from quart import Blueprint, g, jsonify, request
|
|
|
|
from sqlalchemy import select
|
|
|
|
from shared.infrastructure.data_client import DATA_HEADER
|
|
from shared.contracts.dtos import dto_to_dict
|
|
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:
|
|
bp = Blueprint("data", __name__, url_prefix="/internal/data")
|
|
|
|
@bp.before_request
|
|
async def _require_data_header():
|
|
if not request.headers.get(DATA_HEADER):
|
|
return jsonify({"error": "forbidden"}), 403
|
|
from shared.infrastructure.internal_auth import validate_internal_request
|
|
if not validate_internal_request():
|
|
return jsonify({"error": "forbidden"}), 403
|
|
|
|
_handlers: dict[str, object] = {}
|
|
|
|
@bp.get("/<query_name>")
|
|
async def handle_query(query_name: str):
|
|
handler = _handlers.get(query_name)
|
|
if handler is None:
|
|
return jsonify({"error": "unknown query"}), 404
|
|
result = await handler()
|
|
return jsonify(result)
|
|
|
|
# --- post-by-slug ---
|
|
async def _post_by_slug():
|
|
slug = request.args.get("slug", "")
|
|
post = await services.blog.get_post_by_slug(g.s, slug)
|
|
if not post:
|
|
return None
|
|
return dto_to_dict(post)
|
|
|
|
_handlers["post-by-slug"] = _post_by_slug
|
|
|
|
# --- post-by-id ---
|
|
async def _post_by_id():
|
|
post_id = int(request.args.get("id", 0))
|
|
post = await services.blog.get_post_by_id(g.s, post_id)
|
|
if not post:
|
|
return None
|
|
return dto_to_dict(post)
|
|
|
|
_handlers["post-by-id"] = _post_by_id
|
|
|
|
# --- posts-by-ids ---
|
|
async def _posts_by_ids():
|
|
ids_raw = request.args.get("ids", "")
|
|
if not ids_raw:
|
|
return []
|
|
ids = [int(x.strip()) for x in ids_raw.split(",") if x.strip()]
|
|
posts = await services.blog.get_posts_by_ids(g.s, ids)
|
|
return [dto_to_dict(p) for p in posts]
|
|
|
|
_handlers["posts-by-ids"] = _posts_by_ids
|
|
|
|
# --- search-posts ---
|
|
async def _search_posts():
|
|
query = request.args.get("query", "")
|
|
page = int(request.args.get("page", 1))
|
|
per_page = int(request.args.get("per_page", 10))
|
|
posts, total = await services.blog.search_posts(g.s, query, page, per_page)
|
|
return {"posts": [dto_to_dict(p) for p in posts], "total": total}
|
|
|
|
_handlers["search-posts"] = _search_posts
|
|
|
|
# --- page-config ---
|
|
async def _page_config():
|
|
"""Return a single PageConfig by container_type + container_id."""
|
|
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."""
|
|
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)."""
|
|
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
|