72e2ef6721
Backend for #738. Real TOTP 2FA for admin accounts, all roles incl. super_admin (closes #735). - mfaService: otplib TOTP; AES-256-GCM encryption of the secret at rest (key derived from MFA_ENCRYPTION_KEY or JWT_SECRET); bcrypt-hashed, single-use recovery codes; otpauth URI + QR. - Migration 151: adds two_factor_recovery_codes + two_factor_enrolled_at (secret/enabled columns already existed from legacy 016). - Enrollment endpoints (behind adminAuth, per-user): GET /mfa/status, POST /mfa/{setup,enable,disable,recovery-codes}. Disable/regenerate require a current code so a hijacked session can't strip 2FA. - Login challenge: /admin/login returns {mfaRequired, mfaToken} (no session) when 2FA is on; /admin/login/mfa exchanges a TOTP or recovery code for the session. Lockout counter is NOT reset until the second factor passes, so MFA brute-force is rate-limited too. - CLI break-glass: scripts/reset-admin-mfa.js --email <e> | --all --yes, audit-logged, matches reset-admin-password.js convention. - Docs + optional MFA_ENCRYPTION_KEY env. Verified end-to-end on a live backend: enroll (super_admin), challenge, TOTP + single-use recovery login, disable, and CLI reset.
116 lines
4.5 KiB
Bash
116 lines
4.5 KiB
Bash
# Backend Environment Variables Example
|
|
# Copy this file to .env and update with your values
|
|
|
|
# Application
|
|
NODE_ENV=production
|
|
PORT=3001
|
|
|
|
# Security
|
|
# Generate with: openssl rand -base64 32
|
|
JWT_SECRET=your-very-secure-jwt-secret-at-least-32-characters-long-example123456
|
|
|
|
# Admin 2FA (TOTP) secret encryption key — OPTIONAL.
|
|
# Admin authenticator secrets are encrypted at rest (AES-256-GCM). By default
|
|
# the key is derived from JWT_SECRET, so you do NOT need to set this. Set it
|
|
# only if you want the MFA encryption key decoupled from JWT_SECRET (e.g. so
|
|
# rotating JWT_SECRET doesn't invalidate enrolled authenticators). If you set
|
|
# it, changing/losing it makes existing 2FA secrets undecryptable — recover
|
|
# with: docker compose exec backend node scripts/reset-admin-mfa.js --all --yes
|
|
# Generate with: openssl rand -base64 32
|
|
#MFA_ENCRYPTION_KEY=
|
|
|
|
# Auth cookie Secure flag
|
|
# unset - default: 'auto' in production, false in dev (#427)
|
|
# true - always set Secure (HTTPS-only cookies; breaks plain-HTTP access —
|
|
# login appears to succeed but the browser silently drops the
|
|
# cookie, leaving you in a redirect loop. Only set this if you
|
|
# ALWAYS reach the site via HTTPS)
|
|
# false - never set Secure (allows HTTP; cookies not protected on HTTPS)
|
|
# auto - decide per request: Secure on HTTPS, not on HTTP. Reads
|
|
# req.secure from Express which respects X-Forwarded-Proto from a
|
|
# trusted reverse proxy. This is the default and is the right
|
|
# choice for most deployments.
|
|
#
|
|
# Why 'auto' is the default in production:
|
|
# - On real HTTPS (reverse proxy with X-Forwarded-Proto), req.secure is
|
|
# true → Secure flag is still emitted. No security regression vs. true.
|
|
# - On plain HTTP (LAN access, first-time install before reverse proxy is
|
|
# wired up), req.secure is false → Secure flag is omitted → login works
|
|
# instead of silently looping back to /admin/login.
|
|
#
|
|
# When you'd set this explicitly:
|
|
# - COOKIE_SECURE=true → strict HTTPS-only deployments where you want
|
|
# defense in depth against accidentally serving over HTTP.
|
|
# - COOKIE_SECURE=false → you intentionally only ever serve over HTTP and
|
|
# don't want the per-request check (rare).
|
|
#
|
|
# Requirements for 'auto' mode to detect HTTPS correctly:
|
|
# 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
|
|
|
|
# URLs (adjust for your domain)
|
|
ADMIN_URL=https://photos.example.com
|
|
FRONTEND_URL=https://photos.example.com
|
|
BACKEND_URL=https://photos.example.com # Or https://api.photos.example.com if separate
|
|
|
|
# API URL for email assets (logos, images in emails)
|
|
# This must be the publicly accessible URL where recipients can load images
|
|
# If not set, defaults to http://localhost:3001 which will break images in production emails
|
|
API_URL=https://photos.example.com/api
|
|
|
|
# Database Configuration
|
|
DATABASE_CLIENT=pg
|
|
DB_HOST=localhost
|
|
DB_PORT=5432
|
|
DB_USER=picpeak
|
|
DB_PASSWORD=your-secure-database-password-change-this
|
|
DB_NAME=picpeak
|
|
|
|
# Email Configuration (Examples for common providers)
|
|
# Gmail example:
|
|
# SMTP_HOST=smtp.gmail.com
|
|
# SMTP_PORT=587
|
|
# SMTP_SECURE=false
|
|
# SMTP_USER=your-email@gmail.com
|
|
# SMTP_PASS=your-app-specific-password
|
|
|
|
# SendGrid example:
|
|
SMTP_HOST=smtp.sendgrid.net
|
|
SMTP_PORT=587
|
|
SMTP_SECURE=false
|
|
SMTP_USER=apikey
|
|
SMTP_PASS=your-sendgrid-api-key
|
|
EMAIL_FROM=noreply@example.com
|
|
|
|
# Storage Paths
|
|
# IMPORTANT: STORAGE_PATH must be set to avoid file path resolution issues
|
|
# Docker deployment:
|
|
STORAGE_PATH=/app/storage
|
|
EVENTS_PATH=/app/storage/events
|
|
ARCHIVE_PATH=/app/storage/events/archived
|
|
|
|
# Local development:
|
|
# STORAGE_PATH=./storage
|
|
# EVENTS_PATH=./storage/events
|
|
# ARCHIVE_PATH=./storage/events/archived
|
|
|
|
# Analytics Backend Configuration (OPTIONAL)
|
|
# Used for server-side tracking only
|
|
# Primary configuration should be done through Admin UI > Settings > Analytics
|
|
# UMAMI_URL=https://analytics.example.com
|
|
# UMAMI_WEBSITE_ID=b4d3c2a1-5678-90ab-cdef-1234567890ab
|
|
|
|
# Logging
|
|
LOG_LEVEL=info |