d62407f431
Setting EMAIL_WEBHOOK_URL makes PicPeak stop sending mail itself and POST each composed message as JSON instead, for something downstream (n8n, Make, a self-hosted relay) to deliver. Unset, every SMTP path is unchanged. Settles the four things #1225 left open: - SSRF: the URL goes through the same DNS-resolving check the outbound webhook worker uses, before every send. Private receivers are opt-in. - Transport security: https is required for anything leaving the machine. The HMAC proves who sent the body, not who can read it, and these bodies carry password-reset links and guest recovery codes. The private-network opt-in doubles as the plaintext opt-in. - Authentication: EMAIL_WEBHOOK_SECRET is required and signs the body as X-PicPeak-Signature, the same scheme as gallery webhooks. A URL without a secret leaves the transport OFF and says so once. - Attachments: carried as base64, not dropped. Oversized ones fail and stay queued rather than arriving without the invoice. Configuration is environment-only on purpose: this redirects every outbound message including password resets, so it must not be changeable from a compromised admin session. Three wiring details decide whether it works at all: docker-compose.yml declares an explicit environment block, so the vars had to be forwarded there; a fresh webhook-only install has no email_configs row (migration 001 seeds it only when SMTP_HOST is set), so the From identity falls back to EMAIL_FROM; and processEmailQueue used to return early when SMTP could not initialise, which would have left the queue permanently unprocessed. guestRecoveryService and the admin test-email endpoint were bypassing the transport — the first dereferenced a null transporter, the second told webhook-only admins to go configure SMTP. emailIntakeService deliberately stays on SMTP: it round-trips a specific mailbox's own credentials. Response handling is streamed and read bounded by hand rather than capped via axios: maxContentLength throws while reading, so a receiver that delivered the mail and then echoed a large body would have been recorded as failed and the message sent again. Note: docker-compose.dev.yml is gitignored and local-only, so the equivalent entries there are not part of this change. docker-compose.production.yml needs none — it passes .env through with env_file. Three rounds of external review; 21 transport tests, 61 across the email suites.
339 lines
15 KiB
Bash
339 lines
15 KiB
Bash
# PicPeak Environment Configuration
|
|
# Copy this file to .env and update with your values
|
|
|
|
# Environment
|
|
NODE_ENV=production
|
|
|
|
# JWT Secret — OPTIONAL. Leave unset and it is auto-generated on first run
|
|
# (Docker: the secrets-init service writes it to a private volume and reuses it
|
|
# across restarts). Set it explicitly only to pin your own value.
|
|
# Generate one with: openssl rand -base64 64
|
|
#JWT_SECRET=your_very_long_random_jwt_secret_here
|
|
|
|
# How long a gallery guest stays recognised (#1210). Default 30d. It was 24h,
|
|
# which meant a client reviewing a gallery across two weekends registered again
|
|
# in between — and each re-registration is a separate guest whose likes and
|
|
# favourites no longer join up with the first visit's. Takes any jsonwebtoken
|
|
# duration ('7d', '12h'); shorten it if your galleries hold sensitive work.
|
|
#GUEST_TOKEN_TTL=30d
|
|
|
|
# OIDC SSO for admins (#798) — configured in the admin UI; only these two
|
|
# values live in the environment:
|
|
# Key encrypting the OIDC client secret at rest (defaults to JWT_SECRET).
|
|
#OIDC_ENCRYPTION_KEY=
|
|
# Break-glass: 'true' re-enables local password login even while the SSO
|
|
# settings disable it (recovery when the IdP is down or misconfigured).
|
|
#OIDC_BREAK_GLASS=false
|
|
|
|
# Auth cookie Secure flag
|
|
# unset - default: follows NODE_ENV (production=true, dev=false)
|
|
# true - always set Secure (HTTPS-only cookies; breaks plain-HTTP access)
|
|
# false - never set Secure (allows HTTP; cookies not protected on HTTPS)
|
|
# auto - decide per request: Secure on HTTPS, not on HTTP
|
|
#
|
|
# Use COOKIE_SECURE=auto if your deployment is reachable over both HTTPS
|
|
# (via reverse proxy like Nginx Proxy Manager, Traefik, Caddy) AND plain
|
|
# HTTP (e.g. LAN access at http://192.168.x.x:3010). The backend reads
|
|
# req.secure from Express, which respects the X-Forwarded-Proto header
|
|
# when the proxy is in the trust list.
|
|
#
|
|
# Requirements for auto mode:
|
|
# 1. Your reverse proxy MUST send X-Forwarded-Proto: https on HTTPS
|
|
# requests. Standard configs for NPM/Traefik/Caddy do this by default.
|
|
# 2. The proxy must be on a trusted IP range. By default PicPeak trusts
|
|
# loopback and private networks (127.0.0.1, 10.x, 172.16-31.x,
|
|
# 192.168.x, link-local). Proxies outside those ranges need custom
|
|
# trust proxy configuration.
|
|
# COOKIE_SECURE=auto
|
|
|
|
# Cookie SameSite attribute (Lax | Strict | None). Default: Lax
|
|
# COOKIE_SAMESITE=Lax
|
|
|
|
# Cookie Domain — set this if serving auth cookies across subdomains.
|
|
# Leave unset for same-origin setups.
|
|
# COOKIE_DOMAIN=.example.com
|
|
|
|
# Database Configuration (PostgreSQL)
|
|
DATABASE_CLIENT=pg
|
|
DB_USER=picpeak
|
|
# DB_PASSWORD — OPTIONAL. Leave unset and it is auto-generated on first run
|
|
# (Docker). Set it explicitly to pin your own, e.g. for an external database.
|
|
# IMPORTANT: Avoid $ character in passwords - Docker Compose interprets it as variable substitution
|
|
# If you must use $, escape it as $$ (e.g., Pass$$word instead of Pass$word)
|
|
#DB_PASSWORD=your_secure_postgres_password_here
|
|
DB_NAME=picpeak_prod
|
|
|
|
# Redis Configuration
|
|
# REDIS_PASSWORD — OPTIONAL. Leave unset and it is auto-generated on first run (Docker).
|
|
# IMPORTANT: Same warning applies - avoid $ or escape as $$
|
|
#REDIS_PASSWORD=your_secure_redis_password_here
|
|
|
|
# Admin Account (initial setup) — OPTIONAL
|
|
# Leave these unset (default) to create your admin IN THE BROWSER on first run:
|
|
# open /admin and PicPeak shows a setup screen. The one-time setup token is
|
|
# written to data/SETUP_TOKEN with mode 0600 — read it with
|
|
# `docker compose exec backend cat /app/data/SETUP_TOKEN`. It is NOT logged
|
|
# unless that write fails, so it never sits in `docker logs`.
|
|
# The all-in-one image keeps it at /data/db/SETUP_TOKEN — inside the volume,
|
|
# in the db/ subdirectory (#1218). On a NAS with no shell, set ADMIN_PASSWORD
|
|
# below instead: it needs no file at all.
|
|
# Set ADMIN_PASSWORD to auto-create the admin on first boot instead (legacy;
|
|
# credentials written to data/ADMIN_CREDENTIALS.txt).
|
|
#ADMIN_USERNAME=admin
|
|
#ADMIN_EMAIL=admin@yourdomain.com
|
|
#ADMIN_PASSWORD=your_secure_admin_password_here
|
|
|
|
# Email Configuration — OPTIONAL, and normally left alone.
|
|
# SMTP is configured in the setup wizard / Settings -> Email and stored in the
|
|
# database (email_configs); that is what the mail queue actually sends with.
|
|
# These variables are a legacy path kept for config-as-code deployments: when
|
|
# SMTP_HOST is set, the initial migration seeds the database row from it.
|
|
# Developers running the `dev` compose profile want SMTP_HOST=mailhog here so
|
|
# that seed points at the mailhog container.
|
|
# For Gmail: use app-specific password
|
|
# For SendGrid: SMTP_USER=apikey, SMTP_PASS=your-api-key
|
|
#SMTP_HOST=smtp.gmail.com
|
|
#SMTP_PORT=587
|
|
#SMTP_SECURE=false
|
|
#SMTP_USER=your-email@gmail.com
|
|
#SMTP_PASS=your-app-specific-password
|
|
#EMAIL_FROM=noreply@yourdomain.com
|
|
|
|
# Webhook email transport (#1225) — OPTIONAL, an alternative to SMTP entirely.
|
|
# When EMAIL_WEBHOOK_URL is set, PicPeak stops sending mail itself and POSTs
|
|
# each composed message as JSON to that URL instead; something downstream
|
|
# (n8n, Make, a self-hosted relay) delivers it. Useful when SMTP is the part
|
|
# you cannot get working — app passwords, blocked ports, a NAS with no
|
|
# outbound 25.
|
|
#
|
|
# Deliberately environment-only, not an admin setting: it redirects every
|
|
# outbound message including password resets, so it should not be changeable
|
|
# from a compromised admin session.
|
|
#
|
|
# EMAIL_WEBHOOK_SECRET is REQUIRED. The body is signed with it and sent as
|
|
# X-PicPeak-Signature (HMAC-SHA256, hex) — the same scheme as gallery
|
|
# webhooks, so a receiver verifies both the same way. Set the URL without a
|
|
# secret and the transport stays OFF and says so in the log, rather than
|
|
# posting unauthenticated mail to the internet.
|
|
#
|
|
# Payload: { from, to[], cc[], subject, html, text, attachments[] }, where each
|
|
# attachment is { filename, content_type, content_base64 }. Attachments are
|
|
# included rather than dropped; a message whose attachments exceed 10 MB fails
|
|
# and stays in the queue instead of arriving without its invoice.
|
|
#
|
|
# The receiver must be a public https:// address unless you opt in — a container or LAN
|
|
# address is refused by the SSRF check otherwise. Running n8n beside PicPeak is
|
|
# normal, so set EMAIL_WEBHOOK_ALLOW_PRIVATE_URLS=true for that.
|
|
#
|
|
# A mail account with its own SMTP host (Settings -> Mail accounts) keeps
|
|
# sending through it; this replaces the global transport only.
|
|
#
|
|
# Set EMAIL_FROM above as well. A webhook-only install never gets an
|
|
# email_configs row (that is seeded only when SMTP_HOST is set), so EMAIL_FROM
|
|
# is where the sender address comes from.
|
|
#EMAIL_WEBHOOK_URL=https://n8n.example.com/webhook/picpeak-mail
|
|
#EMAIL_WEBHOOK_SECRET=generate-a-long-random-string
|
|
#EMAIL_WEBHOOK_ALLOW_PRIVATE_URLS=false
|
|
|
|
# Application URLs — OPTIONAL. Leave unset for the normal install.
|
|
# The public origin is captured by the setup wizard (it proposes the address
|
|
# you opened the browser at) and stored as the `general_site_url` setting, so
|
|
# you can change it later in Settings -> General without touching this file.
|
|
# Setting FRONTEND_URL here OVERRIDES that setting and makes the field
|
|
# read-only in the admin UI - use it only for config-as-code deployments.
|
|
# Use full origin with scheme, no trailing slash.
|
|
# Admin UI is served by the frontend at /admin.
|
|
#FRONTEND_URL=https://yourdomain.com
|
|
#ADMIN_URL=https://yourdomain.com
|
|
|
|
# Static HTML title + description used for social link previews when the
|
|
# fetcher doesn't trigger the per-event OG endpoint — most notably the
|
|
# WhatsApp Business API and various 3rd-party preview-service caches
|
|
# (#521). Set these to your brand so link previews aren't generic.
|
|
# Substituted into index.html at frontend-container start, so changes
|
|
# take effect on the next `docker compose up -d frontend` — no rebuild
|
|
# required.
|
|
BRAND_TITLE=PicPeak
|
|
BRAND_DESCRIPTION=Photo gallery shared with PicPeak.
|
|
|
|
# API URL for email assets (logos, images in notification emails)
|
|
# OPTIONAL: when unset this is derived from the resolved public origin + /api,
|
|
# so the wizard's answer covers it. Set it only for split-origin deployments
|
|
# where the API lives on a different host than the gallery.
|
|
#API_URL=https://yourdomain.com/api
|
|
|
|
# Frontend API base
|
|
# For pre-built images and production behind a reverse proxy, keep '/api'.
|
|
# If you rebuild the frontend yourself, you may set a full URL at build time.
|
|
VITE_API_URL=/api
|
|
|
|
# Port Configuration (optional)
|
|
# BACKEND_PORT=3001
|
|
# FRONTEND_PORT=3000
|
|
# DB_PORT=5432
|
|
# REDIS_PORT=6379
|
|
|
|
# File watcher (watch-folder auto-import, local storage only)
|
|
# Max photos processed in parallel — raise on hosts with memory headroom,
|
|
# lower to 1 on very small hosts. Default: 2
|
|
# FILE_WATCHER_CONCURRENCY=2
|
|
|
|
# Release Channel
|
|
# Options: 'stable' (default), 'beta', or specific version like 'v2.3.0'
|
|
# 'stable' uses the :stable tag (same as :latest on main)
|
|
# 'beta' uses the :beta tag for pre-release versions
|
|
PICPEAK_CHANNEL=stable
|
|
|
|
# Update Check Configuration
|
|
# Set to 'false' to disable update notifications in admin UI
|
|
UPDATE_CHECK_ENABLED=true
|
|
|
|
# Timezone
|
|
TZ=UTC
|
|
|
|
# Analytics (Optional - Umami)
|
|
VITE_UMAMI_URL=
|
|
VITE_UMAMI_WEBSITE_ID=
|
|
VITE_UMAMI_SHARE_URL=
|
|
|
|
# Storage variables (host paths)
|
|
# These control where data is stored on the host. Defaults are local folders.
|
|
APP_STORAGE=./storage
|
|
APP_DATA=./data
|
|
LOGS=./logs
|
|
|
|
# ─── Storage Backend ────────────────────────────────────────────────────────
|
|
# PicPeak can store photos, thumbnails and archive zips on the local filesystem
|
|
# (default) or on any S3-compatible object store (AWS S3, MinIO, Cloudflare R2,
|
|
# Backblaze B2, Wasabi, DigitalOcean Spaces, …).
|
|
#
|
|
# STORAGE_BACKEND=local (default)
|
|
# Uses STORAGE_PATH on the local filesystem. Backwards compatible — every
|
|
# existing deployment keeps working unchanged.
|
|
#
|
|
# STORAGE_BACKEND=s3
|
|
# Reads STORAGE_S3_* below. Auto-import via the filesystem watcher is
|
|
# disabled in this mode (S3 has no inotify) — every photo must enter via the
|
|
# admin upload UI/API. Run `node backend/scripts/migrate-storage.js` to copy
|
|
# existing local content to S3 before flipping the env.
|
|
#
|
|
# STORAGE_BACKEND=local
|
|
#
|
|
# STORAGE_S3_BUCKET=picpeak
|
|
# STORAGE_S3_REGION=us-east-1
|
|
# STORAGE_S3_ACCESS_KEY=AKIAxxxxxxxxxxxxxxxx
|
|
# STORAGE_S3_SECRET_KEY=xxxxxxxxxxxxxxxxxxxxxxxx
|
|
# Custom endpoint — set this for MinIO / R2 / B2 / Spaces. Leave unset for AWS.
|
|
# STORAGE_S3_ENDPOINT=https://s3.us-west-002.backblazeb2.com
|
|
# Optional namespace prefix inside the bucket — useful for multi-deployment buckets.
|
|
# STORAGE_S3_PREFIX=picpeak
|
|
# STORAGE_S3_FORCE_PATH_STYLE=false # MinIO needs true; auto-on when endpoint is set
|
|
# STORAGE_S3_SSL=true
|
|
#
|
|
# Minimum IAM policy (AWS S3) for the bucket above:
|
|
# {
|
|
# "Version": "2012-10-17",
|
|
# "Statement": [{
|
|
# "Effect": "Allow",
|
|
# "Action": [
|
|
# "s3:GetObject", "s3:PutObject", "s3:DeleteObject",
|
|
# "s3:ListBucket", "s3:GetBucketLocation"
|
|
# ],
|
|
# "Resource": [
|
|
# "arn:aws:s3:::picpeak",
|
|
# "arn:aws:s3:::picpeak/*"
|
|
# ]
|
|
# }]
|
|
# }
|
|
#
|
|
# EXTERNAL_MEDIA_ROOT (above) always lives on the local filesystem regardless
|
|
# of STORAGE_BACKEND — reference-mode galleries are not migrated to S3 in v1.
|
|
|
|
# ─── Outbound Webhooks (#327) ────────────────────────────────────────────────
|
|
# PicPeak POSTs event/photo lifecycle notifications to URLs you configure
|
|
# under Settings → Webhooks. Each delivery is signed HMAC-SHA256 with a
|
|
# per-webhook secret in the X-PicPeak-Signature header.
|
|
#
|
|
# WEBHOOK_ALLOW_PRIVATE_URLS (default: false)
|
|
# Block URLs resolving to private IPs / loopback / .local etc. as an
|
|
# SSRF mitigation. Set to "true" ONLY in dev when your receiver is on
|
|
# the same docker network or localhost. Production deployments must
|
|
# leave this OFF.
|
|
# WEBHOOK_ALLOW_PRIVATE_URLS=false
|
|
#
|
|
# WEBHOOK_DELIVERY_INTERVAL_MS (default: 5000)
|
|
# How often the worker polls webhook_deliveries for pending rows.
|
|
# WEBHOOK_DELIVERY_INTERVAL_MS=5000
|
|
#
|
|
# WEBHOOK_DELIVERY_CONCURRENCY (default: 5)
|
|
# Maximum in-flight deliveries per worker tick. One slow consumer can
|
|
# monopolize all 5 slots — bump this if your receivers are slow OR ship
|
|
# a separate webhook-only deployment.
|
|
# WEBHOOK_DELIVERY_CONCURRENCY=5
|
|
#
|
|
# WEBHOOK_HTTP_TIMEOUT_MS (default: 10000)
|
|
# Per-request timeout. Beyond this, the delivery is recorded as a
|
|
# network error and retried.
|
|
# WEBHOOK_HTTP_TIMEOUT_MS=10000
|
|
#
|
|
# WEBHOOK_MAX_ATTEMPTS (default: 5)
|
|
# Total attempts before a delivery is marked failed. Backoff between
|
|
# attempts is exponential: 1m, 5m, 30m, 2h, 12h.
|
|
# WEBHOOK_MAX_ATTEMPTS=5
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Face recognition — "People in this gallery" (#1074, optional)
|
|
# -----------------------------------------------------------------------------
|
|
# Requires the optional picpeak-ml sidecar container:
|
|
# docker compose --profile faces up -d
|
|
#
|
|
# NONE of these variables do anything until the `faces` feature flag is
|
|
# enabled in Admin → Settings, AND the per-event "Detect people in this
|
|
# gallery" toggle is switched on. Both default to OFF. With the flag off the
|
|
# backend never contacts the sidecar, so leaving these at their defaults on an
|
|
# install without the container is completely inert.
|
|
#
|
|
# Face embeddings are biometric data (GDPR Art. 9 special category in the EU).
|
|
# The photographer is the controller and needs a lawful basis for the people
|
|
# in their photos — read https://docs.picpeak.app/features/face-recognition
|
|
# before enabling.
|
|
#
|
|
# NOT AVAILABLE ON THE ALL-IN-ONE IMAGE. The single-container build sets
|
|
# PICPEAK_SINGLE_CONTAINER=true and the backend refuses to enable face
|
|
# recognition there regardless of these variables or the feature flag: that
|
|
# image runs the backend, frontend, database and every worker in one
|
|
# container, with no ML sidecar to talk to, and face detection would compete
|
|
# with image processing for the same CPU and memory. Use the standard
|
|
# multi-container deployment if you want this feature.
|
|
#
|
|
# FACE_ML_TOKEN (no default — REQUIRED to run the sidecar)
|
|
# Shared secret between the backend and the sidecar. The sidecar refuses to
|
|
# start without it rather than serving anonymously, so an accidentally
|
|
# published port is never a free face-detection API. Generate with:
|
|
# openssl rand -hex 32
|
|
# FACE_ML_TOKEN=
|
|
#
|
|
# FACE_ML_URL (default: http://picpeak-ml:8000)
|
|
# Defaults to the sidecar's compose service name, so the standard
|
|
# deployment needs no configuration here. Only change it if you run the
|
|
# sidecar outside the default compose network.
|
|
# FACE_ML_URL=http://picpeak-ml:8000
|
|
#
|
|
# FACE_PROCESSOR_CONCURRENCY (default: 1)
|
|
# Face-detection workers in the backend. Defaults to 1 deliberately: face
|
|
# scanning shares a host with Sharp image processing, which is the real
|
|
# memory pressure (see UPLOAD_PROCESSOR_CONCURRENCY). Raise only on hosts
|
|
# with headroom to spare.
|
|
# FACE_PROCESSOR_CONCURRENCY=1
|
|
#
|
|
# FACE_ORT_THREADS (default: 1)
|
|
# ONNX Runtime threads inside the sidecar. More threads mean faster
|
|
# per-photo inference and higher RSS.
|
|
# FACE_ORT_THREADS=1
|
|
|
|
# Note on FRONTEND_API_URL (documentation only):
|
|
# When using pre-built frontend images, runtime env vars cannot override the built JS.
|
|
# Do NOT rely on FRONTEND_API_URL in Compose. Instead, keep VITE_API_URL=/api and
|
|
# let the frontend Nginx proxy /api to the backend. Only if you rebuild the frontend
|
|
# should you change VITE_API_URL at build time.
|