diff --git a/backend/app/api/_responses.py b/backend/app/api/_responses.py new file mode 100644 index 0000000..e935321 --- /dev/null +++ b/backend/app/api/_responses.py @@ -0,0 +1,16 @@ +"""Shared API response helpers.""" + +from quart import jsonify + + +def error_response( + error: str, *, status: int = 400, detail: str | None = None, **extra, +): + """JSON error body + HTTP status. `detail` is included only when given; + `extra` keys are merged into the body. Returns the (response, status) + tuple Quart expects. Imported as `_bad` by the blueprints.""" + body = {"error": error} + if detail is not None: + body["detail"] = detail + body.update(extra) + return jsonify(body), status diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py index 29820e8..b82aacf 100644 --- a/backend/app/api/admin.py +++ b/backend/app/api/admin.py @@ -24,16 +24,11 @@ from sqlalchemy import select from ..extensions import get_session from ..models import Artist from ..services.cleanup_service import project_artist_cascade, project_bulk_image_delete +from ._responses import error_response as _bad admin_bp = Blueprint("admin", __name__, url_prefix="/api/admin") -def _bad(error: str, *, status: int = 400, **extra): - body = {"error": error} - body.update(extra) - return jsonify(body), status - - def _bulk_image_confirm_token(image_ids: list[int]) -> str: """Stable 8-hex token derived from the sorted id list. Mutates when the selection changes; stays the same across modal opens of diff --git a/backend/app/api/cleanup.py b/backend/app/api/cleanup.py index 7149525..24853fe 100644 --- a/backend/app/api/cleanup.py +++ b/backend/app/api/cleanup.py @@ -31,18 +31,13 @@ from sqlalchemy import select from ..extensions import get_session from ..models import LibraryAuditRun from ..services import cleanup_service +from ._responses import error_response as _bad cleanup_bp = Blueprint("cleanup", __name__, url_prefix="/api/cleanup") IMAGES_ROOT = Path("/images") -def _bad(error: str, *, status: int = 400, **extra): - body = {"error": error} - body.update(extra) - return jsonify(body), status - - def _min_dim_token(min_w: int, min_h: int) -> str: # SHA-256 (not MD5) — Web Crypto's subtle.digest rejects MD5; both # sides use SHA-256 truncated to 8 hex chars. diff --git a/backend/app/api/credentials.py b/backend/app/api/credentials.py index 395846a..f719b51 100644 --- a/backend/app/api/credentials.py +++ b/backend/app/api/credentials.py @@ -20,6 +20,7 @@ from ..services.credential_service import ( UnknownPlatformError, WrongAuthTypeError, ) +from ._responses import error_response as _bad credentials_bp = Blueprint("credentials", __name__, url_prefix="/api/credentials") @@ -38,14 +39,6 @@ def _get_crypto() -> CredentialCrypto: return _crypto -def _bad(error: str, *, status: int = 400, detail: str | None = None, **extra): - body = {"error": error} - if detail is not None: - body["detail"] = detail - body.update(extra) - return jsonify(body), status - - async def _ext_key_ok(session) -> bool: """If X-Extension-Key is supplied, it must match the stored value. Missing header → True (browser path; accepted per homelab posture). diff --git a/backend/app/api/extension.py b/backend/app/api/extension.py index 4f4766f..5266d46 100644 --- a/backend/app/api/extension.py +++ b/backend/app/api/extension.py @@ -20,6 +20,7 @@ from ..services.extension_service import ( UnknownPlatformError, ) from ..services.source_service import KNOWN_PLATFORMS +from ._responses import error_response as _bad extension_bp = Blueprint("extension", __name__, url_prefix="/api/extension") @@ -30,12 +31,6 @@ XPI_DIR = Path("/app/frontend/dist/extension") _XPI_VERSION_RE = re.compile(r"fabledcurator-(?P[\w.-]+)\.xpi$") -def _bad(error: str, *, status: int = 400, **extra): - body = {"error": error} - body.update(extra) - return jsonify(body), status - - async def _ext_key_required(session) -> bool: """Unlike /api/credentials (which accepts the browser path with no header), quick-add-source writes server state and must be explicitly diff --git a/backend/app/api/migrate.py b/backend/app/api/migrate.py index 8a67fc6..d48e9f4 100644 --- a/backend/app/api/migrate.py +++ b/backend/app/api/migrate.py @@ -13,6 +13,7 @@ from sqlalchemy import select from ..extensions import get_session from ..models import MigrationRun from ..tasks.migration import run_migration +from ._responses import error_response as _bad migrate_bp = Blueprint("migrate", __name__, url_prefix="/api/migrate") @@ -24,12 +25,6 @@ _VALID_KINDS = frozenset({ _INGEST_KINDS = frozenset({"gs_ingest", "ir_ingest"}) -def _bad(error: str, *, status: int = 400, **extra): - body = {"error": error} - body.update(extra) - return jsonify(body), status - - def _run_to_dict(run: MigrationRun) -> dict: return { "id": run.id, diff --git a/backend/app/api/posts.py b/backend/app/api/posts.py index 1f9dd3e..d96e523 100644 --- a/backend/app/api/posts.py +++ b/backend/app/api/posts.py @@ -5,18 +5,11 @@ from quart import Blueprint, jsonify, request from ..extensions import get_session from ..services.post_feed_service import PostFeedService from ..services.source_service import KNOWN_PLATFORMS +from ._responses import error_response as _bad posts_bp = Blueprint("posts", __name__, url_prefix="/api/posts") -def _bad(error: str, *, status: int = 400, detail: str | None = None, **extra): - body = {"error": error} - if detail is not None: - body["detail"] = detail - body.update(extra) - return jsonify(body), status - - @posts_bp.route("", methods=["GET"]) async def list_posts(): args = request.args diff --git a/backend/app/api/sources.py b/backend/app/api/sources.py index 71d5d9d..9d7c9b1 100644 --- a/backend/app/api/sources.py +++ b/backend/app/api/sources.py @@ -15,18 +15,11 @@ from ..services.source_service import ( SourceService, UnknownPlatformError, ) +from ._responses import error_response as _bad sources_bp = Blueprint("sources", __name__, url_prefix="/api/sources") -def _bad(error: str, *, status: int = 400, detail: str | None = None, **extra): - body = {"error": error} - if detail is not None: - body["detail"] = detail - body.update(extra) - return jsonify(body), status - - @sources_bp.route("", methods=["GET"]) async def list_sources(): artist_id_raw = request.args.get("artist_id") diff --git a/backend/app/api/system_backup.py b/backend/app/api/system_backup.py index 24fa94e..fcf57c7 100644 --- a/backend/app/api/system_backup.py +++ b/backend/app/api/system_backup.py @@ -14,6 +14,7 @@ from sqlalchemy import desc, select from ..extensions import get_session from ..models import BackupRun, ImportSettings +from ._responses import error_response as _bad system_backup_bp = Blueprint( "system_backup", __name__, url_prefix="/api/system/backup", @@ -29,12 +30,6 @@ _BACKUP_SETTINGS_FIELDS = ( ) -def _bad(error: str, *, status: int = 400, **extra): - body = {"error": error} - body.update(extra) - return jsonify(body), status - - def _row_to_dict(r: BackupRun) -> dict: return { "id": r.id,