Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 0 additions & 5 deletions apps/sim/app/api/chat/[identifier]/otp/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ const {
mockDbUpdate,
mockSendEmail,
mockRenderOTPEmail,
mockAddCorsHeaders,
mockSetChatAuthCookie,
mockGetStorageMethod,
mockZodParse,
Expand All @@ -50,7 +49,6 @@ const {
const mockDbUpdate = vi.fn()
const mockSendEmail = vi.fn()
const mockRenderOTPEmail = vi.fn()
const mockAddCorsHeaders = vi.fn()
const mockSetChatAuthCookie = vi.fn()
const mockGetStorageMethod = vi.fn()
const mockZodParse = vi.fn()
Expand All @@ -69,7 +67,6 @@ const {
mockDbUpdate,
mockSendEmail,
mockRenderOTPEmail,
mockAddCorsHeaders,
mockSetChatAuthCookie,
mockGetStorageMethod,
mockZodParse,
Expand Down Expand Up @@ -131,7 +128,6 @@ vi.mock('@/components/emails', () => ({
}))

vi.mock('@/lib/core/security/deployment', () => ({
addCorsHeaders: mockAddCorsHeaders,
isEmailAllowed: (email: string, allowedEmails: string[]) => {
if (allowedEmails.includes(email)) return true
const atIndex = email.indexOf('@')
Expand Down Expand Up @@ -248,7 +244,6 @@ describe('Chat OTP API Route', () => {
mockSendEmail.mockResolvedValue({ success: true })
mockRenderOTPEmail.mockResolvedValue('<html>OTP Email</html>')

mockAddCorsHeaders.mockImplementation((response: unknown) => response)
mockCreateSuccessResponse.mockImplementation((data: unknown) => ({
json: () => Promise.resolve(data),
status: 200,
Expand Down
77 changes: 25 additions & 52 deletions apps/sim/app/api/chat/[identifier]/otp/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { renderOTPEmail } from '@/components/emails'
import { requestChatEmailOtpContract, verifyChatEmailOtpContract } from '@/lib/api/contracts/chats'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { RateLimiter } from '@/lib/core/rate-limiter'
import { addCorsHeaders, isEmailAllowed } from '@/lib/core/security/deployment'
import { isEmailAllowed } from '@/lib/core/security/deployment'
import {
decodeOTPValue,
deleteOTP,
Expand Down Expand Up @@ -47,15 +47,12 @@ export const POST = withRouteHandler(
)
const response = createErrorResponse('Too many requests. Please try again later.', 429)
response.headers.set('Retry-After', String(retryAfter))
return addCorsHeaders(response, request)
return response
}

const parsed = await parseRequest(requestChatEmailOtpContract, request, context, {
validationErrorResponse: (error) =>
addCorsHeaders(
createErrorResponse(getValidationErrorMessage(error, 'Invalid request'), 400),
request
),
createErrorResponse(getValidationErrorMessage(error, 'Invalid request'), 400),
})
if (!parsed.success) return parsed.response
const { email } = parsed.data.body
Expand All @@ -75,27 +72,21 @@ export const POST = withRouteHandler(

if (deploymentResult.length === 0) {
logger.warn(`[${requestId}] Chat not found for identifier: ${identifier}`)
return addCorsHeaders(createErrorResponse('Chat not found', 404), request)
return createErrorResponse('Chat not found', 404)
}

const deployment = deploymentResult[0]

if (deployment.authType !== 'email') {
return addCorsHeaders(
createErrorResponse('This chat does not use email authentication', 400),
request
)
return createErrorResponse('This chat does not use email authentication', 400)
}

const allowedEmails: string[] = Array.isArray(deployment.allowedEmails)
? deployment.allowedEmails
: []

if (!isEmailAllowed(email, allowedEmails)) {
return addCorsHeaders(
createErrorResponse('Email not authorized for this chat', 403),
request
)
return createErrorResponse('Email not authorized for this chat', 403)
}

const emailRateLimit = await rateLimiter.checkRateLimitDirect(
Expand All @@ -114,7 +105,7 @@ export const POST = withRouteHandler(
429
)
response.headers.set('Retry-After', String(retryAfter))
return addCorsHeaders(response, request)
return response
}

const otp = generateOTP()
Expand All @@ -135,17 +126,14 @@ export const POST = withRouteHandler(

if (!emailResult.success) {
logger.error(`[${requestId}] Failed to send OTP email:`, emailResult.message)
return addCorsHeaders(
createErrorResponse('Failed to send verification email', 500),
request
)
return createErrorResponse('Failed to send verification email', 500)
}

logger.info(`[${requestId}] OTP sent to ${email} for chat ${deployment.id}`)
return addCorsHeaders(createSuccessResponse({ message: 'Verification code sent' }), request)
return createSuccessResponse({ message: 'Verification code sent' })
} catch (error) {
logger.error(`[${requestId}] Error processing OTP request:`, error)
return addCorsHeaders(createErrorResponse('Failed to process request', 500), request)
return createErrorResponse('Failed to process request', 500)
}
}
)
Expand All @@ -158,10 +146,7 @@ export const PUT = withRouteHandler(
try {
const parsed = await parseRequest(verifyChatEmailOtpContract, request, context, {
validationErrorResponse: (error) =>
addCorsHeaders(
createErrorResponse(getValidationErrorMessage(error, 'Invalid request'), 400),
request
),
createErrorResponse(getValidationErrorMessage(error, 'Invalid request'), 400),
})
if (!parsed.success) return parsed.response
const { email, otp } = parsed.data.body
Expand All @@ -184,61 +169,49 @@ export const PUT = withRouteHandler(

if (deploymentResult.length === 0) {
logger.warn(`[${requestId}] Chat not found for identifier: ${identifier}`)
return addCorsHeaders(createErrorResponse('Chat not found', 404), request)
return createErrorResponse('Chat not found', 404)
}

const deployment = deploymentResult[0]

const storedValue = await getOTP('chat', deployment.id, email)
if (!storedValue) {
return addCorsHeaders(
createErrorResponse('No verification code found, request a new one', 400),
request
)
return createErrorResponse('No verification code found, request a new one', 400)
}

const { otp: storedOTP, attempts } = decodeOTPValue(storedValue)

if (attempts >= MAX_OTP_ATTEMPTS) {
await deleteOTP('chat', deployment.id, email)
logger.warn(`[${requestId}] OTP already locked out for ${email}`)
return addCorsHeaders(
createErrorResponse('Too many failed attempts. Please request a new code.', 429),
request
)
return createErrorResponse('Too many failed attempts. Please request a new code.', 429)
}

if (storedOTP !== otp) {
const result = await incrementOTPAttempts('chat', deployment.id, email, storedValue)
if (result === 'locked') {
logger.warn(`[${requestId}] OTP invalidated after max failed attempts for ${email}`)
return addCorsHeaders(
createErrorResponse('Too many failed attempts. Please request a new code.', 429),
request
)
return createErrorResponse('Too many failed attempts. Please request a new code.', 429)
}
return addCorsHeaders(createErrorResponse('Invalid verification code', 400), request)
return createErrorResponse('Invalid verification code', 400)
}

await deleteOTP('chat', deployment.id, email)

const response = addCorsHeaders(
createSuccessResponse({
id: deployment.id,
title: deployment.title,
description: deployment.description,
customizations: deployment.customizations,
authType: deployment.authType,
outputConfigs: deployment.outputConfigs,
}),
request
)
const response = createSuccessResponse({
id: deployment.id,
title: deployment.title,
description: deployment.description,
customizations: deployment.customizations,
authType: deployment.authType,
outputConfigs: deployment.outputConfigs,
})
setChatAuthCookie(response, deployment.id, deployment.authType, deployment.password)

return response
} catch (error) {
logger.error(`[${requestId}] Error verifying OTP:`, error)
return addCorsHeaders(createErrorResponse('Failed to process request', 500), request)
return createErrorResponse('Failed to process request', 500)
}
}
)
14 changes: 5 additions & 9 deletions apps/sim/app/api/chat/[identifier]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,11 @@ const createMockStream = () => {
})
}

const { mockAddCorsHeaders, mockValidateChatAuth, mockSetChatAuthCookie, mockValidateAuthToken } =
vi.hoisted(() => ({
mockAddCorsHeaders: vi.fn().mockImplementation((response: Response) => response),
mockValidateChatAuth: vi.fn().mockResolvedValue({ authorized: true }),
mockSetChatAuthCookie: vi.fn(),
mockValidateAuthToken: vi.fn().mockReturnValue(false),
}))
const { mockValidateChatAuth, mockSetChatAuthCookie, mockValidateAuthToken } = vi.hoisted(() => ({
mockValidateChatAuth: vi.fn().mockResolvedValue({ authorized: true }),
mockSetChatAuthCookie: vi.fn(),
mockValidateAuthToken: vi.fn().mockReturnValue(false),
}))

const mockCreateErrorResponse = workflowsApiUtilsMockFns.mockCreateErrorResponse
const mockCreateSuccessResponse = workflowsApiUtilsMockFns.mockCreateSuccessResponse
Expand All @@ -81,7 +79,6 @@ vi.mock('@sim/db', () => ({
}))

vi.mock('@/lib/core/security/deployment', () => ({
addCorsHeaders: mockAddCorsHeaders,
validateAuthToken: mockValidateAuthToken,
setDeploymentAuthCookie: vi.fn(),
isEmailAllowed: vi.fn().mockReturnValue(false),
Expand Down Expand Up @@ -181,7 +178,6 @@ describe('Chat Identifier API Route', () => {
},
})

mockAddCorsHeaders.mockImplementation((response: Response) => response)
mockValidateChatAuth.mockResolvedValue({ authorized: true })
mockValidateAuthToken.mockReturnValue(false)
mockCreateErrorResponse.mockImplementation((message: string, status: number, code?: string) => {
Expand Down
Loading
Loading