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
This commit is contained in:
132
CLAUDE.md
Normal file
132
CLAUDE.md
Normal file
@@ -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<id, Promise>` 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-<id>`, `asset-<id>`.
|
||||
- **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:<base64url(iv+authTag+ciphertext)>`. 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_<slug>` (subpage) and `lb_auth_album_<slug>` (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 `<html>` 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=<width>`, 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:`.
|
||||
23
README.md
23
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
|
||||
|
||||
<details>
|
||||
<summary><strong>Security & Infrastructure</strong></summary>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
</details>
|
||||
|
||||
## 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)
|
||||
|
||||
141
docs/admin-panel.md
Normal file
141
docs/admin-panel.md
Normal file
@@ -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.
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user