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
2 changes: 1 addition & 1 deletion apps/sim/app/(landing)/actions/github.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createLogger } from '@/lib/logs/console/logger'

const DEFAULT_STARS = '18.6k'
const DEFAULT_STARS = '19.4k'

const logger = createLogger('GitHubStars')

Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/api/chat/[identifier]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export async function POST(
if ((password || email) && !input) {
const response = addCorsHeaders(createSuccessResponse({ authenticated: true }), request)

setChatAuthCookie(response, deployment.id, deployment.authType)
setChatAuthCookie(response, deployment.id, deployment.authType, deployment.password)

return response
}
Expand Down Expand Up @@ -315,7 +315,7 @@ export async function GET(
if (
deployment.authType !== 'public' &&
authCookie &&
validateAuthToken(authCookie.value, deployment.id)
validateAuthToken(authCookie.value, deployment.id, deployment.password)
) {
return addCorsHeaders(
createSuccessResponse({
Expand Down
41 changes: 32 additions & 9 deletions apps/sim/app/api/chat/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createHash } from 'crypto'
import { db } from '@sim/db'
import { chat, workflow } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
Expand All @@ -9,6 +10,10 @@ import { hasAdminPermission } from '@/lib/workspaces/permissions/utils'

const logger = createLogger('ChatAuthUtils')

function hashPassword(encryptedPassword: string): string {
return createHash('sha256').update(encryptedPassword).digest('hex').substring(0, 8)
}

/**
* Check if user has permission to create a chat for a specific workflow
* Either the user owns the workflow directly OR has admin permission for the workflow's workspace
Expand Down Expand Up @@ -77,43 +82,61 @@ export async function checkChatAccess(
return { hasAccess: false }
}

const encryptAuthToken = (chatId: string, type: string): string => {
return Buffer.from(`${chatId}:${type}:${Date.now()}`).toString('base64')
function encryptAuthToken(chatId: string, type: string, encryptedPassword?: string | null): string {
const pwHash = encryptedPassword ? hashPassword(encryptedPassword) : ''
return Buffer.from(`${chatId}:${type}:${Date.now()}:${pwHash}`).toString('base64')
}

export const validateAuthToken = (token: string, chatId: string): boolean => {
export function validateAuthToken(
token: string,
chatId: string,
encryptedPassword?: string | null
): boolean {
try {
const decoded = Buffer.from(token, 'base64').toString()
const [storedId, _type, timestamp] = decoded.split(':')
const parts = decoded.split(':')
const [storedId, _type, timestamp, storedPwHash] = parts

if (storedId !== chatId) {
return false
}

const createdAt = Number.parseInt(timestamp)
const now = Date.now()
const expireTime = 24 * 60 * 60 * 1000 // 24 hours
const expireTime = 24 * 60 * 60 * 1000

if (now - createdAt > expireTime) {
return false
}

if (encryptedPassword) {
const currentPwHash = hashPassword(encryptedPassword)
if (storedPwHash !== currentPwHash) {
return false
}
}

return true
} catch (_e) {
return false
}
}

export const setChatAuthCookie = (response: NextResponse, chatId: string, type: string): void => {
const token = encryptAuthToken(chatId, type)
export function setChatAuthCookie(
response: NextResponse,
chatId: string,
type: string,
encryptedPassword?: string | null
): void {
const token = encryptAuthToken(chatId, type, encryptedPassword)
response.cookies.set({
name: `chat_auth_${chatId}`,
value: token,
httpOnly: true,
secure: !isDev,
sameSite: 'lax',
path: '/',
maxAge: 60 * 60 * 24, // 24 hours
maxAge: 60 * 60 * 24,
})
}

Expand Down Expand Up @@ -145,7 +168,7 @@ export async function validateChatAuth(
const cookieName = `chat_auth_${deployment.id}`
const authCookie = request.cookies.get(cookieName)

if (authCookie && validateAuthToken(authCookie.value, deployment.id)) {
if (authCookie && validateAuthToken(authCookie.value, deployment.id, deployment.password)) {
return { authorized: true }
}

Expand Down
69 changes: 62 additions & 7 deletions apps/sim/app/api/proxy/tts/stream/route.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,81 @@
import { db } from '@sim/db'
import { chat } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { env } from '@/lib/core/config/env'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { createLogger } from '@/lib/logs/console/logger'
import { validateAuthToken } from '@/app/api/chat/utils'

const logger = createLogger('ProxyTTSStreamAPI')

/**
* Validates chat-based authentication for deployed chat voice mode
* Checks if the user has a valid chat auth cookie for the given chatId
*/
async function validateChatAuth(request: NextRequest, chatId: string): Promise<boolean> {
try {
const chatResult = await db
.select({
id: chat.id,
isActive: chat.isActive,
authType: chat.authType,
password: chat.password,
})
.from(chat)
.where(eq(chat.id, chatId))
.limit(1)

if (chatResult.length === 0 || !chatResult[0].isActive) {
logger.warn('Chat not found or inactive for TTS auth:', chatId)
return false
}

const chatData = chatResult[0]

if (chatData.authType === 'public') {
return true
}

const cookieName = `chat_auth_${chatId}`
const authCookie = request.cookies.get(cookieName)

if (authCookie && validateAuthToken(authCookie.value, chatId, chatData.password)) {
return true
}

return false
} catch (error) {
logger.error('Error validating chat auth for TTS:', error)
return false
}
}

export async function POST(request: NextRequest) {
try {
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.error('Authentication failed for TTS stream proxy:', authResult.error)
return new Response('Unauthorized', { status: 401 })
let body: any
try {
body = await request.json()
} catch {
return new Response('Invalid request body', { status: 400 })
}

const body = await request.json()
const { text, voiceId, modelId = 'eleven_turbo_v2_5' } = body
const { text, voiceId, modelId = 'eleven_turbo_v2_5', chatId } = body

if (!chatId) {
return new Response('chatId is required', { status: 400 })
}

if (!text || !voiceId) {
return new Response('Missing required parameters', { status: 400 })
}

const isChatAuthed = await validateChatAuth(request, chatId)
if (!isChatAuthed) {
logger.warn('Chat authentication failed for TTS, chatId:', chatId)
return new Response('Unauthorized', { status: 401 })
}

const voiceIdValidation = validateAlphanumericId(voiceId, 'voiceId', 255)
if (!voiceIdValidation.isValid) {
logger.error(`Invalid voice ID: ${voiceIdValidation.error}`)
Expand Down
6 changes: 3 additions & 3 deletions apps/sim/app/api/stars/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ export async function GET() {

if (!response.ok) {
console.warn('GitHub API request failed:', response.status)
return NextResponse.json({ stars: formatStarCount(14500) })
return NextResponse.json({ stars: formatStarCount(19400) })
}

const data = await response.json()
return NextResponse.json({ stars: formatStarCount(Number(data?.stargazers_count ?? 14500)) })
return NextResponse.json({ stars: formatStarCount(Number(data?.stargazers_count ?? 19400)) })
} catch (error) {
console.warn('Error fetching GitHub stars:', error)
return NextResponse.json({ stars: formatStarCount(14500) })
return NextResponse.json({ stars: formatStarCount(19400) })
}
}
14 changes: 11 additions & 3 deletions apps/sim/app/chat/[identifier]/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ interface ChatConfig {

interface AudioStreamingOptions {
voiceId: string
chatId?: string
onError: (error: Error) => void
}

Expand All @@ -62,16 +63,19 @@ function fileToBase64(file: File): Promise<string> {
* Creates an audio stream handler for text-to-speech conversion
* @param streamTextToAudio - Function to stream text to audio
* @param voiceId - The voice ID to use for TTS
* @param chatId - Optional chat ID for deployed chat authentication
* @returns Audio stream handler function or undefined
*/
function createAudioStreamHandler(
streamTextToAudio: (text: string, options: AudioStreamingOptions) => Promise<void>,
voiceId: string
voiceId: string,
chatId?: string
) {
return async (text: string) => {
try {
await streamTextToAudio(text, {
voiceId,
chatId,
onError: (error: Error) => {
logger.error('Audio streaming error:', error)
},
Expand Down Expand Up @@ -113,7 +117,7 @@ export default function ChatClient({ identifier }: { identifier: string }) {
const [error, setError] = useState<string | null>(null)
const messagesEndRef = useRef<HTMLDivElement>(null)
const messagesContainerRef = useRef<HTMLDivElement>(null)
const [starCount, setStarCount] = useState('3.4k')
const [starCount, setStarCount] = useState('19.4k')
const [conversationId, setConversationId] = useState('')

const [showScrollButton, setShowScrollButton] = useState(false)
Expand Down Expand Up @@ -391,7 +395,11 @@ export default function ChatClient({ identifier }: { identifier: string }) {
// Use the streaming hook with audio support
const shouldPlayAudio = isVoiceInput || isVoiceFirstMode
const audioHandler = shouldPlayAudio
? createAudioStreamHandler(streamTextToAudio, DEFAULT_VOICE_SETTINGS.voiceId)
? createAudioStreamHandler(
streamTextToAudio,
DEFAULT_VOICE_SETTINGS.voiceId,
chatConfig?.id
)
: undefined

logger.info('Starting to handle streamed response:', { shouldPlayAudio })
Expand Down
Loading