mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-27 15:58:11 -05:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b412c578d | ||
|
|
dddd0c8277 | ||
|
|
be7f3db059 | ||
|
|
416c08267a |
@@ -10,6 +10,7 @@ describe('OAuth Token API Routes', () => {
|
||||
const mockGetUserId = vi.fn()
|
||||
const mockGetCredential = vi.fn()
|
||||
const mockRefreshTokenIfNeeded = vi.fn()
|
||||
const mockGetOAuthToken = vi.fn()
|
||||
const mockAuthorizeCredentialUse = vi.fn()
|
||||
const mockCheckHybridAuth = vi.fn()
|
||||
|
||||
@@ -29,6 +30,7 @@ describe('OAuth Token API Routes', () => {
|
||||
getUserId: mockGetUserId,
|
||||
getCredential: mockGetCredential,
|
||||
refreshTokenIfNeeded: mockRefreshTokenIfNeeded,
|
||||
getOAuthToken: mockGetOAuthToken,
|
||||
}))
|
||||
|
||||
vi.doMock('@sim/logger', () => ({
|
||||
@@ -230,6 +232,140 @@ describe('OAuth Token API Routes', () => {
|
||||
expect(response.status).toBe(401)
|
||||
expect(data).toHaveProperty('error', 'Failed to refresh access token')
|
||||
})
|
||||
|
||||
describe('credentialAccountUserId + providerId path', () => {
|
||||
it('should reject unauthenticated requests', async () => {
|
||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
||||
success: false,
|
||||
error: 'Authentication required',
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
credentialAccountUserId: 'target-user-id',
|
||||
providerId: 'google',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/oauth/token/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
expect(data).toHaveProperty('error', 'User not authenticated')
|
||||
expect(mockGetOAuthToken).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should reject API key authentication', async () => {
|
||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
||||
success: true,
|
||||
authType: 'api_key',
|
||||
userId: 'test-user-id',
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
credentialAccountUserId: 'test-user-id',
|
||||
providerId: 'google',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/oauth/token/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
expect(data).toHaveProperty('error', 'User not authenticated')
|
||||
expect(mockGetOAuthToken).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should reject internal JWT authentication', async () => {
|
||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
||||
success: true,
|
||||
authType: 'internal_jwt',
|
||||
userId: 'test-user-id',
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
credentialAccountUserId: 'test-user-id',
|
||||
providerId: 'google',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/oauth/token/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
expect(data).toHaveProperty('error', 'User not authenticated')
|
||||
expect(mockGetOAuthToken).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should reject requests for other users credentials', async () => {
|
||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
||||
success: true,
|
||||
authType: 'session',
|
||||
userId: 'attacker-user-id',
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
credentialAccountUserId: 'victim-user-id',
|
||||
providerId: 'google',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/oauth/token/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(403)
|
||||
expect(data).toHaveProperty('error', 'Unauthorized')
|
||||
expect(mockGetOAuthToken).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should allow session-authenticated users to access their own credentials', async () => {
|
||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
||||
success: true,
|
||||
authType: 'session',
|
||||
userId: 'test-user-id',
|
||||
})
|
||||
mockGetOAuthToken.mockResolvedValueOnce('valid-access-token')
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
credentialAccountUserId: 'test-user-id',
|
||||
providerId: 'google',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/oauth/token/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data).toHaveProperty('accessToken', 'valid-access-token')
|
||||
expect(mockGetOAuthToken).toHaveBeenCalledWith('test-user-id', 'google')
|
||||
})
|
||||
|
||||
it('should return 404 when credential not found for user', async () => {
|
||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
||||
success: true,
|
||||
authType: 'session',
|
||||
userId: 'test-user-id',
|
||||
})
|
||||
mockGetOAuthToken.mockResolvedValueOnce(null)
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
credentialAccountUserId: 'test-user-id',
|
||||
providerId: 'nonexistent-provider',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/auth/oauth/token/route')
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(404)
|
||||
expect(data.error).toContain('No credential found')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@@ -71,6 +71,22 @@ export async function POST(request: NextRequest) {
|
||||
providerId,
|
||||
})
|
||||
|
||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
if (!auth.success || auth.authType !== 'session' || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized request for credentialAccountUserId path`, {
|
||||
success: auth.success,
|
||||
authType: auth.authType,
|
||||
})
|
||||
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
|
||||
}
|
||||
|
||||
if (auth.userId !== credentialAccountUserId) {
|
||||
logger.warn(
|
||||
`[${requestId}] User ${auth.userId} attempted to access credentials for ${credentialAccountUserId}`
|
||||
)
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
try {
|
||||
const accessToken = await getOAuthToken(credentialAccountUserId, providerId)
|
||||
if (!accessToken) {
|
||||
|
||||
@@ -26,8 +26,9 @@ vi.mock('@/serializer', () => ({
|
||||
Serializer: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/workflows/server-utils', () => ({
|
||||
mergeSubblockState: vi.fn().mockReturnValue({}),
|
||||
vi.mock('@/lib/workflows/subblocks', () => ({
|
||||
mergeSubblockStateWithValues: vi.fn().mockReturnValue({}),
|
||||
mergeSubBlockValues: vi.fn().mockReturnValue({}),
|
||||
}))
|
||||
|
||||
const mockDecryptSecret = vi.fn()
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('AsanaAddCommentAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { accessToken, taskGid, text } = await request.json()
|
||||
|
||||
if (!accessToken) {
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('AsanaCreateTaskAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { accessToken, workspace, name, notes, assignee, due_on } = await request.json()
|
||||
|
||||
if (!accessToken) {
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('AsanaGetProjectsAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { accessToken, workspace } = await request.json()
|
||||
|
||||
if (!accessToken) {
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('AsanaGetTaskAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { accessToken, taskGid, workspace, project, limit } = await request.json()
|
||||
|
||||
if (!accessToken) {
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('AsanaSearchTasksAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { accessToken, workspace, text, assignee, projects, completed } = await request.json()
|
||||
|
||||
if (!accessToken) {
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('AsanaUpdateTaskAPI')
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { accessToken, taskGid, name, notes, assignee, completed, due_on } = await request.json()
|
||||
|
||||
if (!accessToken) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||
|
||||
@@ -8,8 +9,13 @@ const logger = createLogger('ConfluenceAttachmentAPI')
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// Delete an attachment
|
||||
export async function DELETE(request: Request) {
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { domain, accessToken, cloudId: providedCloudId, attachmentId } = await request.json()
|
||||
|
||||
if (!domain) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||
|
||||
@@ -8,8 +9,13 @@ const logger = createLogger('ConfluenceAttachmentsAPI')
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// List attachments on a page
|
||||
export async function GET(request: Request) {
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const domain = searchParams.get('domain')
|
||||
const accessToken = searchParams.get('accessToken')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||
|
||||
@@ -46,8 +47,13 @@ const deleteCommentSchema = z
|
||||
)
|
||||
|
||||
// Update a comment
|
||||
export async function PUT(request: Request) {
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
|
||||
const validation = putCommentSchema.safeParse(body)
|
||||
@@ -128,8 +134,13 @@ export async function PUT(request: Request) {
|
||||
}
|
||||
|
||||
// Delete a comment
|
||||
export async function DELETE(request: Request) {
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
|
||||
const validation = deleteCommentSchema.safeParse(body)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||
|
||||
@@ -8,8 +9,13 @@ const logger = createLogger('ConfluenceCommentsAPI')
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// Create a comment
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { domain, accessToken, cloudId: providedCloudId, pageId, comment } = await request.json()
|
||||
|
||||
if (!domain) {
|
||||
@@ -86,8 +92,13 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
// List comments on a page
|
||||
export async function GET(request: Request) {
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const domain = searchParams.get('domain')
|
||||
const accessToken = searchParams.get('accessToken')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||
|
||||
@@ -7,8 +8,13 @@ const logger = createLogger('ConfluenceCreatePageAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||
|
||||
@@ -8,8 +9,13 @@ const logger = createLogger('ConfluenceLabelsAPI')
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// Add a label to a page
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
@@ -87,8 +93,13 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
// List labels on a page
|
||||
export async function GET(request: Request) {
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const domain = searchParams.get('domain')
|
||||
const accessToken = searchParams.get('accessToken')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||
|
||||
@@ -73,8 +74,13 @@ const deletePageSchema = z
|
||||
}
|
||||
)
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
|
||||
const validation = postPageSchema.safeParse(body)
|
||||
@@ -144,8 +150,13 @@ export async function POST(request: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
|
||||
const validation = putPageSchema.safeParse(body)
|
||||
@@ -248,8 +259,13 @@ export async function PUT(request: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: Request) {
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
|
||||
const validation = deletePageSchema.safeParse(body)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||
|
||||
@@ -8,8 +9,13 @@ const logger = createLogger('ConfluencePagesAPI')
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// List pages or search pages
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||
|
||||
@@ -7,8 +8,13 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('Confluence Search')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||
|
||||
@@ -8,8 +9,13 @@ const logger = createLogger('ConfluenceSpaceAPI')
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// Get a specific space
|
||||
export async function GET(request: Request) {
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const domain = searchParams.get('domain')
|
||||
const accessToken = searchParams.get('accessToken')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||
|
||||
@@ -8,8 +9,13 @@ const logger = createLogger('ConfluenceSpacesAPI')
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// List all spaces
|
||||
export async function GET(request: Request) {
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const domain = searchParams.get('domain')
|
||||
const accessToken = searchParams.get('accessToken')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
@@ -11,6 +12,11 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { domain, accessToken, cloudId: providedCloudId, pageId, file, fileName, comment } = body
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateNumericId } from '@/lib/core/security/input-validation'
|
||||
|
||||
interface DiscordChannel {
|
||||
@@ -13,7 +14,12 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('DiscordChannelsAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const { botToken, serverId, channelId } = await request.json()
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateNumericId } from '@/lib/core/security/input-validation'
|
||||
|
||||
interface DiscordServer {
|
||||
@@ -12,7 +13,12 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('DiscordServersAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const { botToken, serverId } = await request.json()
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
@@ -15,6 +16,11 @@ export async function GET(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
logger.info(`[${requestId}] Google Drive file request received`)
|
||||
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const credentialId = searchParams.get('credentialId')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
@@ -73,14 +73,12 @@ export async function GET(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
logger.info(`[${requestId}] Google Drive files request received`)
|
||||
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const session = await getSession()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
logger.warn(`[${requestId}] Unauthenticated request rejected`)
|
||||
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const credentialId = searchParams.get('credentialId')
|
||||
const mimeType = searchParams.get('mimeType')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createDynamoDBClient, deleteItem } from '@/app/api/tools/dynamodb/utils'
|
||||
|
||||
const DeleteSchema = z.object({
|
||||
@@ -13,8 +14,13 @@ const DeleteSchema = z.object({
|
||||
conditionExpression: z.string().optional(),
|
||||
})
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = DeleteSchema.parse(body)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createDynamoDBClient, getItem } from '@/app/api/tools/dynamodb/utils'
|
||||
|
||||
const GetSchema = z.object({
|
||||
@@ -19,8 +20,13 @@ const GetSchema = z.object({
|
||||
}),
|
||||
})
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = GetSchema.parse(body)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createRawDynamoDBClient, describeTable, listTables } from '@/app/api/tools/dynamodb/utils'
|
||||
|
||||
const logger = createLogger('DynamoDBIntrospectAPI')
|
||||
@@ -17,6 +18,11 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const params = IntrospectSchema.parse(body)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createDynamoDBClient, putItem } from '@/app/api/tools/dynamodb/utils'
|
||||
|
||||
const PutSchema = z.object({
|
||||
@@ -12,8 +13,13 @@ const PutSchema = z.object({
|
||||
}),
|
||||
})
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = PutSchema.parse(body)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createDynamoDBClient, queryItems } from '@/app/api/tools/dynamodb/utils'
|
||||
|
||||
const QuerySchema = z.object({
|
||||
@@ -15,8 +16,13 @@ const QuerySchema = z.object({
|
||||
limit: z.number().positive().optional(),
|
||||
})
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = QuerySchema.parse(body)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createDynamoDBClient, scanItems } from '@/app/api/tools/dynamodb/utils'
|
||||
|
||||
const ScanSchema = z.object({
|
||||
@@ -14,8 +15,13 @@ const ScanSchema = z.object({
|
||||
limit: z.number().positive().optional(),
|
||||
})
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = ScanSchema.parse(body)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createDynamoDBClient, updateItem } from '@/app/api/tools/dynamodb/utils'
|
||||
|
||||
const UpdateSchema = z.object({
|
||||
@@ -16,8 +17,13 @@ const UpdateSchema = z.object({
|
||||
conditionExpression: z.string().optional(),
|
||||
})
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = UpdateSchema.parse(body)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
@@ -29,6 +30,11 @@ export async function GET(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
logger.info(`[${requestId}] Google Sheets sheets request received`)
|
||||
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const credentialId = searchParams.get('credentialId')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId } from '@/tools/jira/utils'
|
||||
|
||||
@@ -7,8 +8,13 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JiraIssueAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { domain, accessToken, issueId, cloudId: providedCloudId } = await request.json()
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId } from '@/tools/jira/utils'
|
||||
|
||||
@@ -26,8 +27,13 @@ const validateRequiredParams = (domain: string | null, accessToken: string | nul
|
||||
return null
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { domain, accessToken, issueKeys = [], cloudId: providedCloudId } = await request.json()
|
||||
|
||||
const validationError = validateRequiredParams(domain || null, accessToken || null)
|
||||
@@ -101,8 +107,13 @@ export async function POST(request: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const url = new URL(request.url)
|
||||
const domain = url.searchParams.get('domain')?.trim()
|
||||
const accessToken = url.searchParams.get('accessToken')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId } from '@/tools/jira/utils'
|
||||
|
||||
@@ -7,8 +8,13 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JiraProjectsAPI')
|
||||
|
||||
export async function GET(request: Request) {
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const url = new URL(request.url)
|
||||
const domain = url.searchParams.get('domain')?.trim()
|
||||
const accessToken = url.searchParams.get('accessToken')
|
||||
@@ -98,8 +104,13 @@ export async function GET(request: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { domain, accessToken, projectId, cloudId: providedCloudId } = await request.json()
|
||||
|
||||
if (!domain) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId } from '@/tools/jira/utils'
|
||||
|
||||
@@ -21,8 +22,13 @@ const jiraUpdateSchema = z.object({
|
||||
cloudId: z.string().optional(),
|
||||
})
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validation = jiraUpdateSchema.safeParse(body)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId } from '@/tools/jira/utils'
|
||||
|
||||
@@ -7,8 +8,13 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JiraWriteAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
validateAlphanumericId,
|
||||
validateEnum,
|
||||
@@ -15,7 +16,12 @@ const logger = createLogger('JsmApprovalsAPI')
|
||||
const VALID_ACTIONS = ['get', 'answer'] as const
|
||||
const VALID_DECISIONS = ['approve', 'decline'] as const
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
@@ -7,7 +8,12 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmCommentAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const {
|
||||
domain,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
@@ -7,7 +8,12 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmCommentsAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
@@ -7,7 +8,12 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmCustomersAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
validateAlphanumericId,
|
||||
validateEnum,
|
||||
@@ -13,7 +14,12 @@ const logger = createLogger('JsmOrganizationAPI')
|
||||
|
||||
const VALID_ACTIONS = ['create', 'add_to_service_desk'] as const
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
@@ -7,7 +8,12 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmOrganizationsAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { domain, accessToken, cloudId: cloudIdParam, serviceDeskId, start, limit } = body
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
validateEnum,
|
||||
validateJiraCloudId,
|
||||
@@ -13,7 +14,12 @@ const logger = createLogger('JsmParticipantsAPI')
|
||||
|
||||
const VALID_ACTIONS = ['get', 'add'] as const
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
@@ -7,7 +8,12 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmQueuesAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
validateAlphanumericId,
|
||||
validateJiraCloudId,
|
||||
@@ -11,7 +12,12 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmRequestAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
@@ -7,7 +8,12 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmRequestsAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
@@ -7,7 +8,12 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmRequestTypesAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { domain, accessToken, cloudId: cloudIdParam, serviceDeskId, start, limit } = body
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
@@ -7,7 +8,12 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmServiceDesksAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { domain, accessToken, cloudId: cloudIdParam, start, limit } = body
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
@@ -7,7 +8,12 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmSlaAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, start, limit } = body
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
validateAlphanumericId,
|
||||
validateJiraCloudId,
|
||||
@@ -11,7 +12,12 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmTransitionAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const {
|
||||
domain,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
@@ -7,7 +8,12 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmTransitionsAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey } = body
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createMongoDBConnection, sanitizeCollectionName, validateFilter } from '../utils'
|
||||
|
||||
const logger = createLogger('MongoDBDeleteAPI')
|
||||
@@ -40,6 +41,12 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
let client = null
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized MongoDB delete attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = DeleteSchema.parse(body)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createMongoDBConnection, sanitizeCollectionName, validatePipeline } from '../utils'
|
||||
|
||||
const logger = createLogger('MongoDBExecuteAPI')
|
||||
@@ -32,6 +33,12 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
let client = null
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized MongoDB execute attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = ExecuteSchema.parse(body)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createMongoDBConnection, sanitizeCollectionName } from '../utils'
|
||||
|
||||
const logger = createLogger('MongoDBInsertAPI')
|
||||
@@ -37,6 +38,12 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
let client = null
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized MongoDB insert attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = InsertSchema.parse(body)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createMongoDBConnection, executeIntrospect } from '../utils'
|
||||
|
||||
const logger = createLogger('MongoDBIntrospectAPI')
|
||||
@@ -20,6 +21,12 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
let client = null
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized MongoDB introspect attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = IntrospectSchema.parse(body)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createMongoDBConnection, sanitizeCollectionName, validateFilter } from '../utils'
|
||||
|
||||
const logger = createLogger('MongoDBQueryAPI')
|
||||
@@ -49,6 +50,12 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
let client = null
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized MongoDB query attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = QuerySchema.parse(body)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createMongoDBConnection, sanitizeCollectionName, validateFilter } from '../utils'
|
||||
|
||||
const logger = createLogger('MongoDBUpdateAPI')
|
||||
@@ -59,6 +60,12 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
let client = null
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized MongoDB update attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = UpdateSchema.parse(body)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
convertNeo4jTypesToJSON,
|
||||
createNeo4jDriver,
|
||||
@@ -26,6 +27,12 @@ export async function POST(request: NextRequest) {
|
||||
let driver = null
|
||||
let session = null
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Neo4j create attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = CreateSchema.parse(body)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createNeo4jDriver, validateCypherQuery } from '@/app/api/tools/neo4j/utils'
|
||||
|
||||
const logger = createLogger('Neo4jDeleteAPI')
|
||||
@@ -23,6 +24,12 @@ export async function POST(request: NextRequest) {
|
||||
let driver = null
|
||||
let session = null
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Neo4j delete attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = DeleteSchema.parse(body)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
convertNeo4jTypesToJSON,
|
||||
createNeo4jDriver,
|
||||
@@ -26,6 +27,12 @@ export async function POST(request: NextRequest) {
|
||||
let driver = null
|
||||
let session = null
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Neo4j execute attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = ExecuteSchema.parse(body)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createNeo4jDriver } from '@/app/api/tools/neo4j/utils'
|
||||
import type { Neo4jNodeSchema, Neo4jRelationshipSchema } from '@/tools/neo4j/types'
|
||||
|
||||
@@ -21,6 +22,12 @@ export async function POST(request: NextRequest) {
|
||||
let driver = null
|
||||
let session = null
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Neo4j introspect attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = IntrospectSchema.parse(body)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
convertNeo4jTypesToJSON,
|
||||
createNeo4jDriver,
|
||||
@@ -26,6 +27,12 @@ export async function POST(request: NextRequest) {
|
||||
let driver = null
|
||||
let session = null
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Neo4j merge attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = MergeSchema.parse(body)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
convertNeo4jTypesToJSON,
|
||||
createNeo4jDriver,
|
||||
@@ -26,6 +27,12 @@ export async function POST(request: NextRequest) {
|
||||
let driver = null
|
||||
let session = null
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Neo4j query attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = QuerySchema.parse(body)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
convertNeo4jTypesToJSON,
|
||||
createNeo4jDriver,
|
||||
@@ -26,6 +27,12 @@ export async function POST(request: NextRequest) {
|
||||
let driver = null
|
||||
let session = null
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Neo4j update attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = UpdateSchema.parse(body)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createRdsClient, executeDelete } from '@/app/api/tools/rds/utils'
|
||||
|
||||
const logger = createLogger('RDSDeleteAPI')
|
||||
@@ -22,6 +23,11 @@ const DeleteSchema = z.object({
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = DeleteSchema.parse(body)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createRdsClient, executeStatement } from '@/app/api/tools/rds/utils'
|
||||
|
||||
const logger = createLogger('RDSExecuteAPI')
|
||||
@@ -19,6 +20,11 @@ const ExecuteSchema = z.object({
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = ExecuteSchema.parse(body)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createRdsClient, executeInsert } from '@/app/api/tools/rds/utils'
|
||||
|
||||
const logger = createLogger('RDSInsertAPI')
|
||||
@@ -22,6 +23,11 @@ const InsertSchema = z.object({
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = InsertSchema.parse(body)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createRdsClient, executeIntrospect, type RdsEngine } from '@/app/api/tools/rds/utils'
|
||||
|
||||
const logger = createLogger('RDSIntrospectAPI')
|
||||
@@ -20,6 +21,11 @@ const IntrospectSchema = z.object({
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = IntrospectSchema.parse(body)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createRdsClient, executeStatement, validateQuery } from '@/app/api/tools/rds/utils'
|
||||
|
||||
const logger = createLogger('RDSQueryAPI')
|
||||
@@ -19,6 +20,11 @@ const QuerySchema = z.object({
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = QuerySchema.parse(body)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createRdsClient, executeUpdate } from '@/app/api/tools/rds/utils'
|
||||
|
||||
const logger = createLogger('RDSUpdateAPI')
|
||||
@@ -25,6 +26,11 @@ const UpdateSchema = z.object({
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = UpdateSchema.parse(body)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createSqsClient, sendMessage } from '../utils'
|
||||
|
||||
const logger = createLogger('SQSSendMessageAPI')
|
||||
@@ -21,6 +22,11 @@ const SendMessageSchema = z.object({
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = SendMessageSchema.parse(body)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { isSensitiveKey, REDACTED_MARKER } from '@/lib/core/security/redaction'
|
||||
import { ensureZodObject, normalizeUrl } from '@/app/api/tools/stagehand/utils'
|
||||
@@ -91,6 +92,11 @@ function substituteVariables(text: string, variables: Record<string, string> | u
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
let stagehand: StagehandType | null = null
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { ensureZodObject, normalizeUrl } from '@/app/api/tools/stagehand/utils'
|
||||
|
||||
@@ -22,6 +23,11 @@ const requestSchema = z.object({
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
let stagehand: StagehandType | null = null
|
||||
|
||||
try {
|
||||
|
||||
@@ -203,7 +203,9 @@ export function ApiInfoModal({ open, onOpenChange, workflowId }: ApiInfoModalPro
|
||||
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
|
||||
{field.name}
|
||||
</span>
|
||||
<Badge size='sm'>{field.type || 'string'}</Badge>
|
||||
<Badge variant='type' size='sm'>
|
||||
{field.type || 'string'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className='border-[var(--border-1)] border-t px-[10px] pt-[6px] pb-[10px]'>
|
||||
|
||||
@@ -511,7 +511,9 @@ export function McpDeploy({
|
||||
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
|
||||
{field.name}
|
||||
</span>
|
||||
<Badge size='sm'>{field.type}</Badge>
|
||||
<Badge variant='type' size='sm'>
|
||||
{field.type}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className='border-[var(--border-1)] border-t px-[10px] pt-[6px] pb-[10px]'>
|
||||
|
||||
@@ -245,7 +245,9 @@ export function DocumentTagEntry({
|
||||
{tag.collapsed ? tag.tagName || `Tag ${index + 1}` : `Tag ${index + 1}`}
|
||||
</span>
|
||||
{tag.collapsed && tag.tagName && (
|
||||
<Badge size='sm'>{FIELD_TYPE_LABELS[tag.fieldType] || 'Text'}</Badge>
|
||||
<Badge variant='type' size='sm'>
|
||||
{FIELD_TYPE_LABELS[tag.fieldType] || 'Text'}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex items-center gap-[8px] pl-[8px]' onClick={(e) => e.stopPropagation()}>
|
||||
|
||||
@@ -223,7 +223,11 @@ function InputMappingField({
|
||||
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
|
||||
{fieldName}
|
||||
</span>
|
||||
{fieldType && <Badge size='sm'>{fieldType}</Badge>}
|
||||
{fieldType && (
|
||||
<Badge variant='type' size='sm'>
|
||||
{fieldType}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -238,7 +238,9 @@ export function KnowledgeTagFilters({
|
||||
{filter.collapsed ? filter.tagName || `Filter ${index + 1}` : `Filter ${index + 1}`}
|
||||
</span>
|
||||
{filter.collapsed && filter.tagName && (
|
||||
<Badge size='sm'>{FIELD_TYPE_LABELS[filter.fieldType] || 'Text'}</Badge>
|
||||
<Badge variant='type' size='sm'>
|
||||
{FIELD_TYPE_LABELS[filter.fieldType] || 'Text'}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex items-center gap-[8px] pl-[8px]' onClick={(e) => e.stopPropagation()}>
|
||||
|
||||
@@ -310,7 +310,11 @@ export function FieldFormat({
|
||||
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
|
||||
{field.name || `${title} ${index + 1}`}
|
||||
</span>
|
||||
{field.name && showType && <Badge size='sm'>{field.type}</Badge>}
|
||||
{field.name && showType && (
|
||||
<Badge variant='type' size='sm'>
|
||||
{field.type}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex items-center gap-[8px] pl-[8px]' onClick={(e) => e.stopPropagation()}>
|
||||
<Button variant='ghost' onClick={addField} disabled={isReadOnly} className='h-auto p-0'>
|
||||
|
||||
@@ -345,7 +345,11 @@ export function VariablesInput({
|
||||
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
|
||||
{assignment.variableName || `Variable ${index + 1}`}
|
||||
</span>
|
||||
{assignment.variableName && <Badge size='sm'>{assignment.type}</Badge>}
|
||||
{assignment.variableName && (
|
||||
<Badge variant='type' size='sm'>
|
||||
{assignment.type}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className='flex items-center gap-[8px] pl-[8px]'
|
||||
|
||||
@@ -796,6 +796,7 @@ export const Terminal = memo(function Terminal() {
|
||||
const terminalRef = useRef<HTMLElement>(null)
|
||||
const prevEntriesLengthRef = useRef(0)
|
||||
const prevWorkflowEntriesLengthRef = useRef(0)
|
||||
const hasInitializedEntriesRef = useRef(false)
|
||||
const isTerminalFocusedRef = useRef(false)
|
||||
const lastExpandedHeightRef = useRef<number>(DEFAULT_EXPANDED_HEIGHT)
|
||||
const setTerminalHeight = useTerminalStore((state) => state.setTerminalHeight)
|
||||
@@ -1007,12 +1008,33 @@ export const Terminal = memo(function Terminal() {
|
||||
return JSON.stringify(outputData, null, 2)
|
||||
}, [outputData])
|
||||
|
||||
/**
|
||||
* Reset entry tracking when switching workflows to ensure auto-open
|
||||
* works correctly for each workflow independently.
|
||||
*/
|
||||
useEffect(() => {
|
||||
hasInitializedEntriesRef.current = false
|
||||
}, [activeWorkflowId])
|
||||
|
||||
/**
|
||||
* Auto-open the terminal on new entries when "Open on run" is enabled.
|
||||
* This mirrors the header toggle behavior by using expandToLastHeight,
|
||||
* ensuring we always get the same smooth height transition.
|
||||
*
|
||||
* Skips the initial sync after console hydration to avoid auto-opening
|
||||
* when persisted entries are restored on page refresh.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!hasConsoleHydrated) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!hasInitializedEntriesRef.current) {
|
||||
hasInitializedEntriesRef.current = true
|
||||
prevWorkflowEntriesLengthRef.current = allWorkflowEntries.length
|
||||
return
|
||||
}
|
||||
|
||||
if (!openOnRun) {
|
||||
prevWorkflowEntriesLengthRef.current = allWorkflowEntries.length
|
||||
return
|
||||
@@ -1026,7 +1048,14 @@ export const Terminal = memo(function Terminal() {
|
||||
}
|
||||
|
||||
prevWorkflowEntriesLengthRef.current = currentLength
|
||||
}, [allWorkflowEntries.length, expandToLastHeight, openOnRun, isExpanded])
|
||||
}, [
|
||||
allWorkflowEntries.length,
|
||||
expandToLastHeight,
|
||||
openOnRun,
|
||||
isExpanded,
|
||||
hasConsoleHydrated,
|
||||
activeWorkflowId,
|
||||
])
|
||||
|
||||
/**
|
||||
* Handle row click - toggle if clicking same entry
|
||||
|
||||
@@ -66,7 +66,6 @@ import { useWorkspaceEnvironment } from '@/hooks/queries/environment'
|
||||
import { useAutoConnect, useSnapToGridSize } from '@/hooks/queries/general-settings'
|
||||
import { useCanvasViewport } from '@/hooks/use-canvas-viewport'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
import { usePermissionConfig } from '@/hooks/use-permission-config'
|
||||
import { useStreamCleanup } from '@/hooks/use-stream-cleanup'
|
||||
import { useCanvasModeStore } from '@/stores/canvas-mode'
|
||||
import { useChatStore } from '@/stores/chat/store'
|
||||
@@ -99,26 +98,6 @@ const logger = createLogger('Workflow')
|
||||
|
||||
const DEFAULT_PASTE_OFFSET = { x: 50, y: 50 }
|
||||
|
||||
/**
|
||||
* Gets the center of the current viewport in flow coordinates
|
||||
*/
|
||||
function getViewportCenter(
|
||||
screenToFlowPosition: (pos: { x: number; y: number }) => { x: number; y: number }
|
||||
): { x: number; y: number } {
|
||||
const flowContainer = document.querySelector('.react-flow')
|
||||
if (!flowContainer) {
|
||||
return screenToFlowPosition({
|
||||
x: window.innerWidth / 2,
|
||||
y: window.innerHeight / 2,
|
||||
})
|
||||
}
|
||||
const rect = flowContainer.getBoundingClientRect()
|
||||
return screenToFlowPosition({
|
||||
x: rect.width / 2,
|
||||
y: rect.height / 2,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the offset to paste blocks at viewport center
|
||||
*/
|
||||
@@ -126,7 +105,7 @@ function calculatePasteOffset(
|
||||
clipboard: {
|
||||
blocks: Record<string, { position: { x: number; y: number }; type: string; height?: number }>
|
||||
} | null,
|
||||
screenToFlowPosition: (pos: { x: number; y: number }) => { x: number; y: number }
|
||||
viewportCenter: { x: number; y: number }
|
||||
): { x: number; y: number } {
|
||||
if (!clipboard) return DEFAULT_PASTE_OFFSET
|
||||
|
||||
@@ -155,8 +134,6 @@ function calculatePasteOffset(
|
||||
)
|
||||
const clipboardCenter = { x: (minX + maxX) / 2, y: (minY + maxY) / 2 }
|
||||
|
||||
const viewportCenter = getViewportCenter(screenToFlowPosition)
|
||||
|
||||
return {
|
||||
x: viewportCenter.x - clipboardCenter.x,
|
||||
y: viewportCenter.y - clipboardCenter.y,
|
||||
@@ -266,7 +243,7 @@ const WorkflowContent = React.memo(() => {
|
||||
const router = useRouter()
|
||||
const reactFlowInstance = useReactFlow()
|
||||
const { screenToFlowPosition, getNodes, setNodes, getIntersectingNodes } = reactFlowInstance
|
||||
const { fitViewToBounds } = useCanvasViewport(reactFlowInstance)
|
||||
const { fitViewToBounds, getViewportCenter } = useCanvasViewport(reactFlowInstance)
|
||||
const { emitCursorUpdate } = useSocket()
|
||||
|
||||
const workspaceId = params.workspaceId as string
|
||||
@@ -338,8 +315,6 @@ const WorkflowContent = React.memo(() => {
|
||||
const isVariablesOpen = useVariablesStore((state) => state.isOpen)
|
||||
const isChatOpen = useChatStore((state) => state.isChatOpen)
|
||||
|
||||
// Permission config for invitation control
|
||||
const { isInvitationsDisabled } = usePermissionConfig()
|
||||
const snapGrid: [number, number] = useMemo(
|
||||
() => [snapToGridSize, snapToGridSize],
|
||||
[snapToGridSize]
|
||||
@@ -901,11 +876,125 @@ const WorkflowContent = React.memo(() => {
|
||||
* Consolidates shared logic for context paste, duplicate, and keyboard paste.
|
||||
*/
|
||||
const executePasteOperation = useCallback(
|
||||
(operation: 'paste' | 'duplicate', pasteOffset: { x: number; y: number }) => {
|
||||
const pasteData = preparePasteData(pasteOffset)
|
||||
(
|
||||
operation: 'paste' | 'duplicate',
|
||||
pasteOffset: { x: number; y: number },
|
||||
targetContainer?: {
|
||||
loopId: string
|
||||
loopPosition: { x: number; y: number }
|
||||
dimensions: { width: number; height: number }
|
||||
} | null,
|
||||
pasteTargetPosition?: { x: number; y: number }
|
||||
) => {
|
||||
// For context menu paste into a subflow, calculate offset to center blocks at click position
|
||||
// Skip click-position centering if blocks came from inside a subflow (relative coordinates)
|
||||
let effectiveOffset = pasteOffset
|
||||
if (targetContainer && pasteTargetPosition && clipboard) {
|
||||
const clipboardBlocks = Object.values(clipboard.blocks)
|
||||
// Only use click-position centering for top-level blocks (absolute coordinates)
|
||||
// Blocks with parentId have relative positions that can't be mixed with absolute click position
|
||||
const hasNestedBlocks = clipboardBlocks.some((b) => b.data?.parentId)
|
||||
if (clipboardBlocks.length > 0 && !hasNestedBlocks) {
|
||||
const minX = Math.min(...clipboardBlocks.map((b) => b.position.x))
|
||||
const maxX = Math.max(
|
||||
...clipboardBlocks.map((b) => b.position.x + BLOCK_DIMENSIONS.FIXED_WIDTH)
|
||||
)
|
||||
const minY = Math.min(...clipboardBlocks.map((b) => b.position.y))
|
||||
const maxY = Math.max(
|
||||
...clipboardBlocks.map((b) => b.position.y + BLOCK_DIMENSIONS.MIN_HEIGHT)
|
||||
)
|
||||
const clipboardCenter = { x: (minX + maxX) / 2, y: (minY + maxY) / 2 }
|
||||
effectiveOffset = {
|
||||
x: pasteTargetPosition.x - clipboardCenter.x,
|
||||
y: pasteTargetPosition.y - clipboardCenter.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pasteData = preparePasteData(effectiveOffset)
|
||||
if (!pasteData) return
|
||||
|
||||
const pastedBlocksArray = Object.values(pasteData.blocks)
|
||||
let pastedBlocksArray = Object.values(pasteData.blocks)
|
||||
|
||||
// If pasting into a subflow, adjust blocks to be children of that subflow
|
||||
if (targetContainer) {
|
||||
// Check if any pasted block is a trigger - triggers cannot be in subflows
|
||||
const hasTrigger = pastedBlocksArray.some((b) => TriggerUtils.isTriggerBlock(b))
|
||||
if (hasTrigger) {
|
||||
addNotification({
|
||||
level: 'error',
|
||||
message: 'Triggers cannot be placed inside loop or parallel subflows.',
|
||||
workflowId: activeWorkflowId || undefined,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if any pasted block is a subflow - subflows cannot be nested
|
||||
const hasSubflow = pastedBlocksArray.some((b) => b.type === 'loop' || b.type === 'parallel')
|
||||
if (hasSubflow) {
|
||||
addNotification({
|
||||
level: 'error',
|
||||
message: 'Subflows cannot be nested inside other subflows.',
|
||||
workflowId: activeWorkflowId || undefined,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Adjust each block's position to be relative to the container and set parentId
|
||||
pastedBlocksArray = pastedBlocksArray.map((block) => {
|
||||
// For blocks already nested (have parentId), positions are already relative - use as-is
|
||||
// For top-level blocks, convert absolute position to relative by subtracting container position
|
||||
const wasNested = Boolean(block.data?.parentId)
|
||||
const relativePosition = wasNested
|
||||
? { x: block.position.x, y: block.position.y }
|
||||
: {
|
||||
x: block.position.x - targetContainer.loopPosition.x,
|
||||
y: block.position.y - targetContainer.loopPosition.y,
|
||||
}
|
||||
|
||||
// Clamp position to keep block inside container (below header)
|
||||
const clampedPosition = {
|
||||
x: Math.max(
|
||||
CONTAINER_DIMENSIONS.LEFT_PADDING,
|
||||
Math.min(
|
||||
relativePosition.x,
|
||||
targetContainer.dimensions.width -
|
||||
BLOCK_DIMENSIONS.FIXED_WIDTH -
|
||||
CONTAINER_DIMENSIONS.RIGHT_PADDING
|
||||
)
|
||||
),
|
||||
y: Math.max(
|
||||
CONTAINER_DIMENSIONS.HEADER_HEIGHT + CONTAINER_DIMENSIONS.TOP_PADDING,
|
||||
Math.min(
|
||||
relativePosition.y,
|
||||
targetContainer.dimensions.height -
|
||||
BLOCK_DIMENSIONS.MIN_HEIGHT -
|
||||
CONTAINER_DIMENSIONS.BOTTOM_PADDING
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
return {
|
||||
...block,
|
||||
position: clampedPosition,
|
||||
data: {
|
||||
...block.data,
|
||||
parentId: targetContainer.loopId,
|
||||
extent: 'parent',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// Update pasteData.blocks with the modified blocks
|
||||
pasteData.blocks = pastedBlocksArray.reduce(
|
||||
(acc, block) => {
|
||||
acc[block.id] = block
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, (typeof pastedBlocksArray)[0]>
|
||||
)
|
||||
}
|
||||
|
||||
const validation = validateTriggerPaste(pastedBlocksArray, blocks, operation)
|
||||
if (!validation.isValid) {
|
||||
addNotification({
|
||||
@@ -926,21 +1015,46 @@ const WorkflowContent = React.memo(() => {
|
||||
pasteData.parallels,
|
||||
pasteData.subBlockValues
|
||||
)
|
||||
|
||||
// Resize container if we pasted into a subflow
|
||||
if (targetContainer) {
|
||||
resizeLoopNodesWrapper()
|
||||
}
|
||||
},
|
||||
[
|
||||
preparePasteData,
|
||||
blocks,
|
||||
clipboard,
|
||||
addNotification,
|
||||
activeWorkflowId,
|
||||
collaborativeBatchAddBlocks,
|
||||
setPendingSelection,
|
||||
resizeLoopNodesWrapper,
|
||||
]
|
||||
)
|
||||
|
||||
const handleContextPaste = useCallback(() => {
|
||||
if (!hasClipboard()) return
|
||||
executePasteOperation('paste', calculatePasteOffset(clipboard, screenToFlowPosition))
|
||||
}, [hasClipboard, executePasteOperation, clipboard, screenToFlowPosition])
|
||||
|
||||
// Convert context menu position to flow coordinates and check if inside a subflow
|
||||
const flowPosition = screenToFlowPosition(contextMenuPosition)
|
||||
const targetContainer = isPointInLoopNode(flowPosition)
|
||||
|
||||
executePasteOperation(
|
||||
'paste',
|
||||
calculatePasteOffset(clipboard, getViewportCenter()),
|
||||
targetContainer,
|
||||
flowPosition // Pass the click position so blocks are centered at where user right-clicked
|
||||
)
|
||||
}, [
|
||||
hasClipboard,
|
||||
executePasteOperation,
|
||||
clipboard,
|
||||
getViewportCenter,
|
||||
screenToFlowPosition,
|
||||
contextMenuPosition,
|
||||
isPointInLoopNode,
|
||||
])
|
||||
|
||||
const handleContextDuplicate = useCallback(() => {
|
||||
copyBlocks(contextMenuBlocks.map((b) => b.id))
|
||||
@@ -1006,10 +1120,6 @@ const WorkflowContent = React.memo(() => {
|
||||
setIsChatOpen(!isChatOpen)
|
||||
}, [])
|
||||
|
||||
const handleContextInvite = useCallback(() => {
|
||||
window.dispatchEvent(new CustomEvent('open-invite-modal'))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let cleanup: (() => void) | null = null
|
||||
|
||||
@@ -1054,7 +1164,7 @@ const WorkflowContent = React.memo(() => {
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.key === 'v') {
|
||||
if (effectivePermissions.canEdit && hasClipboard()) {
|
||||
event.preventDefault()
|
||||
executePasteOperation('paste', calculatePasteOffset(clipboard, screenToFlowPosition))
|
||||
executePasteOperation('paste', calculatePasteOffset(clipboard, getViewportCenter()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1074,7 +1184,7 @@ const WorkflowContent = React.memo(() => {
|
||||
hasClipboard,
|
||||
effectivePermissions.canEdit,
|
||||
clipboard,
|
||||
screenToFlowPosition,
|
||||
getViewportCenter,
|
||||
executePasteOperation,
|
||||
])
|
||||
|
||||
@@ -1507,7 +1617,7 @@ const WorkflowContent = React.memo(() => {
|
||||
if (!type) return
|
||||
if (type === 'connectionBlock') return
|
||||
|
||||
const basePosition = getViewportCenter(screenToFlowPosition)
|
||||
const basePosition = getViewportCenter()
|
||||
|
||||
if (type === 'loop' || type === 'parallel') {
|
||||
const id = crypto.randomUUID()
|
||||
@@ -1576,7 +1686,7 @@ const WorkflowContent = React.memo(() => {
|
||||
)
|
||||
}
|
||||
}, [
|
||||
screenToFlowPosition,
|
||||
getViewportCenter,
|
||||
blocks,
|
||||
addBlock,
|
||||
effectivePermissions.canEdit,
|
||||
|
||||
@@ -611,7 +611,9 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
|
||||
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
|
||||
{name}
|
||||
</span>
|
||||
<Badge size='sm'>{prop.type || 'any'}</Badge>
|
||||
<Badge variant='type' size='sm'>
|
||||
{prop.type || 'any'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className='border-[var(--border-1)] border-t px-[10px] pt-[6px] pb-[10px]'>
|
||||
|
||||
@@ -16,6 +16,7 @@ const badgeVariants = cva(
|
||||
'gap-[4px] rounded-[40px] border border-[var(--border)] text-[var(--text-secondary)] bg-[var(--surface-4)] hover:text-[var(--text-primary)] hover:border-[var(--border-1)] hover:bg-[var(--surface-6)] dark:hover:bg-[var(--surface-5)]',
|
||||
outline:
|
||||
'gap-[4px] rounded-[40px] border border-[var(--border-1)] bg-transparent text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--surface-5)] dark:hover:bg-transparent dark:hover:border-[var(--surface-6)]',
|
||||
type: 'gap-[4px] rounded-[40px] border border-[var(--border)] text-[var(--text-secondary)] bg-[var(--surface-4)] dark:bg-[var(--surface-6)]',
|
||||
green: `${STATUS_BASE} bg-[#bbf7d0] text-[#15803d] dark:bg-[rgba(34,197,94,0.2)] dark:text-[#86efac]`,
|
||||
red: `${STATUS_BASE} bg-[#fecaca] text-[var(--text-error)] dark:bg-[#551a1a] dark:text-[var(--text-error)]`,
|
||||
gray: `${STATUS_BASE} bg-[#e7e5e4] text-[#57534e] dark:bg-[var(--terminal-status-info-bg)] dark:text-[var(--terminal-status-info-color)]`,
|
||||
@@ -84,7 +85,7 @@ export interface BadgeProps
|
||||
*
|
||||
* @remarks
|
||||
* Supports two categories of variants:
|
||||
* - **Bordered**: `default`, `outline` - traditional badges with borders
|
||||
* - **Bordered**: `default`, `outline`, `type` - traditional badges with borders
|
||||
* - **Status colors**: `green`, `red`, `gray`, `blue`, `blue-secondary`, `purple`,
|
||||
* `orange`, `amber`, `teal`, `cyan`, `gray-secondary` - borderless colored badges
|
||||
*
|
||||
|
||||
@@ -57,31 +57,16 @@ function getVisibleCanvasBounds(): VisibleBounds {
|
||||
* Gets the center of the visible canvas in screen coordinates.
|
||||
*/
|
||||
function getVisibleCanvasCenter(): { x: number; y: number } {
|
||||
const style = getComputedStyle(document.documentElement)
|
||||
const sidebarWidth = Number.parseInt(style.getPropertyValue('--sidebar-width') || '0', 10)
|
||||
const panelWidth = Number.parseInt(style.getPropertyValue('--panel-width') || '0', 10)
|
||||
const terminalHeight = Number.parseInt(style.getPropertyValue('--terminal-height') || '0', 10)
|
||||
const bounds = getVisibleCanvasBounds()
|
||||
|
||||
const flowContainer = document.querySelector('.react-flow')
|
||||
if (!flowContainer) {
|
||||
const visibleWidth = window.innerWidth - sidebarWidth - panelWidth
|
||||
const visibleHeight = window.innerHeight - terminalHeight
|
||||
return {
|
||||
x: sidebarWidth + visibleWidth / 2,
|
||||
y: visibleHeight / 2,
|
||||
}
|
||||
}
|
||||
|
||||
const rect = flowContainer.getBoundingClientRect()
|
||||
|
||||
// Calculate actual visible area in screen coordinates
|
||||
const visibleLeft = Math.max(rect.left, sidebarWidth)
|
||||
const visibleRight = Math.min(rect.right, window.innerWidth - panelWidth)
|
||||
const visibleBottom = Math.min(rect.bottom, window.innerHeight - terminalHeight)
|
||||
const rect = flowContainer?.getBoundingClientRect()
|
||||
const containerLeft = rect?.left ?? 0
|
||||
const containerTop = rect?.top ?? 0
|
||||
|
||||
return {
|
||||
x: (visibleLeft + visibleRight) / 2,
|
||||
y: (rect.top + visibleBottom) / 2,
|
||||
x: containerLeft + bounds.offsetLeft + bounds.width / 2,
|
||||
y: containerTop + bounds.height / 2,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
loadDeployedWorkflowState,
|
||||
loadWorkflowFromNormalizedTables,
|
||||
} from '@/lib/workflows/persistence/utils'
|
||||
import { mergeSubblockStateWithValues } from '@/lib/workflows/subblocks'
|
||||
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
|
||||
import { updateWorkflowRunCounts } from '@/lib/workflows/utils'
|
||||
import { Executor } from '@/executor'
|
||||
@@ -26,7 +27,6 @@ import type {
|
||||
import type { ExecutionResult, NormalizedBlockOutput } from '@/executor/types'
|
||||
import { hasExecutionResult } from '@/executor/utils/errors'
|
||||
import { Serializer } from '@/serializer'
|
||||
import { mergeSubblockState } from '@/stores/workflows/server-utils'
|
||||
|
||||
const logger = createLogger('ExecutionCore')
|
||||
|
||||
@@ -172,8 +172,7 @@ export async function executeWorkflowCore(
|
||||
logger.info(`[${requestId}] Using deployed workflow state (deployed execution)`)
|
||||
}
|
||||
|
||||
// Merge block states
|
||||
const mergedStates = mergeSubblockState(blocks)
|
||||
const mergedStates = mergeSubblockStateWithValues(blocks)
|
||||
|
||||
const personalEnvUserId =
|
||||
metadata.isClientSession && metadata.sessionUserId
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
/**
|
||||
* Server-Safe Workflow Utilities
|
||||
*
|
||||
* This file contains workflow utility functions that can be safely imported
|
||||
* by server-side API routes without causing client/server boundary violations.
|
||||
*
|
||||
* Unlike the main utils.ts file, this does NOT import any client-side stores
|
||||
* or React hooks, making it safe for use in Next.js API routes.
|
||||
*/
|
||||
|
||||
import { mergeSubblockStateWithValues } from '@/lib/workflows/subblocks'
|
||||
import type { BlockState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
/**
|
||||
* Server-safe version of mergeSubblockState for API routes
|
||||
*
|
||||
* Merges workflow block states with provided subblock values while maintaining block structure.
|
||||
* This version takes explicit subblock values instead of reading from client stores.
|
||||
*
|
||||
* @param blocks - Block configurations from workflow state
|
||||
* @param subBlockValues - Object containing subblock values keyed by blockId -> subBlockId -> value
|
||||
* @param blockId - Optional specific block ID to merge (merges all if not provided)
|
||||
* @returns Merged block states with updated values
|
||||
*/
|
||||
export function mergeSubblockState(
|
||||
blocks: Record<string, BlockState>,
|
||||
subBlockValues: Record<string, Record<string, any>> = {},
|
||||
blockId?: string
|
||||
): Record<string, BlockState> {
|
||||
return mergeSubblockStateWithValues(blocks, subBlockValues, blockId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Server-safe async version of mergeSubblockState for API routes
|
||||
*
|
||||
* Asynchronously merges workflow block states with provided subblock values.
|
||||
* This version takes explicit subblock values instead of reading from client stores.
|
||||
*
|
||||
* @param blocks - Block configurations from workflow state
|
||||
* @param subBlockValues - Object containing subblock values keyed by blockId -> subBlockId -> value
|
||||
* @param blockId - Optional specific block ID to merge (merges all if not provided)
|
||||
* @returns Promise resolving to merged block states with updated values
|
||||
*/
|
||||
export async function mergeSubblockStateAsync(
|
||||
blocks: Record<string, BlockState>,
|
||||
subBlockValues: Record<string, Record<string, any>> = {},
|
||||
blockId?: string
|
||||
): Promise<Record<string, BlockState>> {
|
||||
// Since we're not reading from client stores, we can just return the sync version
|
||||
// The async nature was only needed for the client-side store operations
|
||||
return mergeSubblockState(blocks, subBlockValues, blockId)
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '@sim/testing'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { normalizeName } from '@/executor/constants'
|
||||
import { getUniqueBlockName } from './utils'
|
||||
import { getUniqueBlockName, regenerateBlockIds } from './utils'
|
||||
|
||||
describe('normalizeName', () => {
|
||||
it.concurrent('should convert to lowercase', () => {
|
||||
@@ -223,3 +223,213 @@ describe('getUniqueBlockName', () => {
|
||||
expect(getUniqueBlockName('myblock', existingBlocks)).toBe('myblock 2')
|
||||
})
|
||||
})
|
||||
|
||||
describe('regenerateBlockIds', () => {
|
||||
const positionOffset = { x: 50, y: 50 }
|
||||
|
||||
it('should preserve parentId and use same offset when duplicating a block inside an existing subflow', () => {
|
||||
const loopId = 'loop-1'
|
||||
const childId = 'child-1'
|
||||
|
||||
const existingBlocks = {
|
||||
[loopId]: createLoopBlock({ id: loopId, name: 'Loop 1' }),
|
||||
}
|
||||
|
||||
const blocksToCopy = {
|
||||
[childId]: createAgentBlock({
|
||||
id: childId,
|
||||
name: 'Agent 1',
|
||||
position: { x: 100, y: 50 },
|
||||
data: { parentId: loopId, extent: 'parent' },
|
||||
}),
|
||||
}
|
||||
|
||||
const result = regenerateBlockIds(
|
||||
blocksToCopy,
|
||||
[],
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
positionOffset, // { x: 50, y: 50 } - small offset, used as-is
|
||||
existingBlocks,
|
||||
getUniqueBlockName
|
||||
)
|
||||
|
||||
const newBlocks = Object.values(result.blocks)
|
||||
expect(newBlocks).toHaveLength(1)
|
||||
|
||||
const duplicatedBlock = newBlocks[0]
|
||||
expect(duplicatedBlock.data?.parentId).toBe(loopId)
|
||||
expect(duplicatedBlock.data?.extent).toBe('parent')
|
||||
expect(duplicatedBlock.position).toEqual({ x: 150, y: 100 })
|
||||
})
|
||||
|
||||
it('should clear parentId when parent does not exist in paste set or existing blocks', () => {
|
||||
const nonExistentParentId = 'non-existent-loop'
|
||||
const childId = 'child-1'
|
||||
|
||||
const blocksToCopy = {
|
||||
[childId]: createAgentBlock({
|
||||
id: childId,
|
||||
name: 'Agent 1',
|
||||
position: { x: 100, y: 50 },
|
||||
data: { parentId: nonExistentParentId, extent: 'parent' },
|
||||
}),
|
||||
}
|
||||
|
||||
const result = regenerateBlockIds(
|
||||
blocksToCopy,
|
||||
[],
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
positionOffset,
|
||||
{},
|
||||
getUniqueBlockName
|
||||
)
|
||||
|
||||
const newBlocks = Object.values(result.blocks)
|
||||
expect(newBlocks).toHaveLength(1)
|
||||
|
||||
const duplicatedBlock = newBlocks[0]
|
||||
expect(duplicatedBlock.data?.parentId).toBeUndefined()
|
||||
expect(duplicatedBlock.data?.extent).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should remap parentId when copying both parent and child together', () => {
|
||||
const loopId = 'loop-1'
|
||||
const childId = 'child-1'
|
||||
|
||||
const blocksToCopy = {
|
||||
[loopId]: createLoopBlock({
|
||||
id: loopId,
|
||||
name: 'Loop 1',
|
||||
position: { x: 200, y: 200 },
|
||||
}),
|
||||
[childId]: createAgentBlock({
|
||||
id: childId,
|
||||
name: 'Agent 1',
|
||||
position: { x: 100, y: 50 },
|
||||
data: { parentId: loopId, extent: 'parent' },
|
||||
}),
|
||||
}
|
||||
|
||||
const result = regenerateBlockIds(
|
||||
blocksToCopy,
|
||||
[],
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
positionOffset,
|
||||
{},
|
||||
getUniqueBlockName
|
||||
)
|
||||
|
||||
const newBlocks = Object.values(result.blocks)
|
||||
expect(newBlocks).toHaveLength(2)
|
||||
|
||||
const newLoop = newBlocks.find((b) => b.type === 'loop')
|
||||
const newChild = newBlocks.find((b) => b.type === 'agent')
|
||||
|
||||
expect(newLoop).toBeDefined()
|
||||
expect(newChild).toBeDefined()
|
||||
expect(newChild!.data?.parentId).toBe(newLoop!.id)
|
||||
expect(newChild!.data?.extent).toBe('parent')
|
||||
|
||||
expect(newLoop!.position).toEqual({ x: 250, y: 250 })
|
||||
expect(newChild!.position).toEqual({ x: 100, y: 50 })
|
||||
})
|
||||
|
||||
it('should apply offset to top-level blocks', () => {
|
||||
const blockId = 'block-1'
|
||||
|
||||
const blocksToCopy = {
|
||||
[blockId]: createAgentBlock({
|
||||
id: blockId,
|
||||
name: 'Agent 1',
|
||||
position: { x: 100, y: 100 },
|
||||
}),
|
||||
}
|
||||
|
||||
const result = regenerateBlockIds(
|
||||
blocksToCopy,
|
||||
[],
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
positionOffset,
|
||||
{},
|
||||
getUniqueBlockName
|
||||
)
|
||||
|
||||
const newBlocks = Object.values(result.blocks)
|
||||
expect(newBlocks).toHaveLength(1)
|
||||
expect(newBlocks[0].position).toEqual({ x: 150, y: 150 })
|
||||
})
|
||||
|
||||
it('should generate unique names for duplicated blocks', () => {
|
||||
const blockId = 'block-1'
|
||||
|
||||
const existingBlocks = {
|
||||
existing: createAgentBlock({ id: 'existing', name: 'Agent 1' }),
|
||||
}
|
||||
|
||||
const blocksToCopy = {
|
||||
[blockId]: createAgentBlock({
|
||||
id: blockId,
|
||||
name: 'Agent 1',
|
||||
position: { x: 100, y: 100 },
|
||||
}),
|
||||
}
|
||||
|
||||
const result = regenerateBlockIds(
|
||||
blocksToCopy,
|
||||
[],
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
positionOffset,
|
||||
existingBlocks,
|
||||
getUniqueBlockName
|
||||
)
|
||||
|
||||
const newBlocks = Object.values(result.blocks)
|
||||
expect(newBlocks).toHaveLength(1)
|
||||
expect(newBlocks[0].name).toBe('Agent 2')
|
||||
})
|
||||
|
||||
it('should ignore large viewport offset for blocks inside existing subflows', () => {
|
||||
const loopId = 'loop-1'
|
||||
const childId = 'child-1'
|
||||
|
||||
const existingBlocks = {
|
||||
[loopId]: createLoopBlock({ id: loopId, name: 'Loop 1' }),
|
||||
}
|
||||
|
||||
const blocksToCopy = {
|
||||
[childId]: createAgentBlock({
|
||||
id: childId,
|
||||
name: 'Agent 1',
|
||||
position: { x: 100, y: 50 },
|
||||
data: { parentId: loopId, extent: 'parent' },
|
||||
}),
|
||||
}
|
||||
|
||||
const largeViewportOffset = { x: 2000, y: 1500 }
|
||||
|
||||
const result = regenerateBlockIds(
|
||||
blocksToCopy,
|
||||
[],
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
largeViewportOffset,
|
||||
existingBlocks,
|
||||
getUniqueBlockName
|
||||
)
|
||||
|
||||
const duplicatedBlock = Object.values(result.blocks)[0]
|
||||
expect(duplicatedBlock.position).toEqual({ x: 280, y: 70 })
|
||||
expect(duplicatedBlock.data?.parentId).toBe(loopId)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { Edge } from 'reactflow'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { DEFAULT_DUPLICATE_OFFSET } from '@/lib/workflows/autolayout/constants'
|
||||
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
|
||||
import { mergeSubBlockValues, mergeSubblockStateWithValues } from '@/lib/workflows/subblocks'
|
||||
import { mergeSubblockStateWithValues } from '@/lib/workflows/subblocks'
|
||||
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
|
||||
import { getBlock } from '@/blocks'
|
||||
import { isAnnotationOnlyBlock, normalizeName } from '@/executor/constants'
|
||||
@@ -16,7 +17,8 @@ import type {
|
||||
} from '@/stores/workflows/workflow/types'
|
||||
import { TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/constants'
|
||||
|
||||
const WEBHOOK_SUBBLOCK_FIELDS = ['webhookId', 'triggerPath']
|
||||
/** Threshold to detect viewport-based offsets vs small duplicate offsets */
|
||||
const LARGE_OFFSET_THRESHOLD = 300
|
||||
|
||||
/**
|
||||
* Checks if an edge is valid (source and target exist, not annotation-only, target is not a trigger)
|
||||
@@ -204,64 +206,6 @@ export function prepareBlockState(options: PrepareBlockStateOptions): BlockState
|
||||
}
|
||||
}
|
||||
|
||||
export interface PrepareDuplicateBlockStateOptions {
|
||||
sourceBlock: BlockState
|
||||
newId: string
|
||||
newName: string
|
||||
positionOffset: { x: number; y: number }
|
||||
subBlockValues: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a BlockState for duplicating an existing block.
|
||||
* Copies block structure and subblock values, excluding webhook fields.
|
||||
*/
|
||||
export function prepareDuplicateBlockState(options: PrepareDuplicateBlockStateOptions): {
|
||||
block: BlockState
|
||||
subBlockValues: Record<string, unknown>
|
||||
} {
|
||||
const { sourceBlock, newId, newName, positionOffset, subBlockValues } = options
|
||||
|
||||
const filteredSubBlockValues = Object.fromEntries(
|
||||
Object.entries(subBlockValues).filter(([key]) => !WEBHOOK_SUBBLOCK_FIELDS.includes(key))
|
||||
)
|
||||
|
||||
const baseSubBlocks: Record<string, SubBlockState> = sourceBlock.subBlocks
|
||||
? JSON.parse(JSON.stringify(sourceBlock.subBlocks))
|
||||
: {}
|
||||
|
||||
WEBHOOK_SUBBLOCK_FIELDS.forEach((field) => {
|
||||
if (field in baseSubBlocks) {
|
||||
delete baseSubBlocks[field]
|
||||
}
|
||||
})
|
||||
|
||||
const mergedSubBlocks = mergeSubBlockValues(baseSubBlocks, filteredSubBlockValues) as Record<
|
||||
string,
|
||||
SubBlockState
|
||||
>
|
||||
|
||||
const block: BlockState = {
|
||||
id: newId,
|
||||
type: sourceBlock.type,
|
||||
name: newName,
|
||||
position: {
|
||||
x: sourceBlock.position.x + positionOffset.x,
|
||||
y: sourceBlock.position.y + positionOffset.y,
|
||||
},
|
||||
data: sourceBlock.data ? JSON.parse(JSON.stringify(sourceBlock.data)) : {},
|
||||
subBlocks: mergedSubBlocks,
|
||||
outputs: sourceBlock.outputs ? JSON.parse(JSON.stringify(sourceBlock.outputs)) : {},
|
||||
enabled: sourceBlock.enabled ?? true,
|
||||
horizontalHandles: sourceBlock.horizontalHandles ?? true,
|
||||
advancedMode: sourceBlock.advancedMode ?? false,
|
||||
triggerMode: sourceBlock.triggerMode ?? false,
|
||||
height: sourceBlock.height || 0,
|
||||
}
|
||||
|
||||
return { block, subBlockValues: filteredSubBlockValues }
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges workflow block states with subblock values while maintaining block structure
|
||||
* @param blocks - Block configurations from workflow store
|
||||
@@ -348,78 +292,6 @@ export function mergeSubblockState(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously merges workflow block states with subblock values
|
||||
* Ensures all values are properly resolved before returning
|
||||
*
|
||||
* @param blocks - Block configurations from workflow store
|
||||
* @param workflowId - ID of the workflow to merge values for
|
||||
* @param blockId - Optional specific block ID to merge (merges all if not provided)
|
||||
* @returns Promise resolving to merged block states with updated values
|
||||
*/
|
||||
export async function mergeSubblockStateAsync(
|
||||
blocks: Record<string, BlockState>,
|
||||
workflowId?: string,
|
||||
blockId?: string
|
||||
): Promise<Record<string, BlockState>> {
|
||||
const subBlockStore = useSubBlockStore.getState()
|
||||
|
||||
if (workflowId) {
|
||||
const workflowValues = subBlockStore.workflowValues[workflowId] || {}
|
||||
return mergeSubblockStateWithValues(blocks, workflowValues, blockId)
|
||||
}
|
||||
|
||||
const blocksToProcess = blockId ? { [blockId]: blocks[blockId] } : blocks
|
||||
|
||||
// Process blocks in parallel for better performance
|
||||
const processedBlockEntries = await Promise.all(
|
||||
Object.entries(blocksToProcess).map(async ([id, block]) => {
|
||||
// Skip if block is undefined or doesn't have subBlocks
|
||||
if (!block || !block.subBlocks) {
|
||||
return [id, block] as const
|
||||
}
|
||||
|
||||
// Process all subblocks in parallel
|
||||
const subBlockEntries = await Promise.all(
|
||||
Object.entries(block.subBlocks).map(async ([subBlockId, subBlock]) => {
|
||||
// Skip if subBlock is undefined
|
||||
if (!subBlock) {
|
||||
return null
|
||||
}
|
||||
|
||||
const storedValue = subBlockStore.getValue(id, subBlockId)
|
||||
|
||||
return [
|
||||
subBlockId,
|
||||
{
|
||||
...subBlock,
|
||||
value: (storedValue !== undefined && storedValue !== null
|
||||
? storedValue
|
||||
: subBlock.value) as SubBlockState['value'],
|
||||
},
|
||||
] as const
|
||||
})
|
||||
)
|
||||
|
||||
// Convert entries back to an object
|
||||
const mergedSubBlocks = Object.fromEntries(
|
||||
subBlockEntries.filter((entry): entry is readonly [string, SubBlockState] => entry !== null)
|
||||
) as Record<string, SubBlockState>
|
||||
|
||||
// Return the full block state with updated subBlocks (including orphaned values)
|
||||
return [
|
||||
id,
|
||||
{
|
||||
...block,
|
||||
subBlocks: mergedSubBlocks,
|
||||
},
|
||||
] as const
|
||||
})
|
||||
)
|
||||
|
||||
return Object.fromEntries(processedBlockEntries) as Record<string, BlockState>
|
||||
}
|
||||
|
||||
function updateValueReferences(value: unknown, nameMap: Map<string, string>): unknown {
|
||||
if (typeof value === 'string') {
|
||||
let updatedValue = value
|
||||
@@ -444,14 +316,10 @@ function updateValueReferences(value: unknown, nameMap: Map<string, string>): un
|
||||
|
||||
function updateBlockReferences(
|
||||
blocks: Record<string, BlockState>,
|
||||
idMap: Map<string, string>,
|
||||
nameMap: Map<string, string>,
|
||||
clearTriggerRuntimeValues = false
|
||||
): void {
|
||||
Object.entries(blocks).forEach(([_, block]) => {
|
||||
// NOTE: parentId remapping is handled in regenerateBlockIds' second pass.
|
||||
// Do NOT remap parentId here as it would incorrectly clear already-mapped IDs.
|
||||
|
||||
if (block.subBlocks) {
|
||||
Object.entries(block.subBlocks).forEach(([subBlockId, subBlock]) => {
|
||||
if (clearTriggerRuntimeValues && TRIGGER_RUNTIME_SUBBLOCK_IDS.includes(subBlockId)) {
|
||||
@@ -533,7 +401,7 @@ export function regenerateWorkflowIds(
|
||||
})
|
||||
}
|
||||
|
||||
updateBlockReferences(newBlocks, blockIdMap, nameMap, clearTriggerRuntimeValues)
|
||||
updateBlockReferences(newBlocks, nameMap, clearTriggerRuntimeValues)
|
||||
|
||||
return {
|
||||
blocks: newBlocks,
|
||||
@@ -574,13 +442,36 @@ export function regenerateBlockIds(
|
||||
const newNormalizedName = normalizeName(newName)
|
||||
nameMap.set(oldNormalizedName, newNormalizedName)
|
||||
|
||||
// Check if this block has a parent that's also being copied
|
||||
// If so, it's a nested block and should keep its relative position (no offset)
|
||||
// Only top-level blocks (no parent in the paste set) get the position offset
|
||||
// Determine position offset based on parent relationship:
|
||||
// 1. Parent also being copied: keep exact relative position (parent itself will be offset)
|
||||
// 2. Parent exists in existing workflow: use provided offset, but cap large viewport-based
|
||||
// offsets since they don't make sense for relative positions
|
||||
// 3. Top-level block (no parent): apply full paste offset
|
||||
const hasParentInPasteSet = block.data?.parentId && blocks[block.data.parentId]
|
||||
const newPosition = hasParentInPasteSet
|
||||
? { x: block.position.x, y: block.position.y } // Keep relative position
|
||||
: { x: block.position.x + positionOffset.x, y: block.position.y + positionOffset.y }
|
||||
const hasParentInExistingWorkflow =
|
||||
block.data?.parentId && existingBlockNames[block.data.parentId]
|
||||
|
||||
let newPosition: Position
|
||||
if (hasParentInPasteSet) {
|
||||
// Parent also being copied - keep exact relative position
|
||||
newPosition = { x: block.position.x, y: block.position.y }
|
||||
} else if (hasParentInExistingWorkflow) {
|
||||
// Block stays in existing subflow - use provided offset unless it's viewport-based (large)
|
||||
const isLargeOffset =
|
||||
Math.abs(positionOffset.x) > LARGE_OFFSET_THRESHOLD ||
|
||||
Math.abs(positionOffset.y) > LARGE_OFFSET_THRESHOLD
|
||||
const effectiveOffset = isLargeOffset ? DEFAULT_DUPLICATE_OFFSET : positionOffset
|
||||
newPosition = {
|
||||
x: block.position.x + effectiveOffset.x,
|
||||
y: block.position.y + effectiveOffset.y,
|
||||
}
|
||||
} else {
|
||||
// Top-level block - apply full paste offset
|
||||
newPosition = {
|
||||
x: block.position.x + positionOffset.x,
|
||||
y: block.position.y + positionOffset.y,
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder block - we'll update parentId in second pass
|
||||
const newBlock: BlockState = {
|
||||
@@ -602,19 +493,30 @@ export function regenerateBlockIds(
|
||||
})
|
||||
|
||||
// Second pass: update parentId references for nested blocks
|
||||
// If a block's parent is also being pasted, map to new parentId; otherwise clear it
|
||||
// If a block's parent is also being pasted, map to new parentId
|
||||
// If parent exists in existing workflow, keep the original parentId (block stays in same subflow)
|
||||
// Otherwise clear the parentId
|
||||
Object.entries(newBlocks).forEach(([, block]) => {
|
||||
if (block.data?.parentId) {
|
||||
const oldParentId = block.data.parentId
|
||||
const newParentId = blockIdMap.get(oldParentId)
|
||||
|
||||
if (newParentId) {
|
||||
// Parent is being pasted - map to new parent ID
|
||||
block.data = {
|
||||
...block.data,
|
||||
parentId: newParentId,
|
||||
extent: 'parent',
|
||||
}
|
||||
} else if (existingBlockNames[oldParentId]) {
|
||||
// Parent exists in existing workflow - keep original parentId (block stays in same subflow)
|
||||
block.data = {
|
||||
...block.data,
|
||||
parentId: oldParentId,
|
||||
extent: 'parent',
|
||||
}
|
||||
} else {
|
||||
// Parent doesn't exist anywhere - clear the relationship
|
||||
block.data = { ...block.data, parentId: undefined, extent: undefined }
|
||||
}
|
||||
}
|
||||
@@ -647,7 +549,7 @@ export function regenerateBlockIds(
|
||||
}
|
||||
})
|
||||
|
||||
updateBlockReferences(newBlocks, blockIdMap, nameMap, false)
|
||||
updateBlockReferences(newBlocks, nameMap, false)
|
||||
|
||||
Object.entries(newSubBlockValues).forEach(([_, blockValues]) => {
|
||||
Object.keys(blockValues).forEach((subBlockId) => {
|
||||
|
||||
Reference in New Issue
Block a user