Files
volumen/CHANGELOG.md
T
petrbalvin 98f7f3c9e3
Test / test (push) Successful in 57s
Release / release (push) Successful in 1m11s
fix: add missing CSP style-src nonce and include static SVGs in wheel
2026-07-27 09:03:44 +02:00

395 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Changelog
All notable changes to **volumen** are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
> Versions ≤ 0.3.0 were implemented in Ruby. The current codebase is Python
> (FastAPI). Earlier release notes are kept verbatim as historical record.
## [development]
## [0.4.1] — 2026-07-27
### Fixed
- **CSP nonce chybí pro `style-src` v admin routách** — přidán `style-src 'self' 'nonce-{nonce}'` do CSP hlavičky pro `/admin` routy. Bez nonce prohlížeč blokoval všechny inline `<style>` tagy v admin šablonách.
- **`web/static/` SVG soubory chybí v distribučním kole** — přidána sekce `[tool.setuptools.package-data]` do `pyproject.toml`, aby `volumen-icon.svg` a `volumen-logo.svg` byly součástí instalovaného wheelu. Admin panel dříve vracel 500 na `/admin/icon.svg` a `/admin/logo.svg`.
## [0.4.0] — 2026-07-26
### Added
- **`volumen init`** — a new `init` subcommand is the *operational*
bootstrap (config, data dirs, admin password, optional systemd unit). Run
as root for a system install (default `--systemd`); pass `--local` for a
per-user install (`~/.config/volumen/`, `~/.local/share/volumen/`). The
command renders the production-hardened config from the same commented
TOML template (`host = "::1"`, `env = "production"`, `trust_proxy = true`,
`cookie_secure = true`), prompts for an admin password (`getpass`), hashes
it via the existing scrypt helper, generates a 64-byte hex session key,
creates the system user (`useradd --system`) and the `/etc/volumen/` /
`/var/lib/volumen/` layout, and (with `--systemd`) installs a hardened
`/etc/systemd/system/volumen.service` and runs
`systemctl daemon-reload && systemctl enable --now`.
- **PyPI publish metadata** — `pyproject.toml` gains `readme`, `keywords`,
a complete `classifiers` array (Development Status, Framework :: FastAPI,
supported Python versions, OS, Topic, Typing), and a `[project.urls]`
block (Homepage, Repository, Issues, Changelog, Documentation). PyPI now
renders the README on the project page and links back to sourcedock.dev.
- **`volumen status`** — read-only inspection of an installation. Prints
human output by default or `--json` for tooling; reports `config_exists`,
`data_dir_exists`, `users_file_exists`, `password_hash_set`,
`session_key_ok`, `systemd_unit_installed`, `service_active`,
`admin_user_present`, and any issues found. Exit code is `1` when any
issue is reported.
- **`volumen check-update`** — compares the installed version (read via
`importlib.metadata`) with the latest release on PyPI. Uses only
`urllib.request` (stdlib, 5 s timeout); exit code is `0` up-to-date,
`1` update available (suggests `uv tool upgrade volumen`), `2` PyPI
unreachable / offline / malformed response. `--json` returns the same
fields plus `checked_at` for cron-friendly alerting.
- **`src/volumen/installer.py`** — a new module exposing `system_install`,
`user_install`, `inspect`, plus the private `_mutate_config_text` helper
used to apply line-anchored substitutions to the rendered TOML template
(preserves comment blocks byte-for-byte).
- **Tests** — `tests/test_installer.py` adds 15 tests covering both install
flows, the config-template mutator, the systemd-unit rendering,
idempotency / backup behaviour, the `inspect` round-trip, and the
`--systemd` + `--local` CLI guard.
### Changed
- **First-run setup is now `volumen init`** — replaces the legacy
`install.sh`. The CLI subcommand renders config, creates data dirs,
prompts for an admin password, generates a session key, and (with
`--systemd`) installs and enables a hardened systemd unit. No bash
required.
- **Publishing now targets PyPI** — `release.yml` runs
`uv publish --token "$PYPI_TOKEN"` after `python -m build` for `v*`
tags. Wheel + sdist continue to be uploaded to sourcedock.dev Releases as a
secondary mirror. End users get `uv tool install volumen` and
`pipx install volumen` as the one-line install path; no checkout
required.
- **Distribution path is `uv tool install`** (or `pipx install`) — the
CLI binary is installed globally; the operational bootstrap is delegated
to `volumen init`. The legacy `uv sync` + project-local venv at
`/opt/volumen/.venv` is gone.
- **`Config.load` round-trips config-file `content_dir` / `users_file`** —
the rendered template places these two keys after the `[server]` header,
so TOML table folding puts them inside `[server]`. `Config.load` now
hoists them to the root so files produced by `volumen init` (and any
hand-edited config) round-trip through the canonical accessors without
relying on CLI overrides.
- **Migrated from Ruby to Python**: the runtime has been rewritten on top of
FastAPI + Uvicorn, replacing Sinatra with FastAPI, kramdown with
python-markdown + pymdown-extensions, Puma with Uvicorn, Bundler with uv,
minitest with pytest, RuboCop with Ruff, and the TOML parser
(`toml-rb`) with the standard library `tomllib` (plus `tomli-w` for
round-tripping edits). The public API (`/api/volumen/*`) and the
server-rendered admin (`/admin/*`) keep the same shape and HTTP semantics.
- **Installer** — `install.sh` now bootstraps `uv` instead of Bundler, runs
`uv sync --frozen` to provision `/opt/volumen/.venv`, and points the
systemd `ExecStart` at the venv-resident `volumen` console script. The
Python runtime is auto-installed on Fedora/openEuler; other distros get a
heredoc with the equivalent `apt` / `pacman` / `zypper` / `apk` lines.
- **CI** — Forgejo Actions moved to `setup-uv` + Ruff + pytest on a single
Python 3.12 job (was: Ruby 3.3/3.4 matrix with Bundler, RuboCop, and
`bundle exec rake test`). The release workflow builds wheel + sdist via
`python -m build` and uploads them with the Forgejo Releases API.
- **Migration guard** — new `.forgejo/workflows/migration-guard.yml` greps
the tracked tree for Ruby-only terms (`bundle exec`, `RuboCop`, `Sinatra`,
`Puma`, `kramdown`, `gem build`, `Volumen::`, `Rack::Session::Cookie`,
`OpenSSL::KDF`, `lib/volumen/`) and fails the build if any of them leak
back into production paths.
- **Configuration** — `config.toml` gains six new keys surfaced in
`config.toml.example`: `[server].env` (`"production"` enables strict
startup), `[server].trust_proxy` (honour `X-Forwarded-Proto` from
nginx), `[admin].min_password_length` (default `10`),
`[admin].max_password_length` (default `1024`, caps scrypt work),
`[admin].max_upload_bytes` (default `10485760`, 10 MB), and
`[admin].cookie_secure` (default `false` in dev, set `true` behind
HTTPS). See `docs/configuration.md` for the full schema.
- **Default bind address** — the installer and `config.toml.example`
default to `host = "::"` (IPv6 with automatic IPv4 fallback) so the
service is reachable on both stacks out of the box. Bind to `"::1"`
when running behind a reverse proxy.
- **Test client now uses `httpx2`** — dev dependencies replace
`httpx>=0.28` with `httpx2>=2.7` (the actively maintained fork by
Pydantic). Starlette 1.3.x prefers `httpx2` in `TestClient` and emits
`StarletteDeprecationWarning` when only legacy `httpx` is installed;
shipping `httpx2` keeps the test suite warning-free. The legacy
`httpx` package is still pulled in via `fastapi[standard]` for the
`fastapi` CLI and remains API-compatible. The unused
`ignore::DeprecationWarning:starlette.testclient` filter is removed
from `[tool.pytest.ini_options]` (it targeted the wrong warning
class and never matched).
### Removed
- `install.sh` — the bash installer is deleted. Use `uv tool install
volumen` (or `pipx install volumen`) followed by `volumen init`.
- `pkg/` build output and `volumen.gem` (Ruby gem packaging is gone; the
project now ships as a wheel + sdist built with `python -m build`).
- `Gemfile`, `volumen.gemspec`, `Rakefile`, `.rubocop.yml` and the
Sinatra-era `lib/` tree.
- `.editorconfig` — Ruff (`pyproject.toml`'s `[tool.ruff]`) is the single
source of truth for Python formatting; modern editors default to UTF-8,
LF, and a trailing newline. Markdown files in this repo use blank lines
between paragraphs (no ` \n` line-break idiom), so trimming trailing
whitespace is safe.
### Security
- **scrypt password hashing** now uses `hashlib.scrypt` and constant-time
verification via `secrets.compare_digest` (was: Ruby's `OpenSSL::KDF`).
- **Session cookies** are signed by Starlette's `SessionMiddleware`
(`itsdangerous`) with `HttpOnly` and `SameSite=Strict`; `Secure` is added
by `SecureCookieMiddleware` when the request was forwarded over HTTPS
(only when `[server].trust_proxy = true`).
- **CSRF tokens** are generated with `secrets.token_hex(32)` and verified
with `secrets.compare_digest` on every state-changing admin POST.
- **Markdown sanitisation policy** — the renderer still passes inline HTML
through (authors are the trusted admin); document the trust model
explicitly in `docs/security.md` so deployments that ingest untrusted
content know to add a sanitiser.
> Note: refine the Security bullets above once the Python agent confirms the
> final cryptographic controls (e.g. whether `image/*` upload signatures get
> re-validated server-side and whether the CSRF token is rotated on login).
## [0.3.0] — 2026-07-12
### Added
**Content & authoring**
- **Fediverse attribution:** a `fediverse_creator` frontmatter field (e.g.
`@user@mastodon.social`) plus a site-wide `[site].fediverse_creator` default
in `config.toml`. The value is exposed as `fediverse_creator` on the site
and post payloads, rendered as `<dc:creator>` in the RSS feed, as
`authors[].name` in the JSON feed, and can be consumed by a front-end to
emit `<meta name="fediverse:creator">` on rendered pages. Per-post values
override the site default.
- **Cover caption:** a `cover_caption` post field rendered next to the cover
image so authors can credit photos or add a short blurb.
- **Titled images as `<figure>`:** Markdown images with a title
(`![alt](url "caption")`) are wrapped in a `<figure>` with a `<figcaption>`
and a small `i` info icon. Images without a title render exactly as before.
**Public API**
- `has_next` and `has_prev` boolean flags in the paginated
`/api/volumen/posts` response so clients can navigate pages without
computing boundaries themselves.
- Distinct `"draft"` error code in `/api/volumen/posts/:slug` when the post
exists but is a draft, replacing the generic `"not_found"`.
**Admin**
- **Modern admin UI:** redesigned layout with a sticky top bar, breadcrumbs
on every page, and a unified design language across login, post list,
post form, and settings.
- **Volumen icon:** a dedicated SVG icon shown in the sidebar and on the
login page (served at `/admin/icon.svg`).
- **Version in sidebar footer** so admins always see which release is
running.
- **Per-user display name** on the user record and admin header.
- **Per-user fediverse handle** editable from the settings page (independent
of the post-level `fediverse_creator`).
- **Profile photo upload:** each admin user can upload a WebP/AVIF avatar
that is stored alongside posts in the content directory and shown in the
settings page.
- **Tabbed settings page** separating account, profile, and admin sections.
- **Restricted uploads:** only WebP (`image/webp`) and AVIF (`image/avif`)
are accepted, with a 415 error message that points users to `cwebp` /
`avifenc` for conversion. Reflects the engine's WebP/AVIF-only posture
end-to-end.
- **Polished native inputs** for `<input type="date|time|file">` so they
match the rest of the admin's design language.
**Tooling**
- `just lint` recipe for standalone RuboCop checks and `just fmt` for
auto-fixing lint issues.
- `config.toml.example` template for local development; `config.toml` is
now gitignored.
### Changed
- Memoize `published_posts` within a single request so repeated calls
(posts list, tags, feeds, sitemap) reuse the cached result.
- `Store#all` cache now invalidates on individual file mtime changes, not
just on the content directory mtime. Manual edits and `git pull` are
detected without a restart.
- `Cache-Control: public, max-age=60` header on all public API responses
for browser and CDN caching.
- Extract feed and sitemap generation into a dedicated `FeedHelpers` module,
reducing `Server` from ~500 to ~420 lines.
### Fixed
- Admin post save/delete now correctly clears the in-memory posts cache, so
the post list reflects edits without a manual reload.
- The post form's date field defaults to today when the underlying post has
no date, preventing the picker from showing an invalid empty value.
### Security
- **Content-Security-Policy** header on all `/admin/*` responses:
`default-src 'self'`, `script-src 'self' 'unsafe-inline'`,
`style-src 'self' 'unsafe-inline'`, `img-src 'self' data:`,
`font-src 'self'`, `connect-src 'self'`, `form-action 'self'`,
`frame-ancestors 'none'`.
- File uploads are rejected (HTTP 413) when exceeding **10 MB**
(`MAX_UPLOAD_BYTES`).
- File uploads are restricted to `image/webp` and `image/avif`
(`ALLOWED_UPLOAD_TYPES`). Anything else is rejected with a 415 and a
conversion hint.
## [0.2.0] — 2026-06-25
### Added
- `<pubDate>` in RSS feed items, derived from the post date.
- `<lastmod>` in sitemap entries for SEO.
- In-memory rate limiting on the login endpoint: 10 attempts per 60 seconds per IP.
- `<language>` element in the RSS feed channel.
- Periodic cleanup of stale `login_attempts` entries to prevent unbounded memory growth.
- Slug format validation (`[a-z0-9._-]`) preventing path traversal and XSS in templates.
- Username validation in `change_username` matching the create-user regex.
- Escape-on-output for slug and username in admin template attributes.
- Memoization of `Store#all` with mtime-based invalidation, eliminating N×read+parse
per request.
- Atomic file writes (tmp + rename) for posts and `users.toml`.
- Post language directory routing: non-default-language posts now land in
`content_dir/<lang>/` instead of overwriting root-level files.
- CLI test coverage (version, help, unknown command, option parsing, config bootstrap).
- Test coverage for RSS feed and sitemap endpoints.
- CORS preflight (`OPTIONS /api/volumen/*`) so browsers can reach the JSON API.
- `Users#all` memoization with mtime-based invalidation, matching Store#all.
- Session invalidation: deleted users can no longer use existing session cookies.
- CSRF protection on `POST /admin/preview`.
- Symlink-safe containment in `Store#media_file` via `File.realpath`.
### Added
- **Scheduled publishing:** a `publish_at` frontmatter field hides posts from the
public API and feeds until their publication date arrives. The admin form
includes a Publish at date picker.
- **JSON Feed:** `GET /api/volumen/feed.json` serves a jsonfeed.org v1.1 feed
with `content_html` and `date_published` per item.
- **Fulltext search:** `GET /api/volumen/posts?q=…` filters posts by
case-insensitive match in title and body.
### Changed
- Split `lib/volumen/server.rb` into `api_routes.rb` and `admin_routes.rb` modules,
keeping the server class focused on configuration and shared helpers.
### Fixed
- `admin.session_ttl` was defined but never read; it now controls
`Rack::Session::Cookie` expiration via `expire_after`.
- Derived post excerpts now strip HTML tags before stripping markdown markup,
avoiding raw HTML leaks in auto-generated excerpts.
- Draft posts no longer leak through the public detail endpoint (`/api/volumen/posts/:slug`).
- `Config::DEFAULTS` inner hashes are now frozen and `deep_merge` builds fresh
copies, preventing mutation from silently poisoning later `Config.load` calls.
- A warning is emitted when `admin.session_key` is configured but shorter than
64 bytes, instead of silently falling back to a random secret.
## [0.1.0] — 2026-06-20
First public release: a small, file-based Markdown blog engine in CRuby. Posts
are plain files, the engine exposes them as a JSON API and ships a self-hosted
admin — no database, no external services.
### Added
**Content & storage**
- File-based store: each post is a `.md` file with a TOML frontmatter block
delimited by `+++`. No database.
- Post fields: `title`, `slug`, `date`, `lang`, `author`, `tags`, `draft`,
`excerpt`, `cover`, `all_langs`, and `translations` (a `lang → slug` map).
- Drafts (`draft = true`) are hidden from the public API.
- Excerpt falls back to the first paragraph (~200 chars) when none is set.
- Estimated reading time (~200 words/min) computed per post.
- Multilingual posts: per-language slugs via `translations`, plus `all_langs`
to surface a single post under every language.
**Markdown rendering**
- GFM rendering via kramdown: tables, fenced code blocks, task lists, and
strikethrough.
- Automatic heading IDs for in-page anchors.
- Raw HTML in post bodies is passed through (authors are the trusted admin).
**Public JSON API (`/api/volumen`)**
- Read-only endpoints: `site`, `posts`, `posts/{slug}`, `tags`, `feed.xml`
(RSS), and `sitemap.xml`.
- `posts` supports pagination (`page`, `limit`, returning `total` and
`page_size`) and filtering by `lang` and `tag`.
- Permissive CORS so a separate front end can consume the API.
**Admin (`/admin`)**
- Server-rendered admin with a sidebar layout and full post CRUD.
- Dual-mode editor: Markdown source (primary) and a visual WYSIWYG, switchable
per post, with a toggleable live preview persisted across sessions.
- Image upload from the editor toolbar into `content_dir/media`; any image
format is accepted (WebP and AVIF included) and served with the correct
`Content-Type`.
- Cover image field with upload, URL, clear, and preview.
**Multi-user & roles**
- Multiple admin users stored in `users.toml`, managed from a settings page.
- Two roles: `admin` (full access incl. user management) and `author` (posts
only).
- Change password and username; last-admin and self-deletion safeguards.
**Configuration & CLI**
- TOML configuration, auto-generated with a commented template on first run.
- Configurable `host`/`port` (defaults to `9090`), `content_dir`,
`users_file`, and `site` metadata.
- CLI: `serve`, `hash-password`, `version`, `help`, with `--config`,
`--content`, `--host`, and `--port` overrides.
**Deployment**
- `install.sh` installs the Ruby toolchain from distro packages on Fedora and
openEuler (and prints guidance on other distros), creates the service user
and directories, and installs a systemd unit.
- Deployment docs for systemd and FreeBSD (`rc.d`), plus an nginx reverse-proxy
setup that runs the admin same-origin, with brute-force rate-limiting and an
optional IP allowlist on the login endpoint.
**Project & tooling**
- Documentation set: architecture, configuration, API reference, deployment,
and security.
- `just` task set (`install`, `run`, `dev`, `build`, `test`, `uninstall`) and
Forgejo Actions CI (RuboCop + tests on Ruby 3.3/3.4, gem build on release).
- `CONTRIBUTING.md` and a minitest suite; clean RuboCop.
### Security
- Passwords hashed with scrypt and verified in constant time.
- Per-form CSRF tokens on every state-changing admin request.
- Session cookies are `HttpOnly` and `SameSite=Strict`, and gain the `Secure`
attribute automatically on HTTPS requests (detected via `request.ssl?` /
`X-Forwarded-Proto`), so the cookie never travels over plain HTTP in
production.
- Configurable, stable `session_key` so sessions survive restarts.
- Generic login error message to avoid username enumeration.
[0.1.0]: https://sourcedock.dev/petrbalvin/volumen/releases/tag/v0.1.0
[0.2.0]: https://sourcedock.dev/petrbalvin/volumen/releases/tag/v0.2.0
[0.3.0]: https://sourcedock.dev/petrbalvin/volumen/releases/tag/v0.3.0
[0.4.0]: https://sourcedock.dev/petrbalvin/volumen/releases/tag/v0.4.0