Files
mroberts 7f5d034a1f feat: auth by default, theme toggle, CoderPad startup, review fixes
- Require a session on every route; public routes opt out with @allow_anonymous
- Split password hashing and pepper loading into passwords.py
- Add a system/light/dark theme toggle backed by light-dark() colors
- Ignore stale 401s from an earlier session, PATCH only changed book fields,
  and block overlapping journal-entry saves
- Add bin/start and CoderPad Vite server settings for the pad's start/restart
- Rewrite README as a mise onboarding guide; expand .gitignore
- Include review-round fixes and tests
2026-10-02 16:49:14 -05:00

86 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, Response, g, jsonify, request
from flask.typing import ResponseReturnValue
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__)
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")
@auth.allow_anonymous
def health() -> ResponseReturnValue:
return {"status": "ok"}
@app.before_request
def require_json_for_mutations() -> None:
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: Response) -> 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) -> ResponseReturnValue:
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) -> ResponseReturnValue:
return jsonify(error=error.description), error.code or 500
@app.errorhandler(psycopg2.errors.CheckViolation)
def handle_check_violation(
error: psycopg2.errors.CheckViolation,
) -> ResponseReturnValue:
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) -> ResponseReturnValue:
log.exception("Unhandled error on %s %s", request.method, request.path)
return jsonify(error="Unexpected server error"), 500
return app
if __name__ == "__main__":
create_app().run(host="127.0.0.1", port=5000)