Compare commits

...
6 Commits
Author SHA1 Message Date
joshua 98ac26bf70 Bump version to 1.9.5 2026-08-27 09:35:58 +02:00
joshua 7e235637c4 Merge branch 'fix/alias-route-bot-traffic-oom' into main 2026-08-27 09:35:33 +02:00
joshuaandClaude Sonnet 5 0e8d5f93c9 Reject bot-probe paths on the event alias route before hitting the DB
The public [redirectUrl] catch-all route (and its backend counterpart,
GET /api/events/by-alias/:redirectUrl) matched any unmatched top-level
path, so routine bot/scanner traffic (/wp-login.php, /.env, etc.) was
firing a live database query on every hit. That traffic pattern looks
like the cause of the P1017 "server has closed the connection" storms
and OOM crashes seen from v1.7 onward. Both now reject anything that
isn't a plausible alias (letters/numbers/hyphens/underscores) before
touching Prisma.

Also adds a max_memory_restart safety net to PM2 for both processes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 09:33:23 +02:00
joshua a0ccce04a3 Run frontend PM2 process on port 3000 2026-08-26 14:38:29 +02:00
joshua fbb84b037c Add TRUST_PROXY env var for reverse-proxy deployments
Fixes express-rate-limit's ERR_ERL_UNEXPECTED_X_FORWARDED_FOR warning
and incorrect IP keying when nginx runs on a separate server in front
of the app.
2026-08-26 14:36:51 +02:00
joshua 2dfe8d32c4 Add PM2 ecosystem config for production deployment 2026-08-26 11:59:08 +02:00
10 changed files with 87 additions and 7 deletions
+16
View File
@@ -7,6 +7,22 @@ and this project follows [Semantic Versioning](https://semver.org/).
## [Unreleased]
## [1.9.5] - 2026-08-27
### Fixed
- The public event-alias route (`/:redirectUrl`, used for short links like `/camp-2025`) is a catch-all matching any unmatched top-level path, so every bot/scanner probe for a nonexistent page (`/wp-login.php`, `/.env`, etc.) was hitting the backend and firing a live database query. Both the frontend route and the `GET /api/events/by-alias/:redirectUrl` endpoint now reject anything that isn't a plausible alias (letters/numbers/hyphens/underscores) before touching the database, instead of forwarding scanner noise straight through — this traffic pattern could exhaust the database connection pool and take the server down under load.
### Added
- PM2 (`ecosystem.config.js`) now restarts either process if it exceeds 500MB of memory, instead of letting an unbounded leak run until the OS OOM-kills it.
## [1.9.4] - 2026-08-26
### Added
- New `TRUST_PROXY` backend env var — set it when the app runs behind a reverse proxy (e.g. nginx on a separate server) so rate limiting reads the real client IP from `X-Forwarded-For` instead of the proxy's. Accepts a hop count, `true`/`false`, or trusted proxy IP(s)/CIDR(s).
## [1.9.3] - 2026-08-26
### Fixed
+7
View File
@@ -8,6 +8,13 @@ JWT_SECRET=your_jwt_secret_here_minimum_32_characters
PORT=5000
NODE_ENV=development
# Set this if the app runs behind a reverse proxy (e.g. nginx on a separate
# server) so rate limiting reads the real client IP instead of the proxy's.
# Accepts a hop count ("1"), "true"/"false", or comma-separated IP(s)/CIDR(s)
# of your trusted proxy (e.g. "10.0.0.5" or "10.0.0.0/8"). Leave unset if the
# app is not behind a proxy.
# TRUST_PROXY=1
# ─── CORS ─────────────────────────────────────────────────────────────────────
# Comma-separated list of allowed frontend origins
FRONTEND_URL=http://localhost:3000
+1
View File
@@ -84,6 +84,7 @@ Create `backend/.env` from `.env.example`. The only variables you must set are:
| `APP_BASE_URL` | — | Public frontend URL — used in email links (fallback if not set via admin panel) |
| `BACKEND_URL` | — | Public backend URL — used to serve ticket PDFs over WhatsApp |
| `PORT` | — | Port to listen on (default `3000`) |
| `TRUST_PROXY` | — | Set when running behind a reverse proxy (e.g. nginx on a separate server), so `req.ip`/`X-Forwarded-For` are read correctly by rate limiting. Accepts a hop count (`1`), `true`/`false`, or comma-separated trusted proxy IP(s)/CIDR(s). |
| `NODE_ENV` | — | `production` or `development` |
| `WAWP_ACCESS_TOKEN` | — | WAWP fallback token (preferred: set via Admin → Site Settings) |
| `WAWP_INSTANCE_ID` | — | WAWP fallback instance ID (preferred: set via Admin → Site Settings) |
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "event-management-backend",
"version": "1.9.3",
"version": "1.9.5",
"description": "Event Management System Backend",
"main": "src/index.js",
"scripts": {
@@ -1696,9 +1696,20 @@ const scheduleWhatsappEventAttendees = async (req, res) => {
* @route GET /api/events/by-alias/:redirectUrl
* @access Public
*/
// Aliases are admin-set slugs (e.g. "camp-2025") — see the event wizard's "URL
// Alias" field. This endpoint is public and also the target of the frontend's
// catch-all [redirectUrl] route, so it's what every bot/scanner probe hitting
// an unmatched top-level path (/wp-login.php, /.env, etc.) ends up calling.
// Rejecting non-slug-shaped values here skips a DB round-trip for that traffic.
const VALID_ALIAS = /^[a-zA-Z0-9_-]{1,100}$/;
const getEventByAlias = async (req, res) => {
const { redirectUrl } = req.params;
if (!VALID_ALIAS.test(redirectUrl)) {
return res.status(404).json({ message: 'Event not found' });
}
try {
const event = await prisma.event.findFirst({
where: {
+16
View File
@@ -18,6 +18,22 @@ const prisma = new PrismaClient();
const app = express();
const PORT = process.env.PORT || 3000;
// Trust proxy — required when running behind a reverse proxy (e.g. nginx on a
// separate server) so req.ip / X-Forwarded-For are read correctly by
// express-rate-limit and friends. Accepts a hop count ("1"), "true"/"false",
// or a comma-separated list of trusted proxy IPs/CIDRs.
if (process.env.TRUST_PROXY) {
const raw = process.env.TRUST_PROXY.trim();
let trustProxyValue;
if (raw === 'true') trustProxyValue = true;
else if (raw === 'false') trustProxyValue = false;
else if (/^\d+$/.test(raw)) trustProxyValue = parseInt(raw, 10);
else if (raw.includes(',')) trustProxyValue = raw.split(',').map((s) => s.trim());
else trustProxyValue = raw;
app.set('trust proxy', trustProxyValue);
console.log(`[startup] trust proxy set to: ${JSON.stringify(trustProxyValue)}`);
}
// CORS — allow only the configured frontend origin
const allowedOrigins = (process.env.FRONTEND_URL || 'http://localhost:3000')
.split(',')
+19
View File
@@ -0,0 +1,19 @@
module.exports = {
apps: [
{
name: 'hope-events-backend',
cwd: __dirname + '/backend',
script: 'src/index.js',
env: { NODE_ENV: 'production' },
max_memory_restart: '500M',
},
{
name: 'hope-events-frontend',
cwd: __dirname + '/frontend',
script: 'npm',
args: 'start -- -p 3000',
env: { NODE_ENV: 'production' },
max_memory_restart: '500M',
},
],
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hope-events-frontend",
"version": "1.9.3",
"version": "1.9.5",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
+14 -4
View File
@@ -3,15 +3,25 @@ import { apiFetch } from "@/lib/api";
export const revalidate = 60;
// Event aliases are admin-set slugs (e.g. "camp-2025", "movie-night") — see the
// "URL Alias" field in the event wizard. This catch-all route matches *any*
// unmatched top-level path, so it's also what every bot/scanner probe hits
// (/wp-login.php, /.env, /xmlrpc.php, etc.). Rejecting anything that isn't a
// plausible slug here skips a live DB query for that background noise instead
// of forwarding it straight to the backend.
const VALID_ALIAS = /^[a-zA-Z0-9_-]{1,100}$/;
export default async function EventRedirectPage({ params }: { params: Promise<{ redirectUrl: string }> }) {
const { redirectUrl } = await params;
let event: any = null;
try {
event = await apiFetch<any>(`/api/events/by-alias/${redirectUrl}`);
} catch (error) {
console.error("Failed to fetch event:", error);
if (VALID_ALIAS.test(redirectUrl)) {
try {
event = await apiFetch<any>(`/api/events/by-alias/${redirectUrl}`);
} catch (error) {
console.error("Failed to fetch event:", error);
}
}
if (!event || event.message?.toLowerCase().includes("not found")) {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hope-events",
"version": "1.9.3",
"version": "1.9.5",
"main": "index.js",
"scripts": {
"dev:backend": "cd backend && npm run dev",