diff --git a/apps/page/pages/api/roadmap/submit-triage.ts b/apps/page/pages/api/roadmap/submit-triage.ts index 5065652..d1e6939 100644 --- a/apps/page/pages/api/roadmap/submit-triage.ts +++ b/apps/page/pages/api/roadmap/submit-triage.ts @@ -1,7 +1,29 @@ +import arcjet, { detectBot, tokenBucket } from "@arcjet/next"; import { supabaseAdmin } from "@changes-page/supabase/admin"; import type { NextApiRequest, NextApiResponse } from "next"; import { v4 } from "uuid"; +import { escape } from "validator"; import { getAuthenticatedVisitor } from "../../../lib/visitor-auth"; +import inngestClient from "../../../utils/inngest"; + +const aj = process.env.ARCJET_KEY + ? arcjet({ + key: process.env.ARCJET_KEY, + rules: [ + tokenBucket({ + mode: "LIVE", + characteristics: ["userId"], + refillRate: 5, + interval: "1h", + capacity: 10, + }), + detectBot({ + mode: "LIVE", + block: ["AUTOMATED"], + }), + ], + }) + : undefined; export default async function submitTriageItem( req: NextApiRequest, @@ -54,15 +76,35 @@ export default async function submitTriageItem( .json({ success: false, error: "Authentication required" }); } + if (aj) { + const decision = await aj.protect(req, { + userId: visitor.id, + requested: 1, + }); + + if (decision.isDenied()) { + console.log( + "roadmap/submit-triage: [Arcjet Block]", + visitor.id, + decision.reason + ); + + return res.status(403).json({ + success: false, + error: "Request blocked.", + }); + } + } + try { - const { data: boardCheck, error: boardCheckError } = await supabaseAdmin + const { data: board, error: boardCheckError } = await supabaseAdmin .from("roadmap_boards") - .select("id, is_public") + .select("id, is_public, title, page_id") .eq("id", board_id) .eq("is_public", true) .maybeSingle(); - if (boardCheckError || !boardCheck) { + if (boardCheckError || !board) { return res .status(404) .json({ success: false, error: "Board not found or not public" }); @@ -87,6 +129,20 @@ export default async function submitTriageItem( .json({ success: false, error: "Failed to submit item" }); } + try { + await inngestClient.send({ + name: "email/roadmap.triage-submitted", + data: { + page_id: board.page_id, + board_id: board.id, + board_title: board.title, + item_title: escape(trimmedTitle), + }, + }); + } catch (emailError) { + console.error("submitTriageItem [Email Error]", emailError); + } + res.status(200).json({ success: true, item: triageItem, diff --git a/apps/web/inngest/email/send-roadmap-triage-notification.ts b/apps/web/inngest/email/send-roadmap-triage-notification.ts new file mode 100644 index 0000000..31f7a78 --- /dev/null +++ b/apps/web/inngest/email/send-roadmap-triage-notification.ts @@ -0,0 +1,182 @@ +import { supabaseAdmin } from "@changes-page/supabase/admin"; +import { getAppBaseURL } from "../../utils/helpers"; +import inngestClient from "../../utils/inngest"; +import postmarkClient from "../../utils/postmark"; + +interface EventData { + page_id: string; + board_id: string; + board_title: string; + item_title: string; +} + +export const sendRoadmapTriageNotification = inngestClient.createFunction( + { name: "Email: Roadmap triage submission" }, + { event: "email/roadmap.triage-submitted" }, + async ({ event }) => { + const { page_id, board_id, board_title, item_title }: EventData = + event.data; + + console.log("Sending roadmap triage notification", { + page_id, + board_id, + item_title, + }); + + const { data: page, error: pageError } = await supabaseAdmin + .from("pages") + .select( + ` + title, + user_id, + page_settings(page_logo) + ` + ) + .eq("id", page_id) + .single(); + + if (!page || pageError) { + console.error("Error fetching page:", pageError); + throw new Error("Page not found"); + } + + const { data: authUser, error: authError } = + await supabaseAdmin.auth.admin.getUserById(page.user_id); + + if (!authUser || authError || !authUser.user?.email) { + console.error("Error fetching user:", authError); + throw new Error("User not found or email missing"); + } + + const pageName = page.title; + const pageLogoUrl = page.page_settings?.page_logo || null; + const adminEmail = authUser.user.email; + + const triageUrl = `${getAppBaseURL()}/pages/${page_id}/roadmap/${board_id}`; + + const result = await postmarkClient.sendEmail({ + MessageStream: "outbound", + From: "notification@mail.changes.page", + To: adminEmail, + Subject: `New roadmap idea submitted: ${item_title}`, + HtmlBody: ` + + +
+ + +A user has submitted a new idea to your roadmap: ${board_title}
+ +You can review and approve this idea from your roadmap triage section.
+ + +- Get started by creating your first roadmap board. -
-