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) ---
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,30 +291,29 @@ 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
}
try {
const accessToken = await getZohoAccessToken()
if (!accessToken) {
log.warn('[Zoho] No access token available, skipping lead forwarding')
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 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 || '',
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,
},
],
@ -325,10 +324,10 @@ async function forwardToZoho(leadData) {
try {
const response = await fetch(url, {
method: 'POST',
method: "POST",
headers: {
'Authorization': `Zoho-oauthtoken ${accessToken}`,
'Content-Type': 'application/json',
"Authorization": `Zoho-oauthtoken ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
signal: controller.signal,
@ -342,19 +341,16 @@ async function forwardToZoho(leadData) {
}
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) {
if (fetchErr.name === 'AbortError') {
log.warn('[Zoho] Lead forwarding timed out after', ZOHO_TIMEOUT_MS, 'ms')
if (fetchErr.name === "AbortError") {
log.warn("[Zoho] Lead forwarding timed out after", ZOHO_TIMEOUT_MS, "ms")
} else {
log.error('[Zoho] Forwarding error:', fetchErr.message)
log.error("[Zoho] Forwarding error:", fetchErr.message)
}
} finally {
clearTimeout(timeoutId)
}
} catch (err) {
log.error('[Zoho] Forwarding error:', err.message)
}
}
// --- 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})`)
// 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`)

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'
// Exponential backoff retry helper
@ -92,5 +90,3 @@ export const api = {
}, { 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
},
},
})