feat: implement beautiful jsx-email templates for OTP and team invites#196
feat: implement beautiful jsx-email templates for OTP and team invites#196
Conversation
WalkthroughAdds the "jsx-email" dependency and a set of server-side email template components and templates, an API route to preview rendered emails in development, updates mailer functions to use template rendering, an index barrel, and a test script to exercise the renderers. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant API as /api/dev/email-preview
participant Templates as Email Templates
Client->>API: GET /api/dev/email-preview?type=otp|invite
API->>Templates: renderOtpEmail/renderTeamInviteEmail(sample props)
Templates-->>API: HTML string
API-->>Client: 200 text/html (or 400/404/500 JSON)
sequenceDiagram
participant Caller
participant Mailer
participant Templates as Email Templates
participant SMTP as Mail Transport
Caller->>Mailer: sendSignUpEmail(...) / sendTeamInviteEmail(...)
Mailer->>Templates: renderOtpEmail / renderTeamInviteEmail(props)
Templates-->>Mailer: HTML string
Mailer->>SMTP: sendMail({ subject, text, html })
SMTP-->>Mailer: send result
Mailer-->>Caller: resolve/return
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (16)
apps/web/src/server/email-templates/components/EmailButton.tsx (3)
2-2: Remove unused import, or switch to using jsx-email's Link component.
Linkis imported but not used. Either remove it or useLinkinstead of a raw<a>for consistency across templates.-import { Link } from "jsx-email"; +import { Link } from "jsx-email";If you decide not to use Link:
-import { Link } from "jsx-email"; +// no jsx-email Link needed
7-7: variant prop is declared but not used; wire it into styles.Implement distinct styles for "primary" vs "secondary" to match the API you exposed.
export function EmailButton({ href, children, variant = "primary" }: EmailButtonProps) { - return ( - <a - href={href} - style={{ - backgroundColor: "#000000", - color: "#ffffff", - border: "1px solid #000000", - borderRadius: "4px", - fontSize: "16px", - fontWeight: "500", - padding: "12px 24px", - textDecoration: "none", - display: "inline-block", - textAlign: "center" as const, - minWidth: "120px", - boxSizing: "border-box" as const, - float: "left" as const, - clear: "both" as const, - }} - > - {children} - </a> - ); + const variantStyles = + variant === "secondary" + ? { + backgroundColor: "#ffffff", + color: "#000000", + border: "1px solid #000000", + } + : { + backgroundColor: "#000000", + color: "#ffffff", + border: "1px solid #000000", + }; + + return ( + <a + href={href} + target="_blank" + rel="noopener noreferrer" + style={{ + ...variantStyles, + borderRadius: "4px", + fontSize: "16px", + fontWeight: "500", + padding: "12px 24px", + textDecoration: "none", + display: "inline-block", + textAlign: "center" as const, + minWidth: "120px", + boxSizing: "border-box" as const, + }} + > + {children} + </a> + ); }
31-33: Avoid floats in email CTAs; prefer inline-block or table-based centering.Floats are inconsistently rendered across clients and can cause layout issues. The button already uses inline-block; dropping float/clear improves robustness.
- float: "left" as const, - clear: "both" as const,apps/web/src/server/email-templates/components/EmailHeader.tsx (1)
18-27: Add explicit width/height attributes for email client compatibility.Some clients rely on attributes in addition to CSS for image sizing. Keep styles, but add attributes for reliability.
<Img src={logoUrl} alt="Unsend" + width="48" + height="48" style={{ width: "48px", height: "48px", margin: "0 0 16px 0", display: "block", }} />apps/web/src/server/email-templates/components/EmailFooter.tsx (1)
29-39: Open support link safely in a new tab for most clients.Add target and rel for security and consistency. Some email clients ignore them, but it’s harmless where unsupported.
<a href={supportUrl} + target="_blank" + rel="noopener noreferrer" style={{ color: "#000000", textDecoration: "underline", }} > contact our support team </a>apps/web/src/server/email-templates/components/EmailLayout.tsx (2)
18-19: Set document language for better accessibility and client hints.Adding lang aids screen readers and improves internationalization defaults.
- <Html> + <Html lang="en">
63-69: Unify background color between CSS and inline Body style.Your CSS sets body background to #f9fafb, but the Body inline style overrides to white. Align them to avoid surprises.
- <Body style={{ backgroundColor: "#ffffff", padding: "20px" }}> + <Body style={{ backgroundColor: "#f9fafb", padding: "20px" }}>apps/web/src/app/api/dev/email-preview/route.ts (2)
28-32: Prevent caching and indexing of the preview outputAdd headers to ensure HTML previews aren’t cached by browsers/proxies and aren’t indexed by bots.
return new NextResponse(html, { headers: { - "Content-Type": "text/html", + "Content-Type": "text/html", + "Cache-Control": "no-store, max-age=0", + "X-Robots-Tag": "noindex, nofollow", }, });
1-2: Use the shared logger instead of console.error for consistencyUse the project logger for structured logs and consistent formatting.
import { NextRequest, NextResponse } from "next/server"; -import { renderOtpEmail, renderTeamInviteEmail } from "~/server/email-templates"; +import { renderOtpEmail, renderTeamInviteEmail } from "~/server/email-templates"; +import { logger } from "~/server/logger/log"; @@ - console.error("Error rendering email template:", error); + logger.error({ error }, "Error rendering email template");Also applies to: 34-34
apps/web/src/server/email-templates/test.ts (1)
34-36: Guard for ESM builds where require may be undefinedThis makes the script compatible with ESM module resolution too and avoids unhandled promise rejections.
-if (require.main === module) { - testEmailTemplates(); -} +// In ESM builds, `require` may be undefined; guard accordingly. +if (typeof require !== "undefined" && require.main === module) { + void testEmailTemplates(); +}apps/web/src/server/email-templates/OtpEmail.tsx (3)
1-7: Combine jsx-email imports to reduce duplicationMinor cleanup to import from the same module once.
-import { Container, Text } from "jsx-email"; -import { render } from "jsx-email"; +import { Container, Text, render } from "jsx-email";
9-14: Export the props type for external consumersExporting OtpEmailProps allows callers to type their inputs when using renderOtpEmail.
-interface OtpEmailProps { +export interface OtpEmailProps { otpCode: string; loginUrl: string; hostName?: string; logoUrl?: string; }
24-27: Personalize the header with hostNameSlightly improves clarity by reflecting the brand in the heading.
- <EmailHeader + <EmailHeader logoUrl={logoUrl} - title="Sign in to your account" + title={`Sign in to your ${hostName} account`} />apps/web/src/server/email-templates/index.ts (1)
1-7: Re-export props types for better DXExpose the props types so callers can import them from the barrel. This depends on the source modules exporting the types.
export { OtpEmail, renderOtpEmail } from "./OtpEmail"; export { TeamInviteEmail, renderTeamInviteEmail } from "./TeamInviteEmail"; export * from "./components/EmailLayout"; export * from "./components/EmailHeader"; export * from "./components/EmailFooter"; export * from "./components/EmailButton"; +export type { OtpEmailProps } from "./OtpEmail"; +export type { TeamInviteEmailProps } from "./TeamInviteEmail";apps/web/src/server/email-templates/TeamInviteEmail.tsx (2)
2-3: Combine imports from the same module.Consolidate imports from "jsx-email" into a single statement to follow the repo’s import grouping/alphabetization guideline and reduce duplication.
-import { Container, Text } from "jsx-email"; -import { render } from "jsx-email"; +import { Container, Text, render } from "jsx-email";
14-14: Constrain role type to known values (optional).If your domain has a fixed set of roles, prefer a string union or shared Role type over a free-form string to avoid unexpected values being rendered to end users.
Would you like me to wire this up to an existing Role type (if present) or propose a quick union like "member" | "admin" | "owner"?
Also applies to: 22-22
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (11)
apps/web/package.json(2 hunks)apps/web/src/app/api/dev/email-preview/route.ts(1 hunks)apps/web/src/server/email-templates/OtpEmail.tsx(1 hunks)apps/web/src/server/email-templates/TeamInviteEmail.tsx(1 hunks)apps/web/src/server/email-templates/components/EmailButton.tsx(1 hunks)apps/web/src/server/email-templates/components/EmailFooter.tsx(1 hunks)apps/web/src/server/email-templates/components/EmailHeader.tsx(1 hunks)apps/web/src/server/email-templates/components/EmailLayout.tsx(1 hunks)apps/web/src/server/email-templates/index.ts(1 hunks)apps/web/src/server/email-templates/test.ts(1 hunks)apps/web/src/server/mailer.ts(3 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
{apps,packages}/**/*.{js,jsx,ts,tsx,css,scss,md,mdx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
Use Prettier with the Tailwind plugin for code formatting
Files:
apps/web/src/server/email-templates/test.tsapps/web/src/app/api/dev/email-preview/route.tsapps/web/src/server/email-templates/components/EmailLayout.tsxapps/web/src/server/mailer.tsapps/web/src/server/email-templates/components/EmailButton.tsxapps/web/src/server/email-templates/components/EmailFooter.tsxapps/web/src/server/email-templates/index.tsapps/web/src/server/email-templates/OtpEmail.tsxapps/web/src/server/email-templates/components/EmailHeader.tsxapps/web/src/server/email-templates/TeamInviteEmail.tsx
{apps,packages}/**/*.{js,jsx,ts,tsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
{apps,packages}/**/*.{js,jsx,ts,tsx}: Group imports by source (internal/external) and alphabetize them
Use camelCase for variables and functions, PascalCase for components and classes
Use try/catch with specific error types for error handling
Files:
apps/web/src/server/email-templates/test.tsapps/web/src/app/api/dev/email-preview/route.tsapps/web/src/server/email-templates/components/EmailLayout.tsxapps/web/src/server/mailer.tsapps/web/src/server/email-templates/components/EmailButton.tsxapps/web/src/server/email-templates/components/EmailFooter.tsxapps/web/src/server/email-templates/index.tsapps/web/src/server/email-templates/OtpEmail.tsxapps/web/src/server/email-templates/components/EmailHeader.tsxapps/web/src/server/email-templates/TeamInviteEmail.tsx
{apps,packages}/**/*.{ts,tsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
{apps,packages}/**/*.{ts,tsx}: Use strong typing in TypeScript, avoidany, and use Zod for validation
Follow Vercel style guides with strict TypeScript
Files:
apps/web/src/server/email-templates/test.tsapps/web/src/app/api/dev/email-preview/route.tsapps/web/src/server/email-templates/components/EmailLayout.tsxapps/web/src/server/mailer.tsapps/web/src/server/email-templates/components/EmailButton.tsxapps/web/src/server/email-templates/components/EmailFooter.tsxapps/web/src/server/email-templates/index.tsapps/web/src/server/email-templates/OtpEmail.tsxapps/web/src/server/email-templates/components/EmailHeader.tsxapps/web/src/server/email-templates/TeamInviteEmail.tsx
apps/web/**/*.{ts,tsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
Use tRPC for internal API endpoints
Files:
apps/web/src/server/email-templates/test.tsapps/web/src/app/api/dev/email-preview/route.tsapps/web/src/server/email-templates/components/EmailLayout.tsxapps/web/src/server/mailer.tsapps/web/src/server/email-templates/components/EmailButton.tsxapps/web/src/server/email-templates/components/EmailFooter.tsxapps/web/src/server/email-templates/index.tsapps/web/src/server/email-templates/OtpEmail.tsxapps/web/src/server/email-templates/components/EmailHeader.tsxapps/web/src/server/email-templates/TeamInviteEmail.tsx
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit Inference Engine (.cursor/rules/general.mdc)
Include all required imports, and ensure proper naming of key components.
Files:
apps/web/src/server/email-templates/test.tsapps/web/src/app/api/dev/email-preview/route.tsapps/web/src/server/email-templates/components/EmailLayout.tsxapps/web/src/server/mailer.tsapps/web/src/server/email-templates/components/EmailButton.tsxapps/web/src/server/email-templates/components/EmailFooter.tsxapps/web/src/server/email-templates/index.tsapps/web/src/server/email-templates/OtpEmail.tsxapps/web/src/server/email-templates/components/EmailHeader.tsxapps/web/src/server/email-templates/TeamInviteEmail.tsx
{apps,packages}/**/*.{jsx,tsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
{apps,packages}/**/*.{jsx,tsx}: Use functional React components with hooks and group related hooks together
In React components, structure code with props at the top, hooks next, helper functions, then JSX
Files:
apps/web/src/server/email-templates/components/EmailLayout.tsxapps/web/src/server/email-templates/components/EmailButton.tsxapps/web/src/server/email-templates/components/EmailFooter.tsxapps/web/src/server/email-templates/OtpEmail.tsxapps/web/src/server/email-templates/components/EmailHeader.tsxapps/web/src/server/email-templates/TeamInviteEmail.tsx
🧬 Code Graph Analysis (5)
apps/web/src/server/email-templates/test.ts (3)
apps/web/src/server/email-templates/OtpEmail.tsx (1)
renderOtpEmail(101-103)apps/web/src/server/email-templates/index.ts (2)
renderOtpEmail(1-1)renderTeamInviteEmail(2-2)apps/web/src/server/email-templates/TeamInviteEmail.tsx (1)
renderTeamInviteEmail(84-86)
apps/web/src/app/api/dev/email-preview/route.ts (2)
apps/web/src/server/email-templates/OtpEmail.tsx (1)
renderOtpEmail(101-103)apps/web/src/server/email-templates/TeamInviteEmail.tsx (1)
renderTeamInviteEmail(84-86)
apps/web/src/server/mailer.ts (3)
apps/web/src/server/email-templates/OtpEmail.tsx (1)
renderOtpEmail(101-103)apps/web/src/server/email-templates/index.ts (2)
renderOtpEmail(1-1)renderTeamInviteEmail(2-2)apps/web/src/server/email-templates/TeamInviteEmail.tsx (1)
renderTeamInviteEmail(84-86)
apps/web/src/server/email-templates/OtpEmail.tsx (6)
apps/web/src/server/email-templates/index.ts (2)
OtpEmail(1-1)renderOtpEmail(1-1)apps/web/src/server/email-templates/components/EmailLayout.tsx (1)
EmailLayout(16-77)apps/web/src/server/email-templates/components/EmailHeader.tsx (1)
EmailHeader(9-45)apps/web/src/server/email-templates/components/EmailButton.tsx (1)
EmailButton(10-38)apps/web/src/server/email-templates/components/EmailFooter.tsx (1)
EmailFooter(9-43)packages/email-editor/src/renderer.tsx (1)
render(196-203)
apps/web/src/server/email-templates/TeamInviteEmail.tsx (6)
apps/web/src/server/email-templates/index.ts (2)
TeamInviteEmail(2-2)renderTeamInviteEmail(2-2)apps/web/src/server/email-templates/components/EmailLayout.tsx (1)
EmailLayout(16-77)apps/web/src/server/email-templates/components/EmailHeader.tsx (1)
EmailHeader(9-45)apps/web/src/server/email-templates/components/EmailButton.tsx (1)
EmailButton(10-38)apps/web/src/server/email-templates/components/EmailFooter.tsx (1)
EmailFooter(9-43)packages/email-editor/src/renderer.tsx (1)
render(196-203)
🪛 ast-grep (0.38.6)
apps/web/src/server/email-templates/components/EmailLayout.tsx
[warning] 30-30: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html
(react-unsafe-html-injection)
🪛 Biome (2.1.2)
apps/web/src/server/email-templates/components/EmailLayout.tsx
[error] 31-31: Avoid passing content using the dangerouslySetInnerHTML prop.
Setting content using code can expose users to cross-site scripting (XSS) attacks
(lint/security/noDangerouslySetInnerHtml)
🔇 Additional comments (11)
apps/web/package.json (1)
39-39: Peer dependencies are compatible with React 19 and Next.js 15All peer dependencies for jsx-email@2.7.1 support React “^18.2.0 || ^19”, which aligns with Next 15’s React 19 requirement.
• react: "^18.2.0 || ^19"
• react-dom: "^18.2.0 || ^19"
• @jsx-email/plugin-inline: "^1.0.1"
• @jsx-email/plugin-minify: "^1.0.2"
• @jsx-email/plugin-pretty: "^1.0.0"Ensure you install any of the @jsx-email plugins you intend to use to satisfy those peer dependencies.
apps/web/src/server/email-templates/components/EmailHeader.tsx (1)
2-2: Optional: Alphabetize named imports to match repo guidelines.Minor consistency tweak.
-import { Container, Heading, Img } from "jsx-email"; +import { Container, Heading, Img } from "jsx-email"; // Already alphabetical; keep as-isapps/web/src/server/email-templates/test.ts (1)
18-21: No changes needed: optional propsThe
TeamInviteEmailPropsinterface marks bothinviterNameandroleas optional, so the test’s use of onlyteamNameandinviteUrlis valid.apps/web/src/server/mailer.ts (4)
33-39: LGTM: switched OTP email HTML to the new template rendererAsynchronously rendering via jsx-email is clean and keeps the HTML concern in templates.
35-35: Confirm OTP casing behavior (uppercase in HTML vs. text/plain)HTML uses token.toUpperCase() while the text body uses the original token. If OTP comparison is case-sensitive, this can cause user confusion or failed sign-ins.
If OTP is case-sensitive, align both representations. Example change to keep original casing:
const html = await renderOtpEmail({ - otpCode: token.toUpperCase(), + otpCode: token, loginUrl: url, hostName: host, });Alternatively, uppercase both HTML and text if your backend normalizes input to uppercase.
61-65: LGTM: team invite HTML now uses the template rendererGood separation of concerns and consistency with OTP emails.
61-64: TeamInviteEmailProps usage is correctThe
TeamInviteEmailPropsinterface (inapps/web/src/server/email-templates/TeamInviteEmail.tsx) defines:
teamName: stringinviteUrl: stringinviterName?: stringThe mailer call in
apps/web/src/server/mailer.tspasses only the required fields (teamNameandinviteUrl), which is valid—the optionalinviterNamewill simply beundefinedin the template. There is noroleprop on the interface, so the review’s reference to a “role” field is outdated.If you’d like richer emails, you can pass
inviterName(and extend the interface/template to include aroleif needed) wherever you have that data; otherwise, no changes are required here.apps/web/src/server/email-templates/OtpEmail.tsx (1)
96-103: LGTM: renderer helper keeps callers simpleThe renderOtpEmail helper is concise and matches how other templates are exposed.
apps/web/src/server/email-templates/index.ts (1)
1-2: Good centralization of template exportsBarrel exports make consumers cleaner and promote consistency.
apps/web/src/server/email-templates/TeamInviteEmail.tsx (2)
17-81: Overall: clean, accessible, and reusable template.Solid structure with EmailLayout/Header/Footer/Button, preview text set, clear CTA, and responsive-friendly inline styles. The conditional “as a {role}” is a nice touch.
61-63: EnsureinviteUrlis fully qualifiedThe JSX template (
<EmailButton href={inviteUrl}>…) and the server code insendTeamInviteEmail(which doesnew URL(url)) both require a complete URL. Please verify that every call tosendTeamInviteEmailconstructs and passes an absolute URL (e.g.https://app.yourdomain.com/join-team?inviteId=…). If you can’t guarantee this upstream, normalize it using your app’s origin (for example viaenv.APP_ORIGIN):// normalize in your invite-generation logic or here in mailer.ts inviteUrl: new URL(url, env.APP_ORIGIN).toString(),What to check:
- The controller/service that calls
sendTeamInviteEmail—ensure it uses a full origin + path- Any helper or constant (e.g.
env.APP_ORIGIN) responsible for building invitation URLsNo change is needed in the template if callers already pass a fully qualified URL.
| "type": "module", | ||
| "scripts": { | ||
| "dev": "next dev -p 3000", | ||
| "dev": "next dev", |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Verify there are no hard-coded localhost:3000 assumptions before removing the explicit dev port.
Dropping the explicit port is fine, but if docs, scripts, or preview routes rely on http://localhost:3000, they’ll break. Please verify.
Run this to find any hard-coded references to port 3000:
🏁 Script executed:
#!/bin/bash
# Search for hard-coded dev ports in code, envs, and docs
rg -n -C2 -S -g '!**/node_modules/**' -P '(localhost|127\.0\.0\.1)\s*:\s*3000|http://localhost/|http://127\.0\.0\.1/'Length of output: 5722
Update hard-coded localhost:3000 references before dropping the explicit dev port
Several docs and scripts still assume http://localhost:3000. These will break once you rely on the default Next.js port or a custom PORT. Please update or parameterize the following locations:
- docker/README.md (line 27): “Access the Unsend application by visiting
http://localhost:3000” - apps/marketing/README.md (line 17): “Open http://localhost:3000”
- apps/marketing/src/app/editor/page.tsx (lines 15–17):
fetch("http://localhost:3000/api/to-html", …) - apps/web/src/app/(dashboard)/domains/[domainId]/send-test-mail.tsx (lines 36, 45, 67, 84): multiple examples (JS fetch, Python, Ruby, PHP) pointing at
http://localhost:3000/api/v1/emails - apps/docs/get-started/local.mdx (lines 115, 121, 159, 171, 201): login, OAuth callback, dashboard URLs and
cloudflared tunnelcommands all reference port 3000 - apps/docs/get-started/set-up-docker.mdx (line 28): “visit
http://localhost:3000” - CONTRIBUTION.md (lines 61–62, 92–93, 98): GitHub OAuth homepage/callback URLs and tunnel commands
Recommend replacing hard-coded URLs with http://localhost:$PORT (or an env var), or updating instructions to match the new default.
🤖 Prompt for AI Agents
In apps/web/package.json around line 7 and across the repository
(docker/README.md line 27; apps/marketing/README.md line 17;
apps/marketing/src/app/editor/page.tsx lines 15–17;
apps/web/src/app/(dashboard)/domains/[domainId]/send-test-mail.tsx lines
36,45,67,84; apps/docs/get-started/local.mdx lines 115,121,159,171,201;
apps/docs/get-started/set-up-docker.mdx line 28; CONTRIBUTION.md lines
61–62,92–93,98) replace hard-coded http://localhost:3000 occurrences with a
parameterized form so the port is not fixed: update docs and README text to
reference http://localhost:$PORT or instruct using an env var (e.g., PORT) and
change code examples to interpolate or read process.env.PORT (or explain using
default Next.js port) so fetch/API examples and tunnel commands use the
configurable port; ensure examples include a fallback (e.g., 3000) and update
any shell commands to use $PORT or explicit instruction to set PORT before
running.
| export async function GET(request: NextRequest) { | ||
| const { searchParams } = new URL(request.url); | ||
| const type = searchParams.get("type") || "otp"; | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
Restrict the dev preview route to development-only
This is a dev-only utility; gate it by NODE_ENV to avoid exposing it in production.
Apply this diff:
export async function GET(request: NextRequest) {
+ // Dev-only endpoint: return 404 in non-development environments
+ if (process.env.NODE_ENV !== "development") {
+ return NextResponse.json({ error: "Not Found" }, { status: 404 });
+ }
const { searchParams } = new URL(request.url);
const type = searchParams.get("type") || "otp";📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function GET(request: NextRequest) { | |
| const { searchParams } = new URL(request.url); | |
| const type = searchParams.get("type") || "otp"; | |
| export async function GET(request: NextRequest) { | |
| // Dev-only endpoint: return 404 in non-development environments | |
| if (process.env.NODE_ENV !== "development") { | |
| return NextResponse.json({ error: "Not Found" }, { status: 404 }); | |
| } | |
| const { searchParams } = new URL(request.url); | |
| const type = searchParams.get("type") || "otp"; | |
| // …rest of your logic | |
| } |
🤖 Prompt for AI Agents
In apps/web/src/app/api/dev/email-preview/route.ts around lines 4–7, the
dev-only email preview route is currently exposed unconditionally; add a
NODE_ENV check at the start of the GET handler so it only runs in development
(process.env.NODE_ENV === 'development'), and immediately return a 404/403
response (or throw) when not in development to prevent exposure in production;
keep the rest of the handler unchanged and ensure the guard executes before
creating the URL/searchParams or reading any request data.
| <style | ||
| dangerouslySetInnerHTML={{ | ||
| __html: ` | ||
| * { | ||
| margin: 0; | ||
| padding: 0; | ||
| box-sizing: border-box; | ||
| } | ||
| body { | ||
| font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | ||
| line-height: 1.6; | ||
| color: #374151; | ||
| background-color: #f9fafb; | ||
| } | ||
| .email-container { | ||
| max-width: 600px; | ||
| margin: 0 auto; | ||
| background-color: #ffffff; | ||
| } | ||
| `, | ||
| }} | ||
| /> |
There was a problem hiding this comment.
Address linter error: remove dangerouslySetInnerHTML usage in <style>.
Static analysis flags this as an error; although the CSS is static and safe, the rule will fail CI. Use a normal style child string instead.
- <style
- dangerouslySetInnerHTML={{
- __html: `
- * {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }
- body {
- font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- line-height: 1.6;
- color: #374151;
- background-color: #f9fafb;
- }
- .email-container {
- max-width: 600px;
- margin: 0 auto;
- background-color: #ffffff;
- }
- `,
- }}
- />
+ <style>{`
+ * {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ }
+ body {
+ font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ line-height: 1.6;
+ color: #374151;
+ background-color: #f9fafb;
+ }
+ .email-container {
+ max-width: 600px;
+ margin: 0 auto;
+ background-color: #ffffff;
+ }
+ `}</style>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <style | |
| dangerouslySetInnerHTML={{ | |
| __html: ` | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| line-height: 1.6; | |
| color: #374151; | |
| background-color: #f9fafb; | |
| } | |
| .email-container { | |
| max-width: 600px; | |
| margin: 0 auto; | |
| background-color: #ffffff; | |
| } | |
| `, | |
| }} | |
| /> | |
| <style>{` | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| line-height: 1.6; | |
| color: #374151; | |
| background-color: #f9fafb; | |
| } | |
| .email-container { | |
| max-width: 600px; | |
| margin: 0 auto; | |
| background-color: #ffffff; | |
| } | |
| `}</style> |
🧰 Tools
🪛 ast-grep (0.38.6)
[warning] 30-30: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html
(react-unsafe-html-injection)
🪛 Biome (2.1.2)
[error] 31-31: Avoid passing content using the dangerouslySetInnerHTML prop.
Setting content using code can expose users to cross-site scripting (XSS) attacks
(lint/security/noDangerouslySetInnerHtml)
🤖 Prompt for AI Agents
In apps/web/src/server/email-templates/components/EmailLayout.tsx around lines
30 to 51, remove the dangerouslySetInnerHTML usage on the <style> tag and
replace it with a normal child string so the linter rule no longer flags it;
specifically, delete the dangerouslySetInnerHTML prop and place the same CSS as
the element's child using a JSX expression (e.g. <style>{`...css...`}</style>),
preserving the exact CSS content and formatting.
- Add minimal, professional email templates using jsx-email - Create reusable email components (layout, header, footer, button) - Implement OTP email with prominent code display and one-click login - Implement team invite email with feature highlights and clear CTA - Update mailer service to use new jsx-email templates - Add development email preview endpoint for testing - Ensure mobile-responsive design with accessibility features 🤖 Generated with [opencode](https://opencode.ai) Co-Authored-By: opencode <noreply@opencode.ai>
- Change buttons to black background with white text - Remove colored elements, use only black and white styling - Change text alignment to left instead of center - Remove "This code will expire in 10 minutes" from OTP email - Remove team features list and team card from invite email - Simplify invite email to just show invitation text and accept button - Use minimal border radius and remove shadows for cleaner look 🤖 Generated with [opencode](https://opencode.ai) Co-Authored-By: opencode <noreply@opencode.ai>
- Remove all borders and card-like styling from email containers - Remove border radius and shadows for plain appearance - Make OTP code display simple with just light background - Reduce padding for normal email spacing - Remove horizontal rules/separators - Left-align all text including footer - Make emails look like standard plain text emails with buttons 🤖 Generated with [opencode](https://opencode.ai) Co-Authored-By: opencode <noreply@opencode.ai>
- Set OTP code container and text to left alignment - Set button containers to left alignment - Change button text alignment from center to left - Ensure all elements follow left-aligned layout consistently 🤖 Generated with [opencode](https://opencode.ai) Co-Authored-By: opencode <noreply@opencode.ai>
- Remove margin: 0 auto from main container that was centering content - Add textAlign: left to all Container components - Add textAlign: left to all Text components - Fix logo alignment from center to left in header - Add textAlign: left to heading in header - Ensure jsx-email Container components don't use default centering - Make all content consistently left-aligned throughout emails 🤖 Generated with [opencode](https://opencode.ai) Co-Authored-By: opencode <noreply@opencode.ai>
- Replace jsx-email Link component with native anchor tag - Add float: left and clear: both to force left positioning - Keep textAlign: center for button text itself (normal for buttons) - Ensure button appears at left edge instead of centering 🤖 Generated with [opencode](https://opencode.ai) Co-Authored-By: opencode <noreply@opencode.ai>
fbdcc31 to
270c115
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
apps/web/src/server/email-templates/components/EmailLayout.tsx (1)
30-51: Fix linter error: replace dangerouslySetInnerHTML on <style> with string childThis is blocking CI per Biome and ast-grep. The CSS is static; render it as a normal child string.
Apply this diff:
- <style - dangerouslySetInnerHTML={{ - __html: ` - * { - margin: 0; - padding: 0; - box-sizing: border-box; - } - body { - font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - line-height: 1.6; - color: #374151; - background-color: #f9fafb; - } - .email-container { - max-width: 600px; - margin: 0 auto; - background-color: #ffffff; - } - `, - }} - /> + <style>{` + * { + margin: 0; + padding: 0; + box-sizing: border-box; + } + body { + font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + line-height: 1.6; + color: #374151; + background-color: #f9fafb; + } + .email-container { + max-width: 600px; + margin: 0 auto; + background-color: #ffffff; + } + `}</style>
🧹 Nitpick comments (3)
apps/web/src/server/email-templates/components/EmailLayout.tsx (3)
39-43: Resolve background-color mismatch between CSS and styleCSS sets body to #f9fafb while the inline style sets #ffffff. Align them to avoid surprises across clients.
If the intent is a light gray page background with a white container, update :
- <Body style={{ backgroundColor: "#ffffff", padding: "20px" }}> + <Body style={{ backgroundColor: "#f9fafb", padding: "20px" }}>If you prefer a fully white page, instead remove the background-color from the body rule in the CSS block.
Also applies to: 63-63
64-71: Inline centering and width for better email client supportMany clients ignore class-based CSS; inlining width and centering improves reliability and responsiveness.
Apply this diff:
<Container className="email-container" style={{ + width: "100%", maxWidth: "600px", + margin: "0 auto", backgroundColor: "#ffffff", - textAlign: "left" as const, + textAlign: "left", }} >
18-19: Add lang attribute to for accessibility and better client hintsThis helps screen readers and some clients render text appropriately.
- <Html> + <Html lang="en">
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (11)
apps/web/package.json(1 hunks)apps/web/src/app/api/dev/email-preview/route.ts(1 hunks)apps/web/src/server/email-templates/OtpEmail.tsx(1 hunks)apps/web/src/server/email-templates/TeamInviteEmail.tsx(1 hunks)apps/web/src/server/email-templates/components/EmailButton.tsx(1 hunks)apps/web/src/server/email-templates/components/EmailFooter.tsx(1 hunks)apps/web/src/server/email-templates/components/EmailHeader.tsx(1 hunks)apps/web/src/server/email-templates/components/EmailLayout.tsx(1 hunks)apps/web/src/server/email-templates/index.ts(1 hunks)apps/web/src/server/email-templates/test.ts(1 hunks)apps/web/src/server/mailer.ts(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (10)
- apps/web/src/server/email-templates/index.ts
- apps/web/src/app/api/dev/email-preview/route.ts
- apps/web/src/server/email-templates/components/EmailHeader.tsx
- apps/web/src/server/email-templates/components/EmailButton.tsx
- apps/web/src/server/email-templates/TeamInviteEmail.tsx
- apps/web/src/server/email-templates/OtpEmail.tsx
- apps/web/src/server/mailer.ts
- apps/web/package.json
- apps/web/src/server/email-templates/components/EmailFooter.tsx
- apps/web/src/server/email-templates/test.ts
🧰 Additional context used
📓 Path-based instructions (6)
{apps,packages}/**/*.{js,jsx,ts,tsx,css,scss,md,mdx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
Use Prettier with the Tailwind plugin for code formatting
Files:
apps/web/src/server/email-templates/components/EmailLayout.tsx
{apps,packages}/**/*.{js,jsx,ts,tsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
{apps,packages}/**/*.{js,jsx,ts,tsx}: Group imports by source (internal/external) and alphabetize them
Use camelCase for variables and functions, PascalCase for components and classes
Use try/catch with specific error types for error handling
Files:
apps/web/src/server/email-templates/components/EmailLayout.tsx
{apps,packages}/**/*.{ts,tsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
{apps,packages}/**/*.{ts,tsx}: Use strong typing in TypeScript, avoidany, and use Zod for validation
Follow Vercel style guides with strict TypeScript
Files:
apps/web/src/server/email-templates/components/EmailLayout.tsx
{apps,packages}/**/*.{jsx,tsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
{apps,packages}/**/*.{jsx,tsx}: Use functional React components with hooks and group related hooks together
In React components, structure code with props at the top, hooks next, helper functions, then JSX
Files:
apps/web/src/server/email-templates/components/EmailLayout.tsx
apps/web/**/*.{ts,tsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
Use tRPC for internal API endpoints
Files:
apps/web/src/server/email-templates/components/EmailLayout.tsx
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit Inference Engine (.cursor/rules/general.mdc)
Include all required imports, and ensure proper naming of key components.
Files:
apps/web/src/server/email-templates/components/EmailLayout.tsx
🪛 ast-grep (0.38.6)
apps/web/src/server/email-templates/components/EmailLayout.tsx
[warning] 30-30: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html
(react-unsafe-html-injection)
🪛 Biome (2.1.2)
apps/web/src/server/email-templates/components/EmailLayout.tsx
[error] 31-31: Avoid passing content using the dangerouslySetInnerHTML prop.
Setting content using code can expose users to cross-site scripting (XSS) attacks
(lint/security/noDangerouslySetInnerHtml)
🔇 Additional comments (1)
apps/web/src/server/email-templates/components/EmailLayout.tsx (1)
52-61: Good meta coverage for email clientsNice inclusion of viewport, format-detection, color-scheme, and Apple reformatting metas. The Preview placement is also spot on.
🤖 Generated with opencode
Co-Authored-By: opencode noreply@opencode.ai
Summary by CodeRabbit
New Features
Improvements
Chores