Prepare the four landings for production deployment
Fixes that block or weaken a real deployment. Nothing here changes the rendered pages. Bind the lead API to loopback. Each server called app.listen() without a host, so it bound 0.0.0.0. Combined with a blanket `trust proxy: true` — which makes Express take the leftmost X-Forwarded-For entry as req.ip — the in-memory lead rate limiter was spoofable by anyone who could reach the port directly. HOST now defaults to 127.0.0.1 and trust is narrowed to 'loopback', so a request arriving from anywhere but the local proxy has its forged header ignored. Give each landing its own port. All four .env files claimed PORT=3000, and fitnes/.env.example collided with medcenter/.env.example, so three of the four could never have started on one host. Now 3000/3001/3002/3003 consistently across the code defaults, the env templates and the vite dev proxies, so all four also run side by side locally. Template the JSON-LD url. canonical and og:url already resolved from %VITE_SITE_URL%, but the JSON-LD block hardcoded an exodevices.ru sub-path that would not follow the environment. All four now read from the same variable. Declare the Node version. Nothing stated it, yet transitive deps impose a >=22.12 floor (@rolldown/binding, yargs, concurrently). Added engines and .nvmrc so a too-old runtime fails clearly. Typechecked and production-built on all four; verified the socket binds 127.0.0.1 only, health reports amo:true, the site still boots with the CRM unconfigured, and the limiter returns 429 with Retry-After on the ninth request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,8 @@ VITE_BASE_PATH=/
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Server
|
# Server
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
# One port per landing so all four can run side by side:
|
||||||
|
# fitnes 3000, hotel 3001, medcenter 3002, medcenterpersonal 3003.
|
||||||
PORT=3000
|
PORT=3000
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
24
|
||||||
+1
-1
@@ -33,7 +33,7 @@
|
|||||||
"provider": { "@type": "Organization", "name": "Экзо Групп" },
|
"provider": { "@type": "Organization", "name": "Экзо Групп" },
|
||||||
"serviceType": "Проектирование и запуск зоны восстановления в фитнес-клубе",
|
"serviceType": "Проектирование и запуск зоны восстановления в фитнес-клубе",
|
||||||
"areaServed": "RU",
|
"areaServed": "RU",
|
||||||
"url": "https://exodevices.ru/fitness"
|
"url": "%VITE_SITE_URL%"
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.12"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently -n client,server -c cyan,magenta \"vite\" \"tsx watch server/src/index.ts\"",
|
"dev": "concurrently -n client,server -c cyan,magenta \"vite\" \"tsx watch server/src/index.ts\"",
|
||||||
"dev:client": "vite",
|
"dev:client": "vite",
|
||||||
|
|||||||
@@ -54,5 +54,8 @@ export function readAmoConfig(): AmoConfig | null {
|
|||||||
|
|
||||||
export const serverConfig = {
|
export const serverConfig = {
|
||||||
port: optionalNumber('PORT') ?? 3000,
|
port: optionalNumber('PORT') ?? 3000,
|
||||||
|
// Loopback by default: in production nginx is the only thing that should
|
||||||
|
// reach this process, so the port is never exposed to the network.
|
||||||
|
host: optional('HOST') ?? '127.0.0.1',
|
||||||
isProduction: process.env.NODE_ENV === 'production',
|
isProduction: process.env.NODE_ENV === 'production',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ if (!leadService) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
app.set('trust proxy', true)
|
// 'loopback', not true: only the local nginx hop is trusted, so req.ip is the
|
||||||
|
// address nginx actually observed and the rate limiter below cannot be
|
||||||
|
// side-stepped with a forged X-Forwarded-For header.
|
||||||
|
app.set('trust proxy', 'loopback')
|
||||||
app.use(express.json({ limit: '64kb' }))
|
app.use(express.json({ limit: '64kb' }))
|
||||||
|
|
||||||
const limiter = createRateLimiter({ limit: 8, windowMs: 10 * 60 * 1000 })
|
const limiter = createRateLimiter({ limit: 8, windowMs: 10 * 60 * 1000 })
|
||||||
@@ -103,7 +106,7 @@ if (serverConfig.isProduction) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
app.listen(serverConfig.port, () => {
|
app.listen(serverConfig.port, serverConfig.host, () => {
|
||||||
console.info(
|
console.info(
|
||||||
'[server] listening on http://localhost:%d%s',
|
'[server] listening on http://localhost:%d%s',
|
||||||
serverConfig.port,
|
serverConfig.port,
|
||||||
|
|||||||
+2
-1
@@ -10,7 +10,8 @@ VITE_BASE_PATH=/
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Server
|
# Server
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 3001 by default so this landing can run next to the fitness one (3000).
|
# One port per landing so all four can run side by side:
|
||||||
|
# fitnes 3000, hotel 3001, medcenter 3002, medcenterpersonal 3003.
|
||||||
PORT=3001
|
PORT=3001
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
24
|
||||||
+1
-1
@@ -37,7 +37,7 @@
|
|||||||
"provider": { "@type": "Organization", "name": "Экзо Групп" },
|
"provider": { "@type": "Organization", "name": "Экзо Групп" },
|
||||||
"serviceType": "Проектирование и запуск кабинета восстановления в отеле",
|
"serviceType": "Проектирование и запуск кабинета восстановления в отеле",
|
||||||
"areaServed": "RU",
|
"areaServed": "RU",
|
||||||
"url": "https://exodevices.ru/hotels"
|
"url": "%VITE_SITE_URL%"
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.12"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently -n client,server -c cyan,magenta \"vite\" \"tsx watch server/src/index.ts\"",
|
"dev": "concurrently -n client,server -c cyan,magenta \"vite\" \"tsx watch server/src/index.ts\"",
|
||||||
"dev:client": "vite",
|
"dev:client": "vite",
|
||||||
|
|||||||
@@ -54,5 +54,8 @@ export function readAmoConfig(): AmoConfig | null {
|
|||||||
|
|
||||||
export const serverConfig = {
|
export const serverConfig = {
|
||||||
port: optionalNumber('PORT') ?? 3001,
|
port: optionalNumber('PORT') ?? 3001,
|
||||||
|
// Loopback by default: in production nginx is the only thing that should
|
||||||
|
// reach this process, so the port is never exposed to the network.
|
||||||
|
host: optional('HOST') ?? '127.0.0.1',
|
||||||
isProduction: process.env.NODE_ENV === 'production',
|
isProduction: process.env.NODE_ENV === 'production',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ if (!leadService) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
app.set('trust proxy', true)
|
// 'loopback', not true: only the local nginx hop is trusted, so req.ip is the
|
||||||
|
// address nginx actually observed and the rate limiter below cannot be
|
||||||
|
// side-stepped with a forged X-Forwarded-For header.
|
||||||
|
app.set('trust proxy', 'loopback')
|
||||||
app.use(express.json({ limit: '64kb' }))
|
app.use(express.json({ limit: '64kb' }))
|
||||||
|
|
||||||
const limiter = createRateLimiter({ limit: 8, windowMs: 10 * 60 * 1000 })
|
const limiter = createRateLimiter({ limit: 8, windowMs: 10 * 60 * 1000 })
|
||||||
@@ -103,7 +106,7 @@ if (serverConfig.isProduction) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
app.listen(serverConfig.port, () => {
|
app.listen(serverConfig.port, serverConfig.host, () => {
|
||||||
console.info(
|
console.info(
|
||||||
'[server] listening on http://localhost:%d%s',
|
'[server] listening on http://localhost:%d%s',
|
||||||
serverConfig.port,
|
serverConfig.port,
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ VITE_BASE_PATH=/
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Server
|
# Server
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
PORT=3000
|
# One port per landing so all four can run side by side:
|
||||||
|
# fitnes 3000, hotel 3001, medcenter 3002, medcenterpersonal 3003.
|
||||||
|
PORT=3002
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# amoCRM — server-side only, never exposed to the browser
|
# amoCRM — server-side only, never exposed to the browser
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
24
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
"provider": { "@type": "Organization", "name": "Экзо Групп" },
|
"provider": { "@type": "Organization", "name": "Экзо Групп" },
|
||||||
"serviceType": "Аудит и модернизация кабинета физиотерапии медицинского центра",
|
"serviceType": "Аудит и модернизация кабинета физиотерапии медицинского центра",
|
||||||
"areaServed": "RU",
|
"areaServed": "RU",
|
||||||
"url": "https://exodevices.ru/medical-centers/with-physiotherapy"
|
"url": "%VITE_SITE_URL%"
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.12"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently -n client,server -c cyan,magenta \"vite\" \"tsx watch server/src/index.ts\"",
|
"dev": "concurrently -n client,server -c cyan,magenta \"vite\" \"tsx watch server/src/index.ts\"",
|
||||||
"dev:client": "vite",
|
"dev:client": "vite",
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ export function readAmoConfig(): AmoConfig | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const serverConfig = {
|
export const serverConfig = {
|
||||||
port: optionalNumber('PORT') ?? 3000,
|
port: optionalNumber('PORT') ?? 3002,
|
||||||
|
// Loopback by default: in production nginx is the only thing that should
|
||||||
|
// reach this process, so the port is never exposed to the network.
|
||||||
|
host: optional('HOST') ?? '127.0.0.1',
|
||||||
isProduction: process.env.NODE_ENV === 'production',
|
isProduction: process.env.NODE_ENV === 'production',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ if (!leadService) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
app.set('trust proxy', true)
|
// 'loopback', not true: only the local nginx hop is trusted, so req.ip is the
|
||||||
|
// address nginx actually observed and the rate limiter below cannot be
|
||||||
|
// side-stepped with a forged X-Forwarded-For header.
|
||||||
|
app.set('trust proxy', 'loopback')
|
||||||
app.use(express.json({ limit: '64kb' }))
|
app.use(express.json({ limit: '64kb' }))
|
||||||
|
|
||||||
const limiter = createRateLimiter({ limit: 8, windowMs: 10 * 60 * 1000 })
|
const limiter = createRateLimiter({ limit: 8, windowMs: 10 * 60 * 1000 })
|
||||||
@@ -103,7 +106,7 @@ if (serverConfig.isProduction) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
app.listen(serverConfig.port, () => {
|
app.listen(serverConfig.port, serverConfig.host, () => {
|
||||||
console.info(
|
console.info(
|
||||||
'[server] listening on http://localhost:%d%s',
|
'[server] listening on http://localhost:%d%s',
|
||||||
serverConfig.port,
|
serverConfig.port,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': { target: 'http://localhost:3000', changeOrigin: true },
|
'/api': { target: 'http://localhost:3002', changeOrigin: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ VITE_BASE_PATH=/
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Server
|
# Server
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 3000 and 3001 belong to the fitnes/medcenter and hotel landings.
|
# One port per landing so all four can run side by side:
|
||||||
PORT=3002
|
# fitnes 3000, hotel 3001, medcenter 3002, medcenterpersonal 3003.
|
||||||
|
PORT=3003
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# amoCRM — server-side only, never exposed to the browser
|
# amoCRM — server-side only, never exposed to the browser
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
24
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
"provider": { "@type": "Organization", "name": "Экзо Групп" },
|
"provider": { "@type": "Organization", "name": "Экзо Групп" },
|
||||||
"serviceType": "Запуск направления физиотерапии в медицинском центре под ключ",
|
"serviceType": "Запуск направления физиотерапии в медицинском центре под ключ",
|
||||||
"areaServed": "RU",
|
"areaServed": "RU",
|
||||||
"url": "https://exodevices.ru/medical-centers/no-physiotherapy"
|
"url": "%VITE_SITE_URL%"
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.12"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently -n client,server -c cyan,magenta \"vite\" \"tsx watch server/src/index.ts\"",
|
"dev": "concurrently -n client,server -c cyan,magenta \"vite\" \"tsx watch server/src/index.ts\"",
|
||||||
"dev:client": "vite",
|
"dev:client": "vite",
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ export function readAmoConfig(): AmoConfig | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const serverConfig = {
|
export const serverConfig = {
|
||||||
port: optionalNumber('PORT') ?? 3000,
|
port: optionalNumber('PORT') ?? 3003,
|
||||||
|
// Loopback by default: in production nginx is the only thing that should
|
||||||
|
// reach this process, so the port is never exposed to the network.
|
||||||
|
host: optional('HOST') ?? '127.0.0.1',
|
||||||
isProduction: process.env.NODE_ENV === 'production',
|
isProduction: process.env.NODE_ENV === 'production',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ if (!leadService) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
app.set('trust proxy', true)
|
// 'loopback', not true: only the local nginx hop is trusted, so req.ip is the
|
||||||
|
// address nginx actually observed and the rate limiter below cannot be
|
||||||
|
// side-stepped with a forged X-Forwarded-For header.
|
||||||
|
app.set('trust proxy', 'loopback')
|
||||||
app.use(express.json({ limit: '64kb' }))
|
app.use(express.json({ limit: '64kb' }))
|
||||||
|
|
||||||
const limiter = createRateLimiter({ limit: 8, windowMs: 10 * 60 * 1000 })
|
const limiter = createRateLimiter({ limit: 8, windowMs: 10 * 60 * 1000 })
|
||||||
@@ -103,7 +106,7 @@ if (serverConfig.isProduction) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
app.listen(serverConfig.port, () => {
|
app.listen(serverConfig.port, serverConfig.host, () => {
|
||||||
console.info(
|
console.info(
|
||||||
'[server] listening on http://localhost:%d%s',
|
'[server] listening on http://localhost:%d%s',
|
||||||
serverConfig.port,
|
serverConfig.port,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export default defineConfig({
|
|||||||
// landings, so all of them can run side by side.
|
// landings, so all of them can run side by side.
|
||||||
port: 5175,
|
port: 5175,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': { target: 'http://localhost:3002', changeOrigin: true },
|
'/api': { target: 'http://localhost:3003', changeOrigin: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user