summaryrefslogtreecommitdiffstats
path: root/DESIGN.md
diff options
context:
space:
mode:
authorKen D'Ambrosio <ken@jots.org>2026-05-11 18:50:51 +0000
committerKen D'Ambrosio <ken@jots.org>2026-05-11 18:50:51 +0000
commitb40e95ca17f8c9f17af5f475d001c8ec33728e6d (patch)
tree46c242d916fca387164d760c482dda7fc6d7fd5f /DESIGN.md
parent723a9bc34c30ddb0decedd9efe64af5b91b71541 (diff)
v1.01: replace bcrypt with PBKDF2-SHA256; update README and DESIGN docsv1.01
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'DESIGN.md')
-rw-r--r--DESIGN.md63
1 files changed, 46 insertions, 17 deletions
diff --git a/DESIGN.md b/DESIGN.md
index a4d9108..2edcc1a 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -74,7 +74,7 @@ is set to unlimited because large video files may be uploaded via rsync.
img/audio.svg ← placeholder thumbnail for audio files
scripts/
update.rb ← post-upload scan/enrich script
- set_password.rb ← bcrypt password setter
+ set_password.rb ← PBKDF2-SHA256 password setter
cache/thumbs/ ← generated thumbnail cache (mirrored path structure)
tmp/ ← Puma pid / state files
log/ ← Puma stdout / stderr logs
@@ -103,9 +103,11 @@ is set to unlimited because large video files may be uploaded via rsync.
| `puma ~> 6.4` | Multi-threaded Rack application server |
| `mini_magick ~> 4.12` | ImageMagick wrapper — thumbnail generation, EXIF-aware auto-orient |
| `mini_exiftool ~> 2.10` | Reads EXIF `DateTimeOriginal` / `CreateDate` for chronological sorting |
-| `bcrypt ~> 3.1` | Password hashing for the single admin account |
| `rack-session ~> 2.0` | Cookie-based session support (required by Sinatra 4 separately) |
+Password hashing uses `OpenSSL::PKCS5.pbkdf2_hmac` from Ruby's standard library
+(100,000 iterations, SHA-256, 32-byte output). No native gem extension is required.
+
### System tools
| Tool | Purpose |
@@ -257,12 +259,12 @@ no server call is needed to open or navigate the lightbox.
`.hidden` (i.e. `display:none !important`).
3. The appropriate element for the media type has `.hidden` removed and its
`src` set. For video and audio, playback starts immediately.
-4. The URL hash is updated to `#photo=filename` so the link is shareable.
- On page load, if that hash is present, the matching photo is opened
- automatically.
+4. The URL is updated to `?photo=filename` so the link is shareable and
+ the server can read it. On page load, if that parameter is present,
+ the matching photo is opened automatically.
5. Navigation: ← → arrow keys, on-screen buttons, or touch swipe (>50 px).
6. Closing: `Escape`, the ✕ button, or clicking outside the media.
- Video/audio is paused; the hash is stripped from the URL.
+ Video/audio is paused; the `?photo=` param is stripped from the URL.
### Slideshow (`GET /slideshow/SomeAlbum`)
@@ -282,14 +284,41 @@ page. `slideshow.js`:
### Admin authentication
-A single bcrypt-hashed password is stored in `config.yml`
-(`admin_password_hash`). `POST /admin/login` compares the submitted
-password against the hash using `BCrypt::Password#==`. On success,
+A single PBKDF2-SHA256 password hash is stored in `config.yml`
+(`admin_password_hash`) in the format `pbkdf2_sha256$<iterations>$<salt>$<hex>`.
+`POST /admin/login` re-derives the key from the submitted password and compares
+using `OpenSSL.fixed_length_secure_compare` (constant-time). On success,
`session[:admin] = true` is set in an encrypted cookie. All admin routes
call `require_admin!` which halts with the login form on failure.
-The `return_to` parameter on the login form lets the visitor be redirected
-back to the page they were trying to reach.
+The `return_to` parameter on the login form redirects the visitor back to
+the page they came from — the "Admin" nav link passes the current album's
+edit URL so logging in lands directly in the right edit view.
+
+### Open Graph meta tags
+
+Every album page includes `og:title`, `og:url`, `og:type`, and (when
+available) `og:description` and `og:image`. The image is resolved
+deterministically in `browse_album`:
+
+- If the request includes `?photo=filename`, `og:image` points to the
+ full-resolution `/media/` URL for that specific file — so sharing a
+ photo link previews that exact photo.
+- Otherwise, the first image from `@entries` (chronological/filename order)
+ is used, falling back to `cover_candidates(dir).first` if the album has
+ no direct media files.
+
+`request.base_url` is used to build absolute URLs, so the tags are correct
+regardless of hostname or scheme.
+
+### Filtered slideshow
+
+When the album search filter is active on the root page, the Slideshow
+button href is dynamically updated (via `album.js`) to include
+`?dirs=name1,name2,...` — the `data-rel` values of the currently visible
+album cards. `slideshow_view` in `app.rb` passes these to
+`all_media_entries(top_dirs:)`, which walks only those specific
+subdirectories instead of the full media tree.
### Saving album edits (`POST /admin/edit/SomeAlbum`)
@@ -297,9 +326,9 @@ back to the page they were trying to reach.
2. Top-level fields (title, description, cover, sort_reverse, visible) are
updated from form params. `blank_to_nil` converts empty strings to `nil`
so omitted optional fields don't get stored as `""`.
-3. Per-file fields (title, caption, visible) are updated by iterating the
- `file_title[name]` / `file_caption[name]` / `file_visible[name]` param
- hashes.
+3. Per-file fields (caption, visible) are updated by iterating the
+ `file_visible[name]` param hash (present for every file via a hidden
+ input), merging `file_caption[name]` for each.
4. `atomic_write` writes the updated JSON to a `.tmp` file and renames it
into place.
5. Redirect back to the same edit page (PRG pattern — prevents double-submit
@@ -351,7 +380,7 @@ escape the media root gets a `404`.
returns `403` unless the session has admin privileges. The browse routes
filter hidden albums and files from the data sent to the browser.
-**Admin password:** Stored as a bcrypt hash in `config.yml` (not in git).
+**Admin password:** Stored as a PBKDF2-SHA256 hash in `config.yml` (not in git).
The plaintext password is never persisted. Sessions use an encrypted cookie
with a random secret also stored in `config.yml`.
@@ -389,8 +418,8 @@ upload, run `update.rb` (it generates thumbnails as part of its scan).
### Configuration (`config.yml`)
```yaml
-admin_password_hash: "$2a$12$..." # set with scripts/set_password.rb
-session_secret: "random-hex-string" # set once at install time
+admin_password_hash: "pbkdf2_sha256$100000$<salt>$<hex>" # set with scripts/set_password.rb
+session_secret: "random-hex-string" # set once at install time
```
This file is not tracked in git. All three paths (`MEDIA_ROOT`,