From ce7c1742be40442cc7d88931ce3193f9b8ada417 Mon Sep 17 00:00:00 2001 From: ralksta Date: Fri, 29 May 2026 17:04:41 +0200 Subject: [PATCH] docs: add admin panel documentation and update README - Add docs/admin-panel.md with full guide (setup, features, security, Docker) - Update README with admin panel feature listing and setup instructions - Update gallery-config.md with tip about visual alternative - Update CLAUDE.md with admin architecture and API routes --- CLAUDE.md | 132 ++++++++++++++++++++++++++++++++++++++ README.md | 23 +++++++ docs/admin-panel.md | 141 +++++++++++++++++++++++++++++++++++++++++ docs/gallery-config.md | 3 + 4 files changed, 299 insertions(+) create mode 100644 CLAUDE.md create mode 100644 docs/admin-panel.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ad3922c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,132 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this project is + +Immich Folio is a self-hosted photography portfolio that acts as a secure reverse proxy between visitors and a private Immich instance. It serves albums via an image proxy so the Immich server and API key are never exposed to the public internet. Asset UUIDs are AES-256 encrypted into opaque URL tokens. + +## Commands + +```bash +npm run dev # Start dev server at http://localhost:3000 +npm run build # Production build (must pass before any PR) +npm run lint # ESLint +npm run format # Prettier (write) +npm run format:check # Prettier (check only) +npx tsc --noEmit # TypeScript type-check (must be 0 errors before any PR) + +# Unit tests (Vitest, lib/__tests__/ only) +npm run test # Watch mode +npm run test:unit # Single run +npm run test:coverage + +# E2E tests (Playwright, requires dev server running or starts it automatically) +npm run test:e2e +``` + +**Formatting caution:** Running `npm run format` at the repo root applies changes to the entire codebase and pollutes diffs. Prefer formatting only modified files. + +## Architecture + +### Configuration loading (`lib/config/`) + +Configuration is loaded once at startup (cached in a module-level singleton, re-read in dev mode per request). + +- `lib/env.ts` — parses and validates all environment variables into a typed `Env` object. All env access in the codebase must go through this module. +- `lib/config/schema.ts` — TypeScript types for `AppConfig`, `GalleryYaml`, `SettingsYaml`, `SubpageConfig`, `GridConfig`, `ThemeConfig`, etc., plus the `slugify()` utility. +- `lib/config/index.ts` — `getConfig()`: reads `content/gallery.yaml` and `content/settings.yaml`, merges them into a single `AppConfig`. Also exports `buildSubpageGrid()` and re-exports from schema/theme. +- `lib/config/theme.ts` — six built-in theme presets (`studio`, `minimal`, `editorial`, `classic`, `noir`, `monograph`) and `resolveTheme()` which merges partial overrides over a preset. + +`getConfig()` returns a `needsSetup: true` dummy config (instead of throwing) when `gallery.yaml` or credentials are missing — this lets the app render a `SetupScreen` instead of crashing. + +### Content files (`content/`) + +- `content/gallery.yaml` — gallery structure: hero asset IDs, standalone albums, subpages (with optional sections, passwords, per-subpage grid overrides). Use `gallery.yaml.example` as reference. +- `content/settings.yaml` — site-wide settings: title, subtitle, theme, grid defaults, footer, legal, map, transitions, SEO. Use `settings.yaml.example` as reference. +- `content/about.md` — Markdown with frontmatter for the about page (portrait, name, location, gear). + +### Immich client (`lib/immich.ts`) + +Singleton `ImmichClient` class exported as `immich`. All Immich API calls are server-side only. Key design points: + +- **Album allowlist** — `getAlbums()` fetches `?shared=true` albums but only returns IDs listed in `config.albums`. Requests for unlisted albums are silently rejected. +- **Request coalescing** — pending promises are stored in `Map` fields to deduplicate concurrent requests for the same album/asset (important for `Promise.all` calls in grids). +- **In-memory LRU cache** (`lib/cache.ts`) — 200-entry LRU, configurable TTL via `CACHE_TTL`. Cache keys: `albums-list`, `album-`, `asset-`. +- **Image streaming** — `streamAsset()` proxies binary responses; never loads the full image into memory. + +### Asset token security (`lib/tokens.ts`) + +`encodeAssetId(uuid)` / `decodeAssetId(token)` — AES-256-GCM with a deterministic IV derived from the asset ID (same UUID → same token, enabling browser caching). Token format: `v2:`. The encryption key is derived from `AUTH_SECRET` via SHA-256. Legacy CBC tokens are still decoded for backward compatibility. + +### API routes (`app/api/`) + +| Route | Purpose | +|---|---| +| `GET /api/image/[id]` | Image proxy — decodes token, rate-limits, streams from Immich | +| `GET /api/exif/[id]` | EXIF data for lightbox panel | +| `POST /api/auth` | Password submission → sets `HttpOnly` cookie | +| `GET /api/og` | Dynamic OG image generation (rate-limited) | +| `GET /api/map` | Aggregated GPS coordinates for map view | +| `GET /api/health` | Health check | +| `GET/POST/DELETE /api/admin/auth` | Admin login, session check, logout | +| `GET/PUT /api/admin/gallery` | Read/write `gallery.yaml` | +| `GET/PUT /api/admin/settings` | Read/write `settings.yaml` | +| `GET /api/admin/albums` | Browse all shared Immich albums (admin-only) | +| `POST /api/admin/reload` | Invalidate config + Immich cache | + +The image proxy maps requested pixel widths (`?w=`) to Immich size tiers: `≤250px→thumbnail`, `≤1440px→preview`, `>1440px→original`. + +### Routing (`app/[...path]/page.tsx`) + +Single catch-all route handles three cases: +1. `/[subpage-slug]/[album-slug]` — renders album detail with back-link to subpage +2. `/[subpage-slug]` — if subpage has >1 album, renders `SubpageGridView`; if exactly 1 album, renders `AlbumDetailView` directly +3. `/[album-slug]` — standalone album detail + +All pages are `dynamic = 'force-dynamic'` (no SSG; requires live Immich). + +### Password protection (`lib/auth.ts`) + +Per-subpage and per-album password gating using HMAC tokens in `HttpOnly` cookies (no database). Cookie names: `lb_auth_` (subpage) and `lb_auth_album_` (album). Password storage supports plaintext (deprecated, logs a warning with recommended scrypt hash), `scrypt:salt:hash` format, and rejects legacy bcrypt. Token expiry: 24 hours. + +### Rate limiting (`lib/rate-limit.ts`) + +In-memory sliding-window rate limiter. Important: **in-memory only** — does not work across multiple Node.js instances. Uses FIFO eviction (not reject-on-full) to prevent DoS via store flooding. Configurable via `RATE_LIMIT_RPM`. Trusted proxy IPs must be set via `TRUSTED_PROXIES` env var to safely trust `X-Forwarded-For` headers. + +### Theming system + +Theme is applied via CSS custom properties on the `` element (`data-preset`, `data-grain`, `data-photo-frame`, etc.) and inline `style` vars (`--accent`, `--font-serif`, etc.). Theme preset CSS files live in `app/themes/`. Base tokens are in `app/tokens.css`. Vanilla CSS throughout — no Tailwind, no CSS-in-JS. + +### `next/image` loader + +A custom loader (`lib/immichLoader.ts`) maps `next/image` requests to `/api/image/[token]?w=`, keeping all image traffic through the proxy. + +### Admin Panel (`app/admin/`, `lib/admin/`, `app/api/admin/`) + +Visual page builder and settings editor at `/admin`. Enabled by setting `ADMIN_PASSWORD` env var. + +- `lib/admin/auth.ts` — HMAC-signed session tokens (HttpOnly cookie `folio_admin_session`), constant-time password verification, 24h expiry. +- `lib/admin/yaml-service.ts` — Atomic YAML read/write with automatic backups to `content/.backups/` (max 10 per file). Writes use temp-file + rename pattern. +- `app/api/admin/auth` — POST login, GET session check, DELETE logout. +- `app/api/admin/gallery` — GET/PUT for `gallery.yaml`. +- `app/api/admin/settings` — GET/PUT for `settings.yaml`. +- `app/api/admin/albums` — GET lists ALL shared Immich albums (bypasses allowlist, admin-only). +- `app/api/admin/reload` — POST invalidates config cache + Immich cache. +- `app/admin/components/PageBuilder.tsx` — Tree editor for hero, standalone albums, subpages, and sections. +- `app/admin/components/AlbumPicker.tsx` — Modal to browse/search all Immich albums. +- `app/admin/components/SettingsEditor.tsx` — Sidebar-nav settings form (theme, grid, footer, legal, SEO). + +After saving, `invalidateConfigCache()` is called so the next request picks up the new YAML without restart. + +### Middleware (`middleware.ts`) + +Applies CSP (with per-request nonce), HSTS, and other security headers on all non-API, non-static routes. The nonce is passed to pages via the `x-nonce` request header. + +## Code conventions + +- **TypeScript strict mode** — no untyped `any`; use `@ts-expect-error` (not `@ts-ignore`) with a comment when suppressing type errors. +- **Server Components by default** — add `'use client'` only when browser APIs or React state/effects are needed. +- **All Immich data flows server-side** — raw asset UUIDs must never appear in client-rendered HTML or JS. Always use `encodeAssetId()` / `imageUrl()` / `exifUrl()` from `lib/urls.ts` before passing IDs to components. +- **Rate-limit all expensive endpoints** — apply `checkRateLimit` from `lib/rate-limit.ts` to any route doing heavy computation or upstream API calls. +- **Commit style** — Conventional Commits: `feat:`, `fix:`, `security:`, `docs:`, `chore:`. diff --git a/README.md b/README.md index 63d7a13..e7063ee 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,14 @@ Immich Folio acts as a **secure reverse proxy** between your visitors and your p - **Markdown about page** — `content/about.md` with frontmatter for portrait, name, location, and gear list - **Dynamic OG images** — auto-generated social preview images per album +### Admin Panel + +- **Visual page builder** — drag & drop interface to manage hero images, standalone albums, subpages, and sections +- **Album picker** — browse all shared Immich albums with search, see photo counts, and add them with one click +- **Settings editor** — configure theme, grid layout, footer, legal/impressum, and SEO from a visual UI +- **Live YAML sync** — changes are written directly to `gallery.yaml` and `settings.yaml` with automatic backups +- **Password protected** — secured with its own admin password, separate from album passwords +
Security & Infrastructure @@ -84,6 +92,7 @@ SITE_TITLE=My Photography # default: "Gallery" SITE_SUBTITLE=A visual journal # default: empty CACHE_TTL=300 # seconds, default: 300 RATE_LIMIT_RPM=120 # requests/min/IP, default: 120 +ADMIN_PASSWORD=your-secure-password # enables /admin panel ``` ### Gallery Config @@ -169,6 +178,20 @@ photos.example.com {
+## Admin Panel + +A built-in visual editor at `/admin` lets you manage your gallery without editing YAML files manually. + +**Enable it** by setting `ADMIN_PASSWORD` in your environment: + +```env +ADMIN_PASSWORD=your-secure-admin-password +``` + +Then navigate to `http://your-site/admin` and log in. + +→ **[Admin Panel Guide](docs/admin-panel.md)** + ## Tech Stack - **Next.js 16** (App Router, standalone output) diff --git a/docs/admin-panel.md b/docs/admin-panel.md new file mode 100644 index 0000000..d1e0b20 --- /dev/null +++ b/docs/admin-panel.md @@ -0,0 +1,141 @@ +# Admin Panel + +Immich Folio includes a built-in visual admin panel at `/admin` that lets you manage your gallery structure and site settings without editing YAML files by hand. + +**Contents:** + +- [Enabling the Admin Panel](#enabling-the-admin-panel) +- [Page Builder](#page-builder) +- [Settings Editor](#settings-editor) +- [Album Picker](#album-picker) +- [Security](#security) +- [Backups](#backups) +- [Docker Usage](#docker-usage) + +## Enabling the Admin Panel + +Set `ADMIN_PASSWORD` in your `.env.local` (or Docker environment): + +```env +ADMIN_PASSWORD=your-secure-admin-password +``` + +Then navigate to `http://your-site/admin`. The panel is completely disabled if no password is set — it won't even render the login page. + +> [!IMPORTANT] +> The admin password is separate from any album or subpage passwords. It controls access to the entire gallery configuration. + +## Page Builder + +The **Pages** tab provides a visual tree view of your gallery structure: + +### Hero Images + +Add Immich asset UUIDs for the homepage hero carousel. Supports single or multiple images that crossfade automatically. + +### Standalone Albums + +Albums displayed directly on the homepage. Click **+ Add Album** to open the Album Picker and select from your Immich library. + +Each album card supports: +- **Title override** — display a custom name instead of the Immich album name +- **Description** — shown below the album title on the subpage +- **Password** — protect individual albums with a password + +### Subpages + +Group albums under custom URL paths (e.g., `/japan`, `/wedding-smith`). + +Each subpage has: +- **Name** — used for navigation and URL generation (auto-slugified) +- **Title** — optional display title (defaults to name) +- **Subtitle** — optional text shown below the page heading +- **Password** — optional protection for the entire subpage + +### Sections + +Within a subpage, you can organize albums into named sections. This generates a table of contents with anchor links. Add sections with **+ Add Section** and fill each with albums from the picker. + +### Reordering + +Use the **↑/↓** buttons on subpages to change their display order. The order in the admin panel matches the order on your site. + +## Settings Editor + +The **Settings** tab lets you configure global site behavior: + +| Section | Controls | +| --------- | ---------------------------------------------------------------- | +| General | Site title, subtitle, language, EXIF on hover, map, transitions | +| Theme | Preset, accent color, photo frame, hero style, grain, header dot | +| Grid | Layout mode, columns, gap, aspect ratio | +| Footer | Name, Instagram, email, website | +| Legal | Impressum toggle and all legal fields | +| SEO | Meta title, description, noindex, nofollow | + +Changes are applied immediately after saving — no server restart required. + +## Album Picker + +When adding albums, a modal overlay shows **all shared albums** from your Immich instance (not just the ones already configured). Features: + +- **Search** by album name or UUID +- **Photo count** shown for each album +- **"In use" badge** for albums already assigned somewhere +- Albums are sorted: configured first, then alphabetically + +## Security + +| Aspect | Implementation | +| -------------- | ----------------------------------------------------------- | +| Authentication | HMAC-signed session token in an `HttpOnly` cookie | +| Session expiry | 24 hours (automatic logout) | +| Password check | Constant-time comparison to prevent timing attacks | +| Cookie flags | `HttpOnly`, `Secure` (in production), `SameSite=Strict` | +| Rate limiting | Admin endpoints share the global rate limiter | +| Robots | `/admin` is marked `noindex, nofollow` in metadata | + +> [!NOTE] +> The admin panel uses its own session management, completely separate from album password protection. Admin sessions use `AUTH_SECRET` for token signing. + +## Backups + +Every time you save changes, the previous YAML file is automatically backed up to `content/.backups/`: + +``` +content/.backups/ +├── gallery.yaml.2026-05-29T14-30-00-000Z.bak +├── gallery.yaml.2026-05-29T15-45-00-000Z.bak +├── settings.yaml.2026-05-29T14-30-00-000Z.bak +└── ... +``` + +- Up to **10 backups** per file are retained (oldest are pruned) +- Before a restore, a `*.pre-restore.bak` snapshot is created +- All writes are **atomic** (write to temp file, then rename) — no risk of corrupted YAML + +## Docker Usage + +The admin panel works seamlessly with Docker deployments since the `content/` directory is typically a volume mount: + +```yaml +services: + lightbox: + image: immich-folio + environment: + - ADMIN_PASSWORD=your-secure-password + volumes: + - ./content:/app/content # Must be writable for admin panel +``` + +> [!IMPORTANT] +> When using the admin panel, the content volume must be **read-write** (not `:ro`). Remove the `:ro` flag if you had it set previously. + +### Reload Button + +The admin header includes a **↻ Reload** button that: +1. Invalidates the in-memory config cache +2. Clears the Immich album/asset cache +3. Forces the next request to re-read all YAML files + +This is useful after making external changes to config files or when Immich data has changed. diff --git a/docs/gallery-config.md b/docs/gallery-config.md index 7787b7f..73cb7e1 100644 --- a/docs/gallery-config.md +++ b/docs/gallery-config.md @@ -6,6 +6,9 @@ All gallery structure is defined in `content/gallery.yaml`. Copy the example to cp content/gallery.yaml.example content/gallery.yaml ``` +> [!TIP] +> Prefer a visual interface? Enable the **[Admin Panel](admin-panel.md)** by setting `ADMIN_PASSWORD` in your environment. It provides a drag-and-drop page builder that writes to these same YAML files automatically. + **Contents:** - [Hero Images](#hero-images)