Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
3 changes: 1 addition & 2 deletions apps/sim/app/api/careers/submit/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { render } from '@react-email/components'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import CareersConfirmationEmail from '@/components/emails/careers/careers-confirmation-email'
import CareersSubmissionEmail from '@/components/emails/careers/careers-submission-email'
import { CareersConfirmationEmail, CareersSubmissionEmail } from '@/components/emails'
import { generateRequestId } from '@/lib/core/utils/request'
import { sendEmail } from '@/lib/messaging/email/mailer'

Expand Down
5 changes: 5 additions & 0 deletions apps/sim/app/api/chat/[identifier]/otp/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@ describe('Chat OTP API Route', () => {
}),
}))

vi.doMock('@/lib/core/config/env', async () => {
const { createEnvMock } = await import('@sim/testing')
return createEnvMock()
})

vi.doMock('zod', () => ({
z: {
object: vi.fn().mockReturnValue({
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/api/chat/[identifier]/otp/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { createLogger } from '@sim/logger'
import { and, eq, gt } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { z } from 'zod'
import { renderOTPEmail } from '@/components/emails/render-email'
import { renderOTPEmail } from '@/components/emails'
import { getRedisClient } from '@/lib/core/config/redis'
import { getStorageMethod } from '@/lib/core/storage'
import { generateRequestId } from '@/lib/core/utils/request'
Expand Down
26 changes: 10 additions & 16 deletions apps/sim/app/api/chat/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,17 +249,13 @@ describe('Chat API Route', () => {
}),
}))

vi.doMock('@/lib/core/config/env', () => ({
env: {
vi.doMock('@/lib/core/config/env', async () => {
const { createEnvMock } = await import('@sim/testing')
return createEnvMock({
NODE_ENV: 'development',
NEXT_PUBLIC_APP_URL: 'http://localhost:3000',
},
isTruthy: (value: string | boolean | number | undefined) =>
typeof value === 'string'
? value.toLowerCase() === 'true' || value === '1'
: Boolean(value),
getEnv: (variable: string) => process.env[variable],
}))
})
})

const validData = {
workflowId: 'workflow-123',
Expand Down Expand Up @@ -296,15 +292,13 @@ describe('Chat API Route', () => {
}),
}))

vi.doMock('@/lib/core/config/env', () => ({
env: {
vi.doMock('@/lib/core/config/env', async () => {
const { createEnvMock } = await import('@sim/testing')
return createEnvMock({
NODE_ENV: 'development',
NEXT_PUBLIC_APP_URL: 'http://localhost:3000',
},
isTruthy: (value: string | boolean | number | undefined) =>
typeof value === 'string' ? value === 'true' || value === '1' : Boolean(value),
getEnv: (variable: string) => process.env[variable],
}))
})
})

const validData = {
workflowId: 'workflow-123',
Expand Down
11 changes: 6 additions & 5 deletions apps/sim/app/api/copilot/api-keys/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ describe('Copilot API Keys API Route', () => {
SIM_AGENT_API_URL_DEFAULT: 'https://agent.sim.example.com',
}))

vi.doMock('@/lib/core/config/env', () => ({
env: {
SIM_AGENT_API_URL: null,
vi.doMock('@/lib/core/config/env', async () => {
const { createEnvMock } = await import('@sim/testing')
return createEnvMock({
SIM_AGENT_API_URL: undefined,
COPILOT_API_KEY: 'test-api-key',
},
}))
})
})
})

afterEach(() => {
Expand Down
11 changes: 6 additions & 5 deletions apps/sim/app/api/copilot/stats/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,13 @@ describe('Copilot Stats API Route', () => {
SIM_AGENT_API_URL_DEFAULT: 'https://agent.sim.example.com',
}))

vi.doMock('@/lib/core/config/env', () => ({
env: {
SIM_AGENT_API_URL: null,
vi.doMock('@/lib/core/config/env', async () => {
const { createEnvMock } = await import('@sim/testing')
return createEnvMock({
SIM_AGENT_API_URL: undefined,
COPILOT_API_KEY: 'test-api-key',
},
}))
})
})
})

afterEach(() => {
Expand Down
176 changes: 176 additions & 0 deletions apps/sim/app/api/emails/preview/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import {
renderBatchInvitationEmail,
renderCareersConfirmationEmail,
renderCareersSubmissionEmail,
renderCreditPurchaseEmail,
renderEnterpriseSubscriptionEmail,
renderFreeTierUpgradeEmail,
renderHelpConfirmationEmail,
renderInvitationEmail,
renderOTPEmail,
renderPasswordResetEmail,
renderPaymentFailedEmail,
renderPlanWelcomeEmail,
renderUsageThresholdEmail,
renderWelcomeEmail,
renderWorkspaceInvitationEmail,
} from '@/components/emails'

const emailTemplates = {
// Auth emails
otp: () => renderOTPEmail('123456', '[email protected]', 'email-verification'),
'reset-password': () => renderPasswordResetEmail('John', 'https://sim.ai/reset?token=abc123'),
welcome: () => renderWelcomeEmail('John'),

// Invitation emails
invitation: () => renderInvitationEmail('Jane Doe', 'Acme Corp', 'https://sim.ai/invite/abc123'),
'batch-invitation': () =>
renderBatchInvitationEmail(
'Jane Doe',
'Acme Corp',
'admin',
[
{ workspaceId: 'ws_123', workspaceName: 'Engineering', permission: 'write' },
{ workspaceId: 'ws_456', workspaceName: 'Design', permission: 'read' },
],
'https://sim.ai/invite/abc123'
),
'workspace-invitation': () =>
renderWorkspaceInvitationEmail(
'John Smith',
'Engineering Team',
'https://sim.ai/workspace/invite/abc123'
),

// Support emails
'help-confirmation': () => renderHelpConfirmationEmail('feature_request', 2),

// Billing emails
'usage-threshold': () =>
renderUsageThresholdEmail({
userName: 'John',
planName: 'Pro',
percentUsed: 75,
currentUsage: 15,
limit: 20,
ctaLink: 'https://sim.ai/settings/billing',
}),
'enterprise-subscription': () => renderEnterpriseSubscriptionEmail('John'),
'free-tier-upgrade': () =>
renderFreeTierUpgradeEmail({
userName: 'John',
percentUsed: 90,
currentUsage: 9,
limit: 10,
upgradeLink: 'https://sim.ai/settings/billing',
}),
'plan-welcome-pro': () =>
renderPlanWelcomeEmail({
planName: 'Pro',
userName: 'John',
loginLink: 'https://sim.ai/login',
}),
'plan-welcome-team': () =>
renderPlanWelcomeEmail({
planName: 'Team',
userName: 'John',
loginLink: 'https://sim.ai/login',
}),
'credit-purchase': () =>
renderCreditPurchaseEmail({
userName: 'John',
amount: 50,
newBalance: 75,
}),
'payment-failed': () =>
renderPaymentFailedEmail({
userName: 'John',
amountDue: 20,
lastFourDigits: '4242',
billingPortalUrl: 'https://sim.ai/settings/billing',
failureReason: 'Card declined',
}),

// Careers emails
'careers-confirmation': () => renderCareersConfirmationEmail('John Doe', 'Senior Engineer'),
'careers-submission': () =>
renderCareersSubmissionEmail({
name: 'John Doe',
email: '[email protected]',
phone: '+1 (555) 123-4567',
position: 'Senior Engineer',
linkedin: 'https://linkedin.com/in/johndoe',
portfolio: 'https://johndoe.dev',
experience: '5-10',
location: 'San Francisco, CA',
message:
'I have 10 years of experience building scalable distributed systems. Most recently, I led a team at a Series B startup where we scaled from 100K to 10M users.',
}),
} as const

type EmailTemplate = keyof typeof emailTemplates

export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const template = searchParams.get('template') as EmailTemplate | null

if (!template) {
const categories = {
Auth: ['otp', 'reset-password', 'welcome'],
Invitations: ['invitation', 'batch-invitation', 'workspace-invitation'],
Support: ['help-confirmation'],
Billing: [
'usage-threshold',
'enterprise-subscription',
'free-tier-upgrade',
'plan-welcome-pro',
'plan-welcome-team',
'credit-purchase',
'payment-failed',
],
Careers: ['careers-confirmation', 'careers-submission'],
}

const categoryHtml = Object.entries(categories)
.map(
([category, templates]) => `
<h2 style="margin-top: 24px; margin-bottom: 12px; font-size: 14px; color: #666; text-transform: uppercase; letter-spacing: 0.5px;">${category}</h2>
<ul style="list-style: none; padding: 0; margin: 0;">
${templates.map((t) => `<li style="margin: 8px 0;"><a href="?template=${t}" style="color: #32bd7e; text-decoration: none; font-size: 16px;">${t}</a></li>`).join('')}
</ul>
`
)
.join('')

return new NextResponse(
`<!DOCTYPE html>
<html>
<head>
<title>Email Previews</title>
<style>
body { font-family: system-ui, -apple-system, sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; }
h1 { color: #333; margin-bottom: 32px; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<h1>Email Templates</h1>
${categoryHtml}
</body>
</html>`,
{ headers: { 'Content-Type': 'text/html' } }
)
}

if (!(template in emailTemplates)) {
return NextResponse.json({ error: `Unknown template: ${template}` }, { status: 400 })
}

const html = await emailTemplates[template]()

return new NextResponse(html, {
headers: { 'Content-Type': 'text/html' },
})
}
1 change: 0 additions & 1 deletion apps/sim/app/api/help/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ ${message}
// Send confirmation email to the user
try {
const confirmationHtml = await renderHelpConfirmationEmail(
email,
type as 'bug' | 'feedback' | 'feature_request' | 'other',
images.length
)
Expand Down
5 changes: 2 additions & 3 deletions apps/sim/app/api/organizations/[id]/invitations/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
getEmailSubject,
renderBatchInvitationEmail,
renderInvitationEmail,
} from '@/components/emails/render-email'
} from '@/components/emails'
import { getSession } from '@/lib/auth'
import {
validateBulkInvitations,
Expand Down Expand Up @@ -376,8 +376,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
const emailHtml = await renderInvitationEmail(
inviter[0]?.name || 'Someone',
organizationEntry[0]?.name || 'organization',
`${getBaseUrl()}/invite/${orgInvitation.id}`,
email
`${getBaseUrl()}/invite/${orgInvitation.id}`
)

emailResult = await sendEmail({
Expand Down
5 changes: 2 additions & 3 deletions apps/sim/app/api/organizations/[id]/members/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { invitation, member, organization, user, userStats } from '@sim/db/schem
import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getEmailSubject, renderInvitationEmail } from '@/components/emails/render-email'
import { getEmailSubject, renderInvitationEmail } from '@/components/emails'
import { getSession } from '@/lib/auth'
import { getUserUsageData } from '@/lib/billing/core/usage'
import { validateSeatAvailability } from '@/lib/billing/validation/seat-management'
Expand Down Expand Up @@ -260,8 +260,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
const emailHtml = await renderInvitationEmail(
inviter[0]?.name || 'Someone',
organizationEntry[0]?.name || 'organization',
`${getBaseUrl()}/invite/organization?id=${invitationId}`,
normalizedEmail
`${getBaseUrl()}/invite/organization?id=${invitationId}`
)

const emailResult = await sendEmail({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { WorkspaceInvitationEmail } from '@/components/emails/workspace-invitation'
import { WorkspaceInvitationEmail } from '@/components/emails'
import { getSession } from '@/lib/auth'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { sendEmail } from '@/lib/messaging/email/mailer'
Expand Down
12 changes: 4 additions & 8 deletions apps/sim/app/api/workspaces/invitations/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,10 @@ describe('Workspace Invitations API Route', () => {
WorkspaceInvitationEmail: vi.fn(),
}))

vi.doMock('@/lib/core/config/env', () => ({
env: {
RESEND_API_KEY: 'test-resend-key',
NEXT_PUBLIC_APP_URL: 'https://test.sim.ai',
FROM_EMAIL_ADDRESS: 'Sim <[email protected]>',
EMAIL_DOMAIN: 'test.sim.ai',
},
}))
vi.doMock('@/lib/core/config/env', async () => {
const { createEnvMock } = await import('@sim/testing')
return createEnvMock()
})

vi.doMock('@/lib/core/utils/urls', () => ({
getEmailDomain: vi.fn().mockReturnValue('sim.ai'),
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/api/workspaces/invitations/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import { createLogger } from '@sim/logger'
import { and, eq, inArray } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { WorkspaceInvitationEmail } from '@/components/emails/workspace-invitation'
import { WorkspaceInvitationEmail } from '@/components/emails'
import { getSession } from '@/lib/auth'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { sendEmail } from '@/lib/messaging/email/mailer'
Expand Down
Loading