diff --git a/CHANGELOG.md b/CHANGELOG.md index b12993128..73ce60095 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1092,10 +1092,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 the `delete_many` bulk path included — with no repository attribute required. This model form ships for `destroy`, `delete_all`, `nullify`, and `restrict` (grandchild recursion and the `delete_many` bulk path included), driving the - same runtime cascade as the repository attribute; only both-sites conflict - diagnostics are a deferred follow-up — when both a repository-side - `dependent(...)` and a model-side `dependent`/`on_delete` are declared the - repository attribute silently wins — and a `dependent`/`on_delete` on a + same runtime cascade as the repository attribute; when both a repository + `dependent(...)` and a model-side `#[has_many(..., dependent = ...)]` are + declared for the same delete the repository attribute still wins, but a + debug-only `tracing::warn!` now surfaces the silently-inert model-side + declaration (issue #1788), and a `dependent`/`on_delete` on a `through = ` association is a directed compile error rather than a mis-targeted cascade (issue #1738). See `docs/guide/repositories.md`. - **audit:** version/audit writes are auto-attributed to the current actor. A new @@ -1381,6 +1382,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 doctor` gained an `alert_transports` check that (in production) flags a whitespace-mangled routing key, a non-absolute `pagerduty_url`, or a non-absolute-`https` Slack/Discord URL (issue #1630). +- **server:** config-driven in-process TLS termination (part of #1603). A new `[server.tls]` section (`cert_path`, `key_path`, optional `reload_interval_secs` (default 60) and `handshake_timeout_secs` (default 10)) makes the app terminate HTTPS itself on the same host:port instead of needing a sidecar proxy. Off by default and gated behind the off-by-default `tls` cargo feature; startup fails fast on a missing/unreadable file, unparseable or empty PEM, a key that does not match the leaf, or an expired/not-yet-valid leaf or intermediate. Certificates hot-reload by polling the cert/key mtimes, so an ACME/`certbot` renewal is picked up without a restart. `autumn doctor` gains a `tls` check that fails on a missing/invalid/expired certificate and warns when the leaf expires within 30 days. +- **server:** automatic ACME (Let's Encrypt) certificate provisioning and renewal (part of #1608). With the off-by-default `acme` cargo feature, a `[server.tls.acme]` section (`domains`, `contact_email`, optional `directory` [Let's Encrypt staging by default], `cache_dir` [default `config/acme`], `http_challenge_port` [default 80], `renew_before_days` [default 30]) obtains and auto-renews the server's TLS certificate over the HTTP-01 challenge — no static cert on disk and no reverse proxy. Static `cert_path`/`key_path` and ACME are mutually exclusive (exactly one must be configured); the issued cert hot-swaps into the same reloadable resolver the `[server.tls]` listener serves, a `:80` listener answers the challenge and redirects HTTP to HTTPS, and a coordinator-leader-elected loop renews hourly before expiry. Single-host slice for now; wildcards/DNS-01 are out of scope (#1620). +- **security:** one-time submit tokens give forms a server-side, at-most-once guarantee with no client JavaScript (issue #1360). A per-render random token is exposed via the `SubmitToken` extractor (plus `SubmitFormField`) and embedded as a hidden `_submit_token` field; on a mutating POST `SubmitTokenLayer` consumes it against the shared idempotency store — first use runs and caches the handler's 2xx/3xx response, a replayed token short-circuits and returns that first response, and a concurrent duplicate that loses the in-flight lock race gets a `409`. If the response stream errors after the handler already committed a 2xx/3xx, the in-flight lock is held (surfacing a `500`, not recording the token) so a retry still gets a `409`. Enabled by default under `[security.submit_token]` and applied inner to the CSRF layer; the consumed-token store inherits `[idempotency].backend`, where an inherited in-memory backend in production only warns while an explicit `backend = "memory"` in production fails fast. +- **security:** build-time route auth-coverage manifest (part of #1604). Every route now carries an exposure classification — `gated`, `public`, `framework`, or `unclassified` — and a new `autumn routes audit` subcommand lists them and emits a stable-ordered (by path then method) JSON security manifest. It is a CI gate: it exits non-zero when any route is `unclassified` (or omitted from the dump), naming each offender. A new `#[public]` marker attribute mirrors `#[secured]` to declare a handler deliberately unauthenticated. +- **web:** new `autumn_web::a11y` module of typed accessible UI primitives that make the accessible name a compile-time obligation (part of #1706). `Img::new(src, alt)` requires alt text (decorative images opt in via `Img::decorative`); `Button::new(name)`/`Button::icon`, `Link::new(href, text)`/`Link::icon`, and `MenuItem::new(name)` all require an accessible name with no nameless constructor (icon-only forms route the name to `aria-label`); `Link::new_tab()` adds `target="_blank"` with `rel="noopener noreferrer"`. `TextField::new` returns a `TextField` typestate that does not implement `Render` until `.label()`/`.aria_label()`/`.labelled_by()` transitions it to `TextField`, so an unlabeled field is unrepresentable. Inaccessible forms fail to compile (proven by trybuild fixtures); all primitives are re-exported from the prelude under the `maud` feature. +- **cli:** new `autumn a11y verify [PATH] --format --strict` command that statically scans project `.rs` files for raw `maud::html!` markup bypassing the typed `a11y` primitives, emitting WCAG-keyed findings (image-alt 1.1.1; label 1.3.1/3.3.2/4.1.2; button-name 4.1.2; link-name 2.4.4/4.1.2) and exiting non-zero to fail CI (part of #1706). The scan is advisory and best-effort — a token-level heuristic over `html!` bodies, not a parser: it skips anything it cannot statically resolve, so the typed primitives remain the compile-time proof of accessibility and this pass only covers the raw-`html!` escape hatch. +- **web:** allowlisted sort and filter for list views (closes #1126). A new `Infallible` `ListQuery` extractor (with `SortDir`) parses `?sort=`, `?dir=asc|desc`, and `?filter[]=` — it never rejects a request, falling back to the model's default order for an empty/unknown `sort` and to ascending for an invalid `dir`. `#[model]` emits typed per-column allowlist helpers and `#[repository]` emits a tenant- and soft-delete/shard-aware `list()` that routes requested keys through Diesel's typed DSL via `.into_boxed()`, so only real columns reach SQL and an attacker-supplied `?sort=id;DROP TABLE users` falls through to the default ordering. Scaffolded non-live, non-owner-scoped index views wire their `data_table` to this automatically. +- **generate:** `autumn generate scaffold` now emits a default-deny record-level `Policy`/`Scope` for every non-`--api` resource (opt out with `--no-policy`); when an owner column is detected (`user_id` → `author_id` → `owner_id` → first `*_id` referencing `users`) it also authorizes the create/edit/update/delete handlers and scopes the index to the current user's rows. A new standalone `autumn generate policy ` command scaffolds `src/policies/.rs` and wires `.policy(…)`/`.scope(…)` into `main.rs` for an existing model (issue #1125). +- **generate:** `autumn generate scaffold --searchable title,body` makes the named text fields full-text searchable — it adds `#[searchable]` attributes to the model, `searchable` to the `#[repository]`, a migration adding a `search_vector tsvector GENERATED ALWAYS AS (…) STORED` column plus a GIN index, and a search box on the index wired to `GET //search` (an empty `?q` falls back to the plain paginated list). Omitting `--searchable` leaves scaffold output byte-identical; the `--live`/`--live-validation` index variants gate the search box off; naming a non-text/unknown field, a uuid-PK model, or an owner-scoped model fails generation before any files are written (issue #1319). +- **generate:** `autumn generate scaffold post avatar:Attachment` now produces working no-JS file uploads end-to-end — the create/update handlers take an `autumn_web::extract::Multipart` extractor, stream each uploaded file to the `BlobStore` via `field.save_to_blob_store(&*store, key)`, and bind the returned `Blob` from a plain `multipart/form-data` submit. `autumn destroy` also no longer strips the `multipart`/`storage` features when a hand-written route still uses them (part of #1236). +- **cli:** `autumn new --api` scaffolds a JSON-first project — handlers return `Json<…>`, `autumn-web` is pinned `default-features = false` to a lean API feature set (`db`, `cache-moka`, `http-client`, `reporting`, `flash`) that drops the maud/htmx/tailwind view stack, and no `static/` tree, `input.css`, `tailwind.config.js`, vendored assets, or Tailwind CI/README notes are generated (the first `cargo run` serves JSON). `--api` cannot be combined with `--daemon` or `--bundled-pg` but composes with `--with-i18n` and `--with-seed`. ### Fixed @@ -1562,6 +1574,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 child of a `
` — a bare ``/``/``/`