Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,9 @@ WORDPRESS_API_URL=https://wp.keploy.io/graphql
# override here only if you need a different telemetry endpoint or key.
# NEXT_PUBLIC_TELEMETRY_URL=https://telemetry.keploy.io
# NEXT_PUBLIC_RECAPTCHA_SITE_KEY=<score-based reCAPTCHA Enterprise key>

# Google Chat notification for newsletter/lead form submissions. Server-only
# secret (NOT NEXT_PUBLIC_*) — never exposed to the browser. Create it in the
# Chat space: Apps & integrations → Webhooks → add → copy the URL here.
# If unset, submissions still succeed but no Chat message is delivered.
# GOOGLE_CHAT_WEBHOOK_URL=https://chat.googleapis.com/v1/spaces/.../messages?key=...&token=...
17 changes: 17 additions & 0 deletions components/subscribe-newsletter.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useRef, useState } from "react";
import { useRouter } from "next/router";
import styles from "./subscribe-newsletter.module.css";
import { newsLetterSubscriptionUrl } from '../services/constants'
import { useInvisibleRecaptcha, RecaptchaAttribution } from "../lib/use-invisible-recaptcha";
Expand Down Expand Up @@ -39,6 +40,7 @@ export const subscribeMutation = (formData: { fullName: string, email: string, c


export default function SubscribeNewsletter(props: { isSmallScreen?: boolean }) {
const router = useRouter();
const myComponent = useRef<HTMLDivElement>(null);
const [isVisible, setVisible] = useState<boolean>(true);
const [fullName, setFullName] = useState('');
Expand Down Expand Up @@ -108,6 +110,21 @@ export default function SubscribeNewsletter(props: { isSmallScreen?: boolean })
});
});

// Google Chat notification — fire-and-forget, fail-open. Forwards the lead
Comment thread
dhananjay6561 marked this conversation as resolved.
// to a Chat space via the server-side /api/blog-lead-notify handler (which
// holds the webhook secret). Never gates the subscription.
fetch(`${router.basePath || ''}/api/blog-lead-notify`, {
Comment thread
dhananjay6561 marked this conversation as resolved.
Comment thread
dhananjay6561 marked this conversation as resolved.
method: 'POST',
headers: { 'Content-Type': 'application/json' },
keepalive: true,
body: JSON.stringify({
fullName: fullName.trim(),
email: email.trim().toLowerCase(),
companyName: companyName.trim(),
page,
}),
}).catch(() => {});

handleSubscribe(payload)
};
const isSubscribeDisabled = ()=>{
Expand Down
88 changes: 88 additions & 0 deletions pages/api/blog-lead-notify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import type { NextApiRequest, NextApiResponse } from "next";

// Server-side handler for blog newsletter / lead submissions. It forwards each
// lead to a Google Chat space via an incoming webhook (GOOGLE_CHAT_WEBHOOK_URL).
// Nothing is persisted here — the newsletter subscription (api-server) and the
// MQL lead (telemetry /blog-mql) are still handled by their own paths. This is
// a notification-only side channel, mirroring the landing repo's trial form.
//
// Security model:
// • GOOGLE_CHAT_WEBHOOK_URL is a server-only secret (NOT NEXT_PUBLIC_*), so it
// never reaches the browser bundle. This handler runs server-side only, so
// the webhook URL is never exposed to the client, git, or logs.
// • Create it in Google Chat: open the space → Apps & integrations → Webhooks
// → add one → copy the URL into the env var. Rotate by deleting/recreating.
// If the env var is unset the submission still succeeds for the user, but the
// lead is NOT delivered.

const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;

export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method !== "POST") {
res.setHeader("Allow", "POST");
return res.status(405).json({ ok: false, error: "method_not_allowed" });
}

const data = (req.body && typeof req.body === "object" ? req.body : {}) as Record<
string,
unknown
>;

// Honeypot — silently accept (and drop) bot submissions.
if (data.company_website) {
Comment thread
dhananjay6561 marked this conversation as resolved.
Comment thread
dhananjay6561 marked this conversation as resolved.
return res.status(200).json({ ok: true, delivered: false });
}

const name = String(data.fullName ?? "").trim();
const email = String(data.email ?? "").trim();
Comment thread
dhananjay6561 marked this conversation as resolved.
Outdated
// Re-validate server-side: a request could hit this endpoint directly and
// bypass the client-side checks.
if (!name || !EMAIL_RE.test(email)) {
Comment thread
dhananjay6561 marked this conversation as resolved.
Comment thread
dhananjay6561 marked this conversation as resolved.
return res.status(400).json({ ok: false, error: "validation" });
}

const lead = {
Comment thread
dhananjay6561 marked this conversation as resolved.
name,
email,
company: String(data.companyName ?? "").trim(),
page: String(data.page ?? ""),
submittedAt: new Date().toISOString(),
};

const webhook = process.env.GOOGLE_CHAT_WEBHOOK_URL;
if (!webhook) {
// No PII in logs. Set GOOGLE_CHAT_WEBHOOK_URL to deliver leads to the space.
console.error(
Comment thread
dhananjay6561 marked this conversation as resolved.
Outdated
Comment thread
dhananjay6561 marked this conversation as resolved.
Outdated
"[blog-lead] GOOGLE_CHAT_WEBHOOK_URL is not configured — lead accepted but NOT delivered. Set the env var to enable delivery.",
);
return res.status(200).json({ ok: true, delivered: false });
}

try {
// Google Chat renders *bold* / _italic_ and <url|label> in `text` messages.
const text =
`*📨 New Keploy blog subscriber*\n` +
`*Name:* ${lead.name}\n` +
`*Email:* ${lead.email}\n` +
`*Company:* ${lead.company || "—"}\n` +
`*Page:* ${lead.page ? `<${lead.page}|${lead.page}>` : "—"}\n` +
Comment thread
dhananjay6561 marked this conversation as resolved.
Outdated
Comment thread
dhananjay6561 marked this conversation as resolved.
Outdated
`*Submitted:* ${lead.submittedAt}`;

const chatRes = await fetch(webhook, {
Comment thread
dhananjay6561 marked this conversation as resolved.
method: "POST",
headers: { "Content-Type": "application/json; charset=UTF-8" },
body: JSON.stringify({ text }),
});
return res.status(200).json({ ok: true, delivered: chatRes.ok });
} catch (err) {
console.error(
"[blog-lead] delivery to Google Chat failed — verify GOOGLE_CHAT_WEBHOOK_URL is a valid incoming-webhook URL. Lead was NOT delivered.",
err,
);
// Never fail the user — the newsletter subscription path is unaffected.
return res.status(200).json({ ok: true, delivered: false });
}
}
Loading