mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-08 22:48:14 -05:00
fix(chat-deploy): fixed permissions to match the workspace permissions, admins can deploy & edit & delete (#753)
* fix(chat-deploy): fixed permissions to match the workspace permissions, admins can deploy & edit & delete * fixed hanging chat deploy modal * remove unnecessary fallback
This commit is contained in:
396
apps/sim/app/api/chat/edit/[id]/route.test.ts
Normal file
396
apps/sim/app/api/chat/edit/[id]/route.test.ts
Normal file
@@ -0,0 +1,396 @@
|
||||
import { NextRequest } from 'next/server'
|
||||
/**
|
||||
* Tests for chat edit API route
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
describe('Chat Edit API Route', () => {
|
||||
const mockSelect = vi.fn()
|
||||
const mockFrom = vi.fn()
|
||||
const mockWhere = vi.fn()
|
||||
const mockLimit = vi.fn()
|
||||
const mockUpdate = vi.fn()
|
||||
const mockSet = vi.fn()
|
||||
const mockDelete = vi.fn()
|
||||
|
||||
const mockCreateSuccessResponse = vi.fn()
|
||||
const mockCreateErrorResponse = vi.fn()
|
||||
const mockEncryptSecret = vi.fn()
|
||||
const mockCheckChatAccess = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
|
||||
mockSelect.mockReturnValue({ from: mockFrom })
|
||||
mockFrom.mockReturnValue({ where: mockWhere })
|
||||
mockWhere.mockReturnValue({ limit: mockLimit })
|
||||
mockUpdate.mockReturnValue({ set: mockSet })
|
||||
mockSet.mockReturnValue({ where: mockWhere })
|
||||
mockDelete.mockReturnValue({ where: mockWhere })
|
||||
|
||||
vi.doMock('@/db', () => ({
|
||||
db: {
|
||||
select: mockSelect,
|
||||
update: mockUpdate,
|
||||
delete: mockDelete,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.doMock('@/db/schema', () => ({
|
||||
chat: { id: 'id', subdomain: 'subdomain', userId: 'userId' },
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/logs/console-logger', () => ({
|
||||
createLogger: vi.fn().mockReturnValue({
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('@/app/api/workflows/utils', () => ({
|
||||
createSuccessResponse: mockCreateSuccessResponse.mockImplementation((data) => {
|
||||
return new Response(JSON.stringify(data), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}),
|
||||
createErrorResponse: mockCreateErrorResponse.mockImplementation((message, status = 500) => {
|
||||
return new Response(JSON.stringify({ error: message }), {
|
||||
status,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/utils', () => ({
|
||||
encryptSecret: mockEncryptSecret.mockResolvedValue({ encrypted: 'encrypted-password' }),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/urls/utils', () => ({
|
||||
getBaseDomain: vi.fn().mockReturnValue('localhost:3000'),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/environment', () => ({
|
||||
isDev: true,
|
||||
}))
|
||||
|
||||
vi.doMock('@/app/api/chat/utils', () => ({
|
||||
checkChatAccess: mockCheckChatAccess,
|
||||
}))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('GET', () => {
|
||||
it('should return 401 when user is not authenticated', async () => {
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue(null),
|
||||
}))
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/chat/edit/chat-123')
|
||||
const { GET } = await import('./route')
|
||||
const response = await GET(req, { params: Promise.resolve({ id: 'chat-123' }) })
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unauthorized', 401)
|
||||
})
|
||||
|
||||
it('should return 404 when chat not found or access denied', async () => {
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-id' },
|
||||
}),
|
||||
}))
|
||||
|
||||
mockCheckChatAccess.mockResolvedValue({ hasAccess: false })
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/chat/edit/chat-123')
|
||||
const { GET } = await import('./route')
|
||||
const response = await GET(req, { params: Promise.resolve({ id: 'chat-123' }) })
|
||||
|
||||
expect(response.status).toBe(404)
|
||||
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Chat not found or access denied', 404)
|
||||
expect(mockCheckChatAccess).toHaveBeenCalledWith('chat-123', 'user-id')
|
||||
})
|
||||
|
||||
it('should return chat details when user has access', async () => {
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-id' },
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockChat = {
|
||||
id: 'chat-123',
|
||||
subdomain: 'test-chat',
|
||||
title: 'Test Chat',
|
||||
description: 'A test chat',
|
||||
password: 'encrypted-password',
|
||||
customizations: { primaryColor: '#000000' },
|
||||
}
|
||||
|
||||
mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat })
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/chat/edit/chat-123')
|
||||
const { GET } = await import('./route')
|
||||
const response = await GET(req, { params: Promise.resolve({ id: 'chat-123' }) })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(mockCreateSuccessResponse).toHaveBeenCalledWith({
|
||||
id: 'chat-123',
|
||||
subdomain: 'test-chat',
|
||||
title: 'Test Chat',
|
||||
description: 'A test chat',
|
||||
customizations: { primaryColor: '#000000' },
|
||||
chatUrl: 'http://test-chat.localhost:3000',
|
||||
hasPassword: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('PATCH', () => {
|
||||
it('should return 401 when user is not authenticated', async () => {
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue(null),
|
||||
}))
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/chat/edit/chat-123', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ title: 'Updated Chat' }),
|
||||
})
|
||||
const { PATCH } = await import('./route')
|
||||
const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) })
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unauthorized', 401)
|
||||
})
|
||||
|
||||
it('should return 404 when chat not found or access denied', async () => {
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-id' },
|
||||
}),
|
||||
}))
|
||||
|
||||
mockCheckChatAccess.mockResolvedValue({ hasAccess: false })
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/chat/edit/chat-123', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ title: 'Updated Chat' }),
|
||||
})
|
||||
const { PATCH } = await import('./route')
|
||||
const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) })
|
||||
|
||||
expect(response.status).toBe(404)
|
||||
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Chat not found or access denied', 404)
|
||||
expect(mockCheckChatAccess).toHaveBeenCalledWith('chat-123', 'user-id')
|
||||
})
|
||||
|
||||
it('should update chat when user has access', async () => {
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-id' },
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockChat = {
|
||||
id: 'chat-123',
|
||||
subdomain: 'test-chat',
|
||||
title: 'Test Chat',
|
||||
authType: 'public',
|
||||
}
|
||||
|
||||
mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat })
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/chat/edit/chat-123', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ title: 'Updated Chat', description: 'Updated description' }),
|
||||
})
|
||||
const { PATCH } = await import('./route')
|
||||
const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(mockUpdate).toHaveBeenCalled()
|
||||
expect(mockCreateSuccessResponse).toHaveBeenCalledWith({
|
||||
id: 'chat-123',
|
||||
chatUrl: 'http://test-chat.localhost:3000',
|
||||
message: 'Chat deployment updated successfully',
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle subdomain conflicts', async () => {
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-id' },
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockChat = {
|
||||
id: 'chat-123',
|
||||
subdomain: 'test-chat',
|
||||
title: 'Test Chat',
|
||||
}
|
||||
|
||||
mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat })
|
||||
// Mock subdomain conflict
|
||||
mockLimit.mockResolvedValueOnce([{ id: 'other-chat-id', subdomain: 'new-subdomain' }])
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/chat/edit/chat-123', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ subdomain: 'new-subdomain' }),
|
||||
})
|
||||
const { PATCH } = await import('./route')
|
||||
const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) })
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Subdomain already in use', 400)
|
||||
})
|
||||
|
||||
it('should validate password requirement for password auth', async () => {
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-id' },
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockChat = {
|
||||
id: 'chat-123',
|
||||
subdomain: 'test-chat',
|
||||
title: 'Test Chat',
|
||||
authType: 'public',
|
||||
password: null,
|
||||
}
|
||||
|
||||
mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat })
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/chat/edit/chat-123', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ authType: 'password' }), // No password provided
|
||||
})
|
||||
const { PATCH } = await import('./route')
|
||||
const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) })
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(mockCreateErrorResponse).toHaveBeenCalledWith(
|
||||
'Password is required when using password protection',
|
||||
400
|
||||
)
|
||||
})
|
||||
|
||||
it('should allow access when user has workspace admin permission', async () => {
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'admin-user-id' },
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockChat = {
|
||||
id: 'chat-123',
|
||||
subdomain: 'test-chat',
|
||||
title: 'Test Chat',
|
||||
authType: 'public',
|
||||
}
|
||||
|
||||
// User doesn't own chat but has workspace admin access
|
||||
mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat })
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/chat/edit/chat-123', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ title: 'Admin Updated Chat' }),
|
||||
})
|
||||
const { PATCH } = await import('./route')
|
||||
const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(mockCheckChatAccess).toHaveBeenCalledWith('chat-123', 'admin-user-id')
|
||||
})
|
||||
})
|
||||
|
||||
describe('DELETE', () => {
|
||||
it('should return 401 when user is not authenticated', async () => {
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue(null),
|
||||
}))
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/chat/edit/chat-123', {
|
||||
method: 'DELETE',
|
||||
})
|
||||
const { DELETE } = await import('./route')
|
||||
const response = await DELETE(req, { params: Promise.resolve({ id: 'chat-123' }) })
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unauthorized', 401)
|
||||
})
|
||||
|
||||
it('should return 404 when chat not found or access denied', async () => {
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-id' },
|
||||
}),
|
||||
}))
|
||||
|
||||
mockCheckChatAccess.mockResolvedValue({ hasAccess: false })
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/chat/edit/chat-123', {
|
||||
method: 'DELETE',
|
||||
})
|
||||
const { DELETE } = await import('./route')
|
||||
const response = await DELETE(req, { params: Promise.resolve({ id: 'chat-123' }) })
|
||||
|
||||
expect(response.status).toBe(404)
|
||||
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Chat not found or access denied', 404)
|
||||
expect(mockCheckChatAccess).toHaveBeenCalledWith('chat-123', 'user-id')
|
||||
})
|
||||
|
||||
it('should delete chat when user has access', async () => {
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-id' },
|
||||
}),
|
||||
}))
|
||||
|
||||
mockCheckChatAccess.mockResolvedValue({ hasAccess: true })
|
||||
mockWhere.mockResolvedValue(undefined)
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/chat/edit/chat-123', {
|
||||
method: 'DELETE',
|
||||
})
|
||||
const { DELETE } = await import('./route')
|
||||
const response = await DELETE(req, { params: Promise.resolve({ id: 'chat-123' }) })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(mockDelete).toHaveBeenCalled()
|
||||
expect(mockCreateSuccessResponse).toHaveBeenCalledWith({
|
||||
message: 'Chat deployment deleted successfully',
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow deletion when user has workspace admin permission', async () => {
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'admin-user-id' },
|
||||
}),
|
||||
}))
|
||||
|
||||
// User doesn't own chat but has workspace admin access
|
||||
mockCheckChatAccess.mockResolvedValue({ hasAccess: true })
|
||||
mockWhere.mockResolvedValue(undefined)
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/chat/edit/chat-123', {
|
||||
method: 'DELETE',
|
||||
})
|
||||
const { DELETE } = await import('./route')
|
||||
const response = await DELETE(req, { params: Promise.resolve({ id: 'chat-123' }) })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(mockCheckChatAccess).toHaveBeenCalledWith('chat-123', 'admin-user-id')
|
||||
expect(mockDelete).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
@@ -6,6 +6,7 @@ import { isDev } from '@/lib/environment'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { getBaseDomain } from '@/lib/urls/utils'
|
||||
import { encryptSecret } from '@/lib/utils'
|
||||
import { checkChatAccess } from '@/app/api/chat/utils'
|
||||
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
||||
import { db } from '@/db'
|
||||
import { chat } from '@/db/schema'
|
||||
@@ -57,23 +58,19 @@ export async function GET(_request: NextRequest, { params }: { params: Promise<{
|
||||
return createErrorResponse('Unauthorized', 401)
|
||||
}
|
||||
|
||||
// Get the specific chat deployment
|
||||
const chatInstance = await db
|
||||
.select()
|
||||
.from(chat)
|
||||
.where(and(eq(chat.id, chatId), eq(chat.userId, session.user.id)))
|
||||
.limit(1)
|
||||
// Check if user has access to view this chat
|
||||
const { hasAccess, chat: chatRecord } = await checkChatAccess(chatId, session.user.id)
|
||||
|
||||
if (chatInstance.length === 0) {
|
||||
if (!hasAccess || !chatRecord) {
|
||||
return createErrorResponse('Chat not found or access denied', 404)
|
||||
}
|
||||
|
||||
// Create a new result object without the password
|
||||
const { password, ...safeData } = chatInstance[0]
|
||||
const { password, ...safeData } = chatRecord
|
||||
|
||||
const chatUrl = isDev
|
||||
? `http://${chatInstance[0].subdomain}.${getBaseDomain()}`
|
||||
: `https://${chatInstance[0].subdomain}.simstudio.ai`
|
||||
const baseDomain = getBaseDomain()
|
||||
const protocol = isDev ? 'http' : 'https'
|
||||
const chatUrl = `${protocol}://${chatRecord.subdomain}.${baseDomain}`
|
||||
|
||||
const result = {
|
||||
...safeData,
|
||||
@@ -107,17 +104,15 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||
try {
|
||||
const validatedData = chatUpdateSchema.parse(body)
|
||||
|
||||
// Verify the chat exists and belongs to the user
|
||||
const existingChat = await db
|
||||
.select()
|
||||
.from(chat)
|
||||
.where(and(eq(chat.id, chatId), eq(chat.userId, session.user.id)))
|
||||
.limit(1)
|
||||
// Check if user has access to edit this chat
|
||||
const { hasAccess, chat: existingChatRecord } = await checkChatAccess(chatId, session.user.id)
|
||||
|
||||
if (existingChat.length === 0) {
|
||||
if (!hasAccess || !existingChatRecord) {
|
||||
return createErrorResponse('Chat not found or access denied', 404)
|
||||
}
|
||||
|
||||
const existingChat = [existingChatRecord] // Keep array format for compatibility
|
||||
|
||||
// Extract validated data
|
||||
const {
|
||||
workflowId,
|
||||
@@ -219,9 +214,9 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||
|
||||
const updatedSubdomain = subdomain || existingChat[0].subdomain
|
||||
|
||||
const chatUrl = isDev
|
||||
? `http://${updatedSubdomain}.${getBaseDomain()}`
|
||||
: `https://${updatedSubdomain}.simstudio.ai`
|
||||
const baseDomain = getBaseDomain()
|
||||
const protocol = isDev ? 'http' : 'https'
|
||||
const chatUrl = `${protocol}://${updatedSubdomain}.${baseDomain}`
|
||||
|
||||
logger.info(`Chat "${chatId}" updated successfully`)
|
||||
|
||||
@@ -260,14 +255,10 @@ export async function DELETE(
|
||||
return createErrorResponse('Unauthorized', 401)
|
||||
}
|
||||
|
||||
// Verify the chat exists and belongs to the user
|
||||
const existingChat = await db
|
||||
.select()
|
||||
.from(chat)
|
||||
.where(and(eq(chat.id, chatId), eq(chat.userId, session.user.id)))
|
||||
.limit(1)
|
||||
// Check if user has access to delete this chat
|
||||
const { hasAccess } = await checkChatAccess(chatId, session.user.id)
|
||||
|
||||
if (existingChat.length === 0) {
|
||||
if (!hasAccess) {
|
||||
return createErrorResponse('Chat not found or access denied', 404)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ describe('Chat API Route', () => {
|
||||
const mockCreateSuccessResponse = vi.fn()
|
||||
const mockCreateErrorResponse = vi.fn()
|
||||
const mockEncryptSecret = vi.fn()
|
||||
const mockCheckWorkflowAccessForChatCreation = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
@@ -71,6 +72,10 @@ describe('Chat API Route', () => {
|
||||
vi.doMock('uuid', () => ({
|
||||
v4: vi.fn().mockReturnValue('test-uuid'),
|
||||
}))
|
||||
|
||||
vi.doMock('@/app/api/chat/utils', () => ({
|
||||
checkWorkflowAccessForChatCreation: mockCheckWorkflowAccessForChatCreation,
|
||||
}))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -194,7 +199,7 @@ describe('Chat API Route', () => {
|
||||
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Subdomain already in use', 400)
|
||||
})
|
||||
|
||||
it('should reject if workflow not found or not owned by user', async () => {
|
||||
it('should reject if workflow not found', async () => {
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-id' },
|
||||
@@ -212,7 +217,7 @@ describe('Chat API Route', () => {
|
||||
}
|
||||
|
||||
mockLimit.mockResolvedValueOnce([]) // Subdomain is available
|
||||
mockLimit.mockResolvedValueOnce([]) // Workflow not found
|
||||
mockCheckWorkflowAccessForChatCreation.mockResolvedValue({ hasAccess: false })
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/chat', {
|
||||
method: 'POST',
|
||||
@@ -228,6 +233,158 @@ describe('Chat API Route', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should allow chat deployment when user owns workflow directly', async () => {
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-id' },
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/env', () => ({
|
||||
env: {
|
||||
NODE_ENV: 'development',
|
||||
NEXT_PUBLIC_APP_URL: 'http://localhost:3000',
|
||||
},
|
||||
}))
|
||||
|
||||
const validData = {
|
||||
workflowId: 'workflow-123',
|
||||
subdomain: 'test-chat',
|
||||
title: 'Test Chat',
|
||||
customizations: {
|
||||
primaryColor: '#000000',
|
||||
welcomeMessage: 'Hello',
|
||||
},
|
||||
}
|
||||
|
||||
mockLimit.mockResolvedValueOnce([]) // Subdomain is available
|
||||
mockCheckWorkflowAccessForChatCreation.mockResolvedValue({
|
||||
hasAccess: true,
|
||||
workflow: { userId: 'user-id', workspaceId: null, isDeployed: true },
|
||||
})
|
||||
mockReturning.mockResolvedValue([{ id: 'test-uuid' }])
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/chat', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(validData),
|
||||
})
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(mockCheckWorkflowAccessForChatCreation).toHaveBeenCalledWith('workflow-123', 'user-id')
|
||||
})
|
||||
|
||||
it('should allow chat deployment when user has workspace admin permission', async () => {
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-id' },
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/env', () => ({
|
||||
env: {
|
||||
NODE_ENV: 'development',
|
||||
NEXT_PUBLIC_APP_URL: 'http://localhost:3000',
|
||||
},
|
||||
}))
|
||||
|
||||
const validData = {
|
||||
workflowId: 'workflow-123',
|
||||
subdomain: 'test-chat',
|
||||
title: 'Test Chat',
|
||||
customizations: {
|
||||
primaryColor: '#000000',
|
||||
welcomeMessage: 'Hello',
|
||||
},
|
||||
}
|
||||
|
||||
mockLimit.mockResolvedValueOnce([]) // Subdomain is available
|
||||
mockCheckWorkflowAccessForChatCreation.mockResolvedValue({
|
||||
hasAccess: true,
|
||||
workflow: { userId: 'other-user-id', workspaceId: 'workspace-123', isDeployed: true },
|
||||
})
|
||||
mockReturning.mockResolvedValue([{ id: 'test-uuid' }])
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/chat', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(validData),
|
||||
})
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(mockCheckWorkflowAccessForChatCreation).toHaveBeenCalledWith('workflow-123', 'user-id')
|
||||
})
|
||||
|
||||
it('should reject when workflow is in workspace but user lacks admin permission', async () => {
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-id' },
|
||||
}),
|
||||
}))
|
||||
|
||||
const validData = {
|
||||
workflowId: 'workflow-123',
|
||||
subdomain: 'test-chat',
|
||||
title: 'Test Chat',
|
||||
customizations: {
|
||||
primaryColor: '#000000',
|
||||
welcomeMessage: 'Hello',
|
||||
},
|
||||
}
|
||||
|
||||
mockLimit.mockResolvedValueOnce([]) // Subdomain is available
|
||||
mockCheckWorkflowAccessForChatCreation.mockResolvedValue({
|
||||
hasAccess: false,
|
||||
})
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/chat', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(validData),
|
||||
})
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
|
||||
expect(response.status).toBe(404)
|
||||
expect(mockCreateErrorResponse).toHaveBeenCalledWith(
|
||||
'Workflow not found or access denied',
|
||||
404
|
||||
)
|
||||
expect(mockCheckWorkflowAccessForChatCreation).toHaveBeenCalledWith('workflow-123', 'user-id')
|
||||
})
|
||||
|
||||
it('should handle workspace permission check errors gracefully', async () => {
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-id' },
|
||||
}),
|
||||
}))
|
||||
|
||||
const validData = {
|
||||
workflowId: 'workflow-123',
|
||||
subdomain: 'test-chat',
|
||||
title: 'Test Chat',
|
||||
customizations: {
|
||||
primaryColor: '#000000',
|
||||
welcomeMessage: 'Hello',
|
||||
},
|
||||
}
|
||||
|
||||
mockLimit.mockResolvedValueOnce([]) // Subdomain is available
|
||||
mockCheckWorkflowAccessForChatCreation.mockRejectedValue(new Error('Permission check failed'))
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/chat', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(validData),
|
||||
})
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
|
||||
expect(response.status).toBe(500)
|
||||
expect(mockCheckWorkflowAccessForChatCreation).toHaveBeenCalledWith('workflow-123', 'user-id')
|
||||
})
|
||||
|
||||
it('should reject if workflow is not deployed', async () => {
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
@@ -246,7 +403,10 @@ describe('Chat API Route', () => {
|
||||
}
|
||||
|
||||
mockLimit.mockResolvedValueOnce([]) // Subdomain is available
|
||||
mockLimit.mockResolvedValueOnce([{ isDeployed: false }]) // Workflow exists but not deployed
|
||||
mockCheckWorkflowAccessForChatCreation.mockResolvedValue({
|
||||
hasAccess: true,
|
||||
workflow: { userId: 'user-id', workspaceId: null, isDeployed: false },
|
||||
})
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/chat', {
|
||||
method: 'POST',
|
||||
@@ -261,57 +421,5 @@ describe('Chat API Route', () => {
|
||||
400
|
||||
)
|
||||
})
|
||||
|
||||
it('should successfully create a chat deployment', async () => {
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-id' },
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/env', () => ({
|
||||
env: {
|
||||
NODE_ENV: 'development',
|
||||
NEXT_PUBLIC_APP_URL: 'http://localhost:3000',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.stubGlobal('process', {
|
||||
...process,
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: 'development',
|
||||
NEXT_PUBLIC_APP_URL: 'http://localhost:3000',
|
||||
},
|
||||
})
|
||||
|
||||
const validData = {
|
||||
workflowId: 'workflow-123',
|
||||
subdomain: 'test-chat',
|
||||
title: 'Test Chat',
|
||||
customizations: {
|
||||
primaryColor: '#000000',
|
||||
welcomeMessage: 'Hello',
|
||||
},
|
||||
}
|
||||
|
||||
mockLimit.mockResolvedValueOnce([]) // Subdomain is available
|
||||
mockLimit.mockResolvedValueOnce([{ isDeployed: true }]) // Workflow exists and is deployed
|
||||
mockReturning.mockResolvedValue([{ id: 'test-uuid' }])
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/chat', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(validData),
|
||||
})
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(mockCreateSuccessResponse).toHaveBeenCalledWith({
|
||||
id: 'test-uuid',
|
||||
chatUrl: 'http://test-chat.localhost:3000',
|
||||
message: 'Chat deployment created successfully',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { z } from 'zod'
|
||||
@@ -7,9 +7,10 @@ import { env } from '@/lib/env'
|
||||
import { isDev } from '@/lib/environment'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { encryptSecret } from '@/lib/utils'
|
||||
import { checkWorkflowAccessForChatCreation } from '@/app/api/chat/utils'
|
||||
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
||||
import { db } from '@/db'
|
||||
import { chat, workflow } from '@/db/schema'
|
||||
import { chat } from '@/db/schema'
|
||||
|
||||
const logger = createLogger('ChatAPI')
|
||||
|
||||
@@ -107,19 +108,18 @@ export async function POST(request: NextRequest) {
|
||||
return createErrorResponse('Subdomain already in use', 400)
|
||||
}
|
||||
|
||||
// Verify the workflow exists and belongs to the user
|
||||
const workflowExists = await db
|
||||
.select()
|
||||
.from(workflow)
|
||||
.where(and(eq(workflow.id, workflowId), eq(workflow.userId, session.user.id)))
|
||||
.limit(1)
|
||||
// Check if user has permission to create chat for this workflow
|
||||
const { hasAccess, workflow: workflowRecord } = await checkWorkflowAccessForChatCreation(
|
||||
workflowId,
|
||||
session.user.id
|
||||
)
|
||||
|
||||
if (workflowExists.length === 0) {
|
||||
if (!hasAccess || !workflowRecord) {
|
||||
return createErrorResponse('Workflow not found or access denied', 404)
|
||||
}
|
||||
|
||||
// Verify the workflow is deployed (required for chat deployment)
|
||||
if (!workflowExists[0].isDeployed) {
|
||||
if (!workflowRecord.isDeployed) {
|
||||
return createErrorResponse('Workflow must be deployed before creating a chat', 400)
|
||||
}
|
||||
|
||||
@@ -169,23 +169,24 @@ export async function POST(request: NextRequest) {
|
||||
})
|
||||
|
||||
// Return successful response with chat URL
|
||||
// Check if we're in development or production
|
||||
// Generate chat URL based on the configured base URL
|
||||
const baseUrl = env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
||||
|
||||
let chatUrl: string
|
||||
if (isDev) {
|
||||
try {
|
||||
const url = new URL(baseUrl)
|
||||
chatUrl = `${url.protocol}//${subdomain}.${url.host}`
|
||||
} catch (error) {
|
||||
logger.warn('Failed to parse baseUrl, falling back to localhost:', {
|
||||
baseUrl,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
})
|
||||
try {
|
||||
const url = new URL(baseUrl)
|
||||
chatUrl = `${url.protocol}//${subdomain}.${url.host}`
|
||||
} catch (error) {
|
||||
logger.warn('Failed to parse baseUrl, falling back to defaults:', {
|
||||
baseUrl,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
})
|
||||
// Fallback based on environment
|
||||
if (isDev) {
|
||||
chatUrl = `http://${subdomain}.localhost:3000`
|
||||
} else {
|
||||
chatUrl = `https://${subdomain}.simstudio.ai`
|
||||
}
|
||||
} else {
|
||||
chatUrl = `https://${subdomain}.simstudio.ai`
|
||||
}
|
||||
|
||||
logger.info(`Chat "${title}" deployed successfully at ${chatUrl}`)
|
||||
|
||||
@@ -7,7 +7,6 @@ import type { NextResponse } from 'next/server'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { env } from '@/lib/env'
|
||||
|
||||
// Mock all the problematic imports that cause timeouts
|
||||
vi.mock('@/db', () => ({
|
||||
db: {
|
||||
select: vi.fn(),
|
||||
|
||||
@@ -5,7 +5,9 @@ import { isDev } from '@/lib/environment'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { EnhancedLoggingSession } from '@/lib/logs/enhanced-logging-session'
|
||||
import { buildTraceSpans } from '@/lib/logs/trace-spans'
|
||||
import { hasAdminPermission } from '@/lib/permissions/utils'
|
||||
import { processStreamingBlockLogs } from '@/lib/tokenization'
|
||||
import { getEmailDomain } from '@/lib/urls/utils'
|
||||
import { decryptSecret } from '@/lib/utils'
|
||||
import { db } from '@/db'
|
||||
import { chat, environment as envTable, userStats, workflow } from '@/db/schema'
|
||||
@@ -21,6 +23,80 @@ declare global {
|
||||
|
||||
const logger = createLogger('ChatAuthUtils')
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export async function checkWorkflowAccessForChatCreation(
|
||||
workflowId: string,
|
||||
userId: string
|
||||
): Promise<{ hasAccess: boolean; workflow?: any }> {
|
||||
// Get workflow data
|
||||
const workflowData = await db.select().from(workflow).where(eq(workflow.id, workflowId)).limit(1)
|
||||
|
||||
if (workflowData.length === 0) {
|
||||
return { hasAccess: false }
|
||||
}
|
||||
|
||||
const workflowRecord = workflowData[0]
|
||||
|
||||
// Case 1: User owns the workflow directly
|
||||
if (workflowRecord.userId === userId) {
|
||||
return { hasAccess: true, workflow: workflowRecord }
|
||||
}
|
||||
|
||||
// Case 2: Workflow belongs to a workspace and user has admin permission
|
||||
if (workflowRecord.workspaceId) {
|
||||
const hasAdmin = await hasAdminPermission(userId, workflowRecord.workspaceId)
|
||||
if (hasAdmin) {
|
||||
return { hasAccess: true, workflow: workflowRecord }
|
||||
}
|
||||
}
|
||||
|
||||
return { hasAccess: false }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has access to view/edit/delete a specific chat
|
||||
* Either the user owns the chat directly OR has admin permission for the workflow's workspace
|
||||
*/
|
||||
export async function checkChatAccess(
|
||||
chatId: string,
|
||||
userId: string
|
||||
): Promise<{ hasAccess: boolean; chat?: any }> {
|
||||
// Get chat with workflow information
|
||||
const chatData = await db
|
||||
.select({
|
||||
chat: chat,
|
||||
workflowWorkspaceId: workflow.workspaceId,
|
||||
})
|
||||
.from(chat)
|
||||
.innerJoin(workflow, eq(chat.workflowId, workflow.id))
|
||||
.where(eq(chat.id, chatId))
|
||||
.limit(1)
|
||||
|
||||
if (chatData.length === 0) {
|
||||
return { hasAccess: false }
|
||||
}
|
||||
|
||||
const { chat: chatRecord, workflowWorkspaceId } = chatData[0]
|
||||
|
||||
// Case 1: User owns the chat directly
|
||||
if (chatRecord.userId === userId) {
|
||||
return { hasAccess: true, chat: chatRecord }
|
||||
}
|
||||
|
||||
// Case 2: Chat's workflow belongs to a workspace and user has admin permission
|
||||
if (workflowWorkspaceId) {
|
||||
const hasAdmin = await hasAdminPermission(userId, workflowWorkspaceId)
|
||||
if (hasAdmin) {
|
||||
return { hasAccess: true, chat: chatRecord }
|
||||
}
|
||||
}
|
||||
|
||||
return { hasAccess: false }
|
||||
}
|
||||
|
||||
export const encryptAuthToken = (subdomainId: string, type: string): string => {
|
||||
return Buffer.from(`${subdomainId}:${type}:${Date.now()}`).toString('base64')
|
||||
}
|
||||
@@ -66,7 +142,7 @@ export const setChatAuthCookie = (
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
// Using subdomain for the domain in production
|
||||
domain: isDev ? undefined : '.simstudio.ai',
|
||||
domain: isDev ? undefined : `.${getEmailDomain()}`,
|
||||
maxAge: 60 * 60 * 24, // 24 hours
|
||||
})
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { isDev } from '@/lib/environment'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { getBaseDomain } from '@/lib/urls/utils'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -54,11 +53,10 @@ interface ChatDeployProps {
|
||||
type AuthType = 'public' | 'password' | 'email'
|
||||
|
||||
const getDomainSuffix = (() => {
|
||||
const suffix = isDev ? `.${getBaseDomain()}` : '.simstudio.ai'
|
||||
const suffix = `.${getBaseDomain()}`
|
||||
return () => suffix
|
||||
})()
|
||||
|
||||
// Define Zod schema for API request validation
|
||||
const chatSchema = z.object({
|
||||
workflowId: z.string().min(1, 'Workflow ID is required'),
|
||||
subdomain: z
|
||||
@@ -124,10 +122,6 @@ export function ChatDeploy({
|
||||
selectedOutputIds: string[]
|
||||
} | null>(null)
|
||||
|
||||
// State to track if any changes have been made
|
||||
const [hasChanges, setHasChanges] = useState(false)
|
||||
|
||||
// Confirmation dialogs
|
||||
const [showEditConfirmation, setShowEditConfirmation] = useState(false)
|
||||
const [internalShowDeleteConfirmation, setInternalShowDeleteConfirmation] = useState(false)
|
||||
|
||||
@@ -184,53 +178,6 @@ export function ChatDeploy({
|
||||
}
|
||||
}, [workflowId])
|
||||
|
||||
// Check for changes when form values update
|
||||
useEffect(() => {
|
||||
if (originalValues && existingChat) {
|
||||
const currentAuthTypeChanged = authType !== originalValues.authType
|
||||
const subdomainChanged = subdomain !== originalValues.subdomain
|
||||
const titleChanged = title !== originalValues.title
|
||||
const descriptionChanged = description !== originalValues.description
|
||||
const outputBlockChanged = selectedOutputBlocks.some(
|
||||
(blockId) => !originalValues.selectedOutputIds.includes(blockId)
|
||||
)
|
||||
const welcomeMessageChanged =
|
||||
welcomeMessage !==
|
||||
(existingChat.customizations?.welcomeMessage || 'Hi there! How can I help you today?')
|
||||
|
||||
// Check if emails have changed
|
||||
const emailsChanged =
|
||||
emails.length !== originalValues.emails.length ||
|
||||
emails.some((email) => !originalValues.emails.includes(email))
|
||||
|
||||
// Check if password has changed - any value in password field means change
|
||||
const passwordChanged = password.length > 0
|
||||
|
||||
// Determine if any changes have been made
|
||||
const changed =
|
||||
subdomainChanged ||
|
||||
titleChanged ||
|
||||
descriptionChanged ||
|
||||
currentAuthTypeChanged ||
|
||||
emailsChanged ||
|
||||
passwordChanged ||
|
||||
outputBlockChanged ||
|
||||
welcomeMessageChanged
|
||||
|
||||
setHasChanges(changed)
|
||||
}
|
||||
}, [
|
||||
subdomain,
|
||||
title,
|
||||
description,
|
||||
authType,
|
||||
emails,
|
||||
password,
|
||||
selectedOutputBlocks,
|
||||
welcomeMessage,
|
||||
originalValues,
|
||||
])
|
||||
|
||||
// Fetch existing chat data for this workflow
|
||||
const fetchExistingChat = async () => {
|
||||
try {
|
||||
@@ -310,7 +257,6 @@ export function ChatDeploy({
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
setDataFetched(true)
|
||||
setHasChanges(false) // Reset changes detection after loading
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,6 +436,8 @@ export function ChatDeploy({
|
||||
(!originalValues || subdomain !== originalValues.subdomain)
|
||||
) {
|
||||
setIsCheckingSubdomain(true)
|
||||
setSubdomainError('')
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/chat/subdomains/validate?subdomain=${encodeURIComponent(subdomain)}`
|
||||
@@ -497,11 +445,15 @@ export function ChatDeploy({
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok || !data.available) {
|
||||
setSubdomainError('This subdomain is already in use')
|
||||
const errorMsg = data.error || 'This subdomain is already in use'
|
||||
setSubdomainError(errorMsg)
|
||||
setChatSubmitting(false)
|
||||
setIsCheckingSubdomain(false)
|
||||
logger.warn('Subdomain validation failed:', errorMsg)
|
||||
return
|
||||
}
|
||||
|
||||
setSubdomainError('')
|
||||
} catch (error) {
|
||||
logger.error('Error checking subdomain availability:', error)
|
||||
setSubdomainError('Error checking subdomain availability')
|
||||
@@ -512,15 +464,16 @@ export function ChatDeploy({
|
||||
setIsCheckingSubdomain(false)
|
||||
}
|
||||
|
||||
// Verify output selection if it's set
|
||||
if (selectedOutputBlocks.length === 0) {
|
||||
logger.error('No output blocks selected')
|
||||
setErrorMessage('Please select at least one output block')
|
||||
if (subdomainError) {
|
||||
logger.warn('Blocking submission due to subdomain error:', subdomainError)
|
||||
setChatSubmitting(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (subdomainError) {
|
||||
// Verify output selection if it's set
|
||||
if (selectedOutputBlocks.length === 0) {
|
||||
logger.error('No output blocks selected')
|
||||
setErrorMessage('Please select at least one output block')
|
||||
setChatSubmitting(false)
|
||||
return
|
||||
}
|
||||
@@ -722,6 +675,11 @@ export function ChatDeploy({
|
||||
const result = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
if (result.error === 'Subdomain already in use') {
|
||||
setSubdomainError(result.error)
|
||||
setChatSubmitting(false)
|
||||
return
|
||||
}
|
||||
throw new Error(result.error || `Failed to ${existingChat ? 'update' : 'deploy'} chat`)
|
||||
}
|
||||
|
||||
@@ -743,7 +701,14 @@ export function ChatDeploy({
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error(`Failed to ${existingChat ? 'update' : 'deploy'} chat:`, error)
|
||||
setErrorMessage(error.message || 'An unexpected error occurred')
|
||||
|
||||
const errorMessage = error.message || 'An unexpected error occurred'
|
||||
if (errorMessage.includes('Subdomain already in use') || errorMessage.includes('subdomain')) {
|
||||
setSubdomainError(errorMessage)
|
||||
} else {
|
||||
setErrorMessage(errorMessage)
|
||||
}
|
||||
|
||||
logger.error(`Failed to deploy chat: ${error.message}`, workflowId)
|
||||
} finally {
|
||||
setChatSubmitting(false)
|
||||
@@ -751,26 +716,6 @@ export function ChatDeploy({
|
||||
}
|
||||
}
|
||||
|
||||
// Determine button label based on state
|
||||
const _getSubmitButtonLabel = () => {
|
||||
return existingChat ? 'Update Chat' : 'Deploy Chat'
|
||||
}
|
||||
|
||||
// Check if form submission is possible
|
||||
const _isFormSubmitDisabled = () => {
|
||||
return (
|
||||
chatSubmitting ||
|
||||
isDeleting ||
|
||||
!subdomain ||
|
||||
!title ||
|
||||
!!subdomainError ||
|
||||
isCheckingSubdomain ||
|
||||
(authType === 'password' && !password && !existingChat) ||
|
||||
(authType === 'email' && emails.length === 0) ||
|
||||
(existingChat && !hasChanges)
|
||||
)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className='space-y-4 py-3'>
|
||||
@@ -827,12 +772,13 @@ export function ChatDeploy({
|
||||
const port = url.port || (baseDomain.includes(':') ? baseDomain.split(':')[1] : '3000')
|
||||
domainSuffix = `.${baseHost}:${port}`
|
||||
} else {
|
||||
domainSuffix = '.simstudio.ai'
|
||||
domainSuffix = `.${getBaseDomain()}`
|
||||
}
|
||||
|
||||
const baseDomainForSplit = getBaseDomain()
|
||||
const subdomainPart = isDevelopmentUrl
|
||||
? hostname.split('.')[0]
|
||||
: hostname.split('.simstudio.ai')[0]
|
||||
: hostname.split(`.${baseDomainForSplit}`)[0]
|
||||
|
||||
// Success view - simplified with no buttons
|
||||
return (
|
||||
@@ -996,11 +942,6 @@ export function ChatDeploy({
|
||||
onOutputSelect={(values) => {
|
||||
logger.info(`Output block selection changed to: ${values}`)
|
||||
setSelectedOutputBlocks(values)
|
||||
|
||||
// Mark as changed to enable update button
|
||||
if (existingChat) {
|
||||
setHasChanges(true)
|
||||
}
|
||||
}}
|
||||
placeholder='Select which block outputs to use'
|
||||
disabled={isDeploying}
|
||||
@@ -1306,7 +1247,10 @@ export function ChatDeploy({
|
||||
<AlertDialogTitle>Delete Chat?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will permanently delete your chat deployment at{' '}
|
||||
<span className='font-mono text-destructive'>{subdomain}.simstudio.ai</span>.
|
||||
<span className='font-mono text-destructive'>
|
||||
{subdomain}.{getBaseDomain()}
|
||||
</span>
|
||||
.
|
||||
<p className='mt-2'>
|
||||
All users will lose access immediately, and this action cannot be undone.
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user