feat: Zoho CRM forwarding layer with OAuth2 token management (v0.4.6)
This commit is contained in:
parent
4ac0fa250d
commit
6bfd804313
|
|
@ -120,7 +120,7 @@ Version numbers correlate directly to the active phase:
|
|||
- ~~SQLite persistence verified~~ ✅
|
||||
- ~~Client-side validation + Sonner feedback~~ ✅
|
||||
- ~~Server-side validation + input sanitization~~ ✅
|
||||
- Optional Zoho forwarding layer
|
||||
- ~~Optional Zoho forwarding layer~~ ✅
|
||||
- Backend/API hardening as needed
|
||||
|
||||
- **Phase 5 — Verification + Release Readiness**: `0.5.x`
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "queuenorth-website",
|
||||
"private": true,
|
||||
"version": "0.4.5",
|
||||
"version": "0.4.6",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"vite\" \"node server/index.js\"",
|
||||
|
|
|
|||
|
|
@ -105,6 +105,98 @@ const supportSchema = z.object({
|
|||
}).transform((val) => val?.toLowerCase() ?? undefined).optional().or(z.literal('').transform(() => undefined)),
|
||||
})
|
||||
|
||||
// --- 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_SECRET = process.env.ZOHO_CLIENT_SECRET || ''
|
||||
const ZOHO_REFRESH_TOKEN = process.env.ZOHO_REFRESH_TOKEN || ''
|
||||
const ZOHO_REDIRECT_URI = process.env.ZOHO_REDIRECT_URI || ''
|
||||
|
||||
// In-memory access token cache
|
||||
let zohoAccessToken = null
|
||||
let zohoTokenExpiry = 0
|
||||
|
||||
async function getZohoAccessToken() {
|
||||
// Return cached token if still valid (with 60s buffer)
|
||||
if (zohoAccessToken && Date.now() < zohoTokenExpiry - 60000) {
|
||||
return zohoAccessToken
|
||||
}
|
||||
|
||||
try {
|
||||
const url = `${ZOHO_API_DOMAIN}/oauth/v2/token`
|
||||
const params = new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
client_id: ZOHO_CLIENT_ID,
|
||||
client_secret: ZOHO_CLIENT_SECRET,
|
||||
refresh_token: ZOHO_REFRESH_TOKEN,
|
||||
redirect_uri: ZOHO_REDIRECT_URI,
|
||||
})
|
||||
|
||||
const response = await fetch(`${url}?${params.toString()}`, { method: 'POST' })
|
||||
const data = await response.json()
|
||||
|
||||
if (data.access_token) {
|
||||
zohoAccessToken = data.access_token
|
||||
zohoTokenExpiry = Date.now() + (data.expires_in || 3600) * 1000
|
||||
console.log('[Zoho] Access token acquired, expires in', data.expires_in || 3600, 'seconds')
|
||||
return zohoAccessToken
|
||||
} else {
|
||||
console.error('[Zoho] Token exchange failed:', JSON.stringify(data))
|
||||
return null
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Zoho] Token acquisition error:', err.message)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function forwardToZoho(leadData) {
|
||||
if (!ZOHO_ENABLED) return
|
||||
|
||||
try {
|
||||
const accessToken = await getZohoAccessToken()
|
||||
if (!accessToken) {
|
||||
console.error('[Zoho] No access token available, skipping lead forwarding')
|
||||
return
|
||||
}
|
||||
|
||||
const url = `${ZOHO_API_DOMAIN}/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 || '',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Zoho-oauthtoken ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json()
|
||||
console.log('[Zoho] Lead forwarded successfully:', result.data?.[0]?.details?.id || 'no id returned')
|
||||
} else {
|
||||
const text = await response.text()
|
||||
console.error(`[Zoho] Lead forwarding failed (${response.status}):`, text)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Zoho] Forwarding error:', err.message)
|
||||
}
|
||||
}
|
||||
|
||||
// --- API Routes ---
|
||||
|
||||
// Health check
|
||||
|
|
@ -156,6 +248,9 @@ app.post('/api/leads', (req, res) => {
|
|||
sanitized.service_interest || null
|
||||
)
|
||||
|
||||
// Fire-and-forget Zoho forwarding (best-effort, non-blocking)
|
||||
forwardToZoho(sanitized)
|
||||
|
||||
res.json({ success: true, message: 'Thanks! We\'ll be in touch shortly.' })
|
||||
} catch (err) {
|
||||
console.error('Error submitting lead:', err)
|
||||
|
|
|
|||
Loading…
Reference in New Issue