- 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:
parent
9cdc299ade
commit
4f3e20b7a0
|
|
@ -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`)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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'
|
||||
|
|
@ -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 }
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import { QueryClient } from '@tanstack/react-query'
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
},
|
||||
},
|
||||
})
|
||||
Loading…
Reference in New Issue