diff --git a/package.json b/package.json index 767da86..7b56828 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "queuenorth-website", "private": true, - "version": "0.5.7", + "version": "0.6.0", "type": "module", "scripts": { "dev": "concurrently \"vite\" \"node server/index.js\"", diff --git a/server/index.js b/server/index.js index a4efc63..14d95aa 100644 --- a/server/index.js +++ b/server/index.js @@ -38,7 +38,14 @@ const log = { // --- Rate Limiting --- const rateLimitWindowMs = 60 * 1000 // 1 minute -const rateLimitMax = parseInt(process.env.RATE_LIMIT_PER_MINUTE || '5', 10) +const rateLimitMax = (() => { + const val = parseInt(process.env.RATE_LIMIT_PER_MINUTE || '5', 10) + if (isNaN(val) || val < 1) { + log.warn('[RateLimit] Invalid RATE_LIMIT_PER_MINUTE, defaulting to 5') + return 5 + } + return val +})() const apiLimiter = rateLimit({ windowMs: rateLimitWindowMs, @@ -103,8 +110,7 @@ const corsConfig = cors({ app.use(corsConfig) log.info(`[CORS] Enabled with origin: ${corsOrigin}`) -// Middleware -app.use(express.json({ limit: '1mb' })) +// Middleware — JSON body parsing only on POST routes (issue #14) app.use(express.urlencoded({ extended: true, limit: '1mb' })) // Rate limiting for API routes only @@ -366,7 +372,7 @@ app.get('/api/health', (req, res) => { }) // Submit lead -app.post('/api/leads', (req, res) => { +app.post('/api/leads', express.json({ limit: '1mb' }), (req, res) => { try { const parsed = leadSchema.safeParse(req.body) @@ -440,7 +446,7 @@ app.post('/api/leads', (req, res) => { }) // Submit support request -app.post('/api/support', (req, res) => { +app.post('/api/support', express.json({ limit: '1mb' }), (req, res) => { try { const parsed = supportSchema.safeParse(req.body) diff --git a/src/lib/api.js b/src/lib/api.js index 123d665..8cdafec 100644 --- a/src/lib/api.js +++ b/src/lib/api.js @@ -4,7 +4,15 @@ const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001/api' export const api = { get: async (endpoint) => { - const response = await fetch(`${API_BASE_URL}${endpoint}`) + let response + try { + response = await fetch(`${API_BASE_URL}${endpoint}`) + } catch (err) { + if (err instanceof TypeError && err.message === 'Failed to fetch') { + throw new Error('Unable to reach the server. This may be a network or CORS issue.') + } + throw new Error(`Network error: ${err.message}`) + } if (!response.ok) { throw new Error(`API error: ${response.statusText}`) } @@ -12,19 +20,32 @@ export const api = { }, post: async (endpoint, data) => { - const response = await fetch(`${API_BASE_URL}${endpoint}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), - }) + let response + try { + response = await fetch(`${API_BASE_URL}${endpoint}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }) + } catch (err) { + if (err instanceof TypeError && err.message === 'Failed to fetch') { + throw new Error('Unable to reach the server. This may be a network or CORS issue.') + } + throw new Error(`Network error: ${err.message}`) + } if (!response.ok) { - const errorData = await response.json() + let errorData + try { + errorData = await response.json() + } catch { + throw new Error(`Server error (${response.status}): ${response.statusText}`) + } throw new Error(errorData.error || `API error: ${response.statusText}`) } return response.json() }, } -export { queryClient } +export { queryClient } \ No newline at end of file diff --git a/src/pages/Contact.jsx b/src/pages/Contact.jsx index 639bb42..e4c15bf 100644 --- a/src/pages/Contact.jsx +++ b/src/pages/Contact.jsx @@ -172,7 +172,7 @@ const Contact = () => { {/* Right - Form */}