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
This commit is contained in:
2026-10-02 16:49:14 -05:00
parent 9d7b0e805c
commit 7f5d034a1f
46 changed files with 1797 additions and 497 deletions
+14 -12
View File
@@ -3,7 +3,8 @@
import logging
import psycopg2.errors
from flask import Flask, g, jsonify, request
from flask import Flask, Response, g, jsonify, request
from flask.typing import ResponseReturnValue
from werkzeug.exceptions import HTTPException
import auth
@@ -28,18 +29,19 @@ def create_app() -> Flask:
app.register_blueprint(notes.bp)
@app.get("/api/health")
def health():
@auth.allow_anonymous
def health() -> ResponseReturnValue:
return {"status": "ok"}
@app.before_request
def require_json_for_mutations():
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):
def log_request(response: Response) -> Response:
log.info(
"%s %s -> %s user=%s",
request.method,
@@ -50,18 +52,20 @@ def create_app() -> Flask:
return response
@app.errorhandler(ApiError)
def handle_api_error(error: 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):
return jsonify(error=error.description), error.code
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):
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"
@@ -70,14 +74,12 @@ def create_app() -> Flask:
)
@app.errorhandler(Exception)
def handle_unexpected(_error: 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
app = create_app()
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000)
create_app().run(host="127.0.0.1", port=5000)