diff --git a/README.md b/README.md index 5225642..e98427a 100644 --- a/README.md +++ b/README.md @@ -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` diff --git a/package.json b/package.json index e6b4564..8605077 100644 --- a/package.json +++ b/package.json @@ -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\"", diff --git a/server/index.js b/server/index.js index 729382d..abe1dec 100644 --- a/server/index.js +++ b/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)