# Backend

Express server for HLS player with admin authentication, local media sharing, upload, and CORS proxy.

## Quick Start

| Mode | Command | Notes |
|------|---------|-------|
| Dev | `npm install && npm run dev` | Listens on `http://localhost:2738`; uses nodemon |
| Prod | `npm install --omit=dev && npm start` | Production start (no hot reload) |

## API Endpoints

| Area | Method | Path | Auth | Purpose |
|------|--------|------|------|---------|
| Health | GET | `/health` | - | `{ status: 'ok' }` health probe |
| Admin | POST | `/admin/login` | - | Login with `{ username, password }`; sets session cookie |
| Admin | POST | `/admin/logout` | - | Clears session cookie |
| Admin | GET | `/admin/me` | - | Returns `{ authenticated, configured }` |
| Embed | POST | `/embed/tokens` | Admin | Create embed token; body: `{ config, ttlPreset?, customTtlValue?, customTtlUnit?, description? }` |
| Embed | GET | `/embed/tokens` | Admin | List embed tokens + audit fields |
| Embed | DELETE | `/embed/tokens/:tokenId` | Admin | Revoke embed token |
| Embed | GET | `/embed/token/:tokenId` | - | Fetch token config + record usage (404/410 for missing/expired) |
| Local Media | GET | `/local/browse?dir=...` | Admin | List files/folders (path traversal protected) |
| Local Media | POST | `/local/shares` | Admin | Create share token; body: `{ path, ttlSeconds }` |
| Local Media | GET | `/local/shares` | Admin | List active shares |
| Local Media | DELETE | `/local/shares/:id` | Admin | Revoke share |
| Local Media | GET | `/local/share/file/:id` | - | Serve raw file (supports Range requests) |
| Local Media | GET | `/local/share/hls/:id/index.m3u8` | - | HLS playlist for shared media |
| Local Media | GET | `/local/share/hls/:id/:asset` | - | HLS segment/variant file |
| Upload | POST | `/upload` | - | Image upload (multipart/form-data, `file` key) |
| Upload | GET | `/uploads/:filename` | - | Serve uploaded image |
| Proxy | GET | `/proxy?url=...` | - | Proxy external m3u8 with permissive CORS, rewrites URLs |

## Local Media & Sharing

- **Requires:** `ENABLE_LOCAL_MEDIA=1` + `LOCAL_MEDIA_ROOT=<path>`.
- **Auth:** Admin-only endpoints (`/local/browse`, `/local/shares*`) protected by session cookie.
- **Shares:** UUID tokens with TTL (default: 24h, max: 30 days). Auto-expire and prune.
- **Packaging:** `.ts` files auto-packaged via ffmpeg on-demand; cached in `data/hls-cache/<shareId>/`.
- **Security:** Paths validated against traversal; blocked private hosts in proxy.

**Share Response:**
```json
{
  "shareId": "uuid-1234...",
  "path": "/media/video.mp4",
  "kind": "video",
  "createdAt": 1705862400,
  "expiresAt": 1705948800,
  "url": "http://backend:2738/local/share/hls/<shareId>/index.m3u8"
}
```

## File Upload

- **Max size:** 5MB.
- **Allowed MIME:** image/jpeg, image/png, image/gif, image/webp, image/tiff, image/bmp.
- **Storage:** `backend/uploads/`; named with UUID.
- **Access:** Served at `/uploads/:filename`.

## Environment Variables

| Variable | Default | Purpose |
|----------|---------|---------|
| `BACKEND_PORT` | `2738` | Express port |
| `BACKEND_HOST` | `0.0.0.0` | Bind address |
| `TRUST_PROXY` | `1` | Respect X-Forwarded-* headers |
| `ADMIN_USERNAME` / `ADMIN_PASSWORD` | *required for admin* | Admin credentials |
| `ADMIN_SESSION_TTL_SECONDS` | `0` (no expiry) | Session lifetime |
| `ENABLE_LOCAL_MEDIA` | `'0'` | Set to `1` for `/local/*` APIs |
| `LOCAL_MEDIA_ROOT` | `''` | Media library path |
| `LOCAL_SHARES_DB_PATH` | `data/local-shares.json` | Share metadata store |
| `LOCAL_SHARE_TTL_SECONDS` | `86400` (24h) | Default share TTL |
| `LOCAL_SHARE_MAX_TTL_SECONDS` | `2592000` (30d) | Max share TTL |
| `LOCAL_HLS_CACHE_DIR` | `data/hls-cache` | Packaging cache directory |
| `FFMPEG_PATH` | `ffmpeg` | ffmpeg binary path |
| `FFMPEG_MAX_JOBS` | `1` | Concurrent packaging jobs |
| `LOCAL_TRANSCODE_TARGET` | `auto` | `copy`, `h264`, `hevc` override |
| `LOCAL_PACKAGER_URL` | - | Optional external packager URL |
| `LOCAL_PACKAGER_TOKEN` | - | Bearer token for external packager |
| `EMBED_TOKENS_DB_PATH` | `data/embed-tokens.json` | Embed token store path |

## Architecture

| Module | Role | Reference |
|--------|------|-----------|
| `backend/index.js` | Express, helmet, cors, multer, rate-limit, all routes | (backend/index.js:1-217) |
| `adminAuth.js` | Timing-safe auth, session cookies, CORS middleware | (backend/adminAuth.js:1-278) |
| `adminSessionStore.js` | JSON-backed session persistence with pruning | (backend/adminSessionStore.js:1-127) |
| `localMedia.js` | Path validation, MIME detection, playlist rewriting | (backend/localMedia.js:1-200) |
| `localMediaStore.js` | Share persistence (create/list/delete) | (backend/localMediaStore.js:1-127) |
| `hlsPackager.js` | ffmpeg packaging for `.ts` + caching | (backend/hlsPackager.js:1-200) |
| `proxy.js` | URL validation, playlist rewriting | (backend/proxy.js:1-120) |

**Security:**
- Helmet headers (CSP disabled for HLS compatibility). (backend/index.js:140-144)
- Frameguard disabled to allow cross-site `/embed` iframes. (backend/index.js:140-145)
- Rate limit: 100 req/15 min per IP. (backend/index.js:147-152)
- Path validation prevents traversal. (backend/localMedia.js)
- Proxy blocks non-http(s) and private hosts (SSRF protection). (backend/proxy.js:19-31)

## Reverse Proxy Notes

When behind Caddy/Nginx:

1. Pass `X-Forwarded-Proto` (for correct scheme in rewrites).
2. Pass `X-Forwarded-For` (for IP-based rate limiting).
3. `TRUST_PROXY=1` (default) handles header parsing.

**Caddy Example:**
```caddyfile
reverse_proxy localhost:2738 {
  header_up X-Forwarded-Proto {scheme}
  header_up X-Forwarded-For {remote_host}
}
```

## Testing & Troubleshooting

| Command | Purpose |
|---------|---------|
| `npm test` | Run all tests (Node built-in test runner) |
| `cd backend && node --test tests/proxy.test.js` | Run single test file |

**Common Issues:**

| Symptom | Likely Cause | Fix |
|---------|--------------|-----|
| 503 admin_not_configured | `ADMIN_USERNAME`/`ADMIN_PASSWORD` missing | Set env vars |
| 401 admin_required | Session expired or no cookie | Re-login |
| 404 on `/local/*` | Local media disabled | Set `ENABLE_LOCAL_MEDIA=1` + `LOCAL_MEDIA_ROOT` |
| Playlist rewrite errors | Backend URL mismatch | Check `FRONTEND_URL`, `FRONTEND_PUBLIC_URL` |
| HLS packaging fails | ffmpeg not found | Set `FFMPEG_PATH` correctly or install ffmpeg |
