Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e8d5f93c9 | ||
|
|
a0ccce04a3 | ||
|
|
fbb84b037c | ||
|
|
2dfe8d32c4 | ||
|
|
305499ee91 | ||
|
|
c2112bf707 |
@@ -9,6 +9,22 @@ and this project follows [Semantic Versioning](https://semver.org/).
|
|||||||
|
|
||||||
### Fixed
|
### 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
|
||||||
|
|
||||||
- The server used to hard-crash (`process.exit(1)`) on any unhandled promise rejection, so a single missed error handler anywhere in the app's fire-and-forget notification code (email/WhatsApp sending) could take the whole server down. It now logs the error and keeps running.
|
- The server used to hard-crash (`process.exit(1)`) on any unhandled promise rejection, so a single missed error handler anywhere in the app's fire-and-forget notification code (email/WhatsApp sending) could take the whole server down. It now logs the error and keeps running.
|
||||||
- Accounts created on someone's behalf (at-the-door walk-in registration, or manual registration from the Admin/Supervisor dashboard) now get their activation link (email or WhatsApp, whichever they have) sent immediately when the account is created, instead of only on their first failed login attempt — matching what the Terms of Use already promised.
|
- Accounts created on someone's behalf (at-the-door walk-in registration, or manual registration from the Admin/Supervisor dashboard) now get their activation link (email or WhatsApp, whichever they have) sent immediately when the account is created, instead of only on their first failed login attempt — matching what the Terms of Use already promised.
|
||||||
- Manual registration with a real email address used to create the account already active with a fixed, undisclosed password (`Hope123`) — the visitor had no way to know it. That account is now created inactive and gets the same immediate activation link, so the visitor sets their own password — unless a password was supplied directly (see below), in which case it's activated immediately with no link needed.
|
- Manual registration with a real email address used to create the account already active with a fixed, undisclosed password (`Hope123`) — the visitor had no way to know it. That account is now created inactive and gets the same immediate activation link, so the visitor sets their own password — unless a password was supplied directly (see below), in which case it's activated immediately with no link needed.
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ JWT_SECRET=your_jwt_secret_here_minimum_32_characters
|
|||||||
PORT=5000
|
PORT=5000
|
||||||
NODE_ENV=development
|
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 ─────────────────────────────────────────────────────────────────────
|
# ─── CORS ─────────────────────────────────────────────────────────────────────
|
||||||
# Comma-separated list of allowed frontend origins
|
# Comma-separated list of allowed frontend origins
|
||||||
FRONTEND_URL=http://localhost:3000
|
FRONTEND_URL=http://localhost:3000
|
||||||
|
|||||||
@@ -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) |
|
| `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 |
|
| `BACKEND_URL` | — | Public backend URL — used to serve ticket PDFs over WhatsApp |
|
||||||
| `PORT` | — | Port to listen on (default `3000`) |
|
| `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` |
|
| `NODE_ENV` | — | `production` or `development` |
|
||||||
| `WAWP_ACCESS_TOKEN` | — | WAWP fallback token (preferred: set via Admin → Site Settings) |
|
| `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) |
|
| `WAWP_INSTANCE_ID` | — | WAWP fallback instance ID (preferred: set via Admin → Site Settings) |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "event-management-backend",
|
"name": "event-management-backend",
|
||||||
"version": "1.9.2",
|
"version": "1.9.4",
|
||||||
"description": "Event Management System Backend",
|
"description": "Event Management System Backend",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1696,9 +1696,20 @@ const scheduleWhatsappEventAttendees = async (req, res) => {
|
|||||||
* @route GET /api/events/by-alias/:redirectUrl
|
* @route GET /api/events/by-alias/:redirectUrl
|
||||||
* @access Public
|
* @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 getEventByAlias = async (req, res) => {
|
||||||
const { redirectUrl } = req.params;
|
const { redirectUrl } = req.params;
|
||||||
|
|
||||||
|
if (!VALID_ALIAS.test(redirectUrl)) {
|
||||||
|
return res.status(404).json({ message: 'Event not found' });
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const event = await prisma.event.findFirst({
|
const event = await prisma.event.findFirst({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -18,6 +18,22 @@ const prisma = new PrismaClient();
|
|||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
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
|
// CORS — allow only the configured frontend origin
|
||||||
const allowedOrigins = (process.env.FRONTEND_URL || 'http://localhost:3000')
|
const allowedOrigins = (process.env.FRONTEND_URL || 'http://localhost:3000')
|
||||||
.split(',')
|
.split(',')
|
||||||
|
|||||||
@@ -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,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hope-events-frontend",
|
"name": "hope-events-frontend",
|
||||||
"version": "1.9.2",
|
"version": "1.9.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
|
|||||||
@@ -3,16 +3,26 @@ import { apiFetch } from "@/lib/api";
|
|||||||
|
|
||||||
export const revalidate = 60;
|
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 }> }) {
|
export default async function EventRedirectPage({ params }: { params: Promise<{ redirectUrl: string }> }) {
|
||||||
const { redirectUrl } = await params;
|
const { redirectUrl } = await params;
|
||||||
|
|
||||||
let event: any = null;
|
let event: any = null;
|
||||||
|
|
||||||
|
if (VALID_ALIAS.test(redirectUrl)) {
|
||||||
try {
|
try {
|
||||||
event = await apiFetch<any>(`/api/events/by-alias/${redirectUrl}`);
|
event = await apiFetch<any>(`/api/events/by-alias/${redirectUrl}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch event:", error);
|
console.error("Failed to fetch event:", error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!event || event.message?.toLowerCase().includes("not found")) {
|
if (!event || event.message?.toLowerCase().includes("not found")) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hope-events",
|
"name": "hope-events",
|
||||||
"version": "1.9.2",
|
"version": "1.9.4",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev:backend": "cd backend && npm run dev",
|
"dev:backend": "cd backend && npm run dev",
|
||||||
|
|||||||
Reference in New Issue
Block a user