A new auth-gated web UI at /manage lists every post for inline editing of
caption, date and tags, with per-row and bulk delete and bulk tag add/remove.
It is backed by a small JSON API (all behind the same htpasswd Basic auth as
/upload, and unlinked from the public site):
GET /api/photos list posts (newest first)
POST /api/edit rewrite one sidecar (caption, date, tags)
POST /api/tags bulk add/remove tags across posts
POST /api/delete move posts to content/.trash/ (reversible)
GET /api/trash list trashed items
POST /api/restore move a trashed item back (409 on live-slug clash)
POST /api/trash/empty hard delete trashed items (id… or all=1)
Design notes:
- Editing only rewrites the .toml; the slug (and URL) never changes.
- Delete is reversible: image + sidecar move to content/.trash/ with a
timestamp suffix, excluded from the site (loadPhotos skips dirs). The only
hard delete is emptying the trash.
- Build now prunes: it drops the derived HTML trees (photo/ day/ album/ page/
grid/page/) and regenerates them, and pruneRenditions removes orphaned
images/thumbs/medium/og. Without this a deleted post would linger at its old
URL and on stale day/album pages; this also cleans up after a hand-removed
sidecar.
- Sidecars are written atomically (temp + rename) so a concurrent build never
observes a half-written file.
- Slugs and trash ids are validated as bare filenames, closing path traversal
and dotfile access.
- Every state-changing POST passes a sameOrigin (CSRF) check, since Basic-auth
credentials are replayed by the browser on every request to the host. It
honours Sec-Fetch-Site, falls back to Origin vs base-url/Host, and allows
header-less non-browser clients. Ported from the independent /manage on
plex/trunk-ng.
module.nix proxies /manage and /api/ to the app (nginx otherwise serves them
as missing static files). The apps are unchanged; the API is there for them to
adopt edit/delete later.
Add HTTP Basic auth on /upload, reading an Apache htpasswd file (bcrypt
only, `htpasswd -B`) via a new -htpasswd flag. This lets nginx drop to a
plain TLS-terminating reverse proxy with no auth of its own; the existing
bcrypt htpasswd file is reused unchanged.
auth.go:
- LoadHtpasswd parses user:hash lines, keeps bcrypt entries ($2a/$2b/$2y,
so Apache's $2y$ works), skips MD5/SHA/crypt with a warning.
- Unknown users still pay for one bcrypt comparison against a dummy hash,
so response timing can't enumerate valid usernames.
- Failed attempts draw from a token-bucket limiter (x/time/rate, ~1/s,
burst 10) returning 429 once drained; successful and bulk uploads never
touch it.
server.go guards both /upload routes (nil authenticator = passthrough for
local --serve). /health stays open. Empty -htpasswd disables auth and logs
a warning.
module.nix: new top-level services.photogallery.htpasswdFile, delivered to
the service via systemd LoadCredential (so a root-owned 0400 secret works).
Removed nginx.htpasswdFile and basic_auth from the vhost.
Both uploader apps already send Basic credentials, so no client change.
Deps: golang.org/x/crypto/bcrypt, golang.org/x/time/rate (vendorHash bump).
An album is a tag. Photos carry lowercase free-text tags in their .toml
sidecar (`tags = [...]`), parallel to the existing day grouping — no
database, all derived at build time.
- albums.go: parseTags (lowercase/trim/dedupe, slug via sanitiseSlug),
groupByTag mirroring groupByDay, and rendering for album pages, the
index, and the JSON API. Slug collisions between distinct names are
suffixed and logged.
- Routes: /album/<slug>/ (paginated square grid per tag), /albums/ (index
with counts), /tags.json (machine-readable tag list, served statically
like feed.xml).
- POST /upload gains a `tags` field written into the sidecar; the web
upload form gains a tags input.
- Tags shown on each photo page linking to their album; an Albums link in
the timeline/grid header.