Files
volumen/CHANGELOG.md
T
petrbalvin 52be2bb996 security: CORS preflight, session invalidation, CSRF on preview, symlink containment, and hardening
- Add OPTIONS /api/volumen/* for CORS preflight (S-22).
- Invalidate session in require_login! when user no longer exists (S-8).
- Add check_csrf! to POST /admin/preview (S-1).
- Use File.realpath for symlink-safe containment in media_file (S-2).
- Memoize Users#all with mtime-based invalidation (S-12).
- Document that empty permitted_hosts means allow-all (S-5).
- Warn and fall back to random when session_key is too short (S-20).
2026-06-25 22:06:49 +02:00

139 lines
6.1 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).
## [development]
### 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`.
### 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://codeberg.org/petrbalvin/volumen/releases/tag/v0.1.0