feat(zoho): WebToLead forwarding mode, veteran-owned certified badge, Docker/env/CI updates (batch 0.9.1)
This commit is contained in:
parent
05b27d216a
commit
d074e597b2
13
.env.example
13
.env.example
|
|
@ -5,8 +5,17 @@ NODE_ENV=production
|
||||||
SERVER_PORT=3001
|
SERVER_PORT=3001
|
||||||
|
|
||||||
# Zoho CRM Integration
|
# Zoho CRM Integration
|
||||||
# Set ZOHO_ENABLED=true to forward leads/support to Zoho CRM
|
# Preferred current setup: webtolead for contact leads using the legacy Zoho form tokens.
|
||||||
# Get credentials from https://api-console.zoho.com → Self Client
|
ZOHO_FORWARDING_MODE=webtolead
|
||||||
|
ZOHO_WEBTOLEAD_ENABLED=false
|
||||||
|
ZOHO_WEBTOLEAD_URL=https://crm.zoho.com/crm/WebToLeadForm
|
||||||
|
ZOHO_WEBTOLEAD_XNQSJSDP=
|
||||||
|
ZOHO_WEBTOLEAD_XMIWTLD=
|
||||||
|
ZOHO_WEBTOLEAD_ACTION_TYPE=TGVhZHM=
|
||||||
|
ZOHO_WEBTOLEAD_RETURN_URL=null
|
||||||
|
ZOHO_WEBTOLEAD_ZC_GAD=
|
||||||
|
|
||||||
|
# Standby REST API/OAuth setup. Set ZOHO_FORWARDING_MODE=api and ZOHO_ENABLED=true to use it.
|
||||||
ZOHO_ENABLED=false
|
ZOHO_ENABLED=false
|
||||||
ZOHO_API_DOMAIN=https://www.zohoapis.com
|
ZOHO_API_DOMAIN=https://www.zohoapis.com
|
||||||
ZOHO_ACCOUNTS_DOMAIN=https://accounts.zoho.com
|
ZOHO_ACCOUNTS_DOMAIN=https://accounts.zoho.com
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,9 @@ FUTURE.md
|
||||||
HISTORY.md
|
HISTORY.md
|
||||||
BUILD_SUMMARY.md
|
BUILD_SUMMARY.md
|
||||||
SCRIPTS.md
|
SCRIPTS.md
|
||||||
|
.drop/
|
||||||
|
zoho.md
|
||||||
|
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
@ -36,3 +39,4 @@ pnpm-debug.log*
|
||||||
.learnings/
|
.learnings/
|
||||||
Levi.md
|
Levi.md
|
||||||
Queue-North-Website.code-workspace
|
Queue-North-Website.code-workspace
|
||||||
|
Working Site.zip
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,14 @@ ENV SERVER_PORT=3001
|
||||||
ENV RATE_LIMIT_PER_MINUTE=5
|
ENV RATE_LIMIT_PER_MINUTE=5
|
||||||
ENV CORS_ORIGIN=*
|
ENV CORS_ORIGIN=*
|
||||||
ENV LOG_LEVEL=info
|
ENV LOG_LEVEL=info
|
||||||
|
ENV ZOHO_FORWARDING_MODE=webtolead
|
||||||
|
ENV ZOHO_WEBTOLEAD_ENABLED=false
|
||||||
|
ENV ZOHO_WEBTOLEAD_URL=https://crm.zoho.com/crm/WebToLeadForm
|
||||||
|
ENV ZOHO_WEBTOLEAD_XNQSJSDP=
|
||||||
|
ENV ZOHO_WEBTOLEAD_XMIWTLD=
|
||||||
|
ENV ZOHO_WEBTOLEAD_ACTION_TYPE=TGVhZHM=
|
||||||
|
ENV ZOHO_WEBTOLEAD_RETURN_URL=null
|
||||||
|
ENV ZOHO_WEBTOLEAD_ZC_GAD=
|
||||||
ENV ZOHO_ENABLED=false
|
ENV ZOHO_ENABLED=false
|
||||||
ENV ZOHO_API_DOMAIN=https://www.zohoapis.com
|
ENV ZOHO_API_DOMAIN=https://www.zohoapis.com
|
||||||
ENV ZOHO_ACCOUNTS_DOMAIN=https://accounts.zoho.com
|
ENV ZOHO_ACCOUNTS_DOMAIN=https://accounts.zoho.com
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 151 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
|
|
@ -22,12 +22,21 @@ services:
|
||||||
- RATE_LIMIT_PER_MINUTE=5
|
- RATE_LIMIT_PER_MINUTE=5
|
||||||
- CORS_ORIGIN=https://queuenorth.com
|
- CORS_ORIGIN=https://queuenorth.com
|
||||||
- LOG_LEVEL=info
|
- LOG_LEVEL=info
|
||||||
- ZOHO_ENABLED=false
|
- ZOHO_FORWARDING_MODE=${ZOHO_FORWARDING_MODE:-webtolead}
|
||||||
- ZOHO_API_DOMAIN=https://www.zohoapis.com
|
- ZOHO_WEBTOLEAD_ENABLED=${ZOHO_WEBTOLEAD_ENABLED:-false}
|
||||||
- ZOHO_CLIENT_ID=
|
- ZOHO_WEBTOLEAD_URL=${ZOHO_WEBTOLEAD_URL:-https://crm.zoho.com/crm/WebToLeadForm}
|
||||||
- ZOHO_CLIENT_SECRET=
|
- ZOHO_WEBTOLEAD_XNQSJSDP=${ZOHO_WEBTOLEAD_XNQSJSDP:-}
|
||||||
- ZOHO_REFRESH_TOKEN=
|
- ZOHO_WEBTOLEAD_XMIWTLD=${ZOHO_WEBTOLEAD_XMIWTLD:-}
|
||||||
- ZOHO_REDIRECT_URI=
|
- ZOHO_WEBTOLEAD_ACTION_TYPE=${ZOHO_WEBTOLEAD_ACTION_TYPE:-TGVhZHM=}
|
||||||
|
- ZOHO_WEBTOLEAD_RETURN_URL=${ZOHO_WEBTOLEAD_RETURN_URL:-null}
|
||||||
|
- ZOHO_WEBTOLEAD_ZC_GAD=${ZOHO_WEBTOLEAD_ZC_GAD:-}
|
||||||
|
- ZOHO_ENABLED=${ZOHO_ENABLED:-false}
|
||||||
|
- ZOHO_API_DOMAIN=${ZOHO_API_DOMAIN:-https://www.zohoapis.com}
|
||||||
|
- ZOHO_ACCOUNTS_DOMAIN=${ZOHO_ACCOUNTS_DOMAIN:-https://accounts.zoho.com}
|
||||||
|
- ZOHO_CLIENT_ID=${ZOHO_CLIENT_ID:-}
|
||||||
|
- ZOHO_CLIENT_SECRET=${ZOHO_CLIENT_SECRET:-}
|
||||||
|
- ZOHO_REFRESH_TOKEN=${ZOHO_REFRESH_TOKEN:-}
|
||||||
|
- ZOHO_CASES_ENABLED=${ZOHO_CASES_ENABLED:-false}
|
||||||
- RECAPTCHA_ENABLED=${RECAPTCHA_ENABLED:-false}
|
- RECAPTCHA_ENABLED=${RECAPTCHA_ENABLED:-false}
|
||||||
- RECAPTCHA_SECRET_KEY=${RECAPTCHA_SECRET_KEY:-}
|
- RECAPTCHA_SECRET_KEY=${RECAPTCHA_SECRET_KEY:-}
|
||||||
- RECAPTCHA_MIN_SCORE=${RECAPTCHA_MIN_SCORE:-0.5}
|
- RECAPTCHA_MIN_SCORE=${RECAPTCHA_MIN_SCORE:-0.5}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,36 @@
|
||||||
# Zoho CRM Setup Guide for Queue North Admins
|
# Zoho CRM Setup Guide for Queue North Admins
|
||||||
|
|
||||||
This guide walks you through setting up Zoho CRM integration to automatically capture leads and support cases from Queue North's website.
|
This guide walks you through the current Zoho CRM integration. Contact leads use the legacy Zoho WebToLead form tokens, while the OAuth/API integration remains available as a standby option for future lead upserts or support cases.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔒 Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
Before you begin, ensure you have:
|
Before you begin, ensure you have:
|
||||||
- A Zoho CRM account (admin access required)
|
- A Zoho CRM account (admin access required)
|
||||||
- Access to Zoho API Console: https://api-console.zoho.com
|
- The WebToLead hidden field values from the old Zoho form
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Step 1: Create a Zoho Self-Client App
|
## Step 1: Gather WebToLead Values
|
||||||
|
|
||||||
|
The current integration needs the old Zoho form's hidden fields:
|
||||||
|
|
||||||
|
```text
|
||||||
|
xnQsjsdp
|
||||||
|
xmIwtLD
|
||||||
|
actionType
|
||||||
|
returnURL
|
||||||
|
zc_gad
|
||||||
|
```
|
||||||
|
|
||||||
|
These values are stored locally in `zoho.md`, which is ignored by git.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Optional Standby: Create a Zoho Self-Client App
|
||||||
|
|
||||||
|
Only use these OAuth steps if switching `ZOHO_FORWARDING_MODE=api`.
|
||||||
|
|
||||||
1. Go to **https://api-console.zoho.com**
|
1. Go to **https://api-console.zoho.com**
|
||||||
2. Click **"Create Self Client"**
|
2. Click **"Create Self Client"**
|
||||||
|
|
@ -29,7 +47,7 @@ Before you begin, ensure you have:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Step 2: Generate an Authorization Code
|
## Optional Standby: Generate an Authorization Code
|
||||||
|
|
||||||
1. In the Self Client tab, click **"Generate Code"**
|
1. In the Self Client tab, click **"Generate Code"**
|
||||||
2. Set the **Scope** to:
|
2. Set the **Scope** to:
|
||||||
|
|
@ -42,7 +60,7 @@ Before you begin, ensure you have:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Step 3: Exchange Auth Code for Tokens
|
## Optional Standby: Exchange Auth Code for Tokens
|
||||||
|
|
||||||
Run this `curl` command (replace placeholders):
|
Run this `curl` command (replace placeholders):
|
||||||
|
|
||||||
|
|
@ -69,12 +87,27 @@ curl -X POST https://accounts.zoho.com/oauth/v2/token \
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Step 4: Configure Environment Variables
|
## Step 2: Configure Environment Variables
|
||||||
|
|
||||||
Add these to your `.env` file:
|
The current production-friendly setup uses the legacy Zoho WebToLead form tokens for contact leads while keeping the OAuth API integration available as a standby option.
|
||||||
|
|
||||||
|
Add these to your `.env` file for WebToLead lead forwarding:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
# Zoho integration is OFF by default — set to true to enable
|
ZOHO_FORWARDING_MODE=webtolead
|
||||||
|
ZOHO_WEBTOLEAD_ENABLED=true
|
||||||
|
ZOHO_WEBTOLEAD_URL=https://crm.zoho.com/crm/WebToLeadForm
|
||||||
|
ZOHO_WEBTOLEAD_XNQSJSDP=<from Zoho WebToLead hidden field>
|
||||||
|
ZOHO_WEBTOLEAD_XMIWTLD=<from Zoho WebToLead hidden field>
|
||||||
|
ZOHO_WEBTOLEAD_ACTION_TYPE=TGVhZHM=
|
||||||
|
ZOHO_WEBTOLEAD_RETURN_URL=null
|
||||||
|
ZOHO_WEBTOLEAD_ZC_GAD=
|
||||||
|
```
|
||||||
|
|
||||||
|
Use these only if switching back to the Zoho CRM REST API/OAuth integration:
|
||||||
|
|
||||||
|
```env
|
||||||
|
ZOHO_FORWARDING_MODE=api
|
||||||
ZOHO_ENABLED=false
|
ZOHO_ENABLED=false
|
||||||
ZOHO_API_DOMAIN=https://www.zohoapis.com
|
ZOHO_API_DOMAIN=https://www.zohoapis.com
|
||||||
ZOHO_ACCOUNTS_DOMAIN=https://accounts.zoho.com
|
ZOHO_ACCOUNTS_DOMAIN=https://accounts.zoho.com
|
||||||
|
|
@ -85,7 +118,7 @@ ZOHO_REFRESH_TOKEN=<from Step 3>
|
||||||
ZOHO_CASES_ENABLED=false
|
ZOHO_CASES_ENABLED=false
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** Both `ZOHO_ENABLED` and `ZOHO_CASES_ENABLED` default to `false`. Set them to `true` only after completing Steps 1–3 and verifying your credentials.
|
> **Note:** `ZOHO_CASES_ENABLED` only applies to the OAuth/API path. The WebToLead values found in the old site are for lead capture only.
|
||||||
|
|
||||||
### Datacenter Variants
|
### Datacenter Variants
|
||||||
|
|
||||||
|
|
@ -100,7 +133,7 @@ If your Zoho datacenter is **outside the US**, adjust the domains:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Step 5: Test the Integration
|
## Step 3: Test the Integration
|
||||||
|
|
||||||
### Test Lead Capture
|
### Test Lead Capture
|
||||||
1. Submit a lead on the contact form (name, email, phone, message)
|
1. Submit a lead on the contact form (name, email, phone, message)
|
||||||
|
|
@ -156,7 +189,7 @@ Website Contact Form → SQLite (always saved)
|
||||||
3. `refresh_token` never expires — store it securely
|
3. `refresh_token` never expires — store it securely
|
||||||
|
|
||||||
### Upsert Logic
|
### Upsert Logic
|
||||||
- **Leads**: Email-based upsert (update if exists, create if new)
|
- **Leads**: WebToLead creates leads through the legacy Zoho form endpoint. API mode uses email-based upsert.
|
||||||
- **Cases**: Always insert (new case per submission)
|
- **Cases**: Always insert (new case per submission)
|
||||||
|
|
||||||
### Fire-and-Forget Design
|
### Fire-and-Forget Design
|
||||||
|
|
@ -171,7 +204,7 @@ Website Contact Form → SQLite (always saved)
|
||||||
|
|
||||||
After configuration:
|
After configuration:
|
||||||
1. Deploy the environment variables to production
|
1. Deploy the environment variables to production
|
||||||
2. Set `ZOHO_ENABLED=true` and `ZOHO_CASES_ENABLED=true` in production `.env`
|
2. Set `ZOHO_WEBTOLEAD_ENABLED=true` in production `.env`
|
||||||
3. Restart the application
|
3. Restart the application
|
||||||
4. Submit a test lead and support case to verify data flows to Zoho CRM
|
4. Submit a test lead and support case to verify data flows to Zoho CRM
|
||||||
5. Check Zoho CRM Leads and Cases tabs to confirm both appear
|
5. Check Zoho CRM Leads and Cases tabs to confirm both appear
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
|
|
@ -374,6 +374,14 @@ const ZOHO_ACCOUNTS_DOMAIN = process.env.ZOHO_ACCOUNTS_DOMAIN || 'https://accoun
|
||||||
const ZOHO_CLIENT_ID = process.env.ZOHO_CLIENT_ID || null
|
const ZOHO_CLIENT_ID = process.env.ZOHO_CLIENT_ID || null
|
||||||
const ZOHO_CLIENT_SECRET = process.env.ZOHO_CLIENT_SECRET || null
|
const ZOHO_CLIENT_SECRET = process.env.ZOHO_CLIENT_SECRET || null
|
||||||
const ZOHO_REFRESH_TOKEN = process.env.ZOHO_REFRESH_TOKEN || null
|
const ZOHO_REFRESH_TOKEN = process.env.ZOHO_REFRESH_TOKEN || null
|
||||||
|
const ZOHO_FORWARDING_MODE = (process.env.ZOHO_FORWARDING_MODE || 'api').toLowerCase()
|
||||||
|
const ZOHO_WEBTOLEAD_ENABLED = process.env.ZOHO_WEBTOLEAD_ENABLED === 'true'
|
||||||
|
const ZOHO_WEBTOLEAD_URL = process.env.ZOHO_WEBTOLEAD_URL || 'https://crm.zoho.com/crm/WebToLeadForm'
|
||||||
|
const ZOHO_WEBTOLEAD_XNQSJSDP = process.env.ZOHO_WEBTOLEAD_XNQSJSDP || null
|
||||||
|
const ZOHO_WEBTOLEAD_XMIWTLD = process.env.ZOHO_WEBTOLEAD_XMIWTLD || null
|
||||||
|
const ZOHO_WEBTOLEAD_ACTION_TYPE = process.env.ZOHO_WEBTOLEAD_ACTION_TYPE || 'TGVhZHM='
|
||||||
|
const ZOHO_WEBTOLEAD_RETURN_URL = process.env.ZOHO_WEBTOLEAD_RETURN_URL || 'null'
|
||||||
|
const ZOHO_WEBTOLEAD_ZC_GAD = process.env.ZOHO_WEBTOLEAD_ZC_GAD || ''
|
||||||
|
|
||||||
// In-memory access token cache
|
// In-memory access token cache
|
||||||
let zohoAccessToken = null
|
let zohoAccessToken = null
|
||||||
|
|
@ -536,6 +544,70 @@ async function forwardToZoho(leadData) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function forwardToZohoWebToLead(leadData) {
|
||||||
|
if (!ZOHO_WEBTOLEAD_ENABLED) return
|
||||||
|
|
||||||
|
if (!ZOHO_WEBTOLEAD_XNQSJSDP || !ZOHO_WEBTOLEAD_XMIWTLD) {
|
||||||
|
log.warn('[Zoho WebToLead] Skipping forwarding - hidden form tokens are not configured')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const descriptionParts = []
|
||||||
|
if (leadData.message) descriptionParts.push(leadData.message)
|
||||||
|
if (leadData.service_interest) descriptionParts.push(`Service Interest: ${leadData.service_interest}`)
|
||||||
|
|
||||||
|
const payload = new URLSearchParams({
|
||||||
|
xnQsjsdp: ZOHO_WEBTOLEAD_XNQSJSDP,
|
||||||
|
zc_gad: ZOHO_WEBTOLEAD_ZC_GAD,
|
||||||
|
xmIwtLD: ZOHO_WEBTOLEAD_XMIWTLD,
|
||||||
|
actionType: ZOHO_WEBTOLEAD_ACTION_TYPE,
|
||||||
|
returnURL: ZOHO_WEBTOLEAD_RETURN_URL,
|
||||||
|
aG9uZXlwb3Q: '',
|
||||||
|
Company: leadData.company || '',
|
||||||
|
'Last Name': leadData.name || 'Unknown',
|
||||||
|
Email: leadData.email || '',
|
||||||
|
Phone: leadData.phone || '',
|
||||||
|
'Zip Code': leadData.zip || '',
|
||||||
|
Description: descriptionParts.join('\n\n'),
|
||||||
|
})
|
||||||
|
|
||||||
|
const controller = new AbortController()
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), ZOHO_TIMEOUT_MS)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(ZOHO_WEBTOLEAD_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: payload.toString(),
|
||||||
|
signal: controller.signal,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text()
|
||||||
|
log.error(`[Zoho WebToLead] Forwarding failed (${response.status}):`, text.slice(0, 500))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('[Zoho WebToLead] Lead forwarded successfully')
|
||||||
|
} catch (fetchErr) {
|
||||||
|
if (fetchErr.name === 'AbortError') {
|
||||||
|
log.warn('[Zoho WebToLead] Lead forwarding timed out after', ZOHO_TIMEOUT_MS, 'ms')
|
||||||
|
} else {
|
||||||
|
log.error('[Zoho WebToLead] Forwarding error:', fetchErr.message)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function forwardLeadToZoho(leadData) {
|
||||||
|
if (ZOHO_FORWARDING_MODE === 'webtolead') {
|
||||||
|
return forwardToZohoWebToLead(leadData)
|
||||||
|
}
|
||||||
|
|
||||||
|
return forwardToZoho(leadData)
|
||||||
|
}
|
||||||
|
|
||||||
// --- Zoho Cases Forwarding (best-effort, fire-and-forget) ---
|
// --- Zoho Cases Forwarding (best-effort, fire-and-forget) ---
|
||||||
async function forwardSupportToZoho(supportData) {
|
async function forwardSupportToZoho(supportData) {
|
||||||
if (!ZOHO_CASES_ENABLED) return
|
if (!ZOHO_CASES_ENABLED) return
|
||||||
|
|
@ -703,7 +775,7 @@ app.post('/api/leads', express.json({ limit: '1mb' }), async (req, res) => {
|
||||||
log.info(`Lead submitted: ${sanitized.email} from ${sanitized.company} (id: ${result.lastInsertRowid})`)
|
log.info(`Lead submitted: ${sanitized.email} from ${sanitized.company} (id: ${result.lastInsertRowid})`)
|
||||||
|
|
||||||
// Fire-and-forget Zoho forwarding (best-effort, non-blocking)
|
// Fire-and-forget Zoho forwarding (best-effort, non-blocking)
|
||||||
forwardToZoho(sanitized).catch(err => log.error('[Zoho] Forwarding error:', err.message))
|
forwardLeadToZoho(sanitized).catch(err => log.error('[Zoho] Forwarding error:', err.message))
|
||||||
|
|
||||||
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) {
|
||||||
|
|
@ -713,7 +785,7 @@ app.post('/api/leads', express.json({ limit: '1mb' }), async (req, res) => {
|
||||||
log.warn(`Duplicate lead email: ${sanitized.email}`)
|
log.warn(`Duplicate lead email: ${sanitized.email}`)
|
||||||
|
|
||||||
// Still forward to Zoho (non-blocking) for existing leads
|
// Still forward to Zoho (non-blocking) for existing leads
|
||||||
forwardToZoho(sanitized).catch(err => log.error('[Zoho] Forwarding error:', err.message))
|
forwardLeadToZoho(sanitized).catch(err => log.error('[Zoho] Forwarding error:', err.message))
|
||||||
|
|
||||||
return res.status(409).json({
|
return res.status(409).json({
|
||||||
error: 'Duplicate lead',
|
error: 'Duplicate lead',
|
||||||
|
|
@ -857,13 +929,16 @@ app.get('*', (req, res, next) => {
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
log.info(`Server running on http://localhost:${PORT}`)
|
log.info(`Server running on http://localhost:${PORT}`)
|
||||||
log.info(`Health check: http://localhost:${PORT}/api/health`)
|
log.info(`Health check: http://localhost:${PORT}/api/health`)
|
||||||
if (ZOHO_ENABLED) {
|
log.info(`Zoho lead forwarding mode: ${ZOHO_FORWARDING_MODE}`)
|
||||||
log.info(`Zoho CRM forwarding: ENABLED`)
|
if (ZOHO_FORWARDING_MODE === 'webtolead') {
|
||||||
|
log.info(`Zoho WebToLead forwarding: ${ZOHO_WEBTOLEAD_ENABLED ? 'ENABLED' : 'DISABLED'}`)
|
||||||
|
} else if (ZOHO_ENABLED) {
|
||||||
|
log.info(`Zoho CRM API forwarding: ENABLED`)
|
||||||
log.info(`Zoho API domain: ${ZOHO_API_DOMAIN}`)
|
log.info(`Zoho API domain: ${ZOHO_API_DOMAIN}`)
|
||||||
log.info(`Zoho Accounts domain: ${ZOHO_ACCOUNTS_DOMAIN}`)
|
log.info(`Zoho Accounts domain: ${ZOHO_ACCOUNTS_DOMAIN}`)
|
||||||
log.info(`Zoho Cases forwarding: ${process.env.ZOHO_CASES_ENABLED === 'true' ? 'ENABLED' : 'DISABLED'}`)
|
log.info(`Zoho Cases forwarding: ${process.env.ZOHO_CASES_ENABLED === 'true' ? 'ENABLED' : 'DISABLED'}`)
|
||||||
} else {
|
} else {
|
||||||
log.info('Zoho CRM forwarding: DISABLED (set ZOHO_ENABLED=true to enable)')
|
log.info('Zoho CRM API forwarding: DISABLED (set ZOHO_ENABLED=true to enable)')
|
||||||
}
|
}
|
||||||
log.info(`Rate limiting: ${rateLimitMax} requests per ${rateLimitWindowMs / 1000} seconds`)
|
log.info(`Rate limiting: ${rateLimitMax} requests per ${rateLimitWindowMs / 1000} seconds`)
|
||||||
log.info(`Security headers: Helmet enabled with CSP configured`)
|
log.info(`Security headers: Helmet enabled with CSP configured`)
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,13 @@ const Footer = () => {
|
||||||
{/* Veteran Owned & Operated */}
|
{/* Veteran Owned & Operated */}
|
||||||
<div className="border-t border-white/10 mt-10 py-6">
|
<div className="border-t border-white/10 mt-10 py-6">
|
||||||
<div className="flex flex-col items-center gap-4 text-center">
|
<div className="flex flex-col items-center gap-4 text-center">
|
||||||
|
<span className="flex h-24 w-20 items-center justify-center rounded-md border border-white/10 bg-white p-1 shadow-sm">
|
||||||
|
<img
|
||||||
|
src="/assets/brand/veteran-owned-certified.webp"
|
||||||
|
alt="SBA Veteran-Owned Certified badge"
|
||||||
|
className="h-full w-full object-contain"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
<p className="text-primary-cyan text-xs font-semibold uppercase tracking-[0.18em]">
|
<p className="text-primary-cyan text-xs font-semibold uppercase tracking-[0.18em]">
|
||||||
Veteran Owned & Operated
|
Veteran Owned & Operated
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import SEO from '@/components/SEO'
|
import SEO from '@/components/SEO'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { ArrowRight, Award, CheckCircle2, Compass, Cpu, Handshake, Headphones, Route, Users, Wrench } from 'lucide-react'
|
import { ArrowRight, Award, CheckCircle2, Compass, Cpu, Handshake, Headphones, Route, Wrench } from 'lucide-react'
|
||||||
|
|
||||||
const proofPoints = [
|
const proofPoints = [
|
||||||
{ label: '25+ years', detail: 'Communications and infrastructure experience', icon: Award, containerClass: '' },
|
{ label: '25+ years', detail: 'Communications and infrastructure experience', icon: Award, containerClass: '' },
|
||||||
|
|
@ -20,7 +20,14 @@ const proofPoints = [
|
||||||
logoClassName: 'h-full w-full scale-[2]',
|
logoClassName: 'h-full w-full scale-[2]',
|
||||||
containerClass: 'p-1 overflow-hidden',
|
containerClass: 'p-1 overflow-hidden',
|
||||||
},
|
},
|
||||||
{ label: 'Veteran owned', detail: 'Disciplined delivery and direct accountability', icon: Users, containerClass: '' },
|
{
|
||||||
|
label: 'Veteran-Owned Certified',
|
||||||
|
detail: 'Disciplined delivery and direct accountability',
|
||||||
|
logo: '/assets/brand/veteran-owned-certified.webp',
|
||||||
|
logoAlt: 'SBA Veteran-Owned Certified badge',
|
||||||
|
logoClassName: 'h-full w-full',
|
||||||
|
containerClass: 'p-1',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const operatingPrinciples = [
|
const operatingPrinciples = [
|
||||||
|
|
|
||||||
|
|
@ -190,10 +190,14 @@ const Home = () => {
|
||||||
<span className="text-sm font-semibold leading-tight text-primary-navy text-center lg:text-left">Cisco Certified Partner</span>
|
<span className="text-sm font-semibold leading-tight text-primary-navy text-center lg:text-left">Cisco Certified Partner</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center gap-2 lg:flex-row lg:gap-3 lg:justify-start">
|
<div className="flex flex-col items-center gap-2 lg:flex-row lg:gap-3 lg:justify-start">
|
||||||
<span className="flex h-16 w-20 shrink-0 items-center justify-center rounded-md border border-border bg-white">
|
<span className="flex h-16 w-20 shrink-0 items-center justify-center rounded-md border border-border bg-white p-1">
|
||||||
<ShieldCheck className="h-8 w-8 text-primary-blue" aria-hidden="true" />
|
<img
|
||||||
|
src="/assets/brand/veteran-owned-certified.webp"
|
||||||
|
alt="SBA Veteran-Owned Certified badge"
|
||||||
|
className="h-full w-full object-contain"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-semibold leading-tight text-primary-navy text-center lg:text-left">Veteran Owned</span>
|
<span className="text-sm font-semibold leading-tight text-primary-navy text-center lg:text-left">Veteran-Owned Certified</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center gap-2 lg:flex-row lg:gap-3 lg:justify-start">
|
<div className="flex flex-col items-center gap-2 lg:flex-row lg:gap-3 lg:justify-start">
|
||||||
<span className="font-numeric flex h-16 w-20 shrink-0 items-center justify-center rounded-md border border-border bg-white text-2xl font-semibold text-primary-navy">
|
<span className="font-numeric flex h-16 w-20 shrink-0 items-center justify-center rounded-md border border-border bg-white text-2xl font-semibold text-primary-navy">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue