From 0d5766300efbbe0a844e5d697b5d9b76e30c8a48 Mon Sep 17 00:00:00 2001 From: Dave Samojlenko Date: Thu, 19 Sep 2024 10:01:29 -0400 Subject: [PATCH 1/3] Notify Interceptor --- app/(gcforms)/[locale]/layout.tsx | 2 + lib/integration/notifyConnector.ts | 17 +++ lib/notifyCatcher/NotifyCatcher.tsx | 175 ++++++++++++++++++++++++++++ lib/notifyCatcher/actions.ts | 13 +++ lib/notifyCatcher/index.ts | 17 +++ tailwind.config.js | 6 +- 6 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 lib/notifyCatcher/NotifyCatcher.tsx create mode 100644 lib/notifyCatcher/actions.ts create mode 100644 lib/notifyCatcher/index.ts diff --git a/app/(gcforms)/[locale]/layout.tsx b/app/(gcforms)/[locale]/layout.tsx index 9deb26a945..42ce62239b 100644 --- a/app/(gcforms)/[locale]/layout.tsx +++ b/app/(gcforms)/[locale]/layout.tsx @@ -1,6 +1,7 @@ import { authCheckAndThrow } from "@lib/actions"; import { ClientContexts } from "@clientComponents/globals/ClientContexts"; import { ReactHydrationCheck } from "@clientComponents/globals"; +import { NotifyCatcher } from "@lib/notifyCatcher/NotifyCatcher"; export default async function Layout({ children }: { children: React.ReactNode }) { const { session } = await authCheckAndThrow().catch(() => ({ session: null })); @@ -8,6 +9,7 @@ export default async function Layout({ children }: { children: React.ReactNode } <> {children} + {process.env.APP_ENV === "local" && } ); } diff --git a/lib/integration/notifyConnector.ts b/lib/integration/notifyConnector.ts index be25de0e2f..bba8b7d9c1 100644 --- a/lib/integration/notifyConnector.ts +++ b/lib/integration/notifyConnector.ts @@ -1,4 +1,6 @@ import { logMessage } from "@lib/logger"; +import { notifyCatcher } from "@lib/notifyCatcher"; + import axios from "axios"; const NotifyClient = axios.create({ @@ -20,6 +22,21 @@ export const sendEmail = async ( return; } + if (process.env.APP_ENV === "local") { + try { + notifyCatcher(email, personalisation); + } catch (e) { + logMessage.error("NotifyCatcher failed to catch the email", e); + + // just log it out + logMessage.info("Development Notify email sending:", "", { email, personalisation }); + logMessage.info("To: " + email); + logMessage.info("Subject: " + personalisation.subject); + logMessage.info("Body: " + personalisation.formResponse); + } + return; + } + const templateId = process.env.TEMPLATE_ID; if (!templateId) { throw new Error("No Notify template ID configured."); diff --git a/lib/notifyCatcher/NotifyCatcher.tsx b/lib/notifyCatcher/NotifyCatcher.tsx new file mode 100644 index 0000000000..37234a1ca3 --- /dev/null +++ b/lib/notifyCatcher/NotifyCatcher.tsx @@ -0,0 +1,175 @@ +"use client"; +import { useEffect, useRef, useState } from "react"; +import { fetchMessages, resetMessages } from "./actions"; +import Markdown from "markdown-to-jsx"; + +interface Message { + email: string; + personalisation: { + subject: string; + formResponse: string; + }; +} + +export const NotifyCatcher = () => { + const ref = useRef(null); + const [messages, setMessages] = useState([]); + const [loading, setLoading] = useState(true); + const [visible, setVisible] = useState(false); + + const getMessages = async () => { + setLoading(true); + const messages = await fetchMessages(); + setMessages(messages); + setLoading(false); + }; + + const open = () => { + getMessages(); + setVisible(true); + }; + + const handleClickOutside = (event: MouseEvent) => { + if (ref.current && !ref.current.contains(event.target as Node)) { + setVisible(false); + } + }; + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setVisible(false); + } + }; + + const clearMessages = async () => { + resetMessages(); + setMessages([]); + }; + + useEffect(() => { + getMessages(); + document.addEventListener("mousedown", handleClickOutside); + document.addEventListener("keydown", handleKeyDown); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + document.removeEventListener("keydown", handleKeyDown); + }; + }, []); + + return ( + <> + {!visible && ( +
open()} + > + + + + Notify Intercept +
+ )} + {visible && ( +
+ {loading ? ( +
Loading...
+ ) : ( + <> +
+
+ + + +

Notify Intercept

+
+
+ {messages.length > 0 && ( + + )} + + +
+
+
+ {messages.length === 0 && ( +
+ No messages to show +
+ )} + {messages && + messages.map((message, index) => { + return ( +
+
+
To:
+
{message.email}
+
Subject:
+
{message.personalisation.subject}
+
+
+
+ + {message.personalisation.formResponse} + +
+
+
+ ); + })} +
+ + )} +
+ )} + + ); +}; diff --git a/lib/notifyCatcher/actions.ts b/lib/notifyCatcher/actions.ts new file mode 100644 index 0000000000..c4016c0c7e --- /dev/null +++ b/lib/notifyCatcher/actions.ts @@ -0,0 +1,13 @@ +"use server"; +import { getRedisInstance } from "@lib/integration/redisConnector"; + +export const fetchMessages = async () => { + const redis = await getRedisInstance(); + const emails = await redis.lrange("notifyEmails", 0, -1); + return emails.map((email) => JSON.parse(email)); +}; + +export const resetMessages = async () => { + const redis = await getRedisInstance(); + await redis.del("notifyEmails"); +}; diff --git a/lib/notifyCatcher/index.ts b/lib/notifyCatcher/index.ts new file mode 100644 index 0000000000..14ff2b757d --- /dev/null +++ b/lib/notifyCatcher/index.ts @@ -0,0 +1,17 @@ +import { getRedisInstance } from "@lib/integration/redisConnector"; +import { logMessage } from "@lib/logger"; + +export const notifyCatcher = async ( + email: string, + personalisation: Record> +) => { + try { + const redis = await getRedisInstance(); + await redis.lpush("notifyEmails", JSON.stringify({ email, personalisation })); + } catch (e) { + logMessage.info("Development Notify email sending:", "", { email, personalisation }); + logMessage.info("To: " + email); + logMessage.info("Subject: " + personalisation.subject); + logMessage.info("Body: " + personalisation.formResponse); + } +}; diff --git a/tailwind.config.js b/tailwind.config.js index 0c773ab31b..ecc8edb7b9 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -181,6 +181,10 @@ module.exports = { xxs: { max: "290px" }, }, }, - content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}"], + content: [ + "./app/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + "./lib/notifyCatcher/**/*.{ts,tsx}", + ], plugins: [], }; From 82b4b521923674c975b4921922f9fcfc3ba7472f Mon Sep 17 00:00:00 2001 From: Dave Samojlenko Date: Fri, 19 Dec 2025 13:58:13 -0500 Subject: [PATCH 2/3] Some improvements --- lib/integration/notifyConnector.ts | 4 +- lib/notifyCatcher/NotifyCatcher.tsx | 116 ++++++++++++++++++++++++---- lib/notifyCatcher/actions.ts | 11 +++ 3 files changed, 113 insertions(+), 18 deletions(-) diff --git a/lib/integration/notifyConnector.ts b/lib/integration/notifyConnector.ts index 83ed78e286..d33045d852 100644 --- a/lib/integration/notifyConnector.ts +++ b/lib/integration/notifyConnector.ts @@ -21,10 +21,10 @@ export const sendEmail = async (email: string, personalisation: Personalisation, try { notifyCatcher(email, personalisation); } catch (e) { - logMessage.error("NotifyCatcher failed to catch the email", e); + logMessage.error(`NotifyCatcher failed to catch the email: ${(e as Error).message}`); // just log it out - logMessage.info("Development Notify email sending:", "", { email, personalisation }); + logMessage.info("Development Notify email sending"); logMessage.info("To: " + email); logMessage.info("Subject: " + personalisation.subject); logMessage.info("Body: " + personalisation.formResponse); diff --git a/lib/notifyCatcher/NotifyCatcher.tsx b/lib/notifyCatcher/NotifyCatcher.tsx index 37234a1ca3..df32da6965 100644 --- a/lib/notifyCatcher/NotifyCatcher.tsx +++ b/lib/notifyCatcher/NotifyCatcher.tsx @@ -1,6 +1,6 @@ "use client"; import { useEffect, useRef, useState } from "react"; -import { fetchMessages, resetMessages } from "./actions"; +import { fetchMessages, resetMessages, deleteMessageByIndex } from "./actions"; import Markdown from "markdown-to-jsx"; interface Message { @@ -46,6 +46,41 @@ export const NotifyCatcher = () => { setMessages([]); }; + const deleteMessage = async (index: number) => { + await deleteMessageByIndex(index); + setMessages(messages.filter((_, i) => i !== index)); + }; + + const is2FAMessage = (message: Message): boolean => { + return ( + message.personalisation.subject.includes("Your security code") || + message.personalisation.subject.includes("Votre code de sécurité") + ); + }; + + const extract2FACode = (message: Message): string | null => { + if (!is2FAMessage(message)) return null; + // Extract the code - it's typically a short alphanumeric string + const lines = message.personalisation.formResponse.split("\n").filter((line) => line.trim()); + // The code is usually the last non-empty line or a short alphanumeric string + for (const line of lines) { + const trimmed = line.trim(); + // Look for a short code (typically 4-6 characters, alphanumeric) + if (trimmed.length >= 4 && trimmed.length <= 8 && /^[A-Za-z0-9]+$/.test(trimmed)) { + return trimmed; + } + } + return null; + }; + + const copyCodeToClipboard = async (code: string) => { + try { + await navigator.clipboard.writeText(code); + } catch (err) { + // Ignore clipboard errors + } + }; + useEffect(() => { getMessages(); document.addEventListener("mousedown", handleClickOutside); @@ -60,26 +95,26 @@ export const NotifyCatcher = () => { <> {!visible && (
open()} + title={`${messages.length} intercepted message${messages.length !== 1 ? "s" : ""}`} > - Notify Intercept + {messages.length}
)} {visible && (
{loading ? (
Loading...
@@ -97,6 +132,9 @@ export const NotifyCatcher = () => {

Notify Intercept

+ + {messages.length} +
{messages.length > 0 && ( @@ -144,19 +182,65 @@ export const NotifyCatcher = () => { )} {messages && messages.map((message, index) => { + const securityCode = extract2FACode(message); return (
-
-
To:
-
{message.email}
-
Subject:
-
{message.personalisation.subject}
-
-
-
+
+
+
To:
+
{message.email}
+
Subject:
+
{message.personalisation.subject}
+
+ +
+
+ {securityCode ? ( +
+
+ + Security Code + +
+ {securityCode} +
+
+ +
+ ) : null} +
{message.personalisation.formResponse} diff --git a/lib/notifyCatcher/actions.ts b/lib/notifyCatcher/actions.ts index c4016c0c7e..eaaa416b03 100644 --- a/lib/notifyCatcher/actions.ts +++ b/lib/notifyCatcher/actions.ts @@ -11,3 +11,14 @@ export const resetMessages = async () => { const redis = await getRedisInstance(); await redis.del("notifyEmails"); }; + +export const deleteMessageByIndex = async (index: number) => { + const redis = await getRedisInstance(); + const emails = await redis.lrange("notifyEmails", 0, -1); + if (index >= 0 && index < emails.length) { + // Remove the specific item by setting it to a placeholder, then removing all occurrences + const placeholder = "__DELETE_PLACEHOLDER__" + Date.now(); + await redis.lset("notifyEmails", index, placeholder); + await redis.lrem("notifyEmails", 1, placeholder); + } +}; From 0d396b54a7a5b44c27db06a519020b039147c09b Mon Sep 17 00:00:00 2001 From: Dave Samojlenko Date: Fri, 19 Dec 2025 14:01:01 -0500 Subject: [PATCH 3/3] Try/catch handled in plugin --- lib/integration/notifyConnector.ts | 12 +----------- lib/notifyCatcher/index.ts | 5 ++++- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/lib/integration/notifyConnector.ts b/lib/integration/notifyConnector.ts index d33045d852..28faeb2489 100644 --- a/lib/integration/notifyConnector.ts +++ b/lib/integration/notifyConnector.ts @@ -18,17 +18,7 @@ export const sendEmail = async (email: string, personalisation: Personalisation, } if (process.env.APP_ENV === "local") { - try { - notifyCatcher(email, personalisation); - } catch (e) { - logMessage.error(`NotifyCatcher failed to catch the email: ${(e as Error).message}`); - - // just log it out - logMessage.info("Development Notify email sending"); - logMessage.info("To: " + email); - logMessage.info("Subject: " + personalisation.subject); - logMessage.info("Body: " + personalisation.formResponse); - } + notifyCatcher(email, personalisation); return; } diff --git a/lib/notifyCatcher/index.ts b/lib/notifyCatcher/index.ts index aa61d4948e..ac1335c225 100644 --- a/lib/notifyCatcher/index.ts +++ b/lib/notifyCatcher/index.ts @@ -7,7 +7,10 @@ export const notifyCatcher = async (email: string, personalisation: Personalisat const redis = await getRedisInstance(); await redis.lpush("notifyEmails", JSON.stringify({ email, personalisation })); } catch (e) { - logMessage.info("Development Notify email sending:", "", { email, personalisation }); + logMessage.error(`NotifyCatcher failed to catch the email: ${(e as Error).message}`); + + // just log it out + logMessage.info("Development Notify email sending"); logMessage.info("To: " + email); logMessage.info("Subject: " + personalisation.subject); logMessage.info("Body: " + personalisation.formResponse);