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: 35 additions & 56 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, markMessageAsProcessed, releaseLock } from '@/lib/redis'
import { getRedisClient } 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,83 +21,52 @@ function generateOTP() {
// 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> {
async function storeOTP(email: string, chatId: string, otp: string): Promise<boolean> {
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)
if (!redis) {
logger.warn('Redis not available, OTP functionality requires Redis')
return false
}

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

// 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)
if (!redis) {
return null
}
// 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 await redis.get(key)
} catch (error) {
logger.error('Error getting OTP from Redis:', error)
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
if (!redis) {
return
}

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

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

const otp = generateOTP()

await storeOTP(email, deployment.id, otp)
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
)
}

const emailHtml = await renderOTPEmail(
otp,
Expand Down
37 changes: 37 additions & 0 deletions apps/sim/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ 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 @@ -59,6 +60,40 @@ 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 @@ -69,6 +104,8 @@ 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