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
91 changes: 56 additions & 35 deletions apps/sim/app/api/chat/[identifier]/otp/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { z } from 'zod'
import { renderOTPEmail } from '@/components/emails/render-email'
import { sendEmail } from '@/lib/email/mailer'
import { createLogger } from '@/lib/logs/console/logger'
import { getRedisClient } from '@/lib/redis'
import { getRedisClient, markMessageAsProcessed, releaseLock } from '@/lib/redis'
import { generateRequestId } from '@/lib/utils'
import { addCorsHeaders, setChatAuthCookie } from '@/app/api/chat/utils'
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
Expand All @@ -21,52 +21,83 @@ function generateOTP() {
// We use 15 minutes (900 seconds) expiry for OTPs
const OTP_EXPIRY = 15 * 60

async function storeOTP(email: string, chatId: string, otp: string): Promise<boolean> {
// 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) {
logger.warn('Redis not available, OTP functionality requires Redis')
return false
}

try {
if (redis) {
// Use Redis if available
await redis.set(key, otp, 'EX', OTP_EXPIRY)
return true
} catch (error) {
logger.error('Error storing OTP in Redis:', error)
return false
} 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
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) {
return null
if (redis) {
// Use Redis if available
return await redis.get(key)
}
// 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 {
return await redis.get(key)
} catch (error) {
logger.error('Error getting OTP from Redis:', error)
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) {
return
}

try {
if (redis) {
// Use Redis if available
await redis.del(key)
} catch (error) {
logger.error('Error deleting OTP from Redis:', error)
} else {
// Use the existing function as fallback
await releaseLock(`processed:${key}`)
await releaseLock(`processed:${key}:value`)
}
}

Expand Down Expand Up @@ -146,17 +177,7 @@ export async function POST(

const otp = generateOTP()

const stored = await storeOTP(email, deployment.id, otp)
if (!stored) {
logger.error(`[${requestId}] Failed to store OTP - Redis unavailable`)
return addCorsHeaders(
createErrorResponse(
'Email verification temporarily unavailable, please try again later',
503
),
request
)
}
await storeOTP(email, deployment.id, otp)

const emailHtml = await renderOTPEmail(
otp,
Expand Down
37 changes: 0 additions & 37 deletions apps/sim/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ import { quickValidateEmail } from '@/lib/email/validation'
import { env, isTruthy } from '@/lib/env'
import { isBillingEnabled, isEmailVerificationEnabled } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console/logger'
import { getRedisClient } from '@/lib/redis'
import { SSO_TRUSTED_PROVIDERS } from './sso/consts'

const logger = createLogger('Auth')
Expand All @@ -60,40 +59,6 @@ if (validStripeKey) {
})
}

// Configure Redis secondary storage for session data (optional)
const redis = getRedisClient()
const redisSecondaryStorage = redis
? {
get: async (key: string) => {
try {
const value = await redis.get(key)
return value
} catch (error) {
logger.error('Redis get error in secondaryStorage', { key, error })
return null
}
},
set: async (key: string, value: string, ttl?: number) => {
try {
if (ttl) {
await redis.set(key, value, 'EX', ttl)
} else {
await redis.set(key, value)
}
} catch (error) {
logger.error('Redis set error in secondaryStorage', { key, ttl, error })
}
},
delete: async (key: string) => {
try {
await redis.del(key)
} catch (error) {
logger.error('Redis delete error in secondaryStorage', { key, error })
}
},
}
: undefined

export const auth = betterAuth({
baseURL: getBaseURL(),
trustedOrigins: [
Expand All @@ -104,8 +69,6 @@ export const auth = betterAuth({
provider: 'pg',
schema,
}),
// Conditionally add secondaryStorage only if Redis is available
...(redisSecondaryStorage ? { secondaryStorage: redisSecondaryStorage } : {}),
session: {
cookieCache: {
enabled: true,
Expand Down
Loading