Files
Clarium/backend/app.py
T
mroberts 2512c67893 fix: cap request bodies, link login error, add book-form retry, guard double add
Limit request bodies to 1 MiB (413 JSON), wire aria-describedby on the
login form, add Retry and cancellation to the book form load, disable
Add entry while a note POST is in flight, and poll pg_isready in README.

Claude-Session: https://claude.ai/code/session_01M9MLit5Ko3X4s7rzC5Kv7X
2026-10-02 14:36:14 -05:00

72 lines
2.5 KiB
Python

"""Application entry point: wires slices, cross-cutting request rules, and JSON error handling."""
import logging
import psycopg2.errors
from flask import Flask, g, jsonify, request
from werkzeug.exceptions import HTTPException
import auth
import books
import db
import notes
from validation import ApiError
log = logging.getLogger(__name__)
MUTATING_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
def create_app() -> Flask:
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
app = Flask(__name__)
# Werkzeug buffers and parses the whole body before our validation runs, so unauthenticated
# callers could exhaust memory; 1 MiB is far above the largest legitimate payload (10,000-char note).
app.config["MAX_CONTENT_LENGTH"] = 1024 * 1024
db.init_app(app)
auth.init_app(app)
app.register_blueprint(books.bp)
app.register_blueprint(notes.bp)
@app.get("/api/health")
def health():
return {"status": "ok"}
@app.before_request
def require_json_for_mutations():
# CSRF defense (ADR-0002): cross-site requests can only send this content type after a
# CORS preflight, and this API never grants CORS.
if request.method in MUTATING_METHODS and not request.is_json:
raise ApiError(400, "Request body must be JSON with Content-Type: application/json")
@app.after_request
def log_request(response):
log.info("%s %s -> %s user=%s", request.method, request.path, response.status_code, g.get("user_id"))
return response
@app.errorhandler(ApiError)
def handle_api_error(error: ApiError):
body = {"error": error.message}
if error.field:
body["field"] = error.field
return jsonify(body), error.status
@app.errorhandler(HTTPException)
def handle_http_error(error: HTTPException):
return jsonify(error=error.description), error.code
@app.errorhandler(psycopg2.errors.CheckViolation)
def handle_check_violation(error: psycopg2.errors.CheckViolation):
return jsonify(error=f"Value breaks data rule '{error.diag.constraint_name}'; correct it and retry"), 400
@app.errorhandler(Exception)
def handle_unexpected(_error: Exception):
log.exception("Unhandled error on %s %s", request.method, request.path)
return jsonify(error="Unexpected server error"), 500
return app
app = create_app()
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000)