Commit Graph

3 Commits

Author SHA1 Message Date
knacky
76f8443ac2 docs: sprint 2 surface in docs/api.md + D-015/D-016/D-017 + changelog
- `docs/api.md` extended with the sprint-2 surface: pagination envelope
  conventions, engagement members (GET/POST/DELETE), users (GET paginated
  with `?type=`, POST, PATCH, DELETE-soft), audit log viewer with its
  five filters. Anti-enumeration semantics (404 on foreign members) made
  explicit. Drive-by fix: `/engagements<eid>` → `/engagements/<eid>`.
- `tasks/spec-decisions.md` logs the three sprint-2 decisions verbatim:
  - **D-015** USER_MANAGE permission (wording from spec-analyst).
  - **D-016** pagination envelope shape (`{items, total, page, page_size}`).
  - **D-017** `engagement_member.role` stays a free-form label.
- `CHANGELOG.md` summarises the sprint with hashes / behaviours / decisions.
2026-05-23 15:53:45 +02:00
knacky
dd5c508b04 fix(backend): JSON error envelope for every HTTPException + strict_slashes=False
Some checks failed
ci / backend (lint + typecheck + unit tests) (push) Failing after 0s
ci / frontend (lint + typecheck + build + unit tests) (push) Failing after 1s
Two issues spotted by ux-frontend consuming docs/api.md against the actual
code path:

1. `flask.abort(...)` returned the Werkzeug HTML error page for 400/403/404/
   422/etc. — only the 401 paths going through `api_error()` and the
   Flask-Login `unauthorized_handler` honoured the `{error, message}`
   envelope the contract promised. The frontend's `ApiClientError.body`
   parser was forced to fall back to a raw string, and the 422 case
   could not surface Pydantic per-field errors.

   Fix: register `@app.errorhandler(HTTPException)` that serialises every
   `HTTPException` to the same JSON envelope. 422s gain a `details: [...]`
   field holding the Pydantic `errors()` list (`loc` / `msg` / `type`),
   matching the shape now documented in `docs/api.md`.

   A `_HTTP_ERROR_CODES` map maps statuses to stable snake_case codes
   (`bad_request`, `not_found`, `method_not_allowed`,
   `validation_error`, `forbidden`, `internal_error`, ...). Unknown
   statuses fall back to `http_error`.

   `description` is `cast(object, ...)` because the Werkzeug stub pins it
   to `str | None` while `flask.abort(..., description=<list>)` is the
   officially supported way to smuggle a Pydantic errors list to the
   handler.

2. `@bp.get("")` on the engagements blueprint produced `/api/v1/engagements`
   (no slash). Hitting it with a trailing slash issued a 308 redirect,
   and some browsers drop the session cookie across that hop.

   Fix: `app.url_map.strict_slashes = False`. Both forms now match the
   same handler without redirect.

5 new integration tests cover the new envelope shape (422 with details,
unknown 404, malformed-JSON 400) and the dual-slash matching. `docs/api.md`
rewritten to reflect the table of stable codes, the `details` shape, and
the no-trailing-slash convention. `CHANGELOG.md` gains a follow-up entry.

Verification: ruff check / mypy --strict / pytest tests/unit all green
(61 unit + 5 new integration).
2026-05-23 04:33:23 +02:00
knacky
dd321c2cd0 docs: add api.md contract for sprint 1 + update changelog
- docs/api.md: contract the frontend consumes — base URL, auth transport
  (Flask session cookie, credentials: include), uniform error envelope,
  MA6 tenant-scope behaviour (404 not 403), per-endpoint shape for
  /auth/{login,logout,me} and /engagements GET/POST/GET-by-id, plus a
  worked example walking through CLI bootstrap → login → POST engagement →
  list → logout.
- CHANGELOG.md: sprint-1 entry summarising the three endpoints, the dev-
  only CORS, the AuthUser extension, the audit rows, and the test
  coverage.
2026-05-23 04:22:03 +02:00