Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
345cf61
added chatbot table with fk to workflows, added modal to deploy and d…
waleedlatif1 Apr 16, 2025
d2f35d7
fixed styling, added delete and edit routes for chatbot
waleedlatif1 Apr 16, 2025
264054a
use loading-agent animation for editing existing chatbot
waleedlatif1 Apr 16, 2025
beb63dc
add base_url so that we can delpoy in dev as well
waleedlatif1 Apr 16, 2025
16c41d4
fixed CORS issue, fixed password verification, can deploy chatbot and…
waleedlatif1 Apr 17, 2025
17e31d2
fix: renamed chatbot to chat and changed chat to copilot
emir-karabeg Apr 19, 2025
c6d5e55
feat(chat-deploy): refactored api deploy flow
emir-karabeg Apr 21, 2025
faa793e
feat(chat-deploy): added chat to deploy flow
emir-karabeg Apr 21, 2025
100e2db
added output selector to chat deploy, deployment works and we can get…
waleedlatif1 Apr 21, 2025
15e17fe
add missing dependencies, fix build errors, remove old unused route
waleedlatif1 Apr 21, 2025
c8ed6e1
error disappeared for block output selection, need to update UI, add …
waleedlatif1 Apr 21, 2025
acc6b0b
added otp for email verification on chat deploy
waleedlatif1 Apr 22, 2025
3be6ef7
feat(chat-deploy): ux improvements with chat-deploy modal
emir-karabeg Apr 23, 2025
a1bd7ed
improvement(ui/ux): chat display improvement
emir-karabeg Apr 25, 2025
9858745
improvement(ui/ux): deploy modal
emir-karabeg Apr 26, 2025
0d045c0
added logging category for chat panel & chat deploy executions
waleedlatif1 Apr 27, 2025
05bed59
improvement(ui/ux): finished chat-deploy flow
emir-karabeg Apr 27, 2025
00227da
fix: deleted migrations
emir-karabeg Apr 27, 2025
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
319 changes: 319 additions & 0 deletions sim/app/api/chat/[subdomain]/otp/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
import { NextRequest } from 'next/server'
import { eq } from 'drizzle-orm'
import { z } from 'zod'
import { createLogger } from '@/lib/logs/console-logger'
import { db } from '@/db'
import { chat } from '@/db/schema'
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
import { addCorsHeaders, setChatAuthCookie } from '../../utils'
import { sendEmail } from '@/lib/mailer'
import { render } from '@react-email/render'
import OTPVerificationEmail from '@/components/emails/otp-verification-email'
import { getRedisClient, markMessageAsProcessed, releaseLock } from '@/lib/redis'

const logger = createLogger('ChatOtpAPI')

function generateOTP() {
return Math.floor(100000 + Math.random() * 900000).toString()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Math.random() for generating OTPs is not cryptographically secure

}

// OTP storage utility functions using Redis
// We use 15 minutes (900 seconds) expiry for OTPs
const OTP_EXPIRY = 15 * 60

// Store OTP in Redis
async function storeOTP(email: string, chatId: string, otp: string): Promise<void> {
const key = `otp:${email}:${chatId}`
const redis = getRedisClient()

if (redis) {
// Use Redis if available
await redis.set(key, otp, 'EX', OTP_EXPIRY)
} else {
// Use the existing function as fallback to mark that an OTP exists
await markMessageAsProcessed(key, OTP_EXPIRY)

// For the fallback case, we need to handle storing the OTP value separately
// since markMessageAsProcessed only stores "1"
const valueKey = `${key}:value`
try {
// Access the in-memory cache directly - hacky but works for fallback
const inMemoryCache = (global as any).inMemoryCache
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsafe access to global object without proper validation

if (inMemoryCache) {
const fullKey = `processed:${valueKey}`
const expiry = OTP_EXPIRY ? Date.now() + OTP_EXPIRY * 1000 : null
inMemoryCache.set(fullKey, { value: otp, expiry })
}
} catch (error) {
logger.error('Error storing OTP in fallback cache:', error)
}
}
}

// Get OTP from Redis
async function getOTP(email: string, chatId: string): Promise<string | null> {
const key = `otp:${email}:${chatId}`
const redis = getRedisClient()

if (redis) {
// Use Redis if available
return await redis.get(key)
} else {
// Use the existing function as fallback - check if it exists
const exists = await new Promise(resolve => {
try {
// Check the in-memory cache directly - hacky but works for fallback
const inMemoryCache = (global as any).inMemoryCache
const fullKey = `processed:${key}`
const cacheEntry = inMemoryCache?.get(fullKey)
resolve(!!cacheEntry)
} catch {
resolve(false)
}
})

if (!exists) return null

// Try to get the value key
const valueKey = `${key}:value`
try {
const inMemoryCache = (global as any).inMemoryCache
const fullKey = `processed:${valueKey}`
const cacheEntry = inMemoryCache?.get(fullKey)
return cacheEntry?.value || null
} catch {
return null
}
}
}

// Delete OTP from Redis
async function deleteOTP(email: string, chatId: string): Promise<void> {
const key = `otp:${email}:${chatId}`
const redis = getRedisClient()

if (redis) {
// Use Redis if available
await redis.del(key)
} else {
// Use the existing function as fallback
await releaseLock(`processed:${key}`)
await releaseLock(`processed:${key}:value`)
}
}

const otpRequestSchema = z.object({
email: z.string().email('Invalid email address'),
})

const otpVerifySchema = z.object({
email: z.string().email('Invalid email address'),
otp: z.string().length(6, 'OTP must be 6 digits'),
})

// Send OTP endpoint
export async function POST(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No rate limiting for OTP generation requests

request: NextRequest,
{ params }: { params: Promise<{ subdomain: string }> }
) {
const { subdomain } = await params
const requestId = crypto.randomUUID().slice(0, 8)

try {
logger.debug(`[${requestId}] Processing OTP request for subdomain: ${subdomain}`)

// Parse request body
let body
try {
body = await request.json()
const { email } = otpRequestSchema.parse(body)

// Find the chat deployment
const deploymentResult = await db
.select({
id: chat.id,
authType: chat.authType,
allowedEmails: chat.allowedEmails,
title: chat.title,
})
.from(chat)
.where(eq(chat.subdomain, subdomain))
.limit(1)

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

const deployment = deploymentResult[0]

// Verify this is an email-protected chat
if (deployment.authType !== 'email') {
return addCorsHeaders(
createErrorResponse('This chat does not use email authentication', 400),
request
)
}

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

// Check if the email is allowed
const isEmailAllowed =
allowedEmails.includes(email) ||
allowedEmails.some((allowed: string) => {
if (allowed.startsWith('@')) {
const domain = email.split('@')[1]
return domain && allowed === `@${domain}`
}
return false
})

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

// Generate OTP
const otp = generateOTP()

// Store OTP in Redis - AWAIT THIS BEFORE RETURNING RESPONSE
await storeOTP(email, deployment.id, otp)

// Create the email
const emailContent = OTPVerificationEmail({
otp,
email,
type: 'chat-access',
chatTitle: deployment.title || 'Chat',
})

// await the render function
const emailHtml = await render(emailContent)

// MAKE SURE TO AWAIT THE EMAIL SENDING
const emailResult = await sendEmail({
to: email,
subject: `Verification code for ${deployment.title || 'Chat'}`,
html: emailHtml,
})

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

// Add a small delay to ensure Redis has fully processed the operation
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary delay after Redis operations

// This helps with eventual consistency in distributed systems
await new Promise(resolve => setTimeout(resolve, 500))

logger.info(`[${requestId}] OTP sent to ${email} for chat ${deployment.id}`)
return addCorsHeaders(
createSuccessResponse({ message: 'Verification code sent' }),
request
)
} catch (error: any) {
if (error instanceof z.ZodError) {
return addCorsHeaders(
createErrorResponse(error.errors[0]?.message || 'Invalid request', 400),
request
)
}
throw error
}
} catch (error: any) {
logger.error(`[${requestId}] Error processing OTP request:`, error)
return addCorsHeaders(
createErrorResponse(error.message || 'Failed to process request', 500),
request
)
}
}

// Verify OTP endpoint
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ subdomain: string }> }
) {
const { subdomain } = await params
const requestId = crypto.randomUUID().slice(0, 8)

try {
logger.debug(`[${requestId}] Verifying OTP for subdomain: ${subdomain}`)

// Parse request body
let body
try {
body = await request.json()
const { email, otp } = otpVerifySchema.parse(body)

// Find the chat deployment
const deploymentResult = await db
.select({
id: chat.id,
authType: chat.authType,
})
.from(chat)
.where(eq(chat.subdomain, subdomain))
.limit(1)

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

const deployment = deploymentResult[0]

// Check if OTP exists and is valid
const storedOTP = await getOTP(email, deployment.id)
if (!storedOTP) {
return addCorsHeaders(
createErrorResponse('No verification code found, request a new one', 400),
request
)
}

// Check if OTP matches
if (storedOTP !== otp) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential timing attack vulnerability in OTP comparison

return addCorsHeaders(
createErrorResponse('Invalid verification code', 400),
request
)
}

// OTP is valid, clean up
await deleteOTP(email, deployment.id)

// Create success response with auth cookie
const response = addCorsHeaders(
createSuccessResponse({ authenticated: true }),
request
)

// Set authentication cookie
setChatAuthCookie(response, deployment.id, deployment.authType)

return response
} catch (error: any) {
if (error instanceof z.ZodError) {
return addCorsHeaders(
createErrorResponse(error.errors[0]?.message || 'Invalid request', 400),
request
)
}
throw error
}
} catch (error: any) {
logger.error(`[${requestId}] Error verifying OTP:`, error)
return addCorsHeaders(
createErrorResponse(error.message || 'Failed to process request', 500),
request
)
}
}
Loading