feat: add POST /api/notifications endpoint#241
Conversation
REST API endpoint for sending notification emails via Resend, mirroring the send_email MCP tool. Supports to, cc, subject, text (Markdown), html, custom headers, and room_id for chat footer links. Co-Authored-By: Claude Opus 4.6 <[email protected]>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedAn error occurred during the review process. Please try again later. 📝 WalkthroughWalkthroughAdds a new notifications endpoint and supporting server-side email pipeline: route handlers (OPTIONS/POST), request validation (Zod), authenticated notification creation, centralized email processing/sending, and adjustments to a tool to use the new processor. CORS and route cache controls are included. Changes
Sequence DiagramsequenceDiagram
participant Client as Client
participant Route as "app/api/notifications/route.ts"
participant Handler as "createNotificationHandler"
participant Auth as "validateAuthContext"
participant Validator as "validateCreateNotificationBody"
participant Processor as "processAndSendEmail"
participant EmailAPI as "Resend / sendEmail wrapper"
participant Response as Response
Client->>Route: POST /api/notifications
Route->>Handler: forward NextRequest
Handler->>Auth: validateAuthContext(request)
Auth-->>Handler: auth result
Handler->>Validator: validateCreateNotificationBody(body)
Validator-->>Handler: validated body / error
Handler->>Processor: processAndSendEmail({to,cc,subject,text,html,headers,room_id})
Processor->>EmailAPI: sendEmailWithResend(payload)
EmailAPI-->>Processor: success / error
Processor-->>Handler: success {id,message} / error
Handler->>Response: NextResponse JSON (200 / 400 / 502) with CORS headers
Response-->>Client: JSON payload
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 1✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Remove 'to' param from request body. The recipient email is now automatically resolved from the account_emails table using the authenticated account's ID. Returns 400 if no email is found. Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
lib/notifications/createNotificationHandler.ts (1)
20-73: Split this handler into smaller units to meet the file-level function-size rule.This function currently mixes auth, parsing, validation, enrichment, rendering, provider invocation, and response mapping in one block. Extracting a couple of helpers will improve readability and testability.
As per coding guidelines "lib/**/*.ts: ... Keep functions under 50 lines".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/notifications/createNotificationHandler.ts` around lines 20 - 73, The createNotificationHandler function is too large and should be split into small helpers: extract a parseAndValidateBody helper that calls safeParseJson and validateCreateNotificationBody and returns the validated payload (or a NextResponse on error); extract an enrichAndRenderEmail helper that accepts the validated payload and uses selectRoomWithArtist, getEmailFooter, and marked to return the final email fields (to, cc, subject, htmlWithFooter, headers, room_id); and extract a mapSendResultToResponse helper that takes the sendEmailWithResend result and RECOUP_FROM_EMAIL and returns the appropriate NextResponse (including error mapping and getCorsHeaders). Then simplify createNotificationHandler to orchestrate: validateAuthContext, call parseAndValidateBody, call enrichAndRenderEmail, call sendEmailWithResend, and call mapSendResultToResponse. Use the existing function names (createNotificationHandler, validateAuthContext, safeParseJson, validateCreateNotificationBody, selectRoomWithArtist, getEmailFooter, marked, sendEmailWithResend, getCorsHeaders) to locate code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@lib/notifications/createNotificationHandler.ts`:
- Around line 21-35: validateAuthContext() returns the authenticated context but
the code calls selectRoomWithArtist(room_id) without scoping to that context;
update createNotificationHandler to authorize the room_id against the
authenticated account/org from validateAuthContext() before using room metadata:
after getting authResult (the object returned by validateAuthContext), extract
the account/org id (e.g., authResult.accountId or authResult.orgId), then either
replace selectRoomWithArtist(room_id) with a scoped lookup (e.g.,
selectRoomWithArtistForAccount(room_id, accountId)) or fetch roomData and assert
roomData.account_id/org_id matches the authenticated id and return an
unauthorized response (or strip room metadata) if it does not; ensure you
reference validateAuthContext, selectRoomWithArtist and the room_id variable
when making this change.
- Around line 48-54: In createNotificationHandler, the error shape from the
provider is a string, so change the extraction from data?.error?.message to
prefer the string error (e.g., const reason = data?.error || data?.message ||
String(data)); then return NextResponse.json with error: reason || `Failed to
send email from ${RECOUP_FROM_EMAIL} to ${to.join(", ")}.` so the upstream
reason is preserved when result (the NextResponse) is returned.
In `@lib/notifications/validateCreateNotificationBody.ts`:
- Line 8: The Zod schema in validateCreateNotificationBody.ts uses the
deprecated option key `{ message: "..." }` on z.string — update the schema for
the `subject` field to use `{ error: "subject is required" }` instead of `{
message: "subject is required" }`, or simply remove that option and rely on
`z.string().min(1, "subject cannot be empty")`; locate the `subject:
z.string(...)` entry in the exported validation schema (e.g., the create
notification schema variable/function) and make this change so Zod v4 correctly
reports the error.
---
Nitpick comments:
In `@lib/notifications/createNotificationHandler.ts`:
- Around line 20-73: The createNotificationHandler function is too large and
should be split into small helpers: extract a parseAndValidateBody helper that
calls safeParseJson and validateCreateNotificationBody and returns the validated
payload (or a NextResponse on error); extract an enrichAndRenderEmail helper
that accepts the validated payload and uses selectRoomWithArtist,
getEmailFooter, and marked to return the final email fields (to, cc, subject,
htmlWithFooter, headers, room_id); and extract a mapSendResultToResponse helper
that takes the sendEmailWithResend result and RECOUP_FROM_EMAIL and returns the
appropriate NextResponse (including error mapping and getCorsHeaders). Then
simplify createNotificationHandler to orchestrate: validateAuthContext, call
parseAndValidateBody, call enrichAndRenderEmail, call sendEmailWithResend, and
call mapSendResultToResponse. Use the existing function names
(createNotificationHandler, validateAuthContext, safeParseJson,
validateCreateNotificationBody, selectRoomWithArtist, getEmailFooter, marked,
sendEmailWithResend, getCorsHeaders) to locate code.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
lib/notifications/__tests__/createNotificationHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/notifications/__tests__/validateCreateNotificationBody.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (3)
app/api/notifications/route.tslib/notifications/createNotificationHandler.tslib/notifications/validateCreateNotificationBody.ts
| const authResult = await validateAuthContext(request); | ||
| if (authResult instanceof NextResponse) { | ||
| return authResult; | ||
| } | ||
|
|
||
| const body = await safeParseJson(request); | ||
| const validated = validateCreateNotificationBody(body); | ||
| if (validated instanceof NextResponse) { | ||
| return validated; | ||
| } | ||
|
|
||
| const { to, cc = [], subject, text, html = "", headers = {}, room_id } = validated; | ||
|
|
||
| const roomData = room_id ? await selectRoomWithArtist(room_id) : null; | ||
| const footer = getEmailFooter(room_id, roomData?.artist_name || undefined); |
There was a problem hiding this comment.
Authorize room_id against the authenticated account context before room lookup.
validateAuthContext() is called, but room_id is resolved via selectRoomWithArtist(room_id) without scoping it to the authenticated account/org. This allows authenticated callers to reference unrelated room IDs and enrich outbound emails with unauthorized workspace/artist metadata.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/notifications/createNotificationHandler.ts` around lines 21 - 35,
validateAuthContext() returns the authenticated context but the code calls
selectRoomWithArtist(room_id) without scoping to that context; update
createNotificationHandler to authorize the room_id against the authenticated
account/org from validateAuthContext() before using room metadata: after getting
authResult (the object returned by validateAuthContext), extract the account/org
id (e.g., authResult.accountId or authResult.orgId), then either replace
selectRoomWithArtist(room_id) with a scoped lookup (e.g.,
selectRoomWithArtistForAccount(room_id, accountId)) or fetch roomData and assert
roomData.account_id/org_id matches the authenticated id and return an
unauthorized response (or strip room metadata) if it does not; ensure you
reference validateAuthContext, selectRoomWithArtist and the room_id variable
when making this change.
Extract room lookup, footer generation, markdown rendering, and Resend call into lib/emails/processAndSendEmail.ts. Both the POST /api/notifications handler and the send_email MCP tool now call the same shared function. Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
🧹 Nitpick comments (2)
lib/emails/processAndSendEmail.ts (2)
38-70: Normalize unexpected exceptions to preserve the declared result contract.Line 43/45/48 call external helpers that can still throw; without a
try/catch, callers may get unhandled exceptions instead of aProcessAndSendEmailError.♻️ Suggested hardening
export async function processAndSendEmail( input: ProcessAndSendEmailInput, ): Promise<ProcessAndSendEmailResult> { - const { to, cc = [], subject, text, html = "", headers = {}, room_id } = input; - - const roomData = room_id ? await selectRoomWithArtist(room_id) : null; - const footer = getEmailFooter(room_id, roomData?.artist_name || undefined); - const bodyHtml = html || (text ? await marked(text) : ""); - const htmlWithFooter = `${bodyHtml}\n\n${footer}`; - - const result = await sendEmailWithResend({ - from: RECOUP_FROM_EMAIL, - to, - cc: cc.length > 0 ? cc : undefined, - subject, - html: htmlWithFooter, - headers, - }); - - if (result instanceof NextResponse) { - const data = await result.json(); - return { - success: false, - error: data?.error?.message || `Failed to send email from ${RECOUP_FROM_EMAIL} to ${to.join(", ")}.`, - }; - } - - return { - success: true, - message: `Email sent successfully from ${RECOUP_FROM_EMAIL} to ${to.join(", ")}. CC: ${cc.length > 0 ? cc.join(", ") : "none"}.`, - id: result.id, - }; + try { + const { to, cc = [], subject, text, html = "", headers = {}, room_id } = input; + + const roomData = room_id ? await selectRoomWithArtist(room_id) : null; + const footer = getEmailFooter(room_id, roomData?.artist_name || undefined); + const bodyHtml = html || (text ? await marked(text) : ""); + const htmlWithFooter = `${bodyHtml}\n\n${footer}`; + + const result = await sendEmailWithResend({ + from: RECOUP_FROM_EMAIL, + to, + cc: cc.length > 0 ? cc : undefined, + subject, + html: htmlWithFooter, + headers, + }); + + if (result instanceof NextResponse) { + const data = await result.json(); + return { + success: false, + error: data?.error?.message || "Failed to send email.", + }; + } + + return { + success: true, + message: "Email sent successfully.", + id: result.id, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Failed to process and send email.", + }; + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/emails/processAndSendEmail.ts` around lines 38 - 70, Wrap the body of processAndSendEmail in a try/catch to normalize any thrown exceptions from selectRoomWithArtist, getEmailFooter, marked, or sendEmailWithResend and always return a ProcessAndSendEmailResult; on error return success: false and set error to a concise message (include the caught error.message when available) instead of letting exceptions propagate, preserving the declared result contract and keeping existing success path intact.
61-67: Avoid returning raw recipient addresses in public messages.Line 61 and Line 67 include
to/ccvalues in returned strings. Prefer generic response messages and keep address details out of API/tool payloads.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/emails/processAndSendEmail.ts` around lines 61 - 67, The function processAndSendEmail.ts currently embeds raw recipient addresses (variables to and cc) and the sender (RECOUP_FROM_EMAIL) into the returned message object; change the success and error message strings to avoid exposing email addresses by replacing them with generic text (e.g., "Email sent successfully" or "Failed to send email") and, if needed for internal diagnostics, log the full addresses locally rather than returning them in the response object. Update the return objects where message uses to.join(", ") and cc.join(", ") (and the error message that references RECOUP_FROM_EMAIL and to) so they no longer include identifiable email data.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@lib/emails/processAndSendEmail.ts`:
- Around line 38-70: Wrap the body of processAndSendEmail in a try/catch to
normalize any thrown exceptions from selectRoomWithArtist, getEmailFooter,
marked, or sendEmailWithResend and always return a ProcessAndSendEmailResult; on
error return success: false and set error to a concise message (include the
caught error.message when available) instead of letting exceptions propagate,
preserving the declared result contract and keeping existing success path
intact.
- Around line 61-67: The function processAndSendEmail.ts currently embeds raw
recipient addresses (variables to and cc) and the sender (RECOUP_FROM_EMAIL)
into the returned message object; change the success and error message strings
to avoid exposing email addresses by replacing them with generic text (e.g.,
"Email sent successfully" or "Failed to send email") and, if needed for internal
diagnostics, log the full addresses locally rather than returning them in the
response object. Update the return objects where message uses to.join(", ") and
cc.join(", ") (and the error message that references RECOUP_FROM_EMAIL and to)
so they no longer include identifiable email data.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (4)
lib/emails/__tests__/processAndSendEmail.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/mcp/tools/__tests__/registerSendEmailTool.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/notifications/__tests__/createNotificationHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/notifications/__tests__/validateCreateNotificationBody.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (5)
app/api/notifications/route.tslib/emails/processAndSendEmail.tslib/mcp/tools/registerSendEmailTool.tslib/notifications/createNotificationHandler.tslib/notifications/validateCreateNotificationBody.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- lib/notifications/createNotificationHandler.ts
- lib/notifications/validateCreateNotificationBody.ts
Next.js compiler doesn't narrow !result.success on the ProcessAndSendEmailResult union. Use result.success === false for correct type narrowing in both MCP tool and API handler. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Summary
POST /api/notificationsREST API endpoint for sending notification emails via Resendsend_emailMCP tool functionality, enabling sandbox agents to send emails via HTTPto,cc,subject,text(Markdown),html, customheaders, androom_idfor chat footer linksvalidateAuthContextfor auth (supports both API key and Bearer token)sendEmailWithResend,getEmailFooter,marked)Test plan
curl -X POST -H "x-api-key: <key>" -H "Content-Type: application/json" -d '{"to":["[email protected]"],"subject":"Test","text":"Hello"}' https://recoup-api.vercel.app/api/notifications🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Refactor