From 4f3e20b7a029bc2b829226deeb252bab827252c5 Mon Sep 17 00:00:00 2001 From: null Date: Sun, 17 May 2026 17:46:54 -0500 Subject: [PATCH] fix: dead code cleanup, timeout middleware, Zoho error handling (closes #53, #54, #55, #56, #57) - Delete broken barrel exports ui/index.jsx and ui/all.jsx (#53) - Remove duplicate QueryClient instance and dead queryClient.js (#55) - Remove unused queryClient import/export from api.js (#55) - Move timeoutMiddleware before catch-all routes so it actually fires (#54) - Fix async error handling in forwardToZoho - add .catch() (#56) - Add ZOHO_CLIENT_ID to credential guard, normalize defaults to null (#57) (batch 0.6.4) --- server/index.js | 121 +++++++++++++++++------------------- src/components/ui/all.jsx | 39 ------------ src/components/ui/index.jsx | 12 ---- src/lib/api.js | 4 -- src/lib/queryClient.js | 9 --- 5 files changed, 57 insertions(+), 128 deletions(-) delete mode 100644 src/components/ui/all.jsx delete mode 100644 src/components/ui/index.jsx delete mode 100644 src/lib/queryClient.js diff --git a/server/index.js b/server/index.js index 97f0bc7..af20216 100644 --- a/server/index.js +++ b/server/index.js @@ -213,7 +213,7 @@ const supportSchema = z.object({ // --- Zoho CRM Forwarding (best-effort, fire-and-forget) --- const ZOHO_ENABLED = process.env.ZOHO_ENABLED === 'true' const ZOHO_API_DOMAIN = process.env.ZOHO_API_DOMAIN || 'https://www.zohoapis.com' -const ZOHO_CLIENT_ID = process.env.ZOHO_CLIENT_ID || '' +const ZOHO_CLIENT_ID = process.env.ZOHO_CLIENT_ID || null const ZOHO_CLIENT_SECRET = process.env.ZOHO_CLIENT_SECRET || null const ZOHO_REFRESH_TOKEN = process.env.ZOHO_REFRESH_TOKEN || null const ZOHO_REDIRECT_URI = process.env.ZOHO_REDIRECT_URI || '' @@ -291,69 +291,65 @@ async function getZohoAccessToken() { async function forwardToZoho(leadData) { if (!ZOHO_ENABLED) return - // Issue #2: Short-circuit if Zoho credentials are missing - if (!ZOHO_CLIENT_SECRET || !ZOHO_REFRESH_TOKEN) { - log.warn('[Zoho] Skipping forwarding - ZOHO_CLIENT_SECRET or ZOHO_REFRESH_TOKEN not configured') + // Short-circuit if Zoho credentials are missing + if (!ZOHO_CLIENT_ID || !ZOHO_CLIENT_SECRET || !ZOHO_REFRESH_TOKEN) { + log.warn("[Zoho] Skipping forwarding - ZOHO_CLIENT_ID, ZOHO_CLIENT_SECRET, or ZOHO_REFRESH_TOKEN not configured") return } + const accessToken = await getZohoAccessToken() + if (!accessToken) { + log.warn("[Zoho] No access token available, skipping lead forwarding") + return + } + + // Issue #8: Prevent double-slash in URL path + const url = `${ZOHO_API_DOMAIN.replace(/\/$/, "")}/crm/v8/Leads` + const payload = { + data: [ + { + Company: leadData.company || "", + Last_Name: leadData.name || "Unknown", + Email: leadData.email || "", + Phone: leadData.phone || "", + Zip_Code: leadData.zip || "", + Description: leadData.message || "", + Service_Interest: leadData.service_interest || null, + }, + ], + } + + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), ZOHO_TIMEOUT_MS) + try { - const accessToken = await getZohoAccessToken() - if (!accessToken) { - log.warn('[Zoho] No access token available, skipping lead forwarding') + const response = await fetch(url, { + method: "POST", + headers: { + "Authorization": `Zoho-oauthtoken ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + signal: controller.signal, + }) + + // Issue #3: Check response.ok before processing + if (!response.ok) { + const text = await response.text() + log.error(`[Zoho] Lead forwarding failed (${response.status}):`, text) return } - // Issue #8: Prevent double-slash in URL path - const url = `${ZOHO_API_DOMAIN.replace(/\/$/, '')}/crm/v8/Leads` - const payload = { - data: [ - { - Company: leadData.company || '', - Last_Name: leadData.name || 'Unknown', - Email: leadData.email || '', - Phone: leadData.phone || '', - Zip_Code: leadData.zip || '', - Description: leadData.message || '', - Service_Interest: leadData.service_interest || null, - }, - ], + const result = await response.json() + log.info("[Zoho] Lead forwarded successfully:", result.data?.[0]?.details?.id || "no id returned") + } catch (fetchErr) { + if (fetchErr.name === "AbortError") { + log.warn("[Zoho] Lead forwarding timed out after", ZOHO_TIMEOUT_MS, "ms") + } else { + log.error("[Zoho] Forwarding error:", fetchErr.message) } - - const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), ZOHO_TIMEOUT_MS) - - try { - const response = await fetch(url, { - method: 'POST', - headers: { - 'Authorization': `Zoho-oauthtoken ${accessToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - signal: controller.signal, - }) - - // Issue #3: Check response.ok before processing - if (!response.ok) { - const text = await response.text() - log.error(`[Zoho] Lead forwarding failed (${response.status}):`, text) - return - } - - const result = await response.json() - log.info('[Zoho] Lead forwarded successfully:', result.data?.[0]?.details?.id || 'no id returned') - } catch (fetchErr) { - if (fetchErr.name === 'AbortError') { - log.warn('[Zoho] Lead forwarding timed out after', ZOHO_TIMEOUT_MS, 'ms') - } else { - log.error('[Zoho] Forwarding error:', fetchErr.message) - } - } finally { - clearTimeout(timeoutId) - } - } catch (err) { - log.error('[Zoho] Forwarding error:', err.message) + } finally { + clearTimeout(timeoutId) } } @@ -418,7 +414,7 @@ app.post('/api/leads', express.json({ limit: '1mb' }), (req, res) => { log.info(`Lead submitted: ${sanitized.email} from ${sanitized.company} (id: ${result.lastInsertRowid})`) // Fire-and-forget Zoho forwarding (best-effort, non-blocking) - forwardToZoho(sanitized) + forwardToZoho(sanitized).catch(err => log.error('[Zoho] Forwarding error:', err.message)) res.json({ success: true, message: "Thanks! We'll be in touch shortly." }) } catch (err) { @@ -428,11 +424,7 @@ app.post('/api/leads', express.json({ limit: '1mb' }), (req, res) => { log.warn(`Duplicate lead email: ${sanitized.email}`) // Still forward to Zoho (non-blocking) for existing leads - try { - forwardToZoho(sanitized) - } catch (zohoErr) { - log.warn(`[Zoho] Skipped forwarding for duplicate lead: ${sanitized.email}`) - } + forwardToZoho(sanitized).catch(err => log.error('[Zoho] Forwarding error:', err.message)) return res.status(409).json({ error: 'Duplicate lead', @@ -534,8 +526,6 @@ const timeoutMiddleware = (req, res, next) => { next() } -app.use(timeoutMiddleware) - // --- Global error handlers --- process.on('uncaughtException', (err) => { log.error('Uncaught exception:', err.message) @@ -554,6 +544,9 @@ process.on('unhandledRejection', (reason, promise) => { // --- Start Server --- const PORT = process.env.SERVER_PORT || 3001 +// Register timeout middleware BEFORE catch-all routes +app.use(timeoutMiddleware) + app.listen(PORT, () => { log.info(`Server running on http://localhost:${PORT}`) log.info(`Health check: http://localhost:${PORT}/api/health`) diff --git a/src/components/ui/all.jsx b/src/components/ui/all.jsx deleted file mode 100644 index 0e1573d..0000000 --- a/src/components/ui/all.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Button } from './Button.jsx' -import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card.jsx' -import { Input } from './Input.jsx' -import { Textarea } from './Textarea.jsx' -import { Select } from './Select.jsx' -import { Badge } from './Badge.jsx' -import { Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetClose, SheetFooter, SheetOverlay } from './Sheet.jsx' -import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from './Dialog.jsx' - -export { - Button, - Card, - CardHeader, - CardTitle, - CardDescription, - CardContent, - CardFooter, - Input, - Textarea, - Select, - Badge, - Sheet, - SheetTrigger, - SheetContent, - SheetHeader, - SheetTitle, - SheetDescription, - SheetClose, - SheetFooter, - SheetOverlay, - Dialog, - DialogTrigger, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, - DialogFooter, - DialogClose, -} diff --git a/src/components/ui/index.jsx b/src/components/ui/index.jsx deleted file mode 100644 index 794c4a4..0000000 --- a/src/components/ui/index.jsx +++ /dev/null @@ -1,12 +0,0 @@ -export { default as Button } from './Button.jsx' -export { default as Card } from './Card.jsx' -export { default as CardContent } from './CardContent.jsx' -export { default as CardHeader } from './CardHeader.jsx' -export { default as CardTitle } from './CardTitle.jsx' -export { default as CardDescription } from './CardDescription.jsx' -export { default as Input } from './Input.jsx' -export { default as Textarea } from './Textarea.jsx' -export { default as Select } from './Select.jsx' -export { default as Badge } from './Badge.jsx' -export { default as Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetClose, SheetFooter, SheetOverlay } from './Sheet.jsx' -export { default as Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from './Dialog.jsx' diff --git a/src/lib/api.js b/src/lib/api.js index ff60e63..c3a64ea 100644 --- a/src/lib/api.js +++ b/src/lib/api.js @@ -1,5 +1,3 @@ -import { queryClient } from './queryClient' - const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001/api' // Exponential backoff retry helper @@ -92,5 +90,3 @@ export const api = { }, { maxRetries: 3, baseDelay: 1000 }) }, } - -export { queryClient } \ No newline at end of file diff --git a/src/lib/queryClient.js b/src/lib/queryClient.js deleted file mode 100644 index 0642c06..0000000 --- a/src/lib/queryClient.js +++ /dev/null @@ -1,9 +0,0 @@ -import { QueryClient } from '@tanstack/react-query' - -export const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 1000 * 60 * 5, // 5 minutes - }, - }, -})