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)
This commit is contained in:
null 2026-05-17 17:46:54 -05:00
parent 9cdc299ade
commit 4f3e20b7a0
5 changed files with 57 additions and 128 deletions

View File

@ -213,7 +213,7 @@ const supportSchema = z.object({
// --- Zoho CRM Forwarding (best-effort, fire-and-forget) --- // --- Zoho CRM Forwarding (best-effort, fire-and-forget) ---
const ZOHO_ENABLED = process.env.ZOHO_ENABLED === 'true' const ZOHO_ENABLED = process.env.ZOHO_ENABLED === 'true'
const ZOHO_API_DOMAIN = process.env.ZOHO_API_DOMAIN || 'https://www.zohoapis.com' 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_CLIENT_SECRET = process.env.ZOHO_CLIENT_SECRET || null
const ZOHO_REFRESH_TOKEN = process.env.ZOHO_REFRESH_TOKEN || null const ZOHO_REFRESH_TOKEN = process.env.ZOHO_REFRESH_TOKEN || null
const ZOHO_REDIRECT_URI = process.env.ZOHO_REDIRECT_URI || '' const ZOHO_REDIRECT_URI = process.env.ZOHO_REDIRECT_URI || ''
@ -291,30 +291,29 @@ async function getZohoAccessToken() {
async function forwardToZoho(leadData) { async function forwardToZoho(leadData) {
if (!ZOHO_ENABLED) return if (!ZOHO_ENABLED) return
// Issue #2: Short-circuit if Zoho credentials are missing // Short-circuit if Zoho credentials are missing
if (!ZOHO_CLIENT_SECRET || !ZOHO_REFRESH_TOKEN) { if (!ZOHO_CLIENT_ID || !ZOHO_CLIENT_SECRET || !ZOHO_REFRESH_TOKEN) {
log.warn('[Zoho] Skipping forwarding - ZOHO_CLIENT_SECRET or ZOHO_REFRESH_TOKEN not configured') log.warn("[Zoho] Skipping forwarding - ZOHO_CLIENT_ID, ZOHO_CLIENT_SECRET, or ZOHO_REFRESH_TOKEN not configured")
return return
} }
try {
const accessToken = await getZohoAccessToken() const accessToken = await getZohoAccessToken()
if (!accessToken) { if (!accessToken) {
log.warn('[Zoho] No access token available, skipping lead forwarding') log.warn("[Zoho] No access token available, skipping lead forwarding")
return return
} }
// Issue #8: Prevent double-slash in URL path // Issue #8: Prevent double-slash in URL path
const url = `${ZOHO_API_DOMAIN.replace(/\/$/, '')}/crm/v8/Leads` const url = `${ZOHO_API_DOMAIN.replace(/\/$/, "")}/crm/v8/Leads`
const payload = { const payload = {
data: [ data: [
{ {
Company: leadData.company || '', Company: leadData.company || "",
Last_Name: leadData.name || 'Unknown', Last_Name: leadData.name || "Unknown",
Email: leadData.email || '', Email: leadData.email || "",
Phone: leadData.phone || '', Phone: leadData.phone || "",
Zip_Code: leadData.zip || '', Zip_Code: leadData.zip || "",
Description: leadData.message || '', Description: leadData.message || "",
Service_Interest: leadData.service_interest || null, Service_Interest: leadData.service_interest || null,
}, },
], ],
@ -325,10 +324,10 @@ async function forwardToZoho(leadData) {
try { try {
const response = await fetch(url, { const response = await fetch(url, {
method: 'POST', method: "POST",
headers: { headers: {
'Authorization': `Zoho-oauthtoken ${accessToken}`, "Authorization": `Zoho-oauthtoken ${accessToken}`,
'Content-Type': 'application/json', "Content-Type": "application/json",
}, },
body: JSON.stringify(payload), body: JSON.stringify(payload),
signal: controller.signal, signal: controller.signal,
@ -342,19 +341,16 @@ async function forwardToZoho(leadData) {
} }
const result = await response.json() const result = await response.json()
log.info('[Zoho] Lead forwarded successfully:', result.data?.[0]?.details?.id || 'no id returned') log.info("[Zoho] Lead forwarded successfully:", result.data?.[0]?.details?.id || "no id returned")
} catch (fetchErr) { } catch (fetchErr) {
if (fetchErr.name === 'AbortError') { if (fetchErr.name === "AbortError") {
log.warn('[Zoho] Lead forwarding timed out after', ZOHO_TIMEOUT_MS, 'ms') log.warn("[Zoho] Lead forwarding timed out after", ZOHO_TIMEOUT_MS, "ms")
} else { } else {
log.error('[Zoho] Forwarding error:', fetchErr.message) log.error("[Zoho] Forwarding error:", fetchErr.message)
} }
} finally { } finally {
clearTimeout(timeoutId) clearTimeout(timeoutId)
} }
} catch (err) {
log.error('[Zoho] Forwarding error:', err.message)
}
} }
// --- API Routes --- // --- API Routes ---
@ -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})`) log.info(`Lead submitted: ${sanitized.email} from ${sanitized.company} (id: ${result.lastInsertRowid})`)
// Fire-and-forget Zoho forwarding (best-effort, non-blocking) // 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." }) res.json({ success: true, message: "Thanks! We'll be in touch shortly." })
} catch (err) { } catch (err) {
@ -428,11 +424,7 @@ app.post('/api/leads', express.json({ limit: '1mb' }), (req, res) => {
log.warn(`Duplicate lead email: ${sanitized.email}`) log.warn(`Duplicate lead email: ${sanitized.email}`)
// Still forward to Zoho (non-blocking) for existing leads // Still forward to Zoho (non-blocking) for existing leads
try { forwardToZoho(sanitized).catch(err => log.error('[Zoho] Forwarding error:', err.message))
forwardToZoho(sanitized)
} catch (zohoErr) {
log.warn(`[Zoho] Skipped forwarding for duplicate lead: ${sanitized.email}`)
}
return res.status(409).json({ return res.status(409).json({
error: 'Duplicate lead', error: 'Duplicate lead',
@ -534,8 +526,6 @@ const timeoutMiddleware = (req, res, next) => {
next() next()
} }
app.use(timeoutMiddleware)
// --- Global error handlers --- // --- Global error handlers ---
process.on('uncaughtException', (err) => { process.on('uncaughtException', (err) => {
log.error('Uncaught exception:', err.message) log.error('Uncaught exception:', err.message)
@ -554,6 +544,9 @@ process.on('unhandledRejection', (reason, promise) => {
// --- Start Server --- // --- Start Server ---
const PORT = process.env.SERVER_PORT || 3001 const PORT = process.env.SERVER_PORT || 3001
// Register timeout middleware BEFORE catch-all routes
app.use(timeoutMiddleware)
app.listen(PORT, () => { app.listen(PORT, () => {
log.info(`Server running on http://localhost:${PORT}`) log.info(`Server running on http://localhost:${PORT}`)
log.info(`Health check: http://localhost:${PORT}/api/health`) log.info(`Health check: http://localhost:${PORT}/api/health`)

View File

@ -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,
}

View File

@ -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'

View File

@ -1,5 +1,3 @@
import { queryClient } from './queryClient'
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001/api' const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001/api'
// Exponential backoff retry helper // Exponential backoff retry helper
@ -92,5 +90,3 @@ export const api = {
}, { maxRetries: 3, baseDelay: 1000 }) }, { maxRetries: 3, baseDelay: 1000 })
}, },
} }
export { queryClient }

View File

@ -1,9 +0,0 @@
import { QueryClient } from '@tanstack/react-query'
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
},
},
})