Admin routes and the SPA fallback answer errors through responses.py

responses.py is the app's one JSON error shape, yet invites, accounts, the SPA
fallback and the test-email route still built jsonify({"error": ...}) by hand,
and two parsed path ids with their own try/uuid.UUID. They now use json_error,
not_found and parse_uuid. The download limiter's 429 stays for F13, with its
Retry-After twin.

DRY pass #2, batch 4, F10 (#5372).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 14:33:11 -04:00
co-authored by Claude Opus 5.5
parent bfe2ed783f
commit ac9035d9a5
4 changed files with 16 additions and 19 deletions
+5 -6
View File
@@ -6,7 +6,6 @@ What a reset link is, and how it is used, is in `password_resets.py`.
from __future__ import annotations
import logging
import uuid
from quart import Blueprint, g, jsonify, request
from sqlalchemy import select
@@ -18,6 +17,7 @@ from .db import session_scope
from .models.user import User
from .password_resets import issue
from .proxy import client_address
from .responses import not_found, parse_uuid
bp = Blueprint("accounts", __name__, url_prefix="/api/accounts")
@@ -46,14 +46,13 @@ async def list_accounts():
@bp.post("/<account_id>/reset-link")
@require_admin
async def create_reset_link(account_id: str):
try:
uid = uuid.UUID(account_id)
except ValueError:
return jsonify({"error": "not found"}), 404
uid = parse_uuid(account_id)
if uid is None:
return not_found()
async with session_scope() as db:
user = await db.get(User, uid)
if user is None:
return jsonify({"error": "not found"}), 404
return not_found()
token, expires_at = await issue(db, uid, g.user_id)
await db.commit()
email = user.email
+3 -2
View File
@@ -23,6 +23,7 @@ from .invites_api import bp as invites_bp
from .labels import bp as labels_bp
from .notes import bp as notes_bp
from .proxy import is_https
from .responses import json_error, not_found
from .retention import run_sweeper
from .settings import MAX_BODY_MB, get_public_config, get_setting, load_or_create_secret_key, refresh_live
from .settings_api import bp as settings_bp
@@ -225,13 +226,13 @@ def create_app() -> Quart:
@app.get("/<path:path>")
async def spa(path: str):
if path.startswith("api/"):
return jsonify({"error": "not found"}), 404
return not_found()
candidate = os.path.join(STATIC_DIR, path)
if path and os.path.isfile(candidate):
return await send_from_directory(STATIC_DIR, path)
index = os.path.join(STATIC_DIR, "index.html")
if os.path.isfile(index):
return await send_from_directory(STATIC_DIR, "index.html")
return jsonify({"error": "frontend not built"}), 404
return json_error("frontend not built", 404)
return app
+7 -8
View File
@@ -5,7 +5,6 @@ What an invite is, and how it is redeemed, is in `invites.py`.
from __future__ import annotations
import logging
import uuid
from datetime import datetime, timedelta, timezone
from quart import Blueprint, g, jsonify, request
@@ -19,6 +18,7 @@ from .invites import MAX_DAYS, lifetime_days, serialize
from .models.invite import Invite
from .models.user import User
from .proxy import client_address
from .responses import json_error, not_found, parse_uuid
from .security import generate_token, hash_token
bp = Blueprint("invites", __name__, url_prefix="/api/invites")
@@ -33,10 +33,10 @@ async def create_invite():
data = await request.get_json(silent=True) or {}
email = (data.get("email") or "").strip().lower() or None
if email is not None and "@" not in email:
return jsonify({"error": "that doesn't look like an email address"}), 400
return json_error("that doesn't look like an email address", 400)
days = lifetime_days(data.get("days"))
if days is None:
return jsonify({"error": f"an invite lasts between 1 and {MAX_DAYS} days"}), 400
return json_error(f"an invite lasts between 1 and {MAX_DAYS} days", 400)
token = generate_token()
now = datetime.now(timezone.utc)
@@ -82,15 +82,14 @@ async def list_invites():
@bp.delete("/<invite_id>")
@require_admin
async def revoke_invite(invite_id: str):
try:
iid = uuid.UUID(invite_id)
except ValueError:
return jsonify({"error": "not found"}), 404
iid = parse_uuid(invite_id)
if iid is None:
return not_found()
now = datetime.now(timezone.utc)
async with session_scope() as db:
invite = await db.get(Invite, iid)
if invite is None:
return jsonify({"error": "not found"}), 404
return not_found()
# A used invite has done its work and its record stays as it is; an already
# revoked one keeps its first revocation time.
if invite.redeemed_at is None and invite.revoked_at is None:
+1 -3
View File
@@ -64,9 +64,7 @@ async def test_email():
async with session_scope() as db:
cfg = await mail_settings(db)
if cfg is None:
return jsonify(
{"error": "Email is off: it needs an SMTP server, a from address and the public address."}
), 400
return json_error("Email is off: it needs an SMTP server, a from address and the public address.", 400)
admin = await db.get(User, g.user_id)
site = await get_setting(db, "site_name")
try: