diff --git a/src/inkwell/accounts_api.py b/src/inkwell/accounts_api.py index 104c543..1d93a6a 100644 --- a/src/inkwell/accounts_api.py +++ b/src/inkwell/accounts_api.py @@ -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("//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 diff --git a/src/inkwell/app.py b/src/inkwell/app.py index 9c1c412..667b5b2 100644 --- a/src/inkwell/app.py +++ b/src/inkwell/app.py @@ -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("/") 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 diff --git a/src/inkwell/invites_api.py b/src/inkwell/invites_api.py index 0420fea..59c1111 100644 --- a/src/inkwell/invites_api.py +++ b/src/inkwell/invites_api.py @@ -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("/") @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: diff --git a/src/inkwell/settings_api.py b/src/inkwell/settings_api.py index cf5be5b..2af9434 100644 --- a/src/inkwell/settings_api.py +++ b/src/inkwell/settings_api.py @@ -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: