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~~ ✅
|
- ~~SQLite persistence verified~~ ✅
|
||||||
- ~~Client-side validation + Sonner feedback~~ ✅
|
- ~~Client-side validation + Sonner feedback~~ ✅
|
||||||
- ~~Server-side validation + input sanitization~~ ✅
|
- ~~Server-side validation + input sanitization~~ ✅
|
||||||
- Optional Zoho forwarding layer
|
- ~~Optional Zoho forwarding layer~~ ✅
|
||||||
- Backend/API hardening as needed
|
- Backend/API hardening as needed
|
||||||
|
|
||||||
- **Phase 5 — Verification + Release Readiness**: `0.5.x`
|
- **Phase 5 — Verification + Release Readiness**: `0.5.x`
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "queuenorth-website",
|
"name": "queuenorth-website",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.4.5",
|
"version": "0.4.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"vite\" \"node server/index.js\"",
|
"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)),
|
}).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 ---
|
// --- API Routes ---
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
|
|
@ -156,6 +248,9 @@ app.post('/api/leads', (req, res) => {
|
||||||
sanitized.service_interest || null
|
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.' })
|
res.json({ success: true, message: 'Thanks! We\'ll be in touch shortly.' })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error submitting lead:', err)
|
console.error('Error submitting lead:', err)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue