Signing in showed the "Welcome back!" toast but never left /login. The session cookie was host-only on the API subdomain, so the Next middleware guard on the site origin saw no cookie and bounced /dashboard straight back to /login?redirect=/dashboard. - Add AUTH_COOKIE_DOMAIN, wiring Better Auth's crossSubDomainCookies so the cookie also reaches the site origin. Unset in dev, where localhost is single-host and must stay host-only. - Navigate after authentication with a full page load, via a shared authRedirect helper: only a top-level request carries the httpOnly cookie. Used by the login, register, magic-link and Google flows. - Show "Redirecting..." on the login and register pages and keep the submit button disabled until the browser replaces the page, instead of re-enabling it mid-navigation. - Guard against a redirect loop with a sessionStorage marker. A React ref cannot do this: the full page load resets component state. If the destination bounces back, explain it rather than navigating again. - Middleware: accept any *.session_token cookie so a cookiePrefix change cannot lock everyone out, and preserve the destination's query string. - Trust any loopback port in dev, so reaching the dev server through a forwarded port does not fail Better Auth's CSRF origin check. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
166 lines
6.7 KiB
Bash
166 lines
6.7 KiB
Bash
# Database Configuration
|
|
# Use 'sqlite' or 'postgres'
|
|
DB_TYPE=sqlite
|
|
|
|
# For SQLite (relative or absolute path)
|
|
DATABASE_URL=./data/spanglish.db
|
|
|
|
# For PostgreSQL
|
|
# DATABASE_URL=postgresql://user:password@localhost:5432/spanglish
|
|
|
|
# Max PostgreSQL connections per instance (default 10). When running multiple
|
|
# replicas, keep DB_POOL_MAX * replicas below the Postgres max_connections limit.
|
|
# DB_POOL_MAX=10
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Horizontal scaling (all optional)
|
|
# ---------------------------------------------------------------------------
|
|
# Leave everything below UNSET to run as a single instance with in-memory
|
|
# backends and local-disk uploads (zero-config, identical to the original
|
|
# behavior). Set them to run multiple API replicas behind a load balancer.
|
|
#
|
|
# Note: running more than one instance requires DB_TYPE=postgres. SQLite is a
|
|
# single local file and cannot be shared safely across instances.
|
|
|
|
# Redis connection URL. When set, the cache, rate limiter, login lockout,
|
|
# pub/sub (real-time payment events), distributed locks, and the email hourly
|
|
# cap are shared across all instances. When unset, each instance uses in-memory
|
|
# equivalents.
|
|
#
|
|
# In production always set a password (requirepass on the server) and put it in
|
|
# the URL. Use the rediss:// scheme for TLS (handled natively by the client),
|
|
# and an optional /N path to select a DB index when sharing a Redis instance
|
|
# with other applications.
|
|
# REDIS_URL=redis://localhost:6379
|
|
# REDIS_URL=redis://:your-redis-password@redis.internal:6379
|
|
# REDIS_URL=rediss://:your-redis-password@redis.example.com:6380/1
|
|
|
|
# Optional S3-compatible object storage for media uploads (e.g. Garage, MinIO,
|
|
# AWS S3). When S3_ENDPOINT and S3_BUCKET are set, uploads go to the bucket and
|
|
# are shared across instances. When unset, uploads are written to ./uploads on
|
|
# local disk (the default).
|
|
# S3_ENDPOINT=https://garage.example.com
|
|
# S3_REGION=garage
|
|
# S3_BUCKET=spanglish-media
|
|
# S3_ACCESS_KEY_ID=
|
|
# S3_SECRET_ACCESS_KEY=
|
|
# Public base URL used to build fileUrl for stored objects (CDN or web endpoint).
|
|
# If unset, a path-style URL against S3_ENDPOINT/S3_BUCKET is used.
|
|
# S3_PUBLIC_URL=https://media.example.com
|
|
# Use path-style addressing (true for Garage/MinIO). Defaults to true.
|
|
# S3_FORCE_PATH_STYLE=true
|
|
|
|
# Better Auth session secret. REQUIRED in production, 32+ characters.
|
|
# Generate one with: openssl rand -base64 48
|
|
# Rotating it signs everyone out (cookie signatures invalidate).
|
|
BETTER_AUTH_SECRET=
|
|
|
|
# Public site origin Better Auth builds its URLs against (falls back to
|
|
# FRONTEND_URL when unset). E.g. https://spanglishcommunity.com
|
|
BETTER_AUTH_URL=
|
|
|
|
# Session cookie domain, shared across subdomains. Set this when the API is served
|
|
# from a different host than the site (e.g. api.spanglishcommunity.com vs
|
|
# spanglishcommunity.com): without it the cookie is host-only and the frontend's
|
|
# Next middleware cannot see it, so /dashboard and /admin bounce back to /login.
|
|
# Must start with a dot. Leave EMPTY in development (localhost is single-host).
|
|
# E.g. .spanglishcommunity.com
|
|
AUTH_COOKIE_DOMAIN=
|
|
|
|
# Extra reverse-proxy IP prefixes allowed to set X-Real-IP / X-Forwarded-For
|
|
# (comma-separated, e.g. "172.20."). Loopback and RFC1918 ranges are always
|
|
# trusted; anything else is treated as a client and rate-limited by its
|
|
# actual socket address.
|
|
# TRUSTED_PROXIES=
|
|
|
|
# DEPRECATED: no longer used for API auth (Better Auth replaced the JWTs).
|
|
# Still read by photo-api as the fallback secret for gallery view tokens
|
|
# until PHOTO_VIEW_SECRET is set there; safe to remove after that.
|
|
JWT_SECRET=your-super-secret-key-change-in-production
|
|
|
|
# Google OAuth (optional - for Google Sign-In)
|
|
# Get your Client ID from: https://console.cloud.google.com/apis/credentials
|
|
# Note: The same Client ID should be used in frontend/.env
|
|
GOOGLE_CLIENT_ID=
|
|
# Only needed for the redirect OAuth flow; the Google Identity Services
|
|
# button (ID token sign-in) works with the Client ID alone.
|
|
GOOGLE_CLIENT_SECRET=
|
|
|
|
# Server Configuration
|
|
PORT=3001
|
|
API_URL=http://localhost:3001
|
|
FRONTEND_URL=http://localhost:3019
|
|
|
|
# Revalidation secret (shared with frontend for on-demand cache revalidation)
|
|
# Must match the REVALIDATE_SECRET in frontend/.env
|
|
REVALIDATE_SECRET=change-me-to-a-random-secret
|
|
|
|
# Payment Providers (optional)
|
|
STRIPE_SECRET_KEY=
|
|
STRIPE_WEBHOOK_SECRET=
|
|
MERCADOPAGO_ACCESS_TOKEN=
|
|
|
|
# LNbits Configuration (for Bitcoin Lightning payments)
|
|
# Get these from your LNbits instance
|
|
# URL should be like: https://lnbits.yourdomain.com or https://legend.lnbits.com
|
|
LNBITS_URL=
|
|
# Invoice/Read key from your LNbits wallet
|
|
LNBITS_API_KEY=
|
|
# Optional: webhook secret for additional verification
|
|
LNBITS_WEBHOOK_SECRET=
|
|
|
|
# Email Service
|
|
# Provider options: resend, smtp, console (console = log only, no actual email)
|
|
EMAIL_PROVIDER=console
|
|
EMAIL_FROM=
|
|
EMAIL_FROM_NAME=
|
|
|
|
# Resend Configuration (if EMAIL_PROVIDER=resend)
|
|
# Get your API key from https://resend.com
|
|
EMAIL_API_KEY=
|
|
# Alternative env name also supported:
|
|
# RESEND_API_KEY=
|
|
|
|
# SMTP Configuration (if EMAIL_PROVIDER=smtp)
|
|
# Works with any SMTP server: Gmail, SendGrid, Mailgun, Amazon SES, etc.
|
|
SMTP_HOST=mail.spango.lat
|
|
SMTP_PORT=465
|
|
SMTP_USER=
|
|
SMTP_PASS=
|
|
# Set to true for port 465 (implicit TLS), false for port 587 (STARTTLS)
|
|
SMTP_SECURE=true
|
|
# Set to false to allow self-signed certificates (not recommended for production)
|
|
SMTP_TLS_REJECT_UNAUTHORIZED=true
|
|
|
|
# Common SMTP examples:
|
|
# Gmail: SMTP_HOST=smtp.gmail.com, SMTP_PORT=587, use App Password for SMTP_PASS
|
|
# SendGrid: SMTP_HOST=smtp.sendgrid.net, SMTP_PORT=587, SMTP_USER=apikey, SMTP_PASS=your_api_key
|
|
# Mailgun: SMTP_HOST=smtp.mailgun.org, SMTP_PORT=587
|
|
# Amazon SES: SMTP_HOST=email-smtp.us-east-1.amazonaws.com, SMTP_PORT=587
|
|
|
|
# Email Queue Rate Limiting
|
|
# Maximum number of emails that can be sent per hour (default: 30)
|
|
# If the limit is reached, queued emails will pause and resume automatically
|
|
MAX_EMAILS_PER_HOUR=30
|
|
|
|
# Pending Booking Cleanup
|
|
# Pending bookings whose payment is still unpaid (not awaiting admin approval)
|
|
# are cancelled after this many minutes, freeing the seats (default: 30)
|
|
PENDING_BOOKING_TTL_MINUTES=30
|
|
# How often the cleanup job runs, in milliseconds (default: 300000 = 5 min)
|
|
PENDING_BOOKING_CLEANUP_INTERVAL_MS=300000
|
|
|
|
# Auto-Hold Sweep
|
|
# Bookings awaiting admin payment approval are put on hold (seat released) after
|
|
# this many hours with no confirmation (default: 72)
|
|
HOLD_THRESHOLD_HOURS=72
|
|
# How often the hold sweep job runs, in milliseconds (default: 900000 = 15 min)
|
|
HOLD_SWEEP_INTERVAL_MS=900000
|
|
|
|
# Ended-Event Payment Sweep
|
|
# Once an event is over, any unconfirmed payment (pending / pending_approval /
|
|
# on_hold) is auto-rejected and its ticket cancelled. No email is sent.
|
|
# How often this sweep runs, in milliseconds (default: 900000 = 15 min)
|
|
EVENT_END_SWEEP_INTERVAL_MS=900000
|
|
|