- 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).
139 lines
6.1 KiB
Markdown
139 lines
6.1 KiB
Markdown
# 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
|