24 Commits
Author SHA1 Message Date
petrbalvin 1440e9a1fd release: bump version to 0.2.0 2026-06-25 22:12:55 +02:00
petrbalvin b24a8cc377 feat: add fulltext search with ?q= parameter
Filter posts by case-insensitive match in title and body. Extracted
filter_by_lang and search_posts helpers to keep complexity in check.
2026-06-25 22:06:49 +02:00
petrbalvin 599c63e5bc feat: add JSON Feed endpoint at /api/volumen/feed.json
Implements jsonfeed.org version 1.1 with title, home_page_url, feed_url,
description, language, and items with content_html and date_published.
2026-06-25 22:06:49 +02:00
petrbalvin 73be893bf8 feat: add scheduled publishing with publish_at frontmatter field
Posts with a future publish_at date are hidden from the public API
and feeds, same as drafts. The admin form exposes a Publish at field.
2026-06-25 22:06:49 +02:00
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
petrbalvin 5b8236c0a4 test: add sitemap.xml endpoint test
Verify content type, urlset structure, post URL presence, and draft exclusion.
2026-06-25 22:06:49 +02:00
petrbalvin 51e425e6dd test: add CLI test coverage
Cover version, help, unknown command, parse_serve_options defaults
and overrides, prepare_config template generation and no-op.
2026-06-25 22:06:49 +02:00
petrbalvin ce57410044 fix: Store#save writes non-default language posts into lang subdirectory
default_path_for now routes posts whose lang differs from default_lang
into content_dir/<lang>/<slug>.md, matching the documented multi-language
layout and preventing same-slug posts in different languages from
overwriting each other.
2026-06-25 22:06:49 +02:00
petrbalvin afd8ab4bfb fix: atomic writes for posts and users.toml
Write to a .tmp file and then File.rename into place, so a concurrent
read or a mid-write crash never sees a torn file.
2026-06-25 22:06:49 +02:00
petrbalvin ad00872895 perf: memoize Store#all with mtime-based invalidation
Cache the parsed post list per Store instance, invalidated on save,
delete, or content_dir mtime change. Eliminates re-parsing every .md
file on every API and admin request.
2026-06-25 22:06:49 +02:00
petrbalvin d7f9813484 fix: prevent Config DEFAULTS mutation through deep_merge
Freeze inner hashes in DEFAULTS and rewrite deep_merge to build fresh
hashes with {}.merge for uncopied inner maps. Previously, inner hashes
shared objects with the frozen-looking DEFAULTS, so mutating a loaded
config silently poisoned all later loads.
2026-06-25 22:06:49 +02:00
petrbalvin 263e794669 security: hide draft posts from public detail endpoint
Add post.draft? check to GET /api/volumen/posts/:slug so guessing a
draft slug returns 404 like the list endpoint already does.
2026-06-25 22:06:49 +02:00
petrbalvin 1ca0212197 security: add slug validation, username validation, and template escaping
- Enforce SLUG_REGEX (alphanumeric + dot/dash/underscore only) in creation_error
  to prevent path traversal (C-1) and stored XSS (C-3).
- Add File.expand_path containment check in default_path_for as defense-in-depth.
- Escape slug in form action attributes (list, form views).
- Validate username in change_username using the same regex as create_user (C-4).
- Escape username in settings form action attributes.
- Treat submitting the current username as a no-op success instead of 'taken'.
2026-06-25 22:06:49 +02:00
petrbalvin d26d3b5a5b fix: strip HTML tags from derived excerpt
gsub(/<[^>]+>/, '') before stripping markdown markup, so auto-generated
excerpts don't leak raw HTML from post bodies.
2026-06-25 22:06:49 +02:00
petrbalvin 4597c685da test: add RSS feed endpoint tests
Verify content type, RSS structure, draft exclusion, and language
element.
2026-06-25 21:41:19 +02:00
petrbalvin 2181538691 feat: add language element to RSS feed channel 2026-06-25 21:40:57 +02:00
petrbalvin 9dd55adc30 fix: add periodic cleanup of login_attempts hash
Sweep stale entries every 5 minutes to prevent unbounded memory growth
from IPs that touch the login endpoint once and never return.
2026-06-25 21:40:37 +02:00
petrbalvin 7a31b50ca7 docs: add [development] section to CHANGELOG, document in CONTRIBUTING
Record ongoing changes under [development] in CHANGELOG.md. Update the
release workflow to rename [development] to the versioned section.
2026-06-25 21:37:27 +02:00
petrbalvin b2b8156648 refactor: split Server into api_routes and admin_routes modules
Keep server.rb focused on configuration, session setup, and shared
helpers. Extract route definitions into registered Sinatra extension
modules. Exclude the registered methods from AbcSize/MethodLength
metrics — large registered methods are the idiomatic Sinatra pattern.
2026-06-25 21:35:07 +02:00
petrbalvin 140b16b55a feat: add in-memory rate limiting for login endpoint
Rate-limit POST /admin/login at 10 attempts per 60 seconds per IP.
Stored in Volumen.login_attempts so tests can reset the counter.
2026-06-25 21:33:16 +02:00
petrbalvin 2348352fab feat: add lastmod to sitemap entries 2026-06-25 21:31:57 +02:00
petrbalvin 184aa9b3d8 feat: add pubDate to RSS feed items 2026-06-25 21:31:02 +02:00
petrbalvin 0a70b74b55 fix: wire session_ttl config into Rack session cookie expiration
The admin.session_ttl value was defined in defaults, template, and
documentation but never read — sessions had no expiration. Pass it as
expire_after to Rack::Session::Cookie. A value <= 0 means persistent
(until browser close), matching the documented behaviour.
2026-06-25 21:30:20 +02:00
petrbalvin dd3efe870e feat: initial release of volumen — a file-based Markdown blog engine 2026-06-21 13:15:06 +02:00