Files
API/.env.example
T
Paddy e62b46945a Add a CLIENT_IP_HEADERS escape hatch and stop the auth handler hanging
Two changes, both from working out how to verify the client-IP configuration
on the Plesk vhost before deploying.

CLIENT_IP_HEADERS=none now trusts no header at all. This covers the one case
where the wrong setting is worse than no setting: if the proxy does not
overwrite the header we trust, any client can send it and mint itself an
unlimited brute-force budget against /sign-in. Falling back to the shared
rate-limit bucket is a nuisance - one noisy client can lock everyone out for
ten seconds at a time - but it fails closed, and it can be reverted from the
environment without a deploy. An empty or unset value still means "use the
default": a stray blank line in a .env must not silently change how requests
are bucketed, so only the explicit word does that. An empty array is what
better-auth reads as "no headers"; it falls back to its own default only when
the option is absent, and [] is truthy.

The TRUSTED_PROXY_IPS boot warning was overstating the risk. It now says that
a single-value header needs no trusted proxies, so seeing it on a plain
single-proxy setup is expected rather than a problem to chase.

Separately: a rejected promise from better-auth's handler used to escape as an
unhandled rejection, leaving the request hanging forever with no response while
the process logged an uncaughtException. Express 4 does not await an async
handler, and nearly every better-auth route touches the admin database, so any
database blip would have done this. Found by pointing ADMIN_DB at a database
the user cannot open while testing the hatch. The handler now answers 503, so
the caller learns and the other domains keep serving; verified as a 24ms
response instead of a hang, with / and /feedback/admin/me unaffected.

test/admin/admin.config.test.ts is new: it pins both branches of the hatch and
the rule that an unset NODE_ENV counts as production. It stubs dotenv, because
admin.config would otherwise read the repo's own .env and quietly reintroduce
NODE_ENV=development - the exact value several of those cases exist to remove.

157 unit tests and 41 integration tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 12:39:33 +02:00

72 lines
2.7 KiB
Bash

# Values containing #, ", \ or surrounding spaces must be single-quoted
# (dotenv 16 treats an unquoted # as a comment): DB_PASSWORD='abc#def'
# REQUIRED. The admin module treats anything other than "development" or "test"
# as production: strict secrets, cross-subdomain cookies, no relaxed CORS.
# Leaving it unset is therefore safe-by-default but will refuse to boot without
# the admin secrets below. Set it to development for local work.
NODE_ENV=development
PORT=3000
DB_HOST=
DB_USER=
DB_PASSWORD=
EMAIL_HOST=
EMAIL_USERNAME=
EMAIL_PASSWORD=
CALENDAR_DB=
FEEDBACK_DB=
FEEDBACK_IP_SALT=
FEEDBACK_RATE_LIMIT_MAX=5
FEEDBACK_RATE_LIMIT_WINDOW_MIN=10
SALESFORCE_ENABLED=false
SALESFORCE_API_URL=
SALESFORCE_CLIENT_ID=
SALESFORCE_CLIENT_SECRET=
TICKETS_DB=
TICKETS_RATE_LIMIT_MAX=10
TICKETS_RATE_LIMIT_WINDOW_MIN=10
ADMIN_DB=
# 32+ random bytes, e.g. `openssl rand -base64 48`. Mandatory outside
# development/test - there is deliberately no fallback, since a hardcoded one
# would be a published signing key. Rotating it signs everyone out and
# invalidates outstanding password-reset links.
BETTER_AUTH_SECRET=
API_BASE_URL=http://localhost:3000
ADMIN_APP_URL=http://localhost:3002
# Comma-separated origins of the apps that may call /admin/* with credentials.
APP_ORIGINS=http://localhost:3001
# nachklang.art in production; passkeys are bound to this value.
PASSKEY_RP_ID=localhost
# On start-up, makes sure this address can get in (invite, or grant admin if the
# user already exists). Idempotent, safe to leave set.
ADMIN_BOOTSTRAP_EMAIL=
# The header the reverse proxy puts the real client IP in, and the proxy hops to
# trust. Get these right or better-auth cannot resolve a client IP and every
# request shares ONE rate-limit bucket (/sign-in/* allows 3 per 10 seconds, so
# one noisy client locks everyone out). Check with:
# SELECT `key` FROM rateLimit; -- a "no-trusted-ip" row means it is happening.
# The header the reverse proxy puts the real client IP in. Must be one the proxy
# actually overwrites - trusting a header it does not set lets any client send its
# own value and bypass the sign-in rate limit entirely.
# Set to "none" to trust no header at all: every request then shares one rate-limit
# bucket, which is the safe fallback if the check below fails. Verify after deploy
# with: SELECT ipAddress FROM session ORDER BY createdAt DESC LIMIT 3;
CLIENT_IP_HEADERS=x-real-ip
TRUSTED_PROXY_IPS=
# Writes invitation links to the log. That link is a live account-creation
# credential, so this is refused outside development. Needed locally, where the
# mail relay is off and only the token's hash is stored.
ADMIN_LOG_INVITE_LINKS=true
MEMBER_CREDENTIAL=123
CHOIR_CREDENTIAL=123
MANAGEMENT_CREDENTIAL=123