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
+39 -10
View File
@@ -1,15 +1,24 @@
"""PostgreSQL access: a connection pool, one connection per request, and two query helpers."""
"""PostgreSQL access: a connection pool, one connection per request, and query helpers."""
import logging
import os
from collections.abc import Sequence
from pathlib import Path
from typing import Any
import psycopg2.extensions
import psycopg2.extras
import psycopg2.pool
from flask import Flask, current_app, g
from flask import Flask, Response, current_app, g
log = logging.getLogger(__name__)
DEFAULT_DATABASE_URL = "postgresql://postgres:[email protected]:5432/postgres"
SCHEMA = Path(__file__).with_name("schema.sql")
Row = dict[str, Any]
Params = tuple[object, ...]
def init_app(app: Flask) -> None:
pool = psycopg2.pool.ThreadedConnectionPool(
@@ -26,24 +35,34 @@ def init_app(app: Flask) -> None:
app.teardown_appcontext(_release_connection)
def query(sql: str, params: tuple = ()) -> list[dict]:
def query(sql: str, params: Params = ()) -> Sequence[Row]:
with _connection().cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
cur.execute(sql, params)
return cur.fetchall() if cur.description else []
def query_one(sql: str, params: tuple = ()) -> dict | None:
def query_one(sql: str, params: Params = ()) -> Row | None:
rows = query(sql, params)
return rows[0] if rows else None
def _connection():
def query_row(sql: str, params: Params = ()) -> Row:
rows = query(sql, params)
if len(rows) != 1:
raise RuntimeError(
f"Expected exactly one row but got {len(rows)}; the statement must return one row: {sql.strip()}"
)
return rows[0]
def _connection() -> psycopg2.extensions.connection:
if "db" not in g:
g.db = current_app.extensions["db_pool"].getconn()
return g.db
conn: psycopg2.extensions.connection = g.db
return conn
def _finish_transaction(response):
def _finish_transaction(response: Response) -> Response:
conn = g.get("db")
if conn is not None:
if response.status_code < 400:
@@ -53,8 +72,18 @@ def _finish_transaction(response):
return response
def _release_connection(_exc):
def _release_connection(_exc: BaseException | None) -> None:
conn = g.pop("db", None)
if conn is not None:
if conn is None:
return
pool = current_app.extensions["db_pool"]
try:
conn.rollback()
current_app.extensions["db_pool"].putconn(conn)
except psycopg2.Error:
log.warning(
"Discarding a database connection that failed to roll back; the pool will open a new one",
exc_info=True,
)
pool.putconn(conn, close=True)
else:
pool.putconn(conn)